Compare commits
122 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
da7579a2bb | ||
|
|
657e415c0c | ||
|
|
8fa908a201 | ||
|
|
a29b7f3b92 | ||
|
|
8360519408 | ||
|
|
a71d422528 | ||
|
|
7d37e4d79d | ||
|
|
7ae21c160a | ||
|
|
7cbc2bda08 | ||
|
|
5bc2afba8c | ||
| dd3a7fe393 | |||
|
|
ba1b1e6e80 | ||
|
|
3daa85a74a | ||
|
|
d1139857a4 | ||
|
|
cb106c76cb | ||
|
|
ab8da28de1 | ||
| a729d67d3e | |||
| 548a44d665 | |||
|
|
afda43fc7f | ||
| 5291e7f01c | |||
| 39abdad518 | |||
|
|
6aa12ee950 | ||
|
|
b5b5093295 | ||
|
|
114ca21c17 | ||
|
|
88a1f3eac6 | ||
|
|
b00db86811 | ||
|
|
96caadc6b1 | ||
|
|
074a21c4bb | ||
|
|
588a4169bb | ||
|
|
3d4c01622e | ||
|
|
6aee95b21d | ||
| 3d281abbf8 | |||
| 99d783e174 | |||
|
|
33bc8f92cb | ||
|
|
c1704fff78 | ||
| 17a3d1b8a9 | |||
|
|
01af5eb634 | ||
| 0c3797e2de | |||
| c33c509207 | |||
| af351d7375 | |||
| 2b53406cac | |||
| 99cfb8b2af | |||
|
|
891cc13362 | ||
|
|
22fa185715 | ||
|
|
dad99557a7 | ||
| c7cc2e5fa4 | |||
|
|
2bd7544051 | ||
| 1fb972d88a | |||
| 64f8f8c893 | |||
|
|
ddb4a51c55 | ||
| 921d6ef364 | |||
|
|
105675d46a | ||
|
|
e4addba762 | ||
| 8203e918fd | |||
|
|
2210e43880 | ||
|
|
b04df0bc6d | ||
| 98e851b4d8 | |||
| 84306bea3d | |||
| 89d32262f8 | |||
| 257010f57d | |||
| eb9991015b | |||
|
|
9d17e87a09 | ||
| 55f309e998 | |||
| 3d9b40398c | |||
|
|
e0badae973 | ||
|
|
524c561390 | ||
| e7ceac1b78 | |||
|
|
72ddd424a5 | ||
| 85f771d1ed | |||
|
|
1a781c3a56 | ||
|
|
296d4efdfb | ||
| 8df09d01de | |||
|
|
2d4a9d6dee | ||
|
|
38c30f6342 | ||
|
|
dd72304bc7 | ||
|
|
296a543b2d | ||
| 70b31dcb8f | |||
| b0c2a81989 | |||
| 53e167b17d | |||
|
|
5159318d0d | ||
|
|
6842ee13e4 | ||
|
|
2ce0395a2e | ||
|
|
63440ddfd2 | ||
|
|
eb6729a9c7 | ||
|
|
9cd9dc83f3 | ||
|
|
7608887a0e | ||
|
|
b453717fbc | ||
|
|
ecc896e52a | ||
|
|
e881e21f80 | ||
|
|
44d6cfd544 | ||
| b0c9ca3f9f | |||
|
|
e6f7781648 | ||
| b07770eb7b | |||
|
|
c498783d76 | ||
| 7b3b1d7d7d | |||
|
|
60d3ff2d39 | ||
|
|
5217198ef6 | ||
|
|
3a16c83697 | ||
|
|
e65b19b85e | ||
|
|
a1c09ec802 | ||
| 2cbba38af5 | |||
| 61fdf5573b | |||
| 5259cf3942 | |||
| 5f17f77018 | |||
| 84b46d02c9 | |||
| 4ec57d93dd | |||
| 8e73f543c3 | |||
| a45edbc066 | |||
| 9258817887 | |||
| 1f35e01f22 | |||
| 59af907202 | |||
| cd00c483c5 | |||
| a01eb948b5 | |||
|
|
d746d21dee | ||
| d7330617df | |||
| b72cee099c | |||
| 9871ce4fb3 | |||
|
|
16185eba4d | ||
|
|
eae14df683 | ||
|
|
d989f74112 | ||
|
|
64d72ea0c2 | ||
|
|
325202c8e1 |
@@ -96,6 +96,26 @@
|
||||
"test",
|
||||
"review"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "rudvfaden",
|
||||
"name": "Rud Faden",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/2445577?v=4",
|
||||
"profile": "http://rudvfaden.github.io/",
|
||||
"contributions": [
|
||||
"userTesting",
|
||||
"doc"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "saramartinelli1992",
|
||||
"name": "Sara",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/100193908?v=4",
|
||||
"profile": "https://github.com/saramartinelli1992",
|
||||
"contributions": [
|
||||
"userTesting",
|
||||
"platform"
|
||||
]
|
||||
}
|
||||
],
|
||||
"contributorsPerLine": 7,
|
||||
|
||||
12
.github/issue_template.md
vendored
Normal file
@@ -0,0 +1,12 @@
|
||||
## Expected behaviour
|
||||
*Describe what should be happening*
|
||||
|
||||
## Current behaviour
|
||||
*Describe what is actually happening*
|
||||
|
||||
## Environment info
|
||||
**Client tech stack**: *Angular, React, Vue, VanillaJS, NodeJS etc.*
|
||||
**Server type**: SASJS|SASVIYA|SAS9
|
||||
**Login mechanism**: Default|Redirected
|
||||
**Debug**: true|false
|
||||
**Use Compute Api (relevant only on VIYA)**: true|false
|
||||
30
.github/vpn/config.ovpn
vendored
@@ -1,30 +0,0 @@
|
||||
cipher AES-256-CBC
|
||||
setenv FORWARD_COMPATIBLE 1
|
||||
client
|
||||
server-poll-timeout 4
|
||||
nobind
|
||||
remote vpn.analytium.co.uk 1194 udp
|
||||
remote vpn.analytium.co.uk 1194 udp
|
||||
remote vpn.analytium.co.uk 443 tcp
|
||||
remote vpn.analytium.co.uk 1194 udp
|
||||
remote vpn.analytium.co.uk 1194 udp
|
||||
remote vpn.analytium.co.uk 1194 udp
|
||||
remote vpn.analytium.co.uk 1194 udp
|
||||
remote vpn.analytium.co.uk 1194 udp
|
||||
dev tun
|
||||
dev-type tun
|
||||
ns-cert-type server
|
||||
setenv opt tls-version-min 1.0 or-highest
|
||||
reneg-sec 604800
|
||||
sndbuf 0
|
||||
rcvbuf 0
|
||||
# NOTE: LZO commands are pushed by the Access Server at connect time.
|
||||
# NOTE: The below line doesn't disable LZO.
|
||||
comp-lzo no
|
||||
verb 3
|
||||
setenv PUSH_PEER_INFO
|
||||
|
||||
ca ca.crt
|
||||
cert user.crt
|
||||
key user.key
|
||||
tls-auth tls.key 1
|
||||
25
.github/workflows/build.yml
vendored
@@ -45,31 +45,6 @@ jobs:
|
||||
key: ${{ secrets.DCGITLAB_KEY }}
|
||||
known_hosts: 'placeholder'
|
||||
|
||||
- name: Write VPN Files
|
||||
run: |
|
||||
echo "$CA_CRT" > .github/vpn/ca.crt
|
||||
echo "$USER_CRT" > .github/vpn/user.crt
|
||||
echo "$USER_KEY" > .github/vpn/user.key
|
||||
echo "$TLS_KEY" > .github/vpn/tls.key
|
||||
shell: bash
|
||||
env:
|
||||
CA_CRT: ${{ secrets.CA_CRT}}
|
||||
USER_CRT: ${{ secrets.USER_CRT }}
|
||||
USER_KEY: ${{ secrets.USER_KEY }}
|
||||
TLS_KEY: ${{ secrets.TLS_KEY }}
|
||||
|
||||
- name: Install Open VPN
|
||||
run: |
|
||||
sudo apt install apt-transport-https
|
||||
sudo wget https://swupdate.openvpn.net/repos/openvpn-repo-pkg-key.pub
|
||||
sudo apt-key add openvpn-repo-pkg-key.pub
|
||||
sudo wget -O /etc/apt/sources.list.d/openvpn3.list https://swupdate.openvpn.net/community/openvpn3/repos/openvpn3-focal.list
|
||||
sudo apt update
|
||||
sudo apt install openvpn3=16~beta+focal
|
||||
|
||||
- name: Start Open VPN 3
|
||||
run: openvpn3 session-start --config .github/vpn/config.ovpn
|
||||
|
||||
- name: Deploy sasjs-tests
|
||||
run: |
|
||||
npm install -g replace-in-files-cli
|
||||
|
||||
44
.github/workflows/generateDocs.yml
vendored
Normal file
@@ -0,0 +1,44 @@
|
||||
name: Generate docs and Push to docs Branch
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
|
||||
jobs:
|
||||
generate_and_push_docs:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
node-version: [lts/fermium]
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
cache: npm
|
||||
|
||||
- name: Install Dependencies
|
||||
run: npm ci
|
||||
|
||||
- name: Ensure docs folder exists
|
||||
run: |
|
||||
rm -rf docs || true # avoid error if docs folder does not exist
|
||||
mkdir docs
|
||||
|
||||
- name: Generate Docs
|
||||
run: npm run typedoc
|
||||
|
||||
- name: Push generated docs
|
||||
uses: peaceiris/actions-gh-pages@v3
|
||||
with:
|
||||
github_token: ${{ secrets.GH_TOKEN }}
|
||||
publish_branch: gh-pages
|
||||
publish_dir: ./docs
|
||||
cname: adapter.sasjs.io
|
||||
|
||||
7
.github/workflows/npmpublish.yml
vendored
@@ -34,9 +34,10 @@ jobs:
|
||||
run: npm run build
|
||||
|
||||
- name: Semantic Release
|
||||
uses: cycjimmy/semantic-release-action@v2
|
||||
uses: cycjimmy/semantic-release-action@v3
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GH_TOKEN }}
|
||||
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
- name: Send Slack message
|
||||
run: curl -X POST --data-urlencode "payload={\"channel\":\"#sasjs\", \"username\":\"GitHub CI\", \"text\":\"New version of @sasjs/adapter has been released! \n Please deploy and run `dctests` with new adapter to make sure everything is still in place.\", \"icon_emoji\":\":rocket:\"}" ${{ secrets.SLACK_WEBHOOK }}
|
||||
|
||||
- name: Send Matrix message
|
||||
run: curl -XPOST -d "{\"msgtype\":\"m.text\", \"body\":\"New version of @sasjs/adapter has been released! \n Please deploy and run 'dctests' with new adapter to make sure everything is still in place.\"}" https://matrix.4gl.io/_matrix/client/r0/rooms/!jRebyiGmHZlpfDwYXN:4gl.io/send/m.room.message?access_token=${{ secrets.MATRIX_TOKEN }}
|
||||
|
||||
4
.gitignore
vendored
@@ -1,8 +1,10 @@
|
||||
node_modules
|
||||
build
|
||||
|
||||
docs
|
||||
|
||||
.env
|
||||
|
||||
/coverage
|
||||
|
||||
.DS_Store
|
||||
.DS_Store
|
||||
46
README.md
@@ -20,7 +20,7 @@ SASjs is a open-source framework for building Web Apps on SAS® platforms. You c
|
||||
|
||||
1 - `npm install @sasjs/adapter` - for use in a nodeJS project (recommended)
|
||||
|
||||
2 - [Download](https://cdn.jsdelivr.net/npm/@sasjs/adapter@3/index.min.js) and use a copy of the latest JS file
|
||||
2 - [Download](https://cdn.jsdelivr.net/npm/@sasjs/adapter@4/index.min.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/adapter?tab=collection) and select "SRI" to get the script tag with the integrity hash.
|
||||
|
||||
@@ -125,9 +125,9 @@ sasJs.request("/path/to/my/service", dataObject)
|
||||
})
|
||||
```
|
||||
|
||||
We supply the path to the SAS service, and a data object.
|
||||
We supply the path to the SAS service, and a data object.
|
||||
|
||||
If the path starts with a `/` then it should be a full path to the service. If there is no leading `/` then it is relative to the `appLoc`.
|
||||
If the path starts with a `/` then it should be a full path to the service. If there is no leading `/` then it is relative to the `appLoc`.
|
||||
|
||||
The data object can be null (for services with no input), or can contain one or more "tables" in the following format:
|
||||
|
||||
@@ -153,13 +153,21 @@ The response object will contain returned tables and columns. Table names are a
|
||||
|
||||
The adapter will also cache the logs (if debug enabled) and even the work tables. For performance, it is best to keep debug mode off.
|
||||
|
||||
### Session Manager
|
||||
|
||||
To execute a script on Viya a session has to be created first which is time-consuming (~15sec). That is why a Session Manager has been created which is implementing the following logic:
|
||||
|
||||
1. When the first session is requested, we also create one more session (hot session) for future requests. Please notice two pending POST requests to create a session within the same context: 
|
||||
2. When a subsequent request for a session is received and there is a hot session available (not expired), this session is returned and an asynchronous request to create another hot session is sent. Please notice that there is a pending POST request to create a new session while a job has been already finished execution (POST request with status 201): 
|
||||
3. When a subsequent request for a session is received and there is no available hot session, 2 requests are sent asynchronously to create a session. The first created session will be returned and another session will be reserved for future requests.
|
||||
|
||||
### Variable Types
|
||||
|
||||
The SAS type (char/numeric) of the values is determined according to a set of rules:
|
||||
|
||||
* If the values are numeric, the SAS type is numeric
|
||||
* If the values are all string, the SAS type is character
|
||||
* If the values contain a single character (a-Z + underscore + .) AND a numeric, then the SAS type is numeric (with special missing values).
|
||||
* If the values contain a single character (a-Z + underscore + .) AND a numeric, then the SAS type is numeric (with special missing values).
|
||||
* `null` is set to either '.' or '' depending on the assigned or derived type per the above rules. If entire column is `null` then the type will be numeric.
|
||||
|
||||
The following table illustrates the formats applied to columns under various scenarios:
|
||||
@@ -237,8 +245,8 @@ run;
|
||||
%webout(OBJ,a) /* Rows in table `a` are objects (easy to use) */
|
||||
%webout(ARR,b) /* Rows in table `b` are arrays (compact) */
|
||||
%webout(OBJ,c,fmt=N) /* Table `c` is sent unformatted (raw) */
|
||||
%webout(OBJ,c,label=d) /* Rename table as `d` in output JSON */
|
||||
%webout(OBJ,c,label=e, maxobs=10) /* send only 10 rows back */
|
||||
%webout(OBJ,c,dslabel=d) /* Rename table as `d` in output JSON */
|
||||
%webout(OBJ,c,dslabel=e, maxobs=10) /* send only 10 rows back */
|
||||
%webout(CLOSE) /* Close the JSON and add default variables */
|
||||
```
|
||||
|
||||
@@ -332,7 +340,7 @@ If you find this library useful, help us grow our star graph!
|
||||
|
||||
## Contributors ✨
|
||||
<!-- ALL-CONTRIBUTORS-BADGE:START - Do not remove or modify this section -->
|
||||
[](#contributors-)
|
||||
[](#contributors-)
|
||||
<!-- ALL-CONTRIBUTORS-BADGE:END -->
|
||||
|
||||
Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/docs/en/emoji-key)):
|
||||
@@ -341,15 +349,21 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d
|
||||
<!-- prettier-ignore-start -->
|
||||
<!-- markdownlint-disable -->
|
||||
<table>
|
||||
<tr>
|
||||
<td align="center"><a href="https://krishna-acondy.io/"><img src="https://avatars.githubusercontent.com/u/2980428?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Krishna Acondy</b></sub></a><br /><a href="https://github.com/sasjs/adapter/commits?author=krishna-acondy" title="Code">💻</a> <a href="#infra-krishna-acondy" title="Infrastructure (Hosting, Build-Tools, etc)">🚇</a> <a href="#blog-krishna-acondy" title="Blogposts">📝</a> <a href="#content-krishna-acondy" title="Content">🖋</a> <a href="#ideas-krishna-acondy" title="Ideas, Planning, & Feedback">🤔</a> <a href="#video-krishna-acondy" title="Videos">📹</a></td>
|
||||
<td align="center"><a href="https://www.erudicat.com/"><img src="https://avatars.githubusercontent.com/u/25773492?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Yury Shkoda</b></sub></a><br /><a href="https://github.com/sasjs/adapter/commits?author=YuryShkoda" title="Code">💻</a> <a href="#infra-YuryShkoda" title="Infrastructure (Hosting, Build-Tools, etc)">🚇</a> <a href="#ideas-YuryShkoda" title="Ideas, Planning, & Feedback">🤔</a> <a href="https://github.com/sasjs/adapter/commits?author=YuryShkoda" title="Tests">⚠️</a> <a href="#video-YuryShkoda" title="Videos">📹</a></td>
|
||||
<td align="center"><a href="https://github.com/medjedovicm"><img src="https://avatars.githubusercontent.com/u/18329105?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Mihajlo Medjedovic</b></sub></a><br /><a href="https://github.com/sasjs/adapter/commits?author=medjedovicm" title="Code">💻</a> <a href="#infra-medjedovicm" title="Infrastructure (Hosting, Build-Tools, etc)">🚇</a> <a href="https://github.com/sasjs/adapter/commits?author=medjedovicm" title="Tests">⚠️</a> <a href="https://github.com/sasjs/adapter/pulls?q=is%3Apr+reviewed-by%3Amedjedovicm" title="Reviewed Pull Requests">👀</a></td>
|
||||
<td align="center"><a href="https://github.com/allanbowe"><img src="https://avatars.githubusercontent.com/u/4420615?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Allan Bowe</b></sub></a><br /><a href="https://github.com/sasjs/adapter/commits?author=allanbowe" title="Code">💻</a> <a href="https://github.com/sasjs/adapter/pulls?q=is%3Apr+reviewed-by%3Aallanbowe" title="Reviewed Pull Requests">👀</a> <a href="https://github.com/sasjs/adapter/commits?author=allanbowe" title="Tests">⚠️</a> <a href="#mentoring-allanbowe" title="Mentoring">🧑🏫</a> <a href="#maintenance-allanbowe" title="Maintenance">🚧</a></td>
|
||||
<td align="center"><a href="https://github.com/saadjutt01"><img src="https://avatars.githubusercontent.com/u/8914650?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Muhammad Saad </b></sub></a><br /><a href="https://github.com/sasjs/adapter/commits?author=saadjutt01" title="Code">💻</a> <a href="https://github.com/sasjs/adapter/pulls?q=is%3Apr+reviewed-by%3Asaadjutt01" title="Reviewed Pull Requests">👀</a> <a href="https://github.com/sasjs/adapter/commits?author=saadjutt01" title="Tests">⚠️</a> <a href="#mentoring-saadjutt01" title="Mentoring">🧑🏫</a> <a href="#infra-saadjutt01" title="Infrastructure (Hosting, Build-Tools, etc)">🚇</a></td>
|
||||
<td align="center"><a href="https://github.com/sabhas"><img src="https://avatars.githubusercontent.com/u/82647447?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Sabir Hassan</b></sub></a><br /><a href="https://github.com/sasjs/adapter/commits?author=sabhas" title="Code">💻</a> <a href="https://github.com/sasjs/adapter/pulls?q=is%3Apr+reviewed-by%3Asabhas" title="Reviewed Pull Requests">👀</a> <a href="https://github.com/sasjs/adapter/commits?author=sabhas" title="Tests">⚠️</a> <a href="#ideas-sabhas" title="Ideas, Planning, & Feedback">🤔</a></td>
|
||||
<td align="center"><a href="https://github.com/VladislavParhomchik"><img src="https://avatars.githubusercontent.com/u/83717836?v=4?s=100" width="100px;" alt=""/><br /><sub><b>VladislavParhomchik</b></sub></a><br /><a href="https://github.com/sasjs/adapter/commits?author=VladislavParhomchik" title="Tests">⚠️</a> <a href="https://github.com/sasjs/adapter/pulls?q=is%3Apr+reviewed-by%3AVladislavParhomchik" title="Reviewed Pull Requests">👀</a></td>
|
||||
</tr>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://krishna-acondy.io/"><img src="https://avatars.githubusercontent.com/u/2980428?v=4?s=100" width="100px;" alt="Krishna Acondy"/><br /><sub><b>Krishna Acondy</b></sub></a><br /><a href="https://github.com/sasjs/adapter/commits?author=krishna-acondy" title="Code">💻</a> <a href="#infra-krishna-acondy" title="Infrastructure (Hosting, Build-Tools, etc)">🚇</a> <a href="#blog-krishna-acondy" title="Blogposts">📝</a> <a href="#content-krishna-acondy" title="Content">🖋</a> <a href="#ideas-krishna-acondy" title="Ideas, Planning, & Feedback">🤔</a> <a href="#video-krishna-acondy" title="Videos">📹</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://www.erudicat.com/"><img src="https://avatars.githubusercontent.com/u/25773492?v=4?s=100" width="100px;" alt="Yury Shkoda"/><br /><sub><b>Yury Shkoda</b></sub></a><br /><a href="https://github.com/sasjs/adapter/commits?author=YuryShkoda" title="Code">💻</a> <a href="#infra-YuryShkoda" title="Infrastructure (Hosting, Build-Tools, etc)">🚇</a> <a href="#ideas-YuryShkoda" title="Ideas, Planning, & Feedback">🤔</a> <a href="https://github.com/sasjs/adapter/commits?author=YuryShkoda" title="Tests">⚠️</a> <a href="#video-YuryShkoda" title="Videos">📹</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/medjedovicm"><img src="https://avatars.githubusercontent.com/u/18329105?v=4?s=100" width="100px;" alt="Mihajlo Medjedovic"/><br /><sub><b>Mihajlo Medjedovic</b></sub></a><br /><a href="https://github.com/sasjs/adapter/commits?author=medjedovicm" title="Code">💻</a> <a href="#infra-medjedovicm" title="Infrastructure (Hosting, Build-Tools, etc)">🚇</a> <a href="https://github.com/sasjs/adapter/commits?author=medjedovicm" title="Tests">⚠️</a> <a href="https://github.com/sasjs/adapter/pulls?q=is%3Apr+reviewed-by%3Amedjedovicm" title="Reviewed Pull Requests">👀</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/allanbowe"><img src="https://avatars.githubusercontent.com/u/4420615?v=4?s=100" width="100px;" alt="Allan Bowe"/><br /><sub><b>Allan Bowe</b></sub></a><br /><a href="https://github.com/sasjs/adapter/commits?author=allanbowe" title="Code">💻</a> <a href="https://github.com/sasjs/adapter/pulls?q=is%3Apr+reviewed-by%3Aallanbowe" title="Reviewed Pull Requests">👀</a> <a href="https://github.com/sasjs/adapter/commits?author=allanbowe" title="Tests">⚠️</a> <a href="#mentoring-allanbowe" title="Mentoring">🧑🏫</a> <a href="#maintenance-allanbowe" title="Maintenance">🚧</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/saadjutt01"><img src="https://avatars.githubusercontent.com/u/8914650?v=4?s=100" width="100px;" alt="Muhammad Saad "/><br /><sub><b>Muhammad Saad </b></sub></a><br /><a href="https://github.com/sasjs/adapter/commits?author=saadjutt01" title="Code">💻</a> <a href="https://github.com/sasjs/adapter/pulls?q=is%3Apr+reviewed-by%3Asaadjutt01" title="Reviewed Pull Requests">👀</a> <a href="https://github.com/sasjs/adapter/commits?author=saadjutt01" title="Tests">⚠️</a> <a href="#mentoring-saadjutt01" title="Mentoring">🧑🏫</a> <a href="#infra-saadjutt01" title="Infrastructure (Hosting, Build-Tools, etc)">🚇</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/sabhas"><img src="https://avatars.githubusercontent.com/u/82647447?v=4?s=100" width="100px;" alt="Sabir Hassan"/><br /><sub><b>Sabir Hassan</b></sub></a><br /><a href="https://github.com/sasjs/adapter/commits?author=sabhas" title="Code">💻</a> <a href="https://github.com/sasjs/adapter/pulls?q=is%3Apr+reviewed-by%3Asabhas" title="Reviewed Pull Requests">👀</a> <a href="https://github.com/sasjs/adapter/commits?author=sabhas" title="Tests">⚠️</a> <a href="#ideas-sabhas" title="Ideas, Planning, & Feedback">🤔</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/VladislavParhomchik"><img src="https://avatars.githubusercontent.com/u/83717836?v=4?s=100" width="100px;" alt="VladislavParhomchik"/><br /><sub><b>VladislavParhomchik</b></sub></a><br /><a href="https://github.com/sasjs/adapter/commits?author=VladislavParhomchik" title="Tests">⚠️</a> <a href="https://github.com/sasjs/adapter/pulls?q=is%3Apr+reviewed-by%3AVladislavParhomchik" title="Reviewed Pull Requests">👀</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" valign="top" width="14.28%"><a href="http://rudvfaden.github.io/"><img src="https://avatars.githubusercontent.com/u/2445577?v=4?s=100" width="100px;" alt="Rud Faden"/><br /><sub><b>Rud Faden</b></sub></a><br /><a href="#userTesting-rudvfaden" title="User Testing">📓</a> <a href="https://github.com/sasjs/adapter/commits?author=rudvfaden" title="Documentation">📖</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/saramartinelli1992"><img src="https://avatars.githubusercontent.com/u/100193908?v=4?s=100" width="100px;" alt="Sara"/><br /><sub><b>Sara</b></sub></a><br /><a href="#userTesting-saramartinelli1992" title="User Testing">📓</a> <a href="#platform-saramartinelli1992" title="Packaging/porting to new platform">📦</a></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<!-- markdownlint-restore -->
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
TypeDoc added this file to prevent GitHub Pages from using Jekyll. You can turn off this behavior by setting the `githubPages` option to false.
|
||||
@@ -1 +0,0 @@
|
||||
adapter.sasjs.io
|
||||
@@ -1,99 +0,0 @@
|
||||
:root {
|
||||
--light-hl-0: #000000;
|
||||
--dark-hl-0: #D4D4D4;
|
||||
--light-hl-1: #0000FF;
|
||||
--dark-hl-1: #569CD6;
|
||||
--light-hl-2: #001080;
|
||||
--dark-hl-2: #9CDCFE;
|
||||
--light-hl-3: #795E26;
|
||||
--dark-hl-3: #DCDCAA;
|
||||
--light-hl-4: #A31515;
|
||||
--dark-hl-4: #CE9178;
|
||||
--light-hl-5: #AF00DB;
|
||||
--dark-hl-5: #C586C0;
|
||||
--light-hl-6: #0070C1;
|
||||
--dark-hl-6: #4FC1FF;
|
||||
--light-hl-7: #008000;
|
||||
--dark-hl-7: #6A9955;
|
||||
--light-hl-8: #098658;
|
||||
--dark-hl-8: #B5CEA8;
|
||||
--light-hl-9: #000000;
|
||||
--dark-hl-9: #C8C8C8;
|
||||
--light-hl-10: #CD3131;
|
||||
--dark-hl-10: #F44747;
|
||||
--light-code-background: #F5F5F5;
|
||||
--dark-code-background: #1E1E1E;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: light) { :root {
|
||||
--hl-0: var(--light-hl-0);
|
||||
--hl-1: var(--light-hl-1);
|
||||
--hl-2: var(--light-hl-2);
|
||||
--hl-3: var(--light-hl-3);
|
||||
--hl-4: var(--light-hl-4);
|
||||
--hl-5: var(--light-hl-5);
|
||||
--hl-6: var(--light-hl-6);
|
||||
--hl-7: var(--light-hl-7);
|
||||
--hl-8: var(--light-hl-8);
|
||||
--hl-9: var(--light-hl-9);
|
||||
--hl-10: var(--light-hl-10);
|
||||
--code-background: var(--light-code-background);
|
||||
} }
|
||||
|
||||
@media (prefers-color-scheme: dark) { :root {
|
||||
--hl-0: var(--dark-hl-0);
|
||||
--hl-1: var(--dark-hl-1);
|
||||
--hl-2: var(--dark-hl-2);
|
||||
--hl-3: var(--dark-hl-3);
|
||||
--hl-4: var(--dark-hl-4);
|
||||
--hl-5: var(--dark-hl-5);
|
||||
--hl-6: var(--dark-hl-6);
|
||||
--hl-7: var(--dark-hl-7);
|
||||
--hl-8: var(--dark-hl-8);
|
||||
--hl-9: var(--dark-hl-9);
|
||||
--hl-10: var(--dark-hl-10);
|
||||
--code-background: var(--dark-code-background);
|
||||
} }
|
||||
|
||||
body.light {
|
||||
--hl-0: var(--light-hl-0);
|
||||
--hl-1: var(--light-hl-1);
|
||||
--hl-2: var(--light-hl-2);
|
||||
--hl-3: var(--light-hl-3);
|
||||
--hl-4: var(--light-hl-4);
|
||||
--hl-5: var(--light-hl-5);
|
||||
--hl-6: var(--light-hl-6);
|
||||
--hl-7: var(--light-hl-7);
|
||||
--hl-8: var(--light-hl-8);
|
||||
--hl-9: var(--light-hl-9);
|
||||
--hl-10: var(--light-hl-10);
|
||||
--code-background: var(--light-code-background);
|
||||
}
|
||||
|
||||
body.dark {
|
||||
--hl-0: var(--dark-hl-0);
|
||||
--hl-1: var(--dark-hl-1);
|
||||
--hl-2: var(--dark-hl-2);
|
||||
--hl-3: var(--dark-hl-3);
|
||||
--hl-4: var(--dark-hl-4);
|
||||
--hl-5: var(--dark-hl-5);
|
||||
--hl-6: var(--dark-hl-6);
|
||||
--hl-7: var(--dark-hl-7);
|
||||
--hl-8: var(--dark-hl-8);
|
||||
--hl-9: var(--dark-hl-9);
|
||||
--hl-10: var(--dark-hl-10);
|
||||
--code-background: var(--dark-code-background);
|
||||
}
|
||||
|
||||
.hl-0 { color: var(--hl-0); }
|
||||
.hl-1 { color: var(--hl-1); }
|
||||
.hl-2 { color: var(--hl-2); }
|
||||
.hl-3 { color: var(--hl-3); }
|
||||
.hl-4 { color: var(--hl-4); }
|
||||
.hl-5 { color: var(--hl-5); }
|
||||
.hl-6 { color: var(--hl-6); }
|
||||
.hl-7 { color: var(--hl-7); }
|
||||
.hl-8 { color: var(--hl-8); }
|
||||
.hl-9 { color: var(--hl-9); }
|
||||
.hl-10 { color: var(--hl-10); }
|
||||
pre, code { background: var(--code-background); }
|
||||
|
Before Width: | Height: | Size: 9.4 KiB |
|
Before Width: | Height: | Size: 28 KiB |
|
Before Width: | Height: | Size: 480 B |
|
Before Width: | Height: | Size: 855 B |
@@ -1,48 +0,0 @@
|
||||
<!DOCTYPE html><html class="default"><head><meta charSet="utf-8"/><meta http-equiv="x-ua-compatible" content="IE=edge"/><title>SASjsConfig | @sasjs/adapter</title><meta name="description" content="Documentation for @sasjs/adapter"/><meta name="viewport" content="width=device-width, initial-scale=1"/><link rel="stylesheet" href="../assets/style.css"/><link rel="stylesheet" href="../assets/highlight.css"/><script async src="../assets/search.js" id="search-script"></script></head><body><script>document.body.classList.add(localStorage.getItem("tsd-theme") || "os")</script><header><div class="tsd-page-toolbar"><div class="container"><div class="table-wrap"><div class="table-cell" id="tsd-search" data-base=".."><div class="field"><label for="tsd-search-field" class="tsd-widget search no-caption">Search</label><input type="text" id="tsd-search-field"/></div><ul class="results"><li class="state loading">Preparing search index...</li><li class="state failure">The search index is not available</li></ul><a href="../index.html" class="title">@sasjs/adapter</a></div><div class="table-cell" id="tsd-widgets"><div id="tsd-filter"><a href="#" class="tsd-widget options no-caption" data-toggle="options">Options</a><div class="tsd-filter-group"><div class="tsd-select" id="tsd-filter-visibility"><span class="tsd-select-label">All</span><ul class="tsd-select-list"><li data-value="public">Public</li><li data-value="protected">Public/Protected</li><li data-value="private" class="selected">All</li></ul></div> <input type="checkbox" id="tsd-filter-inherited" checked/><label class="tsd-widget" for="tsd-filter-inherited">Inherited</label></div></div><a href="#" class="tsd-widget menu no-caption" data-toggle="menu">Menu</a></div></div></div></div><div class="tsd-page-title"><div class="container"><ul class="tsd-breadcrumb"><li><a href="../modules.html">@sasjs/adapter</a></li><li><a href="../modules/types.html">types</a></li><li><a href="types.SASjsConfig.html">SASjsConfig</a></li></ul><h1>Class SASjsConfig</h1></div></div></header><div class="container container-main"><div class="row"><div class="col-8 col-content"><section class="tsd-panel tsd-comment"><div class="tsd-comment tsd-typography"><div class="lead">
|
||||
<p>Specifies the configuration for the SASjs instance - eg where and how to
|
||||
connect to SAS.</p>
|
||||
</div></div></section><section class="tsd-panel tsd-hierarchy"><h3>Hierarchy</h3><ul class="tsd-hierarchy"><li><span class="target">SASjsConfig</span></li></ul></section><section class="tsd-panel-group tsd-index-group"><h2>Index</h2><section class="tsd-panel tsd-index-panel"><div class="tsd-index-content"><section class="tsd-index-section "><h3>Constructors</h3><ul class="tsd-index-list"><li class="tsd-kind-constructor tsd-parent-kind-class"><a href="types.SASjsConfig.html#constructor" class="tsd-kind-icon">constructor</a></li></ul></section><section class="tsd-index-section "><h3>Properties</h3><ul class="tsd-index-list"><li class="tsd-kind-property tsd-parent-kind-class"><a href="types.SASjsConfig.html#appLoc" class="tsd-kind-icon">app<wbr/>Loc</a></li><li class="tsd-kind-property tsd-parent-kind-class"><a href="types.SASjsConfig.html#contextName" class="tsd-kind-icon">context<wbr/>Name</a></li><li class="tsd-kind-property tsd-parent-kind-class"><a href="types.SASjsConfig.html#debug" class="tsd-kind-icon">debug</a></li><li class="tsd-kind-property tsd-parent-kind-class"><a href="types.SASjsConfig.html#httpsAgentOptions" class="tsd-kind-icon">https<wbr/>Agent<wbr/>Options</a></li><li class="tsd-kind-property tsd-parent-kind-class"><a href="types.SASjsConfig.html#loginMechanism" class="tsd-kind-icon">login<wbr/>Mechanism</a></li><li class="tsd-kind-property tsd-parent-kind-class"><a href="types.SASjsConfig.html#pathSAS9" class="tsd-kind-icon">pathSAS9</a></li><li class="tsd-kind-property tsd-parent-kind-class"><a href="types.SASjsConfig.html#pathSASJS" class="tsd-kind-icon">pathSASJS</a></li><li class="tsd-kind-property tsd-parent-kind-class"><a href="types.SASjsConfig.html#pathSASViya" class="tsd-kind-icon">pathSASViya</a></li><li class="tsd-kind-property tsd-parent-kind-class"><a href="types.SASjsConfig.html#requestHistoryLimit" class="tsd-kind-icon">request<wbr/>History<wbr/>Limit</a></li><li class="tsd-kind-property tsd-parent-kind-class"><a href="types.SASjsConfig.html#serverType" class="tsd-kind-icon">server<wbr/>Type</a></li><li class="tsd-kind-property tsd-parent-kind-class"><a href="types.SASjsConfig.html#serverUrl" class="tsd-kind-icon">server<wbr/>Url</a></li><li class="tsd-kind-property tsd-parent-kind-class"><a href="types.SASjsConfig.html#useComputeApi" class="tsd-kind-icon">use<wbr/>Compute<wbr/>Api</a></li></ul></section></div></section></section><section class="tsd-panel-group tsd-member-group "><h2>Constructors</h2><section class="tsd-panel tsd-member tsd-kind-constructor tsd-parent-kind-class"><a id="constructor" class="tsd-anchor"></a><h3 class="tsd-anchor-link">constructor<a href="#constructor" aria-label="Permalink" class="tsd-anchor-icon"><svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-link" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"></path><path d="M10 14a3.5 3.5 0 0 0 5 0l4 -4a3.5 3.5 0 0 0 -5 -5l-.5 .5"></path><path d="M14 10a3.5 3.5 0 0 0 -5 0l-4 4a3.5 3.5 0 0 0 5 5l.5 -.5"></path></svg></a></h3><ul class="tsd-signatures tsd-kind-constructor tsd-parent-kind-class"><li class="tsd-signature tsd-kind-icon">new SASjs<wbr/>Config<span class="tsd-signature-symbol">(</span><span class="tsd-signature-symbol">)</span><span class="tsd-signature-symbol">: </span><a href="types.SASjsConfig.html" class="tsd-signature-type" data-tsd-kind="Class">SASjsConfig</a></li></ul><ul class="tsd-descriptions"><li class="tsd-description"><h4 class="tsd-returns-title">Returns <a href="types.SASjsConfig.html" class="tsd-signature-type" data-tsd-kind="Class">SASjsConfig</a></h4></li></ul></section></section><section class="tsd-panel-group tsd-member-group "><h2>Properties</h2><section class="tsd-panel tsd-member tsd-kind-property tsd-parent-kind-class"><a id="appLoc" class="tsd-anchor"></a><h3 class="tsd-anchor-link">app<wbr/>Loc<a href="#appLoc" aria-label="Permalink" class="tsd-anchor-icon"><svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-link" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"></path><path d="M10 14a3.5 3.5 0 0 0 5 0l4 -4a3.5 3.5 0 0 0 -5 -5l-.5 .5"></path><path d="M14 10a3.5 3.5 0 0 0 -5 0l-4 4a3.5 3.5 0 0 0 5 5l.5 -.5"></path></svg></a></h3><div class="tsd-signature tsd-kind-icon">app<wbr/>Loc<span class="tsd-signature-symbol">:</span> <span class="tsd-signature-type">string</span><span class="tsd-signature-symbol"> = ''</span></div><aside class="tsd-sources"><ul><li>Defined in <a href="https://github.com/sasjs/adapter/blob/1ee07ee/src/types/SASjsConfig.ts#L39">types/SASjsConfig.ts:39</a></li></ul></aside><div class="tsd-comment tsd-typography"><div class="lead">
|
||||
<p>The appLoc is the parent folder under which the SAS services (STPs or Job
|
||||
Execution Services) are stored. We recommend that each app is stored in
|
||||
a dedicated parent folder (the appLoc) and the services are grouped inside
|
||||
subfolders within the appLoc - allowing functionality to be restricted
|
||||
according to those groups at backend.
|
||||
When using appLoc, the paths provided in the <code>request</code> function should be
|
||||
<em>without</em> a leading slash (/).</p>
|
||||
</div></div></section><section class="tsd-panel tsd-member tsd-kind-property tsd-parent-kind-class"><a id="contextName" class="tsd-anchor"></a><h3 class="tsd-anchor-link">context<wbr/>Name<a href="#contextName" aria-label="Permalink" class="tsd-anchor-icon"><svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-link" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"></path><path d="M10 14a3.5 3.5 0 0 0 5 0l4 -4a3.5 3.5 0 0 0 -5 -5l-.5 .5"></path><path d="M14 10a3.5 3.5 0 0 0 -5 0l-4 4a3.5 3.5 0 0 0 5 5l.5 -.5"></path></svg></a></h3><div class="tsd-signature tsd-kind-icon">context<wbr/>Name<span class="tsd-signature-symbol">:</span> <span class="tsd-signature-type">string</span><span class="tsd-signature-symbol"> = ''</span></div><aside class="tsd-sources"><ul><li>Defined in <a href="https://github.com/sasjs/adapter/blob/1ee07ee/src/types/SASjsConfig.ts#L52">types/SASjsConfig.ts:52</a></li></ul></aside><div class="tsd-comment tsd-typography"><div class="lead">
|
||||
<p>The name of the compute context to use when calling the Viya services directly.
|
||||
Example value: 'SAS Job Execution compute context'</p>
|
||||
</div></div></section><section class="tsd-panel tsd-member tsd-kind-property tsd-parent-kind-class"><a id="debug" class="tsd-anchor"></a><h3 class="tsd-anchor-link">debug<a href="#debug" aria-label="Permalink" class="tsd-anchor-icon"><svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-link" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"></path><path d="M10 14a3.5 3.5 0 0 0 5 0l4 -4a3.5 3.5 0 0 0 -5 -5l-.5 .5"></path><path d="M14 10a3.5 3.5 0 0 0 -5 0l-4 4a3.5 3.5 0 0 0 5 5l.5 -.5"></path></svg></a></h3><div class="tsd-signature tsd-kind-icon">debug<span class="tsd-signature-symbol">:</span> <span class="tsd-signature-type">boolean</span><span class="tsd-signature-symbol"> = true</span></div><aside class="tsd-sources"><ul><li>Defined in <a href="https://github.com/sasjs/adapter/blob/1ee07ee/src/types/SASjsConfig.ts#L47">types/SASjsConfig.ts:47</a></li></ul></aside><div class="tsd-comment tsd-typography"><div class="lead">
|
||||
<p>Set to <code>true</code> to enable additional debugging.</p>
|
||||
</div></div></section><section class="tsd-panel tsd-member tsd-kind-property tsd-parent-kind-class"><a id="httpsAgentOptions" class="tsd-anchor"></a><h3 class="tsd-anchor-link"><span class="tsd-flag ts-flagOptional">Optional</span> https<wbr/>Agent<wbr/>Options<a href="#httpsAgentOptions" aria-label="Permalink" class="tsd-anchor-icon"><svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-link" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"></path><path d="M10 14a3.5 3.5 0 0 0 5 0l4 -4a3.5 3.5 0 0 0 -5 -5l-.5 .5"></path><path d="M14 10a3.5 3.5 0 0 0 -5 0l-4 4a3.5 3.5 0 0 0 5 5l.5 -.5"></path></svg></a></h3><div class="tsd-signature tsd-kind-icon">https<wbr/>Agent<wbr/>Options<span class="tsd-signature-symbol">?:</span> <span class="tsd-signature-type">AgentOptions</span></div><aside class="tsd-sources"><ul><li>Defined in <a href="https://github.com/sasjs/adapter/blob/1ee07ee/src/types/SASjsConfig.ts#L67">types/SASjsConfig.ts:67</a></li></ul></aside><div class="tsd-comment tsd-typography"><div class="lead">
|
||||
<p>Optional setting to configure HTTPS Agent.
|
||||
By providing <code>key</code>, <code>cert</code>, <code>ca</code> to connect with server
|
||||
Other options can be set <code>rejectUnauthorized</code> and <code>requestCert</code></p>
|
||||
</div></div></section><section class="tsd-panel tsd-member tsd-kind-property tsd-parent-kind-class"><a id="loginMechanism" class="tsd-anchor"></a><h3 class="tsd-anchor-link">login<wbr/>Mechanism<a href="#loginMechanism" aria-label="Permalink" class="tsd-anchor-icon"><svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-link" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"></path><path d="M10 14a3.5 3.5 0 0 0 5 0l4 -4a3.5 3.5 0 0 0 -5 -5l-.5 .5"></path><path d="M14 10a3.5 3.5 0 0 0 -5 0l-4 4a3.5 3.5 0 0 0 5 5l.5 -.5"></path></svg></a></h3><div class="tsd-signature tsd-kind-icon">login<wbr/>Mechanism<span class="tsd-signature-symbol">:</span> <a href="../enums/types.LoginMechanism.html" class="tsd-signature-type" data-tsd-kind="Enumeration">LoginMechanism</a><span class="tsd-signature-symbol"> = LoginMechanism.Default</span></div><aside class="tsd-sources"><ul><li>Defined in <a href="https://github.com/sasjs/adapter/blob/1ee07ee/src/types/SASjsConfig.ts#L71">types/SASjsConfig.ts:71</a></li></ul></aside><div class="tsd-comment tsd-typography"><div class="lead">
|
||||
<p>Supported login mechanisms are - Redirected and Default</p>
|
||||
</div></div></section><section class="tsd-panel tsd-member tsd-kind-property tsd-parent-kind-class"><a id="pathSAS9" class="tsd-anchor"></a><h3 class="tsd-anchor-link">pathSAS9<a href="#pathSAS9" aria-label="Permalink" class="tsd-anchor-icon"><svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-link" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"></path><path d="M10 14a3.5 3.5 0 0 0 5 0l4 -4a3.5 3.5 0 0 0 -5 -5l-.5 .5"></path><path d="M14 10a3.5 3.5 0 0 0 -5 0l-4 4a3.5 3.5 0 0 0 5 5l.5 -.5"></path></svg></a></h3><div class="tsd-signature tsd-kind-icon">pathSAS9<span class="tsd-signature-symbol">:</span> <span class="tsd-signature-type">string</span><span class="tsd-signature-symbol"> = ''</span></div><aside class="tsd-sources"><ul><li>Defined in <a href="https://github.com/sasjs/adapter/blob/1ee07ee/src/types/SASjsConfig.ts#L24">types/SASjsConfig.ts:24</a></li></ul></aside><div class="tsd-comment tsd-typography"><div class="lead">
|
||||
<p>The location of the Stored Process Web Application. By default the adapter
|
||||
will use '/SASStoredProcess/do' on SAS 9.</p>
|
||||
</div></div></section><section class="tsd-panel tsd-member tsd-kind-property tsd-parent-kind-class"><a id="pathSASJS" class="tsd-anchor"></a><h3 class="tsd-anchor-link">pathSASJS<a href="#pathSASJS" aria-label="Permalink" class="tsd-anchor-icon"><svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-link" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"></path><path d="M10 14a3.5 3.5 0 0 0 5 0l4 -4a3.5 3.5 0 0 0 -5 -5l-.5 .5"></path><path d="M14 10a3.5 3.5 0 0 0 -5 0l-4 4a3.5 3.5 0 0 0 5 5l.5 -.5"></path></svg></a></h3><div class="tsd-signature tsd-kind-icon">pathSASJS<span class="tsd-signature-symbol">:</span> <span class="tsd-signature-type">string</span><span class="tsd-signature-symbol"> = ''</span></div><aside class="tsd-sources"><ul><li>Defined in <a href="https://github.com/sasjs/adapter/blob/1ee07ee/src/types/SASjsConfig.ts#L19">types/SASjsConfig.ts:19</a></li></ul></aside><div class="tsd-comment tsd-typography"><div class="lead">
|
||||
<p>The location of the STP Process Web Application. By default the adapter
|
||||
will use '/SASjsApi/stp/execute' on SAS JS.</p>
|
||||
</div></div></section><section class="tsd-panel tsd-member tsd-kind-property tsd-parent-kind-class"><a id="pathSASViya" class="tsd-anchor"></a><h3 class="tsd-anchor-link">pathSASViya<a href="#pathSASViya" aria-label="Permalink" class="tsd-anchor-icon"><svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-link" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"></path><path d="M10 14a3.5 3.5 0 0 0 5 0l4 -4a3.5 3.5 0 0 0 -5 -5l-.5 .5"></path><path d="M14 10a3.5 3.5 0 0 0 -5 0l-4 4a3.5 3.5 0 0 0 5 5l.5 -.5"></path></svg></a></h3><div class="tsd-signature tsd-kind-icon">pathSASViya<span class="tsd-signature-symbol">:</span> <span class="tsd-signature-type">string</span><span class="tsd-signature-symbol"> = ''</span></div><aside class="tsd-sources"><ul><li>Defined in <a href="https://github.com/sasjs/adapter/blob/1ee07ee/src/types/SASjsConfig.ts#L29">types/SASjsConfig.ts:29</a></li></ul></aside><div class="tsd-comment tsd-typography"><div class="lead">
|
||||
<p>The location of the Job Execution Web Application. By default the adapter
|
||||
will use '/SASJobExecution' on SAS Viya.</p>
|
||||
</div></div></section><section class="tsd-panel tsd-member tsd-kind-property tsd-parent-kind-class"><a id="requestHistoryLimit" class="tsd-anchor"></a><h3 class="tsd-anchor-link"><span class="tsd-flag ts-flagOptional">Optional</span> request<wbr/>History<wbr/>Limit<a href="#requestHistoryLimit" aria-label="Permalink" class="tsd-anchor-icon"><svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-link" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"></path><path d="M10 14a3.5 3.5 0 0 0 5 0l4 -4a3.5 3.5 0 0 0 -5 -5l-.5 .5"></path><path d="M14 10a3.5 3.5 0 0 0 -5 0l-4 4a3.5 3.5 0 0 0 5 5l.5 -.5"></path></svg></a></h3><div class="tsd-signature tsd-kind-icon">request<wbr/>History<wbr/>Limit<span class="tsd-signature-symbol">?:</span> <span class="tsd-signature-type">number</span><span class="tsd-signature-symbol"> = 10</span></div><aside class="tsd-sources"><ul><li>Defined in <a href="https://github.com/sasjs/adapter/blob/1ee07ee/src/types/SASjsConfig.ts#L76">types/SASjsConfig.ts:76</a></li></ul></aside><div class="tsd-comment tsd-typography"><div class="lead">
|
||||
<p>Optional setting to configure request history limit. Increasing this limit
|
||||
may affect browser performance, especially with debug (logs) enabled.</p>
|
||||
</div></div></section><section class="tsd-panel tsd-member tsd-kind-property tsd-parent-kind-class"><a id="serverType" class="tsd-anchor"></a><h3 class="tsd-anchor-link">server<wbr/>Type<a href="#serverType" aria-label="Permalink" class="tsd-anchor-icon"><svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-link" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"></path><path d="M10 14a3.5 3.5 0 0 0 5 0l4 -4a3.5 3.5 0 0 0 -5 -5l-.5 .5"></path><path d="M14 10a3.5 3.5 0 0 0 -5 0l-4 4a3.5 3.5 0 0 0 5 5l.5 -.5"></path></svg></a></h3><div class="tsd-signature tsd-kind-icon">server<wbr/>Type<span class="tsd-signature-symbol">:</span> <span class="tsd-signature-type">null</span><span class="tsd-signature-symbol"> | </span><span class="tsd-signature-type">ServerType</span><span class="tsd-signature-symbol"> = null</span></div><aside class="tsd-sources"><ul><li>Defined in <a href="https://github.com/sasjs/adapter/blob/1ee07ee/src/types/SASjsConfig.ts#L43">types/SASjsConfig.ts:43</a></li></ul></aside><div class="tsd-comment tsd-typography"><div class="lead">
|
||||
<p>Can be <code>SAS9</code> or <code>SASVIYA</code>.</p>
|
||||
</div></div></section><section class="tsd-panel tsd-member tsd-kind-property tsd-parent-kind-class"><a id="serverUrl" class="tsd-anchor"></a><h3 class="tsd-anchor-link">server<wbr/>Url<a href="#serverUrl" aria-label="Permalink" class="tsd-anchor-icon"><svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-link" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"></path><path d="M10 14a3.5 3.5 0 0 0 5 0l4 -4a3.5 3.5 0 0 0 -5 -5l-.5 .5"></path><path d="M14 10a3.5 3.5 0 0 0 -5 0l-4 4a3.5 3.5 0 0 0 5 5l.5 -.5"></path></svg></a></h3><div class="tsd-signature tsd-kind-icon">server<wbr/>Url<span class="tsd-signature-symbol">:</span> <span class="tsd-signature-type">string</span><span class="tsd-signature-symbol"> = ''</span></div><aside class="tsd-sources"><ul><li>Defined in <a href="https://github.com/sasjs/adapter/blob/1ee07ee/src/types/SASjsConfig.ts#L14">types/SASjsConfig.ts:14</a></li></ul></aside><div class="tsd-comment tsd-typography"><div class="lead">
|
||||
<p>The location (including http protocol and port) of the SAS Server.
|
||||
Can be omitted, eg if serving directly from the SAS Web Server or being
|
||||
streamed.</p>
|
||||
</div></div></section><section class="tsd-panel tsd-member tsd-kind-property tsd-parent-kind-class"><a id="useComputeApi" class="tsd-anchor"></a><h3 class="tsd-anchor-link">use<wbr/>Compute<wbr/>Api<a href="#useComputeApi" aria-label="Permalink" class="tsd-anchor-icon"><svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-link" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"></path><path d="M10 14a3.5 3.5 0 0 0 5 0l4 -4a3.5 3.5 0 0 0 -5 -5l-.5 .5"></path><path d="M14 10a3.5 3.5 0 0 0 -5 0l-4 4a3.5 3.5 0 0 0 5 5l.5 -.5"></path></svg></a></h3><div class="tsd-signature tsd-kind-icon">use<wbr/>Compute<wbr/>Api<span class="tsd-signature-symbol">:</span> <span class="tsd-signature-type">null</span><span class="tsd-signature-symbol"> | </span><span class="tsd-signature-type">boolean</span><span class="tsd-signature-symbol"> = null</span></div><aside class="tsd-sources"><ul><li>Defined in <a href="https://github.com/sasjs/adapter/blob/1ee07ee/src/types/SASjsConfig.ts#L61">types/SASjsConfig.ts:61</a></li></ul></aside><div class="tsd-comment tsd-typography"><div class="lead">
|
||||
<p>If it's <code>false</code> adapter will use the JES API as connection approach. To enhance VIYA
|
||||
performance, set to <code>true</code> and provide a <code>contextName</code> on which to run
|
||||
the code. When running on a named context, the code executes under the
|
||||
user identity. When running as a Job Execution service, the code runs
|
||||
under the identity in the JES context. If <code>useComputeApi</code> is <code>null</code> or <code>undefined</code>, the service will run as a Job, except
|
||||
triggered using the APIs instead of the Job Execution Web Service broker.</p>
|
||||
</div></div></section></section></div><div class="col-4 col-menu menu-sticky-wrap menu-highlight"><nav class="tsd-navigation primary"><ul><li class=""><a href="../modules.html">Modules</a></li><li class=" tsd-kind-module"><a href="../modules/SAS9ApiClient.html">SAS9<wbr/>Api<wbr/>Client</a></li><li class=" tsd-kind-module"><a href="../modules/SASViyaApiClient.html">SASViya<wbr/>Api<wbr/>Client</a></li><li class=" tsd-kind-module"><a href="../modules/SASjs.html">SASjs</a></li><li class=" tsd-kind-module"><a href="../modules/SASjsApiClient.html">SASjs<wbr/>Api<wbr/>Client</a></li><li class="current tsd-kind-module"><a href="../modules/types.html">types</a></li></ul></nav><nav class="tsd-navigation secondary menu-sticky"><ul><li class="current tsd-kind-class tsd-parent-kind-module"><a href="types.SASjsConfig.html" class="tsd-kind-icon">SASjs<wbr/>Config</a><ul><li class="tsd-kind-constructor tsd-parent-kind-class"><a href="types.SASjsConfig.html#constructor" class="tsd-kind-icon">constructor</a></li><li class="tsd-kind-property tsd-parent-kind-class"><a href="types.SASjsConfig.html#appLoc" class="tsd-kind-icon">app<wbr/>Loc</a></li><li class="tsd-kind-property tsd-parent-kind-class"><a href="types.SASjsConfig.html#contextName" class="tsd-kind-icon">context<wbr/>Name</a></li><li class="tsd-kind-property tsd-parent-kind-class"><a href="types.SASjsConfig.html#debug" class="tsd-kind-icon">debug</a></li><li class="tsd-kind-property tsd-parent-kind-class"><a href="types.SASjsConfig.html#httpsAgentOptions" class="tsd-kind-icon">https<wbr/>Agent<wbr/>Options</a></li><li class="tsd-kind-property tsd-parent-kind-class"><a href="types.SASjsConfig.html#loginMechanism" class="tsd-kind-icon">login<wbr/>Mechanism</a></li><li class="tsd-kind-property tsd-parent-kind-class"><a href="types.SASjsConfig.html#pathSAS9" class="tsd-kind-icon">pathSAS9</a></li><li class="tsd-kind-property tsd-parent-kind-class"><a href="types.SASjsConfig.html#pathSASJS" class="tsd-kind-icon">pathSASJS</a></li><li class="tsd-kind-property tsd-parent-kind-class"><a href="types.SASjsConfig.html#pathSASViya" class="tsd-kind-icon">pathSASViya</a></li><li class="tsd-kind-property tsd-parent-kind-class"><a href="types.SASjsConfig.html#requestHistoryLimit" class="tsd-kind-icon">request<wbr/>History<wbr/>Limit</a></li><li class="tsd-kind-property tsd-parent-kind-class"><a href="types.SASjsConfig.html#serverType" class="tsd-kind-icon">server<wbr/>Type</a></li><li class="tsd-kind-property tsd-parent-kind-class"><a href="types.SASjsConfig.html#serverUrl" class="tsd-kind-icon">server<wbr/>Url</a></li><li class="tsd-kind-property tsd-parent-kind-class"><a href="types.SASjsConfig.html#useComputeApi" class="tsd-kind-icon">use<wbr/>Compute<wbr/>Api</a></li></ul></li></ul></nav></div></div></div><footer class="with-border-bottom"><div class="container"><h2>Legend</h2><div class="tsd-legend-group"><ul class="tsd-legend"><li class="tsd-kind-class"><span class="tsd-kind-icon">Class</span></li><li class="tsd-kind-constructor tsd-parent-kind-class"><span class="tsd-kind-icon">Constructor</span></li><li class="tsd-kind-property tsd-parent-kind-class"><span class="tsd-kind-icon">Property</span></li></ul><ul class="tsd-legend"><li class="tsd-kind-function"><span class="tsd-kind-icon">Function</span></li></ul><ul class="tsd-legend"><li class="tsd-kind-enum"><span class="tsd-kind-icon">Enumeration</span></li></ul><ul class="tsd-legend"><li class="tsd-kind-interface"><span class="tsd-kind-icon">Interface</span></li></ul></div><h2>Settings</h2><p>Theme <select id="theme"><option value="os">OS</option><option value="light">Light</option><option value="dark">Dark</option></select></p></div></footer><div class="container tsd-generator"><p>Generated using <a href="https://typedoc.org/" target="_blank">TypeDoc</a></p></div><div class="overlay"></div><script src="../assets/main.js"></script></body></html>
|
||||
292
docs/index.html
@@ -1,292 +0,0 @@
|
||||
<!DOCTYPE html><html class="default"><head><meta charSet="utf-8"/><meta http-equiv="x-ua-compatible" content="IE=edge"/><title>@sasjs/adapter</title><meta name="description" content="Documentation for @sasjs/adapter"/><meta name="viewport" content="width=device-width, initial-scale=1"/><link rel="stylesheet" href="assets/style.css"/><link rel="stylesheet" href="assets/highlight.css"/><script async src="assets/search.js" id="search-script"></script></head><body><script>document.body.classList.add(localStorage.getItem("tsd-theme") || "os")</script><header><div class="tsd-page-toolbar"><div class="container"><div class="table-wrap"><div class="table-cell" id="tsd-search" data-base="."><div class="field"><label for="tsd-search-field" class="tsd-widget search no-caption">Search</label><input type="text" id="tsd-search-field"/></div><ul class="results"><li class="state loading">Preparing search index...</li><li class="state failure">The search index is not available</li></ul><a href="index.html" class="title">@sasjs/adapter</a></div><div class="table-cell" id="tsd-widgets"><div id="tsd-filter"><a href="#" class="tsd-widget options no-caption" data-toggle="options">Options</a><div class="tsd-filter-group"><div class="tsd-select" id="tsd-filter-visibility"><span class="tsd-select-label">All</span><ul class="tsd-select-list"><li data-value="public">Public</li><li data-value="protected">Public/Protected</li><li data-value="private" class="selected">All</li></ul></div> <input type="checkbox" id="tsd-filter-inherited" checked/><label class="tsd-widget" for="tsd-filter-inherited">Inherited</label></div></div><a href="#" class="tsd-widget menu no-caption" data-toggle="menu">Menu</a></div></div></div></div><div class="tsd-page-title"><div class="container"><h1> @sasjs/adapter</h1></div></div></header><div class="container container-main"><div class="row"><div class="col-8 col-content"><div class="tsd-panel tsd-typography">
|
||||
<a href="#sasjsadapter" id="sasjsadapter" style="color: inherit; text-decoration: none;">
|
||||
<h1>@sasjs/adapter</h1>
|
||||
</a>
|
||||
<p><a href="http://npmjs.org/package/@sasjs/adapter"><img src="https://img.shields.io/npm/v/@sasjs/adapter.svg" alt="npm package"></a>
|
||||
<a href="https://github.com/sasjs/adapter/blob/main/.github/workflows/build.yml"><img src="https://github.com/sasjs/adapter/actions/workflows/build.yml/badge.svg" alt="Github Workflow"></a>
|
||||
<a href="https://github.com/sasjs/adapter/blob/main/package.json"><img src="https://david-dm.org/sasjs/adapter.svg" alt="Dependency Status"></a>
|
||||
<a href=""><img src="https://img.shields.io/npm/dt/@sasjs/adapter" alt="npm"></a>
|
||||
<img src="https://img.shields.io/snyk/vulnerabilities/npm/@sasjs/adapter" alt="Snyk Vulnerabilities for npm package">
|
||||
<a href="/LICENSE"><img src="https://img.shields.io/apm/l/atomic-design-ui.svg" alt="License"></a>
|
||||
<img src="https://img.shields.io/github/languages/top/sasjs/adapter" alt="GitHub top language">
|
||||
<img src="https://img.shields.io/github/issues/sasjs/adapter" alt="GitHub issues">
|
||||
<a href="https://gitpod.io/#https://github.com/sasjs/adapter"><img src="https://img.shields.io/badge/Gitpod-ready--to--code-908a85?logo=gitpod" alt="Gitpod ready-to-code"></a></p>
|
||||
<p>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:</p>
|
||||
<p>1 - <code>npm install @sasjs/adapter</code> - for use in a nodeJS project (recommended)</p>
|
||||
<p>2 - <a href="https://cdn.jsdelivr.net/npm/@sasjs/adapter@3/index.min.js">Download</a> and use a copy of the latest JS file</p>
|
||||
<p>3 - Reference directly from the CDN - in which case click <a href="https://www.jsdelivr.com/package/npm/@sasjs/adapter?tab=collection">here</a> and select "SRI" to get the script tag with the integrity hash.</p>
|
||||
<p>If you are short on time and just need to build an app quickly, then check out <a href="https://vimeo.com/393161794">this video</a> and the <a href="https://github.com/sasjs/react-seed-app">react-seed-app</a> which provides some boilerplate.</p>
|
||||
<p>For more information on building web apps with SAS, check out <a href="https://sasjs.io">sasjs.io</a></p>
|
||||
|
||||
<a href="#none-of-this-makes-sense-how-do-i-build-an-app-with-it" id="none-of-this-makes-sense-how-do-i-build-an-app-with-it" style="color: inherit; text-decoration: none;">
|
||||
<h2>None of this makes sense. How do I build an app with it?</h2>
|
||||
</a>
|
||||
<p>Ok ok. Deploy this <a href="https://raw.githubusercontent.com/sasjs/adapter/master/example.html">example.html</a> file to your web server, and update <code>servertype</code> to <code>SAS9</code> or <code>SASVIYA</code> depending on your backend.</p>
|
||||
<p>The backend part can be deployed as follows:</p>
|
||||
<pre><code class="language-sas"><span class="hl-0">%let appLoc=/Public/app/readme; /* Metadata or Viya Folder per SASjs config */</span><br/><span class="hl-0">filename mc url "https://raw.githubusercontent.com/sasjs/core/main/all.sas";</span><br/><span class="hl-0">%inc mc; /* compile macros (can also be downloaded & compiled seperately) */</span><br/><span class="hl-0">filename ft15f001 temp;</span><br/><span class="hl-0">parmcards4;</span><br/><span class="hl-0"> %webout(FETCH) /* receive all data as SAS datasets */</span><br/><span class="hl-0"> proc sql;</span><br/><span class="hl-0"> create table areas as select make,mean(invoice) as avprice</span><br/><span class="hl-0"> from sashelp.cars</span><br/><span class="hl-0"> where type in (select type from work.fromjs)</span><br/><span class="hl-0"> group by 1;</span><br/><span class="hl-0"> %webout(OPEN)</span><br/><span class="hl-0"> %webout(OBJ,areas)</span><br/><span class="hl-0"> %webout(CLOSE)</span><br/><span class="hl-0">;;;;</span><br/><span class="hl-0">%mp_createwebservice(path=&appLoc/common,name=getdata)</span>
|
||||
</code></pre>
|
||||
<p>You now have a simple web app with a backend service!</p>
|
||||
|
||||
<a href="#detailed-overview" id="detailed-overview" style="color: inherit; text-decoration: none;">
|
||||
<h2>Detailed Overview</h2>
|
||||
</a>
|
||||
<p>The SASjs adapter is a JS library and a set of SAS Macros that handle the communication between the frontend app and backend SAS services.</p>
|
||||
<p>There are three parts to consider:</p>
|
||||
<ol>
|
||||
<li>JS request / response</li>
|
||||
<li>SAS inputs / outputs</li>
|
||||
<li>Configuration</li>
|
||||
</ol>
|
||||
|
||||
<a href="#js-request--response" id="js-request--response" style="color: inherit; text-decoration: none;">
|
||||
<h3>JS Request / Response</h3>
|
||||
</a>
|
||||
<p>To install the library you can simply run <code>npm i @sasjs/adapter</code> or include a <code><script></code> tag with a reference to our <a href="https://www.jsdelivr.com/package/npm/@sasjs/adapter">CDN</a>.</p>
|
||||
<p>Full technical documentation is available <a href="https://adapter.sasjs.io">here</a>. The main parts are:</p>
|
||||
|
||||
<a href="#instantiation" id="instantiation" style="color: inherit; text-decoration: none;">
|
||||
<h3>Instantiation</h3>
|
||||
</a>
|
||||
<p>The following code will instantiate an instance of the adapter:</p>
|
||||
<pre><code class="language-javascript"><span class="hl-1">let</span><span class="hl-0"> </span><span class="hl-2">sasJs</span><span class="hl-0"> = </span><span class="hl-1">new</span><span class="hl-0"> </span><span class="hl-2">SASjs</span><span class="hl-0">.</span><span class="hl-3">default</span><span class="hl-0">(</span><br/><span class="hl-0"> {</span><br/><span class="hl-0"> </span><span class="hl-2">appLoc:</span><span class="hl-0"> </span><span class="hl-4">"/Your/SAS/Folder"</span><span class="hl-0">,</span><br/><span class="hl-0"> </span><span class="hl-2">serverType:</span><span class="hl-4">"SAS9"</span><br/><span class="hl-0"> }</span><br/><span class="hl-0">);</span>
|
||||
</code></pre>
|
||||
<p>If you've installed it via NPM, you can import it as a default import like so:</p>
|
||||
<pre><code class="language-js"><span class="hl-0"> </span><span class="hl-5">import</span><span class="hl-0"> </span><span class="hl-2">SASjs</span><span class="hl-0"> </span><span class="hl-5">from</span><span class="hl-0"> </span><span class="hl-4">'@sasjs/adapter'</span><span class="hl-0">;</span>
|
||||
</code></pre>
|
||||
<p>You can then instantiate it with:</p>
|
||||
<pre><code class="language-js"><span class="hl-1">const</span><span class="hl-0"> </span><span class="hl-6">sasJs</span><span class="hl-0"> = </span><span class="hl-1">new</span><span class="hl-0"> </span><span class="hl-3">SASjs</span><span class="hl-0">({your </span><span class="hl-2">config</span><span class="hl-0">})</span>
|
||||
</code></pre>
|
||||
<p>More on the config later.</p>
|
||||
|
||||
<a href="#sas-logon" id="sas-logon" style="color: inherit; text-decoration: none;">
|
||||
<h3>SAS Logon</h3>
|
||||
</a>
|
||||
<p>All authentication from the adapter is done against SASLogon. There are two approaches that can be taken, which are configured using the <code>LoginMechanism</code> attribute of the sasJs config object (above):</p>
|
||||
<ul>
|
||||
<li><code>LoginMechanism:'Redirected'</code> - this approach enables authentication through a SASLogon window, supporting complex authentication flows (such as 2FA) and avoids the need to handle passwords in the application itself. The styling of the window can be modified using CSS.</li>
|
||||
<li><code>LoginMechanism:'Default'</code> - this approach requires that the username and password are captured, and used within the <code>.login()</code> method. This can be helpful for development, or automated testing.</li>
|
||||
</ul>
|
||||
<p>Sample code for logging in with the <code>Default</code> approach:</p>
|
||||
<pre><code class="language-javascript"><span class="hl-2">sasJs</span><span class="hl-0">.</span><span class="hl-3">logIn</span><span class="hl-0">(</span><span class="hl-4">'USERNAME'</span><span class="hl-0">,</span><span class="hl-4">'PASSWORD'</span><br/><span class="hl-0"> ).</span><span class="hl-3">then</span><span class="hl-0">((</span><span class="hl-2">response</span><span class="hl-0">) </span><span class="hl-1">=></span><span class="hl-0"> {</span><br/><span class="hl-0"> </span><span class="hl-5">if</span><span class="hl-0"> (</span><span class="hl-2">response</span><span class="hl-0">.</span><span class="hl-2">isLoggedIn</span><span class="hl-0"> === </span><span class="hl-1">true</span><span class="hl-0">) {</span><br/><span class="hl-0"> </span><span class="hl-2">console</span><span class="hl-0">.</span><span class="hl-3">log</span><span class="hl-0">(</span><span class="hl-4">'do stuff'</span><span class="hl-0">)</span><br/><span class="hl-0"> } </span><span class="hl-5">else</span><span class="hl-0"> {</span><br/><span class="hl-0"> </span><span class="hl-2">console</span><span class="hl-0">.</span><span class="hl-3">log</span><span class="hl-0">(</span><span class="hl-4">'do other stuff'</span><span class="hl-0">)</span><br/><span class="hl-0"> }</span><br/><span class="hl-0">}</span>
|
||||
</code></pre>
|
||||
<p>More examples of using authentication, and more, can be found in the <a href="https://github.com/search?q=topic%3Asasjs-app+org%3Asasjs+fork%3Atrue">SASjs Seed Apps</a> on github.</p>
|
||||
|
||||
<a href="#request--response" id="request--response" style="color: inherit; text-decoration: none;">
|
||||
<h3>Request / Response</h3>
|
||||
</a>
|
||||
<p>A simple request can be sent to SAS in the following fashion:</p>
|
||||
<pre><code class="language-javascript"><span class="hl-2">sasJs</span><span class="hl-0">.</span><span class="hl-3">request</span><span class="hl-0">(</span><span class="hl-4">"/path/to/my/service"</span><span class="hl-0">, </span><span class="hl-2">dataObject</span><span class="hl-0">)</span><br/><span class="hl-0"> .</span><span class="hl-3">then</span><span class="hl-0">((</span><span class="hl-2">response</span><span class="hl-0">) </span><span class="hl-1">=></span><span class="hl-0"> {</span><br/><span class="hl-0"> </span><span class="hl-7">// all tables are in the response object, eg:</span><br/><span class="hl-0"> </span><span class="hl-2">console</span><span class="hl-0">.</span><span class="hl-3">log</span><span class="hl-0">(</span><span class="hl-2">response</span><span class="hl-0">.</span><span class="hl-2">tablewith2cols1row</span><span class="hl-0">[</span><span class="hl-8">0</span><span class="hl-0">].</span><span class="hl-6">COL1</span><span class="hl-0">.</span><span class="hl-2">value</span><span class="hl-0">)</span><br/><span class="hl-0"> })</span>
|
||||
</code></pre>
|
||||
<p>We supply the path to the SAS service, and a data object. The data object can be null (for services with no input), or can contain one or more tables in the following format:</p>
|
||||
<pre><code class="language-javascript"><span class="hl-1">let</span><span class="hl-0"> </span><span class="hl-2">dataObject</span><span class="hl-0">={</span><br/><span class="hl-0"> </span><span class="hl-4">"tablewith2cols1row"</span><span class="hl-2">:</span><span class="hl-0"> [{</span><br/><span class="hl-0"> </span><span class="hl-4">"col1"</span><span class="hl-2">:</span><span class="hl-0"> </span><span class="hl-4">"val1"</span><span class="hl-0">,</span><br/><span class="hl-0"> </span><span class="hl-4">"col2"</span><span class="hl-2">:</span><span class="hl-0"> </span><span class="hl-8">42</span><br/><span class="hl-0"> }],</span><br/><span class="hl-0"> </span><span class="hl-4">"tablewith1col2rows"</span><span class="hl-2">:</span><span class="hl-0"> [{</span><br/><span class="hl-0"> </span><span class="hl-4">"col"</span><span class="hl-2">:</span><span class="hl-0"> </span><span class="hl-4">"row1"</span><br/><span class="hl-0"> }, {</span><br/><span class="hl-0"> </span><span class="hl-4">"col"</span><span class="hl-2">:</span><span class="hl-0"> </span><span class="hl-4">"row2"</span><br/><span class="hl-0"> }]</span><br/><span class="hl-0">};</span>
|
||||
</code></pre>
|
||||
<p>There are optional parameters such as a config object and a callback login function.</p>
|
||||
<p>The response object will contain returned tables and columns. Table names are always lowercase, and column names uppercase.</p>
|
||||
<p>The adapter will also cache the logs (if debug enabled) and even the work tables. For performance, it is best to keep debug mode off.</p>
|
||||
|
||||
<a href="#variable-types" id="variable-types" style="color: inherit; text-decoration: none;">
|
||||
<h3>Variable Types</h3>
|
||||
</a>
|
||||
<p>The SAS type (char/numeric) of the values is determined according to a set of rules:</p>
|
||||
<ul>
|
||||
<li>If the values are numeric, the SAS type is numeric</li>
|
||||
<li>If the values are all string, the SAS type is character</li>
|
||||
<li>If the values contain a single character (a-Z + underscore) AND a numeric, then the SAS type is numeric (with special missing values). </li>
|
||||
<li><code>null</code> is set to either '.' or '' depending on the assigned or derived type per the above rules. If entire column is <code>null</code> then the type will be numeric.</li>
|
||||
</ul>
|
||||
<p>The following table illustrates the formats applied to columns under various scenarios:</p>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>JS Values</th>
|
||||
<th>SAS Format</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody><tr>
|
||||
<td>'a', 'a'</td>
|
||||
<td>$char1.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>0, '_'</td>
|
||||
<td>best.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>'Z', 0</td>
|
||||
<td>best.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>'a', 'aaa'</td>
|
||||
<td>$char3.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>null, 'a', 'aaa'</td>
|
||||
<td>$char3.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>null, 'a', 0</td>
|
||||
<td>best.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>null, null</td>
|
||||
<td>best.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>null, ''</td>
|
||||
<td>$char1.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>null, 'a'</td>
|
||||
<td>$char1.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>'a'</td>
|
||||
<td>$char1.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>'a', null</td>
|
||||
<td>$char1.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>'a', null, 0</td>
|
||||
<td>best.</td>
|
||||
</tr>
|
||||
</tbody></table>
|
||||
<p>Validation is also performed on the values. The following combinations will throw errors:</p>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>JS Values</th>
|
||||
<th>SAS Format</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody><tr>
|
||||
<td>null, 'aaaa', 0</td>
|
||||
<td>Error: mixed types. 'aaaa' is not a special missing value.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>0, 'a', '!'</td>
|
||||
<td>Error: mixed types. '!' is not a special missing value</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>1.1, '.', 0</td>
|
||||
<td>Error: mixed types. For regular nulls, use <code>null</code></td>
|
||||
</tr>
|
||||
</tbody></table>
|
||||
|
||||
<a href="#variable-format-override" id="variable-format-override" style="color: inherit; text-decoration: none;">
|
||||
<h3>Variable Format Override</h3>
|
||||
</a>
|
||||
<p>The auto-detect functionality above is thwarted in the following scenarios:</p>
|
||||
<ul>
|
||||
<li>A character column containing only <code>null</code> values (is considered numeric)</li>
|
||||
<li>A numeric column containing only special missing values (is considered character)</li>
|
||||
</ul>
|
||||
<p>To cater for these scenarios, an optional array of formats can be passed along with the data to ensure that SAS will read them in correctly.</p>
|
||||
<p>To understand these formats, it should be noted that the JSON data is NOT passed directly (as JSON) to SAS. It is first converted into CSV, and the header row is actually an <code>infile</code> statement in disguise. It looks a bit like this:</p>
|
||||
<pre><code class="language-csv">CHARVAR1:$char4. CHARVAR2:$char1. NUMVAR:best.
|
||||
LOAD,,0
|
||||
ABCD,X,.
|
||||
</code></pre>
|
||||
<p>To provide overrides to this header row, the tables object can be constructed as follows (with a leading '$' in the table name):</p>
|
||||
<pre><code class="language-javascript"><span class="hl-1">let</span><span class="hl-0"> </span><span class="hl-2">specialData</span><span class="hl-0">={</span><br/><span class="hl-0"> </span><span class="hl-4">"tablewith2cols2rows"</span><span class="hl-2">:</span><span class="hl-0"> [</span><br/><span class="hl-0"> {</span><span class="hl-4">"col1"</span><span class="hl-2">:</span><span class="hl-0"> </span><span class="hl-4">"val1"</span><span class="hl-0">,</span><span class="hl-4">"specialMissingsCol"</span><span class="hl-2">:</span><span class="hl-0"> </span><span class="hl-4">"A"</span><span class="hl-0">},</span><br/><span class="hl-0"> {</span><span class="hl-4">"col1"</span><span class="hl-2">:</span><span class="hl-0"> </span><span class="hl-4">"val2"</span><span class="hl-0">,</span><span class="hl-4">"specialMissingsCol"</span><span class="hl-2">:</span><span class="hl-0"> </span><span class="hl-4">"_"</span><span class="hl-0">}</span><br/><span class="hl-0"> ],</span><br/><span class="hl-0"> </span><span class="hl-4">"$tablewith2cols2rows"</span><span class="hl-2">:</span><span class="hl-0">{</span><span class="hl-4">"formats"</span><span class="hl-2">:</span><span class="hl-0">{</span><span class="hl-4">"specialMissingsCol"</span><span class="hl-2">:</span><span class="hl-4">"best."</span><span class="hl-0">}</span><br/><span class="hl-0"> }</span><br/><span class="hl-0">};</span>
|
||||
</code></pre>
|
||||
<p>It is not necessary to provide formats for ALL the columns, only the ones that need to be overridden.</p>
|
||||
|
||||
<a href="#sas-inputs--outputs" id="sas-inputs--outputs" style="color: inherit; text-decoration: none;">
|
||||
<h2>SAS Inputs / Outputs</h2>
|
||||
</a>
|
||||
<p>The SAS side is handled by a number of macros in the <a href="https://github.com/sasjs/core">macro core</a> library.</p>
|
||||
<p>The following snippet shows the process of SAS tables arriving / leaving:</p>
|
||||
<pre><code class="language-sas"><span class="hl-7">/* fetch all input tables sent from frontend - they arrive as work tables */</span><br/><span class="hl-0">%webout(FETCH)</span><br/><br/><span class="hl-7">/* some sas code */</span><br/><span class="hl-0">data a b c;</span><br/><span class="hl-0"> set from js;</span><br/><span class="hl-0">run;</span><br/><br/><span class="hl-0">%webout(OPEN) /* Open the JSON to be returned */</span><br/><span class="hl-0">%webout(OBJ,a) /* Rows in table `a` are objects (easy to use) */</span><br/><span class="hl-0">%webout(ARR,b) /* Rows in table `b` are arrays (compact) */</span><br/><span class="hl-0">%webout(OBJ,c,fmt=N) /* Table `c` is sent unformatted (raw) */</span><br/><span class="hl-0">%webout(OBJ,c,label=d) /* Rename as `d` on JS side */</span><br/><span class="hl-0">%webout(CLOSE) /* Close the JSON and add default variables */</span>
|
||||
</code></pre>
|
||||
<p>By default, special SAS numeric missings (_a-Z) are converted to <code>null</code> in the JSON. If you'd like to preserve these, use the <code>missing=STRING</code> option as follows:</p>
|
||||
<pre><code class="language-sas"><span class="hl-0">%webout(OBJ,a,missing=STRING)</span>
|
||||
</code></pre>
|
||||
<p>In this case, special missings (such as <code>.a</code>, <code>.b</code>) are converted to javascript string values (<code>'A', 'B'</code>).</p>
|
||||
<p>Where an entire column is made up of special missing numerics, there would be no way to distinguish it from a single-character column by looking at the values. To cater for this scenario, it is possible to export the variable types (and other attributes such as label and format) by adding a <code>showmeta</code> param to the <code>webout()</code> macro as follows:</p>
|
||||
<pre><code class="language-sas"><span class="hl-0">%webout(OBJ,a,missing=STRING,showmeta=YES)</span>
|
||||
</code></pre>
|
||||
|
||||
<a href="#configuration" id="configuration" style="color: inherit; text-decoration: none;">
|
||||
<h2>Configuration</h2>
|
||||
</a>
|
||||
<p>Configuration on the client side involves passing an object on startup, which can also be passed with each request. Technical documentation on the SASjsConfig class is available <a href="https://adapter.sasjs.io/classes/types.sasjsconfig.html">here</a>. The main config items are:</p>
|
||||
<ul>
|
||||
<li><code>appLoc</code> - this is the folder (eg in metadata or SAS Drive) under which the SAS services are created.</li>
|
||||
<li><code>serverType</code> - either <code>SAS9</code>, <code>SASVIYA</code> or <code>SASJS</code>. The <code>SASJS</code> server type is for use with <a href="https://github.com/sasjs/server">sasjs/server</a>.</li>
|
||||
<li><code>serverUrl</code> - the location (including http protocol and port) of the SAS Server. Can be omitted, eg if serving directly from the SAS Web Server, or in streaming mode.</li>
|
||||
<li><code>debug</code> - if <code>true</code> then SAS Logs and extra debug information is returned.</li>
|
||||
<li><code>LoginMechanism</code> - either <code>Default</code> or <code>Redirected</code>. See <a href="#sas-logon">SAS Logon</a> section.</li>
|
||||
<li><code>useComputeApi</code> - Only relevant when the serverType is <code>SASVIYA</code>. If <code>true</code> the <a href="#using-the-compute-api">Compute API</a> is used. If <code>false</code> the <a href="#using-the-jes-api">JES API</a> is used. If <code>null</code> or <code>undefined</code> the <a href="#using-jes-web-app">Web</a> approach is used.</li>
|
||||
<li><code>contextName</code> - Compute context on which the requests will be called. If missing or not provided, defaults to <code>Job Execution Compute context</code>.</li>
|
||||
<li><code>requestHistoryLimit</code> - Request history limit. Increasing this limit may affect browser performance, especially with debug (logs) enabled. Default is 10.</li>
|
||||
</ul>
|
||||
<p>The adapter supports a number of approaches for interfacing with Viya (<code>serverType</code> is <code>SASVIYA</code>). For maximum performance, be sure to <a href="https://sasjs.io/guide-viya/#shared-account-and-server-re-use">configure your compute context</a> with <code>reuseServerProcesses</code> as <code>true</code> and a system account in <code>runServerAs</code>. This functionality is available since Viya 3.5. This configuration is supported when <a href="https://sasjs.io/sasjs-cli-context/#sasjs-context-create">creating contexts using the CLI</a>.</p>
|
||||
|
||||
<a href="#using-jes-web-app" id="using-jes-web-app" style="color: inherit; text-decoration: none;">
|
||||
<h3>Using JES Web App</h3>
|
||||
</a>
|
||||
<p>In this setup, all requests are routed through the JES web app, at <code>YOURSERVER/SASJobExecution?_program=/your/program</code>. This is the most reliable method, and also the slowest. One request is made to the JES app, and remaining requests (getting job uri, session spawning, passing parameters, running the program, fetching the log) are handled by the SAS server inside the JES app.</p>
|
||||
<pre><code><span class="hl-0">{</span><br/><span class="hl-0"> </span><span class="hl-9">appLoc</span><span class="hl-0">:</span><span class="hl-4">"/Your/Path"</span><span class="hl-0">,</span><br/><span class="hl-0"> </span><span class="hl-9">serverType</span><span class="hl-0">:</span><span class="hl-4">"SASVIYA"</span><span class="hl-0">,</span><br/><span class="hl-0"> </span><span class="hl-9">contextName</span><span class="hl-0">: </span><span class="hl-4">'yourComputeContext'</span><br/><span class="hl-0">}</span>
|
||||
</code></pre>
|
||||
<p>Note - to use the web approach, the <code>useComputeApi</code> property must be <code>undefined</code> or <code>null</code>.</p>
|
||||
|
||||
<a href="#using-the-jes-api" id="using-the-jes-api" style="color: inherit; text-decoration: none;">
|
||||
<h3>Using the JES API</h3>
|
||||
</a>
|
||||
<p>Here we are running Jobs using the Job Execution Service except this time we are making the requests directly using the REST API instead of through the JES Web App. This is helpful when we need to call web services outside of a browser (eg with the SASjs CLI or other commandline tools). To save one network request, the adapter prefetches the JOB URIs and passes them in the <code>__job</code> parameter. Depending on your network bandwidth, it may or may not be faster than the JES Web approach.</p>
|
||||
<p>This approach (<code>useComputeApi: false</code>) also ensures that jobs are displayed in Environment Manager.</p>
|
||||
<pre><code class="language-json"><span class="hl-0">{</span><br/><span class="hl-0"> </span><span class="hl-10">appLoc</span><span class="hl-0">:</span><span class="hl-4">"/Your/Path"</span><span class="hl-0">,</span><br/><span class="hl-0"> </span><span class="hl-10">serverType</span><span class="hl-0">:</span><span class="hl-4">"SASVIYA"</span><span class="hl-0">,</span><br/><span class="hl-0"> </span><span class="hl-10">useComputeApi</span><span class="hl-0">: </span><span class="hl-1">false</span><span class="hl-0">,</span><br/><span class="hl-0"> </span><span class="hl-10">contextName</span><span class="hl-0">: </span><span class="hl-10">'yourComputeContext'</span><br/><span class="hl-0">}</span>
|
||||
</code></pre>
|
||||
|
||||
<a href="#using-the-compute-api" id="using-the-compute-api" style="color: inherit; text-decoration: none;">
|
||||
<h3>Using the Compute API</h3>
|
||||
</a>
|
||||
<p>This approach is by far the fastest, as a result of the optimisations we have built into the adapter. With this configuration, in the first sasjs request, we take a URI map of the services in the target folder, and create a session manager. This manager will spawn a additional session every time a request is made. Subsequent requests will use the existing 'hot' session, if it exists. Sessions are always deleted after every use, which actually makes this <em>less</em> resource intensive than a typical JES web app, in which all sessions are kept alive by default for 15 minutes.</p>
|
||||
<p>With this approach (<code>useComputeApi: true</code>), the requests/logs will <em>not</em> appear in the list in Environment manager.</p>
|
||||
<pre><code class="language-json"><span class="hl-0">{</span><br/><span class="hl-0"> </span><span class="hl-10">appLoc</span><span class="hl-0">:</span><span class="hl-4">"/Your/Path"</span><span class="hl-0">,</span><br/><span class="hl-0"> </span><span class="hl-10">serverType</span><span class="hl-0">:</span><span class="hl-4">"SASVIYA"</span><span class="hl-0">,</span><br/><span class="hl-0"> </span><span class="hl-10">useComputeApi</span><span class="hl-0">: </span><span class="hl-1">true</span><span class="hl-0">,</span><br/><span class="hl-0"> </span><span class="hl-10">contextName</span><span class="hl-0">: </span><span class="hl-4">"yourComputeContext"</span><br/><span class="hl-0">}</span>
|
||||
</code></pre>
|
||||
|
||||
<a href="#more-resources" id="more-resources" style="color: inherit; text-decoration: none;">
|
||||
<h1>More resources</h1>
|
||||
</a>
|
||||
<p>For more information and examples specific to this adapter you can check out the <a href="https://sasjs.io/sasjs-adapter/">user guide</a> or the <a href="http://adapter.sasjs.io/">technical</a> documentation.</p>
|
||||
<p>For more information on building web apps in general, check out these <a href="https://sasjs.io/training/resources/">resources</a> or contact the <a href="https://www.linkedin.com/in/allanbowe/">author</a> directly.</p>
|
||||
<p>If you are a SAS 9 or SAS Viya customer you can also request a copy of <a href="https://datacontroller.io">Data Controller</a> - free for up to 5 users, this tool makes use of all parts of the SASjs framework.</p>
|
||||
|
||||
<a href="#star-gazing" id="star-gazing" style="color: inherit; text-decoration: none;">
|
||||
<h2>Star Gazing</h2>
|
||||
</a>
|
||||
<p>If you find this library useful, help us grow our star graph!</p>
|
||||
<p><img src="https://starchart.cc/sasjs/adapter.svg" alt=""></p>
|
||||
|
||||
<a href="#contributors-✨" id="contributors-✨" style="color: inherit; text-decoration: none;">
|
||||
<h2>Contributors ✨</h2>
|
||||
</a>
|
||||
<!-- ALL-CONTRIBUTORS-BADGE:START - Do not remove or modify this section -->
|
||||
<p><a href="#contributors-"><img src="https://img.shields.io/badge/all_contributors-7-orange.svg?style=flat-square" alt="All Contributors"></a></p>
|
||||
<!-- ALL-CONTRIBUTORS-BADGE:END -->
|
||||
|
||||
<p>Thanks goes to these wonderful people (<a href="https://allcontributors.org/docs/en/emoji-key">emoji key</a>):</p>
|
||||
<!-- ALL-CONTRIBUTORS-LIST:START - Do not remove or modify this section -->
|
||||
<!-- prettier-ignore-start -->
|
||||
<!-- markdownlint-disable -->
|
||||
<table>
|
||||
<tr>
|
||||
<td align="center"><a href="https://krishna-acondy.io/"><img src="https://avatars.githubusercontent.com/u/2980428?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Krishna Acondy</b></sub></a><br /><a href="https://github.com/sasjs/adapter/commits?author=krishna-acondy" title="Code">💻</a> <a href="#infra-krishna-acondy" title="Infrastructure (Hosting, Build-Tools, etc)">🚇</a> <a href="#blog-krishna-acondy" title="Blogposts">📝</a> <a href="#content-krishna-acondy" title="Content">🖋</a> <a href="#ideas-krishna-acondy" title="Ideas, Planning, & Feedback">🤔</a> <a href="#video-krishna-acondy" title="Videos">📹</a></td>
|
||||
<td align="center"><a href="https://www.erudicat.com/"><img src="https://avatars.githubusercontent.com/u/25773492?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Yury Shkoda</b></sub></a><br /><a href="https://github.com/sasjs/adapter/commits?author=YuryShkoda" title="Code">💻</a> <a href="#infra-YuryShkoda" title="Infrastructure (Hosting, Build-Tools, etc)">🚇</a> <a href="#ideas-YuryShkoda" title="Ideas, Planning, & Feedback">🤔</a> <a href="https://github.com/sasjs/adapter/commits?author=YuryShkoda" title="Tests">⚠️</a> <a href="#video-YuryShkoda" title="Videos">📹</a></td>
|
||||
<td align="center"><a href="https://github.com/medjedovicm"><img src="https://avatars.githubusercontent.com/u/18329105?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Mihajlo Medjedovic</b></sub></a><br /><a href="https://github.com/sasjs/adapter/commits?author=medjedovicm" title="Code">💻</a> <a href="#infra-medjedovicm" title="Infrastructure (Hosting, Build-Tools, etc)">🚇</a> <a href="https://github.com/sasjs/adapter/commits?author=medjedovicm" title="Tests">⚠️</a> <a href="https://github.com/sasjs/adapter/pulls?q=is%3Apr+reviewed-by%3Amedjedovicm" title="Reviewed Pull Requests">👀</a></td>
|
||||
<td align="center"><a href="https://github.com/allanbowe"><img src="https://avatars.githubusercontent.com/u/4420615?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Allan Bowe</b></sub></a><br /><a href="https://github.com/sasjs/adapter/commits?author=allanbowe" title="Code">💻</a> <a href="https://github.com/sasjs/adapter/pulls?q=is%3Apr+reviewed-by%3Aallanbowe" title="Reviewed Pull Requests">👀</a> <a href="https://github.com/sasjs/adapter/commits?author=allanbowe" title="Tests">⚠️</a> <a href="#mentoring-allanbowe" title="Mentoring">🧑🏫</a> <a href="#maintenance-allanbowe" title="Maintenance">🚧</a></td>
|
||||
<td align="center"><a href="https://github.com/saadjutt01"><img src="https://avatars.githubusercontent.com/u/8914650?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Muhammad Saad </b></sub></a><br /><a href="https://github.com/sasjs/adapter/commits?author=saadjutt01" title="Code">💻</a> <a href="https://github.com/sasjs/adapter/pulls?q=is%3Apr+reviewed-by%3Asaadjutt01" title="Reviewed Pull Requests">👀</a> <a href="https://github.com/sasjs/adapter/commits?author=saadjutt01" title="Tests">⚠️</a> <a href="#mentoring-saadjutt01" title="Mentoring">🧑🏫</a> <a href="#infra-saadjutt01" title="Infrastructure (Hosting, Build-Tools, etc)">🚇</a></td>
|
||||
<td align="center"><a href="https://github.com/sabhas"><img src="https://avatars.githubusercontent.com/u/82647447?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Sabir Hassan</b></sub></a><br /><a href="https://github.com/sasjs/adapter/commits?author=sabhas" title="Code">💻</a> <a href="https://github.com/sasjs/adapter/pulls?q=is%3Apr+reviewed-by%3Asabhas" title="Reviewed Pull Requests">👀</a> <a href="https://github.com/sasjs/adapter/commits?author=sabhas" title="Tests">⚠️</a> <a href="#ideas-sabhas" title="Ideas, Planning, & Feedback">🤔</a></td>
|
||||
<td align="center"><a href="https://github.com/VladislavParhomchik"><img src="https://avatars.githubusercontent.com/u/83717836?v=4?s=100" width="100px;" alt=""/><br /><sub><b>VladislavParhomchik</b></sub></a><br /><a href="https://github.com/sasjs/adapter/commits?author=VladislavParhomchik" title="Tests">⚠️</a> <a href="https://github.com/sasjs/adapter/pulls?q=is%3Apr+reviewed-by%3AVladislavParhomchik" title="Reviewed Pull Requests">👀</a></td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<!-- markdownlint-restore -->
|
||||
<!-- prettier-ignore-end -->
|
||||
|
||||
<!-- ALL-CONTRIBUTORS-LIST:END -->
|
||||
|
||||
<p>This project follows the <a href="https://github.com/all-contributors/all-contributors">all-contributors</a> specification. Contributions of any kind welcome!</p>
|
||||
</div></div><div class="col-4 col-menu menu-sticky-wrap menu-highlight"><nav class="tsd-navigation primary"><ul><li class="current"><a href="modules.html">Modules</a></li><li class=" tsd-kind-module"><a href="modules/SAS9ApiClient.html">SAS9<wbr/>Api<wbr/>Client</a></li><li class=" tsd-kind-module"><a href="modules/SASViyaApiClient.html">SASViya<wbr/>Api<wbr/>Client</a></li><li class=" tsd-kind-module"><a href="modules/SASjs.html">SASjs</a></li><li class=" tsd-kind-module"><a href="modules/SASjsApiClient.html">SASjs<wbr/>Api<wbr/>Client</a></li><li class=" tsd-kind-module"><a href="modules/types.html">types</a></li></ul></nav></div></div></div><footer class="with-border-bottom"><div class="container"><h2>Legend</h2><div class="tsd-legend-group"><ul class="tsd-legend"><li class="tsd-kind-function"><span class="tsd-kind-icon">Function</span></li></ul><ul class="tsd-legend"><li class="tsd-kind-enum"><span class="tsd-kind-icon">Enumeration</span></li></ul><ul class="tsd-legend"><li class="tsd-kind-interface"><span class="tsd-kind-icon">Interface</span></li></ul><ul class="tsd-legend"><li class="tsd-kind-class"><span class="tsd-kind-icon">Class</span></li></ul></div><h2>Settings</h2><p>Theme <select id="theme"><option value="os">OS</option><option value="light">Light</option><option value="dark">Dark</option></select></p></div></footer><div class="container tsd-generator"><p>Generated using <a href="https://typedoc.org/" target="_blank">TypeDoc</a></p></div><div class="overlay"></div><script src="assets/main.js"></script></body></html>
|
||||
@@ -1,3 +0,0 @@
|
||||
<!DOCTYPE html><html class="default"><head><meta charSet="utf-8"/><meta http-equiv="x-ua-compatible" content="IE=edge"/><title>UploadFile | @sasjs/adapter</title><meta name="description" content="Documentation for @sasjs/adapter"/><meta name="viewport" content="width=device-width, initial-scale=1"/><link rel="stylesheet" href="../assets/style.css"/><link rel="stylesheet" href="../assets/highlight.css"/><script async src="../assets/search.js" id="search-script"></script></head><body><script>document.body.classList.add(localStorage.getItem("tsd-theme") || "os")</script><header><div class="tsd-page-toolbar"><div class="container"><div class="table-wrap"><div class="table-cell" id="tsd-search" data-base=".."><div class="field"><label for="tsd-search-field" class="tsd-widget search no-caption">Search</label><input type="text" id="tsd-search-field"/></div><ul class="results"><li class="state loading">Preparing search index...</li><li class="state failure">The search index is not available</li></ul><a href="../index.html" class="title">@sasjs/adapter</a></div><div class="table-cell" id="tsd-widgets"><div id="tsd-filter"><a href="#" class="tsd-widget options no-caption" data-toggle="options">Options</a><div class="tsd-filter-group"><div class="tsd-select" id="tsd-filter-visibility"><span class="tsd-select-label">All</span><ul class="tsd-select-list"><li data-value="public">Public</li><li data-value="protected">Public/Protected</li><li data-value="private" class="selected">All</li></ul></div> <input type="checkbox" id="tsd-filter-inherited" checked/><label class="tsd-widget" for="tsd-filter-inherited">Inherited</label></div></div><a href="#" class="tsd-widget menu no-caption" data-toggle="menu">Menu</a></div></div></div></div><div class="tsd-page-title"><div class="container"><ul class="tsd-breadcrumb"><li><a href="../modules.html">@sasjs/adapter</a></li><li><a href="../modules/types.html">types</a></li><li><a href="types.UploadFile.html">UploadFile</a></li></ul><h1>Interface UploadFile</h1></div></div></header><div class="container container-main"><div class="row"><div class="col-8 col-content"><section class="tsd-panel tsd-comment"><div class="tsd-comment tsd-typography"><div class="lead">
|
||||
<p>Represents an object that is passed to the file uploader.</p>
|
||||
</div></div></section><section class="tsd-panel tsd-hierarchy"><h3>Hierarchy</h3><ul class="tsd-hierarchy"><li><span class="target">UploadFile</span></li></ul></section><section class="tsd-panel-group tsd-index-group"><h2>Index</h2><section class="tsd-panel tsd-index-panel"><div class="tsd-index-content"><section class="tsd-index-section "><h3>Properties</h3><ul class="tsd-index-list"><li class="tsd-kind-property tsd-parent-kind-interface"><a href="types.UploadFile.html#file" class="tsd-kind-icon">file</a></li><li class="tsd-kind-property tsd-parent-kind-interface"><a href="types.UploadFile.html#fileName" class="tsd-kind-icon">file<wbr/>Name</a></li></ul></section></div></section></section><section class="tsd-panel-group tsd-member-group "><h2>Properties</h2><section class="tsd-panel tsd-member tsd-kind-property tsd-parent-kind-interface"><a id="file" class="tsd-anchor"></a><h3 class="tsd-anchor-link">file<a href="#file" aria-label="Permalink" class="tsd-anchor-icon"><svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-link" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"></path><path d="M10 14a3.5 3.5 0 0 0 5 0l4 -4a3.5 3.5 0 0 0 -5 -5l-.5 .5"></path><path d="M14 10a3.5 3.5 0 0 0 -5 0l-4 4a3.5 3.5 0 0 0 5 5l.5 -.5"></path></svg></a></h3><div class="tsd-signature tsd-kind-icon">file<span class="tsd-signature-symbol">:</span> <span class="tsd-signature-type">File</span></div><aside class="tsd-sources"><ul><li>Defined in <a href="https://github.com/sasjs/adapter/blob/1ee07ee/src/types/UploadFile.ts#L6">types/UploadFile.ts:6</a></li></ul></aside></section><section class="tsd-panel tsd-member tsd-kind-property tsd-parent-kind-interface"><a id="fileName" class="tsd-anchor"></a><h3 class="tsd-anchor-link">file<wbr/>Name<a href="#fileName" aria-label="Permalink" class="tsd-anchor-icon"><svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-link" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"></path><path d="M10 14a3.5 3.5 0 0 0 5 0l4 -4a3.5 3.5 0 0 0 -5 -5l-.5 .5"></path><path d="M14 10a3.5 3.5 0 0 0 -5 0l-4 4a3.5 3.5 0 0 0 5 5l.5 -.5"></path></svg></a></h3><div class="tsd-signature tsd-kind-icon">file<wbr/>Name<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/1ee07ee/src/types/UploadFile.ts#L7">types/UploadFile.ts:7</a></li></ul></aside></section></section></div><div class="col-4 col-menu menu-sticky-wrap menu-highlight"><nav class="tsd-navigation primary"><ul><li class=""><a href="../modules.html">Modules</a></li><li class=" tsd-kind-module"><a href="../modules/SAS9ApiClient.html">SAS9<wbr/>Api<wbr/>Client</a></li><li class=" tsd-kind-module"><a href="../modules/SASViyaApiClient.html">SASViya<wbr/>Api<wbr/>Client</a></li><li class=" tsd-kind-module"><a href="../modules/SASjs.html">SASjs</a></li><li class=" tsd-kind-module"><a href="../modules/SASjsApiClient.html">SASjs<wbr/>Api<wbr/>Client</a></li><li class="current tsd-kind-module"><a href="../modules/types.html">types</a></li></ul></nav><nav class="tsd-navigation secondary menu-sticky"><ul><li class="current tsd-kind-interface tsd-parent-kind-module"><a href="types.UploadFile.html" class="tsd-kind-icon">Upload<wbr/>File</a><ul><li class="tsd-kind-property tsd-parent-kind-interface"><a href="types.UploadFile.html#file" class="tsd-kind-icon">file</a></li><li class="tsd-kind-property tsd-parent-kind-interface"><a href="types.UploadFile.html#fileName" class="tsd-kind-icon">file<wbr/>Name</a></li></ul></li></ul></nav></div></div></div><footer class="with-border-bottom"><div class="container"><h2>Legend</h2><div class="tsd-legend-group"><ul class="tsd-legend"><li class="tsd-kind-interface"><span class="tsd-kind-icon">Interface</span></li><li class="tsd-kind-property tsd-parent-kind-interface"><span class="tsd-kind-icon">Property</span></li></ul><ul class="tsd-legend"><li class="tsd-kind-function"><span class="tsd-kind-icon">Function</span></li></ul><ul class="tsd-legend"><li class="tsd-kind-enum"><span class="tsd-kind-icon">Enumeration</span></li></ul><ul class="tsd-legend"><li class="tsd-kind-class"><span class="tsd-kind-icon">Class</span></li></ul></div><h2>Settings</h2><p>Theme <select id="theme"><option value="os">OS</option><option value="light">Light</option><option value="dark">Dark</option></select></p></div></footer><div class="container tsd-generator"><p>Generated using <a href="https://typedoc.org/" target="_blank">TypeDoc</a></p></div><div class="overlay"></div><script src="../assets/main.js"></script></body></html>
|
||||
@@ -1 +0,0 @@
|
||||
<!DOCTYPE html><html class="default"><head><meta charSet="utf-8"/><meta http-equiv="x-ua-compatible" content="IE=edge"/><title>@sasjs/adapter</title><meta name="description" content="Documentation for @sasjs/adapter"/><meta name="viewport" content="width=device-width, initial-scale=1"/><link rel="stylesheet" href="assets/style.css"/><link rel="stylesheet" href="assets/highlight.css"/><script async src="assets/search.js" id="search-script"></script></head><body><script>document.body.classList.add(localStorage.getItem("tsd-theme") || "os")</script><header><div class="tsd-page-toolbar"><div class="container"><div class="table-wrap"><div class="table-cell" id="tsd-search" data-base="."><div class="field"><label for="tsd-search-field" class="tsd-widget search no-caption">Search</label><input type="text" id="tsd-search-field"/></div><ul class="results"><li class="state loading">Preparing search index...</li><li class="state failure">The search index is not available</li></ul><a href="index.html" class="title">@sasjs/adapter</a></div><div class="table-cell" id="tsd-widgets"><div id="tsd-filter"><a href="#" class="tsd-widget options no-caption" data-toggle="options">Options</a><div class="tsd-filter-group"><div class="tsd-select" id="tsd-filter-visibility"><span class="tsd-select-label">All</span><ul class="tsd-select-list"><li data-value="public">Public</li><li data-value="protected">Public/Protected</li><li data-value="private" class="selected">All</li></ul></div> <input type="checkbox" id="tsd-filter-inherited" checked/><label class="tsd-widget" for="tsd-filter-inherited">Inherited</label></div></div><a href="#" class="tsd-widget menu no-caption" data-toggle="menu">Menu</a></div></div></div></div><div class="tsd-page-title"><div class="container"><h1> @sasjs/adapter</h1></div></div></header><div class="container container-main"><div class="row"><div class="col-8 col-content"><section class="tsd-panel-group tsd-index-group"><h2>Index</h2><section class="tsd-panel tsd-index-panel"><div class="tsd-index-content"><section class="tsd-index-section "><h3>Modules</h3><ul class="tsd-index-list"><li class="tsd-kind-module"><a href="modules/SAS9ApiClient.html" class="tsd-kind-icon">SAS9<wbr/>Api<wbr/>Client</a></li><li class="tsd-kind-module"><a href="modules/SASViyaApiClient.html" class="tsd-kind-icon">SASViya<wbr/>Api<wbr/>Client</a></li><li class="tsd-kind-module"><a href="modules/SASjs.html" class="tsd-kind-icon">SASjs</a></li><li class="tsd-kind-module"><a href="modules/SASjsApiClient.html" class="tsd-kind-icon">SASjs<wbr/>Api<wbr/>Client</a></li><li class="tsd-kind-module"><a href="modules/types.html" class="tsd-kind-icon">types</a></li></ul></section></div></section></section></div><div class="col-4 col-menu menu-sticky-wrap menu-highlight"><nav class="tsd-navigation primary"><ul><li class="current"><a href="modules.html">Modules</a></li><li class=" tsd-kind-module"><a href="modules/SAS9ApiClient.html">SAS9<wbr/>Api<wbr/>Client</a></li><li class=" tsd-kind-module"><a href="modules/SASViyaApiClient.html">SASViya<wbr/>Api<wbr/>Client</a></li><li class=" tsd-kind-module"><a href="modules/SASjs.html">SASjs</a></li><li class=" tsd-kind-module"><a href="modules/SASjsApiClient.html">SASjs<wbr/>Api<wbr/>Client</a></li><li class=" tsd-kind-module"><a href="modules/types.html">types</a></li></ul></nav></div></div></div><footer class="with-border-bottom"><div class="container"><h2>Legend</h2><div class="tsd-legend-group"><ul class="tsd-legend"><li class="tsd-kind-function"><span class="tsd-kind-icon">Function</span></li></ul><ul class="tsd-legend"><li class="tsd-kind-enum"><span class="tsd-kind-icon">Enumeration</span></li></ul><ul class="tsd-legend"><li class="tsd-kind-interface"><span class="tsd-kind-icon">Interface</span></li></ul><ul class="tsd-legend"><li class="tsd-kind-class"><span class="tsd-kind-icon">Class</span></li></ul></div><h2>Settings</h2><p>Theme <select id="theme"><option value="os">OS</option><option value="light">Light</option><option value="dark">Dark</option></select></p></div></footer><div class="container tsd-generator"><p>Generated using <a href="https://typedoc.org/" target="_blank">TypeDoc</a></p></div><div class="overlay"></div><script src="assets/main.js"></script></body></html>
|
||||
@@ -1 +0,0 @@
|
||||
<!DOCTYPE html><html class="default"><head><meta charSet="utf-8"/><meta http-equiv="x-ua-compatible" content="IE=edge"/><title>SAS9ApiClient | @sasjs/adapter</title><meta name="description" content="Documentation for @sasjs/adapter"/><meta name="viewport" content="width=device-width, initial-scale=1"/><link rel="stylesheet" href="../assets/style.css"/><link rel="stylesheet" href="../assets/highlight.css"/><script async src="../assets/search.js" id="search-script"></script></head><body><script>document.body.classList.add(localStorage.getItem("tsd-theme") || "os")</script><header><div class="tsd-page-toolbar"><div class="container"><div class="table-wrap"><div class="table-cell" id="tsd-search" data-base=".."><div class="field"><label for="tsd-search-field" class="tsd-widget search no-caption">Search</label><input type="text" id="tsd-search-field"/></div><ul class="results"><li class="state loading">Preparing search index...</li><li class="state failure">The search index is not available</li></ul><a href="../index.html" class="title">@sasjs/adapter</a></div><div class="table-cell" id="tsd-widgets"><div id="tsd-filter"><a href="#" class="tsd-widget options no-caption" data-toggle="options">Options</a><div class="tsd-filter-group"><div class="tsd-select" id="tsd-filter-visibility"><span class="tsd-select-label">All</span><ul class="tsd-select-list"><li data-value="public">Public</li><li data-value="protected">Public/Protected</li><li data-value="private" class="selected">All</li></ul></div> <input type="checkbox" id="tsd-filter-inherited" checked/><label class="tsd-widget" for="tsd-filter-inherited">Inherited</label></div></div><a href="#" class="tsd-widget menu no-caption" data-toggle="menu">Menu</a></div></div></div></div><div class="tsd-page-title"><div class="container"><ul class="tsd-breadcrumb"><li><a href="../modules.html">@sasjs/adapter</a></li><li><a href="SAS9ApiClient.html">SAS9ApiClient</a></li></ul><h1>Module SAS9ApiClient</h1></div></div></header><div class="container container-main"><div class="row"><div class="col-8 col-content"><section class="tsd-panel-group tsd-index-group"><h2>Index</h2><section class="tsd-panel tsd-index-panel"><div class="tsd-index-content"><section class="tsd-index-section "><h3>Classes</h3><ul class="tsd-index-list"><li class="tsd-kind-class tsd-parent-kind-module"><a href="../classes/SAS9ApiClient.SAS9ApiClient-1.html" class="tsd-kind-icon">SAS9<wbr/>Api<wbr/>Client</a></li></ul></section></div></section></section></div><div class="col-4 col-menu menu-sticky-wrap menu-highlight"><nav class="tsd-navigation primary"><ul><li class=""><a href="../modules.html">Modules</a></li><li class="current tsd-kind-module"><a href="SAS9ApiClient.html">SAS9<wbr/>Api<wbr/>Client</a></li><li class=" tsd-kind-module"><a href="SASViyaApiClient.html">SASViya<wbr/>Api<wbr/>Client</a></li><li class=" tsd-kind-module"><a href="SASjs.html">SASjs</a></li><li class=" tsd-kind-module"><a href="SASjsApiClient.html">SASjs<wbr/>Api<wbr/>Client</a></li><li class=" tsd-kind-module"><a href="types.html">types</a></li></ul></nav><nav class="tsd-navigation secondary menu-sticky"><ul><li class="tsd-kind-class tsd-parent-kind-module"><a href="../classes/SAS9ApiClient.SAS9ApiClient-1.html" class="tsd-kind-icon">SAS9<wbr/>Api<wbr/>Client</a></li></ul></nav></div></div></div><footer class="with-border-bottom"><div class="container"><h2>Legend</h2><div class="tsd-legend-group"><ul class="tsd-legend"><li class="tsd-kind-function"><span class="tsd-kind-icon">Function</span></li></ul><ul class="tsd-legend"><li class="tsd-kind-enum"><span class="tsd-kind-icon">Enumeration</span></li></ul><ul class="tsd-legend"><li class="tsd-kind-interface"><span class="tsd-kind-icon">Interface</span></li></ul><ul class="tsd-legend"><li class="tsd-kind-class"><span class="tsd-kind-icon">Class</span></li></ul></div><h2>Settings</h2><p>Theme <select id="theme"><option value="os">OS</option><option value="light">Light</option><option value="dark">Dark</option></select></p></div></footer><div class="container tsd-generator"><p>Generated using <a href="https://typedoc.org/" target="_blank">TypeDoc</a></p></div><div class="overlay"></div><script src="../assets/main.js"></script></body></html>
|
||||
@@ -1 +0,0 @@
|
||||
<!DOCTYPE html><html class="default"><head><meta charSet="utf-8"/><meta http-equiv="x-ua-compatible" content="IE=edge"/><title>SASViyaApiClient | @sasjs/adapter</title><meta name="description" content="Documentation for @sasjs/adapter"/><meta name="viewport" content="width=device-width, initial-scale=1"/><link rel="stylesheet" href="../assets/style.css"/><link rel="stylesheet" href="../assets/highlight.css"/><script async src="../assets/search.js" id="search-script"></script></head><body><script>document.body.classList.add(localStorage.getItem("tsd-theme") || "os")</script><header><div class="tsd-page-toolbar"><div class="container"><div class="table-wrap"><div class="table-cell" id="tsd-search" data-base=".."><div class="field"><label for="tsd-search-field" class="tsd-widget search no-caption">Search</label><input type="text" id="tsd-search-field"/></div><ul class="results"><li class="state loading">Preparing search index...</li><li class="state failure">The search index is not available</li></ul><a href="../index.html" class="title">@sasjs/adapter</a></div><div class="table-cell" id="tsd-widgets"><div id="tsd-filter"><a href="#" class="tsd-widget options no-caption" data-toggle="options">Options</a><div class="tsd-filter-group"><div class="tsd-select" id="tsd-filter-visibility"><span class="tsd-select-label">All</span><ul class="tsd-select-list"><li data-value="public">Public</li><li data-value="protected">Public/Protected</li><li data-value="private" class="selected">All</li></ul></div> <input type="checkbox" id="tsd-filter-inherited" checked/><label class="tsd-widget" for="tsd-filter-inherited">Inherited</label></div></div><a href="#" class="tsd-widget menu no-caption" data-toggle="menu">Menu</a></div></div></div></div><div class="tsd-page-title"><div class="container"><ul class="tsd-breadcrumb"><li><a href="../modules.html">@sasjs/adapter</a></li><li><a href="SASViyaApiClient.html">SASViyaApiClient</a></li></ul><h1>Module SASViyaApiClient</h1></div></div></header><div class="container container-main"><div class="row"><div class="col-8 col-content"><section class="tsd-panel-group tsd-index-group"><h2>Index</h2><section class="tsd-panel tsd-index-panel"><div class="tsd-index-content"><section class="tsd-index-section "><h3>Classes</h3><ul class="tsd-index-list"><li class="tsd-kind-class tsd-parent-kind-module"><a href="../classes/SASViyaApiClient.SASViyaApiClient-1.html" class="tsd-kind-icon">SASViya<wbr/>Api<wbr/>Client</a></li></ul></section></div></section></section></div><div class="col-4 col-menu menu-sticky-wrap menu-highlight"><nav class="tsd-navigation primary"><ul><li class=""><a href="../modules.html">Modules</a></li><li class=" tsd-kind-module"><a href="SAS9ApiClient.html">SAS9<wbr/>Api<wbr/>Client</a></li><li class="current tsd-kind-module"><a href="SASViyaApiClient.html">SASViya<wbr/>Api<wbr/>Client</a></li><li class=" tsd-kind-module"><a href="SASjs.html">SASjs</a></li><li class=" tsd-kind-module"><a href="SASjsApiClient.html">SASjs<wbr/>Api<wbr/>Client</a></li><li class=" tsd-kind-module"><a href="types.html">types</a></li></ul></nav><nav class="tsd-navigation secondary menu-sticky"><ul><li class="tsd-kind-class tsd-parent-kind-module"><a href="../classes/SASViyaApiClient.SASViyaApiClient-1.html" class="tsd-kind-icon">SASViya<wbr/>Api<wbr/>Client</a></li></ul></nav></div></div></div><footer class="with-border-bottom"><div class="container"><h2>Legend</h2><div class="tsd-legend-group"><ul class="tsd-legend"><li class="tsd-kind-function"><span class="tsd-kind-icon">Function</span></li></ul><ul class="tsd-legend"><li class="tsd-kind-enum"><span class="tsd-kind-icon">Enumeration</span></li></ul><ul class="tsd-legend"><li class="tsd-kind-interface"><span class="tsd-kind-icon">Interface</span></li></ul><ul class="tsd-legend"><li class="tsd-kind-class"><span class="tsd-kind-icon">Class</span></li></ul></div><h2>Settings</h2><p>Theme <select id="theme"><option value="os">OS</option><option value="light">Light</option><option value="dark">Dark</option></select></p></div></footer><div class="container tsd-generator"><p>Generated using <a href="https://typedoc.org/" target="_blank">TypeDoc</a></p></div><div class="overlay"></div><script src="../assets/main.js"></script></body></html>
|
||||
@@ -1 +0,0 @@
|
||||
<!DOCTYPE html><html class="default"><head><meta charSet="utf-8"/><meta http-equiv="x-ua-compatible" content="IE=edge"/><title>SASjs | @sasjs/adapter</title><meta name="description" content="Documentation for @sasjs/adapter"/><meta name="viewport" content="width=device-width, initial-scale=1"/><link rel="stylesheet" href="../assets/style.css"/><link rel="stylesheet" href="../assets/highlight.css"/><script async src="../assets/search.js" id="search-script"></script></head><body><script>document.body.classList.add(localStorage.getItem("tsd-theme") || "os")</script><header><div class="tsd-page-toolbar"><div class="container"><div class="table-wrap"><div class="table-cell" id="tsd-search" data-base=".."><div class="field"><label for="tsd-search-field" class="tsd-widget search no-caption">Search</label><input type="text" id="tsd-search-field"/></div><ul class="results"><li class="state loading">Preparing search index...</li><li class="state failure">The search index is not available</li></ul><a href="../index.html" class="title">@sasjs/adapter</a></div><div class="table-cell" id="tsd-widgets"><div id="tsd-filter"><a href="#" class="tsd-widget options no-caption" data-toggle="options">Options</a><div class="tsd-filter-group"><div class="tsd-select" id="tsd-filter-visibility"><span class="tsd-select-label">All</span><ul class="tsd-select-list"><li data-value="public">Public</li><li data-value="protected">Public/Protected</li><li data-value="private" class="selected">All</li></ul></div> <input type="checkbox" id="tsd-filter-inherited" checked/><label class="tsd-widget" for="tsd-filter-inherited">Inherited</label></div></div><a href="#" class="tsd-widget menu no-caption" data-toggle="menu">Menu</a></div></div></div></div><div class="tsd-page-title"><div class="container"><ul class="tsd-breadcrumb"><li><a href="../modules.html">@sasjs/adapter</a></li><li><a href="SASjs.html">SASjs</a></li></ul><h1>Module SASjs</h1></div></div></header><div class="container container-main"><div class="row"><div class="col-8 col-content"><section class="tsd-panel-group tsd-index-group"><h2>Index</h2><section class="tsd-panel tsd-index-panel"><div class="tsd-index-content"><section class="tsd-index-section "><h3>Classes</h3><ul class="tsd-index-list"><li class="tsd-kind-class tsd-parent-kind-module"><a href="../classes/SASjs.SASjs-1.html" class="tsd-kind-icon">SASjs</a></li></ul></section></div></section></section></div><div class="col-4 col-menu menu-sticky-wrap menu-highlight"><nav class="tsd-navigation primary"><ul><li class=""><a href="../modules.html">Modules</a></li><li class=" tsd-kind-module"><a href="SAS9ApiClient.html">SAS9<wbr/>Api<wbr/>Client</a></li><li class=" tsd-kind-module"><a href="SASViyaApiClient.html">SASViya<wbr/>Api<wbr/>Client</a></li><li class="current tsd-kind-module"><a href="SASjs.html">SASjs</a></li><li class=" tsd-kind-module"><a href="SASjsApiClient.html">SASjs<wbr/>Api<wbr/>Client</a></li><li class=" tsd-kind-module"><a href="types.html">types</a></li></ul></nav><nav class="tsd-navigation secondary menu-sticky"><ul><li class="tsd-kind-class tsd-parent-kind-module"><a href="../classes/SASjs.SASjs-1.html" class="tsd-kind-icon">SASjs</a></li></ul></nav></div></div></div><footer class="with-border-bottom"><div class="container"><h2>Legend</h2><div class="tsd-legend-group"><ul class="tsd-legend"><li class="tsd-kind-function"><span class="tsd-kind-icon">Function</span></li></ul><ul class="tsd-legend"><li class="tsd-kind-enum"><span class="tsd-kind-icon">Enumeration</span></li></ul><ul class="tsd-legend"><li class="tsd-kind-interface"><span class="tsd-kind-icon">Interface</span></li></ul><ul class="tsd-legend"><li class="tsd-kind-class"><span class="tsd-kind-icon">Class</span></li></ul></div><h2>Settings</h2><p>Theme <select id="theme"><option value="os">OS</option><option value="light">Light</option><option value="dark">Dark</option></select></p></div></footer><div class="container tsd-generator"><p>Generated using <a href="https://typedoc.org/" target="_blank">TypeDoc</a></p></div><div class="overlay"></div><script src="../assets/main.js"></script></body></html>
|
||||
@@ -1 +0,0 @@
|
||||
<!DOCTYPE html><html class="default"><head><meta charSet="utf-8"/><meta http-equiv="x-ua-compatible" content="IE=edge"/><title>SASjsApiClient | @sasjs/adapter</title><meta name="description" content="Documentation for @sasjs/adapter"/><meta name="viewport" content="width=device-width, initial-scale=1"/><link rel="stylesheet" href="../assets/style.css"/><link rel="stylesheet" href="../assets/highlight.css"/><script async src="../assets/search.js" id="search-script"></script></head><body><script>document.body.classList.add(localStorage.getItem("tsd-theme") || "os")</script><header><div class="tsd-page-toolbar"><div class="container"><div class="table-wrap"><div class="table-cell" id="tsd-search" data-base=".."><div class="field"><label for="tsd-search-field" class="tsd-widget search no-caption">Search</label><input type="text" id="tsd-search-field"/></div><ul class="results"><li class="state loading">Preparing search index...</li><li class="state failure">The search index is not available</li></ul><a href="../index.html" class="title">@sasjs/adapter</a></div><div class="table-cell" id="tsd-widgets"><div id="tsd-filter"><a href="#" class="tsd-widget options no-caption" data-toggle="options">Options</a><div class="tsd-filter-group"><div class="tsd-select" id="tsd-filter-visibility"><span class="tsd-select-label">All</span><ul class="tsd-select-list"><li data-value="public">Public</li><li data-value="protected">Public/Protected</li><li data-value="private" class="selected">All</li></ul></div> <input type="checkbox" id="tsd-filter-inherited" checked/><label class="tsd-widget" for="tsd-filter-inherited">Inherited</label></div></div><a href="#" class="tsd-widget menu no-caption" data-toggle="menu">Menu</a></div></div></div></div><div class="tsd-page-title"><div class="container"><ul class="tsd-breadcrumb"><li><a href="../modules.html">@sasjs/adapter</a></li><li><a href="SASjsApiClient.html">SASjsApiClient</a></li></ul><h1>Module SASjsApiClient</h1></div></div></header><div class="container container-main"><div class="row"><div class="col-8 col-content"><section class="tsd-panel-group tsd-index-group"><h2>Index</h2><section class="tsd-panel tsd-index-panel"><div class="tsd-index-content"><section class="tsd-index-section "><h3>Classes</h3><ul class="tsd-index-list"><li class="tsd-kind-class tsd-parent-kind-module"><a href="../classes/SASjsApiClient.SASjsApiClient-1.html" class="tsd-kind-icon">SASjs<wbr/>Api<wbr/>Client</a></li></ul></section><section class="tsd-index-section "><h3>Interfaces</h3><ul class="tsd-index-list"><li class="tsd-kind-interface tsd-parent-kind-module"><a href="../interfaces/SASjsApiClient.SASjsAuthResponse.html" class="tsd-kind-icon">SASjs<wbr/>Auth<wbr/>Response</a></li></ul></section></div></section></section></div><div class="col-4 col-menu menu-sticky-wrap menu-highlight"><nav class="tsd-navigation primary"><ul><li class=""><a href="../modules.html">Modules</a></li><li class=" tsd-kind-module"><a href="SAS9ApiClient.html">SAS9<wbr/>Api<wbr/>Client</a></li><li class=" tsd-kind-module"><a href="SASViyaApiClient.html">SASViya<wbr/>Api<wbr/>Client</a></li><li class=" tsd-kind-module"><a href="SASjs.html">SASjs</a></li><li class="current tsd-kind-module"><a href="SASjsApiClient.html">SASjs<wbr/>Api<wbr/>Client</a></li><li class=" tsd-kind-module"><a href="types.html">types</a></li></ul></nav><nav class="tsd-navigation secondary menu-sticky"><ul><li class="tsd-kind-class tsd-parent-kind-module"><a href="../classes/SASjsApiClient.SASjsApiClient-1.html" class="tsd-kind-icon">SASjs<wbr/>Api<wbr/>Client</a></li><li class="tsd-kind-interface tsd-parent-kind-module"><a href="../interfaces/SASjsApiClient.SASjsAuthResponse.html" class="tsd-kind-icon">SASjs<wbr/>Auth<wbr/>Response</a></li></ul></nav></div></div></div><footer class="with-border-bottom"><div class="container"><h2>Legend</h2><div class="tsd-legend-group"><ul class="tsd-legend"><li class="tsd-kind-function"><span class="tsd-kind-icon">Function</span></li></ul><ul class="tsd-legend"><li class="tsd-kind-enum"><span class="tsd-kind-icon">Enumeration</span></li></ul><ul class="tsd-legend"><li class="tsd-kind-interface"><span class="tsd-kind-icon">Interface</span></li></ul><ul class="tsd-legend"><li class="tsd-kind-class"><span class="tsd-kind-icon">Class</span></li></ul></div><h2>Settings</h2><p>Theme <select id="theme"><option value="os">OS</option><option value="light">Light</option><option value="dark">Dark</option></select></p></div></footer><div class="container tsd-generator"><p>Generated using <a href="https://typedoc.org/" target="_blank">TypeDoc</a></p></div><div class="overlay"></div><script src="../assets/main.js"></script></body></html>
|
||||
6886
package-lock.json
generated
29
package.json
@@ -3,10 +3,11 @@
|
||||
"description": "JavaScript adapter for SAS",
|
||||
"homepage": "https://adapter.sasjs.io",
|
||||
"scripts": {
|
||||
"preinstall": "node checkNodeVersion",
|
||||
"prebuild": "node checkNodeVersion",
|
||||
"nodeVersionMessage": "echo \u001b[33m make sure you are running node lts version \u001b[0m",
|
||||
"preinstall": "npm run nodeVersionMessage",
|
||||
"prebuild": "npm run nodeVersionMessage",
|
||||
"build": "rimraf build && rimraf node && mkdir node && copyfiles -u 1 \"./src/**/*\" ./node && webpack && rimraf build/src && rimraf node",
|
||||
"package:lib": "npm run build && copyfiles ./package.json ./checkNodeVersion.js build && cd build && npm version \"5.0.0\" && npm pack",
|
||||
"package:lib": "npm run build && copyfiles ./package.json build && cd build && npm version \"5.0.0\" && npm pack",
|
||||
"publish:lib": "npm run build && cd build && npm publish",
|
||||
"lint:fix": "npx prettier --loglevel silent --write \"src/**/*.{ts,tsx,js,jsx,html,css,sass,less,json,yml,md,graphql}\" && npx prettier --loglevel silent --write \"sasjs-tests/src/**/*.{ts,tsx,js,jsx,html,css,sass,less,json,yml,md,graphql}\" && npx prettier --loglevel silent --write \"cypress/**/*.{ts,tsx,js,jsx,html,css,sass,less,json,yml,md,graphql}\"",
|
||||
"lint": "npx prettier --check \"src/**/*.{ts,tsx,js,jsx,html,css,sass,less,json,yml,md,graphql}\" && npx prettier --check \"sasjs-tests/src/**/*.{ts,tsx,js,jsx,html,css,sass,less,json,yml,md,graphql}\" && npx prettier --check \"cypress/**/*.{ts,tsx,js,jsx,html,css,sass,less,json,yml,md,graphql}\"",
|
||||
@@ -44,9 +45,7 @@
|
||||
"license": "ISC",
|
||||
"devDependencies": {
|
||||
"@cypress/webpack-preprocessor": "5.9.1",
|
||||
"@types/axios": "0.14.0",
|
||||
"@types/express": "4.17.13",
|
||||
"@types/form-data": "2.5.0",
|
||||
"@types/jest": "27.4.0",
|
||||
"@types/mime": "2.0.3",
|
||||
"@types/pem": "1.9.6",
|
||||
@@ -60,28 +59,26 @@
|
||||
"jest-extended": "2.0.0",
|
||||
"node-polyfill-webpack-plugin": "1.1.4",
|
||||
"path": "0.12.7",
|
||||
"pem": "1.14.6",
|
||||
"pem": "1.14.5",
|
||||
"prettier": "2.7.1",
|
||||
"process": "0.11.10",
|
||||
"rimraf": "3.0.2",
|
||||
"semantic-release": "19.0.3",
|
||||
"terser-webpack-plugin": "5.3.1",
|
||||
"terser-webpack-plugin": "5.3.6",
|
||||
"ts-jest": "27.1.3",
|
||||
"ts-loader": "9.2.6",
|
||||
"ts-loader": "9.4.0",
|
||||
"tslint": "6.1.3",
|
||||
"tslint-config-prettier": "1.18.0",
|
||||
"typedoc": "0.22.11",
|
||||
"typedoc-neo-theme": "1.1.1",
|
||||
"typedoc-plugin-external-module-name": "4.0.6",
|
||||
"typedoc-plugin-rename-defaults": "0.4.0",
|
||||
"typescript": "4.5.5",
|
||||
"webpack": "5.69.0",
|
||||
"typedoc": "0.23.24",
|
||||
"typedoc-plugin-rename-defaults": "0.6.4",
|
||||
"typescript": "4.8.3",
|
||||
"webpack": "5.76.2",
|
||||
"webpack-cli": "4.9.2"
|
||||
},
|
||||
"main": "index.js",
|
||||
"dependencies": {
|
||||
"@sasjs/utils": "2.44.0",
|
||||
"axios": "0.26.0",
|
||||
"@sasjs/utils": "2.52.0",
|
||||
"axios": "0.27.2",
|
||||
"axios-cookiejar-support": "1.0.1",
|
||||
"form-data": "4.0.0",
|
||||
"https": "1.0.0",
|
||||
|
||||
3
sasjs-tests/.gitignore
vendored
@@ -21,3 +21,6 @@
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
||||
sasjsbuild
|
||||
sasjsresults
|
||||
19
sasjs-tests/.sasjslint
Normal file
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"lineEndings": "off",
|
||||
"noTrailingSpaces": true,
|
||||
"noEncodedPasswords": true,
|
||||
"hasDoxygenHeader": true,
|
||||
"noSpacesInFileNames": true,
|
||||
"lowerCaseFileNames": true,
|
||||
"maxLineLength": 80,
|
||||
"maxHeaderLineLength": 80,
|
||||
"maxDataLineLength": 80,
|
||||
"noTabIndentation": true,
|
||||
"indentationMultiple": 2,
|
||||
"hasMacroNameInMend": true,
|
||||
"noNestedMacros": true,
|
||||
"hasMacroParentheses": true,
|
||||
"strictMacroDefinition": true,
|
||||
"noGremlins": true,
|
||||
"defaultHeader": "/**{lineEnding} @file{lineEnding} @brief <Your brief here>{lineEnding} <h4> SAS Macros </h4>{lineEnding}**/"
|
||||
}
|
||||
@@ -65,6 +65,7 @@ The code below will work on ALL SAS platforms (Viya, SAS 9 EBI, SASjs Server).
|
||||
```sas
|
||||
filename mc url "https://raw.githubusercontent.com/sasjs/core/main/all.sas";
|
||||
%inc mc;
|
||||
%let apploc=/Public/app/adapter-tests;
|
||||
filename ft15f001 temp lrecl=1000;
|
||||
parmcards4;
|
||||
%webout(FETCH)
|
||||
@@ -80,7 +81,7 @@ parmcards4;
|
||||
%mend; %x()
|
||||
%webout(CLOSE)
|
||||
;;;;
|
||||
%mx_createwebservice(path=/Public/app/common,name=sendObj)
|
||||
%mx_createwebservice(path=&apploc/services/common,name=sendObj)
|
||||
parmcards4;
|
||||
%webout(FETCH)
|
||||
%webout(OPEN)
|
||||
@@ -95,7 +96,7 @@ parmcards4;
|
||||
%mend; %x()
|
||||
%webout(CLOSE)
|
||||
;;;;
|
||||
%mx_createwebservice(path=/Public/app/common,name=sendArr)
|
||||
%mx_createwebservice(path=&apploc/services/common,name=sendArr)
|
||||
parmcards4;
|
||||
data work.macvars;
|
||||
set sashelp.vmacro;
|
||||
@@ -104,14 +105,14 @@ parmcards4;
|
||||
%webout(OBJ,macvars)
|
||||
%webout(CLOSE)
|
||||
;;;;
|
||||
%mx_createwebservice(path=/Public/app/common,name=sendMacVars)
|
||||
%mx_createwebservice(path=&apploc/services/common,name=sendMacVars)
|
||||
parmcards4;
|
||||
If you can keep your head when all about you
|
||||
Are losing theirs and blaming it on you,
|
||||
If you can trust yourself when all men doubt you,
|
||||
But make allowance for their doubting too;
|
||||
;;;;
|
||||
%mx_createwebservice(path=/Public/app/common,name=makeErr)
|
||||
%mx_createwebservice(path=&apploc/services/common,name=makeErr)
|
||||
parmcards4;
|
||||
%webout(OPEN)
|
||||
data _null_;
|
||||
@@ -120,7 +121,7 @@ data _null_;
|
||||
run;
|
||||
%webout(CLOSE)
|
||||
;;;;
|
||||
%mx_createwebservice(path=/Public/app/common,name=invalidJSON)
|
||||
%mx_createwebservice(path=&apploc/services/common,name=invalidJSON)
|
||||
```
|
||||
|
||||
You should now be able to access the tests in your browser at the deployed path on your server.
|
||||
|
||||
863
sasjs-tests/package-lock.json
generated
@@ -5,7 +5,7 @@
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@sasjs/adapter": "file:../build/sasjs-adapter-5.0.0.tgz",
|
||||
"@sasjs/test-framework": "^1.5.6",
|
||||
"@sasjs/test-framework": "1.5.7",
|
||||
"@types/jest": "^26.0.20",
|
||||
"@types/node": "^14.14.41",
|
||||
"@types/react": "^17.0.1",
|
||||
@@ -43,6 +43,6 @@
|
||||
]
|
||||
},
|
||||
"devDependencies": {
|
||||
"node-sass": "^7.0.1"
|
||||
"node-sass": "7.0.3"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,10 +3,10 @@
|
||||
"password": "",
|
||||
"sasJsConfig": {
|
||||
"serverUrl": "",
|
||||
"appLoc": "/Public/app",
|
||||
"serverType": "SASVIYA",
|
||||
"appLoc": "/Public/app/adapter-tests",
|
||||
"serverType": "SASJS",
|
||||
"debug": false,
|
||||
"contextName": "sasjs adapter compute context",
|
||||
"useComputeApi": true
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4,7 +4,7 @@ if npm run cy:run -- --spec "cypress/integration/sasjs.tests.ts" ; then
|
||||
echo "Cypress sasjs testing passed!"
|
||||
else
|
||||
echo '{"msgtype":"m.text", "body":"Automated sasjs-tests failed on the @sasjs/adapter PR: '$2'"}'
|
||||
curl -XPOST -d '{"msgtype":"m.text", "body":"Automated sasjs-tests failed on the @sasjs/adapter PR: '$2'"}' https://matrix.4gl.io/_matrix/client/r0/rooms/%21BDUPBPEGVvRLKLQUxY:4gl.io/send/m.room.message?access_token=$1
|
||||
curl -XPOST -d '{"msgtype":"m.text", "body":"Automated sasjs-tests failed on the @sasjs/adapter PR: '$2'"}' https://matrix.4gl.io/_matrix/client/r0/rooms/%21jRebyiGmHZlpfDwYXN:4gl.io:4gl.io/send/m.room.message?access_token=$1
|
||||
echo "Cypress sasjs testing failed!"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
13
sasjs-tests/sasjs/common/invalidJSON.sas
Normal file
@@ -0,0 +1,13 @@
|
||||
/**
|
||||
@file
|
||||
@brief Makes an invalid JSON file
|
||||
|
||||
<h4> SAS Macros </h4>
|
||||
**/
|
||||
|
||||
%webout(OPEN)
|
||||
data _null_;
|
||||
file _webout;
|
||||
put ' the discovery channel ';
|
||||
run;
|
||||
%webout(CLOSE)
|
||||
11
sasjs-tests/sasjs/common/makeErr.sas
Normal file
@@ -0,0 +1,11 @@
|
||||
/**
|
||||
@file
|
||||
@brief Makes an error
|
||||
|
||||
<h4> SAS Macros </h4>
|
||||
**/
|
||||
|
||||
If you can keep your head when all about you
|
||||
Are losing theirs and blaming it on you,
|
||||
If you can trust yourself when all men doubt you,
|
||||
But make allowance for their doubting too;
|
||||
21
sasjs-tests/sasjs/common/sendArr.sas
Normal file
@@ -0,0 +1,21 @@
|
||||
/**
|
||||
@file
|
||||
@brief Returns JSON in Array format
|
||||
|
||||
<h4> SAS Macros </h4>
|
||||
**/
|
||||
|
||||
%webout(FETCH)
|
||||
%webout(OPEN)
|
||||
%macro x();
|
||||
%if %symexist(sasjs_tables) %then
|
||||
%do i=1 %to %sysfunc(countw(&sasjs_tables));
|
||||
%let table=%scan(&sasjs_tables,&i);
|
||||
%webout(ARR,&table,missing=STRING,showmeta=YES)
|
||||
%end;
|
||||
%else %do i=1 %to &_webin_file_count;
|
||||
%webout(ARR,&&_webin_name&i,missing=STRING,showmeta=YES)
|
||||
%end;
|
||||
%mend x;
|
||||
%x()
|
||||
%webout(CLOSE)
|
||||
13
sasjs-tests/sasjs/common/sendMacVars.sas
Normal file
@@ -0,0 +1,13 @@
|
||||
/**
|
||||
@file
|
||||
@brief Returns Macro Variables
|
||||
|
||||
<h4> SAS Macros </h4>
|
||||
**/
|
||||
|
||||
data work.macvars;
|
||||
set sashelp.vmacro;
|
||||
run;
|
||||
%webout(OPEN)
|
||||
%webout(OBJ,macvars)
|
||||
%webout(CLOSE)
|
||||
21
sasjs-tests/sasjs/common/sendObj.sas
Normal file
@@ -0,0 +1,21 @@
|
||||
/**
|
||||
@file
|
||||
@brief Returns JSON in Object format
|
||||
|
||||
<h4> SAS Macros </h4>
|
||||
**/
|
||||
|
||||
%webout(FETCH)
|
||||
%webout(OPEN)
|
||||
%macro x();
|
||||
%if %symexist(sasjs_tables) %then
|
||||
%do i=1 %to %sysfunc(countw(&sasjs_tables));
|
||||
%let table=%scan(&sasjs_tables,&i);
|
||||
%webout(OBJ,&table,missing=STRING,showmeta=YES)
|
||||
%end;
|
||||
%else %do i=1 %to &_webin_file_count;
|
||||
%webout(OBJ,&&_webin_name&i,missing=STRING,showmeta=YES)
|
||||
%end;
|
||||
%mend x;
|
||||
%x()
|
||||
%webout(CLOSE)
|
||||
40
sasjs-tests/sasjs/doxy/Doxyfile
Normal file
@@ -0,0 +1,40 @@
|
||||
ALPHABETICAL_INDEX = NO
|
||||
|
||||
ENABLE_PREPROCESSING = NO
|
||||
EXTENSION_MAPPING = sas=Java ddl=Java
|
||||
EXTRACT_LOCAL_CLASSES = NO
|
||||
FILE_PATTERNS = *.sas \
|
||||
*.ddl \
|
||||
*.dox
|
||||
GENERATE_LATEX = NO
|
||||
GENERATE_TREEVIEW = YES
|
||||
HIDE_FRIEND_COMPOUNDS = YES
|
||||
HIDE_IN_BODY_DOCS = YES
|
||||
HIDE_SCOPE_NAMES = YES
|
||||
HIDE_UNDOC_CLASSES = YES
|
||||
HIDE_UNDOC_MEMBERS = YES
|
||||
HTML_OUTPUT = $(DOXY_HTML_OUTPUT)
|
||||
HTML_HEADER = $(HTML_HEADER)
|
||||
HTML_EXTRA_FILES = $(HTML_EXTRA_FILES)
|
||||
HTML_FOOTER = $(HTML_FOOTER)
|
||||
HTML_EXTRA_STYLESHEET = $(HTML_EXTRA_STYLESHEET)
|
||||
INHERIT_DOCS = NO
|
||||
INLINE_INFO = NO
|
||||
INPUT = $(DOXY_INPUT)
|
||||
LAYOUT_FILE = $(LAYOUT_FILE)
|
||||
USE_MDFILE_AS_MAINPAGE = README.md
|
||||
MAX_INITIALIZER_LINES = 0
|
||||
PROJECT_NAME = $(PROJECT_NAME)
|
||||
PROJECT_LOGO = $(PROJECT_LOGO)
|
||||
PROJECT_BRIEF = $(PROJECT_BRIEF)
|
||||
RECURSIVE = YES
|
||||
REPEAT_BRIEF = NO
|
||||
SHOW_NAMESPACES = NO
|
||||
SHOW_USED_FILES = NO
|
||||
SOURCE_BROWSER = YES
|
||||
SOURCE_TOOLTIPS = NO
|
||||
STRICT_PROTO_MATCHING = YES
|
||||
STRIP_CODE_COMMENTS = NO
|
||||
SUBGROUPING = NO
|
||||
TAB_SIZE = 2
|
||||
VERBATIM_HEADERS = NO
|
||||
112
sasjs-tests/sasjs/doxy/DoxygenLayout.xml
Normal file
@@ -0,0 +1,112 @@
|
||||
<doxygenlayout version="1.0">
|
||||
<!-- Generated by doxygen 1.8.14 -->
|
||||
<!-- Navigation index tabs for HTML output -->
|
||||
<navindex>
|
||||
<tab type="mainpage" visible="yes" title="Home"/>
|
||||
<tab type="pages" visible="no" title="" intro=""/>
|
||||
<tab type="modules" visible="no" title="" intro=""/>
|
||||
<tab type="namespaces" visible="no" title="">
|
||||
<tab type="namespacelist" visible="no" title="" intro=""/>
|
||||
<tab type="namespacemembers" visible="no" title="" intro=""/>
|
||||
</tab>
|
||||
<tab type="classes" visible="no" title="">
|
||||
<tab type="classlist" visible="no" title="" intro=""/>
|
||||
<tab type="classindex" visible="no" title=""/>
|
||||
<tab type="hierarchy" visible="no" title="" intro=""/>
|
||||
<tab type="classmembers" visible="no" title="" intro=""/>
|
||||
</tab>
|
||||
|
||||
<tab type="filelist" visible="yes" title="" intro="List of Files"/>
|
||||
|
||||
<tab type="examples" visible="no" title="" intro=""/>
|
||||
<tab type="user" url="data_lineage.svg" title="Lineage"/>
|
||||
</navindex>
|
||||
|
||||
|
||||
<!-- Layout definition for a file page -->
|
||||
<file>
|
||||
<briefdescription visible="yes"/>
|
||||
<includes visible="$SHOW_INCLUDE_FILES"/>
|
||||
<includegraph visible="$INCLUDE_GRAPH"/>
|
||||
<includedbygraph visible="$INCLUDED_BY_GRAPH"/>
|
||||
<sourcelink visible="yes"/>
|
||||
<memberdecl>
|
||||
<classes visible="no" title=""/>
|
||||
<namespaces visible="no" title=""/>
|
||||
<constantgroups visible="no" title=""/>
|
||||
<defines title=""/>
|
||||
<typedefs title=""/>
|
||||
<enums title=""/>
|
||||
<functions title=""/>
|
||||
<variables title=""/>
|
||||
<membergroups visible="no"/>
|
||||
</memberdecl>
|
||||
<detaileddescription title=""/>
|
||||
<memberdef>
|
||||
<inlineclasses title=""/>
|
||||
<defines title=""/>
|
||||
<typedefs title=""/>
|
||||
<enums title=""/>
|
||||
<functions title=""/>
|
||||
<variables title=""/>
|
||||
</memberdef>
|
||||
<authorsection/>
|
||||
</file>
|
||||
|
||||
<!-- Layout definition for a group page -->
|
||||
<group>
|
||||
<briefdescription visible="no"/>
|
||||
<groupgraph visible="$GROUP_GRAPHS"/>
|
||||
<memberdecl>
|
||||
<nestedgroups visible="no" title=""/>
|
||||
<dirs visible="yes" title=""/>
|
||||
<files visible="yes" title=""/>
|
||||
<namespaces visible="no" title=""/>
|
||||
<classes visible="no" title=""/>
|
||||
<defines title=""/>
|
||||
<typedefs title=""/>
|
||||
<enums title=""/>
|
||||
<enumvalues title=""/>
|
||||
<functions title=""/>
|
||||
<variables title=""/>
|
||||
<signals title=""/>
|
||||
<publicslots title=""/>
|
||||
<protectedslots title=""/>
|
||||
<privateslots title=""/>
|
||||
<events title=""/>
|
||||
<properties title=""/>
|
||||
<friends title=""/>
|
||||
<membergroups visible="yes"/>
|
||||
</memberdecl>
|
||||
<detaileddescription title=""/>
|
||||
<memberdef>
|
||||
<pagedocs/>
|
||||
<inlineclasses title=""/>
|
||||
<defines title=""/>
|
||||
<typedefs title=""/>
|
||||
<enums title=""/>
|
||||
<enumvalues title=""/>
|
||||
<functions title=""/>
|
||||
<variables title=""/>
|
||||
<signals title=""/>
|
||||
<publicslots title=""/>
|
||||
<protectedslots title=""/>
|
||||
<privateslots title=""/>
|
||||
<events title=""/>
|
||||
<properties title=""/>
|
||||
<friends title=""/>
|
||||
</memberdef>
|
||||
<authorsection visible="yes"/>
|
||||
</group>
|
||||
|
||||
<!-- Layout definition for a directory page -->
|
||||
<directory>
|
||||
<briefdescription visible="yes"/>
|
||||
<detaileddescription visible="yes" title=""/>
|
||||
<directorygraph visible="yes"/>
|
||||
<memberdecl>
|
||||
<dirs visible="yes"/>
|
||||
<files visible="yes"/>
|
||||
</memberdecl>
|
||||
</directory>
|
||||
</doxygenlayout>
|
||||
26
sasjs-tests/sasjs/doxy/doxygen.svg
Normal file
|
After Width: | Height: | Size: 15 KiB |
BIN
sasjs-tests/sasjs/doxy/favicon.ico
Normal file
|
After Width: | Height: | Size: 2.2 KiB |
BIN
sasjs-tests/sasjs/doxy/logo.png
Normal file
|
After Width: | Height: | Size: 6.5 KiB |
33
sasjs-tests/sasjs/doxy/new_footer.html
Normal file
@@ -0,0 +1,33 @@
|
||||
<!-- HTML footer for doxygen 1.8.17-->
|
||||
<!-- start footer part -->
|
||||
<!--BEGIN GENERATE_TREEVIEW-->
|
||||
<div id="nav-path" class="navpath">
|
||||
<!-- id is needed for treeview function! -->
|
||||
<ul>
|
||||
$navpath
|
||||
<li class="footer">
|
||||
$generatedby
|
||||
<a href="https://www.doxygen.org/index.html">
|
||||
<img class="footer" src="$relpath^doxygen.svg" alt="doxygen"
|
||||
/></a>
|
||||
$doxygenversion
|
||||
</li>
|
||||
<li>
|
||||
<i> For more information visit the </i>
|
||||
<a href="https://cli.sasjs.io">SASjs cli</a> documentation.
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<!--END GENERATE_TREEVIEW-->
|
||||
<!--BEGIN !GENERATE_TREEVIEW-->
|
||||
<hr class="footer" />
|
||||
<address class="footer">
|
||||
<small>
|
||||
$generatedby  <a href="http://www.doxygen.org/index.html">
|
||||
<img class="footer" src="$relpath^doxygen.svg" alt="doxygen" />
|
||||
</a>
|
||||
$doxygenversion
|
||||
</small>
|
||||
</address>
|
||||
|
||||
<!--END !GENERATE_TREEVIEW-->
|
||||
57
sasjs-tests/sasjs/doxy/new_header.html
Normal file
@@ -0,0 +1,57 @@
|
||||
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "https://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
|
||||
<!-- HTML header for doxygen 1.8.17-->
|
||||
<html xmlns="https://www.w3.org/1999/xhtml" lang="en">
|
||||
<head>
|
||||
<meta http-equiv="Content-Type" content="text/xhtml;charset=UTF-8" />
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=9" />
|
||||
<meta name="generator" content="Doxygen $doxygenversion" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<!--BEGIN PROJECT_NAME-->
|
||||
<title>$projectname: $title</title>
|
||||
<meta name="description" content="$projectbrief" />
|
||||
<!--END PROJECT_NAME-->
|
||||
<!--BEGIN !PROJECT_NAME-->
|
||||
<title>$title</title>
|
||||
<!--END !PROJECT_NAME-->
|
||||
<link href="$relpath^tabs.css" rel="stylesheet" type="text/css" />
|
||||
<script type="text/javascript" src="$relpath^jquery.js"></script>
|
||||
<script type="text/javascript" src="$relpath^dynsections.js"></script>
|
||||
$treeview $search $mathjax
|
||||
<link href="$relpath^$stylesheet" rel="stylesheet" type="text/css" />
|
||||
<link rel="shortcut icon" href="$relpath^favicon.ico" type="image/x-icon" />
|
||||
$extrastylesheet
|
||||
</head>
|
||||
<body>
|
||||
<div id="top">
|
||||
<!-- do not remove this div, it is closed by doxygen! -->
|
||||
|
||||
<!--BEGIN TITLEAREA-->
|
||||
<div id="titlearea">
|
||||
<table cellspacing="0" cellpadding="0">
|
||||
<tbody>
|
||||
<tr style="height: 56px">
|
||||
<!--BEGIN PROJECT_LOGO-->
|
||||
<td id="projectlogo">
|
||||
<a href="$relpath^"
|
||||
><img alt="Logo" src="$relpath^$projectlogo"
|
||||
/></a>
|
||||
</td>
|
||||
<!--END PROJECT_LOGO-->
|
||||
<td id="projectalign" style="padding-left: 0.5em">
|
||||
<div id="projectname">$projectname</div>
|
||||
<div id="projectbrief">$projectbrief</div>
|
||||
</td>
|
||||
<!--BEGIN DISABLE_INDEX-->
|
||||
<!--BEGIN SEARCHENGINE-->
|
||||
<td>$searchbox</td>
|
||||
<!--END SEARCHENGINE-->
|
||||
<!--END DISABLE_INDEX-->
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<!--END TITLEAREA-->
|
||||
<!-- end header part -->
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
4
sasjs-tests/sasjs/doxy/new_stylesheet.css
Normal file
@@ -0,0 +1,4 @@
|
||||
#projectlogo img {
|
||||
border: 0px none;
|
||||
max-height: 70px;
|
||||
}
|
||||
17
sasjs-tests/sasjs/sasjsconfig.json
Normal file
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"$schema": "https://cli.sasjs.io/sasjsconfig-schema.json",
|
||||
"serviceConfig": {
|
||||
"serviceFolders": [
|
||||
"sasjs/common"
|
||||
]
|
||||
},
|
||||
"defaultTarget": "4gl",
|
||||
"targets": [
|
||||
{
|
||||
"name": "4gl",
|
||||
"serverType": "SASJS",
|
||||
"serverUrl": "https://sas9.4gl.io",
|
||||
"appLoc": "/Public/app/adapter-tests"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -48,7 +48,7 @@ export const computeTests = (adapter: SASjs, appLoc: string): TestSuite => ({
|
||||
test: () => {
|
||||
const data: any = { table1: [{ col1: 'first col value' }] }
|
||||
return adapter.startComputeJob(
|
||||
'/Public/app/common/sendArr',
|
||||
'/Public/app/adapter-tests/services/common/sendArr',
|
||||
data,
|
||||
{},
|
||||
undefined,
|
||||
@@ -71,13 +71,12 @@ export const computeTests = (adapter: SASjs, appLoc: string): TestSuite => ({
|
||||
test: () => {
|
||||
const fileLines = [`data;`, `do x=1 to 100;`, `output;`, `end;`, `run;`]
|
||||
|
||||
return adapter.executeScriptSASViya(
|
||||
'sasCode.sas',
|
||||
fileLines,
|
||||
'SAS Studio compute context',
|
||||
undefined,
|
||||
true
|
||||
)
|
||||
return adapter.executeScript({
|
||||
fileName: 'sasCode.sas',
|
||||
linesOfCode: fileLines,
|
||||
contextName: 'SAS Studio compute context',
|
||||
debug: true
|
||||
})
|
||||
},
|
||||
assertion: (res: any) => {
|
||||
const expectedLogContent = `1 data;\\n2 do x=1 to 100;\\n3 output;\\n4 end;\\n5 run;\\n\\n`
|
||||
@@ -92,13 +91,12 @@ export const computeTests = (adapter: SASjs, appLoc: string): TestSuite => ({
|
||||
const fileLines = [`%abort;`]
|
||||
|
||||
return adapter
|
||||
.executeScriptSASViya(
|
||||
'sasCode.sas',
|
||||
fileLines,
|
||||
'SAS Studio compute context',
|
||||
undefined,
|
||||
true
|
||||
)
|
||||
.executeScript({
|
||||
fileName: 'sasCode.sas',
|
||||
linesOfCode: fileLines,
|
||||
contextName: 'SAS Studio compute context',
|
||||
debug: true
|
||||
})
|
||||
.catch((err: any) => err)
|
||||
},
|
||||
assertion: (res: any) => {
|
||||
|
||||
BIN
screenshots/session-manager-first-request.png
Normal file
|
After Width: | Height: | Size: 164 KiB |
BIN
screenshots/subsequent-session-request.png
Normal file
|
After Width: | Height: | Size: 212 KiB |
@@ -548,6 +548,7 @@ export class SASViyaApiClient {
|
||||
|
||||
/**
|
||||
* Exchanges the refresh token for an access token for the given client.
|
||||
* This method can only be used by Node.
|
||||
* @param clientId - the client ID to authenticate with.
|
||||
* @param clientSecret - the client secret to authenticate with.
|
||||
* @param refreshToken - the refresh token received from the server.
|
||||
|
||||
158
src/SASjs.ts
@@ -4,8 +4,7 @@ import {
|
||||
UploadFile,
|
||||
EditContextInput,
|
||||
PollOptions,
|
||||
LoginMechanism,
|
||||
ExecutionQuery
|
||||
LoginMechanism
|
||||
} from './types'
|
||||
import { SASViyaApiClient } from './SASViyaApiClient'
|
||||
import { SAS9ApiClient } from './SAS9ApiClient'
|
||||
@@ -17,7 +16,7 @@ import {
|
||||
AuthConfig,
|
||||
ExtraResponseAttributes,
|
||||
SasAuthResponse,
|
||||
ServicePackSASjs
|
||||
AuthConfigSas9
|
||||
} from '@sasjs/utils/types'
|
||||
import { RequestClient } from './request/RequestClient'
|
||||
import { SasjsRequestClient } from './request/SasjsRequestClient'
|
||||
@@ -33,6 +32,16 @@ import {
|
||||
import { ErrorResponse } from './types/errors'
|
||||
import { LoginOptions, LoginResult } from './types/Login'
|
||||
|
||||
interface ExecuteScriptParams {
|
||||
linesOfCode: string[]
|
||||
fileName?: string
|
||||
contextName?: string
|
||||
runTime?: string
|
||||
authConfig?: AuthConfig
|
||||
authConfigSas9?: AuthConfigSas9
|
||||
debug?: boolean
|
||||
}
|
||||
|
||||
const defaultConfig: SASjsConfig = {
|
||||
serverUrl: '',
|
||||
pathSASJS: '/SASjsApi/stp/execute',
|
||||
@@ -79,74 +88,73 @@ export default class SASjs {
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes SAS code on a SAS 9 server. Requires a runner to be present in
|
||||
* the users home directory in metadata.
|
||||
* @param linesOfCode - lines of sas code from the file to run.
|
||||
* @param username - a string representing the username.
|
||||
* @param password - a string representing the password.
|
||||
*/
|
||||
public async executeScriptSAS9(
|
||||
linesOfCode: string[],
|
||||
userName: string,
|
||||
password: string
|
||||
) {
|
||||
this.isMethodSupported('executeScriptSAS9', [ServerType.Sas9])
|
||||
|
||||
return await this.sas9ApiClient?.executeScript(
|
||||
linesOfCode,
|
||||
userName,
|
||||
password
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes SAS code on a SASJS server
|
||||
* @param code - a string of code from the file to run.
|
||||
* Executes code on a SAS server.
|
||||
* @param linesOfCode - lines of code to run.
|
||||
* @param fileName - (required for server type sas viya) name of the file to run. It will be converted to path to the file being submitted for execution.
|
||||
* @param contextName - (required for server type sas viya) context name on which code will be run on the server.
|
||||
* @param runTime - (required for server type sasjs) a string to represent runTime for code execution.
|
||||
* @param authConfig - (optional) a valid client, secret, refresh and access tokens that are authorised to execute scripts.
|
||||
*/
|
||||
public async executeScriptSASjs(
|
||||
code: string,
|
||||
runTime?: string,
|
||||
authConfig?: AuthConfig
|
||||
) {
|
||||
this.isMethodSupported('executeScriptSASJS', [ServerType.Sasjs])
|
||||
|
||||
return await this.sasJSApiClient?.executeScript(code, runTime, authConfig)
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes sas code in a SAS Viya compute session.
|
||||
* @param fileName - name of the file to run. It will be converted to path to the file being submitted for execution.
|
||||
* @param linesOfCode - lines of sas code from the file to run.
|
||||
* @param contextName - context name on which code will be run on the server.
|
||||
* @param authConfig - (optional) the access token, refresh token, client and secret for authorizing the request.
|
||||
* @param authConfigSas9 - (required for server type sas9) a valid username and password that are authorised to execute scripts.
|
||||
* @param debug - (optional) if true, global debug config will be overriden
|
||||
*/
|
||||
public async executeScriptSASViya(
|
||||
fileName: string,
|
||||
linesOfCode: string[],
|
||||
contextName: string,
|
||||
authConfig?: AuthConfig,
|
||||
debug?: boolean
|
||||
) {
|
||||
this.isMethodSupported('executeScriptSASViya', [ServerType.SasViya])
|
||||
public async executeScript({
|
||||
linesOfCode,
|
||||
fileName,
|
||||
contextName,
|
||||
runTime,
|
||||
authConfig,
|
||||
authConfigSas9,
|
||||
debug
|
||||
}: ExecuteScriptParams) {
|
||||
this.isMethodSupported('executeScript', [
|
||||
ServerType.Sas9,
|
||||
ServerType.Sasjs,
|
||||
ServerType.SasViya
|
||||
])
|
||||
|
||||
contextName = contextName || this.sasjsConfig.contextName
|
||||
if (this.sasjsConfig.serverType === ServerType.Sas9) {
|
||||
if (!authConfigSas9)
|
||||
throw new Error('Auth config for sas9 is not provided')
|
||||
|
||||
if (!contextName) {
|
||||
throw new Error(
|
||||
'Context name is undefined. Please set a `contextName` in your SASjs or override config.'
|
||||
return await this.sas9ApiClient?.executeScript(
|
||||
linesOfCode,
|
||||
authConfigSas9.userName,
|
||||
authConfigSas9.password
|
||||
)
|
||||
}
|
||||
|
||||
return await this.sasViyaApiClient!.executeScript(
|
||||
fileName,
|
||||
linesOfCode,
|
||||
contextName,
|
||||
authConfig,
|
||||
null,
|
||||
debug ? debug : this.sasjsConfig.debug
|
||||
)
|
||||
if (this.sasjsConfig.serverType === ServerType.Sasjs) {
|
||||
return await this.sasJSApiClient?.executeScript(
|
||||
linesOfCode.join('\n'),
|
||||
runTime,
|
||||
authConfig
|
||||
)
|
||||
}
|
||||
|
||||
if (this.sasjsConfig.serverType === ServerType.SasViya) {
|
||||
contextName = contextName || this.sasjsConfig.contextName
|
||||
|
||||
if (!contextName) {
|
||||
throw new Error(
|
||||
'Context name is undefined. Please set a `contextName` in your SASjs or override config.'
|
||||
)
|
||||
}
|
||||
|
||||
if (!fileName) {
|
||||
throw new Error(
|
||||
'File name is required in case of SAS VIYA. Please provide a `fileName`.'
|
||||
)
|
||||
}
|
||||
|
||||
return await this.sasViyaApiClient!.executeScript(
|
||||
fileName,
|
||||
linesOfCode,
|
||||
contextName,
|
||||
authConfig,
|
||||
null,
|
||||
debug ? debug : this.sasjsConfig.debug
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -826,29 +834,6 @@ export default class SASjs {
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates the folders and services at the given location `appLoc` on the given server `serverUrl`.
|
||||
* @param dataJson - the JSON specifying the folders and files to be created, can also includes
|
||||
* appLoc, streamServiceName, streamWebFolder, streamLogo
|
||||
* @param appLoc - (optional) the base folder in which to create the new folders and
|
||||
* services. If not provided, is taken from SASjsConfig. Precedence will be of appLoc present in dataJson.
|
||||
* @param authConfig - (optional) a valid client, secret, refresh and access tokens that are authorised to execute compute jobs.
|
||||
*/
|
||||
public async deployToSASjs(
|
||||
dataJson: ServicePackSASjs,
|
||||
appLoc?: string,
|
||||
authConfig?: AuthConfig
|
||||
) {
|
||||
if (!appLoc) {
|
||||
appLoc = this.sasjsConfig.appLoc
|
||||
}
|
||||
return await this.sasJSApiClient?.deploy(dataJson, appLoc, authConfig)
|
||||
}
|
||||
|
||||
public async executeJobSASjs(query: ExecutionQuery, authConfig?: AuthConfig) {
|
||||
return await this.sasJSApiClient?.executeJob(query, authConfig)
|
||||
}
|
||||
|
||||
/**
|
||||
* Kicks off execution of the given job via the compute API.
|
||||
* @returns an object representing the compute session created for the given job.
|
||||
@@ -908,6 +893,7 @@ export default class SASjs {
|
||||
await this.computeJobExecutor?.resendWaitingRequests()
|
||||
await this.jesJobExecutor?.resendWaitingRequests()
|
||||
await this.fileUploader?.resendWaitingRequests()
|
||||
await this.sasjsJobExecutor?.resendWaitingRequests()
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,28 +1,39 @@
|
||||
import * as NodeFormData from 'form-data'
|
||||
import { AuthConfig, ServerType, ServicePackSASjs } from '@sasjs/utils/types'
|
||||
import { ExecutionQuery } from './types'
|
||||
import { RequestClient } from './request/RequestClient'
|
||||
import { getAccessTokenForSasjs } from './auth/getAccessTokenForSasjs'
|
||||
import { refreshTokensForSasjs } from './auth/refreshTokensForSasjs'
|
||||
import { parseWeboutResponse, SASJS_LOGS_SEPARATOR } from './utils'
|
||||
import { getTokens } from './auth/getTokens'
|
||||
|
||||
export class SASjsApiClient {
|
||||
constructor(private requestClient: RequestClient) {}
|
||||
|
||||
private async getAccessTokenForRequest(authConfig?: AuthConfig) {
|
||||
if (authConfig) {
|
||||
const { access_token } = await getTokens(
|
||||
this.requestClient,
|
||||
authConfig,
|
||||
ServerType.Sasjs
|
||||
)
|
||||
|
||||
return access_token
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates the folders and services at the given location `appLoc` on the given server `serverUrl`.
|
||||
* @param dataJson - the JSON specifying the folders and files to be created, can also includes
|
||||
* appLoc, streamServiceName, streamWebFolder, streamLogo
|
||||
* @param appLoc - the base folder in which to create the new folders and services.
|
||||
* @param authConfig - (optional) a valid client, secret, refresh and access tokens that are authorised to execute compute jobs.
|
||||
*/
|
||||
public async deploy(
|
||||
dataJson: ServicePackSASjs,
|
||||
appLoc: string,
|
||||
authConfig?: AuthConfig
|
||||
) {
|
||||
let access_token = (authConfig || {}).access_token
|
||||
if (authConfig) {
|
||||
;({ access_token } = await getTokens(
|
||||
this.requestClient,
|
||||
authConfig,
|
||||
ServerType.Sasjs
|
||||
))
|
||||
}
|
||||
|
||||
const access_token = await this.getAccessTokenForRequest(authConfig)
|
||||
dataJson.appLoc = dataJson.appLoc || appLoc
|
||||
|
||||
const { result } = await this.requestClient.post<{
|
||||
@@ -42,25 +53,61 @@ export class SASjsApiClient {
|
||||
return Promise.resolve(result)
|
||||
}
|
||||
|
||||
public async executeJob(query: ExecutionQuery, authConfig?: AuthConfig) {
|
||||
const access_token = authConfig ? authConfig.access_token : undefined
|
||||
/**
|
||||
* Creates/updates files within SASjs drive using uploaded json compressed file.
|
||||
* @param zipFilePath - Compressed file path; file should only contain one JSON file and
|
||||
* should have same name as of compressed file e.g. deploy.JSON should be compressed to deploy.JSON.zip
|
||||
* Any other file or JSON file in zipped will be ignored!
|
||||
* @param authConfig - (optional) a valid client, secret, refresh and access tokens that are authorised to execute compute jobs.
|
||||
*/
|
||||
public async deployZipFile(zipFilePath: string, authConfig?: AuthConfig) {
|
||||
const { createReadStream } = require('@sasjs/utils/file')
|
||||
const access_token = await this.getAccessTokenForRequest(authConfig)
|
||||
|
||||
const file = await createReadStream(zipFilePath)
|
||||
const formData = new NodeFormData()
|
||||
formData.append('file', file)
|
||||
|
||||
const contentType = `multipart/form-data; boundary=${formData.getBoundary()}`
|
||||
|
||||
const { result } = await this.requestClient.post<{
|
||||
status: string
|
||||
message: string
|
||||
log?: string
|
||||
logPath?: string
|
||||
error?: {}
|
||||
_webout?: string
|
||||
}>('SASjsApi/stp/execute', query, access_token)
|
||||
|
||||
if (Object.keys(result).includes('_webout')) {
|
||||
result._webout = parseWeboutResponse(result._webout!)
|
||||
}
|
||||
streamServiceName?: string
|
||||
example?: {}
|
||||
}>(
|
||||
'SASjsApi/drive/deploy/upload',
|
||||
formData,
|
||||
access_token,
|
||||
contentType,
|
||||
{},
|
||||
{ maxContentLength: Infinity, maxBodyLength: Infinity }
|
||||
)
|
||||
|
||||
return Promise.resolve(result)
|
||||
}
|
||||
|
||||
public async executeJob(
|
||||
query: ExecutionQuery,
|
||||
appLoc: string,
|
||||
authConfig?: AuthConfig
|
||||
) {
|
||||
const access_token = authConfig ? authConfig.access_token : undefined
|
||||
|
||||
let _program
|
||||
if (query._program.startsWith('/')) {
|
||||
_program = query._program
|
||||
} else _program = `${appLoc}/${query._program}`
|
||||
|
||||
const response: any = await this.requestClient.post(
|
||||
'SASjsApi/stp/execute',
|
||||
{ _debug: 131, ...query, _program },
|
||||
access_token
|
||||
)
|
||||
|
||||
return { result: response.result, log: response.log }
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes code on a SASJS server.
|
||||
* @param code - a string of code to execute.
|
||||
@@ -72,14 +119,7 @@ export class SASjsApiClient {
|
||||
runTime: string = 'sas',
|
||||
authConfig?: AuthConfig
|
||||
) {
|
||||
let access_token = (authConfig || {}).access_token
|
||||
if (authConfig) {
|
||||
;({ access_token } = await getTokens(
|
||||
this.requestClient,
|
||||
authConfig,
|
||||
ServerType.Sasjs
|
||||
))
|
||||
}
|
||||
const access_token = await this.getAccessTokenForRequest(authConfig)
|
||||
|
||||
let parsedSasjsServerLog = ''
|
||||
|
||||
|
||||
@@ -6,6 +6,10 @@ import { RequestClient } from './request/RequestClient'
|
||||
|
||||
const MAX_SESSION_COUNT = 1
|
||||
|
||||
interface ApiErrorResponse {
|
||||
response: { status: number | string; data: { message: string } }
|
||||
}
|
||||
|
||||
export class SessionManager {
|
||||
private loggedErrors: NoSessionStateError[] = []
|
||||
|
||||
@@ -17,8 +21,10 @@ export class SessionManager {
|
||||
if (serverUrl) isUrl(serverUrl)
|
||||
}
|
||||
|
||||
// INFO: session pool
|
||||
private sessions: Session[] = []
|
||||
private currentContext: Context | null = null
|
||||
private settingContext: boolean = false
|
||||
private _debug: boolean = false
|
||||
private printedSessionState = {
|
||||
printed: false,
|
||||
@@ -33,71 +39,230 @@ export class SessionManager {
|
||||
this._debug = value
|
||||
}
|
||||
|
||||
async getSession(accessToken?: string) {
|
||||
await this.createSessions(accessToken)
|
||||
await this.createAndWaitForSession(accessToken)
|
||||
const session = this.sessions.pop()
|
||||
/**
|
||||
* Checks if session is valid. Session is considered valid if time since it's creation is less than 'sessionInactiveTimeout' attribute.
|
||||
* @param session - session object.
|
||||
* @returns - boolean indicating if session is valid.
|
||||
*/
|
||||
private isSessionValid(session: Session): boolean {
|
||||
if (!session) return false
|
||||
|
||||
const secondsSinceSessionCreation =
|
||||
(new Date().getTime() - new Date(session!.creationTimeStamp).getTime()) /
|
||||
(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 false
|
||||
} else {
|
||||
return true
|
||||
}
|
||||
|
||||
return session
|
||||
}
|
||||
|
||||
async clearSession(id: string, accessToken?: string) {
|
||||
/**
|
||||
* Removes session from pool of hot sessions.
|
||||
* @param session - session object.
|
||||
* @returns - void.
|
||||
*/
|
||||
private removeSessionFromPool(session: Session): void {
|
||||
this.sessions = this.sessions.filter((ses) => ses.id !== session.id)
|
||||
}
|
||||
|
||||
/**
|
||||
* Filters session pool to keep only valid sessions.
|
||||
* @param session - session object.
|
||||
* @returns - void.
|
||||
*/
|
||||
private removeExpiredSessions(): void {
|
||||
this.sessions = this.sessions.filter((session) =>
|
||||
this.isSessionValid(session)
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Throws set of errors as a single error.
|
||||
* @param errors - array of errors or string.
|
||||
* @param prefix - an optional final error prefix.
|
||||
* @returns - never.
|
||||
*/
|
||||
private throwErrors(errors: (Error | string)[], prefix?: string): never {
|
||||
throw prefix
|
||||
? prefixMessage(new Error(errors.join('. ')), prefix)
|
||||
: new Error(
|
||||
errors
|
||||
.map((err) =>
|
||||
(err as Error).message ? (err as Error).message : err
|
||||
)
|
||||
.join('. ')
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns session.
|
||||
* If there is a hot session available, it will be returned immediately and an asynchronous request to create new hot session will be submitted.
|
||||
* If there is no available session, 2 session creation requests will be submitted. The session is returned once it is created and ready.
|
||||
* @param accessToken - an optional access token.
|
||||
* @returns - a promise which resolves with a session.
|
||||
*/
|
||||
async getSession(accessToken?: string) {
|
||||
const errors: (Error | string)[] = []
|
||||
let isErrorThrown = false
|
||||
|
||||
const throwIfError = () => {
|
||||
if (errors.length && !isErrorThrown) {
|
||||
isErrorThrown = true
|
||||
|
||||
this.throwErrors(errors)
|
||||
}
|
||||
}
|
||||
|
||||
this.removeExpiredSessions()
|
||||
|
||||
if (this.sessions.length) {
|
||||
const session = this.sessions[0]
|
||||
|
||||
this.removeSessionFromPool(session)
|
||||
|
||||
this.createSessions(accessToken).catch((err) => {
|
||||
errors.push(err)
|
||||
})
|
||||
|
||||
this.createAndWaitForSession(accessToken).catch((err) => {
|
||||
errors.push(err)
|
||||
})
|
||||
|
||||
throwIfError()
|
||||
|
||||
return session
|
||||
} else {
|
||||
this.createSessions(accessToken).catch((err) => {
|
||||
errors.push(err)
|
||||
})
|
||||
|
||||
await this.createAndWaitForSession(accessToken).catch((err) => {
|
||||
errors.push(err)
|
||||
})
|
||||
|
||||
this.removeExpiredSessions()
|
||||
|
||||
const session = this.sessions.pop()!
|
||||
|
||||
this.removeSessionFromPool(session)
|
||||
|
||||
throwIfError()
|
||||
|
||||
return session
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns error message based on the response from SAS API.
|
||||
* @param err - an optional access token.
|
||||
* @param accessToken - an optional access token.
|
||||
* @returns - an error message.
|
||||
*/
|
||||
private getErrorMessage(
|
||||
err: ApiErrorResponse,
|
||||
url: string,
|
||||
method: 'GET' | 'POST' | 'DELETE'
|
||||
) {
|
||||
return (
|
||||
`${method} request to ${url} failed with status code ${
|
||||
err.response.status || 'unknown'
|
||||
}. ` + err.response.data.message || ''
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes session.
|
||||
* @param id - a session id.
|
||||
* @param accessToken - an optional access token.
|
||||
* @returns - a promise which resolves when session is deleted.
|
||||
*/
|
||||
async clearSession(id: string, accessToken?: string): Promise<void> {
|
||||
const url = `/compute/sessions/${id}`
|
||||
|
||||
return await this.requestClient
|
||||
.delete<Session>(`/compute/sessions/${id}`, accessToken)
|
||||
.delete<Session>(url, accessToken)
|
||||
.then(() => {
|
||||
this.sessions = this.sessions.filter((s) => s.id !== id)
|
||||
})
|
||||
.catch((err) => {
|
||||
throw prefixMessage(err, 'Error while deleting session. ')
|
||||
.catch((err: ApiErrorResponse) => {
|
||||
throw prefixMessage(
|
||||
this.getErrorMessage(err, url, 'DELETE'),
|
||||
'Error while deleting session. '
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
private async createSessions(accessToken?: string) {
|
||||
/**
|
||||
* Creates sessions in amount equal to MAX_SESSION_COUNT.
|
||||
* @param accessToken - an optional access token.
|
||||
* @returns - a promise which resolves when required amount of sessions is created.
|
||||
*/
|
||||
private async createSessions(accessToken?: string): Promise<void> {
|
||||
const errors: (Error | string)[] = []
|
||||
|
||||
if (!this.sessions.length) {
|
||||
if (!this.currentContext) {
|
||||
await this.setCurrentContext(accessToken).catch((err) => {
|
||||
throw err
|
||||
})
|
||||
}
|
||||
|
||||
await asyncForEach(new Array(MAX_SESSION_COUNT), async () => {
|
||||
const createdSession = await this.createAndWaitForSession(
|
||||
accessToken
|
||||
).catch((err) => {
|
||||
throw err
|
||||
await this.createAndWaitForSession(accessToken).catch((err) => {
|
||||
errors.push(err)
|
||||
})
|
||||
|
||||
this.sessions.push(createdSession)
|
||||
}).catch((err) => {
|
||||
throw err
|
||||
})
|
||||
}
|
||||
|
||||
if (errors.length) {
|
||||
this.throwErrors(errors, 'Error while creating session. ')
|
||||
}
|
||||
}
|
||||
|
||||
private async createAndWaitForSession(accessToken?: string) {
|
||||
/**
|
||||
* Waits for the current context to be set.
|
||||
* @returns - a promise which resolves when current context is set.
|
||||
*/
|
||||
private async waitForCurrentContext(): Promise<void> {
|
||||
return new Promise((resolve) => {
|
||||
const timer = setInterval(() => {
|
||||
if (this.currentContext) {
|
||||
this.settingContext = false
|
||||
|
||||
clearInterval(timer)
|
||||
|
||||
resolve()
|
||||
}
|
||||
}, 100)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates and waits for session to be ready.
|
||||
* @param accessToken - an optional access token.
|
||||
* @returns - a promise which resolves with a session.
|
||||
*/
|
||||
private async createAndWaitForSession(
|
||||
accessToken?: string
|
||||
): Promise<Session> {
|
||||
if (!this.currentContext) {
|
||||
if (!this.settingContext) {
|
||||
await this.setCurrentContext(accessToken)
|
||||
} else {
|
||||
await this.waitForCurrentContext()
|
||||
}
|
||||
}
|
||||
|
||||
const url = `${this.serverUrl}/compute/contexts/${
|
||||
this.currentContext!.id
|
||||
}/sessions`
|
||||
|
||||
const { result: createdSession, etag } = await this.requestClient
|
||||
.post<Session>(
|
||||
`${this.serverUrl}/compute/contexts/${
|
||||
this.currentContext!.id
|
||||
}/sessions`,
|
||||
{},
|
||||
accessToken
|
||||
)
|
||||
.catch((err) => {
|
||||
throw err
|
||||
.post<Session>(url, {}, accessToken)
|
||||
.catch((err: ApiErrorResponse) => {
|
||||
throw prefixMessage(
|
||||
this.getErrorMessage(err, url, 'POST'),
|
||||
`Error while creating session. `
|
||||
)
|
||||
})
|
||||
|
||||
await this.waitForSession(createdSession, etag, accessToken)
|
||||
@@ -107,14 +272,26 @@ export class SessionManager {
|
||||
return createdSession
|
||||
}
|
||||
|
||||
private async setCurrentContext(accessToken?: string) {
|
||||
/**
|
||||
* Sets current context.
|
||||
* @param accessToken - an optional access token.
|
||||
* @returns - a promise which resolves when current context is set.
|
||||
*/
|
||||
private async setCurrentContext(accessToken?: string): Promise<void> {
|
||||
if (!this.currentContext) {
|
||||
const url = `${this.serverUrl}/compute/contexts?limit=10000`
|
||||
|
||||
this.settingContext = true
|
||||
|
||||
const { result: contexts } = await this.requestClient
|
||||
.get<{
|
||||
items: Context[]
|
||||
}>(`${this.serverUrl}/compute/contexts?limit=10000`, accessToken)
|
||||
.catch((err) => {
|
||||
throw err
|
||||
}>(url, accessToken)
|
||||
.catch((err: ApiErrorResponse) => {
|
||||
throw prefixMessage(
|
||||
this.getErrorMessage(err, url, 'GET'),
|
||||
`Error while getting list of contexts. `
|
||||
)
|
||||
})
|
||||
|
||||
const contextsList =
|
||||
@@ -138,18 +315,13 @@ export class SessionManager {
|
||||
}
|
||||
}
|
||||
|
||||
private getHeaders(accessToken?: string) {
|
||||
const headers: any = {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
|
||||
if (accessToken) {
|
||||
headers.Authorization = `Bearer ${accessToken}`
|
||||
}
|
||||
|
||||
return headers
|
||||
}
|
||||
|
||||
/**
|
||||
* Waits for session to be ready.
|
||||
* @param session - a session object.
|
||||
* @param etag - an etag that can be a string or null.
|
||||
* @param accessToken - an optional access token.
|
||||
* @returns - a promise which resolves with a session state.
|
||||
*/
|
||||
private async waitForSession(
|
||||
session: Session,
|
||||
etag: string | null,
|
||||
@@ -173,13 +345,11 @@ export class SessionManager {
|
||||
this.printedSessionState.printed = true
|
||||
}
|
||||
|
||||
const url = `${this.serverUrl}${stateLink.href}?wait=30`
|
||||
|
||||
const { result: state, responseStatus: responseStatus } =
|
||||
await this.getSessionState(
|
||||
`${this.serverUrl}${stateLink.href}?wait=30`,
|
||||
etag!,
|
||||
accessToken
|
||||
).catch((err) => {
|
||||
throw prefixMessage(err, 'Error while getting session state.')
|
||||
await this.getSessionState(url, etag!, accessToken).catch((err) => {
|
||||
throw prefixMessage(err, 'Error while waiting for session. ')
|
||||
})
|
||||
|
||||
sessionState = state.trim()
|
||||
@@ -216,7 +386,7 @@ export class SessionManager {
|
||||
|
||||
return sessionState
|
||||
} else {
|
||||
throw 'Error while getting session state link.'
|
||||
throw 'Error while getting session state link. '
|
||||
}
|
||||
} else {
|
||||
this.loggedErrors = []
|
||||
@@ -225,11 +395,21 @@ export class SessionManager {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets session state.
|
||||
* @param url - a URL to get session state.
|
||||
* @param etag - an etag string.
|
||||
* @param accessToken - an optional access token.
|
||||
* @returns - a promise which resolves with a result string and response status.
|
||||
*/
|
||||
private async getSessionState(
|
||||
url: string,
|
||||
etag: string,
|
||||
accessToken?: string
|
||||
) {
|
||||
): Promise<{
|
||||
result: string
|
||||
responseStatus: number
|
||||
}> {
|
||||
return await this.requestClient
|
||||
.get(url, accessToken, 'text/plain', { 'If-None-Match': etag })
|
||||
.then((res) => ({
|
||||
@@ -237,20 +417,37 @@ export class SessionManager {
|
||||
responseStatus: res.status
|
||||
}))
|
||||
.catch((err) => {
|
||||
throw err
|
||||
throw prefixMessage(
|
||||
this.getErrorMessage(err, url, 'GET'),
|
||||
'Error while getting session state. '
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
async getVariable(sessionId: string, variable: string, accessToken?: string) {
|
||||
/**
|
||||
* Gets variable.
|
||||
* @param sessionId - a session id.
|
||||
* @param variable - a variable string.
|
||||
* @param accessToken - an optional access token.
|
||||
* @returns - a promise which resolves with a result that confirms to SessionVariable interface, etag string and status code.
|
||||
*/
|
||||
async getVariable(
|
||||
sessionId: string,
|
||||
variable: string,
|
||||
accessToken?: string
|
||||
): Promise<{
|
||||
result: SessionVariable
|
||||
etag: string
|
||||
status: number
|
||||
}> {
|
||||
const url = `${this.serverUrl}/compute/sessions/${sessionId}/variables/${variable}`
|
||||
|
||||
return await this.requestClient
|
||||
.get<SessionVariable>(
|
||||
`${this.serverUrl}/compute/sessions/${sessionId}/variables/${variable}`,
|
||||
accessToken
|
||||
)
|
||||
.get<SessionVariable>(url, accessToken)
|
||||
.catch((err) => {
|
||||
throw prefixMessage(
|
||||
err,
|
||||
`Error while fetching session variable '${variable}'.`
|
||||
this.getErrorMessage(err, url, 'GET'),
|
||||
`Error while fetching session variable '${variable}'. `
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,16 +1,19 @@
|
||||
import { ServerType } from '@sasjs/utils/types'
|
||||
import { RequestClient } from '../request/RequestClient'
|
||||
import { LoginOptions, LoginResult } from '../types/Login'
|
||||
import { NotFoundError } from '../types/errors'
|
||||
import { LoginOptions, LoginResult, LoginResultInternal } from '../types/Login'
|
||||
import { serialize } from '../utils'
|
||||
import { extractUserLongNameSas9 } from '../utils/sas9/extractUserLongNameSas9'
|
||||
import { openWebPage } from './openWebPage'
|
||||
import { verifySas9Login } from './verifySas9Login'
|
||||
import { verifySasViyaLogin } from './verifySasViyaLogin'
|
||||
|
||||
export class AuthManager {
|
||||
public userName = ''
|
||||
public userLongName = ''
|
||||
private loginUrl: string
|
||||
private logoutUrl: string
|
||||
private redirectedLoginUrl = `/SASLogon/home`
|
||||
private redirectedLoginUrl = `/SASLogon` //SAS 9 M8 no longer redirects from `/SASLogon/home` to the login page. `/SASLogon` seems to be stable enough across SAS versions
|
||||
constructor(
|
||||
private serverUrl: string,
|
||||
private serverType: ServerType,
|
||||
@@ -33,15 +36,22 @@ export class AuthManager {
|
||||
public async redirectedLogIn({
|
||||
onLoggedOut
|
||||
}: LoginOptions): Promise<LoginResult> {
|
||||
const { isLoggedIn: isLoggedInAlready, userName: currentSessionUsername } =
|
||||
await this.fetchUserName()
|
||||
const {
|
||||
isLoggedIn: isLoggedInAlready,
|
||||
userName: currentSessionUserName,
|
||||
userLongName: currentSessionUserLongName
|
||||
} = await this.fetchUserName()
|
||||
|
||||
if (isLoggedInAlready) {
|
||||
const logger = process.logger || console
|
||||
logger.log('login was not attempted as a valid session already exists')
|
||||
|
||||
await this.loginCallback()
|
||||
|
||||
return {
|
||||
isLoggedIn: true,
|
||||
userName: currentSessionUsername
|
||||
userName: currentSessionUserName,
|
||||
userLongName: currentSessionUserLongName
|
||||
}
|
||||
}
|
||||
|
||||
@@ -56,7 +66,7 @@ export class AuthManager {
|
||||
)
|
||||
|
||||
if (!loginPopup) {
|
||||
return { isLoggedIn: false, userName: '' }
|
||||
return { isLoggedIn: false, userName: '', userLongName: '' }
|
||||
}
|
||||
|
||||
const { isLoggedIn } =
|
||||
@@ -71,14 +81,14 @@ export class AuthManager {
|
||||
await this.performCASSecurityCheck()
|
||||
}
|
||||
|
||||
const { userName } = await this.fetchUserName()
|
||||
const { userName, userLongName } = await this.fetchUserName()
|
||||
|
||||
await this.loginCallback()
|
||||
|
||||
return { isLoggedIn: true, userName }
|
||||
return { isLoggedIn: true, userName, userLongName }
|
||||
}
|
||||
|
||||
return { isLoggedIn: false, userName: '' }
|
||||
return { isLoggedIn: false, userName: '', userLongName: '' }
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -93,27 +103,29 @@ export class AuthManager {
|
||||
username,
|
||||
password
|
||||
}
|
||||
this.userName = ''
|
||||
this.userLongName = ''
|
||||
|
||||
let {
|
||||
isLoggedIn: isLoggedInAlready,
|
||||
loginForm,
|
||||
userName: currentSessionUsername
|
||||
userLongName: currentSessionUserLongName
|
||||
} = await this.checkSession()
|
||||
|
||||
if (isLoggedInAlready) {
|
||||
if (currentSessionUsername === loginParams.username) {
|
||||
await this.loginCallback()
|
||||
const logger = process.logger || console
|
||||
logger.log('login was not attempted as a valid session already exists')
|
||||
|
||||
this.userName = currentSessionUsername!
|
||||
return {
|
||||
isLoggedIn: true,
|
||||
userName: this.userName
|
||||
}
|
||||
} else {
|
||||
await this.logOut()
|
||||
loginForm = await this.getNewLoginForm()
|
||||
await this.loginCallback()
|
||||
|
||||
this.userName = loginParams.username
|
||||
this.userLongName = currentSessionUserLongName
|
||||
return {
|
||||
isLoggedIn: true,
|
||||
userName: this.userName,
|
||||
userLongName: this.userLongName
|
||||
}
|
||||
} else this.userName = ''
|
||||
}
|
||||
|
||||
let loginResponse = await this.sendLoginRequest(loginForm, loginParams)
|
||||
|
||||
@@ -126,12 +138,13 @@ export class AuthManager {
|
||||
loginResponse = await this.sendLoginRequest(newLoginForm, loginParams)
|
||||
}
|
||||
|
||||
// Sometimes due to redirection on SAS9 and SASViya we don't get the login response that says
|
||||
// You have signed in. Therefore, we have to make an extra request for checking session to
|
||||
// ensure either user is logged in or not.
|
||||
|
||||
const res = await this.checkSession()
|
||||
isLoggedIn = res.isLoggedIn
|
||||
|
||||
if (isLoggedIn) this.userName = res.userName
|
||||
} else {
|
||||
this.userName = loginParams.username
|
||||
this.userLongName = res.userLongName
|
||||
}
|
||||
|
||||
if (isLoggedIn) {
|
||||
@@ -140,21 +153,25 @@ export class AuthManager {
|
||||
}
|
||||
|
||||
this.loginCallback()
|
||||
} else this.userName = ''
|
||||
this.userName = loginParams.username
|
||||
}
|
||||
|
||||
return {
|
||||
isLoggedIn,
|
||||
userName: this.userName
|
||||
userName: this.userName,
|
||||
userLongName: this.userLongName
|
||||
}
|
||||
}
|
||||
|
||||
private async performCASSecurityCheck() {
|
||||
const casAuthenticationUrl = `${this.serverUrl}/SASStoredProcess/j_spring_cas_security_check`
|
||||
|
||||
await this.requestClient.get<string>(
|
||||
`/SASLogon/login?service=${casAuthenticationUrl}`,
|
||||
undefined
|
||||
)
|
||||
await this.requestClient
|
||||
.get<string>(`/SASLogon/login?service=${casAuthenticationUrl}`, undefined)
|
||||
.catch((err) => {
|
||||
// ignore if resource not found error
|
||||
if (!(err instanceof NotFoundError)) throw err
|
||||
})
|
||||
}
|
||||
|
||||
private async sendLoginRequest(
|
||||
@@ -195,15 +212,12 @@ export class AuthManager {
|
||||
* Checks whether a session is active, or login is required.
|
||||
* @returns - a promise which resolves with an object containing three values
|
||||
* - a boolean `isLoggedIn`
|
||||
* - a string `userName` and
|
||||
* - a string `userName`,
|
||||
* - a string `userFullName` and
|
||||
* - a form `loginForm` if not loggedin.
|
||||
*/
|
||||
public async checkSession(): Promise<{
|
||||
isLoggedIn: boolean
|
||||
userName: string
|
||||
loginForm?: any
|
||||
}> {
|
||||
const { isLoggedIn, userName } = await this.fetchUserName()
|
||||
public async checkSession(): Promise<LoginResultInternal> {
|
||||
const { isLoggedIn, userName, userLongName } = await this.fetchUserName()
|
||||
let loginForm = null
|
||||
|
||||
if (!isLoggedIn) {
|
||||
@@ -216,7 +230,8 @@ export class AuthManager {
|
||||
|
||||
return Promise.resolve({
|
||||
isLoggedIn,
|
||||
userName: userName.toLowerCase(),
|
||||
userName,
|
||||
userLongName,
|
||||
loginForm
|
||||
})
|
||||
}
|
||||
@@ -237,7 +252,7 @@ export class AuthManager {
|
||||
}
|
||||
|
||||
const { result: formResponse } = await this.requestClient.get<string>(
|
||||
this.loginUrl.replace('.do', ''),
|
||||
this.loginUrl.replace('/SASLogon/login.do', '/SASLogon/login'),
|
||||
undefined,
|
||||
'text/plain'
|
||||
)
|
||||
@@ -245,10 +260,7 @@ export class AuthManager {
|
||||
return await this.getLoginForm(formResponse)
|
||||
}
|
||||
|
||||
private async fetchUserName(): Promise<{
|
||||
isLoggedIn: boolean
|
||||
userName: string
|
||||
}> {
|
||||
private async fetchUserName(): Promise<LoginResult> {
|
||||
const url =
|
||||
this.serverType === ServerType.SasViya
|
||||
? `${this.serverUrl}/identities/users/@currentUser`
|
||||
@@ -263,15 +275,19 @@ export class AuthManager {
|
||||
})
|
||||
|
||||
const isLoggedIn = loginResponse !== 'authErr'
|
||||
const userName = isLoggedIn ? this.extractUserName(loginResponse) : ''
|
||||
|
||||
if (!isLoggedIn) {
|
||||
//We will logout to make sure cookies are removed and login form is presented
|
||||
//Residue can happen in case of session expiration
|
||||
await this.logOut()
|
||||
return { isLoggedIn, userName: '', userLongName: '' }
|
||||
}
|
||||
|
||||
return { isLoggedIn, userName }
|
||||
return {
|
||||
isLoggedIn,
|
||||
userName: this.extractUserName(loginResponse),
|
||||
userLongName: this.extractUserLongName(loginResponse)
|
||||
}
|
||||
}
|
||||
|
||||
private extractUserName = (response: any): string => {
|
||||
@@ -280,15 +296,7 @@ export class AuthManager {
|
||||
return response?.id
|
||||
|
||||
case ServerType.Sas9:
|
||||
const matched = response?.match(/"title":"Log Off [0-1a-zA-Z ]*"/)
|
||||
const username = matched?.[0].slice(17, -1)
|
||||
|
||||
if (!username.includes(' ')) return username
|
||||
|
||||
return username
|
||||
.split(' ')
|
||||
.map((name: string) => name.slice(0, 3).toLowerCase())
|
||||
.join('')
|
||||
return ''
|
||||
|
||||
case ServerType.Sasjs:
|
||||
return response?.username
|
||||
@@ -299,13 +307,31 @@ export class AuthManager {
|
||||
}
|
||||
}
|
||||
|
||||
private extractUserLongName = (response: any): string => {
|
||||
switch (this.serverType) {
|
||||
case ServerType.SasViya:
|
||||
return response?.name
|
||||
|
||||
case ServerType.Sas9:
|
||||
return extractUserLongNameSas9(response)
|
||||
|
||||
case ServerType.Sasjs:
|
||||
return response?.displayName
|
||||
|
||||
default:
|
||||
console.error('Server Type not found in extractUserName function')
|
||||
return ''
|
||||
}
|
||||
}
|
||||
|
||||
private getLoginForm(response: any) {
|
||||
const pattern: RegExp = /<form.+action="(.*Logon[^"]*).*>/
|
||||
const pattern: RegExp = /<form.+action="(.*(Logon|login)[^"]*).*>/
|
||||
const matches = pattern.exec(response)
|
||||
const formInputs: any = {}
|
||||
|
||||
if (matches && matches.length) {
|
||||
this.setLoginUrl(matches)
|
||||
response = response.replace(/<input/g, '\n<input')
|
||||
const inputs = response.match(/<input.*"hidden"[^>]*>/g)
|
||||
|
||||
if (inputs) {
|
||||
@@ -336,7 +362,7 @@ export class AuthManager {
|
||||
this.loginUrl =
|
||||
this.serverType === ServerType.SasViya
|
||||
? tempLoginLink
|
||||
: loginUrl.replace('.do', '')
|
||||
: loginUrl.replace('/SASLogon/login.do', '/SASLogon/login')
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ import { refreshTokensForSasjs } from './refreshTokensForSasjs'
|
||||
|
||||
/**
|
||||
* Returns the auth configuration, refreshing the tokens if necessary.
|
||||
* This function can only be used by Node, if a server type is SASVIYA.
|
||||
* @param requestClient - the pre-configured HTTP request client
|
||||
* @param authConfig - an object containing a client ID, secret, access token and refresh token
|
||||
* @param serverType - server type for which refreshing the tokens, defaults to SASVIYA
|
||||
@@ -29,9 +30,12 @@ export async function getTokens(
|
||||
const error =
|
||||
'Unable to obtain new access token. Your refresh token has expired.'
|
||||
logger.error(error)
|
||||
|
||||
throw new Error(error)
|
||||
}
|
||||
|
||||
logger.info('Refreshing access and refresh tokens.')
|
||||
|
||||
const tokens =
|
||||
serverType === ServerType.SasViya
|
||||
? await refreshTokensForViya(
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
export const isLogInRequired = (response: string): boolean => {
|
||||
const pattern: RegExp = /<form.+action="(.*Logon[^"]*).*>/gm
|
||||
const pattern: RegExp = /<form.+action="(.*(Logon)|(login)[^"]*).*>/gm
|
||||
const matches = pattern.test(response)
|
||||
return matches
|
||||
}
|
||||
|
||||
@@ -2,9 +2,11 @@ import { SasAuthResponse } from '@sasjs/utils/types'
|
||||
import { prefixMessage } from '@sasjs/utils/error'
|
||||
import * as NodeFormData from 'form-data'
|
||||
import { RequestClient } from '../request/RequestClient'
|
||||
import { isNode } from '../utils'
|
||||
|
||||
/**
|
||||
* Exchanges the refresh token for an access token for the given client.
|
||||
* This function can only be used by Node.
|
||||
* @param requestClient - the pre-configured HTTP request client
|
||||
* @param clientId - the client ID to authenticate with.
|
||||
* @param clientSecret - the client secret to authenticate with.
|
||||
@@ -16,9 +18,12 @@ export async function refreshTokensForViya(
|
||||
clientSecret: string,
|
||||
refreshToken: string
|
||||
) {
|
||||
if (!isNode()) {
|
||||
throw new Error(`Method 'refreshTokensForViya' can only be used by Node.`)
|
||||
}
|
||||
|
||||
const url = '/SASLogon/oauth/token'
|
||||
let token
|
||||
token =
|
||||
const token =
|
||||
typeof Buffer === 'undefined'
|
||||
? btoa(clientId + ':' + clientSecret)
|
||||
: Buffer.from(clientId + ':' + clientSecret).toString('base64')
|
||||
@@ -27,8 +32,7 @@ export async function refreshTokensForViya(
|
||||
Authorization: 'Basic ' + token
|
||||
}
|
||||
|
||||
const formData =
|
||||
typeof FormData === 'undefined' ? new NodeFormData() : new FormData()
|
||||
const formData = new NodeFormData()
|
||||
formData.append('grant_type', 'refresh_token')
|
||||
formData.append('refresh_token', refreshToken)
|
||||
|
||||
|
||||
@@ -20,6 +20,7 @@ describe('AuthManager', () => {
|
||||
const serverUrl = 'http://test-server.com'
|
||||
const serverType = ServerType.SasViya
|
||||
const userName = 'test-username'
|
||||
const userLongName = 'test-user long name'
|
||||
const password = 'test-password'
|
||||
const requestClient = new RequestClient(serverUrl)
|
||||
|
||||
@@ -73,6 +74,7 @@ describe('AuthManager', () => {
|
||||
Promise.resolve({
|
||||
isLoggedIn: true,
|
||||
userName,
|
||||
userLongName,
|
||||
loginForm: 'test'
|
||||
})
|
||||
)
|
||||
@@ -95,49 +97,24 @@ describe('AuthManager', () => {
|
||||
Promise.resolve({
|
||||
isLoggedIn: true,
|
||||
userName: 'someOtherUsername',
|
||||
userLongName: 'someOtherUser Long name',
|
||||
loginForm: null
|
||||
})
|
||||
)
|
||||
jest
|
||||
.spyOn(authManager, 'logOut')
|
||||
.mockImplementation(() => Promise.resolve(true))
|
||||
jest.spyOn(authManager, 'logOut')
|
||||
|
||||
jest
|
||||
.spyOn<any, any>(authManager, 'getNewLoginForm')
|
||||
.mockImplementation(() =>
|
||||
Promise.resolve({
|
||||
name: 'test'
|
||||
})
|
||||
)
|
||||
mockedAxios.post.mockImplementation(() =>
|
||||
Promise.resolve({ data: mockLoginSuccessResponse })
|
||||
)
|
||||
jest.spyOn<any, any>(authManager, 'getNewLoginForm')
|
||||
jest.spyOn<any, any>(authManager, 'sendLoginRequest')
|
||||
|
||||
const loginResponse = await authManager.logIn(userName, password)
|
||||
|
||||
expect(loginResponse.isLoggedIn).toBeTruthy()
|
||||
expect(loginResponse.userName).toEqual(userName)
|
||||
|
||||
const loginParams = serialize({
|
||||
_service: 'default',
|
||||
username: userName,
|
||||
password,
|
||||
name: 'test'
|
||||
})
|
||||
expect(authCallback).toHaveBeenCalledTimes(1)
|
||||
expect(authManager.logOut).toHaveBeenCalledTimes(1)
|
||||
expect(authManager['getNewLoginForm']).toHaveBeenCalledTimes(1)
|
||||
expect(mockedAxios.post).toHaveBeenCalledWith(
|
||||
`/SASLogon/login`,
|
||||
loginParams,
|
||||
{
|
||||
withCredentials: true,
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
Accept: '*/*'
|
||||
}
|
||||
}
|
||||
)
|
||||
expect(authManager.logOut).toHaveBeenCalledTimes(0)
|
||||
expect(authManager['getNewLoginForm']).toHaveBeenCalledTimes(0)
|
||||
expect(authManager['sendLoginRequest']).toHaveBeenCalledTimes(0)
|
||||
expect(authCallback).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
@@ -152,6 +129,7 @@ describe('AuthManager', () => {
|
||||
Promise.resolve({
|
||||
isLoggedIn: false,
|
||||
userName: '',
|
||||
userLongName: '',
|
||||
loginForm: { name: 'test' }
|
||||
})
|
||||
)
|
||||
@@ -196,6 +174,7 @@ describe('AuthManager', () => {
|
||||
Promise.resolve({
|
||||
isLoggedIn: false,
|
||||
userName: '',
|
||||
userLongName: '',
|
||||
loginForm: { name: 'test' }
|
||||
})
|
||||
)
|
||||
@@ -245,6 +224,7 @@ describe('AuthManager', () => {
|
||||
Promise.resolve({
|
||||
isLoggedIn: false,
|
||||
userName: '',
|
||||
userLongName: '',
|
||||
loginForm: { name: 'test' }
|
||||
})
|
||||
)
|
||||
@@ -290,6 +270,7 @@ describe('AuthManager', () => {
|
||||
Promise.resolve({
|
||||
isLoggedIn: false,
|
||||
userName: 'test',
|
||||
userLongName: 'test Long name',
|
||||
loginForm: { name: 'test' }
|
||||
})
|
||||
)
|
||||
@@ -384,7 +365,7 @@ describe('AuthManager', () => {
|
||||
expect(loginResponse.userName).toEqual(userName)
|
||||
|
||||
expect(openWebPageModule.openWebPage).toHaveBeenCalledWith(
|
||||
`/SASLogon/home`,
|
||||
`/SASLogon`,
|
||||
'SASLogon',
|
||||
{
|
||||
width: 500,
|
||||
@@ -428,7 +409,7 @@ describe('AuthManager', () => {
|
||||
expect(loginResponse.userName).toEqual(userName)
|
||||
|
||||
expect(openWebPageModule.openWebPage).toHaveBeenCalledWith(
|
||||
`/SASLogon/home`,
|
||||
`/SASLogon`,
|
||||
'SASLogon',
|
||||
{
|
||||
width: 500,
|
||||
@@ -472,7 +453,7 @@ describe('AuthManager', () => {
|
||||
expect(loginResponse.userName).toEqual('')
|
||||
|
||||
expect(openWebPageModule.openWebPage).toHaveBeenCalledWith(
|
||||
`/SASLogon/home`,
|
||||
`/SASLogon`,
|
||||
'SASLogon',
|
||||
{
|
||||
width: 500,
|
||||
@@ -516,7 +497,7 @@ describe('AuthManager', () => {
|
||||
expect(loginResponse.userName).toEqual('')
|
||||
|
||||
expect(openWebPageModule.openWebPage).toHaveBeenCalledWith(
|
||||
`/SASLogon/home`,
|
||||
`/SASLogon`,
|
||||
'SASLogon',
|
||||
{
|
||||
width: 500,
|
||||
@@ -560,43 +541,8 @@ describe('AuthManager', () => {
|
||||
)
|
||||
})
|
||||
|
||||
it('return session information when logged in - SAS9', async () => {
|
||||
// username cannot have `-` and cannot be uppercased
|
||||
const username = 'testusername'
|
||||
const serverType = ServerType.Sas9
|
||||
const authManager = new AuthManager(
|
||||
serverUrl,
|
||||
serverType,
|
||||
requestClient,
|
||||
authCallback
|
||||
)
|
||||
mockedAxios.get.mockImplementation(() =>
|
||||
Promise.resolve({
|
||||
data: `"title":"Log Off ${username}","url":"javascript: clearFrame(\"/SASStoredProcess/do?_action=logoff\")"' })`
|
||||
})
|
||||
)
|
||||
|
||||
const response = await authManager.checkSession()
|
||||
expect(response.isLoggedIn).toBeTruthy()
|
||||
expect(response.userName).toEqual(username)
|
||||
expect(mockedAxios.get).toHaveBeenNthCalledWith(
|
||||
1,
|
||||
`http://test-server.com/SASStoredProcess`,
|
||||
{
|
||||
withCredentials: true,
|
||||
responseType: 'text',
|
||||
transformResponse: undefined,
|
||||
headers: {
|
||||
Accept: '*/*',
|
||||
'Content-Type': 'text/plain'
|
||||
}
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
it('return session information when logged in - SAS9 - having full name in html', async () => {
|
||||
const fullname = 'FirstName LastName'
|
||||
const username = 'firlas'
|
||||
const serverType = ServerType.Sas9
|
||||
const authManager = new AuthManager(
|
||||
serverUrl,
|
||||
@@ -612,7 +558,8 @@ describe('AuthManager', () => {
|
||||
|
||||
const response = await authManager.checkSession()
|
||||
expect(response.isLoggedIn).toBeTruthy()
|
||||
expect(response.userName).toEqual(username)
|
||||
expect(response.userName).toEqual('')
|
||||
expect(response.userLongName).toEqual(fullname)
|
||||
expect(mockedAxios.get).toHaveBeenNthCalledWith(
|
||||
1,
|
||||
`http://test-server.com/SASStoredProcess`,
|
||||
|
||||
@@ -3,6 +3,7 @@ import * as NodeFormData from 'form-data'
|
||||
import { generateToken, mockAuthResponse } from './mockResponses'
|
||||
import { RequestClient } from '../../request/RequestClient'
|
||||
import { refreshTokensForViya } from '../refreshTokensForViya'
|
||||
import * as IsNodeModule from '../../utils/isNode'
|
||||
|
||||
const requestClient = new (<jest.Mock<RequestClient>>RequestClient)()
|
||||
|
||||
@@ -70,6 +71,18 @@ describe('refreshTokensForViya', () => {
|
||||
|
||||
expect(error).toEqual(`Error while refreshing tokens: ${tokenError}`)
|
||||
})
|
||||
|
||||
it('should throw an error if environment is not Node', async () => {
|
||||
jest.spyOn(IsNodeModule, 'isNode').mockImplementation(() => false)
|
||||
|
||||
const expectedError = new Error(
|
||||
`Method 'refreshTokensForViya' can only be used by Node.`
|
||||
)
|
||||
|
||||
expect(
|
||||
refreshTokensForViya(requestClient, 'client', 'secret', 'token')
|
||||
).rejects.toEqual(expectedError)
|
||||
})
|
||||
})
|
||||
|
||||
const setupMocks = () => {
|
||||
|
||||
@@ -17,7 +17,7 @@ describe('verifySas9Login', () => {
|
||||
it('should return isLoggedIn true by checking state of popup', async () => {
|
||||
const popup = {
|
||||
window: {
|
||||
location: { href: serverUrl + `/SASLogon/home` },
|
||||
location: { href: serverUrl + `/SASLogon` },
|
||||
document: { body: { innerText: '<h3>You have signed in.</h3>' } }
|
||||
}
|
||||
} as unknown as Window
|
||||
|
||||
@@ -18,7 +18,7 @@ describe('verifySasViyaLogin', () => {
|
||||
it('should return isLoggedIn true by checking state of popup', async () => {
|
||||
const popup = {
|
||||
window: {
|
||||
location: { href: serverUrl + `/SASLogon/home` },
|
||||
location: { href: serverUrl + `/SASLogon` },
|
||||
document: { body: { innerText: '<h3>You have signed in.</h3>' } }
|
||||
}
|
||||
} as unknown as Window
|
||||
|
||||
@@ -3,4 +3,6 @@ export * from './types'
|
||||
export * from './types/errors'
|
||||
export * from './SASViyaApiClient'
|
||||
export * from './SAS9ApiClient'
|
||||
export * from './SASjsApiClient'
|
||||
export * from './request/SasjsRequestClient'
|
||||
export default SASjs
|
||||
|
||||
273
src/minified/sas9/SASjs.ts
Normal file
@@ -0,0 +1,273 @@
|
||||
import { validateInput, compareTimestamps } from '../../utils'
|
||||
import { SASjsConfig, UploadFile, LoginMechanism } from '../../types'
|
||||
import { AuthManager } from '../../auth'
|
||||
import {
|
||||
ServerType,
|
||||
AuthConfig,
|
||||
ExtraResponseAttributes
|
||||
} from '@sasjs/utils/types'
|
||||
import { RequestClient } from '../../request/RequestClient'
|
||||
import { FileUploader } from '../../job-execution/FileUploader'
|
||||
import { WebJobExecutor } from './WebJobExecutor'
|
||||
import { ErrorResponse } from '../../types/errors/ErrorResponse'
|
||||
import { LoginOptions, LoginResult } from '../../types/Login'
|
||||
|
||||
const defaultConfig: SASjsConfig = {
|
||||
serverUrl: '',
|
||||
pathSASJS: '/SASjsApi/stp/execute',
|
||||
pathSAS9: '/SASStoredProcess/do',
|
||||
pathSASViya: '/SASJobExecution',
|
||||
appLoc: '/Public/seedapp',
|
||||
serverType: ServerType.Sas9,
|
||||
debug: false,
|
||||
contextName: 'SAS Job Execution compute context',
|
||||
useComputeApi: null,
|
||||
loginMechanism: LoginMechanism.Default
|
||||
}
|
||||
|
||||
/**
|
||||
* SASjs is a JavaScript adapter for SAS.
|
||||
*
|
||||
*/
|
||||
export default class SASjs {
|
||||
private sasjsConfig: SASjsConfig = new SASjsConfig()
|
||||
private jobsPath: string = ''
|
||||
private fileUploader: FileUploader | null = null
|
||||
private authManager: AuthManager | null = null
|
||||
private requestClient: RequestClient | null = null
|
||||
private webJobExecutor: WebJobExecutor | null = null
|
||||
|
||||
constructor(config?: Partial<SASjsConfig>) {
|
||||
this.sasjsConfig = {
|
||||
...defaultConfig,
|
||||
...config
|
||||
}
|
||||
|
||||
this.setupConfiguration()
|
||||
}
|
||||
|
||||
/**
|
||||
* Logs into the SAS server with the supplied credentials.
|
||||
* @param username - a string representing the username.
|
||||
* @param password - a string representing the password.
|
||||
* @param clientId - a string representing the client ID.
|
||||
*/
|
||||
public async logIn(
|
||||
username?: string,
|
||||
password?: string,
|
||||
clientId?: string,
|
||||
options: LoginOptions = {}
|
||||
): Promise<LoginResult> {
|
||||
if (this.sasjsConfig.loginMechanism === LoginMechanism.Default) {
|
||||
if (!username || !password)
|
||||
throw new Error(
|
||||
'A username and password are required when using the default login mechanism.'
|
||||
)
|
||||
|
||||
return this.authManager!.logIn(username, password)
|
||||
}
|
||||
|
||||
if (typeof window === typeof undefined) {
|
||||
throw new Error(
|
||||
'The redirected login mechanism is only available for use in the browser.'
|
||||
)
|
||||
}
|
||||
|
||||
return this.authManager!.redirectedLogIn(options)
|
||||
}
|
||||
|
||||
/**
|
||||
* Logs out of the configured SAS server.
|
||||
*/
|
||||
public logOut() {
|
||||
return this.authManager!.logOut()
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the current SASjs configuration.
|
||||
*
|
||||
*/
|
||||
public getSasjsConfig() {
|
||||
return this.sasjsConfig
|
||||
}
|
||||
|
||||
/**
|
||||
* this method returns an array of SASjsRequest
|
||||
* @returns SASjsRequest[]
|
||||
*/
|
||||
public getSasRequests() {
|
||||
const requests = [...this.requestClient!.getRequests()]
|
||||
const sortedRequests = requests.sort(compareTimestamps)
|
||||
return sortedRequests
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the debug state. Turning this on will enable additional logging in the adapter.
|
||||
* @param value - boolean indicating debug state (on/off).
|
||||
*/
|
||||
public setDebugState(value: boolean) {
|
||||
this.sasjsConfig.debug = value
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 files - array of files to be uploaded, including File object and file name.
|
||||
* @param params - request URL parameters.
|
||||
* @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 - a function that is called if the
|
||||
* user is not logged in (eg to display a login form). The request will be
|
||||
* resubmitted after successful login.
|
||||
*/
|
||||
public async uploadFile(
|
||||
sasJob: string,
|
||||
files: UploadFile[],
|
||||
params: { [key: string]: any } | null,
|
||||
config: { [key: string]: any } = {},
|
||||
loginRequiredCallback?: () => any
|
||||
) {
|
||||
config = {
|
||||
...this.sasjsConfig,
|
||||
...config
|
||||
}
|
||||
const data = { files, params }
|
||||
|
||||
return await this.fileUploader!.execute(
|
||||
sasJob,
|
||||
data,
|
||||
config,
|
||||
loginRequiredCallback
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Makes a request to program specified in `SASjob` (could be a Viya Job, a
|
||||
* SAS 9 Stored Process, or a SASjs Server Stored Program). The response
|
||||
* object will always contain table names in lowercase, and column names in
|
||||
* uppercase. Values are returned formatted by default, unformatted
|
||||
* values can be configured as an option in the `%webout` macro.
|
||||
*
|
||||
* @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. For an example of the table structure, see the project README. This
|
||||
* value can be `null` if no inputs are 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 loginRequiredCallback - a function that is called if the
|
||||
* user is not logged in (eg to display a login form). The request will be
|
||||
* resubmitted after successful login.
|
||||
* When using a `loginRequiredCallback`, the call to the request will look, for example, like so:
|
||||
* `await request(sasJobPath, data, config, () => setIsLoggedIn(false))`
|
||||
* If you are not passing in any data and configuration, it will look like so:
|
||||
* `await request(sasJobPath, {}, {}, () => setIsLoggedIn(false))`
|
||||
* @param extraResponseAttributes - a array of predefined values that are used
|
||||
* to provide extra attributes (same names as those values) to be added in response
|
||||
* Supported values are declared in ExtraResponseAttributes type.
|
||||
*/
|
||||
public async request(
|
||||
sasJob: string,
|
||||
data: { [key: string]: any } | null,
|
||||
config: { [key: string]: any } = {},
|
||||
loginRequiredCallback?: () => any,
|
||||
authConfig?: AuthConfig,
|
||||
extraResponseAttributes: ExtraResponseAttributes[] = []
|
||||
) {
|
||||
config = {
|
||||
...this.sasjsConfig,
|
||||
...config
|
||||
}
|
||||
|
||||
const validationResult = validateInput(data)
|
||||
|
||||
// status is true if the data passes validation checks above
|
||||
if (validationResult.status) {
|
||||
return await this.webJobExecutor!.execute(
|
||||
sasJob,
|
||||
data,
|
||||
config,
|
||||
loginRequiredCallback,
|
||||
authConfig,
|
||||
extraResponseAttributes
|
||||
)
|
||||
} else {
|
||||
return Promise.reject(new ErrorResponse(validationResult.msg))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether a session is active, or login is required.
|
||||
* @returns - a promise which resolves with an object containing two values - a boolean `isLoggedIn`, and a string `userName`.
|
||||
*/
|
||||
public async checkSession() {
|
||||
return this.authManager!.checkSession()
|
||||
}
|
||||
|
||||
private setupConfiguration() {
|
||||
if (
|
||||
this.sasjsConfig.serverUrl === undefined ||
|
||||
this.sasjsConfig.serverUrl === ''
|
||||
) {
|
||||
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 = ''
|
||||
}
|
||||
}
|
||||
|
||||
if (this.sasjsConfig.serverUrl.slice(-1) === '/') {
|
||||
this.sasjsConfig.serverUrl = this.sasjsConfig.serverUrl.slice(0, -1)
|
||||
}
|
||||
|
||||
if (!this.requestClient) {
|
||||
this.requestClient = new RequestClient(
|
||||
this.sasjsConfig.serverUrl,
|
||||
this.sasjsConfig.httpsAgentOptions,
|
||||
this.sasjsConfig.requestHistoryLimit
|
||||
)
|
||||
} else {
|
||||
this.requestClient.setConfig(
|
||||
this.sasjsConfig.serverUrl,
|
||||
this.sasjsConfig.httpsAgentOptions
|
||||
)
|
||||
}
|
||||
|
||||
this.jobsPath = this.sasjsConfig.pathSAS9
|
||||
|
||||
this.authManager = new AuthManager(
|
||||
this.sasjsConfig.serverUrl,
|
||||
this.sasjsConfig.serverType!,
|
||||
this.requestClient,
|
||||
this.resendWaitingRequests
|
||||
)
|
||||
|
||||
this.fileUploader = new FileUploader(
|
||||
this.sasjsConfig.serverUrl,
|
||||
this.sasjsConfig.serverType!,
|
||||
this.jobsPath,
|
||||
this.requestClient
|
||||
)
|
||||
|
||||
this.webJobExecutor = new WebJobExecutor(
|
||||
this.sasjsConfig.serverUrl,
|
||||
this.sasjsConfig.serverType!,
|
||||
this.jobsPath,
|
||||
this.requestClient
|
||||
)
|
||||
}
|
||||
|
||||
private resendWaitingRequests = async () => {
|
||||
await this.webJobExecutor?.resendWaitingRequests()
|
||||
await this.fileUploader?.resendWaitingRequests()
|
||||
}
|
||||
}
|
||||
157
src/minified/sas9/WebJobExecutor.ts
Normal file
@@ -0,0 +1,157 @@
|
||||
import {
|
||||
AuthConfig,
|
||||
ExtraResponseAttributes,
|
||||
ServerType
|
||||
} from '@sasjs/utils/types'
|
||||
import {
|
||||
ErrorResponse,
|
||||
JobExecutionError,
|
||||
LoginRequiredError
|
||||
} from '../../types/errors'
|
||||
import { RequestClient } from '../../request/RequestClient'
|
||||
import {
|
||||
isRelativePath,
|
||||
parseSasViyaDebugResponse,
|
||||
appendExtraResponseAttributes,
|
||||
convertToCSV
|
||||
} from '../../utils'
|
||||
import { BaseJobExecutor } from '../../job-execution/JobExecutor'
|
||||
import { parseWeboutResponse } from '../../utils/parseWeboutResponse'
|
||||
|
||||
export interface WaitingRequstPromise {
|
||||
promise: Promise<any> | null
|
||||
resolve: any
|
||||
reject: any
|
||||
}
|
||||
export class WebJobExecutor extends BaseJobExecutor {
|
||||
constructor(
|
||||
serverUrl: string,
|
||||
serverType: ServerType,
|
||||
private jobsPath: string,
|
||||
private requestClient: RequestClient
|
||||
) {
|
||||
super(serverUrl, serverType)
|
||||
}
|
||||
|
||||
async execute(
|
||||
sasJob: string,
|
||||
data: any,
|
||||
config: any,
|
||||
loginRequiredCallback?: any,
|
||||
authConfig?: AuthConfig,
|
||||
extraResponseAttributes: ExtraResponseAttributes[] = []
|
||||
) {
|
||||
const loginCallback = loginRequiredCallback
|
||||
const program = isRelativePath(sasJob)
|
||||
? config.appLoc
|
||||
? config.appLoc.replace(/\/?$/, '/') + sasJob.replace(/^\//, '')
|
||||
: sasJob
|
||||
: sasJob
|
||||
let apiUrl = `${config.serverUrl}${this.jobsPath}/?${'_program=' + program}`
|
||||
|
||||
let requestParams = {
|
||||
...this.getRequestParams(config)
|
||||
}
|
||||
|
||||
let formData = new FormData()
|
||||
|
||||
if (data) {
|
||||
try {
|
||||
formData = generateFileUploadForm(formData, data)
|
||||
} catch (e: any) {
|
||||
return Promise.reject(new ErrorResponse(e?.message, e))
|
||||
}
|
||||
}
|
||||
|
||||
for (const key in requestParams) {
|
||||
if (requestParams.hasOwnProperty(key)) {
|
||||
formData.append(key, requestParams[key])
|
||||
}
|
||||
}
|
||||
|
||||
const requestPromise = new Promise((resolve, reject) => {
|
||||
this.requestClient!.post(apiUrl, formData, authConfig?.access_token)
|
||||
.then(async (res: any) => {
|
||||
this.requestClient!.appendRequest(res, sasJob, config.debug)
|
||||
|
||||
const jsonResponse =
|
||||
config.debug && typeof res.result === 'string'
|
||||
? parseWeboutResponse(res.result, apiUrl)
|
||||
: res.result
|
||||
|
||||
const responseObject = appendExtraResponseAttributes(
|
||||
{ result: jsonResponse, log: res.log },
|
||||
extraResponseAttributes
|
||||
)
|
||||
resolve(responseObject)
|
||||
})
|
||||
.catch(async (e: Error) => {
|
||||
if (e instanceof JobExecutionError) {
|
||||
this.requestClient!.appendRequest(e, sasJob, config.debug)
|
||||
reject(new ErrorResponse(e?.message, e))
|
||||
}
|
||||
|
||||
if (e instanceof LoginRequiredError) {
|
||||
if (!loginRequiredCallback) {
|
||||
reject(
|
||||
new ErrorResponse(
|
||||
'Request is not authenticated. Make sure .env file exists with valid credentials.',
|
||||
e
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
this.appendWaitingRequest(() => {
|
||||
return this.execute(
|
||||
sasJob,
|
||||
data,
|
||||
config,
|
||||
loginRequiredCallback,
|
||||
authConfig,
|
||||
extraResponseAttributes
|
||||
).then(
|
||||
(res: any) => {
|
||||
resolve(res)
|
||||
},
|
||||
(err: any) => {
|
||||
reject(err)
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
if (loginCallback) await loginCallback()
|
||||
} else reject(new ErrorResponse(e?.message, e))
|
||||
})
|
||||
})
|
||||
|
||||
return requestPromise
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* One of the approaches SASjs takes to send tables-formatted JSON (see README)
|
||||
* to SAS is as multipart form data, where each table is provided as a specially
|
||||
* formatted CSV file.
|
||||
*/
|
||||
const generateFileUploadForm = (formData: FormData, data: any): FormData => {
|
||||
for (const tableName in data) {
|
||||
if (!Array.isArray(data[tableName])) continue
|
||||
|
||||
const name = tableName
|
||||
const csv = convertToCSV(data, tableName)
|
||||
|
||||
if (csv === 'ERROR: LARGE STRING LENGTH') {
|
||||
throw new Error(
|
||||
'The max length of a string value in SASjs is 32765 characters.'
|
||||
)
|
||||
}
|
||||
|
||||
const file = new Blob([csv], {
|
||||
type: 'application/csv'
|
||||
})
|
||||
|
||||
formData.append(name, file, `${name}.csv`)
|
||||
}
|
||||
|
||||
return formData
|
||||
}
|
||||
3
src/minified/sas9/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
import SASjs from './SASjs'
|
||||
export * from '../../types'
|
||||
export default SASjs
|
||||
@@ -691,7 +691,9 @@ const parseError = (data: string) => {
|
||||
const parts = data.split(/stored process not found: /i)
|
||||
if (parts.length > 1) {
|
||||
const storedProcessPath = parts[1].split('<i>')[1].split('</i>')[0]
|
||||
const message = `Stored process not found: ${storedProcessPath}`
|
||||
const message = storedProcessPath.endsWith('runner')
|
||||
? `SASJS runner not found. Here's the link (https://cli.sasjs.io/auth/#sasjs-runner) to the SAS code for registering the SASjs runner`
|
||||
: `Stored process not found: ${storedProcessPath}`
|
||||
return new JobExecutionError(500, message, '')
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import axiosCookieJarSupport from 'axios-cookiejar-support'
|
||||
import * as tough from 'tough-cookie'
|
||||
import { prefixMessage } from '@sasjs/utils/error'
|
||||
import { RequestClient, throwIfError } from './RequestClient'
|
||||
import { JobExecutionError } from '../types/errors'
|
||||
|
||||
/**
|
||||
* Specific request client for SAS9 in Node.js environments.
|
||||
@@ -69,6 +70,8 @@ export class Sas9RequestClient extends RequestClient {
|
||||
return this.parseResponse<T>(response)
|
||||
})
|
||||
.catch(async (e: any) => {
|
||||
if (e instanceof JobExecutionError) throw e
|
||||
|
||||
return await this.handleError(
|
||||
e,
|
||||
() =>
|
||||
|
||||
@@ -3,10 +3,11 @@ import { RequestClient } from '../request/RequestClient'
|
||||
import * as dotenv from 'dotenv'
|
||||
import axios from 'axios'
|
||||
import { Logger, LogLevel } from '@sasjs/utils'
|
||||
import { Session } from '../types'
|
||||
import { Session, Context } from '../types'
|
||||
|
||||
jest.mock('axios')
|
||||
const mockedAxios = axios as jest.Mocked<typeof axios>
|
||||
const requestClient = new (<jest.Mock<RequestClient>>RequestClient)()
|
||||
|
||||
describe('SessionManager', () => {
|
||||
dotenv.config()
|
||||
@@ -14,9 +15,23 @@ describe('SessionManager', () => {
|
||||
const sessionManager = new SessionManager(
|
||||
process.env.SERVER_URL as string,
|
||||
process.env.DEFAULT_COMPUTE_CONTEXT as string,
|
||||
new RequestClient('https://sample.server.com')
|
||||
requestClient
|
||||
)
|
||||
|
||||
const getMockSession = () => ({
|
||||
id: ['id', new Date().getTime(), Math.random()].join('-'),
|
||||
state: '',
|
||||
links: [{ rel: 'state', href: '', uri: '', type: '', method: 'GET' }],
|
||||
attributes: {
|
||||
sessionInactiveTimeout: 900
|
||||
},
|
||||
creationTimeStamp: `${new Date(new Date().getTime()).toISOString()}`
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
jest.restoreAllMocks()
|
||||
})
|
||||
|
||||
describe('getVariable', () => {
|
||||
it('should fetch session variable', async () => {
|
||||
const sampleResponse = {
|
||||
@@ -45,6 +60,30 @@ describe('SessionManager', () => {
|
||||
)
|
||||
).resolves.toEqual(expectedResponse)
|
||||
})
|
||||
|
||||
it('should throw an error if GET request failed', async () => {
|
||||
const responseStatus = 500
|
||||
const responseErrorMessage = `The process timed out after 60 seconds. Request failed with status code ${responseStatus}`
|
||||
const response = {
|
||||
status: responseStatus,
|
||||
data: {
|
||||
message: responseErrorMessage
|
||||
}
|
||||
}
|
||||
const testVariable = 'testVariable'
|
||||
|
||||
jest.spyOn(requestClient, 'get').mockImplementation(() =>
|
||||
Promise.reject({
|
||||
response
|
||||
})
|
||||
)
|
||||
|
||||
const expectedError = `Error while fetching session variable '${testVariable}'. GET request to ${process.env.SERVER_URL}/compute/sessions/testId/variables/${testVariable} failed with status code ${responseStatus}. ${responseErrorMessage}`
|
||||
|
||||
await expect(
|
||||
sessionManager.getVariable('testId', testVariable)
|
||||
).rejects.toEqual(expectedError)
|
||||
})
|
||||
})
|
||||
|
||||
describe('waitForSession', () => {
|
||||
@@ -115,11 +154,25 @@ describe('SessionManager', () => {
|
||||
})
|
||||
|
||||
it('should throw an error if could not get session state', async () => {
|
||||
mockedAxios.get.mockImplementation(() => Promise.reject('Mocked error'))
|
||||
const gettingSessionStatus = 500
|
||||
const sessionStatusError = `Getting session status timed out after 60 seconds. Request failed with status code ${gettingSessionStatus}`
|
||||
|
||||
mockedAxios.get.mockImplementation(() =>
|
||||
Promise.reject({
|
||||
response: {
|
||||
status: gettingSessionStatus,
|
||||
data: {
|
||||
message: sessionStatusError
|
||||
}
|
||||
}
|
||||
})
|
||||
)
|
||||
|
||||
const expectedError = `Error while waiting for session. Error while getting session state. GET request to ${process.env.SERVER_URL}?wait=30 failed with status code ${gettingSessionStatus}. ${sessionStatusError}`
|
||||
|
||||
await expect(
|
||||
sessionManager['waitForSession'](session, null, 'access_token')
|
||||
).rejects.toContain('Error while getting session state.')
|
||||
).rejects.toEqual(expectedError)
|
||||
})
|
||||
|
||||
it('should return session state', async () => {
|
||||
@@ -135,4 +188,243 @@ describe('SessionManager', () => {
|
||||
).resolves.toEqual(customSession.state)
|
||||
})
|
||||
})
|
||||
|
||||
describe('isSessionValid', () => {
|
||||
const session: Session = getMockSession()
|
||||
|
||||
it('should return false if not a session provided', () => {
|
||||
expect(sessionManager['isSessionValid'](undefined as any)).toEqual(false)
|
||||
})
|
||||
|
||||
it('should return true if session is not expired', () => {
|
||||
expect(sessionManager['isSessionValid'](session)).toEqual(true)
|
||||
})
|
||||
|
||||
it('should return false if session is expired', () => {
|
||||
session.creationTimeStamp = `${new Date(
|
||||
new Date().getTime() -
|
||||
(session.attributes.sessionInactiveTimeout * 1000 + 1000)
|
||||
).toISOString()}`
|
||||
expect(sessionManager['isSessionValid'](session)).toEqual(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('removeSessionFromPool', () => {
|
||||
it('should remove session from the pool of sessions', () => {
|
||||
const session: Session = getMockSession()
|
||||
const sessions: Session[] = [getMockSession(), session]
|
||||
|
||||
sessionManager['sessions'] = sessions
|
||||
sessionManager['removeSessionFromPool'](session)
|
||||
|
||||
expect(sessionManager['sessions'].length).toEqual(1)
|
||||
})
|
||||
})
|
||||
|
||||
describe('getSession', () => {
|
||||
it('should return session if there is a valid session and create new session', async () => {
|
||||
jest
|
||||
.spyOn(sessionManager as any, 'createAndWaitForSession')
|
||||
.mockImplementation(async () => Promise.resolve(getMockSession()))
|
||||
|
||||
const session = getMockSession()
|
||||
sessionManager['sessions'] = [session]
|
||||
|
||||
await expect(sessionManager.getSession()).resolves.toEqual(session)
|
||||
expect(sessionManager['createAndWaitForSession']).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should return a session and keep one session if there is no sessions available', async () => {
|
||||
jest
|
||||
.spyOn(sessionManager as any, 'createAndWaitForSession')
|
||||
.mockImplementation(async () => {
|
||||
const session = getMockSession()
|
||||
sessionManager['sessions'].push(session)
|
||||
|
||||
return Promise.resolve(session)
|
||||
})
|
||||
|
||||
const session = await sessionManager.getSession()
|
||||
|
||||
expect(Object.keys(session)).toEqual(Object.keys(getMockSession()))
|
||||
expect(sessionManager['createAndWaitForSession']).toHaveBeenCalledTimes(2)
|
||||
expect(sessionManager['sessions'].length).toEqual(1)
|
||||
})
|
||||
|
||||
it.concurrent(
|
||||
'should throw an error if session creation request returned 500',
|
||||
async () => {
|
||||
const sessionCreationStatus = 500
|
||||
const sessionCreationError = `The process initialization for the Compute server with the ID 'ed40398a-ec8a-422b-867a-61493ee8a57f' timed out after 60 seconds. Request failed with status code ${sessionCreationStatus}`
|
||||
|
||||
jest.spyOn(requestClient, 'post').mockImplementation(() =>
|
||||
Promise.reject({
|
||||
response: {
|
||||
status: sessionCreationStatus,
|
||||
data: {
|
||||
message: sessionCreationError
|
||||
}
|
||||
}
|
||||
})
|
||||
)
|
||||
|
||||
const contextId = 'testContextId'
|
||||
const context: Context = {
|
||||
name: 'testContext',
|
||||
id: contextId,
|
||||
createdBy: 'createdBy',
|
||||
version: 1
|
||||
}
|
||||
|
||||
sessionManager['currentContext'] = context
|
||||
|
||||
const expectedError = new Error(
|
||||
`Error while creating session. POST request to ${process.env.SERVER_URL}/compute/contexts/${contextId}/sessions failed with status code ${sessionCreationStatus}. ${sessionCreationError}`
|
||||
)
|
||||
|
||||
await expect(sessionManager.getSession()).rejects.toEqual(expectedError)
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
describe('clearSession', () => {
|
||||
it('should clear session', async () => {
|
||||
jest
|
||||
.spyOn(requestClient, 'delete')
|
||||
.mockImplementation(() =>
|
||||
Promise.resolve({ result: '', etag: '', status: 200 })
|
||||
)
|
||||
|
||||
const sessionToBeCleared = getMockSession()
|
||||
const sessionToStay = getMockSession()
|
||||
|
||||
sessionManager['sessions'] = [sessionToBeCleared, sessionToStay]
|
||||
|
||||
await sessionManager.clearSession(sessionToBeCleared.id)
|
||||
|
||||
expect(sessionManager['sessions']).toEqual([sessionToStay])
|
||||
})
|
||||
|
||||
it('should throw error if DELETE request failed', async () => {
|
||||
const sessionCreationStatus = 500
|
||||
const sessionDeleteError = `The process timed out after 60 seconds. Request failed with status code ${sessionCreationStatus}`
|
||||
|
||||
jest.spyOn(requestClient, 'delete').mockImplementation(() =>
|
||||
Promise.reject({
|
||||
response: {
|
||||
status: sessionCreationStatus,
|
||||
data: {
|
||||
message: sessionDeleteError
|
||||
}
|
||||
}
|
||||
})
|
||||
)
|
||||
|
||||
const session = getMockSession()
|
||||
|
||||
sessionManager['sessions'] = [session]
|
||||
|
||||
const expectedError = `Error while deleting session. DELETE request to /compute/sessions/${session.id} failed with status code ${sessionCreationStatus}. ${sessionDeleteError}`
|
||||
|
||||
await expect(sessionManager.clearSession(session.id)).rejects.toEqual(
|
||||
expectedError
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('waitForCurrentContext', () => {
|
||||
it('should resolve when current context is ready', async () => {
|
||||
sessionManager['settingContext'] = true
|
||||
sessionManager['contextName'] = 'test context'
|
||||
|
||||
await expect(sessionManager['waitForCurrentContext']()).toResolve()
|
||||
expect(sessionManager['settingContext']).toEqual(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('setCurrentContext', () => {
|
||||
it('should set current context', async () => {
|
||||
const contextName = 'test context'
|
||||
const testContext: Context = {
|
||||
name: contextName,
|
||||
id: 'string',
|
||||
createdBy: 'string',
|
||||
version: 1
|
||||
}
|
||||
|
||||
jest.spyOn(requestClient, 'get').mockImplementation(() => {
|
||||
return Promise.resolve({
|
||||
result: {
|
||||
items: [testContext]
|
||||
},
|
||||
etag: '',
|
||||
status: 200
|
||||
})
|
||||
})
|
||||
|
||||
sessionManager['currentContext'] = null
|
||||
sessionManager['contextName'] = contextName
|
||||
sessionManager['settingContext'] = false
|
||||
|
||||
await expect(sessionManager['setCurrentContext']()).toResolve()
|
||||
expect(sessionManager['currentContext']).toEqual(testContext)
|
||||
})
|
||||
|
||||
it('should throw error if GET request failed', async () => {
|
||||
const responseStatus = 500
|
||||
const responseErrorMessage = `The process timed out after 60 seconds. Request failed with status code ${responseStatus}`
|
||||
const response = {
|
||||
status: responseStatus,
|
||||
data: {
|
||||
message: responseErrorMessage
|
||||
}
|
||||
}
|
||||
|
||||
jest.spyOn(requestClient, 'get').mockImplementation(() =>
|
||||
Promise.reject({
|
||||
response
|
||||
})
|
||||
)
|
||||
|
||||
const expectedError = `Error while getting list of contexts. GET request to ${process.env.SERVER_URL}/compute/contexts?limit=10000 failed with status code ${responseStatus}. ${responseErrorMessage}`
|
||||
|
||||
sessionManager['currentContext'] = null
|
||||
|
||||
await expect(sessionManager['setCurrentContext']()).rejects.toEqual(
|
||||
expectedError
|
||||
)
|
||||
})
|
||||
|
||||
it('should throw an error if current context is not in the list of contexts', async () => {
|
||||
const contextName = 'test context'
|
||||
const testContext: Context = {
|
||||
name: `${contextName} does not exist`,
|
||||
id: 'string',
|
||||
createdBy: 'string',
|
||||
version: 1
|
||||
}
|
||||
|
||||
jest.spyOn(requestClient, 'get').mockImplementation(() => {
|
||||
return Promise.resolve({
|
||||
result: {
|
||||
items: [testContext]
|
||||
},
|
||||
etag: '',
|
||||
status: 200
|
||||
})
|
||||
})
|
||||
|
||||
sessionManager['currentContext'] = null
|
||||
sessionManager['contextName'] = contextName
|
||||
sessionManager['settingContext'] = false
|
||||
|
||||
const expectedError = new Error(
|
||||
`The context '${contextName}' was not found on the server ${process.env.SERVER_URL}.`
|
||||
)
|
||||
|
||||
await expect(sessionManager['setCurrentContext']()).rejects.toEqual(
|
||||
expectedError
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -5,4 +5,11 @@ export interface LoginOptions {
|
||||
export interface LoginResult {
|
||||
isLoggedIn: boolean
|
||||
userName: string
|
||||
userLongName: string
|
||||
}
|
||||
export interface LoginResultInternal {
|
||||
isLoggedIn: boolean
|
||||
userName: string
|
||||
userLongName: string
|
||||
loginForm?: any
|
||||
}
|
||||
|
||||