1
0
mirror of https://github.com/sasjs/adapter.git synced 2025-12-13 02:04:36 +00:00

Compare commits

..

111 Commits

Author SHA1 Message Date
Krishna Acondy
e068d3263c Merge pull request #419 from sasjs/issue-327
fix(*): SASWORK is not being parsed correctly
2021-06-15 08:41:58 +01:00
630f2e9c37 fix: test regarding Request with extra attributes on JES approach fixed 2021-06-15 11:29:21 +05:00
51ac6b052b fix: test case which check extra attributes on JES approach fixed 2021-06-14 23:21:17 +05:00
c32258eb3c fix: code modified in appendRequest method fixes #327 2021-06-14 23:18:26 +05:00
Allan Bowe
88f50e3c74 Update README.md 2021-06-14 21:11:18 +03:00
Krishna Acondy
bfe5ac0ff7 Merge pull request #417 from sasjs/force-sas9-webout
fix(sas9): force webout output when executing arbitrary code on SAS9
2021-06-14 09:17:32 +01:00
Krishna Acondy
d50f5a030a chore(lint): fix formatting 2021-06-14 09:12:11 +01:00
Krishna Acondy
c320caec99 fix(sas9): force webout output when executing arbitrary code on SAS9 2021-06-14 09:10:26 +01:00
Allan Bowe
16a5b2b012 Merge pull request #414 from sasjs/issue-276
fix: Issue 276
2021-06-13 21:20:18 +03:00
Allan Bowe
2951e0cc2d Merge branch 'master' into issue-276 2021-06-13 21:04:56 +03:00
Allan Bowe
6bb4a7ea18 Update SASjs.ts
fix grammar
2021-06-13 21:01:15 +03:00
Allan Bowe
2827978fe5 Merge pull request #390 from sasjs/service-pack-with-file-resource
feat: create file resource while deploying service pack for viya
2021-06-13 14:52:09 +03:00
Saad Jutt
541c19c1a4 chore(merge): Merge branch 'service-pack-with-file-resource' of github.com:sasjs/adapter into service-pack-with-file-resource 2021-06-13 16:26:27 +05:00
Saad Jutt
c5e995f8d6 chore: TSDoc comments updated 2021-06-13 16:25:04 +05:00
Allan Bowe
8bf36da566 Merge branch 'master' into service-pack-with-file-resource 2021-06-13 11:56:54 +03:00
ccb4ec6e03 chore: code refactored for better readability 2021-06-11 22:53:06 +05:00
06ebb52bc9 chore(merge): merge master into issue-276 2021-06-10 22:12:36 +05:00
Yury Shkoda
6e23a0362f Merge pull request #411 from sasjs/issue-408
feat: select extra attributes in JES response
2021-06-10 19:38:16 +03:00
a59d78bcf7 chore(git): Merge branch 'master' into issue-408 2021-06-10 15:06:10 +02:00
33d4ee92a7 chore: updated utils and comment 2021-06-10 15:03:51 +02:00
dadce3d4c9 chore: added extra attributes type from @sasjs/utils 2021-06-10 14:22:31 +02:00
Saad Jutt
b61cf34723 chore(merge): Merge branch 'master' into service-pack-with-file-resource 2021-06-10 16:55:35 +05:00
Saad Jutt
22445d1268 fix: uploading file Buffer with FormData 2021-06-10 16:49:20 +05:00
Allan Bowe
cba9dacb37 Merge branch 'master' into issue-276 2021-06-10 14:03:14 +03:00
Yury Shkoda
a055b36c5c Merge pull request #389 from sasjs/issue-381
fix: sas fails with verifying credentials
2021-06-10 13:42:21 +03:00
06895cc9f8 style: lint 2021-06-10 12:08:56 +02:00
24496a997a chore: addressing comments 2021-06-10 12:08:16 +02:00
6419686269 chore: lint fixes 2021-06-09 17:28:27 +00:00
Sabir Hassan
4554c9100c Merge branch 'master' into issue-276 2021-06-09 16:51:49 +05:00
919c83c143 chore: lint fixes 2021-06-09 16:40:29 +05:00
00ba2957fb Merge branch 'master' into issue-381 2021-06-09 13:10:06 +02:00
5beda6547a Merge branch 'master' into issue-408 2021-06-09 13:09:59 +02:00
bd49b3757a chore(git): Merge branch 'master' into issue-408 2021-06-09 13:05:48 +02:00
Yury Shkoda
b32352a369 Merge pull request #413 from sasjs/webpack-fix
fix(webpack): removed process plugin from nodeConfig
2021-06-09 14:04:47 +03:00
b306f11148 chore(git): Merge branch 'master' into issue-381 2021-06-09 13:04:47 +02:00
Yury Shkoda
8c4955cb65 chore(git): merge branch 'master' of https://github.com/sasjs/adapter into webpack-fix 2021-06-09 13:58:59 +03:00
Yury Shkoda
155f2bb0e8 fix(webpack): removed process plugin from nodeConfig 2021-06-09 13:53:27 +03:00
3ca971134a Merge pull request #366 from sasjs/snyk-upgrade-0c3cac4dc7e5009cbff727c995cc3ebe
[Snyk] Upgrade @types/node from 14.14.25 to 14.14.41
2021-06-09 11:06:22 +02:00
488d8b9316 chore(git): Merge branch 'master' into issue-381 2021-06-09 10:38:25 +02:00
c20bdba4ae Merge branch 'master' into snyk-upgrade-0c3cac4dc7e5009cbff727c995cc3ebe 2021-06-09 10:36:10 +02:00
0be2d69aee Merge pull request #404 from sasjs/dependabot/npm_and_yarn/ts-jest-27.0.3
chore(deps-dev): bump ts-jest from 27.0.2 to 27.0.3
2021-06-09 10:33:18 +02:00
a6e67c3478 chore(merge): branch 'master' into dependabot/npm_and_yarn/ts-jest-27.0.3 2021-06-09 10:28:05 +02:00
5968988984 Merge pull request #405 from sasjs/dependabot/npm_and_yarn/webpack-cli-4.7.2
chore(deps-dev): bump webpack-cli from 4.7.0 to 4.7.2
2021-06-09 10:24:12 +02:00
31cd01610a Merge branch 'master' into dependabot/npm_and_yarn/webpack-cli-4.7.2 2021-06-09 10:21:58 +02:00
a67824762c Merge pull request #412 from sasjs/dependabot/npm_and_yarn/sasjs/utils-2.18.0
chore(deps): bump @sasjs/utils from 2.17.1 to 2.18.0
2021-06-09 10:21:37 +02:00
dependabot-preview[bot]
0336541d40 chore(deps-dev): bump webpack-cli from 4.7.0 to 4.7.2
Bumps [webpack-cli](https://github.com/webpack/webpack-cli) from 4.7.0 to 4.7.2.
- [Release notes](https://github.com/webpack/webpack-cli/releases)
- [Changelog](https://github.com/webpack/webpack-cli/blob/master/CHANGELOG.md)
- [Commits](https://github.com/webpack/webpack-cli/compare/webpack-cli@4.7.0...webpack-cli@4.7.2)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2021-06-09 08:01:57 +00:00
dependabot-preview[bot]
01de3836d7 chore(deps): bump @sasjs/utils from 2.17.1 to 2.18.0
Bumps [@sasjs/utils](https://github.com/sasjs/utils) from 2.17.1 to 2.18.0.
- [Release notes](https://github.com/sasjs/utils/releases)
- [Commits](https://github.com/sasjs/utils/compare/v2.17.1...v2.18.0)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2021-06-09 08:01:54 +00:00
Krishna Acondy
c571bb8490 Merge pull request #342 from sasjs/dependabot/add-v2-config-file
Upgrade to GitHub-native Dependabot
2021-06-09 08:59:58 +01:00
Krishna Acondy
5b4d354ea2 chore(*): remove ignores 2021-06-09 08:53:57 +01:00
Krishna Acondy
b0ce0dc40a Merge branch 'master' into dependabot/add-v2-config-file 2021-06-09 08:53:24 +01:00
88f70a7966 chore: merge 2021-06-08 17:01:41 +02:00
89ff323206 style: lint 2021-06-08 16:55:10 +02:00
d4357d939e test: extra attributes on JES 2021-06-08 16:54:46 +02:00
Allan Bowe
6cb76f0b5c chore: merge fix 2021-06-08 13:18:16 +00:00
Allan Bowe
ba2baa36c0 chore: updating merge conflicts 2021-06-08 13:14:29 +00:00
Yury Shkoda
e36cd785e8 Merge pull request #410 from sasjs/macro-vars
feat(variables): added macro variables to executeComputeJob method
2021-06-08 14:50:33 +03:00
2fa3a353fa feat: select extra attributes in JES response 2021-06-08 13:25:08 +02:00
Yury Shkoda
bdb1ffb2ef chore(cleanup): removed console.log 2021-06-08 13:40:35 +03:00
Yury Shkoda
84090661cf chore(git): Merge branch 'master' of https://github.com/sasjs/adapter into macro-vars 2021-06-08 13:31:46 +03:00
Yury Shkoda
68e14bbf05 feat(variables): added macro variables to executeComputeJob method 2021-06-08 13:03:02 +03:00
Allan Bowe
e4f23334d3 Merge pull request #407 from sasjs/fix-built-package
fix(build): provide process module for compatibility with browser
2021-06-08 11:03:46 +03:00
Krishna Acondy
5593963b89 fix(build): provide process module for compatibility with browser 2021-06-08 08:42:48 +01:00
Krishna Acondy
81c9138b93 Merge branch 'master' into dependabot/npm_and_yarn/ts-jest-27.0.3 2021-06-07 09:09:13 +01:00
Krishna Acondy
83fa82108b Merge pull request #401 from sasjs/dependabot/npm_and_yarn/sasjs/utils-2.17.1
chore(deps): bump @sasjs/utils from 2.10.2 to 2.17.1
2021-06-07 09:08:59 +01:00
dependabot-preview[bot]
76039c3ec7 chore(deps-dev): bump ts-jest from 27.0.2 to 27.0.3
Bumps [ts-jest](https://github.com/kulshekhar/ts-jest) from 27.0.2 to 27.0.3.
- [Release notes](https://github.com/kulshekhar/ts-jest/releases)
- [Changelog](https://github.com/kulshekhar/ts-jest/blob/master/CHANGELOG.md)
- [Commits](https://github.com/kulshekhar/ts-jest/compare/v27.0.2...v27.0.3)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2021-06-07 08:07:47 +00:00
Krishna Acondy
9b57c9ca1c Merge branch 'master' into snyk-upgrade-0c3cac4dc7e5009cbff727c995cc3ebe 2021-06-07 09:05:35 +01:00
dependabot-preview[bot]
4018cf95ba chore(deps): bump @sasjs/utils from 2.10.2 to 2.17.1
Bumps [@sasjs/utils](https://github.com/sasjs/utils) from 2.10.2 to 2.17.1.
- [Release notes](https://github.com/sasjs/utils/releases)
- [Commits](https://github.com/sasjs/utils/compare/v2.10.2...v2.17.1)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2021-06-07 07:58:28 +00:00
Krishna Acondy
173b6e3e8d Merge pull request #400 from sasjs/sas9-execute-code
feat(sas9-support): execute arbitrary code on SAS9 servers
2021-06-07 08:56:56 +01:00
Krishna Acondy
0ed5447aff chore(sas9-api): fix filename 2021-06-07 08:45:50 +01:00
Krishna Acondy
6344a906d8 chore(tests): fix tests - remove done callback 2021-06-07 08:37:44 +01:00
Allan Bowe
b2c135ae61 Merge branch 'master' into issue-381 2021-06-07 10:34:16 +03:00
Krishna Acondy
2032aacba3 chore(deps): update package versions 2021-06-04 08:59:15 +01:00
Krishna Acondy
fadccfc94c chore(refactor): upgrade utils, refactor to use timestamp generator 2021-06-04 08:40:27 +01:00
Krishna Acondy
551e4e43c1 feat(sas9-support): execute arbitrary code on SAS9 using SASjs runner 2021-06-04 08:37:50 +01:00
sabir_hassan
1867658cde fix: add validations for table name and table structure #276 2021-06-03 15:08:48 +05:00
3fff4f9c4d Merge pull request #395 from sasjs/makeErr
fix: adding makeErr for SAS 9 in sajss-tests
2021-06-02 15:46:39 +02:00
Allan Bowe
3f119432db fix: adding makeErr for SAS 9 in sajss-tests 2021-06-02 16:44:09 +03:00
0b18fddc3e chore: merge 2021-06-02 11:06:34 +02:00
19503e0b31 style: lint 2021-06-02 11:01:19 +02:00
d8bdc02f09 chore: sasjs-tests compute only on viya, login order fix 2021-06-02 11:00:08 +02:00
2d0833061f chore: merge branch 'master' into issue-381 2021-06-01 11:52:52 +02:00
Yury Shkoda
5dfc4e4086 Merge branch 'master' into issue-381 2021-05-31 08:03:44 +03:00
Saad Jutt
c5824a8a8d fix: using mime package to determine content-type 2021-05-30 23:47:31 +05:00
Allan Bowe
2147c59314 Merge pull request #388 from sasjs/sas9-auth-error
fix(sas9-support): Throw error when invalid credentials are supplied
2021-05-30 08:51:24 +03:00
Saad Jutt
56a1960fff feat: create file resource while deploying service pack for viya 2021-05-30 05:58:17 +05:00
b8c9522a55 chore: packages 2021-05-28 16:58:56 +02:00
b461cff731 Merge branch 'master' into issue-381 2021-05-28 15:24:01 +02:00
728167fd71 test: fix 2021-05-28 15:22:57 +02:00
460575b462 fix: when sas fails with verifying credentials, resend request with new csrf token 2021-05-28 15:05:44 +02:00
Krishna Acondy
b247da249a chore(git-hooks): allow numbers in commit message 2021-05-28 08:52:18 +01:00
Krishna Acondy
e79089b880 fix(sas9-support): throw error with invalid credentials 2021-05-28 08:52:00 +01:00
Krishna Acondy
fe907e1c43 Merge pull request #384 from sasjs/sas9-support
feat(sas9-support): add support for SAS9 job execution outside of the browser
2021-05-28 07:46:47 +01:00
Allan Bowe
e95e894365 Merge branch 'master' into sas9-support 2021-05-27 12:29:30 +03:00
Allan Bowe
82414d8b8b Merge pull request #379 from sasjs/dependabot/npm_and_yarn/sasjs/utils-2.14.0
chore(deps): bump @sasjs/utils from 2.10.2 to 2.14.0
2021-05-27 12:29:13 +03:00
Allan Bowe
456fa68f0f Merge branch 'master' into dependabot/npm_and_yarn/sasjs/utils-2.14.0 2021-05-27 11:55:31 +03:00
Allan Bowe
076adc1f6a Merge pull request #334 from sasjs/dependabot/npm_and_yarn/typedoc-0.20.36
chore(deps-dev): bump typedoc from 0.20.35 to 0.20.36
2021-05-27 11:54:52 +03:00
Krishna Acondy
9676488ff2 chore(refactor): remove unnecessary variables, use jobs path from config 2021-05-27 08:40:50 +01:00
Krishna Acondy
e9affb862d chore(merge): update branch 2021-05-27 08:32:11 +01:00
Krishna Acondy
e04371510e chore(update): update branch with changes from master 2021-05-27 08:30:20 +01:00
dependabot-preview[bot]
19657a1c12 chore(deps-dev): bump typedoc from 0.20.35 to 0.20.36
Bumps [typedoc](https://github.com/TypeStrong/TypeDoc) from 0.20.35 to 0.20.36.
- [Release notes](https://github.com/TypeStrong/TypeDoc/releases)
- [Commits](https://github.com/TypeStrong/TypeDoc/compare/v0.20.35...v0.20.36)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2021-05-27 07:22:22 +00:00
dependabot-preview[bot]
6424c82ac9 chore(deps): bump @sasjs/utils from 2.10.2 to 2.14.0
Bumps [@sasjs/utils](https://github.com/sasjs/utils) from 2.10.2 to 2.14.0.
- [Release notes](https://github.com/sasjs/utils/releases)
- [Commits](https://github.com/sasjs/utils/compare/v2.10.2...v2.14.0)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2021-05-27 07:22:21 +00:00
Allan Bowe
fcab18191f Merge pull request #382 from sasjs/dependabot/npm_and_yarn/browserslist-4.16.6
chore(deps): [security] bump browserslist from 4.16.4 to 4.16.6
2021-05-27 10:20:21 +03:00
Krishna Acondy
f157612a0e Merge branch 'master' into sas9-support 2021-05-27 08:16:49 +01:00
Krishna Acondy
b8cb7d52e7 chore(*): remove unused loader 2021-05-27 08:08:47 +01:00
Krishna Acondy
d8d1968162 chore(*): fix formatting 2021-05-27 08:06:21 +01:00
Krishna Acondy
0e1d1f1d99 chore(dep): remove unused dependency 2021-05-27 08:04:19 +01:00
Krishna Acondy
0b055dd05f feat(sas9-support): add support for SAS9 via username/password login 2021-05-27 08:00:15 +01:00
dependabot-preview[bot]
ba91c29ba8 chore(deps): [security] bump browserslist from 4.16.4 to 4.16.6
Bumps [browserslist](https://github.com/browserslist/browserslist) from 4.16.4 to 4.16.6. **This update includes a security fix.**
- [Release notes](https://github.com/browserslist/browserslist/releases)
- [Changelog](https://github.com/browserslist/browserslist/blob/main/CHANGELOG.md)
- [Commits](https://github.com/browserslist/browserslist/compare/4.16.4...4.16.6)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2021-05-26 08:24:56 +00:00
snyk-bot
55e64ae9d6 fix: upgrade @types/node from 14.14.25 to 14.14.41
Snyk has created this PR to upgrade @types/node from 14.14.25 to 14.14.41.

See this package in npm:
https://www.npmjs.com/package/@types/node

See this project in Snyk:
https://app.snyk.io/org/allanbowe/project/acbafb55-1a7a-485d-a36b-42650bb03cf6?utm_source=github&utm_medium=upgrade-pr
2021-05-15 21:55:56 +00:00
Krishna Acondy
f8c6318a88 chore(*): attempt SAS9 job executor 2021-05-11 08:15:48 +01:00
dependabot-preview[bot]
9b32b28aa7 Upgrade to GitHub-native Dependabot 2021-04-29 15:44:24 +00:00
29 changed files with 3183 additions and 2541 deletions

View File

@@ -6,7 +6,7 @@ GREEN="\033[1;32m"
# temporary file which holds the message). # temporary file which holds the message).
commit_message=$(cat "$1") commit_message=$(cat "$1")
if (echo "$commit_message" | grep -Eq "^(build|chore|ci|docs|feat|fix|perf|refactor|revert|style|test)(\([a-z \-]+\))?!?: .+$") then if (echo "$commit_message" | grep -Eq "^(build|chore|ci|docs|feat|fix|perf|refactor|revert|style|test)(\([a-z0-9 \-]+\))?!?: .+$") then
echo "${GREEN} ✔ Commit message meets Conventional Commit standards" echo "${GREEN} ✔ Commit message meets Conventional Commit standards"
exit 0 exit 0
fi fi

7
.github/dependabot.yml vendored Normal file
View File

@@ -0,0 +1,7 @@
version: 2
updates:
- package-ecosystem: npm
directory: "/"
schedule:
interval: daily
open-pull-requests-limit: 10

4642
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -38,31 +38,38 @@
}, },
"license": "ISC", "license": "ISC",
"devDependencies": { "devDependencies": {
"@types/jest": "^26.0.22", "@types/jest": "^26.0.23",
"@types/mime": "^2.0.3",
"@types/tough-cookie": "^4.0.0",
"cp": "^0.2.0", "cp": "^0.2.0",
"dotenv": "^8.2.0", "dotenv": "^10.0.0",
"jest": "^26.6.3", "jest": "^27.0.4",
"jest-extended": "^0.11.5", "jest-extended": "^0.11.5",
"mime": "^2.5.2",
"path": "^0.12.7", "path": "^0.12.7",
"process": "^0.11.10",
"rimraf": "^3.0.2", "rimraf": "^3.0.2",
"semantic-release": "^17.4.2", "semantic-release": "^17.4.3",
"terser-webpack-plugin": "^4.2.3", "terser-webpack-plugin": "^5.1.3",
"ts-jest": "^25.5.1", "ts-jest": "^27.0.3",
"ts-loader": "^9.1.2", "ts-loader": "^9.2.2",
"tslint": "^6.1.3", "tslint": "^6.1.3",
"tslint-config-prettier": "^1.18.0", "tslint-config-prettier": "^1.18.0",
"typedoc": "^0.20.35", "typedoc": "^0.20.36",
"typedoc-neo-theme": "^1.1.0", "typedoc-neo-theme": "^1.1.1",
"typedoc-plugin-external-module-name": "^4.0.6", "typedoc-plugin-external-module-name": "^4.0.6",
"typescript": "^3.9.9", "typescript": "^4.3.2",
"webpack": "^5.33.2", "webpack": "^5.38.1",
"webpack-cli": "^4.7.0" "webpack-cli": "^4.7.2"
}, },
"main": "index.js", "main": "index.js",
"dependencies": { "dependencies": {
"@sasjs/utils": "^2.10.2", "@sasjs/utils": "^2.20.1",
"axios": "^0.21.1", "axios": "^0.21.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",
"url": "^0.11.0"
} }
} }

View File

@@ -6,7 +6,7 @@ When developing on `@sasjs/adapter`, it's good practice to run the test suite ag
You can use the provided `update:adapter` NPM script for this. You can use the provided `update:adapter` NPM script for this.
``` ```bash
npm run update:adapter npm run update:adapter
``` ```
@@ -37,7 +37,7 @@ To be able to run the `deploy` script, two environment variables need to be set:
So you can run the script like so: So you can run the script like so:
``` ```bash
SSH_ACCOUNT=me@my-sas-server.com DEPLOY_PATH=/var/www/html/my-folder/sasjs-tests npm run deploy SSH_ACCOUNT=me@my-sas-server.com DEPLOY_PATH=/var/www/html/my-folder/sasjs-tests npm run deploy
``` ```
@@ -49,8 +49,7 @@ The below services need to be created on your SAS server, at the location specif
### SAS 9 ### SAS 9
``` ```sas
filename mc url "https://raw.githubusercontent.com/sasjs/core/main/all.sas"; filename mc url "https://raw.githubusercontent.com/sasjs/core/main/all.sas";
%inc mc; %inc mc;
filename ft15f001 temp; filename ft15f001 temp;
@@ -72,11 +71,24 @@ parmcards4;
%webout(CLOSE) %webout(CLOSE)
;;;; ;;;;
%mm_createwebservice(path=/Public/app/common,name=sendArr) %mm_createwebservice(path=/Public/app/common,name=sendArr)
parmcards4;
let he who hath understanding, reckon the number of the beast
;;;;
%mm_createwebservice(path=/Public/app/common,name=makeErr)
parmcards4;
%webout(OPEN)
data _null_;
file _webout;
put ' the discovery channel ';
run;
%webout(CLOSE)
;;;;
%mm_createwebservice(path=/Public/app/common,name=invalidJSON)
``` ```
### SAS Viya ### SAS Viya
``` ```sas
filename mc url "https://raw.githubusercontent.com/sasjs/core/main/all.sas"; filename mc url "https://raw.githubusercontent.com/sasjs/core/main/all.sas";
%inc mc; %inc mc;
filename ft15f001 temp; filename ft15f001 temp;
@@ -115,6 +127,15 @@ If you can trust yourself when all men doubt you,
But make allowance for their doubting too; But make allowance for their doubting too;
;;;; ;;;;
%mp_createwebservice(path=/Public/app/common,name=makeErr) %mp_createwebservice(path=/Public/app/common,name=makeErr)
parmcards4;
%webout(OPEN)
data _null_;
file _webout;
put ' the discovery channel ';
run;
%webout(CLOSE)
;;;;
%mp_createwebservice(path=/Public/app/common,name=invalidJSON)
``` ```
You should now be able to access the tests in your browser at the deployed path on your server. You should now be able to access the tests in your browser at the deployed path on your server.

View File

@@ -2005,12 +2005,15 @@
}, },
"@sasjs/adapter": { "@sasjs/adapter": {
"version": "file:../build/sasjs-adapter-5.0.0.tgz", "version": "file:../build/sasjs-adapter-5.0.0.tgz",
"integrity": "sha512-DxoQbdJqzqOTIuT7qwSfAbmNTWdpOx5zGkiMuZBSwoi9lSsRNoARiWnJq5Vl6h4RXJlc/FVdBFt35RZm4Mc0ZQ==", "integrity": "sha512-nP9O64IslMipxSKAG8PV/X2fRr+0E4/RqwD8jXP2bqZ/QraiKZG0bQPC5hSKqEp7bho8+XpZ4HaXW3Vr9kEZ8Q==",
"requires": { "requires": {
"@sasjs/utils": "^2.10.2", "@sasjs/utils": "^2.14.0",
"axios": "^0.21.1", "axios": "^0.21.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",
"url": "^0.11.0"
}, },
"dependencies": { "dependencies": {
"form-data": { "form-data": {
@@ -2022,6 +2025,21 @@
"combined-stream": "^1.0.8", "combined-stream": "^1.0.8",
"mime-types": "^2.1.12" "mime-types": "^2.1.12"
} }
},
"tough-cookie": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.0.0.tgz",
"integrity": "sha512-tHdtEpQCMrc1YLrMaqXXcj6AxhYi/xgit6mZu1+EDWUn+qhUf8wMQoFIy9NXuq23zAwtcB0t/MjACGR18pcRbg==",
"requires": {
"psl": "^1.1.33",
"punycode": "^2.1.1",
"universalify": "^0.1.2"
}
},
"universalify": {
"version": "0.1.2",
"resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz",
"integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg=="
} }
} }
}, },
@@ -2046,14 +2064,15 @@
} }
}, },
"@sasjs/utils": { "@sasjs/utils": {
"version": "2.12.1", "version": "2.15.5",
"resolved": "https://registry.npmjs.org/@sasjs/utils/-/utils-2.12.1.tgz", "resolved": "https://registry.npmjs.org/@sasjs/utils/-/utils-2.15.5.tgz",
"integrity": "sha512-6gZS5zW0J70P7XaVuEczyfHVaVa8Ks/aWr4PIlpJcxWD0enJtCEmos2DdnezdSoNvODkPq/8rzMPqko5jaXK1Q==", "integrity": "sha512-5HSWX5fy8D0Zy+Le+LgeRZG4vb5quLqhNiHw3dl0MS2hpsWACSRKia060jZk9LNHayKwBuusjlz5Ba0SyyaiEQ==",
"requires": { "requires": {
"@types/prompts": "^2.0.11", "@types/prompts": "^2.0.11",
"chalk": "^4.1.1", "chalk": "^4.1.1",
"cli-table": "^0.3.6", "cli-table": "^0.3.6",
"consola": "^2.15.0", "consola": "^2.15.0",
"fs-extra": "^10.0.0",
"prompts": "^2.4.1", "prompts": "^2.4.1",
"valid-url": "^1.0.9" "valid-url": "^1.0.9"
}, },
@@ -2088,6 +2107,16 @@
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="
}, },
"fs-extra": {
"version": "10.0.0",
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.0.0.tgz",
"integrity": "sha512-C5owb14u9eJwizKGdchcDUQeFtlSHHthBk8pbX9Vc1PFZrLombudjDnNns88aYslCyF6IY5SUw3Roz6xShcEIQ==",
"requires": {
"graceful-fs": "^4.2.0",
"jsonfile": "^6.0.1",
"universalify": "^2.0.0"
}
},
"has-flag": { "has-flag": {
"version": "4.0.0", "version": "4.0.0",
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
@@ -2402,9 +2431,9 @@
"integrity": "sha512-tHq6qdbT9U1IRSGf14CL0pUlULksvY9OZ+5eEgl1N7t+OA3tGvNpxJCzuKQlsNgCVwbAs670L1vcVQi8j9HjnA==" "integrity": "sha512-tHq6qdbT9U1IRSGf14CL0pUlULksvY9OZ+5eEgl1N7t+OA3tGvNpxJCzuKQlsNgCVwbAs670L1vcVQi8j9HjnA=="
}, },
"@types/node": { "@types/node": {
"version": "14.14.25", "version": "14.14.41",
"resolved": "https://registry.npmjs.org/@types/node/-/node-14.14.25.tgz", "resolved": "https://registry.npmjs.org/@types/node/-/node-14.14.41.tgz",
"integrity": "sha512-EPpXLOVqDvisVxtlbvzfyqSsFeQxltFbluZNRndIb8tr9KiBnYNLzrc1N3pyKUCww2RNrfHDViqDWWE1LCJQtQ==" "integrity": "sha512-dueRKfaJL4RTtSa7bWeTK1M+VH+Gns73oCgzvYfHZywRCoPSd8EkXBL0mZ9unPTveBn+D9phZBaxuzpwjWkW0g=="
}, },
"@types/normalize-package-data": { "@types/normalize-package-data": {
"version": "2.4.0", "version": "2.4.0",
@@ -2422,9 +2451,9 @@
"integrity": "sha512-6gOkRe7OIioWAXfnO/2lFiv+SJichKVSys1mSsgyrYHSEjk8Ctv4tSR/Odvnu+HWlH2C8j53dahU03XmQdd5fA==" "integrity": "sha512-6gOkRe7OIioWAXfnO/2lFiv+SJichKVSys1mSsgyrYHSEjk8Ctv4tSR/Odvnu+HWlH2C8j53dahU03XmQdd5fA=="
}, },
"@types/prompts": { "@types/prompts": {
"version": "2.0.11", "version": "2.0.12",
"resolved": "https://registry.npmjs.org/@types/prompts/-/prompts-2.0.11.tgz", "resolved": "https://registry.npmjs.org/@types/prompts/-/prompts-2.0.12.tgz",
"integrity": "sha512-dcF5L3rU9VfpLEJIV++FEyhGhuIpJllNEwllVuJ5g8eoVqjf048tW9+spivIwjzgPbtaGAl7mIZW3cmhDAq2UQ==", "integrity": "sha512-Hr6osqfNg3IcQT3pJDXCsSnb0KnldY/hXeJCKJriwbZLnedN9n1e8kcZwLc25GIWULDb6h5aEyOBbf33XpZBXQ==",
"requires": { "requires": {
"@types/node": "*" "@types/node": "*"
} }
@@ -3467,6 +3496,22 @@
"follow-redirects": "^1.10.0" "follow-redirects": "^1.10.0"
} }
}, },
"axios-cookiejar-support": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/axios-cookiejar-support/-/axios-cookiejar-support-1.0.1.tgz",
"integrity": "sha512-IZJxnAJ99XxiLqNeMOqrPbfR7fRyIfaoSLdPUf4AMQEGkH8URs0ghJK/xtqBsD+KsSr3pKl4DEQjCn834pHMig==",
"requires": {
"is-redirect": "^1.0.0",
"pify": "^5.0.0"
},
"dependencies": {
"pify": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/pify/-/pify-5.0.0.tgz",
"integrity": "sha512-eW/gHNMlxdSP6dmG6uJip6FXN0EQBwm2clYYd8Wul42Cwu/DK8HEftzsapcNdYe2MfLiIwZqsDk2RDEsTE79hA=="
}
}
},
"axobject-query": { "axobject-query": {
"version": "2.2.0", "version": "2.2.0",
"resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-2.2.0.tgz", "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-2.2.0.tgz",
@@ -8557,6 +8602,11 @@
"resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.0.tgz", "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.0.tgz",
"integrity": "sha1-DFLlS8yjkbssSUsh6GJtczbG45c=" "integrity": "sha1-DFLlS8yjkbssSUsh6GJtczbG45c="
}, },
"is-redirect": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/is-redirect/-/is-redirect-1.0.0.tgz",
"integrity": "sha1-HQPd7VO9jbDzDCbk+V02/HyH3CQ="
},
"is-regex": { "is-regex": {
"version": "1.1.0", "version": "1.1.0",
"resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.0.tgz", "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.0.tgz",

View File

@@ -7,7 +7,7 @@
"@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.0", "@sasjs/test-framework": "^1.4.0",
"@types/jest": "^26.0.20", "@types/jest": "^26.0.20",
"@types/node": "^14.14.25", "@types/node": "^14.14.41",
"@types/react": "^17.0.1", "@types/react": "^17.0.1",
"@types/react-dom": "^17.0.0", "@types/react-dom": "^17.0.0",
"@types/react-router-dom": "^5.1.7", "@types/react-router-dom": "^5.1.7",

View File

@@ -13,14 +13,19 @@ const App = (): ReactElement<{}> => {
useEffect(() => { useEffect(() => {
if (adapter) { if (adapter) {
setTestSuites([ const testSuites = [
basicTests(adapter, config.userName, config.password), basicTests(adapter, config.userName, config.password),
sendArrTests(adapter), sendArrTests(adapter),
sendObjTests(adapter), sendObjTests(adapter),
specialCaseTests(adapter), specialCaseTests(adapter),
sasjsRequestTests(adapter), sasjsRequestTests(adapter)
computeTests(adapter) ]
])
if (adapter.getSasjsConfig().serverType === 'SASVIYA') {
testSuites.push(computeTests(adapter))
}
setTestSuites(testSuites)
} }
}, [adapter, config]) }, [adapter, config])

View File

@@ -145,6 +145,29 @@ export const basicTests = (
sasjsConfig.debug === false sasjsConfig.debug === false
) )
} }
},
{
title: 'Request with extra attributes on JES approach',
description:
'Should complete successful request with extra attributes present in response',
test: async () => {
const config = {
useComputeApi: false
}
return await adapter.request(
'common/sendArr',
stringData,
config,
undefined,
undefined,
['file', 'data']
)
},
assertion: (response: any) => {
const responseKeys: any = Object.keys(response)
return responseKeys.includes('file') && responseKeys.includes('data')
}
} }
] ]
}) })

View File

@@ -176,11 +176,59 @@ export const sendObjTests = (adapter: SASjs): TestSuite => ({
name: 'sendObj', name: 'sendObj',
tests: [ tests: [
{ {
title: 'Invalid column name', title: 'Table name starts with numeric',
description: 'Should throw an error', description: 'Should throw an error',
test: async () => { test: async () => {
const invalidData: any = { const invalidData: any = {
'1 invalid table': [{ col1: 42 }] '1InvalidTable': [{ col1: 42 }]
}
return adapter.request('common/sendObj', invalidData).catch((e) => e)
},
assertion: (error: any) =>
!!error && !!error.error && !!error.error.message
},
{
title: 'Table name contains a space',
description: 'Should throw an error',
test: async () => {
const invalidData: any = {
'an invalidTable': [{ col1: 42 }]
}
return adapter.request('common/sendObj', invalidData).catch((e) => e)
},
assertion: (error: any) =>
!!error && !!error.error && !!error.error.message
},
{
title: 'Table name contains a special character',
description: 'Should throw an error',
test: async () => {
const invalidData: any = {
'anInvalidTable#': [{ col1: 42 }]
}
return adapter.request('common/sendObj', invalidData).catch((e) => e)
},
assertion: (error: any) =>
!!error && !!error.error && !!error.error.message
},
{
title: 'Table name exceeds max length of 32 characters',
description: 'Should throw an error',
test: async () => {
const invalidData: any = {
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx: [{ col1: 42 }]
}
return adapter.request('common/sendObj', invalidData).catch((e) => e)
},
assertion: (error: any) =>
!!error && !!error.error && !!error.error.message
},
{
title: "Invalid data object's structure",
description: 'Should throw an error',
test: async () => {
const invalidData: any = {
inData: [[{ data: 'value' }]]
} }
return adapter.request('common/sendObj', invalidData).catch((e) => e) return adapter.request('common/sendObj', invalidData).catch((e) => e)
}, },

View File

@@ -1,4 +1,6 @@
import axios, { AxiosInstance } from 'axios' import { generateTimestamp } from '@sasjs/utils/time'
import * as NodeFormData from 'form-data'
import { Sas9RequestClient } from './request/Sas9RequestClient'
import { isUrl } from './utils' import { isUrl } from './utils'
/** /**
@@ -6,11 +8,11 @@ import { isUrl } from './utils'
* *
*/ */
export class SAS9ApiClient { export class SAS9ApiClient {
private httpClient: AxiosInstance private requestClient: Sas9RequestClient
constructor(private serverUrl: string) { constructor(private serverUrl: string, private jobsPath: string) {
if (serverUrl) isUrl(serverUrl) if (serverUrl) isUrl(serverUrl)
this.httpClient = axios.create({ baseURL: this.serverUrl }) this.requestClient = new Sas9RequestClient(serverUrl, false)
} }
/** /**
@@ -33,27 +35,61 @@ export class SAS9ApiClient {
/** /**
* Executes code on a SAS9 server. * Executes code on a SAS9 server.
* @param linesOfCode - an array of code lines to execute. * @param linesOfCode - an array of code lines to execute.
* @param serverName - the server to execute the code on. * @param userName - the user name to log into the current SAS server.
* @param repositoryName - the repository to execute the code in. * @param password - the password to log into the current SAS server.
*/ */
public async executeScript( public async executeScript(
linesOfCode: string[], linesOfCode: string[],
serverName: string, userName: string,
repositoryName: string password: string
) { ) {
const requestPayload = linesOfCode.join('\n') await this.requestClient.login(userName, password, this.jobsPath)
const executeScriptResponse = await this.httpClient.put( // This piece of code forces a webout to prevent Stored Process Errors.
`/sas/servers/${serverName}/cmd?repositoryName=${repositoryName}`, const forceOutputCode = [
`command=${requestPayload}`, 'data _null_;',
{ 'file _webout;',
headers: { `put 'Executed sasjs run';`,
Accept: 'application/json' 'run;'
}, ]
responseType: 'text' const formData = generateFileUploadForm(
} [...linesOfCode, ...forceOutputCode].join('\n')
) )
return executeScriptResponse.data const codeInjectorPath = `/User Folders/${userName}/My Folder/sasjs/runner`
const contentType =
'multipart/form-data; boundary=' + formData.getBoundary()
const contentLength = formData.getLengthSync()
const headers = {
'cache-control': 'no-cache',
Accept: '*/*',
'Content-Type': contentType,
'Content-Length': contentLength,
Connection: 'keep-alive'
}
const storedProcessUrl = `${this.jobsPath}/?${
'_program=' + codeInjectorPath + '&_debug=log'
}`
const response = await this.requestClient.post(
storedProcessUrl,
formData,
undefined,
contentType,
headers
)
return response.result as string
} }
} }
const generateFileUploadForm = (data: any): NodeFormData => {
const formData = new NodeFormData()
const filename = `sasjs-execute-sas9-${generateTimestamp('')}.sas`
formData.append(filename, data, {
filename,
contentType: 'text/plain'
})
return formData
}

View File

@@ -12,6 +12,7 @@ import {
Context, Context,
ContextAllAttributes, ContextAllAttributes,
Folder, Folder,
File,
EditContextInput, EditContextInput,
JobDefinition, JobDefinition,
PollOptions PollOptions
@@ -28,8 +29,9 @@ import { timestampToYYYYMMDDHHMMSS } from '@sasjs/utils/time'
import { Logger, LogLevel } from '@sasjs/utils/logger' import { Logger, LogLevel } from '@sasjs/utils/logger'
import { isAuthorizeFormRequired } from './auth/isAuthorizeFormRequired' import { isAuthorizeFormRequired } from './auth/isAuthorizeFormRequired'
import { RequestClient } from './request/RequestClient' import { RequestClient } from './request/RequestClient'
import { SasAuthResponse } from '@sasjs/utils/types' import { SasAuthResponse, MacroVar } from '@sasjs/utils/types'
import { prefixMessage } from '@sasjs/utils/error' import { prefixMessage } from '@sasjs/utils/error'
import * as mime from 'mime'
/** /**
* A client for interfacing with the SAS Viya REST API. * A client for interfacing with the SAS Viya REST API.
@@ -271,6 +273,7 @@ export class SASViyaApiClient {
* @param waitForResult - when set to true, function will return the session * @param waitForResult - when set to true, function will return the session
* @param pollOptions - an object that represents poll interval(milliseconds) and maximum amount of attempts. Object example: { MAX_POLL_COUNT: 24 * 60 * 60, POLL_INTERVAL: 1000 }. * @param pollOptions - an object that represents poll interval(milliseconds) and maximum amount of attempts. Object example: { MAX_POLL_COUNT: 24 * 60 * 60, POLL_INTERVAL: 1000 }.
* @param printPid - a boolean that indicates whether the function should print (PID) of the started job. * @param printPid - a boolean that indicates whether the function should print (PID) of the started job.
* @param variables - an object that represents macro variables.
*/ */
public async executeScript( public async executeScript(
jobPath: string, jobPath: string,
@@ -282,7 +285,8 @@ export class SASViyaApiClient {
expectWebout = false, expectWebout = false,
waitForResult = true, waitForResult = true,
pollOptions?: PollOptions, pollOptions?: PollOptions,
printPid = false printPid = false,
variables?: MacroVar
): Promise<any> { ): Promise<any> {
try { try {
const headers: any = { const headers: any = {
@@ -356,6 +360,8 @@ export class SASViyaApiClient {
: jobPath : jobPath
} }
if (variables) jobVariables = { ...jobVariables, ...variables }
let files: any[] = [] let files: any[] = []
if (data) { if (data) {
@@ -532,6 +538,53 @@ export class SASViyaApiClient {
.then((res) => res.result) .then((res) => res.result)
} }
/**
* Creates a file. Path to or URI of the parent folder is required.
* @param fileName - the name of the new file.
* @param contentBuffer - the content of the new file in Buffer.
* @param parentFolderPath - the full path to the parent folder. If not
* provided, the parentFolderUri must be provided.
* @param parentFolderUri - the URI (eg /folders/folders/UUID) of the parent
* folder. If not provided, the parentFolderPath must be provided.
* @param accessToken - an access token for authorizing the request.
*/
public async createFile(
fileName: string,
contentBuffer: Buffer,
parentFolderPath?: string,
parentFolderUri?: string,
accessToken?: string
): Promise<File> {
if (!parentFolderPath && !parentFolderUri) {
throw new Error('Path or URI of the parent folder is required.')
}
if (!parentFolderUri && parentFolderPath) {
parentFolderUri = await this.getFolderUri(parentFolderPath, accessToken)
}
const headers = {
Accept: 'application/vnd.sas.file+json',
'Content-Disposition': `filename="${fileName}";`
}
const formData = new NodeFormData()
formData.append('file', contentBuffer, fileName)
const mimeType =
mime.getType(fileName.match(/\.[0-9a-z]+$/i)?.[0] || '') ?? 'text/plain'
return (
await this.requestClient.post<File>(
`/files/files?parentFolderUri=${parentFolderUri}&typeDefName=file#rawUpload`,
formData,
accessToken,
'multipart/form-data; boundary=' + (formData as any)._boundary,
headers
)
).result
}
/** /**
* Creates a folder. Path to or URI of the parent folder is required. * Creates a folder. Path to or URI of the parent folder is required.
* @param folderName - the name of the new folder. * @param folderName - the name of the new folder.
@@ -719,13 +772,11 @@ export class SASViyaApiClient {
let formData let formData
if (typeof FormData === 'undefined') { if (typeof FormData === 'undefined') {
formData = new NodeFormData() formData = new NodeFormData()
formData.append('grant_type', 'authorization_code')
formData.append('code', authCode)
} else { } else {
formData = new FormData() formData = new FormData()
formData.append('grant_type', 'authorization_code')
formData.append('code', authCode)
} }
formData.append('grant_type', 'authorization_code')
formData.append('code', authCode)
const authResponse = await this.requestClient const authResponse = await this.requestClient
.post( .post(
@@ -814,6 +865,7 @@ export class SASViyaApiClient {
* @param expectWebout - a boolean indicating whether to expect a _webout response. * @param expectWebout - a boolean indicating whether to expect a _webout response.
* @param pollOptions - an object that represents poll interval(milliseconds) and maximum amount of attempts. Object example: { MAX_POLL_COUNT: 24 * 60 * 60, POLL_INTERVAL: 1000 }. * @param pollOptions - an object that represents poll interval(milliseconds) and maximum amount of attempts. Object example: { MAX_POLL_COUNT: 24 * 60 * 60, POLL_INTERVAL: 1000 }.
* @param printPid - a boolean that indicates whether the function should print (PID) of the started job. * @param printPid - a boolean that indicates whether the function should print (PID) of the started job.
* @param variables - an object that represents macro variables.
*/ */
public async executeComputeJob( public async executeComputeJob(
sasJob: string, sasJob: string,
@@ -824,7 +876,8 @@ export class SASViyaApiClient {
waitForResult = true, waitForResult = true,
expectWebout = false, expectWebout = false,
pollOptions?: PollOptions, pollOptions?: PollOptions,
printPid = false printPid = false,
variables?: MacroVar
) { ) {
if (isRelativePath(sasJob) && !this.rootFolderName) { if (isRelativePath(sasJob) && !this.rootFolderName) {
throw new Error( throw new Error(
@@ -903,7 +956,8 @@ export class SASViyaApiClient {
expectWebout, expectWebout,
waitForResult, waitForResult,
pollOptions, pollOptions,
printPid printPid,
variables
) )
} }

View File

@@ -4,15 +4,17 @@ import { SASViyaApiClient } from './SASViyaApiClient'
import { SAS9ApiClient } from './SAS9ApiClient' import { SAS9ApiClient } from './SAS9ApiClient'
import { FileUploader } from './FileUploader' import { FileUploader } from './FileUploader'
import { AuthManager } from './auth' import { AuthManager } from './auth'
import { ServerType } from '@sasjs/utils/types' import { ServerType, MacroVar } from '@sasjs/utils/types'
import { RequestClient } from './request/RequestClient' import { RequestClient } from './request/RequestClient'
import { import {
JobExecutor, JobExecutor,
WebJobExecutor, WebJobExecutor,
ComputeJobExecutor, ComputeJobExecutor,
JesJobExecutor JesJobExecutor,
Sas9JobExecutor
} from './job-execution' } from './job-execution'
import { ErrorResponse } from './types/errors' import { ErrorResponse } from './types/errors'
import { ExtraResponseAttributes } from '@sasjs/utils/types'
const defaultConfig: SASjsConfig = { const defaultConfig: SASjsConfig = {
serverUrl: '', serverUrl: '',
@@ -41,6 +43,7 @@ export default class SASjs {
private webJobExecutor: JobExecutor | null = null private webJobExecutor: JobExecutor | null = null
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
constructor(config?: any) { constructor(config?: any) {
this.sasjsConfig = { this.sasjsConfig = {
@@ -57,15 +60,15 @@ export default class SASjs {
public async executeScriptSAS9( public async executeScriptSAS9(
linesOfCode: string[], linesOfCode: string[],
serverName: string, userName: string,
repositoryName: 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,
serverName, userName,
repositoryName password
) )
} }
@@ -265,7 +268,7 @@ export default class SASjs {
} }
/** /**
* Creates a folder at SAS file system. * 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.
* @param parentFolderPath - the full path (eg `/Public/example/myFolder`) of the parent folder. * @param parentFolderPath - the full path (eg `/Public/example/myFolder`) of the parent folder.
* @param parentFolderUri - the URI of the parent folder. * @param parentFolderUri - the URI of the parent folder.
@@ -297,6 +300,40 @@ export default class SASjs {
) )
} }
/**
* Creates a file in the logical SAS folder tree
* @param fileName - name of the file to be created.
* @param content - content of the file to be created.
* @param parentFolderPath - the full path (eg `/Public/example/myFolder`) of the parent folder.
* @param parentFolderUri - the URI of the parent folder.
* @param accessToken - the access token to authorizing the request.
* @param sasApiClient - a client for interfacing with SAS API.
*/
public async createFile(
fileName: string,
content: Buffer,
parentFolderPath: string,
parentFolderUri?: string,
accessToken?: string,
sasApiClient?: SASViyaApiClient
) {
if (sasApiClient)
return await sasApiClient.createFile(
fileName,
content,
parentFolderPath,
parentFolderUri,
accessToken
)
return await this.sasViyaApiClient!.createFile(
fileName,
content,
parentFolderPath,
parentFolderUri,
accessToken
)
}
/** /**
* Fetches a folder from the SAS file system. * Fetches a folder from the SAS file system.
* @param folderPath - path of the folder to be fetched. * @param folderPath - path of the folder to be fetched.
@@ -538,22 +575,26 @@ export default class SASjs {
* `await request(sasJobPath, data, config, () => setIsLoggedIn(false))` * `await request(sasJobPath, data, config, () => setIsLoggedIn(false))`
* If you are not passing in any data and configuration, it will look like so: * If you are not passing in any data and configuration, it will look like so:
* `await request(sasJobPath, {}, {}, () => setIsLoggedIn(false))` * `await request(sasJobPath, {}, {}, () => setIsLoggedIn(false))`
* @param extraResponseAttributes - a array of predefined values that are used
* to provide extra attributes (same names as those values) to be added in response
* Supported values are declared in ExtraResponseAttributes type.
*/ */
public async request( public async request(
sasJob: string, sasJob: string,
data: { [key: string]: any }, data: { [key: string]: any } | null,
config: { [key: string]: any } = {}, config: { [key: string]: any } = {},
loginRequiredCallback?: () => any, loginRequiredCallback?: () => any,
accessToken?: string accessToken?: string,
extraResponseAttributes: ExtraResponseAttributes[] = []
) { ) {
config = { config = {
...this.sasjsConfig, ...this.sasjsConfig,
...config ...config
} }
if (
typeof loginRequiredCallback === 'function' || const validationResult = this.validateInput(data)
typeof loginRequiredCallback === 'undefined'
) { if (validationResult.status) {
if (config.serverType === ServerType.SasViya && config.contextName) { if (config.serverType === ServerType.SasViya && config.contextName) {
if (config.useComputeApi) { if (config.useComputeApi) {
return await this.computeJobExecutor!.execute( return await this.computeJobExecutor!.execute(
@@ -569,23 +610,91 @@ export default class SASjs {
data, data,
config, config,
loginRequiredCallback, loginRequiredCallback,
accessToken accessToken,
extraResponseAttributes
) )
} }
} else if (
config.serverType === ServerType.Sas9 &&
config.username &&
config.password
) {
return await this.sas9JobExecutor!.execute(sasJob, data, config)
} else { } else {
return await this.webJobExecutor!.execute( return await this.webJobExecutor!.execute(
sasJob, sasJob,
data, data,
config, config,
loginRequiredCallback loginRequiredCallback,
accessToken,
extraResponseAttributes
) )
} }
} else { } else {
return Promise.reject( return Promise.reject(new ErrorResponse(validationResult.msg))
new ErrorResponse( }
`Invalid loginRequiredCallback parameter was provided. Expected Callback function but found ${typeof loginRequiredCallback}` }
)
) /**
* This function validates the input data structure and table naming convention
*
* @param data A json object that contains one or more tables, it can also be null
* @returns An object which contains two attributes: 1) status: boolean, 2) msg: string
*/
private validateInput(data: { [key: string]: any } | null): {
status: boolean
msg: string
} {
if (data === null) return { status: true, msg: '' }
for (const key in data) {
if (!key.match(/^[a-zA-Z_]/)) {
return {
status: false,
msg: 'First letter of table should be alphabet or underscore.'
}
}
if (!key.match(/^[a-zA-Z_][a-zA-Z0-9_]*$/)) {
return { status: false, msg: 'Table name should be alphanumeric.' }
}
if (key.length > 32) {
return {
status: false,
msg: 'Maximum length for table name could be 32 characters.'
}
}
if (this.getType(data[key]) !== 'Array') {
return {
status: false,
msg: 'Parameter data contains invalid table structure.'
}
}
for (let i = 0; i < data[key].length; i++) {
if (this.getType(data[key][i]) !== 'object') {
return {
status: false,
msg: `Table ${key} contains invalid structure.`
}
}
}
}
return { status: true, msg: '' }
}
/**
* this function returns the type of variable
*
* @param data it could be anything, like string, array, object etc.
* @returns a string which tells the type of input parameter
*/
private getType(data: any): string {
if (Array.isArray(data)) {
return 'Array'
} else {
return typeof data
} }
} }
@@ -626,7 +735,7 @@ export default class SASjs {
) )
sasApiClient.debug = this.sasjsConfig.debug sasApiClient.debug = this.sasjsConfig.debug
} else if (this.sasjsConfig.serverType === ServerType.Sas9) { } else if (this.sasjsConfig.serverType === ServerType.Sas9) {
sasApiClient = new SAS9ApiClient(serverUrl) sasApiClient = new SAS9ApiClient(serverUrl, this.jobsPath)
} }
} else { } else {
let sasClientConfig: any = null let sasClientConfig: any = null
@@ -673,6 +782,7 @@ export default class SASjs {
* @param waitForResult - a boolean that indicates whether the function needs to wait for execution to complete. * @param waitForResult - a boolean that indicates whether the function needs to wait for execution to complete.
* @param pollOptions - an object that represents poll interval(milliseconds) and maximum amount of attempts. Object example: { MAX_POLL_COUNT: 24 * 60 * 60, POLL_INTERVAL: 1000 }. * @param pollOptions - an object that represents poll interval(milliseconds) and maximum amount of attempts. Object example: { MAX_POLL_COUNT: 24 * 60 * 60, POLL_INTERVAL: 1000 }.
* @param printPid - a boolean that indicates whether the function should print (PID) of the started job. * @param printPid - a boolean that indicates whether the function should print (PID) of the started job.
* @param variables - an object that represents macro variables.
*/ */
public async startComputeJob( public async startComputeJob(
sasJob: string, sasJob: string,
@@ -681,7 +791,8 @@ export default class SASjs {
accessToken?: string, accessToken?: string,
waitForResult?: boolean, waitForResult?: boolean,
pollOptions?: PollOptions, pollOptions?: PollOptions,
printPid = false printPid = false,
variables?: MacroVar
) { ) {
config = { config = {
...this.sasjsConfig, ...this.sasjsConfig,
@@ -704,7 +815,8 @@ export default class SASjs {
!!waitForResult, !!waitForResult,
false, false,
pollOptions, pollOptions,
printPid printPid,
variables
) )
} }
@@ -815,7 +927,11 @@ export default class SASjs {
if (this.sasjsConfig.serverType === ServerType.Sas9) { if (this.sasjsConfig.serverType === ServerType.Sas9) {
if (this.sas9ApiClient) if (this.sas9ApiClient)
this.sas9ApiClient!.setConfig(this.sasjsConfig.serverUrl) this.sas9ApiClient!.setConfig(this.sasjsConfig.serverUrl)
else this.sas9ApiClient = new SAS9ApiClient(this.sasjsConfig.serverUrl) else
this.sas9ApiClient = new SAS9ApiClient(
this.sasjsConfig.serverUrl,
this.jobsPath
)
} }
this.fileUploader = new FileUploader( this.fileUploader = new FileUploader(
@@ -833,6 +949,12 @@ export default class SASjs {
this.sasViyaApiClient! this.sasViyaApiClient!
) )
this.sas9JobExecutor = new Sas9JobExecutor(
this.sasjsConfig.serverUrl,
this.sasjsConfig.serverType!,
this.jobsPath
)
this.computeJobExecutor = new ComputeJobExecutor( this.computeJobExecutor = new ComputeJobExecutor(
this.sasjsConfig.serverUrl, this.sasjsConfig.serverUrl,
this.sasViyaApiClient! this.sasViyaApiClient!
@@ -863,6 +985,16 @@ export default class SASjs {
isForced isForced
) )
break break
case 'file':
await this.createFile(
member.name,
member.code,
parentFolder,
undefined,
accessToken,
sasApiClient
)
break
case 'service': case 'service':
await this.createJobDefinition( await this.createJobDefinition(
member.name, member.name,

View File

@@ -35,6 +35,7 @@ export class AuthManager {
this.userName = loginParams.username this.userName = loginParams.username
const { isLoggedIn, loginForm } = await this.checkSession() const { isLoggedIn, loginForm } = await this.checkSession()
if (isLoggedIn) { if (isLoggedIn) {
await this.loginCallback() await this.loginCallback()
@@ -44,6 +45,35 @@ export class AuthManager {
} }
} }
let loginResponse = await this.sendLoginRequest(loginForm, loginParams)
let loggedIn = isLogInSuccess(loginResponse)
if (!loggedIn) {
if (isCredentialsVerifyError(loginResponse)) {
const newLoginForm = await this.getLoginForm(loginResponse)
loginResponse = await this.sendLoginRequest(newLoginForm, loginParams)
}
const currentSession = await this.checkSession()
loggedIn = currentSession.isLoggedIn
}
if (loggedIn) {
this.loginCallback()
}
return {
isLoggedIn: !!loggedIn,
userName: this.userName
}
}
private async sendLoginRequest(
loginForm: { [key: string]: any },
loginParams: { [key: string]: any }
) {
for (const key in loginForm) { for (const key in loginForm) {
loginParams[key] = loginForm[key] loginParams[key] = loginForm[key]
} }
@@ -60,21 +90,7 @@ export class AuthManager {
} }
) )
let loggedIn = isLogInSuccess(loginResponse) return loginResponse
if (!loggedIn) {
const currentSession = await this.checkSession()
loggedIn = currentSession.isLoggedIn
}
if (loggedIn) {
this.loginCallback()
}
return {
isLoggedIn: !!loggedIn,
userName: this.userName
}
} }
/** /**
@@ -168,5 +184,10 @@ export class AuthManager {
} }
} }
const isCredentialsVerifyError = (response: string): boolean =>
/An error occurred while the system was verifying your credentials. Please enter your credentials again./gm.test(
response
)
const isLogInSuccess = (response: string): boolean => const isLogInSuccess = (response: string): boolean =>
/You have signed in/gm.test(response) /You have signed in/gm.test(response)

View File

@@ -57,7 +57,7 @@ describe('AuthManager', () => {
expect((authManager as any).logoutUrl).toEqual('/SASLogon/logout?') expect((authManager as any).logoutUrl).toEqual('/SASLogon/logout?')
}) })
it('should call the auth callback and return when already logged in', async (done) => { it('should call the auth callback and return when already logged in', async () => {
const authManager = new AuthManager( const authManager = new AuthManager(
serverUrl, serverUrl,
serverType, serverType,
@@ -77,10 +77,9 @@ describe('AuthManager', () => {
expect(loginResponse.isLoggedIn).toBeTruthy() expect(loginResponse.isLoggedIn).toBeTruthy()
expect(loginResponse.userName).toEqual(userName) expect(loginResponse.userName).toEqual(userName)
expect(authCallback).toHaveBeenCalledTimes(1) expect(authCallback).toHaveBeenCalledTimes(1)
done()
}) })
it('should post a login request to the server if not logged in', async (done) => { it('should post a login request to the server if not logged in', async () => {
const authManager = new AuthManager( const authManager = new AuthManager(
serverUrl, serverUrl,
serverType, serverType,
@@ -121,10 +120,9 @@ describe('AuthManager', () => {
} }
) )
expect(authCallback).toHaveBeenCalledTimes(1) expect(authCallback).toHaveBeenCalledTimes(1)
done()
}) })
it('should parse and submit the authorisation form when necessary', async (done) => { it('should parse and submit the authorisation form when necessary', async () => {
const authManager = new AuthManager( const authManager = new AuthManager(
serverUrl, serverUrl,
serverType, serverType,
@@ -160,10 +158,9 @@ describe('AuthManager', () => {
expect(requestClient.authorize).toHaveBeenCalledWith( expect(requestClient.authorize).toHaveBeenCalledWith(
mockLoginAuthoriseRequiredResponse mockLoginAuthoriseRequiredResponse
) )
done()
}) })
it('should check and return session information if logged in', async (done) => { it('should check and return session information if logged in', async () => {
const authManager = new AuthManager( const authManager = new AuthManager(
serverUrl, serverUrl,
serverType, serverType,
@@ -189,7 +186,5 @@ describe('AuthManager', () => {
} }
} }
) )
done()
}) })
}) })

View File

@@ -5,6 +5,7 @@ import {
JobExecutionError, JobExecutionError,
LoginRequiredError LoginRequiredError
} from '../types/errors' } from '../types/errors'
import { ExtraResponseAttributes } from '@sasjs/utils/types'
import { BaseJobExecutor } from './JobExecutor' import { BaseJobExecutor } from './JobExecutor'
export class JesJobExecutor extends BaseJobExecutor { export class JesJobExecutor extends BaseJobExecutor {
@@ -17,7 +18,8 @@ export class JesJobExecutor extends BaseJobExecutor {
data: any, data: any,
config: any, config: any,
loginRequiredCallback?: any, loginRequiredCallback?: any,
accessToken?: string accessToken?: string,
extraResponseAttributes: ExtraResponseAttributes[] = []
) { ) {
const loginCallback = loginRequiredCallback || (() => Promise.resolve()) const loginCallback = loginRequiredCallback || (() => Promise.resolve())
@@ -30,10 +32,26 @@ export class JesJobExecutor extends BaseJobExecutor {
data, data,
accessToken accessToken
) )
.then((response) => { .then((response: any) => {
this.appendRequest(response, sasJob, config.debug) this.appendRequest(response, sasJob, config.debug)
resolve(response) let responseObject = {}
if (extraResponseAttributes && extraResponseAttributes.length > 0) {
const extraAttributes = extraResponseAttributes.reduce(
(map: any, obj: any) => ((map[obj] = response[obj]), map),
{}
)
responseObject = {
result: response.result,
...extraAttributes
}
} else {
responseObject = response.result
}
resolve(responseObject)
}) })
.catch(async (e: Error) => { .catch(async (e: Error) => {
if (e instanceof JobExecutionError) { if (e instanceof JobExecutionError) {
@@ -50,7 +68,9 @@ export class JesJobExecutor extends BaseJobExecutor {
sasJob, sasJob,
data, data,
config, config,
loginRequiredCallback loginRequiredCallback,
accessToken,
extraResponseAttributes
).then( ).then(
(res: any) => { (res: any) => {
resolve(res) resolve(res)

View File

@@ -1,5 +1,6 @@
import { ServerType } from '@sasjs/utils/types' import { ServerType } from '@sasjs/utils/types'
import { SASjsRequest } from '../types' import { SASjsRequest } from '../types'
import { ExtraResponseAttributes } from '@sasjs/utils/types'
import { asyncForEach, parseGeneratedCode, parseSourceCode } from '../utils' import { asyncForEach, parseGeneratedCode, parseSourceCode } from '../utils'
export type ExecuteFunction = () => Promise<any> export type ExecuteFunction = () => Promise<any>
@@ -10,7 +11,8 @@ export interface JobExecutor {
data: any, data: any,
config: any, config: any,
loginRequiredCallback?: any, loginRequiredCallback?: any,
accessToken?: string accessToken?: string,
extraResponseAttributes?: ExtraResponseAttributes[]
) => Promise<any> ) => Promise<any>
resendWaitingRequests: () => Promise<void> resendWaitingRequests: () => Promise<void>
getRequests: () => SASjsRequest[] getRequests: () => SASjsRequest[]
@@ -28,7 +30,8 @@ export abstract class BaseJobExecutor implements JobExecutor {
data: any, data: any,
config: any, config: any,
loginRequiredCallback?: any, loginRequiredCallback?: any,
accessToken?: string | undefined accessToken?: string | undefined,
extraResponseAttributes?: ExtraResponseAttributes[]
): Promise<any> ): Promise<any>
resendWaitingRequests = async () => { resendWaitingRequests = async () => {
@@ -59,14 +62,14 @@ export abstract class BaseJobExecutor implements JobExecutor {
let sasWork = null let sasWork = null
if (debug) { if (debug) {
if (response?.result && response?.log) { if (response?.log) {
sourceCode = parseSourceCode(response.log) sourceCode = parseSourceCode(response.log)
generatedCode = parseGeneratedCode(response.log) generatedCode = parseGeneratedCode(response.log)
if (response.log) { if (response?.result) {
sasWork = response.log
} else {
sasWork = response.result.WORK sasWork = response.result.WORK
} else {
sasWork = response.log
} }
} else if (response?.result) { } else if (response?.result) {
sourceCode = parseSourceCode(response.result) sourceCode = parseSourceCode(response.result)

View File

@@ -0,0 +1,110 @@
import { ServerType } from '@sasjs/utils/types'
import * as NodeFormData from 'form-data'
import { ErrorResponse } from '../types/errors'
import { convertToCSV, isRelativePath } from '../utils'
import { BaseJobExecutor } from './JobExecutor'
import { Sas9RequestClient } from '../request/Sas9RequestClient'
/**
* Job executor for SAS9 servers for use in Node.js environments.
* Initiates login with the provided username and password from the config
* The cookies are stored in the request client and used in subsequent
* job execution requests.
*/
export class Sas9JobExecutor extends BaseJobExecutor {
private requestClient: Sas9RequestClient
constructor(
serverUrl: string,
serverType: ServerType,
private jobsPath: string
) {
super(serverUrl, serverType)
this.requestClient = new Sas9RequestClient(serverUrl, false)
}
async execute(sasJob: string, data: any, config: any) {
const program = isRelativePath(sasJob)
? config.appLoc
? config.appLoc.replace(/\/?$/, '/') + sasJob.replace(/^\//, '')
: sasJob
: sasJob
let apiUrl = `${config.serverUrl}${this.jobsPath}?${'_program=' + program}`
apiUrl = `${apiUrl}${
config.username && config.password
? '&_username=' + config.username + '&_password=' + config.password
: ''
}`
let requestParams = {
...this.getRequestParams(config)
}
let formData = new NodeFormData()
if (data) {
try {
formData = generateFileUploadForm(formData, data)
} catch (e) {
return Promise.reject(new ErrorResponse(e?.message, e))
}
}
for (const key in requestParams) {
if (requestParams.hasOwnProperty(key)) {
formData.append(key, requestParams[key])
}
}
await this.requestClient.login(
config.username,
config.password,
this.jobsPath
)
const contentType =
data && Object.keys(data).length
? 'multipart/form-data; boundary=' + (formData as any)._boundary
: 'text/plain'
return await this.requestClient!.post(
apiUrl,
formData,
undefined,
contentType,
{
Accept: '*/*',
Connection: 'Keep-Alive'
}
)
}
private getRequestParams(config: any): any {
const requestParams: any = {}
if (config.debug) {
requestParams['_debug'] = 131
}
return requestParams
}
}
const generateFileUploadForm = (
formData: NodeFormData,
data: any
): NodeFormData => {
for (const tableName in data) {
const name = tableName
const csv = convertToCSV(data[tableName])
if (csv === 'ERROR: LARGE STRING LENGTH') {
throw new Error(
'The max length of a string value in SASjs is 32765 characters.'
)
}
formData.append(name, csv, {
filename: `${name}.csv`,
contentType: 'application/csv'
})
}
return formData
}

View File

@@ -1,4 +1,5 @@
export * from './ComputeJobExecutor' export * from './ComputeJobExecutor'
export * from './JesJobExecutor' export * from './JesJobExecutor'
export * from './JobExecutor' export * from './JobExecutor'
export * from './Sas9JobExecutor'
export * from './WebJobExecutor' export * from './WebJobExecutor'

View File

@@ -10,6 +10,7 @@ import {
} from '../types/errors' } from '../types/errors'
import { parseWeboutResponse } from '../utils/parseWeboutResponse' import { parseWeboutResponse } from '../utils/parseWeboutResponse'
import { prefixMessage } from '@sasjs/utils/error' import { prefixMessage } from '@sasjs/utils/error'
import { SAS9AuthError } from '../types/errors/SAS9AuthError'
export interface HttpClient { export interface HttpClient {
get<T>( get<T>(
@@ -44,11 +45,11 @@ export interface HttpClient {
} }
export class RequestClient implements HttpClient { export class RequestClient implements HttpClient {
private csrfToken: CsrfToken = { headerName: '', value: '' } protected csrfToken: CsrfToken = { headerName: '', value: '' }
private fileUploadCsrfToken: CsrfToken | undefined protected fileUploadCsrfToken: CsrfToken | undefined
private httpClient: AxiosInstance protected httpClient: AxiosInstance
constructor(private baseUrl: string, allowInsecure = false) { constructor(protected baseUrl: string, allowInsecure = false) {
const https = require('https') const https = require('https')
if (allowInsecure && https.Agent) { if (allowInsecure && https.Agent) {
this.httpClient = axios.create({ this.httpClient = axios.create({
@@ -290,7 +291,7 @@ export class RequestClient implements HttpClient {
}) })
} }
private getHeaders = ( protected getHeaders = (
accessToken: string | undefined, accessToken: string | undefined,
contentType: string contentType: string
) => { ) => {
@@ -315,7 +316,7 @@ export class RequestClient implements HttpClient {
return headers return headers
} }
private parseAndSetFileUploadCsrfToken = (response: AxiosResponse) => { protected parseAndSetFileUploadCsrfToken = (response: AxiosResponse) => {
const token = this.parseCsrfToken(response) const token = this.parseCsrfToken(response)
if (token) { if (token) {
@@ -323,7 +324,7 @@ export class RequestClient implements HttpClient {
} }
} }
private parseAndSetCsrfToken = (response: AxiosResponse) => { protected parseAndSetCsrfToken = (response: AxiosResponse) => {
const token = this.parseCsrfToken(response) const token = this.parseCsrfToken(response)
if (token) { if (token) {
@@ -347,7 +348,7 @@ export class RequestClient implements HttpClient {
} }
} }
private handleError = async ( protected handleError = async (
e: any, e: any,
callback: any, callback: any,
debug: boolean = false debug: boolean = false
@@ -405,7 +406,7 @@ export class RequestClient implements HttpClient {
throw e throw e
} }
private parseResponse<T>(response: AxiosResponse<any>) { protected parseResponse<T>(response: AxiosResponse<any>) {
const etag = response?.headers ? response.headers['etag'] : '' const etag = response?.headers ? response.headers['etag'] : ''
let parsedResponse let parsedResponse
let includeSAS9Log: boolean = false let includeSAS9Log: boolean = false
@@ -439,7 +440,7 @@ export class RequestClient implements HttpClient {
} }
} }
const throwIfError = (response: AxiosResponse) => { export const throwIfError = (response: AxiosResponse) => {
if (response.status === 401) { if (response.status === 401) {
throw new LoginRequiredError() throw new LoginRequiredError()
} }
@@ -470,6 +471,10 @@ const throwIfError = (response: AxiosResponse) => {
throw new AuthorizeError(response.data.message, authorizeRequestUrl) throw new AuthorizeError(response.data.message, authorizeRequestUrl)
} }
if (response.config?.url?.includes('sasAuthError')) {
throw new SAS9AuthError()
}
const error = parseError(response.data as string) const error = parseError(response.data as string)
if (error) { if (error) {

View File

@@ -0,0 +1,121 @@
import { AxiosRequestConfig } from 'axios'
import axiosCookieJarSupport from 'axios-cookiejar-support'
import * as tough from 'tough-cookie'
import { prefixMessage } from '@sasjs/utils/error'
import { RequestClient, throwIfError } from './RequestClient'
/**
* Specific request client for SAS9 in Node.js environments.
* Handles redirects and cookie management.
*/
export class Sas9RequestClient extends RequestClient {
constructor(baseUrl: string, allowInsecure = false) {
super(baseUrl, allowInsecure)
this.httpClient.defaults.maxRedirects = 0
this.httpClient.defaults.validateStatus = (status) =>
status >= 200 && status < 303
if (axiosCookieJarSupport) {
axiosCookieJarSupport(this.httpClient)
this.httpClient.defaults.jar = new tough.CookieJar()
}
}
public async login(username: string, password: string, jobsPath: string) {
const codeInjectorPath = `/User Folders/${username}/My Folder/sasjs/runner`
if (this.httpClient.defaults.jar) {
;(this.httpClient.defaults.jar as tough.CookieJar).removeAllCookies()
await this.get(
`${jobsPath}?_program=${codeInjectorPath}&_username=${username}&_password=${password}`,
undefined,
'text/plain'
)
}
}
public async get<T>(
url: string,
accessToken: string | undefined,
contentType: string = 'application/json',
overrideHeaders: { [key: string]: string | number } = {},
debug: boolean = false
): Promise<{ result: T; etag: string }> {
const headers = {
...this.getHeaders(accessToken, contentType),
...overrideHeaders
}
const requestConfig: AxiosRequestConfig = {
headers,
responseType: contentType === 'text/plain' ? 'text' : 'json',
withCredentials: true
}
if (contentType === 'text/plain') {
requestConfig.transformResponse = undefined
}
return this.httpClient
.get<T>(url, requestConfig)
.then((response) => {
if (response.status === 302) {
return this.get(
response.headers['location'],
accessToken,
contentType
)
}
throwIfError(response)
return this.parseResponse<T>(response)
})
.catch(async (e) => {
return await this.handleError(
e,
() =>
this.get<T>(url, accessToken, contentType, overrideHeaders).catch(
(err) => {
throw prefixMessage(
err,
'Error while executing handle error callback. '
)
}
),
debug
).catch((err) => {
throw prefixMessage(err, 'Error while handling error. ')
})
})
}
public post<T>(
url: string,
data: any,
accessToken: string | undefined,
contentType = 'application/json',
overrideHeaders: { [key: string]: string | number } = {}
): Promise<{ result: T; etag: string }> {
const headers = {
...this.getHeaders(accessToken, contentType),
...overrideHeaders
}
return this.httpClient
.post<T>(url, data, { headers, withCredentials: true })
.then(async (response) => {
if (response.status === 302) {
return await this.get(
response.headers['location'],
undefined,
contentType,
overrideHeaders
)
}
throwIfError(response)
return this.parseResponse<T>(response)
})
.catch(async (e) => {
return await this.handleError(e, () =>
this.post<T>(url, data, accessToken, contentType, overrideHeaders)
)
})
}
}

View File

@@ -1,3 +1,7 @@
/**
* @jest-environment jsdom
*/
import { FileUploader } from '../FileUploader' import { FileUploader } from '../FileUploader'
import { UploadFile } from '../types' import { UploadFile } from '../types'
import { RequestClient } from '../request/RequestClient' import { RequestClient } from '../request/RequestClient'
@@ -35,41 +39,40 @@ describe('FileUploader', () => {
new RequestClient('https://sample.server.com') new RequestClient('https://sample.server.com')
) )
it('should upload successfully', async (done) => { it('should upload successfully', async () => {
const sasJob = 'test/upload' const sasJob = 'test/upload'
const { files, params } = prepareFilesAndParams() const { files, params } = prepareFilesAndParams()
mockedAxios.post.mockImplementation(() => mockedAxios.post.mockImplementation(() =>
Promise.resolve({ data: sampleResponse }) Promise.resolve({ data: sampleResponse })
) )
fileUploader.uploadFile(sasJob, files, params).then((res: any) => { const res = await fileUploader.uploadFile(sasJob, files, params)
expect(res).toEqual(JSON.parse(sampleResponse))
done() expect(res).toEqual(JSON.parse(sampleResponse))
})
}) })
it('should an error when no files are provided', async (done) => { it('should an error when no files are provided', async () => {
const sasJob = 'test/upload' const sasJob = 'test/upload'
const files: UploadFile[] = [] const files: UploadFile[] = []
const params = { table: 'libtable' } const params = { table: 'libtable' }
fileUploader.uploadFile(sasJob, files, params).catch((err: any) => { const err = await fileUploader
expect(err.error.message).toEqual('At least one file must be provided.') .uploadFile(sasJob, files, params)
done() .catch((err: any) => err)
}) expect(err.error.message).toEqual('At least one file must be provided.')
}) })
it('should throw an error when no sasJob is provided', async (done) => { it('should throw an error when no sasJob is provided', async () => {
const sasJob = '' const sasJob = ''
const { files, params } = prepareFilesAndParams() const { files, params } = prepareFilesAndParams()
fileUploader.uploadFile(sasJob, files, params).catch((err: any) => { const err = await fileUploader
expect(err.error.message).toEqual('sasJob must be provided.') .uploadFile(sasJob, files, params)
done() .catch((err: any) => err)
}) expect(err.error.message).toEqual('sasJob must be provided.')
}) })
it('should throw an error when login is required', async (done) => { it('should throw an error when login is required', async () => {
mockedAxios.post.mockImplementation(() => mockedAxios.post.mockImplementation(() =>
Promise.resolve({ data: '<form action="Logon">' }) Promise.resolve({ data: '<form action="Logon">' })
) )
@@ -77,15 +80,13 @@ describe('FileUploader', () => {
const sasJob = 'test' const sasJob = 'test'
const { files, params } = prepareFilesAndParams() const { files, params } = prepareFilesAndParams()
fileUploader.uploadFile(sasJob, files, params).catch((err: any) => { const err = await fileUploader
expect(err.error.message).toEqual( .uploadFile(sasJob, files, params)
'You must be logged in to upload a file.' .catch((err: any) => err)
) expect(err.error.message).toEqual('You must be logged in to upload a file.')
done()
})
}) })
it('should throw an error when invalid JSON is returned by the server', async (done) => { it('should throw an error when invalid JSON is returned by the server', async () => {
mockedAxios.post.mockImplementation(() => mockedAxios.post.mockImplementation(() =>
Promise.resolve({ data: '{invalid: "json"' }) Promise.resolve({ data: '{invalid: "json"' })
) )
@@ -93,13 +94,13 @@ describe('FileUploader', () => {
const sasJob = 'test' const sasJob = 'test'
const { files, params } = prepareFilesAndParams() const { files, params } = prepareFilesAndParams()
fileUploader.uploadFile(sasJob, files, params).catch((err: any) => { const err = await fileUploader
expect(err.error.message).toEqual('File upload request failed.') .uploadFile(sasJob, files, params)
done() .catch((err: any) => err)
}) expect(err.error.message).toEqual('File upload request failed.')
}) })
it('should throw an error when the server request fails', async (done) => { it('should throw an error when the server request fails', async () => {
mockedAxios.post.mockImplementation(() => mockedAxios.post.mockImplementation(() =>
Promise.reject({ data: '{message: "Server error"}' }) Promise.reject({ data: '{message: "Server error"}' })
) )
@@ -107,10 +108,9 @@ describe('FileUploader', () => {
const sasJob = 'test' const sasJob = 'test'
const { files, params } = prepareFilesAndParams() const { files, params } = prepareFilesAndParams()
fileUploader.uploadFile(sasJob, files, params).catch((err: any) => { const err = await fileUploader
expect(err.error.message).toEqual('File upload request failed.') .uploadFile(sasJob, files, params)
.catch((err: any) => err)
done() expect(err.error.message).toEqual('File upload request failed.')
})
}) })
}) })

View File

@@ -14,7 +14,7 @@ describe('FolderOperations', () => {
beforeEach(() => {}) beforeEach(() => {})
it('should move and rename folder', async (done) => { it('should move and rename folder', async () => {
mockFetchResponse(false) mockFetchResponse(false)
let res: any = await sasViyaApiClient.moveFolder( let res: any = await sasViyaApiClient.moveFolder(
@@ -26,11 +26,9 @@ describe('FolderOperations', () => {
expect(res.folder.name).toEqual('newName') expect(res.folder.name).toEqual('newName')
expect(res.folder.parentFolderUri.split('=')[1]).toEqual('/Test/toFolder') expect(res.folder.parentFolderUri.split('=')[1]).toEqual('/Test/toFolder')
done()
}) })
it('should move and keep the name of folder', async (done) => { it('should move and keep the name of folder', async () => {
mockFetchResponse(true) mockFetchResponse(true)
let res: any = await sasViyaApiClient.moveFolder( let res: any = await sasViyaApiClient.moveFolder(
@@ -42,11 +40,9 @@ describe('FolderOperations', () => {
expect(res.folder.name).toEqual('oldName') expect(res.folder.name).toEqual('oldName')
expect(res.folder.parentFolderUri.split('=')[1]).toEqual('/Test/toFolder') expect(res.folder.parentFolderUri.split('=')[1]).toEqual('/Test/toFolder')
done()
}) })
it('should only rename folder', async (done) => { it('should only rename folder', async () => {
mockFetchResponse(false) mockFetchResponse(false)
let res: any = await sasViyaApiClient.moveFolder( let res: any = await sasViyaApiClient.moveFolder(
@@ -58,8 +54,6 @@ describe('FolderOperations', () => {
expect(res.folder.name).toEqual('newName') expect(res.folder.name).toEqual('newName')
expect(res.folder.parentFolderUri.split('=')[1]).toEqual('/Test/toFolder') expect(res.folder.parentFolderUri.split('=')[1]).toEqual('/Test/toFolder')
done()
}) })
}) })

View File

@@ -1,6 +1,6 @@
import { parseGeneratedCode } from '../../utils/index' import { parseGeneratedCode } from '../../utils/index'
it('should parse generated code', async (done) => { it('should parse generated code', () => {
expect(sampleResponse).toBeTruthy() expect(sampleResponse).toBeTruthy()
const parsedGeneratedCode = parseGeneratedCode(sampleResponse) const parsedGeneratedCode = parseGeneratedCode(sampleResponse)
@@ -15,8 +15,6 @@ it('should parse generated code', async (done) => {
expect(generatedCodeLines[2].startsWith('MPRINT(MM_WEBOUT)')).toBeTruthy() expect(generatedCodeLines[2].startsWith('MPRINT(MM_WEBOUT)')).toBeTruthy()
expect(generatedCodeLines[3].startsWith('MPRINT(MM_WEBRIGHT)')).toBeTruthy() expect(generatedCodeLines[3].startsWith('MPRINT(MM_WEBRIGHT)')).toBeTruthy()
expect(generatedCodeLines[4].startsWith('MPRINT(MM_WEBOUT)')).toBeTruthy() expect(generatedCodeLines[4].startsWith('MPRINT(MM_WEBOUT)')).toBeTruthy()
done()
}) })
/* tslint:disable */ /* tslint:disable */

View File

@@ -1,6 +1,6 @@
import { parseSourceCode } from '../../utils/index' import { parseSourceCode } from '../../utils/index'
it('should parse SAS9 source code', async (done) => { it('should parse SAS9 source code', async () => {
expect(sampleResponse).toBeTruthy() expect(sampleResponse).toBeTruthy()
const parsedSourceCode = parseSourceCode(sampleResponse) const parsedSourceCode = parseSourceCode(sampleResponse)
@@ -15,8 +15,6 @@ it('should parse SAS9 source code', async (done) => {
expect(sourceCodeLines[2].startsWith('8')).toBeTruthy() expect(sourceCodeLines[2].startsWith('8')).toBeTruthy()
expect(sourceCodeLines[3].startsWith('9')).toBeTruthy() expect(sourceCodeLines[3].startsWith('9')).toBeTruthy()
expect(sourceCodeLines[4].startsWith('10')).toBeTruthy() expect(sourceCodeLines[4].startsWith('10')).toBeTruthy()
done()
}) })
/* tslint:disable */ /* tslint:disable */

8
src/types/File.ts Normal file
View File

@@ -0,0 +1,8 @@
import { Link } from './Link'
export interface File {
id: string
name: string
parentUri: string
links: Link[]
}

View File

@@ -0,0 +1,9 @@
export class SAS9AuthError extends Error {
constructor() {
super(
'The credentials you provided cannot be authenticated. Please provide a valid set of credentials.'
)
this.name = 'AuthorizeError'
Object.setPrototypeOf(this, SAS9AuthError.prototype)
}
}

View File

@@ -1,6 +1,7 @@
export * from './Context' export * from './Context'
export * from './CsrfToken' export * from './CsrfToken'
export * from './Folder' export * from './Folder'
export * from './File'
export * from './Job' export * from './Job'
export * from './JobDefinition' export * from './JobDefinition'
export * from './JobResult' export * from './JobResult'

View File

@@ -2,20 +2,30 @@ const path = require('path')
const webpack = require('webpack') const webpack = require('webpack')
const terserPlugin = require('terser-webpack-plugin') const terserPlugin = require('terser-webpack-plugin')
const defaultPlugins = [
new webpack.ContextReplacementPlugin(/moment[\/\\]locale$/, /en/),
new webpack.SourceMapDevToolPlugin({
filename: null,
exclude: [/node_modules/],
test: /\.ts($|\?)/i
})
]
const optimization = {
minimize: true,
minimizer: [
new terserPlugin({
parallel: true,
terserOptions: {}
})
]
}
const browserConfig = { const browserConfig = {
entry: './src/index.ts', entry: './src/index.ts',
devtool: 'inline-source-map', devtool: 'inline-source-map',
mode: 'production', mode: 'production',
optimization: { optimization: optimization,
minimizer: [
new terserPlugin({
cache: true,
parallel: true,
sourceMap: true,
terserOptions: {}
})
]
},
module: { module: {
rules: [ rules: [
{ {
@@ -36,17 +46,26 @@ const browserConfig = {
library: 'SASjs' library: 'SASjs'
}, },
plugins: [ plugins: [
new webpack.ContextReplacementPlugin(/moment[\/\\]locale$/, /en/), ...defaultPlugins,
new webpack.SourceMapDevToolPlugin({ new webpack.ProvidePlugin({
filename: null, process: 'process/browser'
exclude: [/node_modules/],
test: /\.ts($|\?)/i
}) })
] ]
} }
const browserConfigWithoutProcessPlugin = {
entry: browserConfig.entry,
devtool: browserConfig.devtool,
mode: browserConfig.mode,
optimization: optimization,
module: browserConfig.module,
resolve: browserConfig.resolve,
output: browserConfig.output,
plugins: defaultPlugins
}
const nodeConfig = { const nodeConfig = {
...browserConfig, ...browserConfigWithoutProcessPlugin,
target: 'node', target: 'node',
entry: './node/index.ts', entry: './node/index.ts',
output: { output: {