mirror of
https://github.com/sasjs/adapter.git
synced 2025-12-11 17:34:34 +00:00
Compare commits
7 Commits
error-stat
...
fixing-sas
| Author | SHA1 | Date | |
|---|---|---|---|
| 0bd156141c | |||
| a615c5fdb6 | |||
| ca7ee83f7f | |||
| 97a530cc66 | |||
| 317c8c81a0 | |||
| c87776ca1b | |||
| 04032831c3 |
@@ -1,103 +1,24 @@
|
||||
{
|
||||
"projectName": "adapter",
|
||||
"projectOwner": "sasjs",
|
||||
"repoType": "github",
|
||||
"repoHost": "https://github.com",
|
||||
"files": [
|
||||
"README.md"
|
||||
],
|
||||
"imageSize": 100,
|
||||
"commit": false,
|
||||
"commitConvention": "angular",
|
||||
"contributors": [
|
||||
{
|
||||
"login": "krishna-acondy",
|
||||
"name": "Krishna Acondy",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/2980428?v=4",
|
||||
"profile": "https://krishna-acondy.io/",
|
||||
"contributions": [
|
||||
"code",
|
||||
"infra",
|
||||
"blog",
|
||||
"content",
|
||||
"ideas",
|
||||
"video"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "YuryShkoda",
|
||||
"name": "Yury Shkoda",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/25773492?v=4",
|
||||
"profile": "https://www.erudicat.com/",
|
||||
"contributions": [
|
||||
"code",
|
||||
"infra",
|
||||
"ideas",
|
||||
"test",
|
||||
"video"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "medjedovicm",
|
||||
"name": "Mihajlo Medjedovic",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/18329105?v=4",
|
||||
"profile": "https://github.com/medjedovicm",
|
||||
"contributions": [
|
||||
"code",
|
||||
"infra",
|
||||
"test",
|
||||
"review"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "allanbowe",
|
||||
"name": "Allan Bowe",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/4420615?v=4",
|
||||
"profile": "https://github.com/allanbowe",
|
||||
"contributions": [
|
||||
"code",
|
||||
"review",
|
||||
"test",
|
||||
"mentoring",
|
||||
"maintenance"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "saadjutt01",
|
||||
"name": "Muhammad Saad ",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/8914650?v=4",
|
||||
"profile": "https://github.com/saadjutt01",
|
||||
"contributions": [
|
||||
"code",
|
||||
"review",
|
||||
"test",
|
||||
"mentoring",
|
||||
"infra"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "sabhas",
|
||||
"name": "Sabir Hassan",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/82647447?v=4",
|
||||
"profile": "https://github.com/sabhas",
|
||||
"contributions": [
|
||||
"code",
|
||||
"review",
|
||||
"test",
|
||||
"ideas"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "VladislavParhomchik",
|
||||
"name": "VladislavParhomchik",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/83717836?v=4",
|
||||
"profile": "https://github.com/VladislavParhomchik",
|
||||
"contributions": [
|
||||
"test",
|
||||
"review"
|
||||
"code"
|
||||
]
|
||||
}
|
||||
],
|
||||
"contributorsPerLine": 7,
|
||||
"projectName": "adapter",
|
||||
"projectOwner": "sasjs",
|
||||
"repoType": "github",
|
||||
"repoHost": "https://github.com",
|
||||
"skipCi": true
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ GREEN="\033[1;32m"
|
||||
# temporary file which holds the message).
|
||||
commit_message=$(cat "$1")
|
||||
|
||||
if (echo "$commit_message" | grep -Eq "^(build|chore|ci|docs|feat|fix|perf|refactor|revert|style|test)(\([a-z0-9 -\*]+\))?!?: .+$") then
|
||||
if (echo "$commit_message" | grep -Eq "^(build|chore|ci|docs|feat|fix|perf|refactor|revert|style|test)(\([a-z0-9 \-\*]+\))?!?: .+$") then
|
||||
echo "${GREEN} ✔ Commit message meets Conventional Commit standards"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
10
.github/dependabot.yml
vendored
10
.github/dependabot.yml
vendored
@@ -1,7 +1,7 @@
|
||||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: npm
|
||||
directory: '/'
|
||||
schedule:
|
||||
interval: monthly
|
||||
open-pull-requests-limit: 10
|
||||
- package-ecosystem: npm
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: daily
|
||||
open-pull-requests-limit: 10
|
||||
|
||||
5
.github/workflows/build.yml
vendored
5
.github/workflows/build.yml
vendored
@@ -13,15 +13,14 @@ jobs:
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
node-version: [lts/fermium]
|
||||
node-version: [15.x]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Use Node.js ${{ matrix.node-version }}
|
||||
uses: actions/setup-node@v2
|
||||
uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
cache: npm
|
||||
- name: Install Dependencies
|
||||
run: npm ci
|
||||
- name: Check code style
|
||||
|
||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -5,4 +5,4 @@ build
|
||||
|
||||
/coverage
|
||||
|
||||
.DS_Store
|
||||
.DS_Store
|
||||
@@ -1,6 +1,4 @@
|
||||
sasjs-tests/
|
||||
docs/
|
||||
.github/
|
||||
*.md
|
||||
*.spec.ts
|
||||
.all-contributorsrc
|
||||
CONTRIBUTING.md
|
||||
|
||||
29
README.md
29
README.md
@@ -36,7 +36,7 @@ Ok ok. Deploy this [example.html](https://raw.githubusercontent.com/sasjs/adapte
|
||||
|
||||
The backend part can be deployed as follows:
|
||||
|
||||
```sas
|
||||
```
|
||||
%let appLoc=/Public/app/readme; /* Metadata or Viya Folder per SASjs config */
|
||||
filename mc url "https://raw.githubusercontent.com/sasjs/core/main/all.sas";
|
||||
%inc mc; /* compile macros (can also be downloaded & compiled seperately) */
|
||||
@@ -85,11 +85,11 @@ let sasJs = new SASjs.default(
|
||||
);
|
||||
```
|
||||
If you've installed it via NPM, you can import it as a default import like so:
|
||||
```js
|
||||
```
|
||||
import SASjs from '@sasjs/adapter';
|
||||
```
|
||||
You can then instantiate it with:
|
||||
```js
|
||||
```
|
||||
const sasJs = new SASjs({your config})
|
||||
```
|
||||
|
||||
@@ -119,7 +119,6 @@ sasJs.request("/path/to/my/service", dataObject)
|
||||
console.log(response.tablewith2cols1row[0].COL1.value)
|
||||
})
|
||||
```
|
||||
|
||||
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:
|
||||
|
||||
```javascript
|
||||
@@ -147,7 +146,6 @@ The adapter will also cache the logs (if debug enabled) and even the work tables
|
||||
The SAS side is handled by a number of macros in the [macro core](https://github.com/sasjs/core) library.
|
||||
|
||||
The following snippet shows the process of SAS tables arriving / leaving:
|
||||
|
||||
```sas
|
||||
/* fetch all input tables sent from frontend - they arrive as work tables */
|
||||
%webout(FETCH)
|
||||
@@ -163,6 +161,7 @@ run;
|
||||
%webout(OBJ,tables,fmt=N) /* unformatted (raw) data */
|
||||
%webout(OBJ,tables,label=newtable) /* rename tables on export */
|
||||
%webout(CLOSE) /* close the JSON and send some extra useful variables too */
|
||||
|
||||
```
|
||||
|
||||
## Configuration
|
||||
@@ -173,8 +172,7 @@ Configuration on the client side involves passing an object on startup, which ca
|
||||
* `serverType` - either `SAS9` or `SASVIYA`.
|
||||
* `serverUrl` - 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.
|
||||
* `debug` - if `true` then SAS Logs and extra debug information is returned.
|
||||
* `LoginMechanism` - either `Default` or `Redirected`. If `Redirected` then authentication occurs through the injection of an additional screen, which contains the SASLogon prompt. This allows for more complex authentication flows (such as 2FA) and avoids the need to handle passwords in the application itself. The styling of the redirect flow can also be modified. If left at "Default" then the developer must capture the username and password and use these with the `.login()` method.
|
||||
* `useComputeApi` - Only relevant when the serverType is `SASVIYA`. If `true` the [Compute API](#using-the-compute-api) is used. If `false` the [JES API](#using-the-jes-api) is used. If `null` or `undefined` the [Web](#using-jes-web-app) approach is used.
|
||||
* `useComputeApi` - Only relevant when the serverType is `SASVIYA`. If `true` the [Compute API](#using-the-compute-api) is used. If `false` the [JES API](#using-the-jes-api) is used. If `null` or `undefined` the [Web](#using-jes-web-app) approach is used.
|
||||
* `contextName` - Compute context on which the requests will be called. If missing or not provided, defaults to `Job Execution Compute context`.
|
||||
|
||||
The adapter supports a number of approaches for interfacing with Viya (`serverType` is `SASVIYA`). For maximum performance, be sure to [configure your compute context](https://sasjs.io/guide-viya/#shared-account-and-server-re-use) with `reuseServerProcesses` as `true` and a system account in `runServerAs`. This functionality is available since Viya 3.5. This configuration is supported when [creating contexts using the CLI](https://sasjs.io/sasjs-cli-context/#sasjs-context-create).
|
||||
@@ -198,7 +196,7 @@ Here we are running Jobs using the Job Execution Service except this time we are
|
||||
|
||||
This approach (`useComputeApi: false`) also ensures that jobs are displayed in Environment Manager.
|
||||
|
||||
```json
|
||||
```
|
||||
{
|
||||
appLoc:"/Your/Path",
|
||||
serverType:"SASVIYA",
|
||||
@@ -212,12 +210,12 @@ This approach is by far the fastest, as a result of the optimisations we have bu
|
||||
|
||||
With this approach (`useComputeApi: true`), the requests/logs will _not_ appear in the list in Environment manager.
|
||||
|
||||
```json
|
||||
```
|
||||
{
|
||||
appLoc:"/Your/Path",
|
||||
serverType:"SASVIYA",
|
||||
useComputeApi: true,
|
||||
contextName: "yourComputeContext"
|
||||
contextName: 'yourComputeContext'
|
||||
}
|
||||
```
|
||||
|
||||
@@ -238,9 +236,6 @@ 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-)
|
||||
<!-- ALL-CONTRIBUTORS-BADGE:END -->
|
||||
|
||||
Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/docs/en/emoji-key)):
|
||||
|
||||
@@ -249,13 +244,7 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d
|
||||
<!-- 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>
|
||||
<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></td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
const result = process.versions
|
||||
if (result && result.node) {
|
||||
if (parseInt(result.node) < 14) {
|
||||
console.log(
|
||||
'\x1b[31m%s\x1b[0m',
|
||||
`❌ Process failed due to Node Version,\nPlease install and use Node Version >= 14\nYour current Node Version is: ${result.node}`
|
||||
)
|
||||
process.exit(1)
|
||||
}
|
||||
} else {
|
||||
console.log(
|
||||
'\x1b[31m%s\x1b[0m',
|
||||
'Something went wrong while checking Node version'
|
||||
)
|
||||
process.exit(1)
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
139
docs/index.html
139
docs/index.html
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
19515
package-lock.json
generated
19515
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
31
package.json
31
package.json
@@ -3,10 +3,8 @@
|
||||
"description": "JavaScript adapter for SAS",
|
||||
"homepage": "https://adapter.sasjs.io",
|
||||
"scripts": {
|
||||
"preinstall": "node checkNodeVersion",
|
||||
"prebuild": "node checkNodeVersion",
|
||||
"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 --write \"src/**/*.{ts,tsx,js,jsx,html,css,sass,less,json,yml,md,graphql}\" && npx prettier --write \"sasjs-tests/src/**/*.{ts,tsx,js,jsx,html,css,sass,less,json,yml,md,graphql}\"",
|
||||
"lint": "npx prettier --check \"src/**/*.{ts,tsx,js,jsx,html,css,sass,less,json,yml,md,graphql}\" && npx prettier --check \"sasjs-tests/src/**/*.{ts,tsx,js,jsx,html,css,sass,less,json,yml,md,graphql}\"",
|
||||
@@ -41,43 +39,42 @@
|
||||
"license": "ISC",
|
||||
"devDependencies": {
|
||||
"@types/axios": "^0.14.0",
|
||||
"@types/express": "^4.17.13",
|
||||
"@types/form-data": "^2.5.0",
|
||||
"@types/jest": "^27.0.2",
|
||||
"@types/jest": "^26.0.24",
|
||||
"@types/mime": "^2.0.3",
|
||||
"@types/pem": "^1.9.6",
|
||||
"@types/tough-cookie": "^4.0.1",
|
||||
"copyfiles": "^2.4.1",
|
||||
"cp": "^0.2.0",
|
||||
"dotenv": "^10.0.0",
|
||||
"express": "^4.17.1",
|
||||
"jest": "^27.2.0",
|
||||
"jest": "^27.0.6",
|
||||
"jest-extended": "^0.11.5",
|
||||
"node-polyfill-webpack-plugin": "^1.1.4",
|
||||
"path": "^0.12.7",
|
||||
"pem": "^1.14.4",
|
||||
"process": "^0.11.10",
|
||||
"rimraf": "^3.0.2",
|
||||
"semantic-release": "^18.0.0",
|
||||
"terser-webpack-plugin": "^5.2.4",
|
||||
"semantic-release": "^17.4.4",
|
||||
"terser-webpack-plugin": "^5.1.4",
|
||||
"ts-jest": "^27.0.3",
|
||||
"ts-loader": "^9.2.6",
|
||||
"ts-loader": "^9.2.2",
|
||||
"tslint": "^6.1.3",
|
||||
"tslint-config-prettier": "^1.18.0",
|
||||
"typedoc": "0.19.2",
|
||||
"typedoc": "^0.21.4",
|
||||
"typedoc-neo-theme": "^1.1.1",
|
||||
"typedoc-plugin-external-module-name": "^4.0.6",
|
||||
"typescript": "4.3.5",
|
||||
"webpack": "^5.56.0",
|
||||
"typescript": "^4.3.5",
|
||||
"webpack": "^5.44.0",
|
||||
"webpack-cli": "^4.7.2"
|
||||
},
|
||||
"main": "index.js",
|
||||
"dependencies": {
|
||||
"@sasjs/utils": "^2.32.0",
|
||||
"axios": "^0.21.4",
|
||||
"@sasjs/utils": "^2.27.1",
|
||||
"axios": "^0.21.1",
|
||||
"axios-cookiejar-support": "^1.0.1",
|
||||
"form-data": "^4.0.0",
|
||||
"https": "^1.0.0",
|
||||
"tough-cookie": "^4.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=15"
|
||||
}
|
||||
}
|
||||
|
||||
22129
sasjs-tests/package-lock.json
generated
22129
sasjs-tests/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -5,7 +5,7 @@
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@sasjs/adapter": "file:../build/sasjs-adapter-5.0.0.tgz",
|
||||
"@sasjs/test-framework": "^1.4.2",
|
||||
"@sasjs/test-framework": "^1.4.0",
|
||||
"@types/jest": "^26.0.20",
|
||||
"@types/node": "^14.14.41",
|
||||
"@types/react": "^17.0.1",
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import SASjs, { LoginMechanism, SASjsConfig } from '@sasjs/adapter'
|
||||
import SASjs, { SASjsConfig } from '@sasjs/adapter'
|
||||
import { TestSuite } from '@sasjs/test-framework'
|
||||
import { ServerType } from '@sasjs/utils/types'
|
||||
|
||||
@@ -13,7 +13,7 @@ const defaultConfig: SASjsConfig = {
|
||||
debug: false,
|
||||
contextName: 'SAS Job Execution compute context',
|
||||
useComputeApi: false,
|
||||
loginMechanism: LoginMechanism.Default
|
||||
allowInsecureRequests: false
|
||||
}
|
||||
|
||||
const customConfig = {
|
||||
@@ -41,27 +41,16 @@ export const basicTests = (
|
||||
assertion: (response: any) =>
|
||||
response && response.isLoggedIn && response.userName === userName
|
||||
},
|
||||
{
|
||||
title: 'Fetch username for already logged in user',
|
||||
description: 'Should log the user in',
|
||||
test: async () => {
|
||||
await adapter.logIn(userName, password)
|
||||
|
||||
const newAdapterIns = new SASjs(adapter.getSasjsConfig())
|
||||
|
||||
return await newAdapterIns.checkSession()
|
||||
},
|
||||
assertion: (response: any) =>
|
||||
response?.isLoggedIn && response?.userName === userName
|
||||
},
|
||||
{
|
||||
title: 'Multiple Log in attempts',
|
||||
description:
|
||||
'Should fail on first attempt and should log the user in on second attempt',
|
||||
test: async () => {
|
||||
await adapter.logOut()
|
||||
await sleep(1000)
|
||||
await adapter.logIn('invalid', 'invalid')
|
||||
return await adapter.logIn(userName, password)
|
||||
await sleep(1000)
|
||||
return adapter.logIn(userName, password)
|
||||
},
|
||||
assertion: (response: any) =>
|
||||
response && response.isLoggedIn && response.userName === userName
|
||||
@@ -77,8 +66,8 @@ export const basicTests = (
|
||||
'common/sendArr',
|
||||
stringData,
|
||||
undefined,
|
||||
async () => {
|
||||
await adapter.logIn(userName, password)
|
||||
() => {
|
||||
adapter.logIn(userName, password)
|
||||
}
|
||||
)
|
||||
},
|
||||
@@ -164,7 +153,10 @@ export const basicTests = (
|
||||
description:
|
||||
'Should complete successful request with extra attributes present in response',
|
||||
test: async () => {
|
||||
const config: Partial<SASjsConfig> = {
|
||||
if (adapter.getSasjsConfig().serverType !== 'SASVIYA')
|
||||
return Promise.resolve('skip')
|
||||
|
||||
const config = {
|
||||
useComputeApi: false
|
||||
}
|
||||
|
||||
@@ -178,9 +170,15 @@ export const basicTests = (
|
||||
)
|
||||
},
|
||||
assertion: (response: any) => {
|
||||
if (response === 'skip') return true
|
||||
|
||||
const responseKeys: any = Object.keys(response)
|
||||
return responseKeys.includes('file') && responseKeys.includes('data')
|
||||
}
|
||||
}
|
||||
]
|
||||
})
|
||||
|
||||
const sleep = (ms: number) => {
|
||||
return new Promise((resolve) => setTimeout(resolve, ms))
|
||||
}
|
||||
|
||||
101
src/FileUploader.ts
Normal file
101
src/FileUploader.ts
Normal file
@@ -0,0 +1,101 @@
|
||||
import { isUrl, getValidJson, parseSasViyaDebugResponse } from './utils'
|
||||
import { UploadFile } from './types/UploadFile'
|
||||
import { ErrorResponse, LoginRequiredError } from './types/errors'
|
||||
import { RequestClient } from './request/RequestClient'
|
||||
import { ServerType } from '@sasjs/utils/types'
|
||||
import SASjs from './SASjs'
|
||||
import { Server } from 'https'
|
||||
import { SASjsConfig } from './types'
|
||||
import { config } from 'process'
|
||||
|
||||
export class FileUploader {
|
||||
constructor(
|
||||
private sasjsConfig: SASjsConfig,
|
||||
private jobsPath: string,
|
||||
private requestClient: RequestClient
|
||||
) {
|
||||
if (this.sasjsConfig.serverUrl) isUrl(this.sasjsConfig.serverUrl)
|
||||
}
|
||||
|
||||
public uploadFile(sasJob: string, files: UploadFile[], params: any) {
|
||||
if (files?.length < 1)
|
||||
return Promise.reject(
|
||||
new ErrorResponse('At least one file must be provided.')
|
||||
)
|
||||
if (!sasJob || sasJob === '')
|
||||
return Promise.reject(new ErrorResponse('sasJob must be provided.'))
|
||||
|
||||
let paramsString = ''
|
||||
|
||||
for (let param in params) {
|
||||
if (params.hasOwnProperty(param)) {
|
||||
paramsString += `&${param}=${params[param]}`
|
||||
}
|
||||
}
|
||||
|
||||
const program = this.sasjsConfig.appLoc
|
||||
? this.sasjsConfig.appLoc.replace(/\/?$/, '/') + sasJob.replace(/^\//, '')
|
||||
: sasJob
|
||||
const uploadUrl = `${this.jobsPath}/?${
|
||||
'_program=' + program
|
||||
}${paramsString}`
|
||||
|
||||
const formData = new FormData()
|
||||
|
||||
for (let file of files) {
|
||||
formData.append('file', file.file, file.fileName)
|
||||
}
|
||||
|
||||
const csrfToken = this.requestClient.getCsrfToken('file')
|
||||
if (csrfToken) formData.append('_csrf', csrfToken.value)
|
||||
if (this.sasjsConfig.debug) formData.append('_debug', '131')
|
||||
if (
|
||||
this.sasjsConfig.serverType === ServerType.SasViya &&
|
||||
this.sasjsConfig.contextName
|
||||
)
|
||||
formData.append('_contextname', this.sasjsConfig.contextName)
|
||||
|
||||
const headers = {
|
||||
'cache-control': 'no-cache',
|
||||
Accept: '*/*',
|
||||
'Content-Type': 'text/plain'
|
||||
}
|
||||
|
||||
return this.requestClient
|
||||
.post(uploadUrl, formData, undefined, 'application/json', headers)
|
||||
.then(async (res) => {
|
||||
// for web approach on Viya
|
||||
if (
|
||||
this.sasjsConfig.debug &&
|
||||
(this.sasjsConfig.useComputeApi === null ||
|
||||
this.sasjsConfig.useComputeApi === undefined) &&
|
||||
this.sasjsConfig.serverType === ServerType.SasViya
|
||||
) {
|
||||
const jsonResponse = await parseSasViyaDebugResponse(
|
||||
res.result as string,
|
||||
this.requestClient,
|
||||
this.sasjsConfig.serverUrl
|
||||
)
|
||||
return typeof jsonResponse === 'string'
|
||||
? getValidJson(jsonResponse)
|
||||
: jsonResponse
|
||||
}
|
||||
|
||||
return typeof res.result === 'string'
|
||||
? getValidJson(res.result)
|
||||
: res.result
|
||||
|
||||
//TODO: append to SASjs requests
|
||||
})
|
||||
.catch((err: Error) => {
|
||||
if (err instanceof LoginRequiredError) {
|
||||
return Promise.reject(
|
||||
new ErrorResponse('You must be logged in to upload a file.', err)
|
||||
)
|
||||
}
|
||||
return Promise.reject(
|
||||
new ErrorResponse('File upload request failed.', err)
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,3 @@
|
||||
import * as https from 'https'
|
||||
import { generateTimestamp } from '@sasjs/utils/time'
|
||||
import * as NodeFormData from 'form-data'
|
||||
import { Sas9RequestClient } from './request/Sas9RequestClient'
|
||||
@@ -11,13 +10,9 @@ import { isUrl } from './utils'
|
||||
export class SAS9ApiClient {
|
||||
private requestClient: Sas9RequestClient
|
||||
|
||||
constructor(
|
||||
private serverUrl: string,
|
||||
private jobsPath: string,
|
||||
httpsAgentOptions?: https.AgentOptions
|
||||
) {
|
||||
constructor(private serverUrl: string, private jobsPath: string) {
|
||||
if (serverUrl) isUrl(serverUrl)
|
||||
this.requestClient = new Sas9RequestClient(serverUrl, httpsAgentOptions)
|
||||
this.requestClient = new Sas9RequestClient(serverUrl, false)
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,51 +0,0 @@
|
||||
import { Logger, LogLevel } from '@sasjs/utils/logger'
|
||||
import { RequestClient } from './request/RequestClient'
|
||||
import { SASViyaApiClient } from './SASViyaApiClient'
|
||||
import { Folder } from './types'
|
||||
import { RootFolderNotFoundError } from './types/errors'
|
||||
|
||||
const mockFolder: Folder = {
|
||||
id: '1',
|
||||
uri: '/folder',
|
||||
links: [],
|
||||
memberCount: 1
|
||||
}
|
||||
|
||||
const requestClient = new (<jest.Mock<RequestClient>>RequestClient)()
|
||||
const sasViyaApiClient = new SASViyaApiClient(
|
||||
'https://test.com',
|
||||
'/test',
|
||||
'test context',
|
||||
requestClient
|
||||
)
|
||||
|
||||
describe('SASViyaApiClient', () => {
|
||||
beforeEach(() => {
|
||||
;(process as any).logger = new Logger(LogLevel.Off)
|
||||
setupMocks()
|
||||
})
|
||||
|
||||
it('should throw an error when the root folder is not found on the server', async () => {
|
||||
jest
|
||||
.spyOn(requestClient, 'get')
|
||||
.mockImplementation(() => Promise.reject('Not Found'))
|
||||
const error = await sasViyaApiClient
|
||||
.createFolder('test', '/foo')
|
||||
.catch((e) => e)
|
||||
expect(error).toBeInstanceOf(RootFolderNotFoundError)
|
||||
})
|
||||
})
|
||||
|
||||
const setupMocks = () => {
|
||||
jest
|
||||
.spyOn(requestClient, 'get')
|
||||
.mockImplementation(() =>
|
||||
Promise.resolve({ result: mockFolder, etag: '', status: 200 })
|
||||
)
|
||||
|
||||
jest
|
||||
.spyOn(requestClient, 'post')
|
||||
.mockImplementation(() =>
|
||||
Promise.resolve({ result: mockFolder, etag: '', status: 200 })
|
||||
)
|
||||
}
|
||||
@@ -11,7 +11,7 @@ import {
|
||||
JobDefinition,
|
||||
PollOptions
|
||||
} from './types'
|
||||
import { JobExecutionError, RootFolderNotFoundError } from './types/errors'
|
||||
import { JobExecutionError } from './types/errors'
|
||||
import { SessionManager } from './SessionManager'
|
||||
import { ContextManager } from './ContextManager'
|
||||
import { SasAuthResponse, MacroVar, AuthConfig } from '@sasjs/utils/types'
|
||||
@@ -51,16 +51,6 @@ export class SASViyaApiClient {
|
||||
)
|
||||
private folderMap = new Map<string, Job[]>()
|
||||
|
||||
/**
|
||||
* A helper method used to call appendRequest method of RequestClient
|
||||
* @param response - response from sasjs request
|
||||
* @param program - name of program
|
||||
* @param debug - a boolean that indicates whether debug was enabled or not
|
||||
*/
|
||||
public appendRequest(response: any, program: string, debug: boolean) {
|
||||
this.requestClient!.appendRequest(response, program, debug)
|
||||
}
|
||||
|
||||
public get debug() {
|
||||
return this._debug
|
||||
}
|
||||
@@ -391,11 +381,7 @@ export class SASViyaApiClient {
|
||||
)
|
||||
const newFolderName = `${parentFolderPath.split('/').pop()}`
|
||||
if (newParentFolderPath === '') {
|
||||
throw new RootFolderNotFoundError(
|
||||
parentFolderPath,
|
||||
this.serverUrl,
|
||||
accessToken
|
||||
)
|
||||
throw new Error('Root folder has to be present on the server.')
|
||||
}
|
||||
logger.info(
|
||||
`Creating parent folder:\n'${newFolderName}' in '${newParentFolderPath}'`
|
||||
|
||||
177
src/SASjs.ts
177
src/SASjs.ts
@@ -1,17 +1,8 @@
|
||||
import { compareTimestamps, asyncForEach } from './utils'
|
||||
import {
|
||||
SASjsConfig,
|
||||
UploadFile,
|
||||
EditContextInput,
|
||||
PollOptions,
|
||||
LoginMechanism,
|
||||
FolderMember,
|
||||
ServiceMember,
|
||||
ExecutionQuery
|
||||
} from './types'
|
||||
import { SASjsConfig, UploadFile, EditContextInput, PollOptions } from './types'
|
||||
import { SASViyaApiClient } from './SASViyaApiClient'
|
||||
import { SAS9ApiClient } from './SAS9ApiClient'
|
||||
import { SASjsApiClient } from './SASjsApiClient'
|
||||
import { FileUploader } from './FileUploader'
|
||||
import { AuthManager } from './auth'
|
||||
import {
|
||||
ServerType,
|
||||
@@ -25,15 +16,12 @@ import {
|
||||
WebJobExecutor,
|
||||
ComputeJobExecutor,
|
||||
JesJobExecutor,
|
||||
Sas9JobExecutor,
|
||||
FileUploader
|
||||
Sas9JobExecutor
|
||||
} from './job-execution'
|
||||
import { ErrorResponse } from './types/errors'
|
||||
import { LoginOptions, LoginResult } from './types/Login'
|
||||
|
||||
const defaultConfig: SASjsConfig = {
|
||||
serverUrl: '',
|
||||
pathSASJS: '/SASjsApi/stp/execute',
|
||||
pathSAS9: '/SASStoredProcess/do',
|
||||
pathSASViya: '/SASJobExecution',
|
||||
appLoc: '/Public/seedapp',
|
||||
@@ -41,7 +29,7 @@ const defaultConfig: SASjsConfig = {
|
||||
debug: false,
|
||||
contextName: 'SAS Job Execution compute context',
|
||||
useComputeApi: null,
|
||||
loginMechanism: LoginMechanism.Default
|
||||
allowInsecureRequests: false
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -53,7 +41,6 @@ export default class SASjs {
|
||||
private jobsPath: string = ''
|
||||
private sasViyaApiClient: SASViyaApiClient | null = null
|
||||
private sas9ApiClient: SAS9ApiClient | null = null
|
||||
private SASjsApiClient: SASjsApiClient | null = null
|
||||
private fileUploader: FileUploader | null = null
|
||||
private authManager: AuthManager | null = null
|
||||
private requestClient: RequestClient | null = null
|
||||
@@ -62,7 +49,7 @@ export default class SASjs {
|
||||
private jesJobExecutor: JobExecutor | null = null
|
||||
private sas9JobExecutor: JobExecutor | null = null
|
||||
|
||||
constructor(config?: Partial<SASjsConfig>) {
|
||||
constructor(config?: any) {
|
||||
this.sasjsConfig = {
|
||||
...defaultConfig,
|
||||
...config
|
||||
@@ -512,7 +499,7 @@ export default class SASjs {
|
||||
...this.sasjsConfig,
|
||||
...config
|
||||
}
|
||||
this.setupConfiguration()
|
||||
await this.setupConfiguration()
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -528,11 +515,10 @@ export default class SASjs {
|
||||
|
||||
/**
|
||||
* Checks whether a session is active, or login is required.
|
||||
* @param accessToken - an optional access token is required for SASjs server type.
|
||||
* @returns - a promise which resolves with an object containing two values - a boolean `isLoggedIn`, and a string `userName`.
|
||||
*/
|
||||
public async checkSession(accessToken?: string) {
|
||||
return this.authManager!.checkSession(accessToken)
|
||||
public async checkSession() {
|
||||
return this.authManager!.checkSession()
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -540,35 +526,15 @@ export default class SASjs {
|
||||
* @param username - a string representing the username.
|
||||
* @param password - a string representing the password.
|
||||
*/
|
||||
public async logIn(
|
||||
username?: string,
|
||||
password?: 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)
|
||||
public async logIn(username: string, password: string) {
|
||||
return this.authManager!.logIn(username, password)
|
||||
}
|
||||
|
||||
/**
|
||||
* Logs out of the configured SAS server.
|
||||
* @param accessToken - an optional access token is required for SASjs server type.
|
||||
*/
|
||||
public logOut(accessToken?: string) {
|
||||
return this.authManager!.logOut(accessToken)
|
||||
public logOut() {
|
||||
return this.authManager!.logOut()
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -578,32 +544,24 @@ export default class SASjs {
|
||||
* 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.
|
||||
* @param overrideSasjsConfig - object to override existing config (optional)
|
||||
*/
|
||||
public async uploadFile(
|
||||
public uploadFile(
|
||||
sasJob: string,
|
||||
files: UploadFile[],
|
||||
params: { [key: string]: any } | null,
|
||||
config: { [key: string]: any } = {},
|
||||
loginRequiredCallback?: () => any
|
||||
params: any,
|
||||
overrideSasjsConfig?: any
|
||||
) {
|
||||
config = {
|
||||
...this.sasjsConfig,
|
||||
...config
|
||||
}
|
||||
const data = { files, params }
|
||||
const fileUploader = overrideSasjsConfig
|
||||
? new FileUploader(
|
||||
{ ...this.sasjsConfig, ...overrideSasjsConfig },
|
||||
this.jobsPath,
|
||||
this.requestClient!
|
||||
)
|
||||
: this.fileUploader ||
|
||||
new FileUploader(this.sasjsConfig, this.jobsPath, this.requestClient!)
|
||||
|
||||
return await this.fileUploader!.execute(
|
||||
sasJob,
|
||||
data,
|
||||
config,
|
||||
loginRequiredCallback
|
||||
)
|
||||
return fileUploader.uploadFile(sasJob, files, params)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -661,11 +619,6 @@ export default class SASjs {
|
||||
authConfig
|
||||
)
|
||||
} else {
|
||||
if (!config.contextName)
|
||||
config = {
|
||||
...config,
|
||||
contextName: 'SAS Job Execution compute context'
|
||||
}
|
||||
return await this.jesJobExecutor!.execute(
|
||||
sasJob,
|
||||
data,
|
||||
@@ -796,11 +749,7 @@ export default class SASjs {
|
||||
)
|
||||
sasApiClient.debug = this.sasjsConfig.debug
|
||||
} else if (this.sasjsConfig.serverType === ServerType.Sas9) {
|
||||
sasApiClient = new SAS9ApiClient(
|
||||
serverUrl,
|
||||
this.jobsPath,
|
||||
this.sasjsConfig.httpsAgentOptions
|
||||
)
|
||||
sasApiClient = new SAS9ApiClient(serverUrl, this.jobsPath)
|
||||
}
|
||||
} else {
|
||||
let sasClientConfig: any = null
|
||||
@@ -831,14 +780,6 @@ export default class SASjs {
|
||||
)
|
||||
}
|
||||
|
||||
public async deployToSASjs(members: [FolderMember, ServiceMember]) {
|
||||
return await this.SASjsApiClient?.deploy(members, this.sasjsConfig.appLoc)
|
||||
}
|
||||
|
||||
public async executeJobSASjs(query: ExecutionQuery) {
|
||||
return await this.SASjsApiClient?.executeJob(query)
|
||||
}
|
||||
|
||||
/**
|
||||
* Kicks off execution of the given job via the compute API.
|
||||
* @returns an object representing the compute session created for the given job.
|
||||
@@ -897,7 +838,6 @@ export default class SASjs {
|
||||
await this.webJobExecutor?.resendWaitingRequests()
|
||||
await this.computeJobExecutor?.resendWaitingRequests()
|
||||
await this.jesJobExecutor?.resendWaitingRequests()
|
||||
await this.fileUploader?.resendWaitingRequests()
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -929,18 +869,20 @@ export default class SASjs {
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* this method returns an array of SASjsRequest
|
||||
* @returns SASjsRequest[]
|
||||
*/
|
||||
public getSasRequests() {
|
||||
const requests = [...this.requestClient!.getRequests()]
|
||||
const requests = [
|
||||
...this.webJobExecutor!.getRequests(),
|
||||
...this.computeJobExecutor!.getRequests(),
|
||||
...this.jesJobExecutor!.getRequests()
|
||||
]
|
||||
const sortedRequests = requests.sort(compareTimestamps)
|
||||
return sortedRequests
|
||||
}
|
||||
|
||||
public clearSasRequests() {
|
||||
this.requestClient!.clearRequests()
|
||||
this.webJobExecutor!.clearRequests()
|
||||
this.computeJobExecutor!.clearRequests()
|
||||
this.jesJobExecutor!.clearRequests()
|
||||
}
|
||||
|
||||
private setupConfiguration() {
|
||||
@@ -963,24 +905,15 @@ export default class SASjs {
|
||||
this.sasjsConfig.serverUrl = this.sasjsConfig.serverUrl.slice(0, -1)
|
||||
}
|
||||
|
||||
if (!this.requestClient) {
|
||||
this.requestClient = new RequestClient(
|
||||
this.sasjsConfig.serverUrl,
|
||||
this.sasjsConfig.httpsAgentOptions
|
||||
)
|
||||
} else {
|
||||
this.requestClient.setConfig(
|
||||
this.sasjsConfig.serverUrl,
|
||||
this.sasjsConfig.httpsAgentOptions
|
||||
)
|
||||
}
|
||||
this.requestClient = new RequestClient(
|
||||
this.sasjsConfig.serverUrl,
|
||||
this.sasjsConfig.allowInsecureRequests
|
||||
)
|
||||
|
||||
this.jobsPath =
|
||||
this.sasjsConfig.serverType === ServerType.SasViya
|
||||
? this.sasjsConfig.pathSASViya
|
||||
: this.sasjsConfig.serverType === ServerType.Sas9
|
||||
? this.sasjsConfig.pathSAS9
|
||||
: this.sasjsConfig.pathSASJS || ''
|
||||
: this.sasjsConfig.pathSAS9
|
||||
|
||||
this.authManager = new AuthManager(
|
||||
this.sasjsConfig.serverUrl,
|
||||
@@ -990,49 +923,33 @@ export default class SASjs {
|
||||
)
|
||||
|
||||
if (this.sasjsConfig.serverType === ServerType.SasViya) {
|
||||
if (this.sasViyaApiClient) {
|
||||
if (this.sasViyaApiClient)
|
||||
this.sasViyaApiClient!.setConfig(
|
||||
this.sasjsConfig.serverUrl,
|
||||
this.sasjsConfig.appLoc
|
||||
)
|
||||
} else {
|
||||
else
|
||||
this.sasViyaApiClient = new SASViyaApiClient(
|
||||
this.sasjsConfig.serverUrl,
|
||||
this.sasjsConfig.appLoc,
|
||||
this.sasjsConfig.contextName,
|
||||
this.requestClient
|
||||
)
|
||||
}
|
||||
|
||||
this.sasViyaApiClient.debug = this.sasjsConfig.debug
|
||||
}
|
||||
|
||||
if (this.sasjsConfig.serverType === ServerType.Sas9) {
|
||||
if (this.sas9ApiClient) {
|
||||
if (this.sas9ApiClient)
|
||||
this.sas9ApiClient!.setConfig(this.sasjsConfig.serverUrl)
|
||||
} else {
|
||||
else
|
||||
this.sas9ApiClient = new SAS9ApiClient(
|
||||
this.sasjsConfig.serverUrl,
|
||||
this.jobsPath,
|
||||
this.sasjsConfig.httpsAgentOptions
|
||||
this.jobsPath
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (this.sasjsConfig.serverType === ServerType.Sasjs) {
|
||||
if (this.SASjsApiClient) {
|
||||
this.SASjsApiClient.setConfig(this.sasjsConfig.serverUrl)
|
||||
} else {
|
||||
this.SASjsApiClient = new SASjsApiClient(
|
||||
this.sasjsConfig.serverUrl,
|
||||
this.requestClient
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
this.fileUploader = new FileUploader(
|
||||
this.sasjsConfig.serverUrl,
|
||||
this.sasjsConfig.serverType!,
|
||||
this.sasjsConfig,
|
||||
this.jobsPath,
|
||||
this.requestClient
|
||||
)
|
||||
@@ -1048,9 +965,7 @@ export default class SASjs {
|
||||
this.sas9JobExecutor = new Sas9JobExecutor(
|
||||
this.sasjsConfig.serverUrl,
|
||||
this.sasjsConfig.serverType!,
|
||||
this.jobsPath,
|
||||
this.requestClient,
|
||||
this.sasjsConfig.httpsAgentOptions
|
||||
this.jobsPath
|
||||
)
|
||||
|
||||
this.computeJobExecutor = new ComputeJobExecutor(
|
||||
|
||||
@@ -1,39 +0,0 @@
|
||||
import { FolderMember, ServiceMember, ExecutionQuery } from './types'
|
||||
import { RequestClient } from './request/RequestClient'
|
||||
|
||||
export class SASjsApiClient {
|
||||
constructor(
|
||||
private serverUrl: string,
|
||||
private requestClient: RequestClient
|
||||
) {}
|
||||
|
||||
public setConfig(serverUrl: string) {
|
||||
if (serverUrl) this.serverUrl = serverUrl
|
||||
}
|
||||
|
||||
public async deploy(members: [FolderMember, ServiceMember], appLoc: string) {
|
||||
const { result } = await this.requestClient.post<{
|
||||
status: string
|
||||
message: string
|
||||
example?: {}
|
||||
}>(
|
||||
'SASjsApi/drive/deploy',
|
||||
{ fileTree: members, appLoc: appLoc },
|
||||
undefined
|
||||
)
|
||||
|
||||
return Promise.resolve(result)
|
||||
}
|
||||
|
||||
public async executeJob(query: ExecutionQuery) {
|
||||
const { result } = await this.requestClient.post<{
|
||||
status: string
|
||||
message: string
|
||||
log?: string
|
||||
logPath?: string
|
||||
error?: {}
|
||||
}>('SASjsApi/stp/execute', query, undefined)
|
||||
|
||||
return Promise.resolve(result)
|
||||
}
|
||||
}
|
||||
@@ -5,10 +5,10 @@ import { prefixMessage } from '@sasjs/utils/error'
|
||||
import { RequestClient } from './request/RequestClient'
|
||||
|
||||
const MAX_SESSION_COUNT = 1
|
||||
const RETRY_LIMIT: number = 3
|
||||
let RETRY_COUNT: number = 0
|
||||
|
||||
export class SessionManager {
|
||||
private loggedErrors: NoSessionStateError[] = []
|
||||
|
||||
constructor(
|
||||
private serverUrl: string,
|
||||
private contextName: string,
|
||||
@@ -154,75 +154,69 @@ export class SessionManager {
|
||||
session: Session,
|
||||
etag: string | null,
|
||||
accessToken?: string
|
||||
): Promise<string> {
|
||||
) {
|
||||
const logger = process.logger || console
|
||||
|
||||
let sessionState = session.state
|
||||
|
||||
const stateLink = session.links.find((l: any) => l.rel === 'state')
|
||||
|
||||
if (
|
||||
sessionState === 'pending' ||
|
||||
sessionState === 'running' ||
|
||||
sessionState === ''
|
||||
) {
|
||||
if (stateLink) {
|
||||
if (this.debug && !this.printedSessionState.printed) {
|
||||
logger.info('Polling session status...')
|
||||
return new Promise(async (resolve, reject) => {
|
||||
if (
|
||||
sessionState === 'pending' ||
|
||||
sessionState === 'running' ||
|
||||
sessionState === ''
|
||||
) {
|
||||
if (stateLink) {
|
||||
if (this.debug && !this.printedSessionState.printed) {
|
||||
logger.info('Polling session status...')
|
||||
|
||||
this.printedSessionState.printed = true
|
||||
}
|
||||
|
||||
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.')
|
||||
})
|
||||
|
||||
sessionState = state.trim()
|
||||
|
||||
if (this.debug && this.printedSessionState.state !== sessionState) {
|
||||
logger.info(`Current session state is '${sessionState}'`)
|
||||
|
||||
this.printedSessionState.state = sessionState
|
||||
this.printedSessionState.printed = false
|
||||
}
|
||||
|
||||
if (!sessionState) {
|
||||
const stateError = new NoSessionStateError(
|
||||
responseStatus,
|
||||
this.serverUrl + stateLink.href,
|
||||
session.links.find((l: any) => l.rel === 'log')?.href as string
|
||||
)
|
||||
|
||||
if (
|
||||
!this.loggedErrors.find(
|
||||
(err: NoSessionStateError) =>
|
||||
err.serverResponseStatus === stateError.serverResponseStatus
|
||||
)
|
||||
) {
|
||||
this.loggedErrors.push(stateError)
|
||||
|
||||
logger.info(stateError.message)
|
||||
this.printedSessionState.printed = true
|
||||
}
|
||||
|
||||
return await this.waitForSession(session, etag, accessToken)
|
||||
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.')
|
||||
})
|
||||
|
||||
sessionState = state.trim()
|
||||
|
||||
if (this.debug && this.printedSessionState.state !== sessionState) {
|
||||
logger.info(`Current session state is '${sessionState}'`)
|
||||
|
||||
this.printedSessionState.state = sessionState
|
||||
this.printedSessionState.printed = false
|
||||
}
|
||||
|
||||
// There is an internal error present in SAS Viya 3.5
|
||||
// Retry to wait for a session status in such case of SAS internal error
|
||||
if (!sessionState) {
|
||||
if (RETRY_COUNT < RETRY_LIMIT) {
|
||||
RETRY_COUNT++
|
||||
|
||||
resolve(this.waitForSession(session, etag, accessToken))
|
||||
} else {
|
||||
reject(
|
||||
new NoSessionStateError(
|
||||
responseStatus,
|
||||
this.serverUrl + stateLink.href,
|
||||
session.links.find((l: any) => l.rel === 'log')
|
||||
?.href as string
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
resolve(sessionState)
|
||||
}
|
||||
|
||||
this.loggedErrors = []
|
||||
|
||||
return sessionState
|
||||
} else {
|
||||
throw 'Error while getting session state link.'
|
||||
resolve(sessionState)
|
||||
}
|
||||
} else {
|
||||
this.loggedErrors = []
|
||||
|
||||
return sessionState
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private async getSessionState(
|
||||
|
||||
@@ -4,7 +4,7 @@ import { getTokens } from '../../auth/getTokens'
|
||||
import { RequestClient } from '../../request/RequestClient'
|
||||
import { JobStatePollError } from '../../types/errors'
|
||||
import { Link, WriteStream } from '../../types'
|
||||
import { delay, isNode } from '../../utils'
|
||||
import { isNode } from '../../utils'
|
||||
|
||||
export async function pollJobState(
|
||||
requestClient: RequestClient,
|
||||
@@ -246,3 +246,5 @@ const doPoll = async (
|
||||
|
||||
return { state, pollCount }
|
||||
}
|
||||
|
||||
const delay = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms))
|
||||
|
||||
@@ -1,16 +1,11 @@
|
||||
import { ServerType } from '@sasjs/utils/types'
|
||||
import { RequestClient } from '../request/RequestClient'
|
||||
import { LoginOptions, LoginResult } from '../types/Login'
|
||||
import { serialize } from '../utils'
|
||||
import { openWebPage } from './openWebPage'
|
||||
import { verifySas9Login } from './verifySas9Login'
|
||||
import { verifySasViyaLogin } from './verifySasViyaLogin'
|
||||
|
||||
export class AuthManager {
|
||||
public userName = ''
|
||||
private loginUrl: string
|
||||
private logoutUrl: string
|
||||
private redirectedLoginUrl = `/SASLogon/home`
|
||||
constructor(
|
||||
private serverUrl: string,
|
||||
private serverType: ServerType,
|
||||
@@ -21,142 +16,68 @@ export class AuthManager {
|
||||
this.logoutUrl =
|
||||
this.serverType === ServerType.Sas9
|
||||
? '/SASLogon/logout?'
|
||||
: this.serverType === ServerType.SasViya
|
||||
? '/SASLogon/logout.do?'
|
||||
: '/SASjsApi/auth/logout'
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens Pop up window to SAS Login screen.
|
||||
* And checks if user has finished login process.
|
||||
*/
|
||||
public async redirectedLogIn({
|
||||
onLoggedOut
|
||||
}: LoginOptions): Promise<LoginResult> {
|
||||
const { isLoggedIn: isLoggedInAlready, userName: currentSessionUsername } =
|
||||
await this.fetchUserName()
|
||||
|
||||
if (isLoggedInAlready) {
|
||||
await this.loginCallback()
|
||||
|
||||
return {
|
||||
isLoggedIn: true,
|
||||
userName: currentSessionUsername
|
||||
}
|
||||
}
|
||||
|
||||
const loginPopup = await openWebPage(
|
||||
this.redirectedLoginUrl,
|
||||
'SASLogon',
|
||||
{
|
||||
width: 500,
|
||||
height: 600
|
||||
},
|
||||
onLoggedOut
|
||||
)
|
||||
|
||||
if (!loginPopup) {
|
||||
return { isLoggedIn: false, userName: '' }
|
||||
}
|
||||
|
||||
const { isLoggedIn } =
|
||||
this.serverType === ServerType.SasViya
|
||||
? await verifySasViyaLogin(loginPopup)
|
||||
: await verifySas9Login(loginPopup)
|
||||
|
||||
loginPopup.close()
|
||||
|
||||
if (isLoggedIn) {
|
||||
if (this.serverType === ServerType.Sas9) {
|
||||
await this.performCASSecurityCheck()
|
||||
}
|
||||
|
||||
const { userName } = await this.fetchUserName()
|
||||
|
||||
await this.loginCallback()
|
||||
|
||||
return { isLoggedIn: true, userName }
|
||||
}
|
||||
|
||||
return { isLoggedIn: false, userName: '' }
|
||||
: '/SASLogon/logout.do?'
|
||||
}
|
||||
|
||||
/**
|
||||
* Logs into the SAS server with the supplied credentials.
|
||||
* @param username - a string representing the username.
|
||||
* @param password - a string representing the password.
|
||||
* @returns - a boolean `isLoggedin` and a string `username`
|
||||
*/
|
||||
public async logIn(username: string, password: string): Promise<LoginResult> {
|
||||
const loginParams = {
|
||||
public async logIn(username: string, password: string) {
|
||||
const loginParams: any = {
|
||||
_service: 'default',
|
||||
username,
|
||||
password
|
||||
}
|
||||
|
||||
let {
|
||||
isLoggedIn: isLoggedInAlready,
|
||||
loginForm,
|
||||
userName: currentSessionUsername
|
||||
} = await this.checkSession()
|
||||
this.userName = loginParams.username
|
||||
|
||||
if (isLoggedInAlready) {
|
||||
if (currentSessionUsername === loginParams.username) {
|
||||
await this.loginCallback()
|
||||
const { isLoggedIn, loginForm } = await this.checkSession()
|
||||
|
||||
this.userName = currentSessionUsername!
|
||||
return {
|
||||
isLoggedIn: true,
|
||||
userName: this.userName
|
||||
}
|
||||
} else {
|
||||
await this.logOut()
|
||||
loginForm = await this.getNewLoginForm()
|
||||
if (isLoggedIn) {
|
||||
await this.loginCallback()
|
||||
|
||||
return {
|
||||
isLoggedIn,
|
||||
userName: this.userName
|
||||
}
|
||||
} else this.userName = ''
|
||||
}
|
||||
|
||||
let loginResponse = await this.sendLoginRequest(loginForm, loginParams)
|
||||
|
||||
let isLoggedIn = isLogInSuccess(loginResponse)
|
||||
let loggedIn = isLogInSuccess(loginResponse)
|
||||
|
||||
if (!isLoggedIn) {
|
||||
if (!loggedIn) {
|
||||
if (isCredentialsVerifyError(loginResponse)) {
|
||||
const newLoginForm = await this.getLoginForm(loginResponse)
|
||||
|
||||
loginResponse = await this.sendLoginRequest(newLoginForm, loginParams)
|
||||
}
|
||||
|
||||
const res = await this.checkSession()
|
||||
isLoggedIn = res.isLoggedIn
|
||||
|
||||
if (isLoggedIn) this.userName = res.userName
|
||||
} else {
|
||||
this.userName = loginParams.username
|
||||
const currentSession = await this.checkSession()
|
||||
loggedIn = currentSession.isLoggedIn
|
||||
}
|
||||
|
||||
if (isLoggedIn) {
|
||||
if (loggedIn) {
|
||||
if (this.serverType === ServerType.Sas9) {
|
||||
await this.performCASSecurityCheck()
|
||||
const casAuthenticationUrl = `${this.serverUrl}/SASStoredProcess/j_spring_cas_security_check`
|
||||
|
||||
await this.requestClient.get<string>(
|
||||
`/SASLogon/login?service=${casAuthenticationUrl}`,
|
||||
undefined
|
||||
)
|
||||
}
|
||||
|
||||
this.loginCallback()
|
||||
} else this.userName = ''
|
||||
}
|
||||
|
||||
return {
|
||||
isLoggedIn,
|
||||
isLoggedIn: !!loggedIn,
|
||||
userName: this.userName
|
||||
}
|
||||
}
|
||||
|
||||
private async performCASSecurityCheck() {
|
||||
const casAuthenticationUrl = `${this.serverUrl}/SASStoredProcess/j_spring_cas_security_check`
|
||||
|
||||
await this.requestClient.get<string>(
|
||||
`/SASLogon/login?service=${casAuthenticationUrl}`,
|
||||
undefined
|
||||
)
|
||||
}
|
||||
|
||||
private async sendLoginRequest(
|
||||
loginForm: { [key: string]: any },
|
||||
loginParams: { [key: string]: any }
|
||||
@@ -182,92 +103,43 @@ export class AuthManager {
|
||||
|
||||
/**
|
||||
* Checks whether a session is active, or login is required.
|
||||
* @param accessToken - an optional access token is required for SASjs server type.
|
||||
* @returns - a promise which resolves with an object containing three values
|
||||
* - a boolean `isLoggedIn`
|
||||
* - a string `userName` and
|
||||
* - a form `loginForm` if not loggedin.
|
||||
* @returns - a promise which resolves with an object containing two values - a boolean `isLoggedIn`, and a string `userName`.
|
||||
*/
|
||||
public async checkSession(accessToken?: string): Promise<{
|
||||
isLoggedIn: boolean
|
||||
userName: string
|
||||
loginForm?: any
|
||||
}> {
|
||||
const { isLoggedIn, userName } = await this.fetchUserName(accessToken)
|
||||
let loginForm = null
|
||||
|
||||
if (!isLoggedIn && this.serverType !== ServerType.Sasjs) {
|
||||
//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()
|
||||
|
||||
loginForm = await this.getNewLoginForm()
|
||||
}
|
||||
|
||||
return Promise.resolve({
|
||||
isLoggedIn,
|
||||
userName: userName.toLowerCase(),
|
||||
loginForm
|
||||
})
|
||||
}
|
||||
|
||||
private async getNewLoginForm() {
|
||||
const { result: formResponse } = await this.requestClient.get<string>(
|
||||
this.loginUrl.replace('.do', ''),
|
||||
undefined,
|
||||
'text/plain'
|
||||
)
|
||||
|
||||
return await this.getLoginForm(formResponse)
|
||||
}
|
||||
|
||||
private async fetchUserName(accessToken?: string): Promise<{
|
||||
isLoggedIn: boolean
|
||||
userName: string
|
||||
}> {
|
||||
public async checkSession() {
|
||||
//For VIYA we will send request on API endpoint. Which is faster then pinging SASJobExecution.
|
||||
//For SAS9 we will send request on SASStoredProcess
|
||||
const url =
|
||||
this.serverType === ServerType.SasViya
|
||||
? `${this.serverUrl}/identities/users/@currentUser`
|
||||
: this.serverType === ServerType.Sas9
|
||||
? `${this.serverUrl}/SASStoredProcess`
|
||||
: `${this.serverUrl}/SASjsApi/session`
|
||||
this.serverType === 'SASVIYA'
|
||||
? `${this.serverUrl}/identities`
|
||||
: `${this.serverUrl}/SASStoredProcess`
|
||||
|
||||
// Access token is required for server type `SASjs`
|
||||
const { result: loginResponse } = await this.requestClient
|
||||
.get<string>(url, accessToken, 'text/plain')
|
||||
.get<string>(url, undefined, 'text/plain')
|
||||
.catch((err: any) => {
|
||||
return { result: 'authErr' }
|
||||
})
|
||||
|
||||
const isLoggedIn = loginResponse !== 'authErr'
|
||||
const userName = isLoggedIn ? this.extractUserName(loginResponse) : ''
|
||||
let loginForm = null
|
||||
|
||||
return { isLoggedIn, userName }
|
||||
}
|
||||
if (!isLoggedIn) {
|
||||
//We will logout to make sure cookies are removed and login form is presented
|
||||
this.logOut()
|
||||
|
||||
private extractUserName = (response: any): string => {
|
||||
switch (this.serverType) {
|
||||
case ServerType.SasViya:
|
||||
return response?.id
|
||||
const { result: formResponse } = await this.requestClient.get<string>(
|
||||
this.loginUrl.replace('.do', ''),
|
||||
undefined,
|
||||
'text/plain'
|
||||
)
|
||||
|
||||
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('')
|
||||
|
||||
case ServerType.Sasjs:
|
||||
return response?.username
|
||||
|
||||
default:
|
||||
console.error('Server Type not found in extractUserName function')
|
||||
return ''
|
||||
loginForm = await this.getLoginForm(formResponse)
|
||||
}
|
||||
|
||||
return Promise.resolve({
|
||||
isLoggedIn,
|
||||
userName: this.userName,
|
||||
loginForm
|
||||
})
|
||||
}
|
||||
|
||||
private getLoginForm(response: any) {
|
||||
@@ -313,12 +185,8 @@ export class AuthManager {
|
||||
|
||||
/**
|
||||
* Logs out of the configured SAS server.
|
||||
* @param accessToken - an optional access token is required for SASjs server type.
|
||||
*/
|
||||
public logOut(accessToken?: string) {
|
||||
if (this.serverType === ServerType.Sasjs) {
|
||||
return this.requestClient.post(this.logoutUrl, undefined, accessToken)
|
||||
}
|
||||
public logOut() {
|
||||
this.requestClient.clearCsrfTokens()
|
||||
return this.requestClient.get(this.logoutUrl, undefined).then(() => true)
|
||||
}
|
||||
|
||||
@@ -46,7 +46,7 @@ export async function getAccessToken(
|
||||
)
|
||||
.then((res) => res.result as SasAuthResponse)
|
||||
.catch((err) => {
|
||||
throw prefixMessage(err, 'Error while getting access token. ')
|
||||
throw prefixMessage(err, 'Error while getting access token')
|
||||
})
|
||||
|
||||
return authResponse
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user