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

Compare commits

...

106 Commits

Author SHA1 Message Date
Yury Shkoda
1a0d62d8f3 Merge pull request #209 from sasjs/job-status-fix
feat(*): improved session and job logging
2021-01-12 17:53:25 +03:00
Yury Shkoda
8f4d1c7aea chore(*): improved session and job state logging 2021-01-12 17:26:57 +03:00
Yury Shkoda
5a2ee88cbc fix(job-status): print job status only if it changes 2021-01-11 13:52:38 +03:00
Allan Bowe
b23f199334 Merge pull request #203 from sasjs/relative-service-paths
fix(relative-paths): process relative and absolute paths the same way
2021-01-05 21:02:45 +01:00
Krishna Acondy
ed5dabee9f fix(relative-paths): process relative and absolute paths the same way 2021-01-05 19:37:40 +00:00
Allan Bowe
0c88c5a522 Merge pull request #201 from sasjs/services-subfolder
fix(deploy-service-pack): deploy services into 'services' subfolder
2021-01-05 18:23:52 +01:00
Krishna Acondy
640e7015c8 fix(*): deploy services into 'services' subfolder 2021-01-05 17:19:05 +00:00
Yury Shkoda
2fd306f435 Merge pull request #195 from sasjs/v2-bump
test(coverage): enabled gathering coverage
2020-12-30 15:31:01 +03:00
Yury Shkoda
e3f779dbd1 test(coverage): enabled gathering coverage
BREAKING CHANGE: SASjs Adapter 2.0 - Electric Boogaloo
2020-12-30 15:04:59 +03:00
Yury Shkoda
1064f11663 Merge pull request #177 from sasjs/cli-issue-182
feat(context): moved context related logic to ContextManager
2020-12-30 13:24:34 +03:00
Yury Shkoda
46abc54cb0 chore(default-contexts): made default contexts private 2020-12-30 13:13:28 +03:00
Yury Shkoda
2c808a937a docs(*): updated docs 2020-12-30 11:43:10 +03:00
Yury Shkoda
52cf9a420f style(*): fixed styling 2020-12-30 11:42:31 +03:00
Yury Shkoda
2d29be45f5 test(contextManager): added unit tests 2020-12-30 11:38:56 +03:00
Yury Shkoda
a44222c3ba refactor(contextManager): used helper methods 2020-12-30 11:38:10 +03:00
Yury Shkoda
efc82101c1 chore(context-delete): renamed method 2020-12-30 11:36:10 +03:00
Yury Shkoda
09ce2fb6be chore(context-delete): renamed method 2020-12-30 11:35:41 +03:00
Yury Shkoda
a383388e54 feat(context-delete): restricted system compute context deletion 2020-12-29 11:42:14 +03:00
Yury Shkoda
362078b12c docs(context): updated docs 2020-12-29 11:12:33 +03:00
Yury Shkoda
9d0c3410a5 feat(context-edit): restricted editing system compute contexts 2020-12-29 11:11:26 +03:00
Yury Shkoda
dfb9c28f3a feat(createComputeContext): added throw an error if context already exists 2020-12-29 10:00:49 +03:00
Yury Shkoda
8d155283dd fix(context): fixed executeScript method 2020-12-24 13:54:19 +03:00
Yury Shkoda
d991ead86a Merge branch 'master' into cli-issue-182 2020-12-23 15:11:19 +03:00
Yury Shkoda
33a202fa1c Merge pull request #189 from sasjs/job-pid
feat(job-pid): added print PID of the executed job
2020-12-23 12:06:24 +03:00
Yury Shkoda
ff5463a84c chore(example): fixed example 2020-12-23 09:24:15 +03:00
Yury Shkoda
aa7c3ae4a9 docs(pid): updated docs 2020-12-23 09:18:49 +03:00
Yury Shkoda
2e66bfde4b chore(pid): made printing PID optional 2020-12-23 09:17:40 +03:00
Yury Shkoda
16e21adb20 chore: updated docs 2020-12-22 16:58:14 +03:00
Yury Shkoda
01c5682c3d Merge branch 'job-pid' of https://github.com/sasjs/adapter into job-pid 2020-12-22 16:46:48 +03:00
Yury Shkoda
cfc8ff2837 chore: added 'Assign Reviewer' CI step 2020-12-22 16:46:16 +03:00
Yury Shkoda
edf25b471a chore: added 'Assign Reviewer' CI step 2020-12-22 16:41:22 +03:00
Yury Shkoda
bb894e6107 feat(job-pid): added print PID of the executed job 2020-12-22 16:21:24 +03:00
Yury Shkoda
6b3a0cdb13 wip(context): created ContextManager 2020-12-21 14:51:01 +03:00
Yury Shkoda
8c98a26160 chore(SessionManager): removed unnecessary comment 2020-12-11 08:50:44 +03:00
Yury Shkoda
bcd9310f26 feat(context): add public method createLauncherContext 2020-12-09 16:51:07 +03:00
Yury Shkoda
57e9b67207 feat(context): added create launcher context method 2020-12-09 16:41:29 +03:00
Yury Shkoda
7bf53858f0 Merge pull request #176 from sasjs/cli-issue-317
fix(session-status): fixed stop polling session status
2020-12-09 12:56:17 +03:00
Yury Shkoda
02780d0bcd fix(session-status): fixed stop polling session status 2020-12-09 12:45:37 +03:00
Allan Bowe
6356aed06b Merge pull request #173 from sasjs/fix
fix: fetch all items of folder
2020-12-08 00:43:38 +01:00
Saad Jutt
69fd7b2cb5 fix: fetch all items of folder 2020-12-08 04:37:42 +05:00
Allan Bowe
5d1eed1494 Merge pull request #169 from sasjs/execScriptReturn
fix: executeScript return no log
2020-12-04 15:01:47 +01:00
Mihajlo Medjedovic
e2e2824f37 chore: cleanup 2020-12-03 13:05:02 +01:00
Mihajlo Medjedovic
d461135980 chore: job object rename 2020-12-03 12:29:50 +01:00
Mihajlo Medjedovic
65fbae7610 docs: generated 2020-12-03 12:26:23 +01:00
Mihajlo Medjedovic
761428502a fix: added debug override on executeScriptSASViya and added tests for same function 2020-12-03 12:25:42 +01:00
Mihajlo Medjedovic
6eb2ceaf53 test: fixed compute job 2020-12-02 20:48:31 +01:00
Mihajlo Medjedovic
66813b9824 fix: executeScript return no log 2020-12-02 18:18:56 +01:00
Krishna Acondy
140d8e4eac Merge pull request #165 from sasjs/debugIssue
fix: sasjsconfig.debug not passed in executeScriptSASViya function
2020-12-01 11:13:37 +00:00
Mihajlo Medjedovic
0d730e0576 Merge branch 'master' into debugIssue 2020-12-01 12:09:46 +01:00
Yury Shkoda
ca18fcecf0 Merge pull request #167 from sasjs/cli-issue-249
feat(pollJobState): added ability to configure poll options
2020-11-30 12:55:11 +03:00
Yury Shkoda
009069169f chore(pollJobState): updated docs and added note 2020-11-30 12:45:37 +03:00
Yury Shkoda
6d166efd11 feat(pollJobState): made pollOptions optional and updated docs 2020-11-30 12:27:09 +03:00
Yury Shkoda
1b117a67aa feat(pollJobState): added ability to configure poll options 2020-11-30 10:21:49 +03:00
Mihajlo Medjedovic
9037160362 fix: sasjsconfig.debug not passed in executeScriptSASViya function 2020-11-27 15:45:36 +01:00
Yury Shkoda
505d85c256 Merge pull request #163 from sasjs/context-fix
fix(context): fixed result parsing
2020-11-26 13:36:07 +03:00
Yury Shkoda
71a3fe04a0 fix(context): fixed result parsing 2020-11-26 13:27:35 +03:00
Krishna Acondy
79bb27524c fix(package): include node version of adapter in package 2020-11-26 08:27:55 +00:00
Krishna Acondy
9651b7adb4 Merge pull request #161 from sasjs/fix-build-process
fix(*): export type declarations for node version of adapter
2020-11-25 21:06:20 +00:00
Krishna Acondy
59e5bec731 chore(*): fix PR template 2020-11-25 20:29:43 +00:00
Krishna Acondy
182e66216f fix(*): export type declarations for node version of adapter 2020-11-25 20:23:59 +00:00
Krishna Acondy
2408fd091e Merge pull request #155 from sasjs/dependabot/npm_and_yarn/semantic-release-17.3.0
chore(deps-dev): bump semantic-release from 17.2.3 to 17.3.0
2020-11-25 11:23:15 +00:00
dependabot-preview[bot]
0e38a24664 chore(deps-dev): bump semantic-release from 17.2.3 to 17.3.0
Bumps [semantic-release](https://github.com/semantic-release/semantic-release) from 17.2.3 to 17.3.0.
- [Release notes](https://github.com/semantic-release/semantic-release/releases)
- [Commits](https://github.com/semantic-release/semantic-release/compare/v17.2.3...v17.3.0)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2020-11-25 11:20:35 +00:00
Krishna Acondy
aa643d1782 Merge pull request #159 from sasjs/dependabot/npm_and_yarn/highlight.js-10.4.0
chore(deps): [security] bump highlight.js from 10.1.1 to 10.4.0
2020-11-25 11:18:17 +00:00
dependabot-preview[bot]
cdc91e9cda chore(deps): [security] bump highlight.js from 10.1.1 to 10.4.0
Bumps [highlight.js](https://github.com/highlightjs/highlight.js) from 10.1.1 to 10.4.0. **This update includes a security fix.**
- [Release notes](https://github.com/highlightjs/highlight.js/releases)
- [Changelog](https://github.com/highlightjs/highlight.js/blob/master/CHANGES.md)
- [Commits](https://github.com/highlightjs/highlight.js/compare/10.1.1...10.4.0)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2020-11-25 11:16:14 +00:00
Krishna Acondy
3f0590e0fe Merge pull request #158 from sasjs/issue157
Issue157
2020-11-25 11:14:39 +00:00
Mihajlo Medjedovic
5efb294ff2 fix: tests updated 2020-11-25 12:03:07 +01:00
Mihajlo Medjedovic
011e2d83dc fix: computeApi debug override 2020-11-24 15:19:19 +01:00
Mihajlo Medjedovic
e36b511530 test: make error and capture log, fixed 2020-11-24 13:02:17 +01:00
Mihajlo Medjedovic
b6a2a85d1d fix: compute api log not appended to sasjs requests 2020-11-24 11:59:17 +01:00
Krishna Acondy
f1cceeb5e6 Merge pull request #156 from sasjs/fix
fix(job execute): details for failing job
2020-11-24 07:46:32 +00:00
Krishna Acondy
6fee2548fd fix(job-failure): return original job object when a job has failed 2020-11-24 07:44:11 +00:00
Saad Jutt
91005066cf fix(job execute): details for failing job 2020-11-24 04:16:18 +05:00
Krishna Acondy
e1f17ef47d Merge pull request #127 from sasjs/dependabot/npm_and_yarn/terser-webpack-plugin-4.2.3
chore(deps-dev): bump terser-webpack-plugin from 4.2.2 to 4.2.3
2020-11-20 09:07:14 +00:00
dependabot-preview[bot]
8a40071c35 chore(deps-dev): bump terser-webpack-plugin from 4.2.2 to 4.2.3
Bumps [terser-webpack-plugin](https://github.com/webpack-contrib/terser-webpack-plugin) from 4.2.2 to 4.2.3.
- [Release notes](https://github.com/webpack-contrib/terser-webpack-plugin/releases)
- [Changelog](https://github.com/webpack-contrib/terser-webpack-plugin/blob/master/CHANGELOG.md)
- [Commits](https://github.com/webpack-contrib/terser-webpack-plugin/compare/v4.2.2...v4.2.3)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2020-11-20 09:01:25 +00:00
Krishna Acondy
430957eb3d Merge pull request #132 from sasjs/dependabot/npm_and_yarn/npm-user-validate-1.0.1
chore(deps): [security] bump npm-user-validate from 1.0.0 to 1.0.1
2020-11-20 08:58:57 +00:00
dependabot-preview[bot]
25874be679 chore(deps): [security] bump npm-user-validate from 1.0.0 to 1.0.1
Bumps [npm-user-validate](https://github.com/npm/npm-user-validate) from 1.0.0 to 1.0.1. **This update includes a security fix.**
- [Release notes](https://github.com/npm/npm-user-validate/releases)
- [Commits](https://github.com/npm/npm-user-validate/compare/v1.0.0...v1.0.1)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2020-11-20 08:57:20 +00:00
Krishna Acondy
ed8440434f Merge pull request #135 from sasjs/dependabot/npm_and_yarn/types/jest-26.0.15
chore(deps-dev): bump @types/jest from 26.0.14 to 26.0.15
2020-11-20 08:55:42 +00:00
dependabot-preview[bot]
0f9884c1b6 chore(deps-dev): bump @types/jest from 26.0.14 to 26.0.15
Bumps [@types/jest](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/jest) from 26.0.14 to 26.0.15.
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/jest)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2020-11-20 08:46:00 +00:00
Krishna Acondy
d126a05347 Merge pull request #146 from sasjs/dependabot/npm_and_yarn/webpack-cli-4.2.0
chore(deps-dev): bump webpack-cli from 3.3.12 to 4.2.0
2020-11-20 08:43:50 +00:00
dependabot-preview[bot]
3e26bbbbba chore(deps-dev): bump webpack-cli from 3.3.12 to 4.2.0
Bumps [webpack-cli](https://github.com/webpack/webpack-cli) from 3.3.12 to 4.2.0.
- [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/v3.3.12...webpack-cli@4.2.0)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2020-11-20 08:34:49 +00:00
Krishna Acondy
982cc8f7a0 Merge pull request #149 from sasjs/dependabot/npm_and_yarn/ts-loader-8.0.11
chore(deps-dev): bump ts-loader from 8.0.4 to 8.0.11
2020-11-20 08:32:39 +00:00
dependabot-preview[bot]
d1770698e0 chore(deps-dev): bump ts-loader from 8.0.4 to 8.0.11
Bumps [ts-loader](https://github.com/TypeStrong/ts-loader) from 8.0.4 to 8.0.11.
- [Release notes](https://github.com/TypeStrong/ts-loader/releases)
- [Changelog](https://github.com/TypeStrong/ts-loader/blob/master/CHANGELOG.md)
- [Commits](https://github.com/TypeStrong/ts-loader/compare/8.0.4...v8.0.11)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2020-11-20 08:29:41 +00:00
Krishna Acondy
b78e8617c4 Merge pull request #151 from sasjs/dependabot/npm_and_yarn/semantic-release-17.2.3
[security] chore(deps-dev): bump semantic-release from 17.1.2 to 17.2.3
2020-11-20 08:27:32 +00:00
dependabot-preview[bot]
3ce9ca0986 chore(deps-dev): bump semantic-release from 17.1.2 to 17.2.3
Bumps [semantic-release](https://github.com/semantic-release/semantic-release) from 17.1.2 to 17.2.3.
- [Release notes](https://github.com/semantic-release/semantic-release/releases)
- [Commits](https://github.com/semantic-release/semantic-release/compare/v17.1.2...v17.2.3)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2020-11-20 08:19:07 +00:00
Yury Shkoda
04d17c3680 Merge pull request #153 from sasjs/context-issue
fix(context): fixed log parsing
2020-11-19 16:53:48 +03:00
Yury Shkoda
d26e15f91c Merge branch 'master' into context-issue 2020-11-19 16:43:30 +03:00
Yury Shkoda
83c46091b3 fix(context): fixed log parsing 2020-11-19 16:39:47 +03:00
Krishna Acondy
d640d7c040 Merge pull request #152 from sasjs/issue117
fix: viya login issue
2020-11-18 17:16:26 +00:00
Mihajlo Medjedovic
c934eb2b08 test: added test for multiple login attempts 2020-11-18 14:13:13 +01:00
Mihajlo Medjedovic
24dd5e32ad style: lint 2020-11-18 13:10:29 +01:00
Mihajlo Medjedovic
a23103b2c3 fix: viya login issue 2020-11-18 13:09:49 +01:00
Yury Shkoda
35aa4235e4 Merge pull request #148 from sasjs/issue113
feat: service not found error handling
2020-11-16 09:54:03 +03:00
Mihajlo Medjedovic
e9be1cf99a fix: service not found error handling 2020-11-12 16:03:32 +01:00
Mihajlo Medjedovic
c7b0821081 style: lint 2020-11-09 18:24:10 +01:00
Mihajlo Medjedovic
4a4618dd32 feat: service not found error handling for SAS9 2020-11-09 18:19:39 +01:00
Krishna Acondy
d223e83c60 Merge pull request #142 from sasjs/issue138
fix(file-uploader): handle errors during file upload
2020-11-02 14:54:58 +00:00
Krishna Acondy
d1f1a20126 chore(file-uploader): move uploader to describe scope 2020-11-02 09:29:33 +00:00
Krishna Acondy
4b89e3762f chore(file-uploader): remove duplication 2020-11-02 08:54:26 +00:00
Krishna Acondy
bc110288de chore(file-uploader): improve mocking of fetch, add tests for all error scenarios 2020-11-02 08:51:27 +00:00
Krishna Acondy
e94e16b52c chore(*): fix linting errors 2020-11-02 07:55:48 +00:00
Krishna Acondy
76aacee016 Merge branch 'master' into issue138 2020-11-02 07:42:39 +00:00
Mihajlo Medjedovic
1a3bd5d1f5 chore: lint 2020-10-30 16:13:03 +01:00
Mihajlo Medjedovic
3f6e89d716 fix: file uploader error handling and tests 2020-10-30 16:11:50 +01:00
Mihajlo Medjedovic
7279c23fe2 fix: FIleUploader added catch 2020-10-27 14:50:05 +01:00
Mihajlo Medjedovic
80707d77d9 gitfe Merge branches 'errorResponse' and 'master' of github.com:sasjs/adapter 2020-10-27 14:40:41 +01:00
Mihajlo Medjedovic
3f796b300d fix: ErrorResponse body changed to error 2020-10-07 11:15:00 +02:00
81 changed files with 22723 additions and 4005 deletions

2
.env.example Normal file
View File

@@ -0,0 +1,2 @@
SERVER_URL=https://server.com
DEFAULT_COMPUTE_CONTEXT=SAS Job Execution compute context

9
.github/reviewer-lottery.yml vendored Normal file
View File

@@ -0,0 +1,9 @@
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

13
.github/workflows/assign-reviewer.yml vendored Normal file
View File

@@ -0,0 +1,13 @@
name: 'Assign Reviewer'
on:
pull_request:
types: [opened]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: uesteibar/reviewer-lottery@v1
with:
repo-token: ${{ secrets.GH_TOKEN }}

4
.gitignore vendored
View File

@@ -1,2 +1,6 @@
node_modules
build
.env
/coverage

View File

@@ -14,4 +14,5 @@ What code changes have been made to achieve the intent.
- [ ] Code is formatted correctly (`npm run lint:fix`).
- [ ] All unit tests are passing (`npm test`).
- [ ] All `sasjs-cli` unit tests are passing (`npm test`).
- [ ] All `sasjs-tests` are passing (instructions available [here](https://github.com/sasjs/adapter/blob/master/sasjs-tests/README.md)).

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

1818
docs/classes/root.sasjs.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

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

129
docs/modules/root.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

5589
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -2,12 +2,12 @@
"name": "@sasjs/adapter",
"description": "JavaScript adapter for SAS",
"scripts": {
"build": "rimraf build && webpack",
"build": "rimraf build && rimraf node && mkdir node && cp -r src/* node && webpack && rimraf build/src && rimraf node",
"package:lib": "npm run build && cp ./package.json build && cd build && npm version \"5.0.0\" && npm pack",
"publish:lib": "npm run build && cd build && npm publish",
"lint:fix": "npx prettier --write 'src/**/*.{ts,tsx,js,jsx,html,css,sass,less,json,yml,md,graphql}'",
"lint": "npx prettier --check 'src/**/*.{ts,tsx,js,jsx,html,css,sass,less,json,yml,md,graphql}'",
"test": "jest",
"test": "jest --coverage",
"prepublishOnly": "cp -r ./build/* . && rm -rf ./build",
"postpublish": "git clean -fd",
"semantic-release": "semantic-release",
@@ -37,15 +37,16 @@
"license": "ISC",
"devDependencies": {
"@types/isomorphic-fetch": "0.0.35",
"@types/jest": "^26.0.14",
"@types/jest": "^26.0.15",
"cp": "^0.2.0",
"dotenv": "^8.2.0",
"jest": "^25.5.4",
"path": "^0.12.7",
"rimraf": "^3.0.2",
"semantic-release": "^17.1.2",
"terser-webpack-plugin": "^4.2.2",
"semantic-release": "^17.3.0",
"terser-webpack-plugin": "^4.2.3",
"ts-jest": "^25.5.1",
"ts-loader": "^8.0.4",
"ts-loader": "^8.0.11",
"tslint": "^6.1.3",
"tslint-config-prettier": "^1.18.0",
"typedoc": "^0.17.8",
@@ -53,10 +54,11 @@
"typedoc-plugin-external-module-name": "^4.0.3",
"typescript": "^3.9.7",
"webpack": "^4.44.2",
"webpack-cli": "^3.3.12"
"webpack-cli": "^4.2.0"
},
"main": "index.js",
"dependencies": {
"@sasjs/utils": "^1.5.0",
"es6-promise": "^4.2.8",
"form-data": "^3.0.0",
"isomorphic-fetch": "^2.2.1"

View File

@@ -1357,9 +1357,9 @@
"integrity": "sha512-shAmDyaQC4H92APFoIaVDHCx5bStIocgvbwQyxPRrbUY20V1EYTbSDchWbuwlMG3V17cprZhA6+78JfB+3DTPw=="
},
"@sasjs/adapter": {
"version": "1.12.0",
"resolved": "https://registry.npmjs.org/@sasjs/adapter/-/adapter-1.12.0.tgz",
"integrity": "sha512-0uGQH9ynomWzdBaEujEtcR38q6V7LCgG0mrb1Wellv6cC/IHD3j6WfeZZAgtiMPeOSJjbCDBOlVnzC2TlBqJFw==",
"version": "1.18.3",
"resolved": "https://registry.npmjs.org/@sasjs/adapter/-/adapter-1.18.3.tgz",
"integrity": "sha512-wzDFJRyt2dXFeQP+JzqRGunYUbujrAbU/Jc4IWg5URsCkGAzwsNl/4G0xJVbqOTy1MvoZ431rfCnvhkUlg7D3Q==",
"requires": {
"es6-promise": "^4.2.8",
"form-data": "^3.0.0",

View File

@@ -4,7 +4,7 @@
"homepage": ".",
"private": true,
"dependencies": {
"@sasjs/adapter": "^1.12.0",
"@sasjs/adapter": "^1.18.2",
"@sasjs/test-framework": "^1.4.0",
"@testing-library/jest-dom": "^4.2.4",
"@testing-library/react": "^9.5.0",

View File

@@ -1,9 +0,0 @@
import React from "react";
import { render } from "@testing-library/react";
import App from "./App";
test("renders learn react link", () => {
const { getByText } = render(<App />);
const linkElement = getByText(/learn react/i);
expect(linkElement).toBeInTheDocument();
});

View File

@@ -3,12 +3,12 @@ import { TestSuite } from "@sasjs/test-framework";
const defaultConfig: SASjsConfig = {
serverUrl: window.location.origin,
pathSAS9: "/SASStoredProcess/do",
pathSASViya: "/SASJobExecution",
appLoc: "/Public/seedapp",
pathSAS9: '/SASStoredProcess/do',
pathSASViya: '/SASJobExecution',
appLoc: '/Public/seedapp',
serverType: ServerType.SASViya,
debug: true,
contextName: "SAS Job Execution compute context",
debug: false,
contextName: 'SAS Job Execution compute context',
useComputeApi: false
};
@@ -37,6 +37,17 @@ export const basicTests = (
assertion: (response: any) =>
response && response.isLoggedIn && response.userName === userName
},
{
title: "Multiple Log in attempts",
description: "Should fail on first attempt and should log the user in on second attempt",
test: async () => {
await adapter.logOut()
await adapter.logIn('invalid', 'invalid')
return adapter.logIn(userName, password)
},
assertion: (response: any) =>
response && response.isLoggedIn && response.userName === userName
},
{
title: "Default config",
description:
@@ -46,6 +57,7 @@ export const basicTests = (
},
assertion: (sasjsInstance: SASjs) => {
const sasjsConfig = sasjsInstance.getSasjsConfig();
return (
sasjsConfig.serverUrl === defaultConfig.serverUrl &&
sasjsConfig.pathSAS9 === defaultConfig.pathSAS9 &&

View File

@@ -25,17 +25,71 @@ export const computeTests = (adapter: SASjs): TestSuite => ({
},
assertion: (res: any) => {
const expectedProperties = ["id", "state", "creationTimeStamp", "jobConditionCode"]
return validate(expectedProperties, res);
return validate(expectedProperties, res.result);
}
},
{
title: "Execute Script Viya - complete job",
description: "Should execute sas file and return log",
test: () => {
const fileLines = [
`data;`,
`do x=1 to 100;`,
`output;`,
`end;`,
`run;`
]
return adapter.executeScriptSASViya(
'sasCode.sas',
fileLines,
'SAS Studio compute context',
undefined,
true
)
},
assertion: (res: any) => {
const expectedLogContent = `1 data;\\n2 do x=1 to 100;\\n3 output;\\n4 end;\\n5 run;\\n\\n`
return validateLog(expectedLogContent, res.log);
}
},
{
title: "Execute Script Viya - failed job",
description: "Should execute sas file and return log",
test: () => {
const fileLines = [
`%abort;`
]
return adapter.executeScriptSASViya(
'sasCode.sas',
fileLines,
'SAS Studio compute context',
undefined,
true
).catch((err: any) => err )
},
assertion: (res: any) => {
const expectedLogContent = `1 %abort;\\nERROR: The %ABORT statement is not valid in open code.\\n`
return validateLog(expectedLogContent, res.log);
}
}
]
});
const validateLog = (text: string, log: string): boolean => {
const isValid = JSON.stringify(log).includes(text)
return isValid
}
const validate = (expectedProperties: string[], data: any): boolean => {
const actualProperties = Object.keys(data);
const isValid = expectedProperties.every(
(property) => actualProperties.includes(property)
);
return isValid
const isValid = expectedProperties.every(
(property) => actualProperties.includes(property)
);
return isValid
}

View File

@@ -88,7 +88,7 @@ export const sendArrTests = (adapter: SASjs): TestSuite => ({
return adapter.request("common/sendArr", data).catch((e) => e);
},
assertion: (error: any) => {
return !!error && !!error.body && !!error.body.message;
return !!error && !!error.error && !!error.error.message;
}
},
{
@@ -185,7 +185,7 @@ export const sendObjTests = (adapter: SASjs): TestSuite => ({
};
return adapter.request("common/sendObj", invalidData).catch((e) => e);
},
assertion: (error: any) => !!error && !!error.body && !!error.body.message
assertion: (error: any) => !!error && !!error.error && !!error.error.message
},
{
title: "Single string value",
@@ -219,7 +219,7 @@ export const sendObjTests = (adapter: SASjs): TestSuite => ({
.catch((e) => e);
},
assertion: (error: any) => {
return !!error && !!error.body && !!error.body.message;
return !!error && !!error.error && !!error.error.message;
}
},
{

View File

@@ -23,22 +23,24 @@ export const sasjsRequestTests = (adapter: SASjs): TestSuite => ({
},
{
title: "Make error and capture log",
description: "Should make an error and capture log",
description: "Should make an error and capture log, in the same time it is testing if debug override is working",
test: async () => {
return new Promise(async (resolve, reject) => {
adapter
.request("common/makeErr", data)
.request("common/makeErr", data, {debug: true})
.then((res) => {
//no action here, this request must throw error
})
.catch((err) => {
let sasRequests = adapter.getSasRequests();
let makeErrRequest =
let makeErrRequest: any =
sasRequests.find((req) =>
req.serviceLink.includes("makeErr")
) || null;
resolve(!!makeErrRequest);
if (!makeErrRequest) resolve(false)
resolve(!!(makeErrRequest.logFile && makeErrRequest.logFile.length > 0));
});
});
},

538
src/ContextManager.ts Normal file
View File

@@ -0,0 +1,538 @@
import {
Context,
CsrfToken,
EditContextInput,
ContextAllAttributes
} from './types'
import { makeRequest, isUrl } from './utils'
import { SASViyaApiClient } from './SASViyaApiClient'
import { prefixMessage } from '@sasjs/utils/error'
export class ContextManager {
private defaultComputeContexts = [
'CAS Formats service compute context',
'Data Mining compute context',
'Import 9 service compute context',
'SAS Job Execution compute context',
'SAS Model Manager compute context',
'SAS Studio compute context',
'SAS Visual Forecasting compute context'
]
private defaultLauncherContexts = [
'CAS Formats service launcher context',
'Data Mining launcher context',
'Import 9 service launcher context',
'Job Flow Execution launcher context',
'SAS Job Execution launcher context',
'SAS Model Manager launcher context',
'SAS Studio launcher context',
'SAS Visual Forecasting launcher context'
]
private csrfToken: CsrfToken | null = null
get getDefaultComputeContexts() {
return this.defaultComputeContexts
}
get getDefaultLauncherContexts() {
return this.defaultLauncherContexts
}
constructor(
private serverUrl: string,
private setCsrfToken: (csrfToken: CsrfToken) => void
) {
if (serverUrl) isUrl(serverUrl)
}
public async getComputeContexts(accessToken?: string) {
const headers: any = {
'Content-Type': 'application/json'
}
if (accessToken) {
headers.Authorization = `Bearer ${accessToken}`
}
const { result: contexts } = await this.request<{ items: Context[] }>(
`${this.serverUrl}/compute/contexts?limit=10000`,
{ headers }
).catch((err) => {
throw prefixMessage(err, 'Error while getting compute contexts. ')
})
const contextsList = contexts && contexts.items ? contexts.items : []
return contextsList.map((context: any) => ({
createdBy: context.createdBy,
id: context.id,
name: context.name,
version: context.version,
attributes: {}
}))
}
public async getLauncherContexts(accessToken?: string) {
const headers: any = {
'Content-Type': 'application/json'
}
if (accessToken) {
headers.Authorization = `Bearer ${accessToken}`
}
const { result: contexts } = await this.request<{ items: Context[] }>(
`${this.serverUrl}/launcher/contexts?limit=10000`,
{ headers }
).catch((err) => {
throw prefixMessage(err, 'Error while getting launcher contexts. ')
})
const contextsList = contexts && contexts.items ? contexts.items : []
return contextsList.map((context: any) => ({
createdBy: context.createdBy,
id: context.id,
name: context.name,
version: context.version,
attributes: {}
}))
}
public async createComputeContext(
contextName: string,
launchContextName: string,
sharedAccountId: string,
autoExecLines: string[],
accessToken?: string,
authorizedUsers?: string[]
) {
this.validateContextName(contextName)
this.isDefaultContext(
contextName,
this.defaultComputeContexts,
`Compute context '${contextName}' already exists.`
)
const existingComputeContexts = await this.getComputeContexts(accessToken)
if (
existingComputeContexts.find((context) => context.name === contextName)
) {
throw new Error(`Compute context '${contextName}' already exists.`)
}
if (launchContextName) {
if (!this.defaultLauncherContexts.includes(launchContextName)) {
const launcherContexts = await this.getLauncherContexts(accessToken)
if (
!launcherContexts.find(
(context) => context.name === launchContextName
)
) {
const description = `The launcher context for ${launchContextName}`
const launchType = 'direct'
const newLauncherContext = await this.createLauncherContext(
launchContextName,
description,
launchType,
accessToken
).catch((err) => {
throw new Error(`Error while creating launcher context. ${err}`)
})
if (newLauncherContext && newLauncherContext.name) {
launchContextName = newLauncherContext.name
} else {
throw new Error('Error while creating launcher context.')
}
}
}
}
const headers: any = {
'Content-Type': 'application/json'
}
if (accessToken) {
headers.Authorization = `Bearer ${accessToken}`
}
let attributes = { reuseServerProcesses: true } as object
if (sharedAccountId)
attributes = { ...attributes, runServerAs: sharedAccountId }
const requestBody: any = {
name: contextName,
launchContext: {
contextName: launchContextName || ''
},
attributes
}
if (authorizedUsers && authorizedUsers.length) {
requestBody['authorizedUsers'] = authorizedUsers
} else {
requestBody['authorizeAllAuthenticatedUsers'] = true
}
if (autoExecLines) {
requestBody.environment = { autoExecLines }
}
const createContextRequest: RequestInit = {
method: 'POST',
headers,
body: JSON.stringify(requestBody)
}
const { result: context } = await this.request<Context>(
`${this.serverUrl}/compute/contexts`,
createContextRequest
).catch((err) => {
throw prefixMessage(err, 'Error while creating compute context. ')
})
return context
}
public async createLauncherContext(
contextName: string,
description: string,
launchType = 'direct',
accessToken?: string
) {
if (!contextName) {
throw new Error('Context name is required.')
}
this.isDefaultContext(
contextName,
this.defaultLauncherContexts,
`Launcher context '${contextName}' already exists.`
)
const existingLauncherContexts = await this.getLauncherContexts(accessToken)
if (
existingLauncherContexts.find((context) => context.name === contextName)
) {
throw new Error(`Launcher context '${contextName}' already exists.`)
}
const headers: any = {
'Content-Type': 'application/json'
}
if (accessToken) {
headers.Authorization = `Bearer ${accessToken}`
}
const requestBody: any = {
name: contextName,
description: description,
launchType
}
const createContextRequest: RequestInit = {
method: 'POST',
headers,
body: JSON.stringify(requestBody)
}
const { result: context } = await this.request<Context>(
`${this.serverUrl}/launcher/contexts`,
createContextRequest
).catch((err) => {
throw prefixMessage(err, 'Error while creating launcher context. ')
})
return context
}
public async editComputeContext(
contextName: string,
editedContext: EditContextInput,
accessToken?: string
) {
this.validateContextName(contextName)
this.isDefaultContext(
contextName,
this.defaultComputeContexts,
'Editing default SAS compute contexts is not allowed.',
true
)
const headers: any = {
'Content-Type': 'application/json'
}
if (accessToken) {
headers.Authorization = `Bearer ${accessToken}`
}
let originalContext
originalContext = await this.getComputeContextByName(
contextName,
accessToken
)
// Try to find context by id, when context name has been changed.
if (!originalContext) {
originalContext = await this.getComputeContextById(
editedContext.id!,
accessToken
)
}
const { result: context, etag } = await this.request<Context>(
`${this.serverUrl}/compute/contexts/${originalContext.id}`,
{
headers
}
).catch((err) => {
if (err && err.status === 404) {
throw new Error(
`The context '${contextName}' was not found on this server.`
)
}
throw err
})
// An If-Match header with the value of the last ETag for the context
// is required to be able to update it
// https://developer.sas.com/apis/rest/Compute/#update-a-context-definition
headers['If-Match'] = etag
const updateContextRequest: RequestInit = {
method: 'PUT',
headers,
body: JSON.stringify({
...context,
...editedContext,
attributes: { ...context.attributes, ...editedContext.attributes }
})
}
return await this.request<Context>(
`${this.serverUrl}/compute/contexts/${context.id}`,
updateContextRequest
)
}
public async getComputeContextByName(
contextName: string,
accessToken?: string
): Promise<Context> {
const headers: any = {
'Content-Type': 'application/json'
}
if (accessToken) {
headers.Authorization = `Bearer ${accessToken}`
}
const { result: contexts } = await this.request<{ items: Context[] }>(
`${this.serverUrl}/compute/contexts?filter=eq(name, "${contextName}")`,
{ headers }
).catch((err) => {
throw prefixMessage(err, 'Error while getting compute context by name. ')
})
if (!contexts || !(contexts.items && contexts.items.length)) {
throw new Error(
`The context '${contextName}' was not found at '${this.serverUrl}'.`
)
}
return contexts.items[0]
}
public async getComputeContextById(
contextId: string,
accessToken?: string
): Promise<ContextAllAttributes> {
const headers: any = {
'Content-Type': 'application/json'
}
if (accessToken) {
headers.Authorization = `Bearer ${accessToken}`
}
const { result: context } = await this.request<ContextAllAttributes>(
`${this.serverUrl}/compute/contexts/${contextId}`,
{ headers }
).catch((err) => {
throw prefixMessage(err, 'Error while getting compute context by id. ')
})
return context
}
public async getExecutableContexts(
executeScript: Function,
accessToken?: string
) {
const headers: any = {
'Content-Type': 'application/json'
}
if (accessToken) {
headers.Authorization = `Bearer ${accessToken}`
}
const { result: contexts } = await this.request<{ items: Context[] }>(
`${this.serverUrl}/compute/contexts?limit=10000`,
{ headers }
).catch((err) => {
throw prefixMessage(err, 'Error while fetching compute contexts.')
})
const contextsList = contexts.items || []
const executableContexts: any[] = []
const promises = contextsList.map((context: any) => {
const linesOfCode = ['%put &=sysuserid;']
return () =>
executeScript(
`test-${context.name}`,
linesOfCode,
context.name,
accessToken,
null,
false,
true,
true
).catch((err: any) => err)
})
let results: any[] = []
for (const promise of promises) results.push(await promise())
results.forEach((result: any, index: number) => {
if (result && result.log) {
try {
const resultParsed = result.log
let sysUserId = ''
const sysUserIdLog = resultParsed
.split('\n')
.find((line: string) => line.startsWith('SYSUSERID='))
if (sysUserIdLog) {
sysUserId = sysUserIdLog.replace('SYSUSERID=', '')
executableContexts.push({
createdBy: contextsList[index].createdBy,
id: contextsList[index].id,
name: contextsList[index].name,
version: contextsList[index].version,
attributes: {
sysUserId
}
})
}
} catch (error) {
throw error
}
}
})
return executableContexts
}
public async deleteComputeContext(contextName: string, accessToken?: string) {
this.validateContextName(contextName)
this.isDefaultContext(
contextName,
this.defaultComputeContexts,
'Deleting default SAS compute contexts is not allowed.',
true
)
const headers: any = {
'Content-Type': 'application/json'
}
if (accessToken) {
headers.Authorization = `Bearer ${accessToken}`
}
const context = await this.getComputeContextByName(contextName, accessToken)
const deleteContextRequest: RequestInit = {
method: 'DELETE',
headers
}
return await this.request<Context>(
`${this.serverUrl}/compute/contexts/${context.id}`,
deleteContextRequest
)
}
// TODO: implement editLauncherContext method
// TODO: implement deleteLauncherContext method
private async request<T>(
url: string,
options: RequestInit,
contentType: 'text' | 'json' = 'json'
) {
if (this.csrfToken) {
options.headers = {
...options.headers,
[this.csrfToken.headerName]: this.csrfToken.value
}
}
return await makeRequest<T>(
url,
options,
(token) => {
this.csrfToken = token
this.setCsrfToken(token)
},
contentType
).catch((err) => {
throw prefixMessage(
err,
'Error while making request in Context Manager. '
)
})
}
private validateContextName(name: string) {
if (!name) throw new Error('Context name is required.')
}
public isDefaultContext(
context: string,
defaultContexts: string[] = this.defaultComputeContexts,
errorMessage = '',
listDefaults = false
) {
if (defaultContexts.includes(context)) {
throw new Error(
`${errorMessage}${
listDefaults
? '\nDefault contexts:' +
defaultContexts.map((context, i) => `\n${i + 1}. ${context}`)
: ''
}`
)
}
}
}

View File

@@ -1,6 +1,7 @@
import { isLogInRequired, needsRetry, isUrl } from './utils'
import { CsrfToken } from './types/CsrfToken'
import { UploadFile } from './types/UploadFile'
import { ErrorResponse } from './types'
const requestRetryLimit = 5
@@ -18,29 +19,31 @@ export class FileUploader {
private retryCount = 0
public uploadFile(sasJob: string, files: UploadFile[], params: any) {
if (files?.length < 1)
throw new Error('At least one file must be provided.')
let paramsString = ''
for (let param in params) {
if (params.hasOwnProperty(param)) {
paramsString += `&${param}=${params[param]}`
}
}
const program = this.appLoc
? this.appLoc.replace(/\/?$/, '/') + sasJob.replace(/^\//, '')
: sasJob
const uploadUrl = `${this.serverUrl}${this.jobsPath}/?${
'_program=' + program
}${paramsString}`
const headers = {
'cache-control': 'no-cache'
}
return new Promise((resolve, reject) => {
if (files?.length < 1)
reject(new ErrorResponse('At least one file must be provided.'))
if (!sasJob || sasJob === '')
reject(new ErrorResponse('sasJob must be provided.'))
let paramsString = ''
for (let param in params) {
if (params.hasOwnProperty(param)) {
paramsString += `&${param}=${params[param]}`
}
}
const program = this.appLoc
? this.appLoc.replace(/\/?$/, '/') + sasJob.replace(/^\//, '')
: sasJob
const uploadUrl = `${this.serverUrl}${this.jobsPath}/?${
'_program=' + program
}${paramsString}`
const headers = {
'cache-control': 'no-cache'
}
const formData = new FormData()
for (let file of files) {
@@ -76,7 +79,7 @@ export class FileUploader {
})
.then((responseText) => {
if (isLogInRequired(responseText))
reject('You must be logged in to upload a file')
reject(new ErrorResponse('You must be logged in to upload a file.'))
if (needsRetry(responseText)) {
if (this.retryCount < requestRetryLimit) {
@@ -95,10 +98,18 @@ export class FileUploader {
try {
resolve(JSON.parse(responseText))
} catch (e) {
reject(e)
reject(
new ErrorResponse(
'Error while parsing json from upload response.',
e
)
)
}
}
})
.catch((err: any) => {
reject(new ErrorResponse('Upload request failed.', err))
})
})
}
}

View File

@@ -16,11 +16,14 @@ import {
Folder,
CsrfToken,
EditContextInput,
ErrorResponse,
JobDefinition
JobDefinition,
PollOptions
} from './types'
import { formatDataForRequest } from './utils/formatDataForRequest'
import { SessionManager } from './SessionManager'
import { ContextManager } from './ContextManager'
import { timestampToYYYYMMDDHHMMSS } from '@sasjs/utils/time'
import { Logger, LogLevel } from '@sasjs/utils/logger'
/**
* A client for interfacing with the SAS Viya REST API.
@@ -44,6 +47,7 @@ export class SASViyaApiClient {
this.contextName,
this.setCsrfToken
)
private contextManager = new ContextManager(this.serverUrl, this.setCsrfToken)
private folderMap = new Map<string, Job[]>()
public get debug() {
@@ -96,29 +100,23 @@ export class SASViyaApiClient {
* Returns all available compute contexts on this server.
* @param accessToken - an access token for an authorized user.
*/
public async getAllContexts(accessToken?: string) {
const headers: any = {
'Content-Type': 'application/json'
}
public async getComputeContexts(accessToken?: string) {
return await this.contextManager.getComputeContexts(accessToken)
}
if (accessToken) {
headers.Authorization = `Bearer ${accessToken}`
}
/**
* Returns default(system) compute contexts.
*/
public getDefaultComputeContexts() {
return this.contextManager.getDefaultComputeContexts
}
const { result: contexts } = await this.request<{ items: Context[] }>(
`${this.serverUrl}/compute/contexts?limit=10000`,
{ headers }
)
const contextsList = contexts && contexts.items ? contexts.items : []
return contextsList.map((context: any) => ({
createdBy: context.createdBy,
id: context.id,
name: context.name,
version: context.version,
attributes: {}
}))
/**
* Returns all available launcher contexts on this server.
* @param accessToken - an access token for an authorized user.
*/
public async getLauncherContexts(accessToken?: string) {
return await this.contextManager.getLauncherContexts(accessToken)
}
/**
@@ -126,76 +124,12 @@ export class SASViyaApiClient {
* @param accessToken - an access token for an authorized user.
*/
public async getExecutableContexts(accessToken?: string) {
const headers: any = {
'Content-Type': 'application/json'
}
const bindedExecuteScript = this.executeScript.bind(this)
if (accessToken) {
headers.Authorization = `Bearer ${accessToken}`
}
const { result: contexts } = await this.request<{ items: Context[] }>(
`${this.serverUrl}/compute/contexts?limit=10000`,
{ headers }
).catch((err) => {
throw err
})
const contextsList = contexts.items || []
const executableContexts: any[] = []
const promises = contextsList.map((context: any) => {
const linesOfCode = ['%put &=sysuserid;']
return () =>
this.executeScript(
`test-${context.name}`,
linesOfCode,
context.name,
accessToken,
null,
true,
true
).catch((err) => err)
})
let results: any[] = []
for (const promise of promises) results.push(await promise())
results.forEach((result: any, index: number) => {
if (result && result.body && result.body.details) {
try {
const resultParsed = JSON.parse(result.body.details)
if (resultParsed && resultParsed.body) {
let sysUserId = ''
const sysUserIdLog = resultParsed.body
.split('\n')
.find((line: string) => line.startsWith('SYSUSERID='))
if (sysUserIdLog) {
sysUserId = sysUserIdLog.replace('SYSUSERID=', '')
executableContexts.push({
createdBy: contextsList[index].createdBy,
id: contextsList[index].id,
name: contextsList[index].name,
version: contextsList[index].version,
attributes: {
sysUserId
}
})
}
}
} catch (error) {
throw error
}
}
})
return executableContexts
return await this.contextManager.getExecutableContexts(
bindedExecuteScript,
accessToken
)
}
/**
@@ -248,7 +182,7 @@ export class SASViyaApiClient {
* @param accessToken - an access token for an authorized user.
* @param authorizedUsers - an optional list of authorized user IDs.
*/
public async createContext(
public async createComputeContext(
contextName: string,
launchContextName: string,
sharedAccountId: string,
@@ -256,59 +190,35 @@ export class SASViyaApiClient {
accessToken?: string,
authorizedUsers?: string[]
) {
if (!contextName) {
throw new Error('Context name is required.')
}
if (!launchContextName) {
throw new Error('Launch context name is required.')
}
if (!sharedAccountId) {
throw new Error('Shared account ID is required.')
}
const headers: any = {
'Content-Type': 'application/json'
}
if (accessToken) {
headers.Authorization = `Bearer ${accessToken}`
}
const requestBody: any = {
name: contextName,
launchContext: {
contextName: launchContextName
},
attributes: {
reuseServerProcesses: true,
runServerAs: sharedAccountId
}
}
if (authorizedUsers && authorizedUsers.length) {
requestBody['authorizedUsers'] = authorizedUsers
} else {
requestBody['authorizeAllAuthenticatedUsers'] = true
}
if (autoExecLines) {
requestBody.environment = { autoExecLines }
}
const createContextRequest: RequestInit = {
method: 'POST',
headers,
body: JSON.stringify(requestBody)
}
const { result: context } = await this.request<Context>(
`${this.serverUrl}/compute/contexts`,
createContextRequest
return await this.contextManager.createComputeContext(
contextName,
launchContextName,
sharedAccountId,
autoExecLines,
accessToken,
authorizedUsers
)
}
return context
/**
* Creates a launcher context on the given server.
* @param contextName - the name of the context to be created.
* @param description - the description of the context to be created.
* @param launchType - launch type of the context to be created.
* @param accessToken - an access token for an authorized user.
*/
public async createLauncherContext(
contextName: string,
description: string,
launchType = 'direct',
accessToken?: string
) {
return await this.contextManager.createLauncherContext(
contextName,
description,
launchType,
accessToken
)
}
/**
@@ -317,75 +227,15 @@ export class SASViyaApiClient {
* @param editedContext - an object with the properties to be updated.
* @param accessToken - an access token for an authorized user.
*/
public async editContext(
public async editComputeContext(
contextName: string,
editedContext: EditContextInput,
accessToken?: string
) {
if (!contextName) {
throw new Error('Invalid context name.')
}
const headers: any = {
'Content-Type': 'application/json'
}
if (accessToken) {
headers.Authorization = `Bearer ${accessToken}`
}
let originalContext
originalContext = await this.getComputeContextByName(
return await this.contextManager.editComputeContext(
contextName,
editedContext,
accessToken
).catch((err) => {
throw err
})
// Try to find context by id, when context name has been changed.
if (!originalContext) {
originalContext = await this.getComputeContextById(
editedContext.id!,
accessToken
).catch((err) => {
throw err
})
}
const { result: context, etag } = await this.request<Context>(
`${this.serverUrl}/compute/contexts/${originalContext.id}`,
{
headers
}
).catch((err) => {
if (err && err.status === 404) {
throw new Error(
`The context '${contextName}' was not found on this server.`
)
}
throw err
})
// An If-Match header with the value of the last ETag for the context
// is required to be able to update it
// https://developer.sas.com/apis/rest/Compute/#update-a-context-definition
headers['If-Match'] = etag
const updateContextRequest: RequestInit = {
method: 'PUT',
headers,
body: JSON.stringify({
...context,
...editedContext,
attributes: { ...context.attributes, ...editedContext.attributes }
})
}
return await this.request<Context>(
`${this.serverUrl}/compute/contexts/${context.id}`,
updateContextRequest
)
}
@@ -394,29 +244,10 @@ export class SASViyaApiClient {
* @param contextName - the name of the context to be deleted.
* @param accessToken - an access token for an authorized user.
*/
public async deleteContext(contextName: string, accessToken?: string) {
if (!contextName) {
throw new Error('Invalid context name.')
}
const headers: any = {
'Content-Type': 'application/json'
}
if (accessToken) {
headers.Authorization = `Bearer ${accessToken}`
}
const context = await this.getComputeContextByName(contextName, accessToken)
const deleteContextRequest: RequestInit = {
method: 'DELETE',
headers
}
return await this.request<Context>(
`${this.serverUrl}/compute/contexts/${context.id}`,
deleteContextRequest
public async deleteComputeContext(contextName: string, accessToken?: string) {
return await this.contextManager.deleteComputeContext(
contextName,
accessToken
)
}
@@ -426,10 +257,12 @@ export class SASViyaApiClient {
* @param linesOfCode - an array of code lines to execute.
* @param contextName - the context to execute the code in.
* @param accessToken - an access token for an authorized user.
* @param sessionId - optional session ID to reuse.
* @param data - execution data.
* @param debug - when set to true, the log will be returned.
* @param expectWebout - when set to true, the automatic _webout fileref will be checked for content, and that content returned. This fileref is used when the Job contains a SASjs web request (as opposed to executing arbitrary SAS code).
* @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 printPid - a boolean that indicates whether the function should print (PID) of the started job.
*/
public async executeScript(
jobPath: string,
@@ -437,8 +270,11 @@ export class SASViyaApiClient {
contextName: string,
accessToken?: string,
data = null,
debug: boolean = false,
expectWebout = false,
waitForResult = true
waitForResult = true,
pollOptions?: PollOptions,
printPid = false
): Promise<any> {
try {
const headers: any = {
@@ -458,6 +294,28 @@ export class SASViyaApiClient {
executionSessionId = session!.id
if (printPid) {
const { result: jobIdVariable } = await this.sessionManager.getVariable(
executionSessionId,
'SYSJOBID',
accessToken
)
if (jobIdVariable && jobIdVariable.value) {
const relativeJobPath = this.rootFolderName
? jobPath.split(this.rootFolderName).join('').replace(/^\//, '')
: jobPath
const logger = new Logger(debug ? LogLevel.Debug : LogLevel.Info)
logger.info(
`Triggered '${relativeJobPath}' with PID ${
jobIdVariable.value
} at ${timestampToYYYYMMDDHHMMSS()}`
)
}
}
const jobArguments: { [key: string]: any } = {
_contextName: contextName,
_OMITJSONLISTING: true,
@@ -467,7 +325,7 @@ export class SASViyaApiClient {
_OMITTEXTLOG: true
}
if (this.debug) {
if (debug) {
jobArguments['_OMITTEXTLOG'] = false
jobArguments['_OMITSESSIONRESULTS'] = false
jobArguments['_DEBUG'] = 131
@@ -535,7 +393,7 @@ export class SASViyaApiClient {
return session
}
if (this.debug) {
if (debug) {
console.log(`Job has been submitted for '${fileName}'.`)
console.log(
`You can monitor the job progress at '${this.serverUrl}${
@@ -544,7 +402,12 @@ export class SASViyaApiClient {
)
}
const jobStatus = await this.pollJobState(postedJob, etag, accessToken)
const jobStatus = await this.pollJobState(
postedJob,
etag,
accessToken,
pollOptions
)
const { result: currentJob } = await this.request<Job>(
`${this.serverUrl}/compute/sessions/${executionSessionId}/jobs/${postedJob.id}`,
@@ -558,7 +421,7 @@ export class SASViyaApiClient {
const logLink = currentJob.links.find((l) => l.rel === 'log')
if (this.debug && logLink) {
if (debug && logLink) {
log = await this.request<any>(
`${this.serverUrl}${logLink.href}/content?limit=10000`,
{
@@ -574,7 +437,7 @@ export class SASViyaApiClient {
}
if (jobStatus === 'failed' || jobStatus === 'error') {
return Promise.reject({ error: currentJob.error, log })
return Promise.reject({ job: currentJob, log })
}
let resultLink
@@ -582,7 +445,7 @@ export class SASViyaApiClient {
if (expectWebout) {
resultLink = `/compute/sessions/${executionSessionId}/filerefs/_webout/content`
} else {
return currentJob
return { job: currentJob, log }
}
if (resultLink) {
@@ -606,12 +469,10 @@ export class SASViyaApiClient {
throw err
})
return Promise.reject(
new ErrorResponse('Job execution failed', {
status: 500,
body: log
})
)
return Promise.reject({
status: 500,
log: log
})
}
}
return {
@@ -635,6 +496,7 @@ export class SASViyaApiClient {
contextName,
accessToken,
data,
debug,
false,
true
)
@@ -951,14 +813,19 @@ export class SASViyaApiClient {
* @param accessToken - an optional access token for an authorized user.
* @param waitForResult - a boolean indicating if the function should wait for a result.
* @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 printPid - a boolean that indicates whether the function should print (PID) of the started job.
*/
public async executeComputeJob(
sasJob: string,
contextName: string,
debug?: boolean,
data?: any,
accessToken?: string,
waitForResult = true,
expectWebout = false
expectWebout = false,
pollOptions?: PollOptions,
printPid = false
) {
if (isRelativePath(sasJob) && !this.rootFolderName) {
throw new Error(
@@ -966,23 +833,20 @@ export class SASViyaApiClient {
)
}
if (isRelativePath(sasJob)) {
const folderName = sasJob.split('/')[0]
await this.populateFolderMap(
`${this.rootFolderName}/${folderName}`,
accessToken
)
const folderPathParts = sasJob.split('/')
const jobName = folderPathParts.pop()
const folderPath = folderPathParts.join('/')
const fullFolderPath = isRelativePath(sasJob)
? `${this.rootFolderName}/${folderPath}`
: folderPath
if (!this.folderMap.get(`${this.rootFolderName}/${folderName}`)) {
throw new Error(
`The folder '${folderName}' was not found at '${this.serverUrl}/${this.rootFolderName}'`
)
}
} else {
const folderPathParts = sasJob.split('/')
folderPathParts.pop()
const folderPath = folderPathParts.join('/')
await this.populateFolderMap(folderPath, accessToken)
await this.populateFolderMap(fullFolderPath, accessToken)
const jobFolder = this.folderMap.get(fullFolderPath)
if (!jobFolder) {
throw new Error(
`The folder '${fullFolderPath}' was not found on '${this.serverUrl}'`
)
}
const headers: any = { 'Content-Type': 'application/json' }
@@ -990,21 +854,7 @@ export class SASViyaApiClient {
headers.Authorization = `Bearer ${accessToken}`
}
let jobToExecute
if (isRelativePath(sasJob)) {
const folderName = sasJob.split('/')[0]
const jobName = sasJob.split('/')[1]
const jobFolder = this.folderMap.get(
`${this.rootFolderName}/${folderName}`
)
jobToExecute = jobFolder?.find((item) => item.name === jobName)
} else {
const folderPathParts = sasJob.split('/')
const jobName = folderPathParts.pop()
const folderPath = folderPathParts.join('/')
const jobFolder = this.folderMap.get(folderPath)
jobToExecute = jobFolder?.find((item) => item.name === jobName)
}
const jobToExecute = jobFolder?.find((item) => item.name === jobName)
if (!jobToExecute) {
throw new Error(`Job was not found.`)
@@ -1041,8 +891,11 @@ export class SASViyaApiClient {
contextName,
accessToken,
data,
debug,
expectWebout,
waitForResult
waitForResult,
pollOptions,
printPid
)
}
@@ -1067,54 +920,30 @@ export class SASViyaApiClient {
)
}
if (isRelativePath(sasJob)) {
const folderName = sasJob.split('/')[0]
await this.populateFolderMap(
`${this.rootFolderName}/${folderName}`,
accessToken
)
const folderPathParts = sasJob.split('/')
const jobName = folderPathParts.pop()
const folderPath = folderPathParts.join('/')
const fullFolderPath = isRelativePath(sasJob)
? `${this.rootFolderName}/${folderPath}`
: folderPath
await this.populateFolderMap(fullFolderPath, accessToken)
if (!this.folderMap.get(`${this.rootFolderName}/${folderName}`)) {
throw new Error(
`The folder '${folderName}' was not found at '${this.serverUrl}/${this.rootFolderName}'.`
)
}
} else {
const folderPathParts = sasJob.split('/')
folderPathParts.pop()
const folderPath = folderPathParts.join('/')
await this.populateFolderMap(folderPath, accessToken)
if (!this.folderMap.get(folderPath)) {
throw new Error(
`The folder '${folderPath}' was not found at '${this.serverUrl}'.`
)
}
const jobFolder = this.folderMap.get(fullFolderPath)
if (!jobFolder) {
throw new Error(
`The folder '${fullFolderPath}' was not found on '${this.serverUrl}'.`
)
}
const jobToExecute = jobFolder?.find((item) => item.name === jobName)
let files: any[] = []
if (data && Object.keys(data).length) {
files = await this.uploadTables(data, accessToken)
}
let jobToExecute: Job | undefined
let jobName: string | undefined
let jobPath: string | undefined
if (isRelativePath(sasJob)) {
const folderName = sasJob.split('/')[0]
jobName = sasJob.split('/')[1]
jobPath = `${this.rootFolderName}/${folderName}`
const jobFolder = this.folderMap.get(jobPath)
jobToExecute = jobFolder?.find((item) => item.name === jobName)
} else {
const folderPathParts = sasJob.split('/')
jobName = folderPathParts.pop()
jobPath = folderPathParts.join('/')
const jobFolder = this.folderMap.get(jobPath)
jobToExecute = jobFolder?.find((item) => item.name === jobName)
}
if (!jobToExecute) {
throw new Error(`The job ${sasJob} was not found.`)
throw new Error(`Job was not found.`)
}
const jobDefinitionLink = jobToExecute?.links.find(
(l) => l.rel === 'getResource'
@@ -1137,7 +966,7 @@ export class SASViyaApiClient {
const jobArguments: { [key: string]: any } = {
_contextName: contextName,
_program: `${jobPath}/${jobName}`,
_program: `${fullFolderPath}/${jobName}`,
_webin_file_count: files.length,
_OMITJSONLISTING: true,
_OMITJSONLOG: true,
@@ -1227,7 +1056,7 @@ export class SASViyaApiClient {
throw new Error(`The path ${path} does not exist on ${this.serverUrl}`)
}
const { result: members } = await this.request<{ items: any[] }>(
`${this.serverUrl}/folders/folders/${folder.id}/members`,
`${this.serverUrl}/folders/folders/${folder.id}/members?limit=${folder.memberCount}`,
requestInfo
)
@@ -1235,13 +1064,21 @@ export class SASViyaApiClient {
this.folderMap.set(path, itemsAtRoot)
}
// REFACTOR: set default value for 'pollOptions' attribute
private async pollJobState(
postedJob: any,
etag: string | null,
accessToken?: string
accessToken?: string,
pollOptions?: PollOptions
) {
const MAX_POLL_COUNT = 1000
const POLL_INTERVAL = 100
let POLL_INTERVAL = 100
let MAX_POLL_COUNT = 1000
if (pollOptions) {
POLL_INTERVAL = pollOptions.POLL_INTERVAL || POLL_INTERVAL
MAX_POLL_COUNT = pollOptions.MAX_POLL_COUNT || MAX_POLL_COUNT
}
let postedJobState = ''
let pollCount = 0
const headers: any = {
@@ -1270,6 +1107,8 @@ export class SASViyaApiClient {
}
return new Promise(async (resolve, _) => {
let printedState = ''
const interval = setInterval(async () => {
if (
postedJobState === 'running' ||
@@ -1277,9 +1116,6 @@ export class SASViyaApiClient {
postedJobState === 'pending'
) {
if (stateLink) {
if (this.debug) {
console.log('Polling job status... \n')
}
const { result: jobState } = await this.request<string>(
`${this.serverUrl}${stateLink.href}?_action=wait&wait=30`,
{
@@ -1289,10 +1125,16 @@ export class SASViyaApiClient {
)
postedJobState = jobState.trim()
if (this.debug) {
console.log(`Current state: ${postedJobState}\n`)
if (this.debug && printedState !== postedJobState) {
console.log('Polling job status...')
console.log(`Current job state: ${postedJobState}`)
printedState = postedJobState
}
pollCount++
if (pollCount >= MAX_POLL_COUNT) {
resolve(postedJobState)
}
@@ -1391,26 +1233,10 @@ export class SASViyaApiClient {
contextName: string,
accessToken?: string
): Promise<Context> {
const headers: any = {
'Content-Type': 'application/json'
}
if (accessToken) {
headers.Authorization = `Bearer ${accessToken}`
}
const { result: contexts } = await this.request<{ items: Context[] }>(
`${this.serverUrl}/compute/contexts?filter=eq(name, "${contextName}")`,
{ headers }
return await this.contextManager.getComputeContextByName(
contextName,
accessToken
)
if (!contexts || !(contexts.items && contexts.items.length)) {
throw new Error(
`The context '${contextName}' was not found at '${this.serverUrl}'.`
)
}
return contexts.items[0]
}
/**
@@ -1422,22 +1248,10 @@ export class SASViyaApiClient {
contextId: string,
accessToken?: string
): Promise<ContextAllAttributes> {
const headers: any = {
'Content-Type': 'application/json'
}
if (accessToken) {
headers.Authorization = `Bearer ${accessToken}`
}
const { result: context } = await this.request<ContextAllAttributes>(
`${this.serverUrl}/compute/contexts/${contextId}`,
{ headers }
).catch((err) => {
throw err
})
return context
return await this.contextManager.getComputeContextById(
contextId,
accessToken
)
}
/**

View File

@@ -32,7 +32,8 @@ import {
CsrfToken,
UploadFile,
EditContextInput,
ErrorResponse
ErrorResponse,
PollOptions
} from './types'
import { SASViyaApiClient } from './SASViyaApiClient'
import { SAS9ApiClient } from './SAS9ApiClient'
@@ -95,12 +96,39 @@ export default class SASjs {
)
}
public async getAllContexts(accessToken: string) {
this.isMethodSupported('getAllContexts', ServerType.SASViya)
/**
* Gets compute contexts.
* @param accessToken - an access token for an authorized user.
*/
public async getComputeContexts(accessToken: string) {
this.isMethodSupported('getComputeContexts', ServerType.SASViya)
return await this.sasViyaApiClient!.getAllContexts(accessToken)
return await this.sasViyaApiClient!.getComputeContexts(accessToken)
}
/**
* Gets launcher contexts.
* @param accessToken - an access token for an authorized user.
*/
public async getLauncherContexts(accessToken: string) {
this.isMethodSupported('getLauncherContexts', ServerType.SASViya)
return await this.sasViyaApiClient!.getLauncherContexts(accessToken)
}
/**
* Gets default(system) launcher contexts.
*/
public getDefaultComputeContexts() {
this.isMethodSupported('getDefaultComputeContexts', ServerType.SASViya)
return this.sasViyaApiClient!.getDefaultComputeContexts()
}
/**
* Gets executable compute contexts.
* @param accessToken - an access token for an authorized user.
*/
public async getExecutableContexts(accessToken: string) {
this.isMethodSupported('getExecutableContexts', ServerType.SASViya)
@@ -116,7 +144,7 @@ export default class SASjs {
* @param accessToken - an access token for an authorized user.
* @param authorizedUsers - an optional list of authorized user IDs.
*/
public async createContext(
public async createComputeContext(
contextName: string,
launchContextName: string,
sharedAccountId: string,
@@ -124,9 +152,9 @@ export default class SASjs {
accessToken: string,
authorizedUsers?: string[]
) {
this.isMethodSupported('createContext', ServerType.SASViya)
this.isMethodSupported('createComputeContext', ServerType.SASViya)
return await this.sasViyaApiClient!.createContext(
return await this.sasViyaApiClient!.createComputeContext(
contextName,
launchContextName,
sharedAccountId,
@@ -136,20 +164,43 @@ export default class SASjs {
)
}
/**
* Creates a launcher context on the given server.
* @param contextName - the name of the context to be created.
* @param description - the description of the context to be created.
* @param launchType - launch type of the context to be created.
* @param accessToken - an access token for an authorized user.
*/
public async createLauncherContext(
contextName: string,
description: string,
launchType: string,
accessToken: string
) {
this.isMethodSupported('createLauncherContext', ServerType.SASViya)
return await this.sasViyaApiClient!.createLauncherContext(
contextName,
description,
launchType,
accessToken
)
}
/**
* Updates a compute context on the given server.
* @param contextName - the original name of the context to be deleted.
* @param editedContext - an object with the properties to be updated.
* @param accessToken - an access token for an authorized user.
*/
public async editContext(
public async editComputeContext(
contextName: string,
editedContext: EditContextInput,
accessToken?: string
) {
this.isMethodSupported('editContext', ServerType.SASViya)
this.isMethodSupported('editComputeContext', ServerType.SASViya)
return await this.sasViyaApiClient!.editContext(
return await this.sasViyaApiClient!.editComputeContext(
contextName,
editedContext,
accessToken
@@ -161,10 +212,13 @@ export default class SASjs {
* @param contextName - the name of the context to be deleted.
* @param accessToken - an access token for an authorized user.
*/
public async deleteContext(contextName: string, accessToken?: string) {
this.isMethodSupported('deleteContext', ServerType.SASViya)
public async deleteComputeContext(contextName: string, accessToken?: string) {
this.isMethodSupported('deleteComputeContext', ServerType.SASViya)
return await this.sasViyaApiClient!.deleteContext(contextName, accessToken)
return await this.sasViyaApiClient!.deleteComputeContext(
contextName,
accessToken
)
}
/**
@@ -205,11 +259,20 @@ export default class SASjs {
return await this.sasViyaApiClient!.createSession(contextName, accessToken)
}
/**
* Executes the sas code against given sas server
* @param fileName - name of the file to run. It will be converted to path to the file being submitted for execution.
* @param linesOfCode - lines of sas code from the file to run.
* @param contextName - context name on which code will be run on the server.
* @param accessToken - (optional) the access token for authorizing the request.
* @param debug - (optional) if true, global debug config will be overriden
*/
public async executeScriptSASViya(
fileName: string,
linesOfCode: string[],
contextName: string,
accessToken?: string
accessToken?: string,
debug?: boolean
) {
this.isMethodSupported('executeScriptSASViya', ServerType.SASViya)
@@ -218,7 +281,8 @@ export default class SASjs {
linesOfCode,
contextName,
accessToken,
null
null,
debug ? debug : this.sasjsConfig.debug
)
}
@@ -411,6 +475,29 @@ export default class SASjs {
}
}
private async getLoginForm(response: any) {
const pattern: RegExp = /<form.+action="(.*Logon[^"]*).*>/
const matches = pattern.exec(response)
const formInputs: any = {}
if (matches && matches.length) {
this.setLoginUrl(matches)
const inputs = response.match(/<input.*"hidden"[^>]*>/g)
if (inputs) {
inputs.forEach((inputStr: string) => {
const valueMatch = inputStr.match(/name="([^"]*)"\svalue="([^"]*)/)
if (valueMatch && valueMatch.length) {
formInputs[valueMatch[1]] = valueMatch[2]
}
})
}
}
return Object.keys(formInputs).length ? formInputs : null
}
/**
* Checks whether a session is active, or login is required.
* @returns - a promise which resolves with an object containing two values - a boolean `isLoggedIn`, and a string `userName`.
@@ -419,10 +506,16 @@ export default class SASjs {
const loginResponse = await fetch(this.loginUrl.replace('.do', ''))
const responseText = await loginResponse.text()
const isLoggedIn = /<button.+onClick.+logout/gm.test(responseText)
let loginForm: any = null
if (!isLoggedIn) {
loginForm = await this.getLoginForm(responseText)
}
return Promise.resolve({
isLoggedIn,
userName: this.userName
userName: this.userName,
loginForm
})
}
@@ -440,7 +533,7 @@ export default class SASjs {
this.userName = loginParams.username
const { isLoggedIn } = await this.checkSession()
const { isLoggedIn, loginForm } = await this.checkSession()
if (isLoggedIn) {
this.resendWaitingRequests()
@@ -450,15 +543,13 @@ export default class SASjs {
})
}
const loginForm = await this.getLoginForm()
for (const key in loginForm) {
loginParams[key] = loginForm[key]
}
const loginParamsStr = serialize(loginParams)
return fetch(this.loginUrl, {
method: 'post',
method: 'POST',
credentials: 'include',
referrerPolicy: 'same-origin',
body: loginParamsStr,
@@ -656,10 +747,7 @@ export default class SASjs {
)
}
const members =
serviceJson.members[0].name === 'services'
? serviceJson.members[0].members
: serviceJson.members
const members = serviceJson.members
await this.createFoldersAndServices(
appLoc,
@@ -684,13 +772,17 @@ export default class SASjs {
* @param accessToken - a valid access token that is authorised to execute compute jobs.
* The access token is not required when the user is authenticated via the browser.
* @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 printPid - a boolean that indicates whether the function should print (PID) of the started job.
*/
public async startComputeJob(
sasJob: string,
data: any,
config: any = {},
accessToken?: string,
waitForResult?: boolean
waitForResult?: boolean,
pollOptions?: PollOptions,
printPid = false
) {
config = {
...this.sasjsConfig,
@@ -707,10 +799,13 @@ export default class SASjs {
return this.sasViyaApiClient?.executeComputeJob(
sasJob,
config.contextName,
config.debug,
data,
accessToken,
!!waitForResult,
false
false,
pollOptions,
printPid
)
}
@@ -739,6 +834,7 @@ export default class SASjs {
?.executeComputeJob(
sasJob,
config.contextName,
config.debug,
data,
accessToken,
waitForResult,
@@ -780,11 +876,23 @@ export default class SASjs {
} else {
this.retryCountComputeApi = 0
reject(
new ErrorResponse('Compute API retry requests limit reached')
new ErrorResponse('Compute API retry requests limit reached.')
)
}
}
if (response?.log) {
this.appendSasjsRequest(response.log, sasJob, null)
}
if (error.toString().includes('Job was not found')) {
reject(
new ErrorResponse('Service not found on the server.', {
sasJob: sasJob
})
)
}
if (error && error.status === 401) {
if (loginRequiredCallback) loginRequiredCallback(true)
sasjsWaitingRequest.requestPromise.resolve = resolve
@@ -792,10 +900,8 @@ export default class SASjs {
sasjsWaitingRequest.config = config
this.sasjsWaitingRequests.push(sasjsWaitingRequest)
} else {
reject(new ErrorResponse('Job execution failed', error))
reject(new ErrorResponse('Job execution failed.', error))
}
this.appendSasjsRequest(response.log, sasJob, null)
})
}
)
@@ -858,8 +964,8 @@ export default class SASjs {
return responseJson
})
.catch(async (e) => {
if (needsRetry(JSON.stringify(e))) {
.catch(async (response) => {
if (needsRetry(JSON.stringify(response))) {
if (this.retryCountJeseApi < requestRetryLimit) {
let retryResponse = await this.executeJobViaJesApi(
sasJob,
@@ -875,12 +981,24 @@ export default class SASjs {
} else {
this.retryCountJeseApi = 0
reject(
new ErrorResponse('Jes API retry requests limit reached')
new ErrorResponse('Jes API retry requests limit reached.')
)
}
}
reject(new ErrorResponse('Job execution failed', e))
if (response?.log) {
this.appendSasjsRequest(response.log, sasJob, null)
}
if (response.toString().includes('Job was not found')) {
reject(
new ErrorResponse('Service not found on the server.', {
sasJob: sasJob
})
)
}
reject(new ErrorResponse('Job execution failed.', response))
})
)
}
@@ -1064,7 +1182,7 @@ export default class SASjs {
} else {
reject(
new ErrorResponse(
'Job WEB execution failed',
'Job WEB execution failed.',
this.parseSAS9ErrorResponse(responseText)
)
)
@@ -1082,7 +1200,7 @@ export default class SASjs {
} catch (e) {
reject(
new ErrorResponse(
'Job WEB debug response parsing failed',
'Job WEB debug response parsing failed.',
{ response: resText, exception: e }
)
)
@@ -1091,7 +1209,7 @@ export default class SASjs {
(err: any) => {
reject(
new ErrorResponse(
'Job WEB debug response parsing failed',
'Job WEB debug response parsing failed.',
err
)
)
@@ -1100,19 +1218,34 @@ export default class SASjs {
} catch (e) {
reject(
new ErrorResponse(
'Job WEB debug response parsing failed',
'Job WEB debug response parsing failed.',
{ response: responseText, exception: e }
)
)
}
} else {
this.updateUsername(responseText)
if (
responseText.includes(
'The requested URL /SASStoredProcess/do/ was not found on this server.'
) ||
responseText.includes('Stored process not found')
) {
reject(
new ErrorResponse(
'Service not found on the server.',
{ service: sasJob },
responseText
)
)
}
try {
const parsedJson = JSON.parse(responseText)
resolve(parsedJson)
} catch (e) {
reject(
new ErrorResponse('Job WEB response parsing failed', {
new ErrorResponse('Job WEB response parsing failed.', {
response: responseText,
exception: e
})
@@ -1123,7 +1256,7 @@ export default class SASjs {
}
})
.catch((e: Error) => {
reject(new ErrorResponse('Job WEB request failed', e))
reject(new ErrorResponse('Job WEB request failed.', e))
})
}
)
@@ -1448,26 +1581,6 @@ export default class SASjs {
}
}
private async getLoginForm() {
const pattern: RegExp = /<form.+action="(.*Logon[^"]*).*>/
const response = await fetch(this.loginUrl).then((r) => r.text())
const matches = pattern.exec(response)
const formInputs: any = {}
if (matches && matches.length) {
this.setLoginUrl(matches)
const inputs = response.match(/<input.*"hidden"[^>]*>/g)
if (inputs) {
inputs.forEach((inputStr: string) => {
const valueMatch = inputStr.match(/name="([^"]*)"\svalue="([^"]*)/)
if (valueMatch && valueMatch.length) {
formInputs[valueMatch[1]] = valueMatch[2]
}
})
}
}
return Object.keys(formInputs).length ? formInputs : null
}
private async createFoldersAndServices(
parentFolder: string,
membersJson: any[],

View File

@@ -1,5 +1,6 @@
import { Session, Context, CsrfToken } from './types'
import { Session, Context, CsrfToken, SessionVariable } from './types'
import { asyncForEach, makeRequest, isUrl } from './utils'
import { prefixMessage } from '@sasjs/utils/error'
const MAX_SESSION_COUNT = 1
const RETRY_LIMIT: number = 3
@@ -22,6 +23,10 @@ export class SessionManager {
private currentContext: Context | null = null
private csrfToken: CsrfToken | null = null
private _debug: boolean = false
private printedSessionState = {
printed: false,
state: ''
}
public get debug() {
return this._debug
@@ -168,10 +173,16 @@ export class SessionManager {
const stateLink = session.links.find((l: any) => l.rel === 'state')
return new Promise(async (resolve, _) => {
if (sessionState === 'pending') {
if (
sessionState === 'pending' ||
sessionState === 'running' ||
sessionState === ''
) {
if (stateLink) {
if (this.debug) {
console.log('Polling session status... \n') // ?
if (this.debug && !this.printedSessionState.printed) {
console.log('Polling session status...')
this.printedSessionState.printed = true
}
const { result: state } = await this.requestSessionStatus<string>(
@@ -186,8 +197,11 @@ export class SessionManager {
sessionState = state.trim()
if (this.debug) {
console.log(`Current state is '${sessionState}'\n`)
if (this.debug && this.printedSessionState.state !== sessionState) {
console.log(`Current session state is '${sessionState}'`)
this.printedSessionState.state = sessionState
this.printedSessionState.printed = false
}
// There is an internal error present in SAS Viya 3.5
@@ -261,4 +275,21 @@ export class SessionManager {
throw err
})
}
async getVariable(sessionId: string, variable: string, accessToken?: string) {
const getSessionVariable = {
method: 'GET',
headers: this.getHeaders(accessToken)
}
return await this.request<SessionVariable>(
`${this.serverUrl}/compute/sessions/${sessionId}/variables/${variable}`,
getSessionVariable
).catch((err) => {
throw prefixMessage(
err,
`Error while fetching session variable '${variable}'.`
)
})
}
}

View File

@@ -0,0 +1,585 @@
import { ContextManager } from '../ContextManager'
describe('ContextManager', () => {
let originalFetch: any
let fetchCallNumber = 0
const fakeGlobalFetch = (fakeResponses: object[]) => {
;(global as any).fetch = jest.fn().mockImplementation(() => {
const fakeResponse = fakeResponses[fetchCallNumber]
if (
fetchCallNumber !== fakeResponses.length &&
fakeResponses.length > 1
) {
if (fetchCallNumber + 1 === fakeResponses.length) fetchCallNumber = 0
else fetchCallNumber += 1
} else {
fetchCallNumber = 0
}
return Promise.resolve({
ok: true,
headers: { get: () => '' },
json: () => Promise.resolve(fakeResponse)
})
})
}
const contextManager = new ContextManager(
process.env.SERVER_URL as string,
() => {}
)
const defaultComputeContexts = contextManager.getDefaultComputeContexts
const defaultLauncherContexts = contextManager.getDefaultLauncherContexts
const getRandomDefaultComputeContext = () =>
defaultComputeContexts[
Math.floor(Math.random() * defaultComputeContexts.length)
]
const getRandomDefaultLauncherContext = () =>
defaultLauncherContexts[
Math.floor(Math.random() * defaultLauncherContexts.length)
]
beforeAll(() => {
originalFetch = (global as any).fetch
})
afterEach(() => {
;(global as any).fetch = originalFetch
})
describe('getComputeContexts', () => {
it('should fetch compute contexts', async () => {
const sampleComputeContext = {
createdBy: 'fake creator',
id: 'fakeId',
version: 2,
name: 'Fake Compute Context',
attributes: {}
}
const sampleResponse = {
items: [sampleComputeContext]
}
fakeGlobalFetch([sampleResponse])
await expect(contextManager.getComputeContexts()).resolves.toEqual([
sampleComputeContext
])
})
})
describe('getLauncherContexts', () => {
it('should fetch launcher contexts', async () => {
const sampleComputeContext = {
createdBy: 'fake creator',
id: 'fakeId',
version: 2,
name: 'Fake Launcher Context',
attributes: {}
}
const sampleResponse = {
items: [sampleComputeContext]
}
fakeGlobalFetch([sampleResponse])
await expect(contextManager.getLauncherContexts()).resolves.toEqual([
sampleComputeContext
])
})
})
describe('createComputeContext', () => {
it('should throw an error if context name was not provided', async () => {
await expect(
contextManager.createComputeContext(
'',
'Test Launcher Context',
'fakeAccountId',
[]
)
).rejects.toEqual(new Error('Context name is required.'))
})
it('should throw an error when attempt to create context with reserved name', async () => {
const contextName = getRandomDefaultComputeContext()
await expect(
contextManager.createComputeContext(
contextName,
'Test Launcher Context',
'fakeAccountId',
[]
)
).rejects.toEqual(
new Error(`Compute context '${contextName}' already exists.`)
)
})
it('should throw an error if context already exists', async () => {
const contextName = 'Existing Compute Context'
const sampleComputeContext = {
createdBy: 'fake creator',
id: 'fakeId',
version: 2,
name: contextName,
attributes: {}
}
const sampleResponse = {
items: [sampleComputeContext]
}
fakeGlobalFetch([sampleResponse])
await expect(
contextManager.createComputeContext(
contextName,
'Test Launcher Context',
'fakeAccountId',
[]
)
).rejects.toEqual(
new Error(`Compute context '${contextName}' already exists.`)
)
})
it('should create compute context without launcher context', async () => {
const contextName = 'New Compute Context'
const sampleExistingComputeContext = {
createdBy: 'fake creator',
id: 'fakeId',
version: 2,
name: 'Existing Compute Context',
attributes: {}
}
const sampleNewComputeContext = {
createdBy: 'fake creator',
id: 'fakeId',
version: 2,
name: contextName,
attributes: {}
}
const sampleResponseExistingComputeContexts = {
items: [sampleExistingComputeContext]
}
const sampleResponseCreatedComputeContext = {
items: [sampleNewComputeContext]
}
fakeGlobalFetch([
sampleResponseExistingComputeContexts,
sampleResponseCreatedComputeContext
])
await expect(
contextManager.createComputeContext(
contextName,
'',
'fakeAccountId',
[]
)
).resolves.toEqual({
items: [
{
attributes: {},
createdBy: 'fake creator',
id: 'fakeId',
name: contextName,
version: 2
}
]
})
})
it('should create compute context with default launcher context', async () => {
const contextName = 'New Compute Context'
const sampleExistingComputeContext = {
createdBy: 'fake creator',
id: 'fakeId',
version: 2,
name: 'Existing Compute Context',
attributes: {}
}
const sampleNewComputeContext = {
createdBy: 'fake creator',
id: 'fakeId',
version: 2,
name: contextName,
attributes: {}
}
const sampleResponseExistingComputeContexts = {
items: [sampleExistingComputeContext]
}
const sampleResponseCreatedComputeContext = {
items: [sampleNewComputeContext]
}
fakeGlobalFetch([
sampleResponseExistingComputeContexts,
sampleResponseCreatedComputeContext
])
await expect(
contextManager.createComputeContext(
contextName,
getRandomDefaultLauncherContext(),
'fakeAccountId',
[]
)
).resolves.toEqual({
items: [
{
attributes: {},
createdBy: 'fake creator',
id: 'fakeId',
name: contextName,
version: 2
}
]
})
})
it('should create compute context with not existing launcher context', async () => {
const computeContextName = 'New Compute Context'
const launcherContextName = 'New Launcher Context'
const sampleExistingComputeContext = {
createdBy: 'fake creator',
id: 'fakeId',
version: 2,
name: 'Existing Compute Context',
attributes: {}
}
const sampleNewComputeContext = {
createdBy: 'fake creator',
id: 'fakeId',
version: 2,
name: computeContextName,
attributes: {}
}
const sampleNewLauncherContext = {
createdBy: 'fake creator',
id: 'fakeId',
version: 2,
name: launcherContextName,
attributes: {}
}
const sampleResponseExistingComputeContexts = {
items: [sampleExistingComputeContext]
}
const sampleResponseCreatedLauncherContext = {
items: [sampleNewLauncherContext]
}
const sampleResponseCreatedComputeContext = {
items: [sampleNewComputeContext]
}
fakeGlobalFetch([
sampleResponseExistingComputeContexts,
sampleResponseCreatedLauncherContext,
sampleResponseCreatedComputeContext
])
await expect(
contextManager.createComputeContext(
computeContextName,
launcherContextName,
'fakeAccountId',
[]
)
).resolves.toEqual({
items: [
{
attributes: {},
createdBy: 'fake creator',
id: 'fakeId',
name: computeContextName,
version: 2
}
]
})
})
})
describe('createLauncherContext', () => {
it('should throw an error if context name was not provided', async () => {
await expect(
contextManager.createLauncherContext('', 'Test Description')
).rejects.toEqual(new Error('Context name is required.'))
})
it('should throw an error when attempt to create context with reserved name', async () => {
const contextName = getRandomDefaultLauncherContext()
await expect(
contextManager.createLauncherContext(contextName, 'Test Description')
).rejects.toEqual(
new Error(`Launcher context '${contextName}' already exists.`)
)
})
it('should throw an error if context already exists', async () => {
const contextName = 'Existing Launcher Context'
const sampleLauncherContext = {
createdBy: 'fake creator',
id: 'fakeId',
version: 2,
name: contextName,
attributes: {}
}
const sampleResponse = {
items: [sampleLauncherContext]
}
fakeGlobalFetch([sampleResponse])
await expect(
contextManager.createLauncherContext(contextName, 'Test Description')
).rejects.toEqual(
new Error(`Launcher context '${contextName}' already exists.`)
)
})
it('should create launcher context', async () => {
const contextName = 'New Launcher Context'
const sampleExistingLauncherContext = {
createdBy: 'fake creator',
id: 'fakeId',
version: 2,
name: 'Existing Launcher Context',
attributes: {}
}
const sampleNewLauncherContext = {
createdBy: 'fake creator',
id: 'fakeId',
version: 2,
name: contextName,
attributes: {}
}
const sampleResponseExistingLauncherContext = {
items: [sampleExistingLauncherContext]
}
const sampleResponseCreatedLauncherContext = {
items: [sampleNewLauncherContext]
}
fakeGlobalFetch([
sampleResponseExistingLauncherContext,
sampleResponseCreatedLauncherContext
])
await expect(
contextManager.createLauncherContext(contextName, 'Test Description')
).resolves.toEqual({
items: [
{
attributes: {},
createdBy: 'fake creator',
id: 'fakeId',
name: contextName,
version: 2
}
]
})
})
})
describe('editComputeContext', () => {
const editedContext = {
name: 'updated name',
description: 'updated description',
id: 'someId'
}
it('should throw an error if context name was not provided', async () => {
await expect(
contextManager.editComputeContext('', editedContext)
).rejects.toEqual(new Error('Context name is required.'))
})
it('should throw an error when attempt to edit context with reserved name', async () => {
const contextName = getRandomDefaultComputeContext()
let editError: Error = { name: '', message: '' }
try {
contextManager.isDefaultContext(
contextName,
defaultComputeContexts,
'Editing default SAS compute contexts is not allowed.',
true
)
} catch (error) {
editError = error
}
await expect(
contextManager.editComputeContext(contextName, editedContext)
).rejects.toEqual(editError)
})
it('should edit context if founded by name', async () => {
const sampleComputeContext = {
createdBy: 'fake creator',
id: 'fakeId',
version: 2,
name: editedContext.name,
attributes: {}
}
const sampleResponseGetComputeContextByName = {
items: [sampleComputeContext]
}
fakeGlobalFetch([sampleResponseGetComputeContextByName])
const expectedResponse = {
etag: '',
result: sampleResponseGetComputeContextByName
}
await expect(
contextManager.editComputeContext(editedContext.name, editedContext)
).resolves.toEqual(expectedResponse)
})
})
describe('getExecutableContexts', () => {
it('should return executable contexts', async () => {
const sampleComputeContext = {
createdBy: 'fake creator',
id: 'fakeId',
version: 2,
name: 'Executable Compute Context',
attributes: {}
}
const sampleResponse = {
items: [sampleComputeContext]
}
fakeGlobalFetch([sampleResponse])
const user = 'testUser'
const fakedExecuteScript = async () => {
return Promise.resolve({ log: `SYSUSERID=${user}` })
}
const expectedResponse = [
{
...sampleComputeContext,
attributes: { sysUserId: user }
}
]
await expect(
contextManager.getExecutableContexts(fakedExecuteScript)
).resolves.toEqual(expectedResponse)
})
it('should not return executable contexts', async () => {
const sampleComputeContext = {
createdBy: 'fake creator',
id: 'fakeId',
version: 2,
name: 'Not Executable Compute Context',
attributes: {}
}
const sampleResponse = {
items: [sampleComputeContext]
}
fakeGlobalFetch([sampleResponse])
const fakedExecuteScript = async () => {
return Promise.resolve({ log: '' })
}
await expect(
contextManager.getExecutableContexts(fakedExecuteScript)
).resolves.toEqual([])
})
})
describe('deleteComputeContext', () => {
it('should throw an error if context name was not provided', async () => {
await expect(contextManager.deleteComputeContext('')).rejects.toEqual(
new Error('Context name is required.')
)
})
it('should throw an error when attempt to delete context with reserved name', async () => {
const contextName = getRandomDefaultComputeContext()
let deleteError: Error = { name: '', message: '' }
try {
contextManager.isDefaultContext(
contextName,
defaultComputeContexts,
'Deleting default SAS compute contexts is not allowed.',
true
)
} catch (error) {
deleteError = error
}
await expect(
contextManager.deleteComputeContext(contextName)
).rejects.toEqual(deleteError)
})
it('should delete context', async () => {
const contextName = 'Compute Context To Delete'
const sampleComputeContext = {
createdBy: 'fake creator',
id: 'fakeId',
version: 2,
name: contextName,
attributes: {}
}
const sampleResponseGetComputeContextByName = {
items: [sampleComputeContext]
}
const sampleResponseDeletedContext = {
items: [sampleComputeContext]
}
fakeGlobalFetch([
sampleResponseGetComputeContextByName,
sampleResponseDeletedContext
])
const expectedResponse = {
etag: '',
result: sampleResponseDeletedContext
}
await expect(
contextManager.deleteComputeContext(contextName)
).resolves.toEqual(expectedResponse)
})
})
})

View File

@@ -0,0 +1,137 @@
import { FileUploader } from '../FileUploader'
import { UploadFile } from '../types'
const sampleResponse = `{
"SYSUSERID": "cas",
"_DEBUG":" ",
"SYS_JES_JOB_URI": "/jobExecution/jobs/000-000-000-000",
"_PROGRAM" : "/Public/app/editors/loadfile",
"SYSCC" : "0",
"SYSJOBID" : "117382",
"SYSWARNINGTEXT" : ""
}`
const prepareFilesAndParams = () => {
const files: UploadFile[] = [
{
file: new File([''], 'testfile'),
fileName: 'testfile'
}
]
const params = { table: 'libtable' }
return { files, params }
}
describe('FileUploader', () => {
let originalFetch: any
const fileUploader = new FileUploader(
'/sample/apploc',
'https://sample.server.com',
'/jobs/path',
null,
null
)
beforeAll(() => {
originalFetch = (global as any).fetch
})
beforeEach(() => {
;(global as any).fetch = jest.fn().mockImplementation(() =>
Promise.resolve({
text: () => Promise.resolve(sampleResponse)
})
)
})
afterAll(() => {
;(global as any).fetch = originalFetch
})
it('should upload successfully', async (done) => {
const sasJob = 'test/upload'
const { files, params } = prepareFilesAndParams()
fileUploader.uploadFile(sasJob, files, params).then((res: any) => {
expect(JSON.stringify(res)).toEqual(
JSON.stringify(JSON.parse(sampleResponse))
)
done()
})
})
it('should an error when no files are provided', async (done) => {
const sasJob = 'test/upload'
const files: UploadFile[] = []
const params = { table: 'libtable' }
fileUploader.uploadFile(sasJob, files, params).catch((err: any) => {
expect(err.error.message).toEqual('At least one file must be provided.')
done()
})
})
it('should throw an error when no sasJob is provided', async (done) => {
const sasJob = ''
const { files, params } = prepareFilesAndParams()
fileUploader.uploadFile(sasJob, files, params).catch((err: any) => {
expect(err.error.message).toEqual('sasJob must be provided.')
done()
})
})
it('should throw an error when login is required', async (done) => {
;(global as any).fetch = jest.fn().mockImplementation(() =>
Promise.resolve({
text: () => Promise.resolve('<form action="Logon">')
})
)
const sasJob = 'test'
const { files, params } = prepareFilesAndParams()
fileUploader.uploadFile(sasJob, files, params).catch((err: any) => {
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) => {
;(global as any).fetch = jest.fn().mockImplementation(() =>
Promise.resolve({
text: () => Promise.resolve('{invalid: "json"')
})
)
const sasJob = 'test'
const { files, params } = prepareFilesAndParams()
fileUploader.uploadFile(sasJob, files, params).catch((err: any) => {
expect(err.error.message).toEqual(
'Error while parsing json from upload response.'
)
done()
})
})
it('should throw an error when the server request fails', async (done) => {
;(global as any).fetch = jest.fn().mockImplementation(() =>
Promise.resolve({
text: () => Promise.reject('{message: "Server error"}')
})
)
const sasJob = 'test'
const { files, params } = prepareFilesAndParams()
fileUploader.uploadFile(sasJob, files, params).catch((err: any) => {
expect(err.error.message).toEqual('Upload request failed.')
done()
})
})
})

View File

@@ -0,0 +1,53 @@
import { SessionManager } from '../SessionManager'
import * as dotenv from 'dotenv'
describe('SessionManager', () => {
dotenv.config()
let originalFetch: any
const sessionManager = new SessionManager(
process.env.SERVER_URL as string,
process.env.DEFAULT_COMPUTE_CONTEXT as string,
() => {}
)
beforeAll(() => {
originalFetch = (global as any).fetch
})
afterEach(() => {
;(global as any).fetch = originalFetch
})
describe('getVariable', () => {
it('should fetch session variable', async () => {
const sampleResponse = {
ok: true,
links: [],
name: 'SYSJOBID',
scope: 'GLOBAL',
value: '25218',
version: 1
}
;(global as any).fetch = jest.fn().mockImplementation(() =>
Promise.resolve({
ok: true,
headers: { get: () => '' },
json: () => Promise.resolve(sampleResponse)
})
)
const expectedResponse = { etag: '', result: sampleResponse }
await expect(
sessionManager.getVariable(
'fakeSessionId',
'SYSJOBID',
'fakeAccessToken'
)
).resolves.toEqual(expectedResponse)
})
})
})

View File

@@ -1,4 +1,4 @@
import { parseGeneratedCode } from './index'
import { parseGeneratedCode } from '../../utils/index'
it('should parse generated code', async (done) => {
expect(sampleResponse).toBeTruthy()

View File

@@ -1,4 +1,4 @@
import { parseSourceCode } from './index'
import { parseSourceCode } from '../../utils/index'
it('should parse SAS9 source code', async (done) => {
expect(sampleResponse).toBeTruthy()

View File

@@ -1,17 +1,19 @@
export class ErrorResponse {
body: ErrorBody
error: ErrorBody
constructor(message: string, details?: any) {
let detailsString = ''
let raw
constructor(message: string, details?: any, raw?: any) {
let detailsString = details
try {
detailsString = JSON.stringify(details)
} catch {
raw = details
if (typeof details !== 'object') {
try {
detailsString = JSON.parse(details)
} catch {
raw = details
detailsString = ''
}
}
this.body = {
this.error = {
message,
details: detailsString,
raw

View File

@@ -4,4 +4,5 @@ export interface Folder {
id: string
uri: string
links: Link[]
memberCount: number
}

4
src/types/PollOptions.ts Normal file
View File

@@ -0,0 +1,4 @@
export interface PollOptions {
MAX_POLL_COUNT?: number
POLL_INTERVAL?: number
}

View File

@@ -9,3 +9,7 @@ export interface Session {
}
creationTimeStamp: string
}
export interface SessionVariable {
value: string
}

View File

@@ -12,3 +12,4 @@ export * from './SASjsWaitingRequest'
export * from './ServerType'
export * from './Session'
export * from './UploadFile'
export * from './PollOptions'

View File

@@ -47,6 +47,7 @@ const browserConfig = {
const nodeConfig = {
...browserConfig,
target: 'node',
entry: './node/index.ts',
output: {
...browserConfig.output,
path: path.resolve(__dirname, 'build', 'node')