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

Compare commits

...

45 Commits

Author SHA1 Message Date
Krishna Acondy
2cdab7522d Merge pull request #139 from sasjs/location-issue
fix(location): added handle cases when 'location' is not defined
2020-10-29 08:07:11 +00:00
Yury Shkoda
a07eabc408 fix(location): added handle cases when 'location' is not defined 2020-10-29 10:07:30 +03:00
Yury Shkoda
d5920c5885 Merge pull request #134 from sasjs/executeComputeJob
fix(executeComputeJob): added fix for cases when code was not provided
2020-10-21 11:55:43 +03:00
Yury Shkoda
6a3a6b4485 fix(executeComputeJob): added fix for cases when code was not provided 2020-10-21 11:45:21 +03:00
Krishna Acondy
2b1df0c61a Merge pull request #123 from sasjs/sasjs-job
feat(start-compute-job): Add API that returns immediately after job is started
2020-10-16 11:27:02 +01:00
Krishna Acondy
216725f306 chore(doc): update documentation 2020-10-16 11:04:03 +01:00
Krishna Acondy
3183f89a62 chore(*): fix lint warning 2020-10-16 10:58:04 +01:00
Krishna Acondy
f5cc16c3bd chore(create-job): add tests 2020-10-16 10:56:10 +01:00
Krishna Acondy
e78dc76e56 fix(config): set debug to false by default
feat(create-job): add the ability to wait for result
2020-10-16 10:55:56 +01:00
Krishna Acondy
bfdb5ef0a6 chore(*): regenerate documentation 2020-10-16 09:13:48 +01:00
Krishna Acondy
35353d3fce Merge branch 'master' into sasjs-job 2020-10-15 09:11:50 +01:00
Yury Shkoda
7a02c8ad34 Merge pull request #131 from sasjs/issue-124
fix(session): add internal SAS error handler
2020-10-14 14:03:58 +03:00
Yury Shkoda
331d9b0010 fix(session): add internal SAS error handler 2020-10-14 12:53:59 +03:00
Yury Shkoda
ef5686cce7 Merge branch 'master' into sasjs-job 2020-10-12 09:21:00 +03:00
Yury Shkoda
fa87111f4a Merge pull request #126 from sasjs/issue-124
fix(context): fixed 'getExecutableContexts' method
2020-10-07 17:53:31 +03:00
Yury Shkoda
94967b0f6c fix(context): fixed 'getExecutableContexts' method 2020-10-07 17:25:47 +03:00
Krishna Acondy
a07c16fb52 chore(start-compute-job): add test 2020-10-06 09:21:58 +01:00
Krishna Acondy
fd6905ea9f feat(start-compute-job): add API that starts a compute job and immediately returns the session 2020-10-06 09:21:15 +01:00
Krishna Acondy
08f58b5f4f fix(debug): only set session manager debug if it is defined 2020-10-06 08:17:02 +01:00
Krishna Acondy
bd8012fe3e fix(*): revert to older version of isomorphic-fetch 2020-10-03 18:19:06 +01:00
Krishna Acondy
fa531b34fd Merge pull request #120 from sasjs/session-manager-debug
fix(debug): propagate debug value from SASjs config
2020-10-03 17:41:35 +01:00
Krishna Acondy
354443c98b fix(debug): propagate debug value from SASjs config 2020-10-03 16:53:00 +01:00
Krishna Acondy
ee30ab195f Merge pull request #115 from sasjs/issue-114
chore(error-message): updated error message for forbidden request
2020-10-01 09:10:35 +01:00
Yury Shkoda
02c1712d22 chore(error-message): updated error message for forbidden request 2020-10-01 09:49:24 +03:00
Krishna Acondy
37def7a956 Merge pull request #111 from sasjs/dependabot/npm_and_yarn/isomorphic-fetch-3.0.0
chore(deps): bump isomorphic-fetch from 2.2.1 to 3.0.0
2020-09-29 20:03:06 +01:00
Krishna Acondy
653e3d05e0 Merge branch 'master' into dependabot/npm_and_yarn/isomorphic-fetch-3.0.0 2020-09-29 19:55:08 +01:00
Yury Shkoda
d8467c24b1 Merge pull request #112 from sasjs/cli-issue-73
feat(folder-management): made folder related methods public
2020-09-28 15:16:33 +03:00
Yury Shkoda
fc9056c1ac chore(folder-management): made 'moveFolder' method public and fixed 'createFolder' method 2020-09-28 14:59:27 +03:00
Yury Shkoda
9b1d295b82 feat(folder): made 'deleteFolder' method public 2020-09-26 11:41:18 +03:00
dependabot-preview[bot]
e2ea3f4ddc chore(deps): bump isomorphic-fetch from 2.2.1 to 3.0.0
Bumps [isomorphic-fetch](https://github.com/matthew-andrews/isomorphic-fetch) from 2.2.1 to 3.0.0.
- [Release notes](https://github.com/matthew-andrews/isomorphic-fetch/releases)
- [Commits](https://github.com/matthew-andrews/isomorphic-fetch/compare/v2.2.1...v3.0.0)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2020-09-25 23:51:17 +00:00
Allan Bowe
99d0b01a24 Update example.html 2020-09-24 23:00:37 +02:00
Yury Shkoda
131c672020 Merge pull request #110 from sasjs/cli-issue-105
fix(context): fixed 'ContextAllAttributes' interface
2020-09-24 17:10:14 +03:00
Yury Shkoda
338f2fb2dd Merge branch 'master' into cli-issue-105 2020-09-24 17:08:04 +03:00
Yury Shkoda
4552a9a856 fix(context): fixed 'ContextAllAttributes' interface 2020-09-24 16:51:50 +03:00
Yury Shkoda
daeb753f9e Merge pull request #109 from sasjs/cli-issue-105
feat(context): added getComputeContextById method
2020-09-24 16:10:02 +03:00
Yury Shkoda
f50a99d0b8 Merge branch 'master' into cli-issue-105 2020-09-24 16:08:09 +03:00
Yury Shkoda
e6d0d3efd5 docs(context): add docs for 'getComputeContextByName' method 2020-09-24 16:05:42 +03:00
Yury Shkoda
057460467c feat(context): added getComputeContextById method 2020-09-24 15:53:07 +03:00
Yury Shkoda
5aee9d955e Merge pull request #106 from sasjs/cli-issue-105
feat(context): made getContextByName function public
2020-09-24 08:38:47 +03:00
Yury Shkoda
7fb1da31e4 Merge branch 'master' into cli-issue-105 2020-09-24 08:35:28 +03:00
Allan Bowe
1aa92c0a69 Merge pull request #107 from sasjs/access-token-missed
Access token missed
2020-09-23 21:26:46 +02:00
Mihajlo Medjedovic
4c097a69fd style: lint 2020-09-23 20:43:41 +02:00
Mihajlo Medjedovic
2634933e84 fix: accessToken not passed in function calls 2020-09-23 20:41:31 +02:00
Yury Shkoda
d60c0850c2 docs(context): update docs related to getComputeContextByName function 2020-09-23 17:21:27 +03:00
Yury Shkoda
491bc3371c feat(context): made getContextByName function public 2020-09-23 16:38:21 +03:00
63 changed files with 20621 additions and 284 deletions

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -181,6 +181,9 @@
<li class=" tsd-kind-interface tsd-parent-kind-module root">
<a href="../interfaces/types.context.html" class="tsd-kind-icon">Context</a>
</li>
<li class=" tsd-kind-interface tsd-parent-kind-module root">
<a href="../interfaces/types.contextallattributes.html" class="tsd-kind-icon">Context<wbr>All<wbr>Attributes</a>
</li>
<li class=" tsd-kind-interface tsd-parent-kind-module root">
<a href="../interfaces/types.csrftoken.html" class="tsd-kind-icon">Csrf<wbr>Token</a>
</li>

View File

@@ -331,6 +331,9 @@
<li class=" tsd-kind-interface tsd-parent-kind-module root">
<a href="../interfaces/types.context.html" class="tsd-kind-icon">Context</a>
</li>
<li class=" tsd-kind-interface tsd-parent-kind-module root">
<a href="../interfaces/types.contextallattributes.html" class="tsd-kind-icon">Context<wbr>All<wbr>Attributes</a>
</li>
<li class=" tsd-kind-interface tsd-parent-kind-module root">
<a href="../interfaces/types.csrftoken.html" class="tsd-kind-icon">Csrf<wbr>Token</a>
</li>

View File

@@ -154,6 +154,9 @@
<li class=" tsd-kind-interface tsd-parent-kind-module root">
<a href="../interfaces/types.context.html" class="tsd-kind-icon">Context</a>
</li>
<li class=" tsd-kind-interface tsd-parent-kind-module root">
<a href="../interfaces/types.contextallattributes.html" class="tsd-kind-icon">Context<wbr>All<wbr>Attributes</a>
</li>
<li class=" tsd-kind-interface tsd-parent-kind-module root">
<a href="../interfaces/types.csrftoken.html" class="tsd-kind-icon">Csrf<wbr>Token</a>
</li>

View File

@@ -76,7 +76,7 @@
<section class="tsd-index-section ">
<h3>Modules</h3>
<ul class="tsd-index-list">
<li class="tsd-kind-module tsd-is-not-exported"><a href="modules/reflection-725.html" class="tsd-kind-icon"><em>Module</em></a></li>
<li class="tsd-kind-module tsd-is-not-exported"><a href="modules/reflection-787.html" class="tsd-kind-icon"><em>Module</em></a></li>
<li class="tsd-kind-module"><a href="modules/types.html" class="tsd-kind-icon">types</a></li>
<li class="tsd-kind-module"><a href="modules/utils.html" class="tsd-kind-icon">utils</a></li>
</ul>

View File

@@ -211,6 +211,9 @@
</li>
</ul>
<ul class="after-current">
<li class=" tsd-kind-interface tsd-parent-kind-module root">
<a href="types.contextallattributes.html" class="tsd-kind-icon">Context<wbr>All<wbr>Attributes</a>
</li>
<li class=" tsd-kind-interface tsd-parent-kind-module root">
<a href="types.csrftoken.html" class="tsd-kind-icon">Csrf<wbr>Token</a>
</li>

File diff suppressed because one or more lines are too long

View File

@@ -145,6 +145,9 @@
<li class=" tsd-kind-interface tsd-parent-kind-module root">
<a href="types.context.html" class="tsd-kind-icon">Context</a>
</li>
<li class=" tsd-kind-interface tsd-parent-kind-module root">
<a href="types.contextallattributes.html" class="tsd-kind-icon">Context<wbr>All<wbr>Attributes</a>
</li>
</ul>
<ul class="current">
<li class="current tsd-kind-interface tsd-parent-kind-module root">

View File

@@ -95,6 +95,7 @@
<li class="tsd-kind-property tsd-parent-kind-interface"><a href="types.editcontextinput.html#authorizedusers" class="tsd-kind-icon">authorizedUsers</a></li>
<li class="tsd-kind-property tsd-parent-kind-interface"><a href="types.editcontextinput.html#description" class="tsd-kind-icon">description</a></li>
<li class="tsd-kind-property tsd-parent-kind-interface"><a href="types.editcontextinput.html#environment" class="tsd-kind-icon">environment</a></li>
<li class="tsd-kind-property tsd-parent-kind-interface"><a href="types.editcontextinput.html#id" class="tsd-kind-icon">id</a></li>
<li class="tsd-kind-property tsd-parent-kind-interface"><a href="types.editcontextinput.html#launchcontext" class="tsd-kind-icon">launchContext</a></li>
<li class="tsd-kind-property tsd-parent-kind-interface"><a href="types.editcontextinput.html#name" class="tsd-kind-icon">name</a></li>
</ul>
@@ -174,6 +175,20 @@
</ul>
</aside>
</section>
<section class="tsd-panel tsd-member tsd-kind-property tsd-parent-kind-interface">
<a name="id" class="tsd-anchor"></a>
<h3><span class="tsd-flag ts-flagOptional">Optional</span> id</h3>
<div class="tsd-signature tsd-kind-icon">id<span class="tsd-signature-symbol">:</span> <span class="tsd-signature-type">undefined</span><span class="tsd-signature-symbol"> | </span><span class="tsd-signature-type">string</span></div>
<aside class="tsd-sources">
<ul>
<li>Defined in
<a href="https://github.com/sasjs/adapter/blob/master/src/types/Context.ts#L17">
types/Context.ts:17
</a>
</li>
</ul>
</aside>
</section>
<section class="tsd-panel tsd-member tsd-kind-property tsd-parent-kind-interface">
<a name="launchcontext" class="tsd-anchor"></a>
<h3><span class="tsd-flag ts-flagOptional">Optional</span> launch<wbr>Context</h3>
@@ -220,6 +235,9 @@
<li class=" tsd-kind-interface tsd-parent-kind-module root">
<a href="types.context.html" class="tsd-kind-icon">Context</a>
</li>
<li class=" tsd-kind-interface tsd-parent-kind-module root">
<a href="types.contextallattributes.html" class="tsd-kind-icon">Context<wbr>All<wbr>Attributes</a>
</li>
<li class=" tsd-kind-interface tsd-parent-kind-module root">
<a href="types.csrftoken.html" class="tsd-kind-icon">Csrf<wbr>Token</a>
</li>
@@ -243,6 +261,9 @@
<li class=" tsd-kind-property tsd-parent-kind-interface">
<a href="types.editcontextinput.html#environment" class="tsd-kind-icon">environment</a>
</li>
<li class=" tsd-kind-property tsd-parent-kind-interface">
<a href="types.editcontextinput.html#id" class="tsd-kind-icon">id</a>
</li>
<li class=" tsd-kind-property tsd-parent-kind-interface">
<a href="types.editcontextinput.html#launchcontext" class="tsd-kind-icon">launch<wbr>Context</a>
</li>

View File

@@ -160,6 +160,9 @@
<li class=" tsd-kind-interface tsd-parent-kind-module root">
<a href="types.context.html" class="tsd-kind-icon">Context</a>
</li>
<li class=" tsd-kind-interface tsd-parent-kind-module root">
<a href="types.contextallattributes.html" class="tsd-kind-icon">Context<wbr>All<wbr>Attributes</a>
</li>
<li class=" tsd-kind-interface tsd-parent-kind-module root">
<a href="types.csrftoken.html" class="tsd-kind-icon">Csrf<wbr>Token</a>
</li>

View File

@@ -235,6 +235,9 @@
<li class=" tsd-kind-interface tsd-parent-kind-module root">
<a href="types.context.html" class="tsd-kind-icon">Context</a>
</li>
<li class=" tsd-kind-interface tsd-parent-kind-module root">
<a href="types.contextallattributes.html" class="tsd-kind-icon">Context<wbr>All<wbr>Attributes</a>
</li>
<li class=" tsd-kind-interface tsd-parent-kind-module root">
<a href="types.csrftoken.html" class="tsd-kind-icon">Csrf<wbr>Token</a>
</li>

View File

@@ -130,6 +130,9 @@
<li class=" tsd-kind-interface tsd-parent-kind-module root">
<a href="types.context.html" class="tsd-kind-icon">Context</a>
</li>
<li class=" tsd-kind-interface tsd-parent-kind-module root">
<a href="types.contextallattributes.html" class="tsd-kind-icon">Context<wbr>All<wbr>Attributes</a>
</li>
<li class=" tsd-kind-interface tsd-parent-kind-module root">
<a href="types.csrftoken.html" class="tsd-kind-icon">Csrf<wbr>Token</a>
</li>

View File

@@ -130,6 +130,9 @@
<li class=" tsd-kind-interface tsd-parent-kind-module root">
<a href="types.context.html" class="tsd-kind-icon">Context</a>
</li>
<li class=" tsd-kind-interface tsd-parent-kind-module root">
<a href="types.contextallattributes.html" class="tsd-kind-icon">Context<wbr>All<wbr>Attributes</a>
</li>
<li class=" tsd-kind-interface tsd-parent-kind-module root">
<a href="types.csrftoken.html" class="tsd-kind-icon">Csrf<wbr>Token</a>
</li>

View File

@@ -190,6 +190,9 @@
<li class=" tsd-kind-interface tsd-parent-kind-module root">
<a href="types.context.html" class="tsd-kind-icon">Context</a>
</li>
<li class=" tsd-kind-interface tsd-parent-kind-module root">
<a href="types.contextallattributes.html" class="tsd-kind-icon">Context<wbr>All<wbr>Attributes</a>
</li>
<li class=" tsd-kind-interface tsd-parent-kind-module root">
<a href="types.csrftoken.html" class="tsd-kind-icon">Csrf<wbr>Token</a>
</li>

View File

@@ -210,6 +210,9 @@
<li class=" tsd-kind-interface tsd-parent-kind-module root">
<a href="types.context.html" class="tsd-kind-icon">Context</a>
</li>
<li class=" tsd-kind-interface tsd-parent-kind-module root">
<a href="types.contextallattributes.html" class="tsd-kind-icon">Context<wbr>All<wbr>Attributes</a>
</li>
<li class=" tsd-kind-interface tsd-parent-kind-module root">
<a href="types.csrftoken.html" class="tsd-kind-icon">Csrf<wbr>Token</a>
</li>

View File

@@ -194,6 +194,9 @@
<li class=" tsd-kind-interface tsd-parent-kind-module root">
<a href="types.context.html" class="tsd-kind-icon">Context</a>
</li>
<li class=" tsd-kind-interface tsd-parent-kind-module root">
<a href="types.contextallattributes.html" class="tsd-kind-icon">Context<wbr>All<wbr>Attributes</a>
</li>
<li class=" tsd-kind-interface tsd-parent-kind-module root">
<a href="types.csrftoken.html" class="tsd-kind-icon">Csrf<wbr>Token</a>
</li>

View File

@@ -198,6 +198,9 @@
<li class=" tsd-kind-interface tsd-parent-kind-module root">
<a href="types.context.html" class="tsd-kind-icon">Context</a>
</li>
<li class=" tsd-kind-interface tsd-parent-kind-module root">
<a href="types.contextallattributes.html" class="tsd-kind-icon">Context<wbr>All<wbr>Attributes</a>
</li>
<li class=" tsd-kind-interface tsd-parent-kind-module root">
<a href="types.csrftoken.html" class="tsd-kind-icon">Csrf<wbr>Token</a>
</li>

View File

@@ -150,6 +150,9 @@
<li class=" tsd-kind-interface tsd-parent-kind-module root">
<a href="types.context.html" class="tsd-kind-icon">Context</a>
</li>
<li class=" tsd-kind-interface tsd-parent-kind-module root">
<a href="types.contextallattributes.html" class="tsd-kind-icon">Context<wbr>All<wbr>Attributes</a>
</li>
<li class=" tsd-kind-interface tsd-parent-kind-module root">
<a href="types.csrftoken.html" class="tsd-kind-icon">Csrf<wbr>Token</a>
</li>

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -93,6 +93,7 @@
<h3>Interfaces</h3>
<ul class="tsd-index-list">
<li class="tsd-kind-interface tsd-parent-kind-module"><a href="../interfaces/types.context.html" class="tsd-kind-icon">Context</a></li>
<li class="tsd-kind-interface tsd-parent-kind-module"><a href="../interfaces/types.contextallattributes.html" class="tsd-kind-icon">ContextAllAttributes</a></li>
<li class="tsd-kind-interface tsd-parent-kind-module"><a href="../interfaces/types.csrftoken.html" class="tsd-kind-icon">CsrfToken</a></li>
<li class="tsd-kind-interface tsd-parent-kind-module"><a href="../interfaces/types.editcontextinput.html" class="tsd-kind-icon">EditContextInput</a></li>
<li class="tsd-kind-interface tsd-parent-kind-module"><a href="../interfaces/types.folder.html" class="tsd-kind-icon">Folder</a></li>
@@ -126,6 +127,9 @@
<li class=" tsd-kind-interface tsd-parent-kind-module root">
<a href="../interfaces/types.context.html" class="tsd-kind-icon">Context</a>
</li>
<li class=" tsd-kind-interface tsd-parent-kind-module root">
<a href="../interfaces/types.contextallattributes.html" class="tsd-kind-icon">Context<wbr>All<wbr>Attributes</a>
</li>
<li class=" tsd-kind-interface tsd-parent-kind-module root">
<a href="../interfaces/types.csrftoken.html" class="tsd-kind-icon">Csrf<wbr>Token</a>
</li>

View File

@@ -30,8 +30,8 @@
$('#chart-container').append('<canvas id="myChart" style="display: none;"></canvas>')
// make a request to a SAS service
var type = $("#cars")[0].options[$("#cars")[0].selectedIndex].value;
// request data from an endpoint under your appLoc
sasJs.request("/common/getdata", {
// request data from an endpoint under your appLoc (missing opening slash implies relative path)
sasJs.request("common/getdata", {
// send data as an array of objects - each object is one row
fromjs: [{ type: type }]
}).then((response) => {

25
package-lock.json generated
View File

@@ -3704,11 +3704,21 @@
"dev": true
},
"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": {
@@ -4967,6 +4977,7 @@
"version": "0.4.24",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
"integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
"dev": true,
"requires": {
"safer-buffer": ">= 2.1.2 < 3"
}
@@ -16070,9 +16081,9 @@
}
},
"whatwg-fetch": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-3.0.0.tgz",
"integrity": "sha512-9GSJUgz1D4MfyKU7KRqwOjXCXTqWdFNvEr7eUBYchQiVc744mqK/MzXPNR2WsPkmkOa4ywfg8C2n8h+13Bey1Q=="
"version": "3.4.1",
"resolved": "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-3.4.1.tgz",
"integrity": "sha512-sofZVzE1wKwO+EYPbWfiwzaKovWiZXf4coEzjGP9b2GBVgQRLQUZ2QcuPpQExGDAW5GItpEm6Tl4OU5mywnAoQ=="
},
"whatwg-mimetype": {
"version": "2.3.0",

View File

@@ -5,6 +5,7 @@ import { sendArrTests, sendObjTests } from "./testSuites/RequestData";
import { specialCaseTests } from "./testSuites/SpecialCases";
import { sasjsRequestTests } from "./testSuites/SasjsRequests";
import "@sasjs/test-framework/dist/index.css";
import { computeTests } from "./testSuites/Compute";
const App = (): ReactElement<{}> => {
const { adapter, config } = useContext(AppContext);
@@ -17,7 +18,8 @@ const App = (): ReactElement<{}> => {
sendArrTests(adapter),
sendObjTests(adapter),
specialCaseTests(adapter),
sasjsRequestTests(adapter)
sasjsRequestTests(adapter),
computeTests(adapter)
]);
}
}, [adapter, config]);

View File

@@ -0,0 +1,41 @@
import SASjs from "@sasjs/adapter";
import { TestSuite } from "@sasjs/test-framework";
export const computeTests = (adapter: SASjs): TestSuite => ({
name: "Compute",
tests: [
{
title: "Start Compute Job - not waiting for result",
description: "Should start a compute job and return the session",
test: () => {
const data: any = { table1: [{ col1: "first col value" }] };
return adapter.startComputeJob("/Public/app/common/sendArr", data);
},
assertion: (res: any) => {
const expectedProperties = ["id", "applicationName", "attributes"]
return validate(expectedProperties, res);
}
},
{
title: "Start Compute Job - waiting for result",
description: "Should start a compute job and return the job",
test: () => {
const data: any = { table1: [{ col1: "first col value" }] };
return adapter.startComputeJob("/Public/app/common/sendArr", data, {}, "", true);
},
assertion: (res: any) => {
const expectedProperties = ["id", "state", "creationTimeStamp", "jobConditionCode"]
return validate(expectedProperties, res);
}
}
]
});
const validate = (expectedProperties: string[], data: any): boolean => {
const actualProperties = Object.keys(data);
const isValid = expectedProperties.every(
(property) => actualProperties.includes(property)
);
return isValid
}

View File

@@ -12,6 +12,7 @@ import {
Job,
Session,
Context,
ContextAllAttributes,
Folder,
CsrfToken,
EditContextInput,
@@ -37,14 +38,25 @@ export class SASViyaApiClient {
private csrfToken: CsrfToken | null = null
private fileUploadCsrfToken: CsrfToken | null = null
private _debug = false
private sessionManager = new SessionManager(
this.serverUrl,
this.contextName,
this.setCsrfToken
)
private isForceDeploy: boolean = false
private folderMap = new Map<string, Job[]>()
public get debug() {
return this._debug
}
public set debug(value: boolean) {
this._debug = value
if (this.sessionManager) {
this.sessionManager.debug = value
}
}
/**
* Returns a list of jobs in the currently set root folder.
*/
@@ -135,42 +147,51 @@ export class SASViyaApiClient {
const promises = contextsList.map((context: any) => {
const linesOfCode = ['%put &=sysuserid;']
return this.executeScript(
`test-${context.name}`,
linesOfCode,
context.name,
accessToken,
false,
null,
true
).catch(() => null)
return () =>
this.executeScript(
`test-${context.name}`,
linesOfCode,
context.name,
accessToken,
null,
true,
true
).catch((err) => err)
})
const results = await Promise.all(promises)
let results: any[] = []
for (const promise of promises) results.push(await promise())
results.forEach((result: any, index: number) => {
if (result) {
let sysUserId = ''
if (result && result.body && result.body.details) {
try {
const resultParsed = JSON.parse(result.body.details)
if (result.log) {
const sysUserIdLog = result.log
.split('\n')
.find((line: string) => line.startsWith('SYSUSERID='))
if (resultParsed && resultParsed.body) {
let sysUserId = ''
if (sysUserIdLog) {
sysUserId = sysUserIdLog.replace('SYSUSERID=', '')
const sysUserIdLog = resultParsed.body
.split('\n')
.find((line: string) => line.startsWith('SYSUSERID='))
if (sysUserIdLog) {
sysUserId = sysUserIdLog.replace('SYSUSERID=', '')
executableContexts.push({
createdBy: contextsList[index].createdBy,
id: contextsList[index].id,
name: contextsList[index].name,
version: contextsList[index].version,
attributes: {
sysUserId
}
})
}
}
} catch (error) {
throw error
}
executableContexts.push({
createdBy: contextsList[index].createdBy,
id: contextsList[index].id,
name: contextsList[index].name,
version: contextsList[index].version,
attributes: {
sysUserId
}
})
}
})
@@ -224,16 +245,16 @@ export class SASViyaApiClient {
* @param launchContextName - the name of the launcher context used by the compute service.
* @param sharedAccountId - the ID of the account to run the servers for this context.
* @param autoExecLines - the lines of code to execute during session initialization.
* @param authorizedUsers - an optional list of authorized user IDs.
* @param accessToken - an access token for an authorized user.
* @param authorizedUsers - an optional list of authorized user IDs.
*/
public async createContext(
contextName: string,
launchContextName: string,
sharedAccountId: string,
autoExecLines: string[],
authorizedUsers: string[],
accessToken?: string
accessToken?: string,
authorizedUsers?: string[]
) {
if (!contextName) {
throw new Error('Context name is required.')
@@ -313,10 +334,24 @@ export class SASViyaApiClient {
headers.Authorization = `Bearer ${accessToken}`
}
const originalContext = await this.getContextByName(
let originalContext
originalContext = await this.getComputeContextByName(
contextName,
accessToken
)
).catch((err) => {
throw err
})
// Try to find context by id, when context name has been changed.
if (!originalContext) {
originalContext = await this.getComputeContextById(
editedContext.id!,
accessToken
).catch((err) => {
throw err
})
}
const { result: context, etag } = await this.request<Context>(
`${this.serverUrl}/compute/contexts/${originalContext.id}`,
@@ -372,7 +407,7 @@ export class SASViyaApiClient {
headers.Authorization = `Bearer ${accessToken}`
}
const context = await this.getContextByName(contextName, accessToken)
const context = await this.getComputeContextByName(contextName, accessToken)
const deleteContextRequest: RequestInit = {
method: 'DELETE',
@@ -392,7 +427,6 @@ export class SASViyaApiClient {
* @param contextName - the context to execute the code in.
* @param accessToken - an access token for an authorized user.
* @param sessionId - optional session ID to reuse.
* @param silent - optional flag to disable logging.
* @param data - execution data.
* @param debug - when set to true, the log will be returned.
* @param expectWebout - when set to true, the automatic _webout fileref will be checked for content, and that content returned. This fileref is used when the Job contains a SASjs web request (as opposed to executing arbitrary SAS code).
@@ -402,12 +436,10 @@ export class SASViyaApiClient {
linesOfCode: string[],
contextName: string,
accessToken?: string,
silent = false,
data = null,
debug = false,
expectWebout = false
expectWebout = false,
waitForResult = true
): Promise<any> {
silent = !debug
try {
const headers: any = {
'Content-Type': 'application/json'
@@ -418,7 +450,12 @@ export class SASViyaApiClient {
}
let executionSessionId: string
const session = await this.sessionManager.getSession(accessToken)
const session = await this.sessionManager
.getSession(accessToken)
.catch((err) => {
throw err
})
executionSessionId = session!.id
const jobArguments: { [key: string]: any } = {
@@ -430,7 +467,7 @@ export class SASViyaApiClient {
_OMITTEXTLOG: true
}
if (debug) {
if (this.debug) {
jobArguments['_OMITTEXTLOG'] = false
jobArguments['_OMITSESSIONRESULTS'] = false
jobArguments['_DEBUG'] = 131
@@ -457,7 +494,9 @@ export class SASViyaApiClient {
if (data) {
if (JSON.stringify(data).includes(';')) {
files = await this.uploadTables(data, accessToken)
files = await this.uploadTables(data, accessToken).catch((err) => {
throw err
})
jobVariables['_webin_file_count'] = files.length
@@ -488,9 +527,15 @@ export class SASViyaApiClient {
const { result: postedJob, etag } = await this.request<Job>(
`${this.serverUrl}/compute/sessions/${executionSessionId}/jobs`,
postJobRequest
)
).catch((err) => {
throw err
})
if (!silent) {
if (!waitForResult) {
return session
}
if (this.debug) {
console.log(`Job has been submitted for '${fileName}'.`)
console.log(
`You can monitor the job progress at '${this.serverUrl}${
@@ -499,32 +544,33 @@ export class SASViyaApiClient {
)
}
const jobStatus = await this.pollJobState(
postedJob,
etag,
accessToken,
silent
)
const jobStatus = await this.pollJobState(postedJob, etag, accessToken)
const { result: currentJob } = await this.request<Job>(
`${this.serverUrl}/compute/sessions/${executionSessionId}/jobs/${postedJob.id}`,
{ headers }
)
).catch((err) => {
throw err
})
let jobResult
let log
const logLink = currentJob.links.find((l) => l.rel === 'log')
if (debug && logLink) {
if (this.debug && 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')
)
.then((res: any) =>
res.result.items.map((i: any) => i.line).join('\n')
)
.catch((err) => {
throw err
})
}
if (jobStatus === 'failed' || jobStatus === 'error') {
@@ -535,6 +581,8 @@ export class SASViyaApiClient {
if (expectWebout) {
resultLink = `/compute/sessions/${executionSessionId}/filerefs/_webout/content`
} else {
return currentJob
}
if (resultLink) {
@@ -550,9 +598,13 @@ export class SASViyaApiClient {
{
headers
}
).then((res: any) =>
res.result.items.map((i: any) => i.line).join('\n')
)
.then((res: any) =>
res.result.items.map((i: any) => i.line).join('\n')
)
.catch((err) => {
throw err
})
return Promise.reject(
new ErrorResponse('Job execution failed', {
@@ -568,7 +620,11 @@ export class SASViyaApiClient {
})
}
await this.sessionManager.clearSession(executionSessionId, accessToken)
await this.sessionManager
.clearSession(executionSessionId, accessToken)
.catch((err) => {
throw err
})
return { result: jobResult?.result, log }
} catch (e) {
@@ -578,9 +634,9 @@ export class SASViyaApiClient {
linesOfCode,
contextName,
accessToken,
silent,
data,
debug
false,
true
)
} else {
throw e
@@ -612,8 +668,6 @@ export class SASViyaApiClient {
if (!parentFolderUri && parentFolderPath) {
parentFolderUri = await this.getFolderUri(parentFolderPath, accessToken)
if (!parentFolderUri) {
if (isForced) this.isForceDeploy = true
console.log(
`Parent folder at path '${parentFolderPath}' is not present.`
)
@@ -639,37 +693,16 @@ export class SASViyaApiClient {
`Parent folder '${newFolderName}' has been successfully created.`
)
parentFolderUri = `/folders/folders/${parentFolder.id}`
} else if (isForced && accessToken && !this.isForceDeploy) {
this.isForceDeploy = true
} else if (isForced && accessToken) {
const folderPath = parentFolderPath + '/' + folderName
const folderUri = await this.getFolderUri(folderPath, accessToken)
await this.deleteFolder(parentFolderPath, accessToken)
const newParentFolderPath = parentFolderPath.substring(
0,
parentFolderPath.lastIndexOf('/')
)
const newFolderName = `${parentFolderPath.split('/').pop()}`
if (newParentFolderPath === '') {
throw new Error(`Root folder has to be present on the server.`)
if (folderUri) {
await this.deleteFolder(
parentFolderPath + '/' + folderName,
accessToken
)
}
console.log(
`Creating parent folder:\n'${newFolderName}' in '${newParentFolderPath}'`
)
const parentFolder = await this.createFolder(
newFolderName,
newParentFolderPath,
undefined,
accessToken
)
console.log(
`Parent folder '${newFolderName}' has been successfully created.`
)
parentFolderUri = `/folders/folders/${parentFolder.id}`
}
}
@@ -916,13 +949,16 @@ export class SASViyaApiClient {
* @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.
* @param waitForResult - a boolean indicating if the function should wait for a result.
* @param expectWebout - a boolean indicating whether to expect a _webout response.
*/
public async executeComputeJob(
sasJob: string,
contextName: string,
debug: boolean,
data?: any,
accessToken?: string
accessToken?: string,
waitForResult = true,
expectWebout = false
) {
if (isRelativePath(sasJob) && !this.rootFolderName) {
throw new Error(
@@ -932,7 +968,10 @@ export class SASViyaApiClient {
if (isRelativePath(sasJob)) {
const folderName = sasJob.split('/')[0]
await this.populateFolderMap(`${this.rootFolderName}/${folderName}`)
await this.populateFolderMap(
`${this.rootFolderName}/${folderName}`,
accessToken
)
if (!this.folderMap.get(`${this.rootFolderName}/${folderName}`)) {
throw new Error(
@@ -993,16 +1032,17 @@ export class SASViyaApiClient {
jobToExecute.code = code
}
if (!code) code = ''
const linesToExecute = code.replace(/\r\n/g, '\n').split('\n')
return await this.executeScript(
sasJob,
linesToExecute,
contextName,
accessToken,
true,
data,
debug,
true
expectWebout,
waitForResult
)
}
@@ -1029,7 +1069,10 @@ export class SASViyaApiClient {
if (isRelativePath(sasJob)) {
const folderName = sasJob.split('/')[0]
await this.populateFolderMap(`${this.rootFolderName}/${folderName}`)
await this.populateFolderMap(
`${this.rootFolderName}/${folderName}`,
accessToken
)
if (!this.folderMap.get(`${this.rootFolderName}/${folderName}`)) {
throw new Error(
@@ -1130,12 +1173,7 @@ export class SASViyaApiClient {
`${this.serverUrl}/jobExecution/jobs?_action=wait`,
postJobRequest
)
const jobStatus = await this.pollJobState(
postedJob,
etag,
accessToken,
true
)
const jobStatus = await this.pollJobState(postedJob, etag, accessToken)
const { result: currentJob } = await this.request<Job>(
`${this.serverUrl}/jobExecution/jobs/${postedJob.id}`,
{ headers }
@@ -1200,8 +1238,7 @@ export class SASViyaApiClient {
private async pollJobState(
postedJob: any,
etag: string | null,
accessToken?: string,
silent = false
accessToken?: string
) {
const MAX_POLL_COUNT = 1000
const POLL_INTERVAL = 100
@@ -1240,7 +1277,7 @@ export class SASViyaApiClient {
postedJobState === 'pending'
) {
if (stateLink) {
if (!silent) {
if (this.debug) {
console.log('Polling job status... \n')
}
const { result: jobState } = await this.request<string>(
@@ -1252,7 +1289,7 @@ export class SASViyaApiClient {
)
postedJobState = jobState.trim()
if (!silent) {
if (this.debug) {
console.log(`Current state: ${postedJobState}\n`)
}
pollCount++
@@ -1268,49 +1305,6 @@ export class SASViyaApiClient {
})
}
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)
}
})
}
private async uploadTables(data: any, accessToken?: string) {
const uploadedFiles = []
const headers: any = {
@@ -1387,7 +1381,13 @@ export class SASViyaApiClient {
return `/folders/folders/${folder.id}`
}
private async getContextByName(
/**
* Returns a JSON representation of a compute context.
* @example: { "createdBy": "admin", "links": [...], "id": "ID", "version": 2, "name": "context1" }
* @param contextName - the name of the context to return.
* @param accessToken - an access token for an authorized user.
*/
public async getComputeContextByName(
contextName: string,
accessToken?: string
): Promise<Context> {
@@ -1402,9 +1402,7 @@ export class SASViyaApiClient {
const { result: contexts } = await this.request<{ items: Context[] }>(
`${this.serverUrl}/compute/contexts?filter=eq(name, "${contextName}")`,
{ headers }
).catch((err) => {
throw err
})
)
if (!contexts || !(contexts.items && contexts.items.length)) {
throw new Error(
@@ -1415,6 +1413,33 @@ export class SASViyaApiClient {
return contexts.items[0]
}
/**
* Returns a JSON representation of a compute context.
* @param contextId - an id of the context to return.
* @param accessToken - an access token for an authorized user.
*/
public async getComputeContextById(
contextId: string,
accessToken?: string
): Promise<ContextAllAttributes> {
const headers: any = {
'Content-Type': 'application/json'
}
if (accessToken) {
headers.Authorization = `Bearer ${accessToken}`
}
const { result: context } = await this.request<ContextAllAttributes>(
`${this.serverUrl}/compute/contexts/${contextId}`,
{ headers }
).catch((err) => {
throw err
})
return context
}
/**
* Moves a Viya folder to a new location. The folder may be renamed at the same time.
* @param sourceFolder - the full path (eg `/Public/example/myFolder`) or URI of the source folder to be moved. Providing URI instead of path will save one extra request.
@@ -1458,6 +1483,16 @@ export class SASViyaApiClient {
`${this.serverUrl}${url}`,
requestInfo
).catch((err) => {
if (err.code && err.code === 'ENOTFOUND') {
const notFoundError = {
body: JSON.stringify({
message: `Folder '${sourceFolder.split('/').pop()}' was not found.`
})
}
throw notFoundError
}
throw err
})

View File

@@ -44,7 +44,7 @@ const defaultConfig: SASjsConfig = {
pathSASViya: '/SASJobExecution',
appLoc: '/Public/seedapp',
serverType: ServerType.SASViya,
debug: true,
debug: false,
contextName: 'SAS Job Execution compute context',
useComputeApi: false
}
@@ -113,16 +113,16 @@ export default class SASjs {
* @param launchContextName - the name of the launcher context used by the compute service.
* @param sharedAccountId - the ID of the account to run the servers for this context as.
* @param autoExecLines - the lines of code to execute during session initialization.
* @param authorizedUsers - an optional list of authorized user IDs.
* @param accessToken - an access token for an authorized user.
* @param authorizedUsers - an optional list of authorized user IDs.
*/
public async createContext(
contextName: string,
launchContextName: string,
sharedAccountId: string,
autoExecLines: string[],
authorizedUsers: string[],
accessToken: string
accessToken: string,
authorizedUsers?: string[]
) {
this.isMethodSupported('createContext', ServerType.SASViya)
@@ -131,8 +131,8 @@ export default class SASjs {
launchContextName,
sharedAccountId,
autoExecLines,
authorizedUsers,
accessToken
accessToken,
authorizedUsers
)
}
@@ -167,6 +167,38 @@ export default class SASjs {
return await this.sasViyaApiClient!.deleteContext(contextName, accessToken)
}
/**
* Returns a JSON representation of a compute context.
* @example: { "createdBy": "admin", "links": [...], "id": "ID", "version": 2, "name": "context1" }
* @param contextName - the name of the context to return.
* @param accessToken - an access token for an authorized user.
*/
public async getComputeContextByName(
contextName: string,
accessToken?: string
) {
this.isMethodSupported('getComputeContextByName', ServerType.SASViya)
return await this.sasViyaApiClient!.getComputeContextByName(
contextName,
accessToken
)
}
/**
* Returns a JSON representation of a compute context.
* @param contextId - an id of the context to return.
* @param accessToken - an access token for an authorized user.
*/
public async getComputeContextById(contextId: string, accessToken?: string) {
this.isMethodSupported('getComputeContextById', ServerType.SASViya)
return await this.sasViyaApiClient!.getComputeContextById(
contextId,
accessToken
)
}
public async createSession(contextName: string, accessToken: string) {
this.isMethodSupported('createSession', ServerType.SASViya)
@@ -177,9 +209,7 @@ export default class SASjs {
fileName: string,
linesOfCode: string[],
contextName: string,
accessToken?: string,
sessionId = '',
silent = false
accessToken?: string
) {
this.isMethodSupported('executeScriptSASViya', ServerType.SASViya)
@@ -188,9 +218,7 @@ export default class SASjs {
linesOfCode,
contextName,
accessToken,
silent,
null,
this.sasjsConfig.debug
null
)
}
@@ -211,8 +239,6 @@ export default class SASjs {
sasApiClient?: SASViyaApiClient,
isForced?: boolean
) {
this.isMethodSupported('createFolder', ServerType.SASViya)
if (sasApiClient)
return await sasApiClient.createFolder(
folderName,
@@ -229,6 +255,40 @@ export default class SASjs {
)
}
/**
* For performance (and in case of accidental error) the `deleteFolder` function does not actually delete the folder (and all its content and subfolder content). Instead the folder is simply moved to the recycle bin. Deletion time will be added to the folder name.
* @param folderPath - the full path (eg `/Public/example/deleteThis`) of the folder to be deleted.
* @param accessToken - an access token for authorizing the request.
*/
public async deleteFolder(folderPath: string, accessToken: string) {
this.isMethodSupported('deleteFolder', ServerType.SASViya)
return await this.sasViyaApiClient?.deleteFolder(folderPath, accessToken)
}
/**
* Moves folder to a new location. The folder may be renamed at the same time.
* @param sourceFolder - the full path (eg `/Public/example/myFolder`) or URI of the source folder to be moved. Providing URI instead of path will save one extra request.
* @param targetParentFolder - the full path or URI of the _parent_ folder to which the `sourceFolder` will be moved (eg `/Public/newDestination`). To move a folder, a user has to have write permissions in targetParentFolder. Providing URI instead of path will save one extra request.
* @param targetFolderName - the name of the "moved" folder. If left blank, the original folder name will be used (eg `myFolder` in `/Public/newDestination/myFolder` for the example above). Optional field.
* @param accessToken - an access token for authorizing the request.
*/
public async moveFolder(
sourceFolder: string,
targetParentFolder: string,
targetFolderName: string,
accessToken: string
) {
this.isMethodSupported('moveFolder', ServerType.SASViya)
return await this.sasViyaApiClient?.moveFolder(
sourceFolder,
targetParentFolder,
targetFolderName,
accessToken
)
}
public async createJobDefinition(
jobName: string,
code: string,
@@ -346,6 +406,9 @@ export default class SASjs {
*/
public setDebugState(value: boolean) {
this.sasjsConfig.debug = value
if (this.sasViyaApiClient) {
this.sasViyaApiClient.debug = value
}
}
/**
@@ -571,6 +634,7 @@ export default class SASjs {
this.sasjsConfig.contextName,
this.setCsrfTokenApi
)
sasApiClient.debug = this.sasjsConfig.debug
} else if (this.sasjsConfig.serverType === ServerType.SAS9) {
sasApiClient = new SAS9ApiClient(serverUrl)
}
@@ -606,6 +670,50 @@ export default class SASjs {
)
}
/**
* Kicks off execution of the given job via the compute API.
* @returns an object representing the compute session created for the given job.
* @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 data - a JSON object containing one or more tables to be sent to
* SAS. Can be `null` if no inputs required.
* @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 accessToken - a valid access token that is authorised to execute compute jobs.
* The access token is not required when the user is authenticated via the browser.
* @param waitForResult - a boolean that indicates whether the function needs to wait for execution to complete.
*/
public async startComputeJob(
sasJob: string,
data: any,
config: any = {},
accessToken?: string,
waitForResult?: boolean
) {
config = {
...this.sasjsConfig,
...config
}
this.isMethodSupported('startComputeJob', ServerType.SASViya)
if (!config.contextName) {
throw new Error(
'Context name is undefined. Please set a `contextName` in your SASjs or override config.'
)
}
return this.sasViyaApiClient?.executeComputeJob(
sasJob,
config.contextName,
data,
accessToken,
!!waitForResult,
false
)
}
private async executeJobViaComputeApi(
sasJob: string,
data: any,
@@ -625,13 +733,16 @@ export default class SASjs {
sasjsWaitingRequest.requestPromise.promise = new Promise(
async (resolve, reject) => {
const waitForResult = true
const expectWebout = true
this.sasViyaApiClient
?.executeComputeJob(
sasJob,
config.contextName,
config.debug,
data,
accessToken
accessToken,
waitForResult,
expectWebout
)
.then((response) => {
if (!config.debug) {
@@ -1254,11 +1365,15 @@ export default class SASjs {
this.sasjsConfig.serverUrl === undefined ||
this.sasjsConfig.serverUrl === ''
) {
let url = `${location.protocol}//${location.hostname}`
if (location.port) {
url = `${url}:${location.port}`
if (typeof location !== 'undefined') {
let url = `${location.protocol}//${location.hostname}`
if (location.port) url = `${url}:${location.port}`
this.sasjsConfig.serverUrl = url
} else {
this.sasjsConfig.serverUrl = ''
}
this.sasjsConfig.serverUrl = url
}
if (this.sasjsConfig.serverUrl.slice(-1) === '/') {
@@ -1288,6 +1403,8 @@ export default class SASjs {
this.sasjsConfig.contextName,
this.setCsrfTokenApi
)
this.sasViyaApiClient.debug = this.sasjsConfig.debug
}
if (this.sasjsConfig.serverType === ServerType.SAS9) {
if (this.sas9ApiClient)

View File

@@ -2,6 +2,12 @@ import { Session, Context, CsrfToken } from './types'
import { asyncForEach, makeRequest, isUrl } from './utils'
const MAX_SESSION_COUNT = 1
const RETRY_LIMIT: number = 3
let RETRY_COUNT: number = 0
const INTERNAL_SAS_ERROR = {
status: 304,
message: 'Not Modified'
}
export class SessionManager {
constructor(
@@ -15,22 +21,34 @@ export class SessionManager {
private sessions: Session[] = []
private currentContext: Context | null = null
private csrfToken: CsrfToken | null = null
private _debug: boolean = false
public get debug() {
return this._debug
}
public set debug(value: boolean) {
this._debug = value
}
async getSession(accessToken?: string) {
await this.createSessions(accessToken)
this.createAndWaitForSession(accessToken)
await this.createAndWaitForSession(accessToken)
const session = this.sessions.pop()
const secondsSinceSessionCreation =
(new Date().getTime() - new Date(session!.creationTimeStamp).getTime()) /
1000
if (
!session!.attributes ||
secondsSinceSessionCreation >= session!.attributes.sessionInactiveTimeout
) {
await this.createSessions(accessToken)
const freshSession = this.sessions.pop()
return freshSession
}
return session
}
@@ -39,22 +57,37 @@ export class SessionManager {
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)
})
)
.then(() => {
this.sessions = this.sessions.filter((s) => s.id !== id)
})
.catch((err) => {
throw err
})
}
private async createSessions(accessToken?: string) {
if (!this.sessions.length) {
if (!this.currentContext) {
await this.setCurrentContext(accessToken)
await this.setCurrentContext(accessToken).catch((err) => {
throw err
})
}
await asyncForEach(new Array(MAX_SESSION_COUNT), async () => {
const createdSession = await this.createAndWaitForSession(accessToken)
const createdSession = await this.createAndWaitForSession(
accessToken
).catch((err) => {
throw err
})
this.sessions.push(createdSession)
}).catch((err) => {
throw err
})
}
}
@@ -64,13 +97,18 @@ export class SessionManager {
method: 'POST',
headers: this.getHeaders(accessToken)
}
const { result: createdSession, etag } = await this.request<Session>(
`${this.serverUrl}/compute/contexts/${this.currentContext!.id}/sessions`,
createSessionRequest
)
).catch((err) => {
throw err
})
await this.waitForSession(createdSession, etag, accessToken)
this.sessions.push(createdSession)
return createdSession
}
@@ -80,6 +118,8 @@ export class SessionManager {
items: Context[]
}>(`${this.serverUrl}/compute/contexts?limit=10000`, {
headers: this.getHeaders(accessToken)
}).catch((err) => {
throw err
})
const contextsList =
@@ -98,6 +138,8 @@ export class SessionManager {
}
this.currentContext = currentContext
Promise.resolve()
}
}
@@ -105,6 +147,7 @@ export class SessionManager {
const headers: any = {
'Content-Type': 'application/json'
}
if (accessToken) {
headers.Authorization = `Bearer ${accessToken}`
}
@@ -115,8 +158,7 @@ export class SessionManager {
private async waitForSession(
session: Session,
etag: string | null,
accessToken?: string,
silent = false
accessToken?: string
) {
let sessionState = session.state
const headers: any = {
@@ -124,24 +166,41 @@ export class SessionManager {
'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) {
if (this.debug) {
console.log('Polling session status... \n') // ?
}
const { result: state } = await this.request<string>(
const { result: state } = await this.requestSessionStatus<string>(
`${this.serverUrl}${stateLink.href}?wait=30`,
{
headers
},
'text'
)
).catch((err) => {
throw err
})
sessionState = state.trim()
if (!silent) {
if (this.debug) {
console.log(`Current state is '${sessionState}'\n`)
}
// There is an internal error present in SAS Viya 3.5
// Retry to wait for a session status in such case of SAS internal error
if (
sessionState === INTERNAL_SAS_ERROR.message &&
RETRY_COUNT < RETRY_LIMIT
) {
RETRY_COUNT++
resolve(this.waitForSession(session, etag, accessToken))
}
resolve(sessionState)
}
} else {
@@ -161,6 +220,7 @@ export class SessionManager {
[this.csrfToken.headerName]: this.csrfToken.value
}
}
return await makeRequest<T>(
url,
options,
@@ -169,6 +229,36 @@ export class SessionManager {
this.setCsrfToken(token)
},
contentType
)
).catch((err) => {
throw err
})
}
private async requestSessionStatus<T>(
url: string,
options: RequestInit,
contentType: 'text' | 'json' = 'json'
) {
if (this.csrfToken) {
options.headers = {
...options.headers,
[this.csrfToken.headerName]: this.csrfToken.value
}
}
return await makeRequest<T>(
url,
options,
(token) => {
this.csrfToken = token
this.setCsrfToken(token)
},
contentType
).catch((err) => {
if (err.status === INTERNAL_SAS_ERROR.status)
return { result: INTERNAL_SAS_ERROR.message }
throw err
})
}
}

View File

@@ -14,4 +14,26 @@ export interface EditContextInput {
attributes?: any
authorizedUsers?: string[]
authorizeAllAuthenticatedUsers?: boolean
id?: string
}
export interface ContextAllAttributes {
attributes: {
reuseServerProcesses: boolean
runServerAs: string
}
modifiedTimeStamp: string
createdBy: string
creationTimeStamp: string
launchType: string
environment: {
autoExecLines: [string]
}
launchContext: {
contextName: string
}
modifiedBy: string
id: string
version: number
name: string
}

View File

@@ -2,7 +2,7 @@ import { CsrfToken } from '../types'
import { needsRetry } from './needsRetry'
let retryCount: number = 0
let retryLimit: number = 5
const retryLimit: number = 5
export async function makeRequest<T>(
url: string,
@@ -18,57 +18,118 @@ export async function makeRequest<T>(
: (res: Response) => res.text()
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')
const result = await fetch(url, request)
.then(async (response) => {
if (response.redirected && response.url.includes('SASLogon/login')) {
return Promise.reject({ status: 401 })
}
if (tokenHeader) {
const token = response.headers.get(tokenHeader)
callback({
headerName: tokenHeader,
value: token || ''
if (!response.ok) {
if (response.status === 403) {
const tokenHeader = response.headers.get('X-CSRF-HEADER')
if (tokenHeader) {
const token = response.headers.get(tokenHeader)
callback({
headerName: tokenHeader,
value: token || ''
})
retryRequest = {
...request,
headers: { ...request.headers, [tokenHeader]: token }
}
return await fetch(url, retryRequest).then((res) => {
etag = res.headers.get('ETag')
return responseTransform(res)
})
} else {
let body: any = await response.text().catch((err) => {
throw err
})
try {
body = JSON.parse(body)
body.message = `Forbidden. Check your permissions and user groups, and also the scopes granted when registering your CLIENT_ID. ${
body.message || ''
}`
body = JSON.stringify(body)
} catch (_) {}
return Promise.reject({ status: response.status, body })
}
} else {
let body: any = await response.text().catch((err) => {
throw err
})
retryRequest = {
...request,
headers: { ...request.headers, [tokenHeader]: token }
if (needsRetry(body)) {
if (retryCount < retryLimit) {
retryCount++
let retryResponse = await makeRequest(
url,
retryRequest || request,
callback,
contentType
).catch((err) => {
throw err
})
retryCount = 0
etag = retryResponse.etag
return retryResponse.result
} else {
retryCount = 0
throw new Error('Request retry limit exceeded')
}
}
return fetch(url, retryRequest).then((res) => {
etag = res.headers.get('ETag')
return responseTransform(res)
})
} else {
let body: any = await response.text()
if (response.status === 401) {
try {
body = JSON.parse(body)
try {
body = JSON.parse(body)
body.message = `Unauthorized request. Check your credentials(client, secret, access token). ${
body.message || ''
}`
body.message = `Forbidden. Check your permissions and user groups. ${
body.message || ''
}`
body = JSON.stringify(body)
} catch (_) {}
body = JSON.stringify(body)
} catch (_) {}
}
return Promise.reject({ status: response.status, body })
}
} else {
let body: any = await response.text()
if (response.status === 204) {
return Promise.resolve()
}
const responseTransformed = await responseTransform(response).catch(
(err) => {
throw err
}
)
let responseText = ''
if (needsRetry(body)) {
if (typeof responseTransformed === 'string') {
responseText = responseTransformed
} else {
responseText = JSON.stringify(responseTransformed)
}
if (needsRetry(responseText)) {
if (retryCount < retryLimit) {
retryCount++
let retryResponse = await makeRequest(
const retryResponse = await makeRequest(
url,
retryRequest || request,
callback,
contentType
)
).catch((err) => {
throw err
})
retryCount = 0
etag = retryResponse.etag
@@ -80,57 +141,14 @@ export async function makeRequest<T>(
}
}
if (response.status === 401) {
try {
body = JSON.parse(body)
etag = response.headers.get('ETag')
body.message = `Unauthorized request. Check your credentials(client, secret, access token). ${
body.message || ''
}`
body = JSON.stringify(body)
} catch (_) {}
}
return Promise.reject({ status: response.status, body })
return responseTransformed
}
} else {
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
}
})
})
.catch((err) => {
throw err
})
return { result, etag }
}