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

Compare commits

..

111 Commits

Author SHA1 Message Date
Krishna Acondy
a409d8cdb6 Merge pull request #38 from sasjs/brwoserCheckFix
fix: isIEorEdgeOrOldFirefox error
2020-08-14 13:10:44 +01:00
Mihajlo Medjedovic
618a20eaba fix: isIEorEdgeOrOldFirefox error 2020-08-14 14:09:28 +02:00
Krishna Acondy
c9b1273c31 Merge pull request #37 from sasjs/ie-edge-fetch
fix(*): use fetch polyfill in IE, Edge and Firefox <60
2020-08-13 21:56:11 +01:00
Krishna Acondy
59674744be fix(*): use fetch polyfill for Firefox versions older than 60 2020-08-13 21:46:08 +01:00
Krishna Acondy
870cc0055b fix(*): use fetch polyfill in Firefox 60 2020-08-13 21:07:21 +01:00
Krishna Acondy
0ffa62fab4 fix(*): use fetch polyfill in IE and Edge 2020-08-13 20:53:27 +01:00
Allan Bowe
b4c7868fb6 Merge pull request #36 from sasjs/issue34
fix: job definition debug log parse
2020-08-11 18:44:39 +02:00
Mihajlo Medjedovic
2266578013 Merge branch 'master' into issue34 2020-08-11 18:29:41 +02:00
Mihajlo Medjedovic
f2ebe1a5b0 fix: job definition debug log parse 2020-08-10 18:13:11 +02:00
Allan Bowe
6a52bbe560 Merge pull request #35 from sasjs/issue33
fix: makeRequest inconsistent response structure
2020-08-10 17:19:05 +02:00
Mihajlo Medjedovic
a5c725e677 fix: makeRequest incositent response structure 2020-08-10 17:05:10 +02:00
Krishna Acondy
f5e1907e28 feat(clear-requests): add function to clear debug requests 2020-08-08 14:27:40 +01:00
Krishna Acondy
f7a9b0cbb6 fix(compute-api): ignore 404s when requesting webout content 2020-08-08 14:26:09 +01:00
Krishna Acondy
1258a1a180 fix(login-callback): fix request failure when login is required 2020-08-08 12:56:31 +01:00
Krishna Acondy
0bb343a1de chore(deps): bump adapter and test framework 2020-08-08 11:42:16 +01:00
Krishna Acondy
929c89b70f Merge pull request #32 from sasjs/dependabot/npm_and_yarn/types/jest-26.0.9
chore(deps-dev): bump @types/jest from 26.0.8 to 26.0.9
2020-08-08 11:04:25 +01:00
dependabot-preview[bot]
169ca35238 chore(deps-dev): bump @types/jest from 26.0.8 to 26.0.9
Bumps [@types/jest](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/jest) from 26.0.8 to 26.0.9.
- [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-08-06 12:59:52 +00:00
Allan Bowe
60be28f149 Merge pull request #31 from sasjs/session-cleanup
feat(session-cleanup): delete a session after it has been used
2020-08-05 23:17:42 +02:00
Krishna Acondy
14daa55184 feat(session-cleanup): delete a session after it has been used 2020-08-05 21:52:23 +01:00
Krishna Acondy
f763f05b5e Merge pull request #30 from sasjs/deployFix
fix: makeRequest retry
2020-08-05 20:02:44 +01:00
Mihajlo Medjedovic
b6aced5bad fix: makeRequest retry 2020-08-05 20:58:10 +02:00
Allan Bowe
7bb7db0f27 Merge pull request #24 from sasjs/dependabot/npm_and_yarn/webpack-4.44.1
chore(deps-dev): bump webpack from 4.43.0 to 4.44.1
2020-08-04 20:57:22 +02:00
Allan Bowe
36ea148446 Merge branch 'master' into dependabot/npm_and_yarn/webpack-4.44.1 2020-08-04 20:56:51 +02:00
Allan Bowe
762254d8c4 Merge pull request #29 from sasjs/deployFixing
fix: makeRequest retry, getFolderUri error catching
2020-08-04 20:56:20 +02:00
Mihajlo Medjedovic
8474b222ea fix: makeRequest retry, getFolderUri error catching 2020-08-04 20:39:09 +02:00
dependabot-preview[bot]
c1750c014e chore(deps-dev): bump webpack from 4.43.0 to 4.44.1
Bumps [webpack](https://github.com/webpack/webpack) from 4.43.0 to 4.44.1.
- [Release notes](https://github.com/webpack/webpack/releases)
- [Commits](https://github.com/webpack/webpack/compare/v4.43.0...v4.44.1)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2020-08-03 20:41:21 +00:00
Krishna Acondy
d7a7909529 Merge pull request #26 from sasjs/dependabot/npm_and_yarn/tslint-6.1.3
chore(deps-dev): bump tslint from 6.1.2 to 6.1.3
2020-08-03 21:39:07 +01:00
dependabot-preview[bot]
31b60a985e chore(deps-dev): bump tslint from 6.1.2 to 6.1.3
Bumps [tslint](https://github.com/palantir/tslint) from 6.1.2 to 6.1.3.
- [Release notes](https://github.com/palantir/tslint/releases)
- [Changelog](https://github.com/palantir/tslint/blob/master/CHANGELOG.md)
- [Commits](https://github.com/palantir/tslint/compare/6.1.2...6.1.3)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2020-08-03 20:38:05 +00:00
Krishna Acondy
a6b13d9cb9 Merge pull request #27 from sasjs/dependabot/npm_and_yarn/types/jest-26.0.8
chore(deps-dev): bump @types/jest from 26.0.5 to 26.0.8
2020-08-03 21:35:56 +01:00
dependabot-preview[bot]
55fcbf2e36 chore(deps-dev): bump @types/jest from 26.0.5 to 26.0.8
Bumps [@types/jest](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/jest) from 26.0.5 to 26.0.8.
- [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-08-03 19:34:31 +00:00
Krishna Acondy
fad8549d92 Merge pull request #28 from sasjs/dependabot/npm_and_yarn/ts-loader-8.0.2
chore(deps-dev): bump ts-loader from 8.0.1 to 8.0.2
2020-08-03 20:32:10 +01:00
dependabot-preview[bot]
95c03e5d07 chore(deps-dev): bump ts-loader from 8.0.1 to 8.0.2
Bumps [ts-loader](https://github.com/TypeStrong/ts-loader) from 8.0.1 to 8.0.2.
- [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/v8.0.1...v8.0.2)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2020-08-03 18:57:46 +00:00
Krishna Acondy
e241a47c23 Merge pull request #11 from sasjs/api-execution
feat(*): Job Execution Approaches
2020-08-03 19:52:59 +01:00
Krishna Acondy
ed7f36dbed Merge branch 'api-execution' of https://github.com/sasjs/adapter into api-execution 2020-08-03 19:51:21 +01:00
Krishna Acondy
74f0c263db Merge branch 'master' of https://github.com/sasjs/adapter into api-execution 2020-08-03 19:50:30 +01:00
Krishna Acondy
5a2a4bf39c Merge pull request #19 from sasjs/issue17
File upload function and separate WEB and API csrf tokens
2020-08-01 19:10:29 +01:00
Mihajlo Medjedovic
0ea91ddd3b Merge branch 'api-execution' into issue17 2020-07-31 19:08:39 +02:00
Mihajlo Medjedovic
4422c37827 fix: retry requests with API approach 2020-07-30 00:43:12 +02:00
Krishna Acondy
5fad9d01bc chore(sasjs-tests): remove unnecessary code, bump test framework version 2020-07-29 22:52:17 +01:00
Mihajlo Medjedovic
fb02c77a3a fix: login required callback 2020-07-29 13:20:03 +02:00
Mihajlo Medjedovic
5de84c07a8 fix: computeApi approach loginRequired callback 2020-07-29 12:33:18 +02:00
Mihajlo Medjedovic
54e2319183 fix: public upload file params 2020-07-28 14:10:46 +02:00
Mihajlo Medjedovic
261913d2d7 fix: uploadFile accept multiple files 2020-07-28 13:23:57 +02:00
Allan Bowe
187917cb32 Merge pull request #20 from sasjs/use-test-framework
chore(sasjs-tests): use test framework in SASjs Tests
2020-07-23 10:15:12 +02:00
Allan Bowe
d22b6c77b3 Merge branch 'master' into use-test-framework 2020-07-23 10:14:30 +02:00
Allan Bowe
785b276741 chore: doc update 2020-07-22 23:34:41 +02:00
Krishna Acondy
2ce3669b10 Merge branch 'api-execution' of https://github.com/sasjs/adapter into issue17 2020-07-22 20:00:19 +01:00
Krishna Acondy
3a1ea1614f Merge branch 'master' of https://github.com/sasjs/adapter into issue17 2020-07-22 19:57:10 +01:00
Krishna Acondy
3c5988aacf chore(sasjs-tests): add missing dependency 2020-07-22 19:55:14 +01:00
Krishna Acondy
1a25c354fa Merge pull request #13 from sasjs/requestConfigOverride
Request config override
2020-07-22 19:53:33 +01:00
Krishna Acondy
eb1668d6c1 Merge pull request #15 from sasjs/dependabot/npm_and_yarn/types/jest-26.0.5
chore(deps-dev): bump @types/jest from 26.0.4 to 26.0.5
2020-07-22 19:46:32 +01:00
Krishna Acondy
69d088a9c6 chore(sasjs-tests): use test framework in SASjs Tests 2020-07-22 19:42:34 +01:00
Krishna Acondy
ccd44c31c7 feat(file-upload): move functionality into FileUploader 2020-07-22 07:14:31 +01:00
Krishna Acondy
dec7c18ecb Merge branch 'api-execution' of https://github.com/sasjs/adapter into api-execution 2020-07-22 06:42:53 +01:00
Krishna Acondy
c8a5eb5993 chore(deploy): update script to deploy tests 2020-07-22 06:42:50 +01:00
Mihajlo Medjedovic
700a67a600 feat: file upload function, WEB and API csrf tokens 2020-07-21 22:35:43 +02:00
Mihajlo Medjedovic
a2778bed52 docs: request() update 2020-07-20 23:03:05 +02:00
Mihajlo Medjedovic
2b04fe0c3e docs: update 2020-07-20 21:31:54 +02:00
Mihajlo Medjedovic
1c1e6b8efd fix: waiting requests passing config 2020-07-20 20:44:30 +02:00
Mihajlo Medjedovic
c92a0a53f2 Merge pull request #16 from sasjs/csrfTokenGetter
feat: csrf token main file sync
2020-07-20 20:35:43 +02:00
Mihajlo Medjedovic
ee024c67ab feat: csrf token main file sync 2020-07-20 20:34:12 +02:00
dependabot-preview[bot]
27301651be chore(deps-dev): bump @types/jest from 26.0.4 to 26.0.5
Bumps [@types/jest](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/jest) from 26.0.4 to 26.0.5.
- [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-07-20 08:58:58 +00:00
Krishna Acondy
9742f53dde Merge pull request #12 from sasjs/dependabot/npm_and_yarn/typescript-3.9.7
chore(deps-dev): bump typescript from 3.9.6 to 3.9.7
2020-07-20 08:19:25 +01:00
dependabot-preview[bot]
38a950a036 chore(deps-dev): bump typescript from 3.9.6 to 3.9.7
Bumps [typescript](https://github.com/Microsoft/TypeScript) from 3.9.6 to 3.9.7.
- [Release notes](https://github.com/Microsoft/TypeScript/releases)
- [Commits](https://github.com/Microsoft/TypeScript/compare/v3.9.6...v3.9.7)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2020-07-20 07:17:13 +00:00
Allan Bowe
e41c54e37e docs: adding link attributes due to security vulnerability 2020-07-19 19:57:03 +02:00
Mihajlo Medjedovic
405a19f0cf fix: local config 2020-07-19 17:44:29 +02:00
Mihajlo Medjedovic
c7d6c66093 feat: request config override 2020-07-19 16:10:56 +02:00
Krishna Acondy
8bf74d17e9 fix(*): handle login required state, fix session creation logic 2020-07-18 15:08:40 +01:00
Krishna Acondy
a12244cf78 feat(session-manager): Manage a pool of sessions for job execution 2020-07-17 08:31:27 +01:00
Krishna Acondy
a579f481c5 fix(context): cache contexts after first request 2020-07-16 17:31:03 +01:00
Krishna Acondy
2ecd57169f fix(context): cache contexts after first request 2020-07-16 17:27:28 +01:00
Krishna Acondy
7d84033ad4 feat(compute-api): implement job execution via compute API 2020-07-16 09:06:24 +01:00
Allan Bowe
c4109b225b chore: readme update 2020-07-15 16:00:05 +02:00
Allan Bowe
2ec8696615 chore: readme update 2020-07-15 15:40:58 +02:00
Allan Bowe
c5339276e4 chore: readme update 2020-07-15 15:38:16 +02:00
Allan Bowe
97db960f62 chore: updating example.html to be consistent with README 2020-07-15 13:33:04 +02:00
Krishna Acondy
0ac7f8892e fix(build): switch default branch to master 2020-07-15 12:25:20 +01:00
Krishna Acondy
68f5e5bec5 Merge pull request #10 from sasjs/main
Merge main into master
2020-07-15 12:21:45 +01:00
Krishna Acondy
69a14ff6d7 Merge branch 'master' into main 2020-07-15 12:20:08 +01:00
Krishna Acondy
70e461224a Merge pull request #8 from sasjs/allanbowe-patch-1
Update npmpublish.yml
2020-07-15 12:18:46 +01:00
Krishna Acondy
4da22ee6b4 Merge pull request #9 from sasjs/dependabot/npm_and_yarn/ts-loader-8.0.1
chore(deps-dev): bump ts-loader from 7.0.5 to 8.0.1
2020-07-15 12:18:22 +01:00
Allan Bowe
2c8ba09578 docs: viya test routine in CONTRIBUTING.md 2020-07-15 11:12:46 +02:00
dependabot-preview[bot]
c12d6f1c9c chore(deps-dev): bump ts-loader from 7.0.5 to 8.0.1
Bumps [ts-loader](https://github.com/TypeStrong/ts-loader) from 7.0.5 to 8.0.1.
- [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/v7.0.5...v8.0.1)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2020-07-15 08:35:48 +00:00
Krishna Acondy
92504b0c16 feat(jes-api): implement job execution via API to JES 2020-07-13 18:38:30 +01:00
Allan Bowe
e5fb7a7698 Update npmpublish.yml 2020-07-11 18:23:19 +02:00
Krishna Acondy
c22b9066d8 chore(sasjs-tests): update package version 2020-07-11 11:39:59 +01:00
Krishna Acondy
77d7e03de5 fix(*): switch to file upload approach with large datasets and special characters 2020-07-11 11:27:45 +01:00
Krishna Acondy
b614bafd03 chore(*): update references to master 2020-07-09 08:14:50 +01:00
Krishna Acondy
34cabcde2d fix(ci): change target branch for build action 2020-07-09 08:13:53 +01:00
Krishna Acondy
de82058850 fix(deps-ci): update build, remove unnecessary dependencies 2020-07-09 08:13:28 +01:00
Krishna Acondy
327be9e141 Merge pull request #2 from sasjs/dependabot/npm_and_yarn/npm-6.14.6
chore(deps): [security] bump npm from 6.14.5 to 6.14.6
2020-07-09 08:09:16 +01:00
dependabot-preview[bot]
f1502c0773 chore(deps): [security] bump npm from 6.14.5 to 6.14.6
Bumps [npm](https://github.com/npm/cli) from 6.14.5 to 6.14.6. **This update includes a security fix.**
- [Release notes](https://github.com/npm/cli/releases)
- [Changelog](https://github.com/npm/cli/blob/latest/CHANGELOG.md)
- [Commits](https://github.com/npm/cli/compare/v6.14.5...v6.14.6)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2020-07-09 07:08:09 +00:00
Krishna Acondy
c8a2df2d1f Merge pull request #3 from sasjs/dependabot/npm_and_yarn/types/jest-26.0.4
chore(deps-dev): bump @types/jest from 26.0.3 to 26.0.4
2020-07-09 08:04:09 +01:00
Krishna Acondy
2be6200b90 Merge branch 'master' into dependabot/npm_and_yarn/types/jest-26.0.4 2020-07-09 07:56:47 +01:00
Krishna Acondy
333289cd20 Merge pull request #4 from sasjs/dependabot/npm_and_yarn/npm-registry-fetch-4.0.5
chore(deps): [security] bump npm-registry-fetch from 4.0.4 to 4.0.5
2020-07-09 07:56:31 +01:00
Krishna Acondy
204139cd01 Merge branch 'master' into dependabot/npm_and_yarn/npm-registry-fetch-4.0.5 2020-07-09 07:55:28 +01:00
Krishna Acondy
2a38b68e69 Merge pull request #5 from sasjs/dependabot/npm_and_yarn/typedoc-neo-theme-1.0.9
chore(deps-dev): bump typedoc-neo-theme from 1.0.8 to 1.0.9
2020-07-09 07:55:13 +01:00
Krishna Acondy
39cc20b680 Merge branch 'master' into dependabot/npm_and_yarn/typedoc-neo-theme-1.0.9 2020-07-09 07:53:48 +01:00
Allan Bowe
8b3c9746fc Merge pull request #6 from sasjs/allanbowe-patch-1
Update README.md
2020-07-08 23:35:59 +02:00
Allan Bowe
7a76f5f343 Merge branch 'master' into allanbowe-patch-1 2020-07-08 23:35:49 +02:00
Allan Bowe
2bbcd7dee7 Update README.md 2020-07-08 23:35:20 +02:00
Allan Bowe
b02ce07ddf Update README.md 2020-07-08 23:34:59 +02:00
dependabot-preview[bot]
41400bea86 chore(deps-dev): bump typedoc-neo-theme from 1.0.8 to 1.0.9
Bumps [typedoc-neo-theme](https://github.com/google/typedoc-neo-theme) from 1.0.8 to 1.0.9.
- [Release notes](https://github.com/google/typedoc-neo-theme/releases)
- [Commits](https://github.com/google/typedoc-neo-theme/compare/v1.0.8...v1.0.9)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2020-07-07 20:07:49 +00:00
dependabot-preview[bot]
991ac100f6 chore(deps): [security] bump npm-registry-fetch from 4.0.4 to 4.0.5
Bumps [npm-registry-fetch](https://github.com/npm/registry-fetch) from 4.0.4 to 4.0.5. **This update includes a security fix.**
- [Release notes](https://github.com/npm/registry-fetch/releases)
- [Changelog](https://github.com/npm/npm-registry-fetch/blob/latest/CHANGELOG.md)
- [Commits](https://github.com/npm/registry-fetch/commits)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2020-07-07 20:07:02 +00:00
dependabot-preview[bot]
66c156d299 chore(deps-dev): bump @types/jest from 26.0.3 to 26.0.4
Bumps [@types/jest](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/jest) from 26.0.3 to 26.0.4.
- [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-07-07 20:06:44 +00:00
Krishna Acondy
7d0d830391 Set theme jekyll-theme-minimal 2020-07-07 21:00:20 +01:00
Krishna Acondy
0b038380c7 chore(doc): update documentation 2020-07-07 20:51:44 +01:00
Krishna Acondy
162ba5e837 chore(example): update adapter import in example 2020-07-07 20:47:28 +01:00
Krishna Acondy
f217b3eb04 fix(doc): update readme references 2020-07-07 20:44:15 +01:00
Krishna Acondy
6b9436b1c6 fix(doc): update readme 2020-07-07 20:35:26 +01:00
Krishna Acondy
3b40e6cd14 fix(package): make package public 2020-07-07 20:11:55 +01:00
103 changed files with 21985 additions and 4503 deletions

View File

@@ -1,6 +1,6 @@
# Contributing
Contributions to SASjs are very welcome! When making a PR, test cases should be included. To help in unit testing, be sure to run the following when making changes:
Contributions to SASjs are very welcome! When making a PR, test cases should be included. To help in unit testing, be sure to run the following when making changes:
```
# the following creates a tarball in the build folder of SASjs
@@ -10,12 +10,13 @@ npm run-script package:lib
npm install ../sasjs/build/<tarball filename>
```
Tests are run using cypress. Before running tests, you need to define the following backend services:
Tests are run using cypress. Before running tests, you need to define the following backend services:
# SAS 9
```
filename mc url "https://raw.githubusercontent.com/macropeople/macrocore/master/mc_all.sas?_=1";
filename mc url "https://raw.githubusercontent.com/sasjs/core/main/all.sas";
%inc mc;
filename ft15f001 temp;
parmcards4;
@@ -37,19 +38,15 @@ parmcards4;
```
# Viya
```
filename mc url "https://raw.githubusercontent.com/macropeople/macrocore/master/mc_all.sas";
%inc mc;
```
filename mc url "https://raw.githubusercontent.com/sasjs/core/main/all.sas";
%inc mc;
filename ft15f001 temp;
parmcards4;
%webout(FETCH)
%webout(OPEN)
%global sasjs_tables;
%let sasjs_tables=&sasjs_tables;
%put &=sasjs_tables;
%let sasjs_tables=&sasjs_tables;
%macro x();
%global sasjs_tables;
%do i=1 %to %sysfunc(countw(&sasjs_tables));
%let table=%scan(&sasjs_tables,&i);
%webout(OBJ,&table)
@@ -58,13 +55,11 @@ parmcards4;
%x()
%webout(CLOSE)
;;;;
%mv_createwebservice(path=/Public/app/common,name=sendObj)
%mp_createwebservice(path=/Public/app/common,name=sendObj)
filename ft15f001 temp;
parmcards4;
%webout(FETCH)
%webout(OPEN)
%global sasjs_tables;
%let sasjs_tables=&sasjs_tables;
%put &=sasjs_tables;
%macro x();
%do i=1 %to %sysfunc(countw(&sasjs_tables));
%let table=%scan(&sasjs_tables,&i);
@@ -74,7 +69,7 @@ parmcards4;
%x()
%webout(CLOSE)
;;;;
%mv_createwebservice(path=/Public/app/common,name=sendArr)
%mp_createwebservice(path=/Public/app/common,name=sendArr)
```
The above services will return anything you send. To run the tests simply launch `npm run cypress`.
The above services will return anything you send. To run the tests simply launch `npm run cypress`.

View File

@@ -1,31 +1,29 @@
[![](https://data.jsdelivr.com/v1/package/npm/sasjs/badge)](https://www.jsdelivr.com/package/npm/sasjs)
[![](https://data.jsdelivr.com/v1/package/npm/@sasjs/adapter/badge)](https://www.jsdelivr.com/package/npm/@sasjs/adapter)
# SASjs
# @sasjs/adapter
SASjs is a open-source framework for building Web Apps on SAS® platforms. You can use as much or as little of it as you like. This repository contains the JS adapter, the part that handles the to/from SAS communication on the client side. There are 3 ways to install it:
1 - `npm install sasjs` - for use in a node project
1 - `npm install @sasjs/adapter` - for use in a node project
2 - [Download](https://cdn.jsdelivr.net/npm/sasjs/index.js) and use a copy of the latest JS file
2 - [Download](https://cdn.jsdelivr.net/npm/@sasjs/adapter@1/index.js) and use a copy of the latest JS file
3 - Reference directly from the CDN - in which case click [here](https://www.jsdelivr.com/package/npm/sasjs?tab=collection) and select "SRI" to get the script tag with the integrity hash.
3 - Reference directly from the CDN - in which case click [here](https://www.jsdelivr.com/package/npm/@sasjs/adapter?tab=collection) and select "SRI" to get the script tag with the integrity hash.
If you are short on time and just need to build an app quickly, then check out [this video](https://vimeo.com/393161794) and the [react-seed-app](https://github.com/macropeople/react-seed-app) which provides some boilerplate.
If you are short on time and just need to build an app quickly, then check out [this video](https://vimeo.com/393161794) and the [react-seed-app](https://github.com/sasjs/react-seed-app) which provides some boilerplate.
For more information on building web apps with SAS, check out [sasjs.io](https://sasjs.io)
## None of this makes sense. How do I build an app with it?
## None of this makes sense. How do I build an app with it?
Ok ok. Deploy this [example.html](https://github.com/macropeople/sasjs/blob/master/example.html) file to your web server, and update `servertype` to `SAS9` or `SASVIYA` depending on your backend.
Ok ok. Deploy this [example.html](https://raw.githubusercontent.com/sasjs/adapter/master/example.html) file to your web server, and update `servertype` to `SAS9` or `SASVIYA` depending on your backend.
The backend part can be deployed as follows:
```
%let appLoc=/Public/app/readme; /* Metadata or Viya Folder location as per SASjs config */
/* compile macros (can also be downloaded & compiled seperately) */
filename mc url "https://raw.githubusercontent.com/macropeople/macrocore/master/mc_all.sas";
%inc mc;
%let appLoc=/Public/app/readme; /* Metadata or Viya Folder per SASjs config */
filename mc url "https://raw.githubusercontent.com/sasjs/core/main/all.sas";
%inc mc; /* compile macros (can also be downloaded & compiled seperately) */
filename ft15f001 temp;
parmcards4;
%webout(FETCH) /* receive all data as SAS datasets */
@@ -45,6 +43,6 @@ You now have a simple web app with a backend service!
# More resources
For more information specific to this adapter you can check out this [user guide](https://sasjs.io/sasjs/sasjs-adapter/) or the [technical](http://adapter.sasjs.io/) documentation.
For more information specific to this adapter you can check out this [user guide](https://sasjs.io/sasjs-adapter/) or the [technical](http://adapter.sasjs.io/) documentation.
For more information on building web apps in general, check out these [resources](https://sasjs.io/training/resources/) or contact the [author](https://www.linkedin.com/in/allanbowe/) directly.
For more information on building web apps in general, check out these [resources](https://sasjs.io/training/resources/) or contact the [author](https://www.linkedin.com/in/allanbowe/) directly.

1
docs/_config.yml Normal file
View File

@@ -0,0 +1 @@
theme: jekyll-theme-minimal

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

168
docs/modules/types.html Normal file

File diff suppressed because one or more lines are too long

579
docs/modules/utils.html Normal file

File diff suppressed because one or more lines are too long

View File

@@ -1,12 +1,12 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" http-equiv="X-UA-Compatible" content="IE=edge" />
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/css/bootstrap.min.css" integrity="sha384-Vkoo8x4CGsO3+Hhxv8T/Q5PaXtkKtu6ug5TOeNV6gBiFeWPGFN9MuhOf23Q9Ifjh" crossorigin="anonymous">
<script src="https://cdn.jsdelivr.net/combine/npm/chart.js@2.9.3,npm/jquery@3.5.1,npm/sasjs@2.11.0"></script>
<script src="https://cdn.jsdelivr.net/combine/npm/chart.js@2.9.3,npm/jquery@3.5.1,npm/@sasjs/adapter@1.0.6"></script>
<script>
var sasJs = new SASjs.default({appLoc: "/Products/demo/readme"
,serverType:"SAS9", debug: "false"
var sasJs = new SASjs.default({
appLoc: "/Public/app/readme"
,serverType:"SAS9"
,debug: false
});
function initSasJs() {
$('#loading-spinner').show()
@@ -70,6 +70,8 @@
});
}
</script>
<meta charset="utf-8" http-equiv="X-UA-Compatible" content="IE=edge" />
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/css/bootstrap.min.css" integrity="sha384-Vkoo8x4CGsO3+Hhxv8T/Q5PaXtkKtu6ug5TOeNV6gBiFeWPGFN9MuhOf23Q9Ifjh" crossorigin="anonymous">
</head>
<body>
<div class="container-fluid" style="text-align: center; margin-top: 10px;">

3602
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -13,6 +13,9 @@
"semantic-release": "semantic-release",
"typedoc": "typedoc"
},
"publishConfig": {
"access": "public"
},
"release": {
"plugins": [
"@semantic-release/npm",
@@ -33,26 +36,24 @@
},
"license": "ISC",
"devDependencies": {
"@cypress/webpack-preprocessor": "^4.1.5",
"@types/isomorphic-fetch": "0.0.35",
"@types/jest": "^26.0.3",
"@types/jest": "^26.0.9",
"cp": "^0.2.0",
"cypress": "^4.9.0",
"jest": "^25.5.4",
"path": "^0.12.7",
"prettier": "^2.0.5",
"rimraf": "^3.0.2",
"semantic-release": "^17.1.1",
"ts-jest": "^25.5.1",
"ts-loader": "^7.0.5",
"tslint": "^6.1.2",
"ts-loader": "^8.0.2",
"tslint": "^6.1.3",
"tslint-config-prettier": "^1.18.0",
"typedoc": "^0.17.8",
"typedoc-neo-theme": "^1.0.8",
"typedoc-neo-theme": "^1.0.9",
"typedoc-plugin-external-module-name": "^4.0.3",
"typescript": "^3.9.6",
"typescript": "^3.9.7",
"uglifyjs-webpack-plugin": "^2.2.0",
"webpack": "^4.43.0",
"webpack": "^4.44.1",
"webpack-cli": "^3.3.12"
},
"main": "index.js",

View File

@@ -1,6 +1,6 @@
{
"name": "sasjs-tests",
"version": "0.1.0",
"name": "@sasjs/tests",
"version": "1.0.0",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
@@ -1356,11 +1356,81 @@
"resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-1.1.3.tgz",
"integrity": "sha512-shAmDyaQC4H92APFoIaVDHCx5bStIocgvbwQyxPRrbUY20V1EYTbSDchWbuwlMG3V17cprZhA6+78JfB+3DTPw=="
},
"@sasjs/adapter": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/@sasjs/adapter/-/adapter-1.2.0.tgz",
"integrity": "sha512-PcQcmb7TsfPJ94tzFnvycm+tMYD3wKx2a6niwHfsV9+g6XHtmwReVV3EPZZ5XB4s565vU6Qc+ZnFbMIAeik8QA==",
"requires": {
"es6-promise": "^4.2.8",
"form-data": "^3.0.0",
"isomorphic-fetch": "^2.2.1"
},
"dependencies": {
"form-data": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-3.0.0.tgz",
"integrity": "sha512-CKMFDglpbMi6PyN+brwB9Q/GOw0eAnsrEZDgcsH5Krhz5Od/haKHAX0NmQfha2zPPz0JpWzA7GJHGSnvCRLWsg==",
"requires": {
"asynckit": "^0.4.0",
"combined-stream": "^1.0.8",
"mime-types": "^2.1.12"
}
}
}
},
"@sasjs/test-framework": {
"version": "1.3.3",
"resolved": "https://registry.npmjs.org/@sasjs/test-framework/-/test-framework-1.3.3.tgz",
"integrity": "sha512-Ou4UXlxBAVR8jv7boVvJ/eKLHRTQvDi9LouPAasLCO2EC4AD0wX1hLMwVhmydCvsdgVEeXs6InvX3ROHiKSADg==",
"requires": {
"@types/react-highlight.js": "^1.0.0",
"immer": "^7.0.7",
"moment": "^2.27.0",
"react-highlight.js": "^1.0.7",
"semantic-ui-css": "^2.4.1",
"semantic-ui-react": "^1.0.0"
},
"dependencies": {
"immer": {
"version": "7.0.7",
"resolved": "https://registry.npmjs.org/immer/-/immer-7.0.7.tgz",
"integrity": "sha512-Q8yYwVADJXrNfp1ZUAh4XDHkcoE3wpdpb4mC5abDSajs2EbW8+cGdPyAnglMyLnm7EF6ojD2xBFX7L5i4TIytw=="
}
}
},
"@semantic-ui-react/event-stack": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/@semantic-ui-react/event-stack/-/event-stack-3.1.1.tgz",
"integrity": "sha512-SA7VOu/tY3OkooR++mm9voeQrJpYXjJaMHO1aFCcSouS2xhqMR9Gnz0LEGLOR0h9ueWPBKaQzKIrx3FTTJZmUQ==",
"requires": {
"exenv": "^1.2.2",
"prop-types": "^15.6.2"
}
},
"@sheerun/mutationobserver-shim": {
"version": "0.3.3",
"resolved": "https://registry.npmjs.org/@sheerun/mutationobserver-shim/-/mutationobserver-shim-0.3.3.tgz",
"integrity": "sha512-DetpxZw1fzPD5xUBrIAoplLChO2VB8DlL5Gg+I1IR9b2wPqYIca2WSUxL5g1vLeR4MsQq1NeWriXAVffV+U1Fw=="
},
"@stardust-ui/react-component-event-listener": {
"version": "0.38.0",
"resolved": "https://registry.npmjs.org/@stardust-ui/react-component-event-listener/-/react-component-event-listener-0.38.0.tgz",
"integrity": "sha512-sIP/e0dyOrrlb8K7KWumfMxj/gAifswTBC4o68Aa+C/GA73ccRp/6W1VlHvF/dlOR4KLsA+5SKnhjH36xzPsWg==",
"requires": {
"@babel/runtime": "^7.1.2",
"prop-types": "^15.7.2"
}
},
"@stardust-ui/react-component-ref": {
"version": "0.38.0",
"resolved": "https://registry.npmjs.org/@stardust-ui/react-component-ref/-/react-component-ref-0.38.0.tgz",
"integrity": "sha512-xjs6WnvJVueSIXMWw0C3oWIgAPpcD03qw43oGOjUXqFktvpNkB73JoKIhS4sCrtQxBdct75qqr4ZL6JiyPcESw==",
"requires": {
"@babel/runtime": "^7.1.2",
"prop-types": "^15.7.2",
"react-is": "^16.6.3"
}
},
"@svgr/babel-plugin-add-jsx-attribute": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/@svgr/babel-plugin-add-jsx-attribute/-/babel-plugin-add-jsx-attribute-4.2.0.tgz",
@@ -1836,6 +1906,14 @@
"@types/react": "*"
}
},
"@types/react-highlight.js": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/@types/react-highlight.js/-/react-highlight.js-1.0.0.tgz",
"integrity": "sha512-5VXEuo2O9L66y/2GDQSGFTggQkpOvDc/p2ma1KHadu7o/H720HK3Fr83epd4wtQky7B/RoCPat0SKyhlhiUo7A==",
"requires": {
"@types/react": "*"
}
},
"@types/react-router": {
"version": "5.1.8",
"resolved": "https://registry.npmjs.org/@types/react-router/-/react-router-5.1.8.tgz",
@@ -3754,6 +3832,11 @@
"shallow-clone": "^0.1.2"
}
},
"clsx": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/clsx/-/clsx-1.1.1.tgz",
"integrity": "sha512-6/bPho624p3S2pMyvP5kKBPXnI3ufHLObBFCfgx+LkeR5lg2XYy2hqZqUf45ypD8COn2bhgGJSUE+l5dhNBieA=="
},
"co": {
"version": "4.6.0",
"resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz",
@@ -4095,6 +4178,15 @@
"sha.js": "^2.4.8"
}
},
"create-react-context": {
"version": "0.3.0",
"resolved": "https://registry.npmjs.org/create-react-context/-/create-react-context-0.3.0.tgz",
"integrity": "sha512-dNldIoSuNSvlTJ7slIKC/ZFGKexBMBrrcc+TTe1NdmROnaASuLPvqpwj9v4XS4uXZ8+YPu0sNmShX2rXI5LNsw==",
"requires": {
"gud": "^1.0.0",
"warning": "^4.0.3"
}
},
"cross-spawn": {
"version": "6.0.5",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz",
@@ -4912,11 +5004,21 @@
"integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k="
},
"encoding": {
"version": "0.1.12",
"resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.12.tgz",
"integrity": "sha1-U4tm8+5izRq1HsMjgp0flIDHS+s=",
"version": "0.1.13",
"resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.13.tgz",
"integrity": "sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==",
"requires": {
"iconv-lite": "~0.4.13"
"iconv-lite": "^0.6.2"
},
"dependencies": {
"iconv-lite": {
"version": "0.6.2",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.2.tgz",
"integrity": "sha512-2y91h5OpQlolefMPmUlivelittSWy0rP+oYVpn6A7GwVHNE8AWzoYOBNmlwks3LobaJxgHCYZAnyNo2GgpNRNQ==",
"requires": {
"safer-buffer": ">= 2.1.2 < 3.0.0"
}
}
}
},
"end-of-stream": {
@@ -5609,6 +5711,11 @@
"strip-eof": "^1.0.0"
}
},
"exenv": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/exenv/-/exenv-1.2.2.tgz",
"integrity": "sha1-KueOhdmJQVhnCwPUe+wfA72Ru50="
},
"exit": {
"version": "0.1.2",
"resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz",
@@ -6478,6 +6585,11 @@
"resolved": "https://registry.npmjs.org/growly/-/growly-1.3.0.tgz",
"integrity": "sha1-8QdIy+dq+WS3yWyTxrzCivEgwIE="
},
"gud": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/gud/-/gud-1.0.0.tgz",
"integrity": "sha512-zGEOVKFM5sVPPrYs7J5/hYEw2Pof8KCyOwyhG8sAF26mCAeUFAcYPu1mwB7hhpIP29zOIBaDqwuHdLp0jvZXjw=="
},
"gzip-size": {
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/gzip-size/-/gzip-size-5.1.1.tgz",
@@ -6622,6 +6734,11 @@
"resolved": "https://registry.npmjs.org/hex-color-regex/-/hex-color-regex-1.1.0.tgz",
"integrity": "sha512-l9sfDFsuqtOqKDsQdqrMRk0U85RZc0RtOR9yPI7mRVOa4FsR/BVnZ0shmQRM96Ji99kYZP/7hn1cedc1+ApsTQ=="
},
"highlight.js": {
"version": "9.18.3",
"resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-9.18.3.tgz",
"integrity": "sha512-zBZAmhSupHIl5sITeMqIJnYCDfAEc3Gdkqj65wC1lpI468MMQeeQkhcIAvk+RylAkxrCcI9xy9piHiXeQ1BdzQ=="
},
"history": {
"version": "4.10.1",
"resolved": "https://registry.npmjs.org/history/-/history-4.10.1.tgz",
@@ -7974,6 +8091,11 @@
}
}
},
"jquery": {
"version": "3.5.1",
"resolved": "https://registry.npmjs.org/jquery/-/jquery-3.5.1.tgz",
"integrity": "sha512-XwIBPqcMn57FxfT+Go5pzySnm4KWkT1Tv7gjrpT1srtf8Weynl6R273VJ5GjkRb51IzMp5nbaPjJXMWeju2MKg=="
},
"js-base64": {
"version": "2.6.2",
"resolved": "https://registry.npmjs.org/js-base64/-/js-base64-2.6.2.tgz",
@@ -8123,6 +8245,11 @@
"object.assign": "^4.1.0"
}
},
"keyboard-key": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/keyboard-key/-/keyboard-key-1.1.0.tgz",
"integrity": "sha512-qkBzPTi3rlAKvX7k0/ub44sqOfXeLc/jcnGGmj5c7BJpU8eDrEVPyhCvNYAaoubbsLm9uGWwQJO1ytQK1a9/dQ=="
},
"killable": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/killable/-/killable-1.0.1.tgz",
@@ -8887,6 +9014,11 @@
"minimist": "^1.2.5"
}
},
"moment": {
"version": "2.27.0",
"resolved": "https://registry.npmjs.org/moment/-/moment-2.27.0.tgz",
"integrity": "sha512-al0MUK7cpIcglMv3YF13qSgdAIqxHTO7brRtaz3DlSULbqfazqkc5kEjNrLDOM7fsjshoFIihnU8snrP7zUvhQ=="
},
"move-concurrently": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/move-concurrently/-/move-concurrently-1.0.1.tgz",
@@ -9880,6 +10012,11 @@
"ts-pnp": "^1.1.6"
}
},
"popper.js": {
"version": "1.16.1",
"resolved": "https://registry.npmjs.org/popper.js/-/popper.js-1.16.1.tgz",
"integrity": "sha512-Wb4p1J4zyFTbM+u6WuO4XstYx4Ky9Cewe4DWrel7B0w6VVICvPwdOpotjzcf6eD8TsckVnIMNONQyPIUFOUbCQ=="
},
"portfinder": {
"version": "1.0.26",
"resolved": "https://registry.npmjs.org/portfinder/-/portfinder-1.0.26.tgz",
@@ -11321,11 +11458,34 @@
"resolved": "https://registry.npmjs.org/react-error-overlay/-/react-error-overlay-6.0.7.tgz",
"integrity": "sha512-TAv1KJFh3RhqxNvhzxj6LeT5NWklP6rDr2a0jaTfsZ5wSZWHOGeqQyejUp3xxLfPt2UpyJEcVQB/zyPcmonNFA=="
},
"react-highlight.js": {
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/react-highlight.js/-/react-highlight.js-1.0.7.tgz",
"integrity": "sha512-OVPKnV0ZvU+V//HExwbV8M9CWy49Eo/9y9pBN2OsNWUFPN6dE4YZBLmJW/5sM2DxI5v/QQLyxOnTnSSfGCP+9Q==",
"requires": {
"highlight.js": "^9.3.0",
"prop-types": "^15.6.0"
}
},
"react-is": {
"version": "16.13.1",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="
},
"react-popper": {
"version": "1.3.7",
"resolved": "https://registry.npmjs.org/react-popper/-/react-popper-1.3.7.tgz",
"integrity": "sha512-nmqYTx7QVjCm3WUZLeuOomna138R1luC4EqkW3hxJUrAe+3eNz3oFCLYdnPwILfn0mX1Ew2c3wctrjlUMYYUww==",
"requires": {
"@babel/runtime": "^7.1.2",
"create-react-context": "^0.3.0",
"deep-equal": "^1.1.1",
"popper.js": "^1.14.4",
"prop-types": "^15.6.1",
"typed-styles": "^0.0.7",
"warning": "^4.0.2"
}
},
"react-router": {
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/react-router/-/react-router-5.2.0.tgz",
@@ -11950,27 +12110,6 @@
"resolved": "https://registry.npmjs.org/sanitize.css/-/sanitize.css-10.0.0.tgz",
"integrity": "sha512-vTxrZz4dX5W86M6oVWVdOVe72ZiPs41Oi7Z6Km4W5Turyz28mrXSJhhEBZoRtzJWIv3833WKVwLSDWWkEfupMg=="
},
"sasjs": {
"version": "file:../build/sasjs-5.0.0.tgz",
"integrity": "sha512-8Ez2iS8BKzu2GG1Cwf/pe5PgNvdhowFodQNCTHIxMlDYgLqmg1mcpwRjJjnXF9A73gX0NkR65olYYAesp8cMMA==",
"requires": {
"es6-promise": "^4.2.8",
"form-data": "^3.0.0",
"isomorphic-fetch": "^2.2.1"
},
"dependencies": {
"form-data": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-3.0.0.tgz",
"integrity": "sha512-CKMFDglpbMi6PyN+brwB9Q/GOw0eAnsrEZDgcsH5Krhz5Od/haKHAX0NmQfha2zPPz0JpWzA7GJHGSnvCRLWsg==",
"requires": {
"asynckit": "^0.4.0",
"combined-stream": "^1.0.8",
"mime-types": "^2.1.12"
}
}
}
},
"sass-graph": {
"version": "2.2.5",
"resolved": "https://registry.npmjs.org/sass-graph/-/sass-graph-2.2.5.tgz",
@@ -12086,6 +12225,47 @@
"node-forge": "0.9.0"
}
},
"semantic-ui-css": {
"version": "2.4.1",
"resolved": "https://registry.npmjs.org/semantic-ui-css/-/semantic-ui-css-2.4.1.tgz",
"integrity": "sha512-Pkp0p9oWOxlH0kODx7qFpIRYpK1T4WJOO4lNnpNPOoWKCrYsfHqYSKgk5fHfQtnWnsAKy7nLJMW02bgDWWFZFg==",
"requires": {
"jquery": "x.*"
}
},
"semantic-ui-react": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/semantic-ui-react/-/semantic-ui-react-1.1.1.tgz",
"integrity": "sha512-QtzLNkK4MUe1HQo4S7/tIkSp4NFtxSGDzTMKxmvztMJ6jt+nKGmMyjpyxJsrm3ohU8Z3sTyBUyiBsDYW4jNtjw==",
"requires": {
"@babel/runtime": "^7.10.5",
"@semantic-ui-react/event-stack": "^3.1.0",
"@stardust-ui/react-component-event-listener": "~0.38.0",
"@stardust-ui/react-component-ref": "~0.38.0",
"clsx": "^1.1.1",
"keyboard-key": "^1.1.0",
"lodash": "^4.17.19",
"prop-types": "^15.7.2",
"react-is": "^16.8.6",
"react-popper": "^1.3.7",
"shallowequal": "^1.1.0"
},
"dependencies": {
"@babel/runtime": {
"version": "7.11.2",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.11.2.tgz",
"integrity": "sha512-TeWkU52so0mPtDcaCTxNBI/IHiz0pZgr8VEFqXFtZWpYD08ZB6FaSwVAS8MKRQAP3bYKiVjwysOJgMFY28o6Tw==",
"requires": {
"regenerator-runtime": "^0.13.4"
}
},
"lodash": {
"version": "4.17.19",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.19.tgz",
"integrity": "sha512-JNvd8XER9GQX0v2qJgsaN/mzFCNA5BRe/j8JN9d+tWyGLSodKQHKFicdwNYzWwI3wjRnaKPsGj1XkBjx/F96DQ=="
}
}
},
"semver": {
"version": "6.3.0",
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz",
@@ -12275,6 +12455,11 @@
}
}
},
"shallowequal": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/shallowequal/-/shallowequal-1.1.0.tgz",
"integrity": "sha512-y0m1JoUZSlPAjXVtPPW70aZWfIL/dSP7AFkRnniLCrK/8MDKog3TySTBmckD+RObVxH0v4Tox67+F14PdED2oQ=="
},
"shebang-command": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz",
@@ -13443,6 +13628,11 @@
"mime-types": "~2.1.24"
}
},
"typed-styles": {
"version": "0.0.7",
"resolved": "https://registry.npmjs.org/typed-styles/-/typed-styles-0.0.7.tgz",
"integrity": "sha512-pzP0PWoZUhsECYjABgCGQlRGL1n7tOHsgwYv3oIiEpJwGhFTuty/YNeduxQYzXXa3Ge5BdT6sHYIQYpl4uJ+5Q=="
},
"typedarray": {
"version": "0.0.6",
"resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz",
@@ -13744,6 +13934,14 @@
"makeerror": "1.0.x"
}
},
"warning": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/warning/-/warning-4.0.3.tgz",
"integrity": "sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w==",
"requires": {
"loose-envify": "^1.0.0"
}
},
"watchpack": {
"version": "1.7.2",
"resolved": "https://registry.npmjs.org/watchpack/-/watchpack-1.7.2.tgz",

View File

@@ -1,9 +1,11 @@
{
"name": "sasjs-tests",
"version": "0.1.0",
"name": "@sasjs/tests",
"version": "1.0.0",
"homepage": ".",
"private": true,
"dependencies": {
"@sasjs/adapter": "^1.2.0",
"@sasjs/test-framework": "^1.3.3",
"@testing-library/jest-dom": "^4.2.4",
"@testing-library/react": "^9.5.0",
"@testing-library/user-event": "^7.2.1",
@@ -16,7 +18,6 @@
"react-dom": "^16.13.1",
"react-router-dom": "^5.2.0",
"react-scripts": "3.4.1",
"sasjs": "file:../build/sasjs-5.0.0.tgz",
"typescript": "^3.9.6"
},
"scripts": {
@@ -24,7 +25,7 @@
"build": "react-scripts build",
"test": "react-scripts test",
"eject": "react-scripts eject",
"deploy": "rsync -avhe ssh ./build/* --delete kriaco@sas.analytium.co.uk:/var/www/html/kriaco/sasjs-tests"
"deploy": "cd .. && npm run package:lib && cd sasjs-tests && npm i ../build/sasjs-adapter-5.0.0.tgz && npm run build && rsync -avhe ssh ./build/* --delete kriaco@sas.analytium.co.uk:/var/www/html/kriaco/sasjs-tests"
},
"eslintConfig": {
"extends": "react-app"

View File

@@ -6,6 +6,7 @@
"appLoc": "/Public/app",
"serverType": "SASVIYA",
"debug": false,
"contextName": null
"contextName": "SharedCompute",
"useComputeApi": true
}
}

View File

@@ -1,102 +0,0 @@
.app {
padding: 16px;
.controls {
display: flex;
align-items: center;
.debug-toggle,
.app-loc-input,
.submit-button {
margin: 16px 0;
}
.row {
margin: 16px;
&.app-loc {
width: 20vw;
}
}
.submit-button {
padding: 16px;
font-size: 1.25em;
}
.app-loc-input {
width: 100%;
}
}
.debug-toggle {
display: inline-flex;
justify-content: center;
align-items: center;
.label {
padding: 0 8px;
font-size: 1.25em;
}
}
$height: 40px;
$width: 70px;
.switch {
position: relative;
display: inline-flex;
width: $width;
height: $height;
input[type="checkbox"] {
display: none;
}
input:checked + .knob {
animation: colorChange 0.4s linear forwards;
}
input:checked + .knob:before {
animation: turnON 0.4s linear forwards;
}
}
@keyframes colorChange {
from {
background-color: #ccc;
}
50% {
background-color: #a4d9ad;
}
to {
background-color: #4bd663;
}
}
@keyframes turnON {
from {
transform: translateX(0px);
}
to {
transform: translateX($width - ($height * 0.99));
box-shadow: -10px 0px 44px 0px #434343;
}
}
.knob {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: #ccc;
border-radius: $height;
}
.knob:before {
position: absolute;
background-color: white;
content: "";
left: $height * 0.1;
top: $height * 0.1;
width: ($height * 0.8);
height: ($height * 0.8);
border-radius: 50%;
}
}

View File

@@ -1,56 +1,30 @@
import React, { ReactElement, useState, useContext, useEffect } from "react";
import "./App.scss";
import TestSuiteRunner from "./TestSuiteRunner";
import { AppContext } from "./context/AppContext";
import { TestSuiteRunner, TestSuite, AppContext } from "@sasjs/test-framework";
import { basicTests } from "./testSuites/Basic";
import { sendArrTests, sendObjTests } from "./testSuites/RequestData";
import { specialCaseTests } from "./testSuites/SpecialCases";
import { sasjsRequestTests } from "./testSuites/SasjsRequests";
import "@sasjs/test-framework/dist/index.css";
const App = (): ReactElement<{}> => {
const [appLoc, setAppLoc] = useState("");
const [debug, setDebug] = useState(false);
const { adapter } = useContext(AppContext);
const { adapter, config } = useContext(AppContext);
const [testSuites, setTestSuites] = useState<TestSuite[]>([]);
useEffect(() => {
if (adapter) {
adapter.setDebugState(debug);
setTestSuites([
basicTests(adapter, config.userName, config.password),
sendArrTests(adapter),
sendObjTests(adapter),
specialCaseTests(adapter),
sasjsRequestTests(adapter),
]);
}
}, [debug, adapter]);
useEffect(() => {
if (appLoc && adapter) {
adapter.setSASjsConfig({ ...adapter.getSasjsConfig(), appLoc });
}
}, [appLoc, adapter]);
useEffect(() => {
setAppLoc(adapter.getSasjsConfig().appLoc);
}, [adapter]);
}, [adapter, config]);
return (
<div className="app">
<div className="controls">
<div className="row">
<label>Debug</label>
<div className="debug-toggle">
<label className="switch">
<input
type="checkbox"
onChange={(e) => setDebug(e.target.checked)}
/>
<span className="knob"></span>
</label>
</div>
</div>
<div className="row app-loc">
<label>App Loc</label>
<input
type="text"
className="app-loc-input"
value={appLoc}
onChange={(e) => setAppLoc(e.target.value)}
placeholder="AppLoc"
/>
</div>
</div>
{adapter && <TestSuiteRunner adapter={adapter} />}
{adapter && testSuites && <TestSuiteRunner testSuites={testSuites} />}
</div>
);
};

View File

@@ -1,6 +1,6 @@
import React, { ReactElement, useState, useCallback, useContext } from "react";
import "./Login.scss";
import { AppContext } from "./context/AppContext";
import { AppContext } from "@sasjs/test-framework";
import { Redirect } from "react-router-dom";
const Login = (): ReactElement<{}> => {
@@ -11,8 +11,8 @@ const Login = (): ReactElement<{}> => {
const handleSubmit = useCallback(
(e) => {
e.preventDefault();
appContext.adapter.logIn(username, password).then(() => {
appContext.setIsLoggedIn(true);
appContext.adapter.logIn(username, password).then((res) => {
appContext.setIsLoggedIn(res.isLoggedIn);
});
},
[username, password, appContext]

View File

@@ -1,6 +1,6 @@
import React, { ReactElement, useContext, FunctionComponent } from "react";
import { Redirect, Route } from "react-router-dom";
import { AppContext } from "./context/AppContext";
import { AppContext } from "@sasjs/test-framework";
interface PrivateRouteProps {
component: FunctionComponent;

View File

@@ -1,19 +0,0 @@
.button-container {
display: flex;
justify-content: center;
align-items: center;
padding: 16px;
.loading-spinner {
margin: 0 8px;
}
.submit-button {
padding: 10px;
min-height: 80px;
font-size: 2em;
display: flex;
justify-content: center;
align-items: center;
}
}

View File

@@ -1,126 +0,0 @@
import React, { useEffect, useState, ReactElement, useContext } from "react";
import TestSuiteComponent from "./components/TestSuite";
import TestSuiteCard from "./components/TestSuiteCard";
import { TestSuite, Test } from "./types";
import { basicTests } from "./testSuites/Basic";
import "./TestSuiteRunner.scss";
import SASjs from "sasjs";
import { AppContext } from "./context/AppContext";
import { sendArrTests, sendObjTests } from "./testSuites/RequestData";
import { specialCaseTests } from "./testSuites/SpecialCases";
import { sasjsRequestTests } from "./testSuites/SasjsRequests";
interface TestSuiteRunnerProps {
adapter: SASjs;
}
const TestSuiteRunner = (
props: TestSuiteRunnerProps
): ReactElement<TestSuiteRunnerProps> => {
const { adapter } = props;
const { config } = useContext(AppContext);
const [testSuites, setTestSuites] = useState<TestSuite[]>([]);
const [runTests, setRunTests] = useState(false);
const [completedTestSuites, setCompletedTestSuites] = useState<
{
name: string;
completedTests: {
test: Test;
result: boolean;
error: Error | null;
executionTime: number;
}[];
}[]
>([]);
const [currentTestSuite, setCurrentTestSuite] = useState<TestSuite | null>(
(null as unknown) as TestSuite
);
useEffect(() => {
if (adapter) {
setTestSuites([
// basicTests(adapter, config.userName, config.password),
// sendArrTests(adapter),
// sendObjTests(adapter),
specialCaseTests(adapter),
// sasjsRequestTests(adapter),
]);
setCompletedTestSuites([]);
}
}, [adapter]);
useEffect(() => {
if (testSuites.length) {
setCurrentTestSuite(testSuites[0]);
}
}, [testSuites]);
useEffect(() => {
if (runTests) {
setCompletedTestSuites([]);
setCurrentTestSuite(testSuites[0]);
}
}, [runTests, testSuites]);
return (
<>
<div className="button-container">
<button
className={runTests ? "submit-button disabled" : "submit-button"}
onClick={() => setRunTests(true)}
disabled={runTests}
>
{runTests ? (
<>
<div className="loading-spinner"></div>Running tests...
</>
) : (
"Run tests!"
)}
</button>
</div>
{completedTestSuites.map((completedTestSuite, index) => {
return (
<TestSuiteCard
key={index}
tests={completedTestSuite.completedTests}
name={completedTestSuite.name}
/>
);
})}
{currentTestSuite && runTests && (
<TestSuiteComponent
{...currentTestSuite}
onCompleted={(
name,
completedTests: {
test: Test;
result: boolean;
error: Error | null;
executionTime: number;
}[]
) => {
const currentIndex = testSuites.indexOf(currentTestSuite);
const nextIndex =
currentIndex < testSuites.length - 1 ? currentIndex + 1 : -1;
if (nextIndex >= 0) {
setCurrentTestSuite(testSuites[nextIndex]);
} else {
setCurrentTestSuite(null);
}
const newCompletedTestSuites = [
...completedTestSuites,
{ name, completedTests },
];
setCompletedTestSuites(newCompletedTestSuites);
if (newCompletedTestSuites.length === testSuites.length) {
setRunTests(false);
}
}}
/>
)}
</>
);
};
export default TestSuiteRunner;

View File

@@ -1,79 +0,0 @@
import React, { ReactElement, useEffect, useState } from "react";
import TestCard from "./TestCard";
import { start } from "repl";
interface TestProps {
title: string;
description: string;
beforeTest?: (...args: any) => Promise<any>;
afterTest?: (...args: any) => Promise<any>;
test: (context: any) => Promise<any>;
assertion: (...args: any) => boolean;
onCompleted: (payload: {
result: boolean;
error: Error | null;
executionTime: number;
}) => void;
context: any;
}
const getStatus = (isRunning: boolean, isPassed: boolean): string => {
return isRunning ? "running" : isPassed ? "passed" : "failed";
};
const Test = (props: TestProps): ReactElement<TestProps> => {
const {
title,
description,
test,
beforeTest,
afterTest,
assertion,
onCompleted,
context,
} = props;
const beforeTestFunction = beforeTest ? beforeTest : () => Promise.resolve();
const afterTestFunction = afterTest ? afterTest : () => Promise.resolve();
const [isRunning, setIsRunning] = useState(false);
const [isPassed, setIsPassed] = useState(false);
useEffect(() => {
if (test && assertion) {
const startTime = new Date().valueOf();
setIsRunning(true);
setIsPassed(false);
beforeTestFunction()
.then(() => test(context))
.then((res) => {
setIsRunning(false);
setIsPassed(assertion(res, context));
return Promise.resolve(assertion(res, context));
})
.then((testResult) => {
afterTestFunction();
const endTime = new Date().valueOf();
const executionTime = (endTime - startTime) / 1000;
onCompleted({ result: testResult, error: null, executionTime });
})
.catch((e) => {
setIsRunning(false);
setIsPassed(false);
console.error(e);
const endTime = new Date().valueOf();
const executionTime = (endTime - startTime) / 1000;
onCompleted({ result: false, error: e, executionTime });
});
}
}, [test, assertion]);
return (
<TestCard
title={title}
description={description}
status={getStatus(isRunning, isPassed)}
error={null}
/>
);
};
export default Test;

View File

@@ -1,62 +0,0 @@
.test {
display: inline-flex;
padding: 8px;
margin: 8px;
flex-direction: column;
border: 1px solid #ddd;
border-radius: 5px;
width: 20%;
.title {
font-weight: bold;
color: #eee;
font-size: 1em;
}
.description,
.execution-time {
color: #c6c0c0;
padding: 8px 0;
font-size: 0.8em;
}
.description {
min-height: 50px;
}
.execution-time {
color: #f9e804;
}
.icon {
border-radius: 50%;
width: 12px;
height: 12px;
margin-right: 8px;
display: inline-block;
&.running {
background-color: yellow;
}
&.passed {
background-color: green;
}
&.failed {
background-color: red;
}
}
}
@media only screen and (max-width: 900px) {
.test {
width: 90%;
}
}
@media only screen and (min-width: 901px) and (max-width: 1280px) {
.test {
width: 30%;
}
}

View File

@@ -1,43 +0,0 @@
import React, { ReactElement } from "react";
import "./TestCard.scss";
interface TestCardProps {
title: string;
description: string;
status: string;
error: Error | null;
executionTime?: number;
}
const TestCard = (props: TestCardProps): ReactElement<TestCardProps> => {
const { title, description, status, error, executionTime } = props;
return (
<div className="test">
<code className="title">{title}</code>
<span className="description">{description}</span>
<span className="execution-time">
{executionTime ? executionTime.toFixed(2) + "s" : ""}
</span>
{status === "running" && (
<div>
<span className="icon running"></span>Running...
</div>
)}
{status === "passed" && (
<div>
<span className="icon passed"></span>Passed
</div>
)}
{status === "failed" && (
<>
<div>
<span className="icon failed"></span>Failed
</div>
{!!error && <code>{error.message}</code>}
</>
)}
</div>
);
};
export default TestCard;

View File

@@ -1,106 +0,0 @@
import React, { ReactElement, useState, useEffect } from "react";
import "./TestSuiteCard.scss";
import { Test } from "../types";
import TestComponent from "./Test";
import TestCard from "./TestCard";
interface TestSuiteProps {
name: string;
tests: Test[];
beforeAll?: (...args: any) => Promise<any>;
afterAll?: (...args: any) => Promise<any>;
onCompleted: (
name: string,
completedTests: {
test: Test;
result: boolean;
error: Error | null;
executionTime: number;
}[]
) => void;
}
const TestSuite = (props: TestSuiteProps): ReactElement<TestSuiteProps> => {
const { name, tests, beforeAll, afterAll, onCompleted } = props;
const [context, setContext] = useState<any>(null);
const [completedTests, setCompletedTests] = useState<
{
test: Test;
result: boolean;
error: Error | null;
executionTime: number;
}[]
>([]);
const [currentTest, setCurrentTest] = useState<Test | null>(
(null as unknown) as Test
);
useEffect(() => {
if (beforeAll) {
beforeAll().then((data) => setContext({ data }));
}
}, [beforeAll]);
useEffect(() => {
if (tests.length) {
setCurrentTest(tests[0]);
}
setCompletedTests([]);
setContext(null);
}, [tests]);
return (!!beforeAll && !!context) || !beforeAll ? (
<div className="test-suite">
<div className="test-suite-name running">{name}</div>
{currentTest && (
<TestComponent
{...currentTest}
context={context}
onCompleted={(completedTest) => {
const newCompleteTests = [
...completedTests,
{
test: currentTest,
result: completedTest.result,
error: completedTest.error,
executionTime: completedTest.executionTime,
},
];
setCompletedTests(newCompleteTests);
const currentIndex = tests.indexOf(currentTest);
const nextIndex =
currentIndex < tests.length - 1 ? currentIndex + 1 : -1;
if (nextIndex >= 0) {
setCurrentTest(tests[nextIndex]);
} else {
setCurrentTest(null);
}
if (newCompleteTests.length === tests.length) {
if (afterAll) {
afterAll().then(() => onCompleted(name, newCompleteTests));
} else {
onCompleted(name, newCompleteTests);
}
}
}}
/>
)}
{completedTests.map((completedTest, index) => {
const { test, result, error } = completedTest;
const { title, description } = test;
return (
<TestCard
key={index}
title={title}
description={description}
status={result === true ? "passed" : "failed"}
error={error}
/>
);
})}
</div>
) : (
<></>
);
};
export default TestSuite;

View File

@@ -1,19 +0,0 @@
.test-suite {
.test-suite-name {
font-size: 1.5em;
font-weight: bold;
color: #1f2027;
&.passed {
color: green;
}
&.failed {
color: red;
}
&.running {
color: yellow;
}
}
}

View File

@@ -1,44 +0,0 @@
import React, { ReactElement } from "react";
import "./TestSuiteCard.scss";
import { Test } from "../types";
import TestCard from "./TestCard";
interface TestSuiteCardProps {
name: string;
tests: {
test: Test;
result: boolean;
error: Error | null;
executionTime: number;
}[];
}
const TestSuiteCard = (
props: TestSuiteCardProps
): ReactElement<TestSuiteCardProps> => {
const { name, tests } = props;
const overallStatus = tests.map((t) => t.result).reduce((x, y) => x && y);
return (
<div className="test-suite">
<div className={`test-suite-name ${overallStatus ? "passed" : "failed"}`}>
{name}
</div>
{tests.map((completedTest, index) => {
const { test, result, error, executionTime } = completedTest;
const { title, description } = test;
return (
<TestCard
key={index}
title={title}
description={description}
status={result === true ? "passed" : "failed"}
error={error}
executionTime={executionTime}
/>
);
})}
</div>
);
};
export default TestSuiteCard;

View File

@@ -1,53 +0,0 @@
import React, { createContext, useState, useEffect, ReactNode } from "react";
import SASjs from "sasjs";
export const AppContext = createContext<{
config: any;
sasJsConfig: any;
isLoggedIn: boolean;
setIsLoggedIn: (value: boolean) => void;
adapter: SASjs;
}>({
config: null,
sasJsConfig: null,
isLoggedIn: false,
setIsLoggedIn: (null as unknown) as (value: boolean) => void,
adapter: (null as unknown) as SASjs,
});
export const AppProvider = (props: { children: ReactNode }) => {
const [config, setConfig] = useState<{ sasJsConfig: any }>({
sasJsConfig: null,
});
const [adapter, setAdapter] = useState<SASjs>((null as unknown) as SASjs);
const [isLoggedIn, setIsLoggedIn] = useState(false);
useEffect(() => {
fetch("config.json")
.then((res) => res.json())
.then((configJson: any) => {
setConfig(configJson);
const sasjs = new SASjs(configJson.sasJsConfig);
setAdapter(sasjs);
sasjs.checkSession().then((response) => {
setIsLoggedIn(response.isLoggedIn);
});
});
}, []);
return (
<AppContext.Provider
value={{
config,
sasJsConfig: config.sasJsConfig,
isLoggedIn,
setIsLoggedIn,
adapter,
}}
>
{props.children}
</AppContext.Provider>
);
};

View File

@@ -3,7 +3,7 @@ import ReactDOM from "react-dom";
import { Route, HashRouter, Switch } from "react-router-dom";
import "./index.scss";
import * as serviceWorker from "./serviceWorker";
import { AppProvider } from "./context/AppContext";
import { AppProvider } from "@sasjs/test-framework";
import PrivateRoute from "./PrivateRoute";
import Login from "./Login";
import App from "./App";

View File

@@ -1,5 +1,5 @@
import SASjs, { ServerType, SASjsConfig } from "sasjs";
import { TestSuite } from "../types";
import SASjs, { ServerType, SASjsConfig } from "@sasjs/adapter";
import { TestSuite } from "@sasjs/test-framework";
const defaultConfig: SASjsConfig = {
serverUrl: window.location.origin,
@@ -9,6 +9,7 @@ const defaultConfig: SASjsConfig = {
serverType: ServerType.SASViya,
debug: true,
contextName: "SAS Job Execution compute context",
useComputeApi: false,
};
const customConfig = {

View File

@@ -1,5 +1,5 @@
import SASjs from "sasjs";
import { TestSuite } from "../types";
import SASjs from "@sasjs/adapter";
import { TestSuite } from "@sasjs/test-framework";
const stringData: any = { table1: [{ col1: "first col value" }] };
const numericData: any = { table1: [{ col1: 3.14159265 }] };
@@ -74,9 +74,8 @@ export const sendArrTests = (adapter: SASjs): TestSuite => ({
description:
"Should error out with long string values over 32765 characters",
test: () => {
return adapter
.request("common/sendArr", getLongStringData(32767))
.catch((e) => e);
const data = getLongStringData(32767);
return adapter.request("common/sendArr", data).catch((e) => e);
},
assertion: (error: any) => {
return !!error && !!error.MESSAGE;

View File

@@ -1,5 +1,5 @@
import SASjs from "sasjs";
import { TestSuite } from "../types";
import SASjs from "@sasjs/adapter";
import { TestSuite } from "@sasjs/test-framework";
const data: any = { table1: [{ col1: "first col value" }] };
@@ -12,7 +12,7 @@ export const sasjsRequestTests = (adapter: SASjs): TestSuite => ({
test: async () => {
return adapter.request("common/sendArr", data);
},
assertion: (res: any) => {
assertion: () => {
const requests = adapter.getSasRequests();
if (adapter.getSasjsConfig().debug) {
return requests[0].SASWORK !== null;

View File

@@ -1,5 +1,5 @@
import SASjs from "sasjs";
import { TestSuite } from "../types";
import SASjs from "@sasjs/adapter";
import { TestSuite } from "@sasjs/test-framework";
const specialCharData: any = {
table1: [
@@ -102,86 +102,86 @@ export const specialCaseTests = (adapter: SASjs): TestSuite => ({
);
},
},
// {
// title: "Other special characters",
// description: "Should handle other special characters",
// test: () => {
// return adapter.request("common/sendArr", moreSpecialCharData);
// },
// assertion: (res: any) => {
// return (
// res.table1[0][0] === moreSpecialCharData.table1[0].speech0 &&
// res.table1[0][1] === moreSpecialCharData.table1[0].pct &&
// res.table1[0][2] === moreSpecialCharData.table1[0].speech &&
// res.table1[0][3] === moreSpecialCharData.table1[0].slash &&
// res.table1[0][4] === moreSpecialCharData.table1[0].slashWithSpecial &&
// res.table1[0][5] === moreSpecialCharData.table1[0].macvar &&
// res.table1[0][6] === moreSpecialCharData.table1[0].chinese &&
// res.table1[0][7] === moreSpecialCharData.table1[0].sigma &&
// res.table1[0][8] === moreSpecialCharData.table1[0].at &&
// res.table1[0][9] === moreSpecialCharData.table1[0].serbian &&
// res.table1[0][10] === moreSpecialCharData.table1[0].dollar
// );
// },
// },
// {
// title: "Wide table with sendArr",
// description: "Should handle data with 10000 columns",
// test: () => {
// return adapter.request("common/sendArr", getWideData());
// },
// assertion: (res: any) => {
// const data = getWideData();
// let result = true;
// for (let i = 0; i <= 10; i++) {
// result =
// result && res.table1[0][i] === data.table1[0]["col" + (i + 1)];
// }
// return result;
// },
// },
// {
// title: "Wide table with sendObj",
// description: "Should handle data with 10000 columns",
// test: () => {
// return adapter.request("common/sendObj", getWideData());
// },
// assertion: (res: any) => {
// const data = getWideData();
// let result = true;
// for (let i = 0; i <= 10; i++) {
// result =
// result &&
// res.table1[0]["COL" + (i + 1)] === data.table1[0]["col" + (i + 1)];
// }
// return result;
// },
// },
// {
// title: "Multiple tables",
// description: "Should handle data with 100 tables",
// test: () => {
// return adapter.request("common/sendArr", getTables());
// },
// assertion: (res: any) => {
// const data = getTables();
// return (
// res.table1[0][0] === data.table1[0].col1 &&
// res.table1[0][1] === data.table1[0].col2 &&
// res.table1[0][2] === data.table1[0].col3 &&
// res.table1[0][3] === data.table1[0].col4 &&
// res.table50[0][0] === data.table50[0].col1 &&
// res.table50[0][1] === data.table50[0].col2 &&
// res.table50[0][2] === data.table50[0].col3 &&
// res.table50[0][3] === data.table50[0].col4
// );
// },
// },
{
title: "Large dataset",
title: "Other special characters",
description: "Should handle other special characters",
test: () => {
return adapter.request("common/sendArr", moreSpecialCharData);
},
assertion: (res: any) => {
return (
res.table1[0][0] === moreSpecialCharData.table1[0].speech0 &&
res.table1[0][1] === moreSpecialCharData.table1[0].pct &&
res.table1[0][2] === moreSpecialCharData.table1[0].speech &&
res.table1[0][3] === moreSpecialCharData.table1[0].slash &&
res.table1[0][4] === moreSpecialCharData.table1[0].slashWithSpecial &&
res.table1[0][5] === moreSpecialCharData.table1[0].macvar &&
res.table1[0][6] === moreSpecialCharData.table1[0].chinese &&
res.table1[0][7] === moreSpecialCharData.table1[0].sigma &&
res.table1[0][8] === moreSpecialCharData.table1[0].at &&
res.table1[0][9] === moreSpecialCharData.table1[0].serbian &&
res.table1[0][10] === moreSpecialCharData.table1[0].dollar
);
},
},
{
title: "Wide table with sendArr",
description: "Should handle data with 10000 columns",
test: () => {
return adapter.request("common/sendArr", getWideData());
},
assertion: (res: any) => {
const data = getWideData();
let result = true;
for (let i = 0; i <= 10; i++) {
result =
result && res.table1[0][i] === data.table1[0]["col" + (i + 1)];
}
return result;
},
},
{
title: "Wide table with sendObj",
description: "Should handle data with 10000 columns",
test: () => {
return adapter.request("common/sendObj", getWideData());
},
assertion: (res: any) => {
const data = getWideData();
let result = true;
for (let i = 0; i <= 10; i++) {
result =
result &&
res.table1[0]["COL" + (i + 1)] === data.table1[0]["col" + (i + 1)];
}
return result;
},
},
{
title: "Multiple tables",
description: "Should handle data with 100 tables",
test: () => {
return adapter.request("common/sendArr", getTables());
},
assertion: (res: any) => {
const data = getTables();
return (
res.table1[0][0] === data.table1[0].col1 &&
res.table1[0][1] === data.table1[0].col2 &&
res.table1[0][2] === data.table1[0].col3 &&
res.table1[0][3] === data.table1[0].col4 &&
res.table50[0][0] === data.table50[0].col1 &&
res.table50[0][1] === data.table50[0].col2 &&
res.table50[0][2] === data.table50[0].col3 &&
res.table50[0][3] === data.table50[0].col4
);
},
},
{
title: "Large dataset with sendObj",
description: "Should handle 5mb of data",
test: () => {
return adapter.request("common/sendArr", getLargeDataset());
return adapter.request("common/sendObj", getLargeDataset());
},
assertion: (res: any) => {
const data = getLargeDataset();
@@ -192,44 +192,59 @@ export const specialCaseTests = (adapter: SASjs): TestSuite => ({
return result;
},
},
// {
// title: "Error and _csrf tables with sendArr",
// description: "Should handle error and _csrf tables",
// test: () => {
// return adapter.request("common/sendArr", errorAndCsrfData);
// },
// assertion: (res: any) => {
// return (
// res.error[0][0] === errorAndCsrfData.error[0].col1 &&
// res.error[0][1] === errorAndCsrfData.error[0].col2 &&
// res.error[0][2] === errorAndCsrfData.error[0].col3 &&
// res.error[0][3] === errorAndCsrfData.error[0].col4 &&
// res._csrf[0][0] === errorAndCsrfData._csrf[0].col1 &&
// res._csrf[0][1] === errorAndCsrfData._csrf[0].col2 &&
// res._csrf[0][2] === errorAndCsrfData._csrf[0].col3 &&
// res._csrf[0][3] === errorAndCsrfData._csrf[0].col4
// );
// },
// },
// {
// title: "Error and _csrf tables with sendObj",
// description: "Should handle error and _csrf tables",
// test: () => {
// return adapter.request("common/sendObj", errorAndCsrfData);
// },
// assertion: (res: any) => {
// return (
// res.error[0].COL1 === errorAndCsrfData.error[0].col1 &&
// res.error[0].COL2 === errorAndCsrfData.error[0].col2 &&
// res.error[0].COL3 === errorAndCsrfData.error[0].col3 &&
// res.error[0].COL4 === errorAndCsrfData.error[0].col4 &&
// res._csrf[0].COL1 === errorAndCsrfData._csrf[0].col1 &&
// res._csrf[0].COL2 === errorAndCsrfData._csrf[0].col2 &&
// res._csrf[0].COL3 === errorAndCsrfData._csrf[0].col3 &&
// res._csrf[0].COL4 === errorAndCsrfData._csrf[0].col4
// );
// },
// },
{
title: "Large dataset with sendArr",
description: "Should handle 5mb of data",
test: () => {
return adapter.request("common/sendArr", getLargeDataset());
},
assertion: (res: any) => {
const data = getLargeDataset();
let result = true;
for (let i = 0; i <= 10; i++) {
result =
result && res.table1[i][0] === Object.values(data.table1[i])[0];
}
return result;
},
},
{
title: "Error and _csrf tables with sendArr",
description: "Should handle error and _csrf tables",
test: () => {
return adapter.request("common/sendArr", errorAndCsrfData);
},
assertion: (res: any) => {
return (
res.error[0][0] === errorAndCsrfData.error[0].col1 &&
res.error[0][1] === errorAndCsrfData.error[0].col2 &&
res.error[0][2] === errorAndCsrfData.error[0].col3 &&
res.error[0][3] === errorAndCsrfData.error[0].col4 &&
res._csrf[0][0] === errorAndCsrfData._csrf[0].col1 &&
res._csrf[0][1] === errorAndCsrfData._csrf[0].col2 &&
res._csrf[0][2] === errorAndCsrfData._csrf[0].col3 &&
res._csrf[0][3] === errorAndCsrfData._csrf[0].col4
);
},
},
{
title: "Error and _csrf tables with sendObj",
description: "Should handle error and _csrf tables",
test: () => {
return adapter.request("common/sendObj", errorAndCsrfData);
},
assertion: (res: any) => {
return (
res.error[0].COL1 === errorAndCsrfData.error[0].col1 &&
res.error[0].COL2 === errorAndCsrfData.error[0].col2 &&
res.error[0].COL3 === errorAndCsrfData.error[0].col3 &&
res.error[0].COL4 === errorAndCsrfData.error[0].col4 &&
res._csrf[0].COL1 === errorAndCsrfData._csrf[0].col1 &&
res._csrf[0].COL2 === errorAndCsrfData._csrf[0].col2 &&
res._csrf[0].COL3 === errorAndCsrfData._csrf[0].col3 &&
res._csrf[0].COL4 === errorAndCsrfData._csrf[0].col4
);
},
},
],
});

View File

@@ -1,15 +0,0 @@
export interface Test {
title: string;
description: string;
beforeTest?: (...args: any) => Promise<any>;
afterTest?: (...args: any) => Promise<any>;
test: (context?: any) => Promise<any>;
assertion: (...args: any) => boolean;
}
export interface TestSuite {
name: string;
tests: Test[];
beforeAll?: (...args: any) => Promise<any>;
afterAll?: (...args: any) => Promise<any>;
}

View File

@@ -1,23 +0,0 @@
export const uploadFile = (file: File, fileName: string, url: string) => {
return new Promise((resolve, reject) => {
const data = new FormData();
data.append("file", file);
data.append("filename", fileName);
const xhr = new XMLHttpRequest();
xhr.withCredentials = true;
xhr.addEventListener("readystatechange", function () {
if (this.readyState === 4) {
let response: any;
try {
response = JSON.parse(this.responseText);
} catch (e) {
reject(e);
}
resolve(response);
}
});
xhr.open("POST", url);
xhr.setRequestHeader("cache-control", "no-cache");
xhr.send(data);
});
};

97
src/FileUploader.ts Normal file
View File

@@ -0,0 +1,97 @@
import { isLogInRequired, needsRetry } from "./utils";
import { CsrfToken } from "./types/CsrfToken";
import { UploadFile } from "./types/UploadFile";
const requestRetryLimit = 5;
export class FileUploader {
constructor(
private appLoc: string,
private serverUrl: string,
private jobsPath: string,
private csrfToken: CsrfToken | null = null
) {}
private retryCount = 0;
public uploadFile(sasJob: string, files: UploadFile[], params: any) {
if (files?.length < 1) throw new Error("Atleast 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) => {
const formData = new FormData();
for (let file of files) {
formData.append("file", file.file, file.fileName);
}
if (this.csrfToken) formData.append("_csrf", this.csrfToken.value);
fetch(uploadUrl, {
method: "POST",
body: formData,
referrerPolicy: "same-origin",
headers,
})
.then(async (response) => {
if (!response.ok) {
if (response.status === 403) {
const tokenHeader = response.headers.get("X-CSRF-HEADER");
if (tokenHeader) {
const token = response.headers.get(tokenHeader);
this.csrfToken = {
headerName: tokenHeader,
value: token || "",
};
}
}
}
return response.text();
})
.then((responseText) => {
if (isLogInRequired(responseText))
reject("You must be logged in to upload a fle");
if (needsRetry(responseText)) {
if (this.retryCount < requestRetryLimit) {
this.retryCount++;
this.uploadFile(sasJob, files, params).then(
(res: any) => resolve(res),
(err: any) => reject(err)
);
} else {
this.retryCount = 0;
reject(responseText);
}
} else {
this.retryCount = 0;
try {
resolve(JSON.parse(responseText));
} catch (e) {
reject(e);
}
}
});
});
}
}

View File

@@ -6,7 +6,10 @@ import {
} from "./utils";
import * as NodeFormData from "form-data";
import * as path from "path";
import { Job, Session, Context, Folder } from "./types";
import { Job, Session, Context, Folder, CsrfToken } from "./types";
import { JobDefinition } from "./types/JobDefinition";
import { formatDataForRequest } from "./utils/formatDataForRequest";
import { SessionManager } from "./SessionManager";
/**
* A client for interfacing with the SAS Viya REST API
@@ -16,14 +19,21 @@ export class SASViyaApiClient {
constructor(
private serverUrl: string,
private rootFolderName: string,
private contextName: string,
private setCsrfToken: (csrfToken: CsrfToken) => void,
private rootFolderMap = new Map<string, Job[]>()
) {
if (!rootFolderName) {
throw new Error("Root folder must be provided.");
}
}
private csrfToken: { headerName: string; value: string } | null = null;
private csrfToken: CsrfToken | null = null;
private rootFolder: Folder | null = null;
private sessionManager = new SessionManager(
this.serverUrl,
this.contextName,
this.setCsrfToken
);
/**
* Returns a map containing the directory structure in the currently set root folder.
@@ -32,7 +42,7 @@ export class SASViyaApiClient {
if (this.rootFolderMap.size) {
return this.rootFolderMap;
}
this.populateRootFolderMap();
return this.rootFolderMap;
}
@@ -68,7 +78,7 @@ export class SASViyaApiClient {
if (accessToken) {
headers.Authorization = `Bearer ${accessToken}`;
}
const contexts = await this.request<{ items: Context[] }>(
const { result: contexts } = await this.request<{ items: Context[] }>(
`${this.serverUrl}/compute/contexts`,
{ headers }
);
@@ -93,7 +103,8 @@ export class SASViyaApiClient {
if (accessToken) {
headers.Authorization = `Bearer ${accessToken}`;
}
const contexts = await this.request<{ items: Context[] }>(
const { result: contexts } = await this.request<{ items: Context[] }>(
`${this.serverUrl}/compute/contexts`,
{ headers }
);
@@ -106,9 +117,7 @@ export class SASViyaApiClient {
`test-${context.name}`,
linesOfCode,
context.name,
accessToken,
undefined,
true
accessToken
).catch(() => null);
});
const results = await Promise.all(promises);
@@ -153,7 +162,7 @@ export class SASViyaApiClient {
headers.Authorization = `Bearer ${accessToken}`;
}
const contexts = await this.request<{ items: Context[] }>(
const { result: contexts } = await this.request<{ items: Context[] }>(
`${this.serverUrl}/compute/contexts`,
{ headers }
);
@@ -172,7 +181,7 @@ export class SASViyaApiClient {
"Content-Type": "application/json",
},
};
const createdSession = this.request<Session>(
const { result: createdSession } = await this.request<Session>(
`${this.serverUrl}/compute/contexts/${executionContext.id}/sessions`,
createSessionRequest
);
@@ -190,98 +199,153 @@ export class SASViyaApiClient {
* @param silent - optional flag to turn of logging.
*/
public async executeScript(
fileName: string,
jobName: string,
linesOfCode: string[],
contextName: string,
accessToken?: string,
sessionId = "",
silent = false
silent = false,
data = null,
debug = false
) {
silent = !debug;
const headers: any = {
"Content-Type": "application/json",
};
if (accessToken) {
headers.Authorization = `Bearer ${accessToken}`;
}
if (this.csrfToken) {
headers[this.csrfToken.headerName] = this.csrfToken.value;
let executionSessionId: string;
const session = await this.sessionManager.getSession(accessToken);
executionSessionId = session!.id;
const jobArguments: { [key: string]: any } = {
_contextName: contextName,
_OMITJSONLISTING: true,
_OMITJSONLOG: true,
_OMITSESSIONRESULTS: true,
_OMITTEXTLISTING: true,
_OMITTEXTLOG: true,
};
if (debug) {
jobArguments["_OMITTEXTLOG"] = false;
jobArguments["_OMITSESSIONRESULTS"] = false;
jobArguments["_DEBUG"] = 131;
}
const contexts = await this.request<{ items: Context[] }>(
`${this.serverUrl}/compute/contexts`,
const fileName = `exec-${
jobName.includes("/") ? jobName.split("/")[1] : jobName
}`;
let jobVariables: any = {
SYS_JES_JOB_URI: "",
_program: this.rootFolderName + "/" + jobName,
};
let files: any[] = [];
if (data) {
if (JSON.stringify(data).includes(";")) {
files = await this.uploadTables(data, accessToken);
jobVariables["_webin_file_count"] = files.length;
files.forEach((fileInfo, index) => {
jobVariables[
`_webin_fileuri${index + 1}`
] = `/files/files/${fileInfo.file.id}`;
jobVariables[`_webin_name${index + 1}`] = fileInfo.tableName;
});
} else {
jobVariables = { ...jobVariables, ...formatDataForRequest(data) };
}
}
// Execute job in session
const postJobRequest = {
method: "POST",
headers,
body: JSON.stringify({
name: fileName,
description: "Powered by SASjs",
code: linesOfCode,
variables: jobVariables,
arguments: jobArguments,
}),
};
const { result: postedJob, etag } = await this.request<Job>(
`${this.serverUrl}/compute/sessions/${executionSessionId}/jobs`,
postJobRequest
);
if (!silent) {
console.log(`Job has been submitted for ${fileName}`);
console.log(
`You can monitor the job progress at ${this.serverUrl}${
postedJob.links.find((l: any) => l.rel === "state")!.href
}`
);
}
const jobStatus = await this.pollJobState(
postedJob,
etag,
accessToken,
silent
);
const { result: currentJob } = await this.request<Job>(
`${this.serverUrl}/compute/sessions/${executionSessionId}/jobs/${postedJob.id}`,
{ headers }
);
const executionContext =
contexts.items && contexts.items.length
? contexts.items.find((c: any) => c.name === contextName)
: null;
if (executionContext) {
// Request new session in context or use the ID passed in
let executionSessionId: string;
if (sessionId) {
executionSessionId = sessionId;
} else {
const createSessionRequest = {
method: "POST",
headers,
};
const createdSession = await this.request<Session>(
`${this.serverUrl}/compute/contexts/${executionContext.id}/sessions`,
createSessionRequest
);
let jobResult, log;
executionSessionId = createdSession.id;
}
// Execute job in session
const postJobRequest = {
method: "POST",
headers,
body: JSON.stringify({
name: fileName,
description: "Powered by SASjs",
code: linesOfCode,
}),
};
const postedJob = await this.request<Job>(
`${this.serverUrl}/compute/sessions/${executionSessionId}/jobs`,
postJobRequest
);
if (!silent) {
console.log(`Job has been submitted for ${fileName}`);
console.log(
`You can monitor the job progress at ${this.serverUrl}${
postedJob.links.find((l: any) => l.rel === "state")!.href
}`
);
}
const jobStatus = await this.pollJobState(postedJob, accessToken, silent);
const logLink = postedJob.links.find((l: any) => l.rel === "log");
if (logLink) {
const log = await this.request(
`${this.serverUrl}${logLink.href}?limit=100000`,
{
headers,
}
);
return { jobStatus, log };
}
} else {
console.error(
`Unable to find execution context ${contextName}.\nPlease check the contextName in the tgtDeployVars and try again.`
);
console.error("Response from server: ", JSON.stringify(contexts));
if (jobStatus === "failed" || jobStatus === "error") {
return Promise.reject(currentJob.error);
}
const resultLink = `/compute/sessions/${executionSessionId}/filerefs/_webout/content`;
const logLink = currentJob.links.find((l) => l.rel === "log");
if (resultLink) {
jobResult = await this.request<any>(
`${this.serverUrl}${resultLink}`,
{ headers },
"text"
).catch((e) => ({
result: JSON.stringify(e),
}));
}
if (true && logLink) {
log = await this.request<any>(
`${this.serverUrl}${logLink.href}/content?limit=10000`,
{
headers,
}
).then((res: any) => res.result.items.map((i: any) => i.line).join("\n"));
}
await this.sessionManager.clearSession(executionSessionId, accessToken);
return { result: jobResult?.result, log };
// } else {
// console.error(
// `Unable to find execution context ${contextName}.\nPlease check the contextName in the tgtDeployVars and try again.`
// );
// console.error("Response from server: ", JSON.stringify(this.contexts));
// }
}
/**
* Creates a folder in the specified location. Either parentFolderPath or
* parentFolderUri must be provided.
* Creates a folder in the specified location. Either parentFolderPath or
* parentFolderUri must be provided.
* @param folderName - the name of the new folder.
* @param parentFolderPath - the full path to the parent folder. If not
* @param parentFolderPath - the full path to the parent folder. If not
* provided, the parentFolderUri must be provided.
* @param parentFolderUri - the URI (eg /folders/folders/UUID) of the parent
* @param parentFolderUri - the URI (eg /folders/folders/UUID) of the parent
* folder. If not provided, the parentFolderPath must be provided.
*/
public async createFolder(
@@ -296,17 +360,27 @@ export class SASViyaApiClient {
if (!parentFolderUri && parentFolderPath) {
parentFolderUri = await this.getFolderUri(parentFolderPath, accessToken);
if (!parentFolderUri){
if (!parentFolderUri) {
console.log(`Parent folder is not present: ${parentFolderPath}`);
const newParentFolderPath = parentFolderPath.substring(0, parentFolderPath.lastIndexOf("/"));
const newParentFolderPath = parentFolderPath.substring(
0,
parentFolderPath.lastIndexOf("/")
);
const newFolderName = `${parentFolderPath.split("/").pop()}`;
if (newParentFolderPath === ""){
if (newParentFolderPath === "") {
throw new Error("Root Folder should have been present on server");
}
console.log(`Creating Parent Folder:\n${newFolderName} in ${newParentFolderPath}`)
const parentFolder = await this.createFolder(newFolderName, newParentFolderPath, undefined, accessToken)
console.log(`Parent Folder "${newFolderName}" successfully created.`)
console.log(
`Creating Parent Folder:\n${newFolderName} in ${newParentFolderPath}`
);
const parentFolder = await this.createFolder(
newFolderName,
newParentFolderPath,
undefined,
accessToken
);
console.log(`Parent Folder "${newFolderName}" successfully created.`);
parentFolderUri = `/folders/folders/${parentFolder.id}`;
}
}
@@ -324,7 +398,7 @@ export class SASViyaApiClient {
createFolderRequest.headers.Authorization = `Bearer ${accessToken}`;
}
const createFolderResponse = await this.request<Folder>(
const { result: createFolderResponse } = await this.request<Folder>(
`${this.serverUrl}/folders/folders?parentFolderUri=${parentFolderUri}`,
createFolderRequest
);
@@ -350,7 +424,9 @@ export class SASViyaApiClient {
accessToken?: string
) {
if (!parentFolderPath && !parentFolderUri) {
throw new Error('Either parentFolderPath or parentFolderUri must be provided');
throw new Error(
"Either parentFolderPath or parentFolderUri must be provided"
);
}
if (!parentFolderUri && parentFolderPath) {
@@ -365,12 +441,12 @@ export class SASViyaApiClient {
},
body: JSON.stringify({
name: jobName,
parameters:[
parameters: [
{
"name":"_addjesbeginendmacros",
"type":"CHARACTER",
"defaultValue":"false"
}
name: "_addjesbeginendmacros",
type: "CHARACTER",
defaultValue: "false",
},
],
type: "Compute",
code,
@@ -544,6 +620,74 @@ export class SASViyaApiClient {
return deleteResponse;
}
/**
* Executes a job via the SAS Viya Compute API
* @param sasJob - the relative path to the job.
* @param contextName - the name of the context where the job is to be executed.
* @param debug - sets the _debug flag in the job arguments.
* @param data - any data to be passed in as input to the job.
* @param accessToken - an optional access token for an authorized user.
*/
public async executeComputeJob(
sasJob: string,
contextName: string,
debug: boolean,
data?: any,
accessToken?: string
) {
if (!this.rootFolder) {
await this.populateRootFolder(accessToken);
}
if (!this.rootFolder) {
console.error("Root folder was not found");
throw new Error("Root folder was not found");
}
if (!this.rootFolderMap.size) {
await this.populateRootFolderMap(accessToken);
}
if (!this.rootFolderMap.size) {
console.error(
`The job ${sasJob} was not found in ${this.rootFolderName}`
);
throw new Error(
`The job ${sasJob} was not found in ${this.rootFolderName}`
);
}
const headers: any = { "Content-Type": "application/json" };
if (!!accessToken) {
headers.Authorization = `Bearer ${accessToken}`;
}
const folderName = sasJob.split("/")[0];
const jobName = sasJob.split("/")[1];
const jobFolder = this.rootFolderMap.get(folderName);
const jobToExecute = jobFolder?.find((item) => item.name === jobName);
const jobDefinitionLink = jobToExecute?.links.find(
(l) => l.rel === "getResource"
);
if (!jobDefinitionLink) {
console.error("Job definition URI was not found.");
throw new Error("Job definition URI was not found.");
}
const { result: jobDefinition } = await this.request<JobDefinition>(
`${this.serverUrl}${jobDefinitionLink.href}`,
headers
);
const linesToExecute = jobDefinition.code
.replace(/\r\n/g, "\n")
.split("\n");
return await this.executeScript(
sasJob,
linesToExecute,
contextName,
accessToken,
true,
data,
debug
);
}
/**
* Executes a job via the SAS Viya Job Execution API
* @param sasJob - the relative path to the job.
@@ -574,11 +718,12 @@ export class SASViyaApiClient {
`The job ${sasJob} was not found in ${this.rootFolderName}`
);
}
let files: any[] = [];
if (data && Object.keys(data).length) {
files = await this.uploadTables(data, accessToken);
}
const jobName = path.basename(sasJob);
const jobFolder = sasJob.replace(`/${jobName}`, "");
const allJobsInFolder = this.rootFolderMap.get(jobFolder.replace("/", ""));
@@ -595,7 +740,7 @@ export class SASViyaApiClient {
headers.Authorization = `Bearer ${accessToken}`;
}
requestInfo.headers = headers;
const jobDefinition = await this.request<Job>(
const { result: jobDefinition } = await this.request<Job>(
`${this.serverUrl}${jobDefinitionLink}`,
requestInfo
);
@@ -612,15 +757,15 @@ export class SASViyaApiClient {
};
if (debug) {
jobArguments["_omittextlog"] = "false";
jobArguments["_omitsessionresults"] = "false";
jobArguments["_debug"] = 131;
jobArguments["_OMITTEXTLOG"] = "false";
jobArguments["_OMITSESSIONRESULTS"] = "false";
jobArguments["_DEBUG"] = 131;
}
files.forEach((fileInfo, index) => {
jobArguments[
`_webin_fileuri${index + 1}`
] = `/files/files/${fileInfo.id}`;
] = `/files/files/${fileInfo.file.id}`;
jobArguments[`_webin_name${index + 1}`] = fileInfo.tableName;
});
@@ -634,25 +779,45 @@ export class SASViyaApiClient {
arguments: jobArguments,
}),
};
const postedJob = await this.request<Job>(
const { result: postedJob, etag } = await this.request<Job>(
`${this.serverUrl}/jobExecution/jobs?_action=wait`,
postJobRequest
);
const jobStatus = await this.pollJobState(postedJob, accessToken, true);
const currentJob = await this.request<Job>(
const jobStatus = await this.pollJobState(
postedJob,
etag,
accessToken,
true
);
const { result: currentJob } = await this.request<Job>(
`${this.serverUrl}/jobExecution/jobs/${postedJob.id}`,
{ headers }
);
const resultLink = currentJob.results["_webout.json"];
if (resultLink) {
const result = await this.request<any>(
`${this.serverUrl}${resultLink}/content`,
{ headers }
);
return result;
}
return postedJob;
let jobResult, log;
if (jobStatus === "failed") {
return Promise.reject(currentJob.error);
}
const resultLink = currentJob.results["_webout.json"];
const logLink = currentJob.links.find((l) => l.rel === "log");
if (resultLink) {
jobResult = await this.request<any>(
`${this.serverUrl}${resultLink}/content`,
{ headers },
"text"
);
}
if (debug && logLink) {
log = await this.request<any>(
`${this.serverUrl}${logLink.href}/content`,
{
headers,
}
).then((res: any) =>
res.result.items.map((i: any) => i.line).join("\n")
);
}
return { result: jobResult?.result, log };
} else {
throw new Error(
`The job ${sasJob} was not found at the location ${this.rootFolderName}`
@@ -669,14 +834,14 @@ export class SASViyaApiClient {
if (accessToken) {
requestInfo.headers = { Authorization: `Bearer ${accessToken}` };
}
const folder = await this.request<Folder>(
const { result: folder } = await this.request<Folder>(
`${this.serverUrl}${url}`,
requestInfo
);
if (!folder){
if (!folder) {
throw new Error("Cannot populate RootFolderMap unless rootFolder exists");
}
const members = await this.request<{ items: any[] }>(
const { result: members } = await this.request<{ items: any[] }>(
`${this.serverUrl}/folders/folders/${folder.id}/members`,
requestInfo
);
@@ -691,7 +856,7 @@ export class SASViyaApiClient {
this.rootFolderName +
"/" +
member.name;
const memberDetail = await this.request<Folder>(
const { result: memberDetail } = await this.request<Folder>(
`${this.serverUrl}${subFolderUrl}`,
requestInfo
);
@@ -700,7 +865,7 @@ export class SASViyaApiClient {
(l: any) => l.rel === "members"
);
const memberContents = await this.request<{ items: any[] }>(
const { result: memberContents } = await this.request<{ items: any[] }>(
`${this.serverUrl}${membersLink!.href}`,
requestInfo
);
@@ -721,29 +886,37 @@ export class SASViyaApiClient {
if (accessToken) {
requestInfo.headers = { Authorization: `Bearer ${accessToken}` };
}
let error;
const rootFolder = await this.request<Folder>(
`${this.serverUrl}${url}`,
requestInfo
).catch(() => null);
);
this.rootFolder = rootFolder;
this.rootFolder = rootFolder?.result || null;
if (error) {
throw new Error(JSON.stringify(error));
}
}
private async pollJobState(
postedJob: any,
etag: string | null,
accessToken?: string,
silent = false
) {
const MAX_POLL_COUNT = 1000;
const POLL_INTERVAL = 100;
let postedJobState = "";
let pollCount = 0;
const headers: any = {
"Content-Type": "application/json",
"If-None-Match": etag,
};
if (accessToken) {
headers.Authorization = `Bearer ${accessToken}`;
}
const stateLink = postedJob.links.find((l: any) => l.rel === "state");
return new Promise((resolve, _) => {
return new Promise(async (resolve, _) => {
const interval = setInterval(async () => {
if (
postedJobState === "running" ||
@@ -754,8 +927,8 @@ export class SASViyaApiClient {
if (!silent) {
console.log("Polling job status... \n");
}
const jobState = await this.request<string>(
`${this.serverUrl}${stateLink.href}?wait=30`,
const { result: jobState } = await this.request<string>(
`${this.serverUrl}${stateLink.href}?_action=wait&wait=30`,
{
headers,
},
@@ -767,7 +940,7 @@ export class SASViyaApiClient {
console.log(`Current state: ${postedJobState}\n`);
}
pollCount++;
if (pollCount >= 100) {
if (pollCount >= MAX_POLL_COUNT) {
resolve(postedJobState);
}
}
@@ -775,7 +948,50 @@ export class SASViyaApiClient {
clearInterval(interval);
resolve(postedJobState);
}
}, 100);
}, POLL_INTERVAL);
});
}
private async waitForSession(
session: Session,
etag: string | null,
accessToken?: string,
silent = false
) {
let sessionState = session.state;
let pollCount = 0;
const headers: any = {
"Content-Type": "application/json",
"If-None-Match": etag,
};
if (accessToken) {
headers.Authorization = `Bearer ${accessToken}`;
}
const stateLink = session.links.find((l: any) => l.rel === "state");
return new Promise(async (resolve, _) => {
if (sessionState === "pending") {
if (stateLink) {
if (!silent) {
console.log("Polling session status... \n");
}
const { result: state } = await this.request<string>(
`${this.serverUrl}${stateLink.href}?wait=30`,
{
headers,
},
"text"
);
sessionState = state.trim();
if (!silent) {
console.log(`Current state: ${sessionState}\n`);
}
pollCount++;
resolve(sessionState);
}
} else {
resolve(sessionState);
}
});
}
@@ -802,33 +1018,40 @@ export class SASViyaApiClient {
headers,
};
const file = await this.request<any>(
const uploadResponse = await this.request<any>(
`${this.serverUrl}/files/files#rawUpload`,
createFileRequest
);
uploadedFiles.push({ tableName, file });
uploadedFiles.push({ tableName, file: uploadResponse.result });
}
return uploadedFiles;
}
private async getFolderUri(folderPath: string, accessToken?: string) {
const url = "/folders/folders/@item?path=" + folderPath;
const requestInfo: any = {
method: "GET",
};
if (accessToken) {
requestInfo.headers = { Authorization: `Bearer ${accessToken}` };
}
const folder = await this.request<Folder>(
`${this.serverUrl}${url}`,
requestInfo
);
if (!folder)
return undefined;
return `/folders/folders/${folder.id}`;
const requestInfo: any = {
method: "GET",
};
if (accessToken) {
requestInfo.headers = { Authorization: `Bearer ${accessToken}` };
}
const { result: folder } = await this.request<Folder>(
`${this.serverUrl}${url}`,
requestInfo
).catch((err) => {
return { result: null };
});
if (!folder) return undefined;
return `/folders/folders/${folder.id}`;
}
setCsrfTokenLocal = (csrfToken: CsrfToken) => {
this.csrfToken = csrfToken;
this.setCsrfToken(csrfToken);
};
private async request<T>(
url: string,
options: RequestInit,
@@ -843,7 +1066,7 @@ export class SASViyaApiClient {
return await makeRequest<T>(
url,
options,
(csrfToken) => (this.csrfToken = csrfToken),
this.setCsrfTokenLocal,
contentType
);
}

View File

@@ -1,6 +1,11 @@
import "isomorphic-fetch";
import { isIEorEdgeOrOldFirefox } from "./utils/isIeOrEdge";
import * as e6p from "es6-promise";
(e6p as any).polyfill();
if (isIEorEdgeOrOldFirefox()) {
window.fetch = undefined as any; // ensure the polyfill runs
}
// tslint:disable-next-line
require("isomorphic-fetch");
import {
convertToCSV,
compareTimestamps,
@@ -12,6 +17,7 @@ import {
isLogInSuccess,
parseSourceCode,
parseGeneratedCode,
parseWeboutResponse,
needsRetry,
asyncForEach,
} from "./utils";
@@ -20,9 +26,12 @@ import {
SASjsRequest,
SASjsWaitingRequest,
ServerType,
CsrfToken,
UploadFile,
} from "./types";
import { SASViyaApiClient } from "./SASViyaApiClient";
import { SAS9ApiClient } from "./SAS9ApiClient";
import { FileUploader } from "./FileUploader";
const defaultConfig: SASjsConfig = {
serverUrl: "",
@@ -32,6 +41,7 @@ const defaultConfig: SASjsConfig = {
serverType: ServerType.SASViya,
debug: true,
contextName: "SAS Job Execution compute context",
useComputeApi: false,
};
const requestRetryLimit = 5;
@@ -41,18 +51,21 @@ const requestRetryLimit = 5;
*
*/
export default class SASjs {
private sasjsConfig = new SASjsConfig();
private sasjsConfig: SASjsConfig = new SASjsConfig();
private jobsPath: string = "";
private logoutUrl: string = "";
private loginUrl: string = "";
private _csrf: string | null = null;
private _csrfHeader: string | null = null;
private retryCount: number = 0;
private csrfTokenApi: CsrfToken | null = null;
private csrfTokenWeb: CsrfToken | null = null;
private retryCountWeb: number = 0;
private retryCountComputeApi: number = 0;
private retryCountJeseApi: number = 0;
private sasjsRequests: SASjsRequest[] = [];
private sasjsWaitingRequests: SASjsWaitingRequest[] = [];
private userName: string = "";
private sasViyaApiClient: SASViyaApiClient | null = null;
private sas9ApiClient: SAS9ApiClient | null = null;
private fileUploader: FileUploader | null = null;
constructor(config?: any) {
this.sasjsConfig = {
@@ -115,7 +128,6 @@ export default class SASjs {
linesOfCode,
contextName,
accessToken,
sessionId,
silent
);
}
@@ -234,11 +246,19 @@ export default class SASjs {
}
/**
* Returns the _csrf token of the current session.
* Returns the _csrf token of the current session for the API approach
*
*/
public getCsrf() {
return this._csrf;
public getCsrfApi() {
return this.csrfTokenApi?.value;
}
/**
* Returns the _csrf token of the current session for the WEB approach.
*
*/
public getCsrfWeb() {
return this.csrfTokenWeb?.value;
}
/**
@@ -362,6 +382,26 @@ export default class SASjs {
});
}
/**
* Uploads a file to the given service
* @param sasJob - The path to the SAS program (ultimately resolves to
* the SAS `_program` parameter to run a Job Definition or SAS 9 Stored
* Process.) Is prepended at runtime with the value of `appLoc`.
* @param file - Array of files to be uploaded, including File object and file name.
* @param params - Request URL paramaters
*/
public uploadFile(sasJob: string, files: UploadFile[], params: any) {
const fileUploader =
this.fileUploader ||
new FileUploader(
this.sasjsConfig.appLoc,
this.sasjsConfig.serverUrl,
this.jobsPath,
this.csrfTokenWeb
);
return fileUploader.uploadFile(sasJob, files, params);
}
/**
* Makes a request to the SAS Service specified in `SASjob`. The response
* object will always contain table names in lowercase, and column names in
@@ -373,8 +413,9 @@ export default class SASjs {
* Process.) Is prepended at runtime with the value of `appLoc`.
* @param data - A JSON object containing one or more tables to be sent to
* SAS. Can be `null` if no inputs required.
* @param params - Provide any changes to the config here, for instance to
* enable / disable `debug`.
* @param config - Provide any changes to the config here, for instance to
* enable / disable `debug`. Any change provided will override the global config,
* for that particular function call.
* @param loginRequiredCallback - provide a function here to be called if the
* user is not logged in (eg to display a login form). The request will be
* resubmitted after logon.
@@ -382,7 +423,118 @@ export default class SASjs {
public async request(
sasJob: string,
data: any,
params?: any,
config: any = {},
loginRequiredCallback?: any,
accessToken?: string
) {
let requestResponse;
config = {
...this.sasjsConfig,
...config,
};
sasJob = sasJob.startsWith("/") ? sasJob.replace("/", "") : sasJob;
if (config.serverType === ServerType.SASViya && config.contextName) {
if (config.useComputeApi) {
requestResponse = await this.executeJobViaComputeApi(
sasJob,
data,
config,
loginRequiredCallback,
accessToken
);
this.retryCountComputeApi = 0;
} else {
requestResponse = await this.executeJobViaJesApi(
sasJob,
data,
config,
loginRequiredCallback,
accessToken
);
this.retryCountJeseApi = 0;
}
} else {
requestResponse = await this.executeJobViaWeb(
sasJob,
data,
config,
loginRequiredCallback
);
}
return requestResponse;
}
/**
* Creates the folders and services in the provided JSON on the given location
* (appLoc) on the given server (serverUrl).
* @param serviceJson - the JSON specifying the folders and services to be created.
* @param appLoc - the base folder in which to create the new folders and
* services. If not provided, is taken from SASjsConfig.
* @param serverUrl - the server on which to deploy the folders and services.
* If not provided, is taken from SASjsConfig.
* @param accessToken - an optional access token to be passed in when
* using this function from the command line.
*/
public async deployServicePack(
serviceJson: any,
appLoc?: string,
serverUrl?: string,
accessToken?: string
) {
if (this.sasjsConfig.serverType !== ServerType.SASViya) {
throw new Error("This operation is only supported on SAS Viya servers.");
}
let sasApiClient: any = null;
if (serverUrl || appLoc) {
if (!serverUrl) {
serverUrl = this.sasjsConfig.serverUrl;
}
if (!appLoc) {
appLoc = this.sasjsConfig.appLoc;
}
if (this.sasjsConfig.serverType === ServerType.SASViya) {
sasApiClient = new SASViyaApiClient(
serverUrl,
appLoc,
this.sasjsConfig.contextName,
this.setCsrfTokenApi
);
} else if (this.sasjsConfig.serverType === ServerType.SAS9) {
sasApiClient = new SAS9ApiClient(serverUrl);
}
} else {
let sasClientConfig: any = null;
if (this.sasjsConfig.serverType === ServerType.SASViya) {
sasClientConfig = this.sasViyaApiClient!.getConfig();
} else if (this.sasjsConfig.serverType === ServerType.SAS9) {
sasClientConfig = this.sas9ApiClient!.getConfig();
}
serverUrl = sasClientConfig.serverUrl;
appLoc = sasClientConfig.rootFolderName as string;
}
const members =
serviceJson.members[0].name === "services"
? serviceJson.members[0].members
: serviceJson.members;
await this.createFoldersAndServices(
appLoc,
members,
accessToken,
sasApiClient
);
}
private async executeJobViaComputeApi(
sasJob: string,
data: any,
config: any,
loginRequiredCallback?: any,
accessToken?: string
) {
@@ -394,55 +546,183 @@ export default class SASjs {
},
SASjob: sasJob,
data,
params,
};
// if (
// this.sasjsConfig.serverType === ServerType.SASViya &&
// this.sasjsConfig.contextName
// ) {
// sasjsWaitingRequest.requestPromise.promise = new Promise(
// async (resolve, reject) => {
// const session = await this.checkSession();
sasjsWaitingRequest.requestPromise.promise = new Promise(
async (resolve, reject) => {
this.sasViyaApiClient
?.executeComputeJob(
sasJob,
config.contextName,
config.debug,
data,
accessToken
)
.then((response) => {
if (!config.debug) {
this.appendSasjsRequest(null, sasJob, null);
} else {
this.appendSasjsRequest(response, sasJob, null);
}
// if (!session.isLoggedIn) {
// if (loginRequiredCallback) loginRequiredCallback(true);
// logInRequired = true;
// sasjsWaitingRequest.requestPromise.resolve = resolve;
// sasjsWaitingRequest.requestPromise.reject = reject;
// this.sasjsWaitingRequests.push(sasjsWaitingRequest);
// } else {
// resolve(
// await this.sasViyaApiClient?.executeJob(
// sasJob,
// this.sasjsConfig.contextName,
// this.sasjsConfig.debug,
// data,
// accessToken
// )
// );
// }
// }
// );
// return sasjsWaitingRequest.requestPromise.promise;
// } else {
const program = this.sasjsConfig.appLoc
? this.sasjsConfig.appLoc.replace(/\/?$/, "/") + sasJob.replace(/^\//, "")
let responseJson;
try {
responseJson = JSON.parse(response!.result);
} catch {
responseJson = JSON.parse(parseWeboutResponse(response!.result));
}
resolve(responseJson);
})
.catch(async (e) => {
if (needsRetry(JSON.stringify(e))) {
if (this.retryCountComputeApi < requestRetryLimit) {
let retryResponse = await this.executeJobViaComputeApi(
sasJob,
data,
config,
loginRequiredCallback,
accessToken
);
this.retryCountComputeApi++;
resolve(retryResponse);
} else {
this.retryCountComputeApi = 0;
reject({ MESSAGE: "Compute API retry requests limit reached" });
}
}
if (e && e.status === 401) {
if (loginRequiredCallback) loginRequiredCallback(true);
sasjsWaitingRequest.requestPromise.resolve = resolve;
sasjsWaitingRequest.requestPromise.reject = reject;
sasjsWaitingRequest.config = config;
this.sasjsWaitingRequests.push(sasjsWaitingRequest);
} else {
reject({ MESSAGE: e || "Job execution failed" });
}
});
}
);
return sasjsWaitingRequest.requestPromise.promise;
}
private async executeJobViaJesApi(
sasJob: string,
data: any,
config: any,
loginRequiredCallback?: any,
accessToken?: string
) {
const sasjsWaitingRequest: SASjsWaitingRequest = {
requestPromise: {
promise: null,
resolve: null,
reject: null,
},
SASjob: sasJob,
data,
};
sasjsWaitingRequest.requestPromise.promise = new Promise(
async (resolve, reject) => {
const session = await this.checkSession();
if (!session.isLoggedIn) {
if (loginRequiredCallback) loginRequiredCallback(true);
sasjsWaitingRequest.requestPromise.resolve = resolve;
sasjsWaitingRequest.requestPromise.reject = reject;
sasjsWaitingRequest.config = config;
this.sasjsWaitingRequests.push(sasjsWaitingRequest);
} else {
resolve(
await this.sasViyaApiClient
?.executeJob(
sasJob,
config.contextName,
config.debug,
data,
accessToken
)
.then((response) => {
if (!config.debug) {
this.appendSasjsRequest(null, sasJob, null);
} else {
this.appendSasjsRequest(response, sasJob, null);
}
let responseJson;
try {
responseJson = JSON.parse(response!.result);
} catch {
responseJson = JSON.parse(
parseWeboutResponse(response!.result)
);
}
return responseJson;
})
.catch(async (e) => {
if (needsRetry(JSON.stringify(e))) {
if (this.retryCountJeseApi < requestRetryLimit) {
let retryResponse = await this.executeJobViaJesApi(
sasJob,
data,
config,
loginRequiredCallback,
accessToken
);
this.retryCountJeseApi++;
resolve(retryResponse);
} else {
this.retryCountJeseApi = 0;
reject({ MESSAGE: "Jes API retry requests limit reached" });
}
}
reject({ MESSAGE: (e && e.message) || "Job execution failed" });
})
);
}
}
);
return sasjsWaitingRequest.requestPromise.promise;
}
private async executeJobViaWeb(
sasJob: string,
data: any,
config: any,
loginRequiredCallback?: any
) {
const sasjsWaitingRequest: SASjsWaitingRequest = {
requestPromise: {
promise: null,
resolve: null,
reject: null,
},
SASjob: sasJob,
data,
};
const program = config.appLoc
? config.appLoc.replace(/\/?$/, "/") + sasJob.replace(/^\//, "")
: sasJob;
const jobUri =
this.sasjsConfig.serverType === "SASVIYA"
? await this.getJobUri(sasJob)
: "";
const apiUrl = `${this.sasjsConfig.serverUrl}${this.jobsPath}/?${
config.serverType === "SASVIYA" ? await this.getJobUri(sasJob) : "";
const apiUrl = `${config.serverUrl}${this.jobsPath}/?${
jobUri.length > 0
? "__program=" + program + "&_job=" + jobUri
: "_program=" + program
}`;
const inputParams = params ? params : {};
const requestParams = {
...inputParams,
...this.getRequestParams(),
...this.getRequestParamsWeb(),
};
const formData = new FormData();
@@ -451,74 +731,68 @@ export default class SASjs {
let errorMsg = "";
if (data) {
console.log("Input data", data);
const stringifiedData = JSON.stringify(data);
if (
this.sasjsConfig.serverType === ServerType.SAS9 ||
config.serverType === ServerType.SAS9 ||
stringifiedData.length > 500000 ||
stringifiedData.includes(";")
) {
// file upload approach
for (const tableName in data) {
console.log("TableName: ", tableName);
if (isError) {
return;
}
const name = tableName;
const csv = convertToCSV(data[tableName]);
console.log("Converted CSV", csv);
if (csv === "ERROR: LARGE STRING LENGTH") {
console.log("String too long");
isError = true;
errorMsg =
"The max length of a string value in SASjs is 32765 characters.";
}
const file = new Blob([csv], { type: "application/csv" });
console.log("File", file);
const file = new Blob([csv], {
type: "application/csv",
});
formData.append(name, file, `${name}.csv`);
}
} else {
// param based approach
const sasjsTables = [];
let tableCounter = 0;
for (const tableName in data) {
if (isError) {
return;
}
tableCounter++;
sasjsTables.push(tableName);
const csv = convertToCSV(data[tableName]);
if (csv === "ERROR: LARGE STRING LENGTH") {
isError = true;
errorMsg =
"The max length of a string value in SASjs is 32765 characters.";
}
// if csv has length more then 16k, send in chunks
if (csv.length > 16000) {
const csvChunks = splitChunks(csv);
// append chunks to form data with same key
csvChunks.map((chunk) => {
formData.append(`sasjs${tableCounter}data`, chunk);
});
} else {
requestParams[`sasjs${tableCounter}data`] = csv;
}
}
requestParams["sasjs_tables"] = sasjsTables.join(" ");
}
// param based approach
const sasjsTables = [];
let tableCounter = 0;
for (const tableName in data) {
if (isError) {
return;
}
tableCounter++;
sasjsTables.push(tableName);
const csv = convertToCSV(data[tableName]);
if (csv === "ERROR: LARGE STRING LENGTH") {
isError = true;
errorMsg =
"The max length of a string value in SASjs is 32765 characters.";
}
// if csv has length more then 16k, send in chunks
if (csv.length > 16000) {
const csvChunks = splitChunks(csv);
// append chunks to form data with same key
csvChunks.map((chunk) => {
formData.append(`sasjs${tableCounter}data`, chunk);
});
} else {
requestParams[`sasjs${tableCounter}data`] = csv;
}
}
requestParams["sasjs_tables"] = sasjsTables.join(" ");
}
console.log("Request params", requestParams);
for (const key in requestParams) {
if (requestParams.hasOwnProperty(key)) {
formData.append(key, requestParams[key]);
}
}
console.log("Form data", formData);
let isRedirected = false;
sasjsWaitingRequest.requestPromise.promise = new Promise(
@@ -526,11 +800,9 @@ export default class SASjs {
if (isError) {
reject({ MESSAGE: errorMsg });
}
const headers: any = {
"Content-Type": "application/x-www-form-urlencoded",
};
if (this._csrfHeader && this._csrf) {
headers[this._csrfHeader] = this._csrf;
const headers: any = {};
if (this.csrfTokenWeb) {
headers[this.csrfTokenWeb.headerName] = this.csrfTokenWeb.value;
}
fetch(apiUrl, {
method: "POST",
@@ -545,16 +817,15 @@ export default class SASjs {
if (tokenHeader) {
const token = response.headers.get(tokenHeader);
this._csrfHeader = tokenHeader;
this._csrf = token;
this.csrfTokenWeb = {
headerName: tokenHeader,
value: token || "",
};
}
}
}
if (
response.redirected &&
this.sasjsConfig.serverType === ServerType.SAS9
) {
if (response.redirected && config.serverType === ServerType.SAS9) {
isRedirected = true;
}
@@ -565,32 +836,30 @@ export default class SASjs {
(needsRetry(responseText) || isRedirected) &&
!isLogInRequired(responseText)
) {
if (this.retryCount < requestRetryLimit) {
this.retryCount++;
this.request(sasJob, data, params).then(
if (this.retryCountWeb < requestRetryLimit) {
this.retryCountWeb++;
this.request(sasJob, data).then(
(res: any) => resolve(res),
(err: any) => reject(err)
);
} else {
this.retryCount = 0;
this.retryCountWeb = 0;
reject(responseText);
}
} else {
this.retryCount = 0;
this.retryCountWeb = 0;
this.parseLogFromResponse(responseText, program);
if (isLogInRequired(responseText)) {
if (loginRequiredCallback) loginRequiredCallback(true);
sasjsWaitingRequest.requestPromise.resolve = resolve;
sasjsWaitingRequest.requestPromise.reject = reject;
sasjsWaitingRequest.config = config;
this.sasjsWaitingRequests.push(sasjsWaitingRequest);
} else {
if (
this.sasjsConfig.serverType === ServerType.SAS9 &&
this.sasjsConfig.debug
) {
if (config.serverType === ServerType.SAS9 && config.debug) {
this.updateUsername(responseText);
const jsonResponseText = this.parseSAS9Response(responseText);
const jsonResponseText = parseWeboutResponse(responseText);
if (jsonResponseText !== "") {
resolve(JSON.parse(jsonResponseText));
@@ -600,8 +869,8 @@ export default class SASjs {
});
}
} else if (
this.sasjsConfig.serverType === ServerType.SASViya &&
this.sasjsConfig.debug
config.serverType === ServerType.SASViya &&
config.debug
) {
try {
this.parseSASVIYADebugResponse(responseText).then(
@@ -639,72 +908,15 @@ export default class SASjs {
);
return sasjsWaitingRequest.requestPromise.promise;
// }
}
/**
* Creates the folders and services in the provided JSON on the given location
* (appLoc) on the given server (serverUrl).
* @param serviceJson - the JSON specifying the folders and services to be created.
* @param appLoc - the base folder in which to create the new folders and
* services. If not provided, is taken from SASjsConfig.
* @param serverUrl - the server on which to deploy the folders and services.
* If not provided, is taken from SASjsConfig.
* @param accessToken - an optional access token to be passed in when
* using this function from the command line.
*/
public async deployServicePack(
serviceJson: any,
appLoc?: string,
serverUrl?: string,
accessToken?: string
) {
if (this.sasjsConfig.serverType !== ServerType.SASViya) {
throw new Error("This operation is only supported on SAS Viya servers.");
}
let sasApiClient: any = null;
if (serverUrl || appLoc) {
if (!serverUrl) {
serverUrl = this.sasjsConfig.serverUrl;
}
if (!appLoc) {
appLoc = this.sasjsConfig.appLoc;
}
if (this.sasjsConfig.serverType === ServerType.SASViya) {
sasApiClient = new SASViyaApiClient(serverUrl, appLoc);
} else if (this.sasjsConfig.serverType === ServerType.SAS9) {
sasApiClient = new SAS9ApiClient(serverUrl);
}
} else {
let sasClientConfig: any = null;
if (this.sasjsConfig.serverType === ServerType.SASViya) {
sasClientConfig = this.sasViyaApiClient!.getConfig();
} else if (this.sasjsConfig.serverType === ServerType.SAS9) {
sasClientConfig = this.sas9ApiClient!.getConfig();
}
serverUrl = sasClientConfig.serverUrl;
appLoc = sasClientConfig.rootFolderName as string;
}
const members =
serviceJson.members[0].name === "services"
? serviceJson.members[0].members
: serviceJson.members;
await this.createFoldersAndServices(
appLoc,
members,
accessToken,
sasApiClient
);
}
private setCsrfTokenApi = (csrfToken: CsrfToken) => {
this.csrfTokenApi = csrfToken;
};
private async resendWaitingRequests() {
for (const sasjsWaitingRequest of this.sasjsWaitingRequests) {
this.request(
sasjsWaitingRequest.SASjob,
sasjsWaitingRequest.data,
sasjsWaitingRequest.params
).then(
this.request(sasjsWaitingRequest.SASjob, sasjsWaitingRequest.data).then(
(res: any) => {
sasjsWaitingRequest.requestPromise.resolve(res);
},
@@ -717,12 +929,12 @@ export default class SASjs {
this.sasjsWaitingRequests = [];
}
private getRequestParams(): any {
private getRequestParamsWeb(): any {
const requestParams: any = {};
// if (this._csrf) {
// requestParams["_csrf"] = this._csrf;
// }
if (this.csrfTokenWeb) {
requestParams["_csrf"] = this.csrfTokenWeb.value;
}
if (this.sasjsConfig.debug) {
requestParams["_omittextlog"] = "false";
@@ -788,23 +1000,6 @@ export default class SASjs {
return uri;
}
private parseSAS9Response(response: string) {
let sas9Response = "";
if (response.includes(">>weboutBEGIN<<")) {
try {
sas9Response = response
.split(">>weboutBEGIN<<")[1]
.split(">>weboutEND<<")[0];
} catch (e) {
sas9Response = "";
console.error(e);
}
}
return sas9Response;
}
private parseSAS9ErrorResponse(response: string) {
const logLines = response.split("\n");
const parsedLines: string[] = [];
@@ -859,14 +1054,25 @@ export default class SASjs {
let generatedCode = "";
let sasWork = null;
if (response) {
sourceCode = parseSourceCode(response);
generatedCode = parseGeneratedCode(response);
sasWork = await this.parseSasWork(response);
if (response && response.result && response.log) {
sourceCode = parseSourceCode(response.log);
generatedCode = parseGeneratedCode(response.log);
if (this.sasjsConfig.debug) {
sasWork = JSON.parse(parseWeboutResponse(response.result)).WORK;
} else {
sasWork = JSON.parse(response.result).WORK;
}
} else {
if (response) {
sourceCode = parseSourceCode(response);
generatedCode = parseGeneratedCode(response);
sasWork = await this.parseSasWork(response);
}
}
this.sasjsRequests.push({
logFile: response,
logFile: (response && response.log) || response,
serviceLink: program,
timestamp: new Date(),
sourceCode,
@@ -885,9 +1091,9 @@ export default class SASjs {
if (this.sasjsConfig.serverType === ServerType.SAS9) {
try {
jsonResponse = JSON.parse(this.parseSAS9Response(response));
jsonResponse = JSON.parse(parseWeboutResponse(response));
} catch (e) {
console.log(e);
console.error(e);
}
} else {
await this.parseSASVIYADebugResponse(response).then(
@@ -895,11 +1101,11 @@ export default class SASjs {
try {
jsonResponse = JSON.parse(resText);
} catch (e) {
console.log(e);
console.error(e);
}
},
(err: any) => {
console.log(err);
console.error(err);
}
);
}
@@ -916,6 +1122,10 @@ export default class SASjs {
return sortedRequests;
}
public clearSasRequests() {
this.sasjsRequests = [];
}
private setupConfiguration() {
if (
this.sasjsConfig.serverUrl === undefined ||
@@ -951,7 +1161,9 @@ export default class SASjs {
else
this.sasViyaApiClient = new SASViyaApiClient(
this.sasjsConfig.serverUrl,
this.sasjsConfig.appLoc
this.sasjsConfig.appLoc,
this.sasjsConfig.contextName,
this.setCsrfTokenApi
);
}
if (this.sasjsConfig.serverType === ServerType.SAS9) {
@@ -959,6 +1171,12 @@ export default class SASjs {
this.sas9ApiClient!.setConfig(this.sasjsConfig.serverUrl);
else this.sas9ApiClient = new SAS9ApiClient(this.sasjsConfig.serverUrl);
}
this.fileUploader = new FileUploader(
this.sasjsConfig.appLoc,
this.sasjsConfig.serverUrl,
this.jobsPath
);
}
private setLoginUrl = (matches: RegExpExecArray) => {

160
src/SessionManager.ts Normal file
View File

@@ -0,0 +1,160 @@
import { Session, Context, CsrfToken } from "./types";
import { asyncForEach, makeRequest } from "./utils";
const MAX_SESSION_COUNT = 1;
export class SessionManager {
constructor(
private serverUrl: string,
private contextName: string,
private setCsrfToken: (csrfToken: CsrfToken) => void
) {}
private sessions: Session[] = [];
private currentContext: Context | null = null;
private csrfToken: CsrfToken | null = null;
async getSession(accessToken?: string) {
await this.createSessions(accessToken);
this.createAndWaitForSession(accessToken);
const session = this.sessions.pop();
return session;
}
async clearSession(id: string, accessToken?: string) {
const deleteSessionRequest = {
method: "DELETE",
headers: this.getHeaders(accessToken),
};
return await this.request<Session>(
`${this.serverUrl}/compute/sessions/${id}`,
deleteSessionRequest
).then(() => {
this.sessions = this.sessions.filter((s) => s.id !== id);
});
}
private async createSessions(accessToken?: string) {
if (!this.sessions.length) {
if (!this.currentContext) {
await this.setCurrentContext(accessToken);
}
await asyncForEach(new Array(MAX_SESSION_COUNT), async () => {
const createdSession = await this.createAndWaitForSession(accessToken);
this.sessions.push(createdSession);
});
}
}
private async createAndWaitForSession(accessToken?: string) {
const createSessionRequest = {
method: "POST",
headers: this.getHeaders(accessToken),
};
const { result: createdSession, etag } = await this.request<Session>(
`${this.serverUrl}/compute/contexts/${this.currentContext!.id}/sessions`,
createSessionRequest
);
await this.waitForSession(createdSession, etag);
this.sessions.push(createdSession);
return createdSession;
}
private async setCurrentContext(accessToken?: string) {
if (!this.currentContext) {
const { result: contexts } = await this.request<{
items: Context[];
}>(`${this.serverUrl}/compute/contexts`, {
headers: this.getHeaders(accessToken),
});
const contextsList =
contexts && contexts.items && contexts.items.length
? contexts.items
: [];
const currentContext = contextsList.find(
(c: any) => c.name === this.contextName
);
if (!currentContext) {
throw new Error(
`The context ${this.contextName} was not found on the server ${this.serverUrl}`
);
}
this.currentContext = currentContext;
}
}
private getHeaders(accessToken?: string) {
const headers: any = {
"Content-Type": "application/json",
};
if (accessToken) {
headers.Authorization = `Bearer ${accessToken}`;
}
return headers;
}
private async waitForSession(
session: Session,
etag: string | null,
accessToken?: string,
silent = false
) {
let sessionState = session.state;
const headers: any = {
...this.getHeaders(accessToken),
"If-None-Match": etag,
};
const stateLink = session.links.find((l: any) => l.rel === "state");
return new Promise(async (resolve, _) => {
if (sessionState === "pending") {
if (stateLink) {
if (!silent) {
console.log("Polling session status... \n");
}
const { result: state } = await this.request<string>(
`${this.serverUrl}${stateLink.href}?wait=30`,
{
headers,
},
"text"
);
sessionState = state.trim();
if (!silent) {
console.log(`Current state: ${sessionState}\n`);
}
resolve(sessionState);
}
} else {
resolve(sessionState);
}
});
}
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
);
}
}

View File

@@ -8,4 +8,5 @@ export interface Job {
createdBy: string;
links: Link[];
results: JobResult;
error?: any;
}

View File

@@ -0,0 +1,3 @@
export interface JobDefinition {
code: string;
}

View File

@@ -27,4 +27,5 @@ export class SASjsConfig {
*/
debug: boolean = true;
contextName: string = "";
useComputeApi = false;
}

View File

@@ -10,5 +10,5 @@ export interface SASjsWaitingRequest {
};
SASjob: string;
data: any;
params?: any;
config?: any;
}

View File

@@ -1,3 +1,7 @@
import { Link } from "./Link";
export interface Session {
id: string;
state: string;
links: Link[];
}

9
src/types/UploadFile.ts Normal file
View File

@@ -0,0 +1,9 @@
/**
* Represents a object that is passed to file uploader
*
*/
export interface UploadFile {
file: File;
fileName: string;
}

View File

@@ -8,3 +8,4 @@ export * from "./SASjsRequest";
export * from "./SASjsWaitingRequest";
export * from "./ServerType";
export * from "./Session";
export * from "./UploadFile";

View File

@@ -0,0 +1,33 @@
import { convertToCSV } from "./convertToCsv";
import { splitChunks } from "./splitChunks";
export const formatDataForRequest = (data: any) => {
const sasjsTables = [];
let tableCounter = 0;
const result: any = {};
for (const tableName in data) {
tableCounter++;
sasjsTables.push(tableName);
const csv = convertToCSV(data[tableName]);
if (csv === "ERROR: LARGE STRING LENGTH") {
throw new Error(
"The max length of a string value in SASjs is 32765 characters."
);
}
// if csv has length more then 16k, send in chunks
if (csv.length > 16000) {
const csvChunks = splitChunks(csv);
// append chunks to form data with same key
result[`sasjs${tableCounter}data0`] = csvChunks.length;
csvChunks.forEach((chunk, index) => {
result[`sasjs${tableCounter}data${index + 1}`] = chunk;
});
} else {
result[`sasjs${tableCounter}data`] = csv;
}
}
result["sasjs_tables"] = sasjsTables.join(" ");
return result;
};

View File

@@ -12,3 +12,4 @@ export * from "./parseSourceCode";
export * from "./parseSasViyaLog";
export * from "./serialize";
export * from "./splitChunks";
export * from "./parseWeboutResponse";

31
src/utils/isIeOrEdge.ts Normal file
View File

@@ -0,0 +1,31 @@
export function isIEorEdgeOrOldFirefox() {
const ua = window.navigator.userAgent;
if (ua.indexOf("Firefox") > 0) {
const version = parseInt(
ua.substring(ua.lastIndexOf("Firefox/") + 8, ua.length),
10
);
return version <= 60;
}
const msie = ua.indexOf("MSIE ");
if (msie > 0) {
// IE 10 or older => return version number
return true;
}
const trident = ua.indexOf("Trident/");
if (trident > 0) {
return true;
}
const edge = ua.indexOf("Edge/");
if (edge > 0) {
// Edge (IE 12+) => return version number
return true;
}
// other browser
return false;
}

View File

@@ -1,16 +1,26 @@
import { CsrfToken } from "../types";
import { needsRetry } from "./needsRetry";
let retryCount: number = 0;
let retryLimit: number = 5;
export async function makeRequest<T>(
url: string,
request: RequestInit,
callback: (value: CsrfToken) => any,
contentType: "text" | "json" = "json"
): Promise<T> {
): Promise<{ result: T; etag: string | null }> {
let retryRequest: any = null;
const responseTransform =
contentType === "json"
? (res: Response) => res.json()
: (res: Response) => res.text();
const result = await fetch(url, request).then((response) => {
let etag = null;
const result = await fetch(url, request).then(async (response) => {
if (response.redirected && response.url.includes("SASLogon/login")) {
return Promise.reject({ status: 401 });
}
if (!response.ok) {
if (response.status === 403) {
const tokenHeader = response.headers.get("X-CSRF-HEADER");
@@ -22,16 +32,76 @@ export async function makeRequest<T>(
value: token || "",
});
const retryRequest = {
retryRequest = {
...request,
headers: { ...request.headers, [tokenHeader]: token },
};
return fetch(url, retryRequest).then(responseTransform);
return fetch(url, retryRequest).then((res) => {
etag = res.headers.get("ETag");
return responseTransform(res);
});
}
} else {
const body = await response.text();
if (needsRetry(body)) {
if (retryCount < retryLimit) {
retryCount++;
let retryResponse = await makeRequest(
url,
retryRequest || request,
callback,
contentType
);
retryCount = 0;
etag = retryResponse.etag;
return retryResponse.result;
} else {
retryCount = 0;
throw new Error("Request retry limit exceeded");
}
}
return Promise.reject({ status: response.status, body });
}
} else {
return responseTransform(response);
if (response.status === 204) {
return Promise.resolve();
}
const responseTransformed = await responseTransform(response);
let responseText = "";
if (typeof responseTransformed === "string") {
responseText = responseTransformed;
} else {
responseText = JSON.stringify(responseTransformed);
}
if (needsRetry(responseText)) {
if (retryCount < retryLimit) {
retryCount++;
const retryResponse = await makeRequest(
url,
retryRequest || request,
callback,
contentType
);
retryCount = 0;
etag = retryResponse.etag;
return retryResponse.result;
} else {
retryCount = 0;
throw new Error("Request retry limit exceeded");
}
}
etag = response.headers.get("ETag");
return responseTransformed;
}
});
return result;
return { result, etag };
}

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