mirror of
https://github.com/sasjs/adapter.git
synced 2025-12-11 09:24:35 +00:00
Compare commits
207 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7cb2a43f95 | ||
|
|
6e85c7a588 | ||
|
|
a68f6962fd | ||
|
|
a650ba15dd | ||
|
|
6ca1b489fc | ||
|
|
a5c9f11c75 | ||
|
|
d4ebef4290 | ||
| 89590f9a37 | |||
| 5d61bebc9e | |||
| 99afa6e7e4 | |||
| b590a9f41b | |||
| 4466ee30d2 | |||
| db372950b4 | |||
| 46f5e07f11 | |||
|
|
a00cb1ebec | ||
|
|
7b1264d140 | ||
|
|
369b9fb023 | ||
|
|
76487b00e9 | ||
|
|
2d0515e25b | ||
|
|
b132b99586 | ||
|
|
5a7b4a1de4 | ||
|
|
6cac008b61 | ||
|
|
929ec6eb1c | ||
|
|
5a35237de5 | ||
|
|
5d77bbba8b | ||
|
|
eda021b6a5 | ||
|
|
259c479ef0 | ||
|
|
a962b8e7cf | ||
|
|
eb0e7247a6 | ||
| ccc77cb9d1 | |||
|
|
5cb5bbdb55 | ||
|
|
ac6cd7be82 | ||
|
|
63f5f4d03d | ||
|
|
a164fb7df9 | ||
|
|
336ba207cf | ||
|
|
3cfd45cc62 | ||
|
|
f7fb917282 | ||
|
|
a182037883 | ||
|
|
f9e79fb756 | ||
|
|
aaf0eef62b | ||
|
|
fafa0c3567 | ||
|
|
4a6845ad6a | ||
|
|
61d66c6f82 | ||
| 123fbc7235 | |||
|
|
eae8694a29 | ||
|
|
2b16be3aef | ||
|
|
d8d4da9c9a | ||
|
|
0b755b7304 | ||
|
|
0816b7b1f9 | ||
|
|
97d45e87ec | ||
|
|
57ef0647b5 | ||
|
|
a34eebba44 | ||
| 857e39eb33 | |||
| 9bd7d84975 | |||
| 731e38bce3 | |||
|
|
2ee6c45d16 | ||
|
|
b80283f8af | ||
|
|
291e23e40a | ||
| d53d1e1e6a | |||
| 8cf249e8fd | |||
| 5d7cfe1e6c | |||
| abc15fb3ab | |||
| 8cc4270e48 | |||
|
|
56b2ba026a | ||
|
|
8beda1ad6c | ||
|
|
b18b471549 | ||
| 93c9a34591 | |||
|
|
9493492dea | ||
|
|
9b976d48ca | ||
|
|
00b19de497 | ||
|
|
f4cdd2d607 | ||
|
|
cdc0c12ec4 | ||
|
|
bc6f109c48 | ||
|
|
cfab64cfa0 | ||
|
|
d4c8c58552 | ||
|
|
2b8cb51a50 | ||
|
|
e068d3263c | ||
| 630f2e9c37 | |||
| 51ac6b052b | |||
| c32258eb3c | |||
|
|
88f50e3c74 | ||
|
|
bfe5ac0ff7 | ||
|
|
d50f5a030a | ||
|
|
c320caec99 | ||
|
|
16a5b2b012 | ||
|
|
2951e0cc2d | ||
|
|
6bb4a7ea18 | ||
|
|
2827978fe5 | ||
|
|
541c19c1a4 | ||
|
|
c5e995f8d6 | ||
|
|
8bf36da566 | ||
| ccb4ec6e03 | |||
| 06ebb52bc9 | |||
|
|
6e23a0362f | ||
| a59d78bcf7 | |||
| 33d4ee92a7 | |||
| dadce3d4c9 | |||
|
|
b61cf34723 | ||
|
|
22445d1268 | ||
|
|
cba9dacb37 | ||
|
|
a055b36c5c | ||
| 06895cc9f8 | |||
| 24496a997a | |||
| 6419686269 | |||
|
|
4554c9100c | ||
| 919c83c143 | |||
| 00ba2957fb | |||
| 5beda6547a | |||
| bd49b3757a | |||
|
|
b32352a369 | ||
| b306f11148 | |||
|
|
8c4955cb65 | ||
|
|
155f2bb0e8 | ||
| 3ca971134a | |||
| 488d8b9316 | |||
| c20bdba4ae | |||
| 0be2d69aee | |||
| a6e67c3478 | |||
| 5968988984 | |||
| 31cd01610a | |||
| a67824762c | |||
|
|
0336541d40 | ||
|
|
01de3836d7 | ||
|
|
c571bb8490 | ||
|
|
5b4d354ea2 | ||
|
|
b0ce0dc40a | ||
| 88f70a7966 | |||
| 89ff323206 | |||
| d4357d939e | |||
|
|
6cb76f0b5c | ||
|
|
ba2baa36c0 | ||
|
|
e36cd785e8 | ||
| 2fa3a353fa | |||
|
|
bdb1ffb2ef | ||
|
|
84090661cf | ||
|
|
68e14bbf05 | ||
|
|
e4f23334d3 | ||
|
|
5593963b89 | ||
|
|
81c9138b93 | ||
|
|
83fa82108b | ||
|
|
76039c3ec7 | ||
|
|
9b57c9ca1c | ||
|
|
4018cf95ba | ||
|
|
173b6e3e8d | ||
|
|
0ed5447aff | ||
|
|
6344a906d8 | ||
|
|
b2c135ae61 | ||
|
|
2032aacba3 | ||
|
|
fadccfc94c | ||
|
|
551e4e43c1 | ||
|
|
1867658cde | ||
| 3fff4f9c4d | |||
|
|
3f119432db | ||
| 0b18fddc3e | |||
| 19503e0b31 | |||
| d8bdc02f09 | |||
| 2d0833061f | |||
|
|
5dfc4e4086 | ||
|
|
c5824a8a8d | ||
|
|
2147c59314 | ||
|
|
56a1960fff | ||
| b8c9522a55 | |||
| b461cff731 | |||
| 728167fd71 | |||
| 460575b462 | |||
|
|
b247da249a | ||
|
|
e79089b880 | ||
|
|
fe907e1c43 | ||
|
|
e95e894365 | ||
|
|
82414d8b8b | ||
|
|
456fa68f0f | ||
|
|
076adc1f6a | ||
|
|
9676488ff2 | ||
|
|
e9affb862d | ||
|
|
e04371510e | ||
|
|
19657a1c12 | ||
|
|
6424c82ac9 | ||
|
|
fcab18191f | ||
|
|
f157612a0e | ||
|
|
b8cb7d52e7 | ||
|
|
d8d1968162 | ||
|
|
0e1d1f1d99 | ||
|
|
0b055dd05f | ||
|
|
ba91c29ba8 | ||
|
|
bd19457c2a | ||
|
|
b0570e1cd9 | ||
|
|
a5f1b59f7b | ||
|
|
01ca29fc01 | ||
|
|
ed9648fdf9 | ||
|
|
7e17aa6eb3 | ||
|
|
9caee9941a | ||
|
|
e309e7a4f4 | ||
|
|
c47441d6d4 | ||
|
|
1844bc48ac | ||
|
|
7a5adebdb5 | ||
|
|
b39f0c577b | ||
|
|
15f4065cd8 | ||
|
|
4c67665b4d | ||
|
|
55e64ae9d6 | ||
|
|
76d0b82b4c | ||
|
|
95d65d270d | ||
|
|
4e5c9c1ccd | ||
|
|
3267af0724 | ||
|
|
75120424d0 | ||
|
|
f8c6318a88 | ||
|
|
dc98ce3b0b | ||
|
|
9b32b28aa7 |
18
.git-hooks/commit-msg
Executable file
18
.git-hooks/commit-msg
Executable file
@@ -0,0 +1,18 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
RED="\033[1;31m"
|
||||||
|
GREEN="\033[1;32m"
|
||||||
|
|
||||||
|
# Get the commit message (the parameter we're given is just the path to the
|
||||||
|
# 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
|
||||||
|
echo "${GREEN} ✔ Commit message meets Conventional Commit standards"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "${RED}❌ Commit message does not meet the Conventional Commit standard!"
|
||||||
|
echo "An example of a valid message is:"
|
||||||
|
echo " feat(login): add the 'remember me' button"
|
||||||
|
echo "ℹ More details at: https://www.conventionalcommits.org/en/v1.0.0/#summary"
|
||||||
|
exit 1
|
||||||
7
.github/dependabot.yml
vendored
Normal file
7
.github/dependabot.yml
vendored
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
version: 2
|
||||||
|
updates:
|
||||||
|
- package-ecosystem: npm
|
||||||
|
directory: "/"
|
||||||
|
schedule:
|
||||||
|
interval: daily
|
||||||
|
open-pull-requests-limit: 10
|
||||||
5
.github/reviewer-lottery.yml
vendored
5
.github/reviewer-lottery.yml
vendored
@@ -7,3 +7,8 @@ groups:
|
|||||||
- saadjutt01
|
- saadjutt01
|
||||||
- medjedovicm
|
- medjedovicm
|
||||||
- allanbowe
|
- allanbowe
|
||||||
|
- sabhas
|
||||||
|
- name: SASjs QA
|
||||||
|
reviewers: 1
|
||||||
|
usernames:
|
||||||
|
- VladislavParhomchik
|
||||||
|
|||||||
4
.npmignore
Normal file
4
.npmignore
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
sasjs-tests/
|
||||||
|
docs/
|
||||||
|
.github/
|
||||||
|
CONTRIBUTING.md
|
||||||
@@ -1,5 +1,9 @@
|
|||||||
# Change Log
|
# Change Log
|
||||||
|
|
||||||
|
Since March 2020 the changelog is managed by github releases - see [https://github.com/sasjs/adapter/releases](https://github.com/sasjs/adapter/releases).
|
||||||
|
|
||||||
|
## Changes up to 5th March 2020
|
||||||
|
|
||||||
All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
|
All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
|
||||||
|
|
||||||
<a name="1.9.0"></a>
|
<a name="1.9.0"></a>
|
||||||
|
|||||||
@@ -2,75 +2,127 @@
|
|||||||
|
|
||||||
## Our Pledge
|
## Our Pledge
|
||||||
|
|
||||||
In the interest of fostering an open and welcoming environment, we as
|
We as members, contributors, and leaders pledge to make participation in our
|
||||||
contributors and maintainers pledge to making participation in our project and
|
community a harassment-free experience for everyone, regardless of age, body
|
||||||
our community a harassment-free experience for everyone, regardless of age, body
|
size, visible or invisible disability, ethnicity, sex characteristics, gender
|
||||||
size, disability, ethnicity, sex characteristics, gender identity and expression,
|
identity and expression, level of experience, education, socio-economic status,
|
||||||
level of experience, education, socio-economic status, nationality, personal
|
nationality, personal appearance, race, religion, or sexual identity
|
||||||
appearance, race, religion, or sexual identity and orientation.
|
and orientation.
|
||||||
|
|
||||||
|
We pledge to act and interact in ways that contribute to an open, welcoming,
|
||||||
|
diverse, inclusive, and healthy community.
|
||||||
|
|
||||||
## Our Standards
|
## Our Standards
|
||||||
|
|
||||||
Examples of behavior that contributes to creating a positive environment
|
Examples of behavior that contributes to a positive environment for our
|
||||||
include:
|
community include:
|
||||||
|
|
||||||
* Using welcoming and inclusive language
|
* Demonstrating empathy and kindness toward other people
|
||||||
* Being respectful of differing viewpoints and experiences
|
* Being respectful of differing opinions, viewpoints, and experiences
|
||||||
* Gracefully accepting constructive criticism
|
* Giving and gracefully accepting constructive feedback
|
||||||
* Focusing on what is best for the community
|
* Accepting responsibility and apologizing to those affected by our mistakes,
|
||||||
* Showing empathy towards other community members
|
and learning from the experience
|
||||||
|
* Focusing on what is best not just for us as individuals, but for the
|
||||||
|
overall community
|
||||||
|
|
||||||
Examples of unacceptable behavior by participants include:
|
Examples of unacceptable behavior include:
|
||||||
|
|
||||||
* The use of sexualized language or imagery and unwelcome sexual attention or
|
* The use of sexualized language or imagery, and sexual attention or
|
||||||
advances
|
advances of any kind
|
||||||
* Trolling, insulting/derogatory comments, and personal or political attacks
|
* Trolling, insulting or derogatory comments, and personal or political attacks
|
||||||
* Public or private harassment
|
* Public or private harassment
|
||||||
* Publishing others' private information, such as a physical or electronic
|
* Publishing others' private information, such as a physical or email
|
||||||
address, without explicit permission
|
address, without their explicit permission
|
||||||
* Other conduct which could reasonably be considered inappropriate in a
|
* Other conduct which could reasonably be considered inappropriate in a
|
||||||
professional setting
|
professional setting
|
||||||
|
|
||||||
## Our Responsibilities
|
## Enforcement Responsibilities
|
||||||
|
|
||||||
Project maintainers are responsible for clarifying the standards of acceptable
|
Community leaders are responsible for clarifying and enforcing our standards of
|
||||||
behavior and are expected to take appropriate and fair corrective action in
|
acceptable behavior and will take appropriate and fair corrective action in
|
||||||
response to any instances of unacceptable behavior.
|
response to any behavior that they deem inappropriate, threatening, offensive,
|
||||||
|
or harmful.
|
||||||
|
|
||||||
Project maintainers have the right and responsibility to remove, edit, or
|
Community leaders have the right and responsibility to remove, edit, or reject
|
||||||
reject comments, commits, code, wiki edits, issues, and other contributions
|
comments, commits, code, wiki edits, issues, and other contributions that are
|
||||||
that are not aligned to this Code of Conduct, or to ban temporarily or
|
not aligned to this Code of Conduct, and will communicate reasons for moderation
|
||||||
permanently any contributor for other behaviors that they deem inappropriate,
|
decisions when appropriate.
|
||||||
threatening, offensive, or harmful.
|
|
||||||
|
|
||||||
## Scope
|
## Scope
|
||||||
|
|
||||||
This Code of Conduct applies both within project spaces and in public spaces
|
This Code of Conduct applies within all community spaces, and also applies when
|
||||||
when an individual is representing the project or its community. Examples of
|
an individual is officially representing the community in public spaces.
|
||||||
representing a project or community include using an official project e-mail
|
Examples of representing our community include using an official e-mail address,
|
||||||
address, posting via an official social media account, or acting as an appointed
|
posting via an official social media account, or acting as an appointed
|
||||||
representative at an online or offline event. Representation of a project may be
|
representative at an online or offline event.
|
||||||
further defined and clarified by project maintainers.
|
|
||||||
|
|
||||||
## Enforcement
|
## Enforcement
|
||||||
|
|
||||||
Instances of abusive, harassing, or otherwise unacceptable behavior may be
|
Instances of abusive, harassing, or otherwise unacceptable behavior may be
|
||||||
reported by contacting the project team at support@macropeople.com. All
|
reported to the community leaders responsible for enforcement at
|
||||||
complaints will be reviewed and investigated and will result in a response that
|
https://sasapps.io/contact-us.
|
||||||
is deemed necessary and appropriate to the circumstances. The project team is
|
All complaints will be reviewed and investigated promptly and fairly.
|
||||||
obligated to maintain confidentiality with regard to the reporter of an incident.
|
|
||||||
Further details of specific enforcement policies may be posted separately.
|
|
||||||
|
|
||||||
Project maintainers who do not follow or enforce the Code of Conduct in good
|
All community leaders are obligated to respect the privacy and security of the
|
||||||
faith may face temporary or permanent repercussions as determined by other
|
reporter of any incident.
|
||||||
members of the project's leadership.
|
|
||||||
|
## Enforcement Guidelines
|
||||||
|
|
||||||
|
Community leaders will follow these Community Impact Guidelines in determining
|
||||||
|
the consequences for any action they deem in violation of this Code of Conduct:
|
||||||
|
|
||||||
|
### 1. Correction
|
||||||
|
|
||||||
|
**Community Impact**: Use of inappropriate language or other behavior deemed
|
||||||
|
unprofessional or unwelcome in the community.
|
||||||
|
|
||||||
|
**Consequence**: A private, written warning from community leaders, providing
|
||||||
|
clarity around the nature of the violation and an explanation of why the
|
||||||
|
behavior was inappropriate. A public apology may be requested.
|
||||||
|
|
||||||
|
### 2. Warning
|
||||||
|
|
||||||
|
**Community Impact**: A violation through a single incident or series
|
||||||
|
of actions.
|
||||||
|
|
||||||
|
**Consequence**: A warning with consequences for continued behavior. No
|
||||||
|
interaction with the people involved, including unsolicited interaction with
|
||||||
|
those enforcing the Code of Conduct, for a specified period of time. This
|
||||||
|
includes avoiding interactions in community spaces as well as external channels
|
||||||
|
like social media. Violating these terms may lead to a temporary or
|
||||||
|
permanent ban.
|
||||||
|
|
||||||
|
### 3. Temporary Ban
|
||||||
|
|
||||||
|
**Community Impact**: A serious violation of community standards, including
|
||||||
|
sustained inappropriate behavior.
|
||||||
|
|
||||||
|
**Consequence**: A temporary ban from any sort of interaction or public
|
||||||
|
communication with the community for a specified period of time. No public or
|
||||||
|
private interaction with the people involved, including unsolicited interaction
|
||||||
|
with those enforcing the Code of Conduct, is allowed during this period.
|
||||||
|
Violating these terms may lead to a permanent ban.
|
||||||
|
|
||||||
|
### 4. Permanent Ban
|
||||||
|
|
||||||
|
**Community Impact**: Demonstrating a pattern of violation of community
|
||||||
|
standards, including sustained inappropriate behavior, harassment of an
|
||||||
|
individual, or aggression toward or disparagement of classes of individuals.
|
||||||
|
|
||||||
|
**Consequence**: A permanent ban from any sort of public interaction within
|
||||||
|
the community.
|
||||||
|
|
||||||
## Attribution
|
## Attribution
|
||||||
|
|
||||||
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
|
This Code of Conduct is adapted from the [Contributor Covenant][homepage],
|
||||||
available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html
|
version 2.0, available at
|
||||||
|
https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
|
||||||
|
|
||||||
|
Community Impact Guidelines were inspired by [Mozilla's code of conduct
|
||||||
|
enforcement ladder](https://github.com/mozilla/diversity).
|
||||||
|
|
||||||
[homepage]: https://www.contributor-covenant.org
|
[homepage]: https://www.contributor-covenant.org
|
||||||
|
|
||||||
For answers to common questions about this code of conduct, see
|
For answers to common questions about this code of conduct, see the FAQ at
|
||||||
https://www.contributor-covenant.org/faq
|
https://www.contributor-covenant.org/faq. Translations are available at
|
||||||
|
https://www.contributor-covenant.org/translations.
|
||||||
|
|||||||
2
LICENSE
2
LICENSE
@@ -1,6 +1,6 @@
|
|||||||
MIT License
|
MIT License
|
||||||
|
|
||||||
Copyright (c) 2020 Macro People
|
Copyright (c) 2021 Macro People
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
|||||||
@@ -12,6 +12,8 @@ What code changes have been made to achieve the intent.
|
|||||||
|
|
||||||
## Checks
|
## Checks
|
||||||
|
|
||||||
|
No PR (that involves a non-trivial code change) should be merged, unless all four of the items below are confirmed! If an urgent fix is needed - use a tar file.
|
||||||
|
|
||||||
- [ ] Code is formatted correctly (`npm run lint:fix`).
|
- [ ] Code is formatted correctly (`npm run lint:fix`).
|
||||||
- [ ] All unit tests are passing (`npm test`).
|
- [ ] All unit tests are passing (`npm test`).
|
||||||
- [ ] All `sasjs-cli` unit tests are passing (`npm test`).
|
- [ ] All `sasjs-cli` unit tests are passing (`npm test`).
|
||||||
|
|||||||
67
README.md
67
README.md
@@ -1,7 +1,23 @@
|
|||||||
[](https://www.jsdelivr.com/package/npm/@sasjs/adapter)
|
|
||||||
|
|
||||||
# @sasjs/adapter
|
# @sasjs/adapter
|
||||||
|
|
||||||
|
[![npm package][npm-image]][npm-url]
|
||||||
|
[![Github Workflow][githubworkflow-image]][githubworkflow-url]
|
||||||
|
[![Dependency Status][dependency-image]][dependency-url]
|
||||||
|
[]()
|
||||||
|

|
||||||
|
[](/LICENSE)
|
||||||
|

|
||||||
|

|
||||||
|
[](https://gitpod.io/#https://github.com/sasjs/adapter)
|
||||||
|
|
||||||
|
|
||||||
|
[npm-image]:https://img.shields.io/npm/v/@sasjs/adapter.svg
|
||||||
|
[npm-url]:http://npmjs.org/package/@sasjs/adapter
|
||||||
|
[githubworkflow-image]:https://github.com/sasjs/adapter/actions/workflows/build.yml/badge.svg
|
||||||
|
[githubworkflow-url]:https://github.com/sasjs/adapter/blob/main/.github/workflows/build.yml
|
||||||
|
[dependency-image]:https://david-dm.org/sasjs/adapter.svg
|
||||||
|
[dependency-url]:https://github.com/sasjs/adapter/blob/main/package.json
|
||||||
|
|
||||||
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:
|
SASjs is a open-source framework for building Web Apps on SAS® platforms. You can use as much or as little of it as you like. This repository contains the JS adapter, the part that handles the to/from SAS communication on the client side. There are 3 ways to install it:
|
||||||
|
|
||||||
1 - `npm install @sasjs/adapter` - for use in a node project
|
1 - `npm install @sasjs/adapter` - for use in a node project
|
||||||
@@ -156,35 +172,43 @@ Configuration on the client side involves passing an object on startup, which ca
|
|||||||
* `serverType` - either `SAS9` or `SASVIYA`.
|
* `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.
|
* `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.
|
* `debug` - if `true` then SAS Logs and extra debug information is returned.
|
||||||
* `useComputeApi` - if `true` and the serverType is `SASVIYA` then the REST APIs will be called directly (rather than using the JES web service).
|
* `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` - if missing or blank, and `useComputeApi` is `true` and `serverType` is `SASVIYA` then the JES API will be 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).
|
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).
|
||||||
|
|
||||||
### Using JES Web App
|
### Using JES Web App
|
||||||
|
|
||||||
In this setup, all requests are routed through the JES web app, at `YOURSERVER/SASJobExecution`. 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 made on the SAS server by the JES app.
|
In this setup, all requests are routed through the JES web app, at `YOURSERVER/SASJobExecution?_program=/your/program`. 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.
|
||||||
|
|
||||||
```
|
|
||||||
{
|
|
||||||
appLoc:"/Your/Path",
|
|
||||||
serverType:"SASVIYA"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Using the JES API
|
|
||||||
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 `__job` parameter.
|
|
||||||
|
|
||||||
```
|
```
|
||||||
{
|
{
|
||||||
appLoc:"/Your/Path",
|
appLoc:"/Your/Path",
|
||||||
serverType:"SASVIYA",
|
serverType:"SASVIYA",
|
||||||
useComputeApi: true
|
contextName: 'yourComputeContext'
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Note - to use the web approach, the `useComputeApi` property must be `undefined` or `null`.
|
||||||
|
|
||||||
|
### Using the JES API
|
||||||
|
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 `__job` parameter. Depending on your network bandwidth, it may or may not be faster than the JES Web approach.
|
||||||
|
|
||||||
|
This approach (`useComputeApi: false`) also ensures that jobs are displayed in Environment Manager.
|
||||||
|
|
||||||
|
```
|
||||||
|
{
|
||||||
|
appLoc:"/Your/Path",
|
||||||
|
serverType:"SASVIYA",
|
||||||
|
useComputeApi: false,
|
||||||
|
contextName: 'yourComputeContext'
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
### Using the Compute API
|
### Using the Compute API
|
||||||
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 - which spawns an extra session. The next time a request is made, the adapter will use the 'hot' session. Sessions are deleted after every use, which actually makes this _less_ resource intensive than a typical JES web app, in which all sessions are kept alive by default for 15 minutes.
|
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 _less_ resource intensive than a typical JES web app, in which all sessions are kept alive by default for 15 minutes.
|
||||||
|
|
||||||
|
With this approach (`useComputeApi: true`), the requests/logs will _not_ appear in the list in Environment manager.
|
||||||
|
|
||||||
```
|
```
|
||||||
{
|
{
|
||||||
@@ -198,8 +222,15 @@ This approach is by far the fastest, as a result of the optimisations we have bu
|
|||||||
|
|
||||||
# More resources
|
# More resources
|
||||||
|
|
||||||
For more information and examples specific to this adapter you can check out the [user guide](https://sasjs.io/sasjs-adapter/) or the [technical](http://adapter.sasjs.io/) documentation.
|
For more information and examples specific to this adapter you can check out the [user guide](https://sasjs.io/sasjs-adapter/) or the [technical](http://adapter.sasjs.io/) documentation.
|
||||||
|
|
||||||
For more information on building web apps in general, check out these [resources](https://sasjs.io/training/resources/) or contact the [author](https://www.linkedin.com/in/allanbowe/) directly.
|
For more information on building web apps in general, check out these [resources](https://sasjs.io/training/resources/) or contact the [author](https://www.linkedin.com/in/allanbowe/) directly.
|
||||||
|
|
||||||
If you are a SAS 9 or SAS Viya customer you can also request a copy of [Data Controller](https://datacontroller.io) - free for up to 5 users, this tool makes use of all parts of the SASjs framework.
|
If you are a SAS 9 or SAS Viya customer you can also request a copy of [Data Controller](https://datacontroller.io) - free for up to 5 users, this tool makes use of all parts of the SASjs framework.
|
||||||
|
|
||||||
|
|
||||||
|
## Star Gazing
|
||||||
|
|
||||||
|
If you find this library useful, help us grow our star graph!
|
||||||
|
|
||||||
|

|
||||||
|
|||||||
5667
package-lock.json
generated
5667
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
49
package.json
49
package.json
@@ -1,17 +1,19 @@
|
|||||||
{
|
{
|
||||||
"name": "@sasjs/adapter",
|
"name": "@sasjs/adapter",
|
||||||
"description": "JavaScript adapter for SAS",
|
"description": "JavaScript adapter for SAS",
|
||||||
|
"homepage": "https://adapter.sasjs.io",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "rimraf build && rimraf node && mkdir node && cp -r src/* node && webpack && rimraf build/src && rimraf node",
|
"build": "rimraf build && rimraf node && mkdir node && copyfiles -u 1 \"./src/**/*\" ./node && webpack && rimraf build/src && rimraf node",
|
||||||
"package:lib": "npm run build && cp ./package.json build && cd build && npm version \"5.0.0\" && npm pack",
|
"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",
|
"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: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}'",
|
"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}\"",
|
||||||
"test": "jest --silent --coverage",
|
"test": "jest --silent --coverage",
|
||||||
"prepublishOnly": "cp -r ./build/* . && rm -rf ./build",
|
"prepublishOnly": "cp -r ./build/* . && rm -rf ./build",
|
||||||
"postpublish": "git clean -fd",
|
"postpublish": "git clean -fd",
|
||||||
"semantic-release": "semantic-release",
|
"semantic-release": "semantic-release",
|
||||||
"typedoc": "typedoc"
|
"typedoc": "typedoc",
|
||||||
|
"prepare": "git rev-parse --git-dir && git config core.hooksPath ./.git-hooks && git config core.autocrlf false || true"
|
||||||
},
|
},
|
||||||
"publishConfig": {
|
"publishConfig": {
|
||||||
"access": "public"
|
"access": "public"
|
||||||
@@ -36,31 +38,40 @@
|
|||||||
},
|
},
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/jest": "^26.0.22",
|
"@types/jest": "^26.0.23",
|
||||||
|
"@types/mime": "^2.0.3",
|
||||||
|
"@types/tough-cookie": "^4.0.0",
|
||||||
|
"copyfiles": "^2.4.1",
|
||||||
"cp": "^0.2.0",
|
"cp": "^0.2.0",
|
||||||
"dotenv": "^8.2.0",
|
"dotenv": "^10.0.0",
|
||||||
"jest": "^26.6.3",
|
"jest": "^27.0.6",
|
||||||
"jest-extended": "^0.11.5",
|
"jest-extended": "^0.11.5",
|
||||||
|
"mime": "^2.5.2",
|
||||||
|
"node-polyfill-webpack-plugin": "^1.1.4",
|
||||||
"path": "^0.12.7",
|
"path": "^0.12.7",
|
||||||
|
"process": "^0.11.10",
|
||||||
"rimraf": "^3.0.2",
|
"rimraf": "^3.0.2",
|
||||||
"semantic-release": "^17.4.2",
|
"semantic-release": "^17.4.4",
|
||||||
"terser-webpack-plugin": "^4.2.3",
|
"terser-webpack-plugin": "^5.1.4",
|
||||||
"ts-jest": "^25.5.1",
|
"ts-jest": "^27.0.3",
|
||||||
"ts-loader": "^9.1.2",
|
"ts-loader": "^9.2.2",
|
||||||
"tslint": "^6.1.3",
|
"tslint": "^6.1.3",
|
||||||
"tslint-config-prettier": "^1.18.0",
|
"tslint-config-prettier": "^1.18.0",
|
||||||
"typedoc": "^0.20.35",
|
"typedoc": "^0.21.2",
|
||||||
"typedoc-neo-theme": "^1.1.0",
|
"typedoc-neo-theme": "^1.1.1",
|
||||||
"typedoc-plugin-external-module-name": "^4.0.6",
|
"typedoc-plugin-external-module-name": "^4.0.6",
|
||||||
"typescript": "^3.9.9",
|
"typescript": "^4.3.4",
|
||||||
"webpack": "^5.33.2",
|
"webpack": "^5.41.1",
|
||||||
"webpack-cli": "^4.7.0"
|
"webpack-cli": "^4.7.2"
|
||||||
},
|
},
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@sasjs/utils": "^2.10.2",
|
"@sasjs/utils": "^2.23.2",
|
||||||
"axios": "^0.21.1",
|
"axios": "^0.21.1",
|
||||||
|
"axios-cookiejar-support": "^1.0.1",
|
||||||
"form-data": "^4.0.0",
|
"form-data": "^4.0.0",
|
||||||
"https": "^1.0.0"
|
"https": "^1.0.0",
|
||||||
|
"tough-cookie": "^4.0.0",
|
||||||
|
"url": "^0.11.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ When developing on `@sasjs/adapter`, it's good practice to run the test suite ag
|
|||||||
|
|
||||||
You can use the provided `update:adapter` NPM script for this.
|
You can use the provided `update:adapter` NPM script for this.
|
||||||
|
|
||||||
```
|
```bash
|
||||||
npm run update:adapter
|
npm run update:adapter
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -37,7 +37,7 @@ To be able to run the `deploy` script, two environment variables need to be set:
|
|||||||
|
|
||||||
So you can run the script like so:
|
So you can run the script like so:
|
||||||
|
|
||||||
```
|
```bash
|
||||||
SSH_ACCOUNT=me@my-sas-server.com DEPLOY_PATH=/var/www/html/my-folder/sasjs-tests npm run deploy
|
SSH_ACCOUNT=me@my-sas-server.com DEPLOY_PATH=/var/www/html/my-folder/sasjs-tests npm run deploy
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -49,12 +49,12 @@ The below services need to be created on your SAS server, at the location specif
|
|||||||
|
|
||||||
### SAS 9
|
### SAS 9
|
||||||
|
|
||||||
```
|
```sas
|
||||||
|
|
||||||
filename mc url "https://raw.githubusercontent.com/sasjs/core/main/all.sas";
|
filename mc url "https://raw.githubusercontent.com/sasjs/core/main/all.sas";
|
||||||
%inc mc;
|
%inc mc;
|
||||||
filename ft15f001 temp;
|
filename ft15f001 temp;
|
||||||
parmcards4;
|
parmcards4;
|
||||||
|
%webout(FETCH)
|
||||||
%webout(OPEN)
|
%webout(OPEN)
|
||||||
%macro x();
|
%macro x();
|
||||||
%do i=1 %to &_webin_file_count; %webout(OBJ,&&_webin_name&i) %end;
|
%do i=1 %to &_webin_file_count; %webout(OBJ,&&_webin_name&i) %end;
|
||||||
@@ -63,6 +63,7 @@ parmcards4;
|
|||||||
;;;;
|
;;;;
|
||||||
%mm_createwebservice(path=/Public/app/common,name=sendObj)
|
%mm_createwebservice(path=/Public/app/common,name=sendObj)
|
||||||
parmcards4;
|
parmcards4;
|
||||||
|
%webout(FETCH)
|
||||||
%webout(OPEN)
|
%webout(OPEN)
|
||||||
%macro x();
|
%macro x();
|
||||||
%do i=1 %to &_webin_file_count; %webout(ARR,&&_webin_name&i) %end;
|
%do i=1 %to &_webin_file_count; %webout(ARR,&&_webin_name&i) %end;
|
||||||
@@ -70,11 +71,24 @@ parmcards4;
|
|||||||
%webout(CLOSE)
|
%webout(CLOSE)
|
||||||
;;;;
|
;;;;
|
||||||
%mm_createwebservice(path=/Public/app/common,name=sendArr)
|
%mm_createwebservice(path=/Public/app/common,name=sendArr)
|
||||||
|
parmcards4;
|
||||||
|
let he who hath understanding, reckon the number of the beast
|
||||||
|
;;;;
|
||||||
|
%mm_createwebservice(path=/Public/app/common,name=makeErr)
|
||||||
|
parmcards4;
|
||||||
|
%webout(OPEN)
|
||||||
|
data _null_;
|
||||||
|
file _webout;
|
||||||
|
put ' the discovery channel ';
|
||||||
|
run;
|
||||||
|
%webout(CLOSE)
|
||||||
|
;;;;
|
||||||
|
%mm_createwebservice(path=/Public/app/common,name=invalidJSON)
|
||||||
```
|
```
|
||||||
|
|
||||||
### SAS Viya
|
### SAS Viya
|
||||||
|
|
||||||
```
|
```sas
|
||||||
filename mc url "https://raw.githubusercontent.com/sasjs/core/main/all.sas";
|
filename mc url "https://raw.githubusercontent.com/sasjs/core/main/all.sas";
|
||||||
%inc mc;
|
%inc mc;
|
||||||
filename ft15f001 temp;
|
filename ft15f001 temp;
|
||||||
@@ -113,6 +127,15 @@ If you can trust yourself when all men doubt you,
|
|||||||
But make allowance for their doubting too;
|
But make allowance for their doubting too;
|
||||||
;;;;
|
;;;;
|
||||||
%mp_createwebservice(path=/Public/app/common,name=makeErr)
|
%mp_createwebservice(path=/Public/app/common,name=makeErr)
|
||||||
|
parmcards4;
|
||||||
|
%webout(OPEN)
|
||||||
|
data _null_;
|
||||||
|
file _webout;
|
||||||
|
put ' the discovery channel ';
|
||||||
|
run;
|
||||||
|
%webout(CLOSE)
|
||||||
|
;;;;
|
||||||
|
%mp_createwebservice(path=/Public/app/common,name=invalidJSON)
|
||||||
```
|
```
|
||||||
|
|
||||||
You should now be able to access the tests in your browser at the deployed path on your server.
|
You should now be able to access the tests in your browser at the deployed path on your server.
|
||||||
|
|||||||
22184
sasjs-tests/package-lock.json
generated
22184
sasjs-tests/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -7,7 +7,7 @@
|
|||||||
"@sasjs/adapter": "file:../build/sasjs-adapter-5.0.0.tgz",
|
"@sasjs/adapter": "file:../build/sasjs-adapter-5.0.0.tgz",
|
||||||
"@sasjs/test-framework": "^1.4.0",
|
"@sasjs/test-framework": "^1.4.0",
|
||||||
"@types/jest": "^26.0.20",
|
"@types/jest": "^26.0.20",
|
||||||
"@types/node": "^14.14.25",
|
"@types/node": "^14.14.41",
|
||||||
"@types/react": "^17.0.1",
|
"@types/react": "^17.0.1",
|
||||||
"@types/react-dom": "^17.0.0",
|
"@types/react-dom": "^17.0.0",
|
||||||
"@types/react-router-dom": "^5.1.7",
|
"@types/react-router-dom": "^5.1.7",
|
||||||
|
|||||||
@@ -13,14 +13,19 @@ const App = (): ReactElement<{}> => {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (adapter) {
|
if (adapter) {
|
||||||
setTestSuites([
|
const testSuites = [
|
||||||
basicTests(adapter, config.userName, config.password),
|
basicTests(adapter, config.userName, config.password),
|
||||||
sendArrTests(adapter),
|
sendArrTests(adapter),
|
||||||
sendObjTests(adapter),
|
sendObjTests(adapter),
|
||||||
specialCaseTests(adapter),
|
specialCaseTests(adapter),
|
||||||
sasjsRequestTests(adapter),
|
sasjsRequestTests(adapter)
|
||||||
computeTests(adapter)
|
]
|
||||||
])
|
|
||||||
|
if (adapter.getSasjsConfig().serverType === 'SASVIYA') {
|
||||||
|
testSuites.push(computeTests(adapter))
|
||||||
|
}
|
||||||
|
|
||||||
|
setTestSuites(testSuites)
|
||||||
}
|
}
|
||||||
}, [adapter, config])
|
}, [adapter, config])
|
||||||
|
|
||||||
|
|||||||
@@ -145,6 +145,29 @@ export const basicTests = (
|
|||||||
sasjsConfig.debug === false
|
sasjsConfig.debug === false
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Request with extra attributes on JES approach',
|
||||||
|
description:
|
||||||
|
'Should complete successful request with extra attributes present in response',
|
||||||
|
test: async () => {
|
||||||
|
const config = {
|
||||||
|
useComputeApi: false
|
||||||
|
}
|
||||||
|
|
||||||
|
return await adapter.request(
|
||||||
|
'common/sendArr',
|
||||||
|
stringData,
|
||||||
|
config,
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
|
['file', 'data']
|
||||||
|
)
|
||||||
|
},
|
||||||
|
assertion: (response: any) => {
|
||||||
|
const responseKeys: any = Object.keys(response)
|
||||||
|
return responseKeys.includes('file') && responseKeys.includes('data')
|
||||||
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ export const computeTests = (adapter: SASjs): TestSuite => ({
|
|||||||
'/Public/app/common/sendArr',
|
'/Public/app/common/sendArr',
|
||||||
data,
|
data,
|
||||||
{},
|
{},
|
||||||
'',
|
undefined,
|
||||||
true
|
true
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -176,11 +176,59 @@ export const sendObjTests = (adapter: SASjs): TestSuite => ({
|
|||||||
name: 'sendObj',
|
name: 'sendObj',
|
||||||
tests: [
|
tests: [
|
||||||
{
|
{
|
||||||
title: 'Invalid column name',
|
title: 'Table name starts with numeric',
|
||||||
description: 'Should throw an error',
|
description: 'Should throw an error',
|
||||||
test: async () => {
|
test: async () => {
|
||||||
const invalidData: any = {
|
const invalidData: any = {
|
||||||
'1 invalid table': [{ col1: 42 }]
|
'1InvalidTable': [{ col1: 42 }]
|
||||||
|
}
|
||||||
|
return adapter.request('common/sendObj', invalidData).catch((e) => e)
|
||||||
|
},
|
||||||
|
assertion: (error: any) =>
|
||||||
|
!!error && !!error.error && !!error.error.message
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Table name contains a space',
|
||||||
|
description: 'Should throw an error',
|
||||||
|
test: async () => {
|
||||||
|
const invalidData: any = {
|
||||||
|
'an invalidTable': [{ col1: 42 }]
|
||||||
|
}
|
||||||
|
return adapter.request('common/sendObj', invalidData).catch((e) => e)
|
||||||
|
},
|
||||||
|
assertion: (error: any) =>
|
||||||
|
!!error && !!error.error && !!error.error.message
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Table name contains a special character',
|
||||||
|
description: 'Should throw an error',
|
||||||
|
test: async () => {
|
||||||
|
const invalidData: any = {
|
||||||
|
'anInvalidTable#': [{ col1: 42 }]
|
||||||
|
}
|
||||||
|
return adapter.request('common/sendObj', invalidData).catch((e) => e)
|
||||||
|
},
|
||||||
|
assertion: (error: any) =>
|
||||||
|
!!error && !!error.error && !!error.error.message
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Table name exceeds max length of 32 characters',
|
||||||
|
description: 'Should throw an error',
|
||||||
|
test: async () => {
|
||||||
|
const invalidData: any = {
|
||||||
|
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx: [{ col1: 42 }]
|
||||||
|
}
|
||||||
|
return adapter.request('common/sendObj', invalidData).catch((e) => e)
|
||||||
|
},
|
||||||
|
assertion: (error: any) =>
|
||||||
|
!!error && !!error.error && !!error.error.message
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Invalid data object's structure",
|
||||||
|
description: 'Should throw an error',
|
||||||
|
test: async () => {
|
||||||
|
const invalidData: any = {
|
||||||
|
inData: [[{ data: 'value' }]]
|
||||||
}
|
}
|
||||||
return adapter.request('common/sendObj', invalidData).catch((e) => e)
|
return adapter.request('common/sendObj', invalidData).catch((e) => e)
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { Context, EditContextInput, ContextAllAttributes } from './types'
|
|||||||
import { isUrl } from './utils'
|
import { isUrl } from './utils'
|
||||||
import { prefixMessage } from '@sasjs/utils/error'
|
import { prefixMessage } from '@sasjs/utils/error'
|
||||||
import { RequestClient } from './request/RequestClient'
|
import { RequestClient } from './request/RequestClient'
|
||||||
|
import { AuthConfig } from '@sasjs/utils/types'
|
||||||
|
|
||||||
export class ContextManager {
|
export class ContextManager {
|
||||||
private defaultComputeContexts = [
|
private defaultComputeContexts = [
|
||||||
@@ -328,12 +329,12 @@ export class ContextManager {
|
|||||||
|
|
||||||
public async getExecutableContexts(
|
public async getExecutableContexts(
|
||||||
executeScript: Function,
|
executeScript: Function,
|
||||||
accessToken?: string
|
authConfig?: AuthConfig
|
||||||
) {
|
) {
|
||||||
const { result: contexts } = await this.requestClient
|
const { result: contexts } = await this.requestClient
|
||||||
.get<{ items: Context[] }>(
|
.get<{ items: Context[] }>(
|
||||||
`${this.serverUrl}/compute/contexts?limit=10000`,
|
`${this.serverUrl}/compute/contexts?limit=10000`,
|
||||||
accessToken
|
authConfig?.access_token
|
||||||
)
|
)
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
throw prefixMessage(err, 'Error while fetching compute contexts.')
|
throw prefixMessage(err, 'Error while fetching compute contexts.')
|
||||||
@@ -350,7 +351,7 @@ export class ContextManager {
|
|||||||
`test-${context.name}`,
|
`test-${context.name}`,
|
||||||
linesOfCode,
|
linesOfCode,
|
||||||
context.name,
|
context.name,
|
||||||
accessToken,
|
authConfig,
|
||||||
null,
|
null,
|
||||||
false,
|
false,
|
||||||
true,
|
true,
|
||||||
|
|||||||
@@ -2,15 +2,19 @@ import { isUrl } from './utils'
|
|||||||
import { UploadFile } from './types/UploadFile'
|
import { UploadFile } from './types/UploadFile'
|
||||||
import { ErrorResponse, LoginRequiredError } from './types/errors'
|
import { ErrorResponse, LoginRequiredError } from './types/errors'
|
||||||
import { RequestClient } from './request/RequestClient'
|
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 {
|
export class FileUploader {
|
||||||
constructor(
|
constructor(
|
||||||
private appLoc: string,
|
private sasjsConfig: SASjsConfig,
|
||||||
serverUrl: string,
|
|
||||||
private jobsPath: string,
|
private jobsPath: string,
|
||||||
private requestClient: RequestClient
|
private requestClient: RequestClient
|
||||||
) {
|
) {
|
||||||
if (serverUrl) isUrl(serverUrl)
|
if (this.sasjsConfig.serverUrl) isUrl(this.sasjsConfig.serverUrl)
|
||||||
}
|
}
|
||||||
|
|
||||||
public uploadFile(sasJob: string, files: UploadFile[], params: any) {
|
public uploadFile(sasJob: string, files: UploadFile[], params: any) {
|
||||||
@@ -29,8 +33,8 @@ export class FileUploader {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const program = this.appLoc
|
const program = this.sasjsConfig.appLoc
|
||||||
? this.appLoc.replace(/\/?$/, '/') + sasJob.replace(/^\//, '')
|
? this.sasjsConfig.appLoc.replace(/\/?$/, '/') + sasJob.replace(/^\//, '')
|
||||||
: sasJob
|
: sasJob
|
||||||
const uploadUrl = `${this.jobsPath}/?${
|
const uploadUrl = `${this.jobsPath}/?${
|
||||||
'_program=' + program
|
'_program=' + program
|
||||||
@@ -44,6 +48,12 @@ export class FileUploader {
|
|||||||
|
|
||||||
const csrfToken = this.requestClient.getCsrfToken('file')
|
const csrfToken = this.requestClient.getCsrfToken('file')
|
||||||
if (csrfToken) formData.append('_csrf', csrfToken.value)
|
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 = {
|
const headers = {
|
||||||
'cache-control': 'no-cache',
|
'cache-control': 'no-cache',
|
||||||
@@ -53,9 +63,15 @@ export class FileUploader {
|
|||||||
|
|
||||||
return this.requestClient
|
return this.requestClient
|
||||||
.post(uploadUrl, formData, undefined, 'application/json', headers)
|
.post(uploadUrl, formData, undefined, 'application/json', headers)
|
||||||
.then((res) =>
|
.then((res) => {
|
||||||
typeof res.result === 'string' ? JSON.parse(res.result) : res.result
|
let result
|
||||||
)
|
|
||||||
|
result =
|
||||||
|
typeof res.result === 'string' ? JSON.parse(res.result) : res.result
|
||||||
|
|
||||||
|
return result
|
||||||
|
//TODO: append to SASjs requests
|
||||||
|
})
|
||||||
.catch((err: Error) => {
|
.catch((err: Error) => {
|
||||||
if (err instanceof LoginRequiredError) {
|
if (err instanceof LoginRequiredError) {
|
||||||
return Promise.reject(
|
return Promise.reject(
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
import axios, { AxiosInstance } from 'axios'
|
import { generateTimestamp } from '@sasjs/utils/time'
|
||||||
|
import * as NodeFormData from 'form-data'
|
||||||
|
import { Sas9RequestClient } from './request/Sas9RequestClient'
|
||||||
import { isUrl } from './utils'
|
import { isUrl } from './utils'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -6,11 +8,11 @@ import { isUrl } from './utils'
|
|||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
export class SAS9ApiClient {
|
export class SAS9ApiClient {
|
||||||
private httpClient: AxiosInstance
|
private requestClient: Sas9RequestClient
|
||||||
|
|
||||||
constructor(private serverUrl: string) {
|
constructor(private serverUrl: string, private jobsPath: string) {
|
||||||
if (serverUrl) isUrl(serverUrl)
|
if (serverUrl) isUrl(serverUrl)
|
||||||
this.httpClient = axios.create({ baseURL: this.serverUrl })
|
this.requestClient = new Sas9RequestClient(serverUrl, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -33,27 +35,61 @@ export class SAS9ApiClient {
|
|||||||
/**
|
/**
|
||||||
* Executes code on a SAS9 server.
|
* Executes code on a SAS9 server.
|
||||||
* @param linesOfCode - an array of code lines to execute.
|
* @param linesOfCode - an array of code lines to execute.
|
||||||
* @param serverName - the server to execute the code on.
|
* @param userName - the user name to log into the current SAS server.
|
||||||
* @param repositoryName - the repository to execute the code in.
|
* @param password - the password to log into the current SAS server.
|
||||||
*/
|
*/
|
||||||
public async executeScript(
|
public async executeScript(
|
||||||
linesOfCode: string[],
|
linesOfCode: string[],
|
||||||
serverName: string,
|
userName: string,
|
||||||
repositoryName: string
|
password: string
|
||||||
) {
|
) {
|
||||||
const requestPayload = linesOfCode.join('\n')
|
await this.requestClient.login(userName, password, this.jobsPath)
|
||||||
|
|
||||||
const executeScriptResponse = await this.httpClient.put(
|
// This piece of code forces a webout to prevent Stored Process Errors.
|
||||||
`/sas/servers/${serverName}/cmd?repositoryName=${repositoryName}`,
|
const forceOutputCode = [
|
||||||
`command=${requestPayload}`,
|
'data _null_;',
|
||||||
{
|
'file _webout;',
|
||||||
headers: {
|
`put 'Executed sasjs run';`,
|
||||||
Accept: 'application/json'
|
'run;'
|
||||||
},
|
]
|
||||||
responseType: 'text'
|
const formData = generateFileUploadForm(
|
||||||
}
|
[...linesOfCode, ...forceOutputCode].join('\n')
|
||||||
)
|
)
|
||||||
|
|
||||||
return executeScriptResponse.data
|
const codeInjectorPath = `/User Folders/${userName}/My Folder/sasjs/runner`
|
||||||
|
const contentType =
|
||||||
|
'multipart/form-data; boundary=' + formData.getBoundary()
|
||||||
|
const contentLength = formData.getLengthSync()
|
||||||
|
|
||||||
|
const headers = {
|
||||||
|
'cache-control': 'no-cache',
|
||||||
|
Accept: '*/*',
|
||||||
|
'Content-Type': contentType,
|
||||||
|
'Content-Length': contentLength,
|
||||||
|
Connection: 'keep-alive'
|
||||||
|
}
|
||||||
|
const storedProcessUrl = `${this.jobsPath}/?${
|
||||||
|
'_program=' + codeInjectorPath + '&_debug=log'
|
||||||
|
}`
|
||||||
|
const response = await this.requestClient.post(
|
||||||
|
storedProcessUrl,
|
||||||
|
formData,
|
||||||
|
undefined,
|
||||||
|
contentType,
|
||||||
|
headers
|
||||||
|
)
|
||||||
|
|
||||||
|
return response.result as string
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const generateFileUploadForm = (data: any): NodeFormData => {
|
||||||
|
const formData = new NodeFormData()
|
||||||
|
const filename = `sasjs-execute-sas9-${generateTimestamp('')}.sas`
|
||||||
|
formData.append(filename, data, {
|
||||||
|
filename,
|
||||||
|
contentType: 'text/plain'
|
||||||
|
})
|
||||||
|
|
||||||
|
return formData
|
||||||
|
}
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import {
|
|||||||
Context,
|
Context,
|
||||||
ContextAllAttributes,
|
ContextAllAttributes,
|
||||||
Folder,
|
Folder,
|
||||||
|
File,
|
||||||
EditContextInput,
|
EditContextInput,
|
||||||
JobDefinition,
|
JobDefinition,
|
||||||
PollOptions
|
PollOptions
|
||||||
@@ -25,11 +26,16 @@ import { formatDataForRequest } from './utils/formatDataForRequest'
|
|||||||
import { SessionManager } from './SessionManager'
|
import { SessionManager } from './SessionManager'
|
||||||
import { ContextManager } from './ContextManager'
|
import { ContextManager } from './ContextManager'
|
||||||
import { timestampToYYYYMMDDHHMMSS } from '@sasjs/utils/time'
|
import { timestampToYYYYMMDDHHMMSS } from '@sasjs/utils/time'
|
||||||
|
import {
|
||||||
|
isAccessTokenExpiring,
|
||||||
|
isRefreshTokenExpiring
|
||||||
|
} from '@sasjs/utils/auth'
|
||||||
import { Logger, LogLevel } from '@sasjs/utils/logger'
|
import { Logger, LogLevel } from '@sasjs/utils/logger'
|
||||||
|
import { SasAuthResponse, MacroVar, AuthConfig } from '@sasjs/utils/types'
|
||||||
import { isAuthorizeFormRequired } from './auth/isAuthorizeFormRequired'
|
import { isAuthorizeFormRequired } from './auth/isAuthorizeFormRequired'
|
||||||
import { RequestClient } from './request/RequestClient'
|
import { RequestClient } from './request/RequestClient'
|
||||||
import { SasAuthResponse } from '@sasjs/utils/types'
|
|
||||||
import { prefixMessage } from '@sasjs/utils/error'
|
import { prefixMessage } from '@sasjs/utils/error'
|
||||||
|
import * as mime from 'mime'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A client for interfacing with the SAS Viya REST API.
|
* A client for interfacing with the SAS Viya REST API.
|
||||||
@@ -128,14 +134,14 @@ export class SASViyaApiClient {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns all compute contexts on this server that the user has access to.
|
* Returns all compute contexts on this server that the user has access to.
|
||||||
* @param accessToken - an access token for an authorized user.
|
* @param authConfig - an access token, refresh token, client and secret for an authorized user.
|
||||||
*/
|
*/
|
||||||
public async getExecutableContexts(accessToken?: string) {
|
public async getExecutableContexts(authConfig?: AuthConfig) {
|
||||||
const bindedExecuteScript = this.executeScript.bind(this)
|
const bindedExecuteScript = this.executeScript.bind(this)
|
||||||
|
|
||||||
return await this.contextManager.getExecutableContexts(
|
return await this.contextManager.getExecutableContexts(
|
||||||
bindedExecuteScript,
|
bindedExecuteScript,
|
||||||
accessToken
|
authConfig
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -264,37 +270,40 @@ export class SASViyaApiClient {
|
|||||||
* @param jobPath - the path to the file being submitted for execution.
|
* @param jobPath - the path to the file being submitted for execution.
|
||||||
* @param linesOfCode - an array of code lines to execute.
|
* @param linesOfCode - an array of code lines to execute.
|
||||||
* @param contextName - the context to execute the code in.
|
* @param contextName - the context to execute the code in.
|
||||||
* @param accessToken - an access token for an authorized user.
|
* @param authConfig - an object containing an access token, refresh token, client ID and secret.
|
||||||
* @param data - execution data.
|
* @param data - execution data.
|
||||||
* @param debug - when set to true, the log will be returned.
|
* @param debug - when set to true, the log will be returned.
|
||||||
* @param expectWebout - when set to true, the automatic _webout fileref will be checked for content, and that content returned. This fileref is used when the Job contains a SASjs web request (as opposed to executing arbitrary SAS code).
|
* @param expectWebout - when set to true, the automatic _webout fileref will be checked for content, and that content returned. This fileref is used when the Job contains a SASjs web request (as opposed to executing arbitrary SAS code).
|
||||||
* @param waitForResult - when set to true, function will return the session
|
* @param waitForResult - when set to true, function will return the session
|
||||||
* @param pollOptions - an object that represents poll interval(milliseconds) and maximum amount of attempts. Object example: { MAX_POLL_COUNT: 24 * 60 * 60, POLL_INTERVAL: 1000 }.
|
* @param pollOptions - an object that represents poll interval(milliseconds) and maximum amount of attempts. Object example: { MAX_POLL_COUNT: 24 * 60 * 60, POLL_INTERVAL: 1000 }.
|
||||||
* @param printPid - a boolean that indicates whether the function should print (PID) of the started job.
|
* @param printPid - a boolean that indicates whether the function should print (PID) of the started job.
|
||||||
|
* @param variables - an object that represents macro variables.
|
||||||
*/
|
*/
|
||||||
public async executeScript(
|
public async executeScript(
|
||||||
jobPath: string,
|
jobPath: string,
|
||||||
linesOfCode: string[],
|
linesOfCode: string[],
|
||||||
contextName: string,
|
contextName: string,
|
||||||
accessToken?: string,
|
authConfig?: AuthConfig,
|
||||||
data = null,
|
data = null,
|
||||||
debug: boolean = false,
|
debug: boolean = false,
|
||||||
expectWebout = false,
|
expectWebout = false,
|
||||||
waitForResult = true,
|
waitForResult = true,
|
||||||
pollOptions?: PollOptions,
|
pollOptions?: PollOptions,
|
||||||
printPid = false
|
printPid = false,
|
||||||
|
variables?: MacroVar
|
||||||
): Promise<any> {
|
): Promise<any> {
|
||||||
|
let access_token = (authConfig || {}).access_token
|
||||||
|
if (authConfig) {
|
||||||
|
;({ access_token } = await this.getTokens(authConfig))
|
||||||
|
}
|
||||||
|
|
||||||
|
const logger = process.logger || console
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const headers: any = {
|
|
||||||
'Content-Type': 'application/json'
|
|
||||||
}
|
|
||||||
|
|
||||||
if (accessToken) headers.Authorization = `Bearer ${accessToken}`
|
|
||||||
|
|
||||||
let executionSessionId: string
|
let executionSessionId: string
|
||||||
|
|
||||||
const session = await this.sessionManager
|
const session = await this.sessionManager
|
||||||
.getSession(accessToken)
|
.getSession(access_token)
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
throw prefixMessage(err, 'Error while getting session. ')
|
throw prefixMessage(err, 'Error while getting session. ')
|
||||||
})
|
})
|
||||||
@@ -303,7 +312,7 @@ export class SASViyaApiClient {
|
|||||||
|
|
||||||
if (printPid) {
|
if (printPid) {
|
||||||
const { result: jobIdVariable } = await this.sessionManager
|
const { result: jobIdVariable } = await this.sessionManager
|
||||||
.getVariable(executionSessionId, 'SYSJOBID', accessToken)
|
.getVariable(executionSessionId, 'SYSJOBID', access_token)
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
throw prefixMessage(err, 'Error while getting session variable. ')
|
throw prefixMessage(err, 'Error while getting session variable. ')
|
||||||
})
|
})
|
||||||
@@ -335,7 +344,6 @@ export class SASViyaApiClient {
|
|||||||
if (debug) {
|
if (debug) {
|
||||||
jobArguments['_OMITTEXTLOG'] = false
|
jobArguments['_OMITTEXTLOG'] = false
|
||||||
jobArguments['_OMITSESSIONRESULTS'] = false
|
jobArguments['_OMITSESSIONRESULTS'] = false
|
||||||
jobArguments['_DEBUG'] = 131
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let fileName
|
let fileName
|
||||||
@@ -356,11 +364,15 @@ export class SASViyaApiClient {
|
|||||||
: jobPath
|
: jobPath
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (variables) jobVariables = { ...jobVariables, ...variables }
|
||||||
|
|
||||||
|
if (debug) jobVariables = { ...jobVariables, _DEBUG: 131 }
|
||||||
|
|
||||||
let files: any[] = []
|
let files: any[] = []
|
||||||
|
|
||||||
if (data) {
|
if (data) {
|
||||||
if (JSON.stringify(data).includes(';')) {
|
if (JSON.stringify(data).includes(';')) {
|
||||||
files = await this.uploadTables(data, accessToken).catch((err) => {
|
files = await this.uploadTables(data, access_token).catch((err) => {
|
||||||
throw prefixMessage(err, 'Error while uploading tables. ')
|
throw prefixMessage(err, 'Error while uploading tables. ')
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -390,7 +402,7 @@ export class SASViyaApiClient {
|
|||||||
.post<Job>(
|
.post<Job>(
|
||||||
`/compute/sessions/${executionSessionId}/jobs`,
|
`/compute/sessions/${executionSessionId}/jobs`,
|
||||||
jobRequestBody,
|
jobRequestBody,
|
||||||
accessToken
|
access_token
|
||||||
)
|
)
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
throw prefixMessage(err, 'Error while posting job. ')
|
throw prefixMessage(err, 'Error while posting job. ')
|
||||||
@@ -399,8 +411,8 @@ export class SASViyaApiClient {
|
|||||||
if (!waitForResult) return session
|
if (!waitForResult) return session
|
||||||
|
|
||||||
if (debug) {
|
if (debug) {
|
||||||
console.log(`Job has been submitted for '${fileName}'.`)
|
logger.info(`Job has been submitted for '${fileName}'.`)
|
||||||
console.log(
|
logger.info(
|
||||||
`You can monitor the job progress at '${this.serverUrl}${
|
`You can monitor the job progress at '${this.serverUrl}${
|
||||||
postedJob.links.find((l: any) => l.rel === 'state')!.href
|
postedJob.links.find((l: any) => l.rel === 'state')!.href
|
||||||
}'.`
|
}'.`
|
||||||
@@ -410,7 +422,7 @@ export class SASViyaApiClient {
|
|||||||
const jobStatus = await this.pollJobState(
|
const jobStatus = await this.pollJobState(
|
||||||
postedJob,
|
postedJob,
|
||||||
etag,
|
etag,
|
||||||
accessToken,
|
authConfig,
|
||||||
pollOptions
|
pollOptions
|
||||||
).catch(async (err) => {
|
).catch(async (err) => {
|
||||||
const error = err?.response?.data
|
const error = err?.response?.data
|
||||||
@@ -423,7 +435,7 @@ export class SASViyaApiClient {
|
|||||||
const logCount = 1000000
|
const logCount = 1000000
|
||||||
err.log = await fetchLogByChunks(
|
err.log = await fetchLogByChunks(
|
||||||
this.requestClient,
|
this.requestClient,
|
||||||
accessToken!,
|
access_token!,
|
||||||
sessionLogUrl,
|
sessionLogUrl,
|
||||||
logCount
|
logCount
|
||||||
)
|
)
|
||||||
@@ -431,10 +443,14 @@ export class SASViyaApiClient {
|
|||||||
throw prefixMessage(err, 'Error while polling job status. ')
|
throw prefixMessage(err, 'Error while polling job status. ')
|
||||||
})
|
})
|
||||||
|
|
||||||
|
if (authConfig) {
|
||||||
|
;({ access_token } = await this.getTokens(authConfig))
|
||||||
|
}
|
||||||
|
|
||||||
const { result: currentJob } = await this.requestClient
|
const { result: currentJob } = await this.requestClient
|
||||||
.get<Job>(
|
.get<Job>(
|
||||||
`/compute/sessions/${executionSessionId}/jobs/${postedJob.id}`,
|
`/compute/sessions/${executionSessionId}/jobs/${postedJob.id}`,
|
||||||
accessToken
|
access_token
|
||||||
)
|
)
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
throw prefixMessage(err, 'Error while getting job. ')
|
throw prefixMessage(err, 'Error while getting job. ')
|
||||||
@@ -450,7 +466,7 @@ export class SASViyaApiClient {
|
|||||||
const logCount = currentJob.logStatistics?.lineCount ?? 1000000
|
const logCount = currentJob.logStatistics?.lineCount ?? 1000000
|
||||||
log = await fetchLogByChunks(
|
log = await fetchLogByChunks(
|
||||||
this.requestClient,
|
this.requestClient,
|
||||||
accessToken!,
|
access_token!,
|
||||||
logUrl,
|
logUrl,
|
||||||
logCount
|
logCount
|
||||||
)
|
)
|
||||||
@@ -470,7 +486,7 @@ export class SASViyaApiClient {
|
|||||||
|
|
||||||
if (resultLink) {
|
if (resultLink) {
|
||||||
jobResult = await this.requestClient
|
jobResult = await this.requestClient
|
||||||
.get<any>(resultLink, accessToken, 'text/plain')
|
.get<any>(resultLink, access_token, 'text/plain')
|
||||||
.catch(async (e) => {
|
.catch(async (e) => {
|
||||||
if (e instanceof NotFoundError) {
|
if (e instanceof NotFoundError) {
|
||||||
if (logLink) {
|
if (logLink) {
|
||||||
@@ -478,7 +494,7 @@ export class SASViyaApiClient {
|
|||||||
const logCount = currentJob.logStatistics?.lineCount ?? 1000000
|
const logCount = currentJob.logStatistics?.lineCount ?? 1000000
|
||||||
log = await fetchLogByChunks(
|
log = await fetchLogByChunks(
|
||||||
this.requestClient,
|
this.requestClient,
|
||||||
accessToken!,
|
access_token!,
|
||||||
logUrl,
|
logUrl,
|
||||||
logCount
|
logCount
|
||||||
)
|
)
|
||||||
@@ -497,7 +513,7 @@ export class SASViyaApiClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
await this.sessionManager
|
await this.sessionManager
|
||||||
.clearSession(executionSessionId, accessToken)
|
.clearSession(executionSessionId, access_token)
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
throw prefixMessage(err, 'Error while clearing session. ')
|
throw prefixMessage(err, 'Error while clearing session. ')
|
||||||
})
|
})
|
||||||
@@ -509,7 +525,7 @@ export class SASViyaApiClient {
|
|||||||
jobPath,
|
jobPath,
|
||||||
linesOfCode,
|
linesOfCode,
|
||||||
contextName,
|
contextName,
|
||||||
accessToken,
|
authConfig,
|
||||||
data,
|
data,
|
||||||
debug,
|
debug,
|
||||||
false,
|
false,
|
||||||
@@ -532,6 +548,53 @@ export class SASViyaApiClient {
|
|||||||
.then((res) => res.result)
|
.then((res) => res.result)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a file. Path to or URI of the parent folder is required.
|
||||||
|
* @param fileName - the name of the new file.
|
||||||
|
* @param contentBuffer - the content of the new file in Buffer.
|
||||||
|
* @param parentFolderPath - the full path to the parent folder. If not
|
||||||
|
* provided, the parentFolderUri must be provided.
|
||||||
|
* @param parentFolderUri - the URI (eg /folders/folders/UUID) of the parent
|
||||||
|
* folder. If not provided, the parentFolderPath must be provided.
|
||||||
|
* @param accessToken - an access token for authorizing the request.
|
||||||
|
*/
|
||||||
|
public async createFile(
|
||||||
|
fileName: string,
|
||||||
|
contentBuffer: Buffer,
|
||||||
|
parentFolderPath?: string,
|
||||||
|
parentFolderUri?: string,
|
||||||
|
accessToken?: string
|
||||||
|
): Promise<File> {
|
||||||
|
if (!parentFolderPath && !parentFolderUri) {
|
||||||
|
throw new Error('Path or URI of the parent folder is required.')
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!parentFolderUri && parentFolderPath) {
|
||||||
|
parentFolderUri = await this.getFolderUri(parentFolderPath, accessToken)
|
||||||
|
}
|
||||||
|
|
||||||
|
const headers = {
|
||||||
|
Accept: 'application/vnd.sas.file+json',
|
||||||
|
'Content-Disposition': `filename="${fileName}";`
|
||||||
|
}
|
||||||
|
|
||||||
|
const formData = new NodeFormData()
|
||||||
|
formData.append('file', contentBuffer, fileName)
|
||||||
|
|
||||||
|
const mimeType =
|
||||||
|
mime.getType(fileName.match(/\.[0-9a-z]+$/i)?.[0] || '') ?? 'text/plain'
|
||||||
|
|
||||||
|
return (
|
||||||
|
await this.requestClient.post<File>(
|
||||||
|
`/files/files?parentFolderUri=${parentFolderUri}&typeDefName=file#rawUpload`,
|
||||||
|
formData,
|
||||||
|
accessToken,
|
||||||
|
'multipart/form-data; boundary=' + (formData as any)._boundary,
|
||||||
|
headers
|
||||||
|
)
|
||||||
|
).result
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a folder. Path to or URI of the parent folder is required.
|
* Creates a folder. Path to or URI of the parent folder is required.
|
||||||
* @param folderName - the name of the new folder.
|
* @param folderName - the name of the new folder.
|
||||||
@@ -549,6 +612,7 @@ export class SASViyaApiClient {
|
|||||||
accessToken?: string,
|
accessToken?: string,
|
||||||
isForced?: boolean
|
isForced?: boolean
|
||||||
): Promise<Folder> {
|
): Promise<Folder> {
|
||||||
|
const logger = process.logger || console
|
||||||
if (!parentFolderPath && !parentFolderUri) {
|
if (!parentFolderPath && !parentFolderUri) {
|
||||||
throw new Error('Path or URI of the parent folder is required.')
|
throw new Error('Path or URI of the parent folder is required.')
|
||||||
}
|
}
|
||||||
@@ -556,7 +620,7 @@ export class SASViyaApiClient {
|
|||||||
if (!parentFolderUri && parentFolderPath) {
|
if (!parentFolderUri && parentFolderPath) {
|
||||||
parentFolderUri = await this.getFolderUri(parentFolderPath, accessToken)
|
parentFolderUri = await this.getFolderUri(parentFolderPath, accessToken)
|
||||||
if (!parentFolderUri) {
|
if (!parentFolderUri) {
|
||||||
console.log(
|
logger.info(
|
||||||
`Parent folder at path '${parentFolderPath}' is not present.`
|
`Parent folder at path '${parentFolderPath}' is not present.`
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -568,7 +632,7 @@ export class SASViyaApiClient {
|
|||||||
if (newParentFolderPath === '') {
|
if (newParentFolderPath === '') {
|
||||||
throw new Error('Root folder has to be present on the server.')
|
throw new Error('Root folder has to be present on the server.')
|
||||||
}
|
}
|
||||||
console.log(
|
logger.info(
|
||||||
`Creating parent folder:\n'${newFolderName}' in '${newParentFolderPath}'`
|
`Creating parent folder:\n'${newFolderName}' in '${newParentFolderPath}'`
|
||||||
)
|
)
|
||||||
const parentFolder = await this.createFolder(
|
const parentFolder = await this.createFolder(
|
||||||
@@ -577,7 +641,7 @@ export class SASViyaApiClient {
|
|||||||
undefined,
|
undefined,
|
||||||
accessToken
|
accessToken
|
||||||
)
|
)
|
||||||
console.log(
|
logger.info(
|
||||||
`Parent folder '${newFolderName}' has been successfully created.`
|
`Parent folder '${newFolderName}' has been successfully created.`
|
||||||
)
|
)
|
||||||
parentFolderUri = `/folders/folders/${parentFolder.id}`
|
parentFolderUri = `/folders/folders/${parentFolder.id}`
|
||||||
@@ -719,13 +783,11 @@ export class SASViyaApiClient {
|
|||||||
let formData
|
let formData
|
||||||
if (typeof FormData === 'undefined') {
|
if (typeof FormData === 'undefined') {
|
||||||
formData = new NodeFormData()
|
formData = new NodeFormData()
|
||||||
formData.append('grant_type', 'authorization_code')
|
|
||||||
formData.append('code', authCode)
|
|
||||||
} else {
|
} else {
|
||||||
formData = new FormData()
|
formData = new FormData()
|
||||||
formData.append('grant_type', 'authorization_code')
|
|
||||||
formData.append('code', authCode)
|
|
||||||
}
|
}
|
||||||
|
formData.append('grant_type', 'authorization_code')
|
||||||
|
formData.append('code', authCode)
|
||||||
|
|
||||||
const authResponse = await this.requestClient
|
const authResponse = await this.requestClient
|
||||||
.post(
|
.post(
|
||||||
@@ -814,18 +876,25 @@ export class SASViyaApiClient {
|
|||||||
* @param expectWebout - a boolean indicating whether to expect a _webout response.
|
* @param expectWebout - a boolean indicating whether to expect a _webout response.
|
||||||
* @param pollOptions - an object that represents poll interval(milliseconds) and maximum amount of attempts. Object example: { MAX_POLL_COUNT: 24 * 60 * 60, POLL_INTERVAL: 1000 }.
|
* @param pollOptions - an object that represents poll interval(milliseconds) and maximum amount of attempts. Object example: { MAX_POLL_COUNT: 24 * 60 * 60, POLL_INTERVAL: 1000 }.
|
||||||
* @param printPid - a boolean that indicates whether the function should print (PID) of the started job.
|
* @param printPid - a boolean that indicates whether the function should print (PID) of the started job.
|
||||||
|
* @param variables - an object that represents macro variables.
|
||||||
*/
|
*/
|
||||||
public async executeComputeJob(
|
public async executeComputeJob(
|
||||||
sasJob: string,
|
sasJob: string,
|
||||||
contextName: string,
|
contextName: string,
|
||||||
debug?: boolean,
|
debug?: boolean,
|
||||||
data?: any,
|
data?: any,
|
||||||
accessToken?: string,
|
authConfig?: AuthConfig,
|
||||||
waitForResult = true,
|
waitForResult = true,
|
||||||
expectWebout = false,
|
expectWebout = false,
|
||||||
pollOptions?: PollOptions,
|
pollOptions?: PollOptions,
|
||||||
printPid = false
|
printPid = false,
|
||||||
|
variables?: MacroVar
|
||||||
) {
|
) {
|
||||||
|
let access_token = (authConfig || {}).access_token
|
||||||
|
if (authConfig) {
|
||||||
|
;({ access_token } = await this.getTokens(authConfig))
|
||||||
|
}
|
||||||
|
|
||||||
if (isRelativePath(sasJob) && !this.rootFolderName) {
|
if (isRelativePath(sasJob) && !this.rootFolderName) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
'Relative paths cannot be used without specifying a root folder name'
|
'Relative paths cannot be used without specifying a root folder name'
|
||||||
@@ -839,7 +908,7 @@ export class SASViyaApiClient {
|
|||||||
? `${this.rootFolderName}/${folderPath}`
|
? `${this.rootFolderName}/${folderPath}`
|
||||||
: folderPath
|
: folderPath
|
||||||
|
|
||||||
await this.populateFolderMap(fullFolderPath, accessToken).catch((err) => {
|
await this.populateFolderMap(fullFolderPath, access_token).catch((err) => {
|
||||||
throw prefixMessage(err, 'Error while populating folder map. ')
|
throw prefixMessage(err, 'Error while populating folder map. ')
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -851,12 +920,6 @@ export class SASViyaApiClient {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const headers: any = { 'Content-Type': 'application/json' }
|
|
||||||
|
|
||||||
if (!!accessToken) {
|
|
||||||
headers.Authorization = `Bearer ${accessToken}`
|
|
||||||
}
|
|
||||||
|
|
||||||
const jobToExecute = jobFolder?.find((item) => item.name === jobName)
|
const jobToExecute = jobFolder?.find((item) => item.name === jobName)
|
||||||
|
|
||||||
if (!jobToExecute) {
|
if (!jobToExecute) {
|
||||||
@@ -877,7 +940,7 @@ export class SASViyaApiClient {
|
|||||||
const { result: jobDefinition } = await this.requestClient
|
const { result: jobDefinition } = await this.requestClient
|
||||||
.get<JobDefinition>(
|
.get<JobDefinition>(
|
||||||
`${this.serverUrl}${jobDefinitionLink.href}`,
|
`${this.serverUrl}${jobDefinitionLink.href}`,
|
||||||
accessToken
|
access_token
|
||||||
)
|
)
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
throw prefixMessage(err, 'Error while getting job definition. ')
|
throw prefixMessage(err, 'Error while getting job definition. ')
|
||||||
@@ -897,13 +960,14 @@ export class SASViyaApiClient {
|
|||||||
sasJob,
|
sasJob,
|
||||||
linesToExecute,
|
linesToExecute,
|
||||||
contextName,
|
contextName,
|
||||||
accessToken,
|
authConfig,
|
||||||
data,
|
data,
|
||||||
debug,
|
debug,
|
||||||
expectWebout,
|
expectWebout,
|
||||||
waitForResult,
|
waitForResult,
|
||||||
pollOptions,
|
pollOptions,
|
||||||
printPid
|
printPid,
|
||||||
|
variables
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -920,8 +984,12 @@ export class SASViyaApiClient {
|
|||||||
contextName: string,
|
contextName: string,
|
||||||
debug: boolean,
|
debug: boolean,
|
||||||
data?: any,
|
data?: any,
|
||||||
accessToken?: string
|
authConfig?: AuthConfig
|
||||||
) {
|
) {
|
||||||
|
let access_token = (authConfig || {}).access_token
|
||||||
|
if (authConfig) {
|
||||||
|
;({ access_token } = await this.getTokens(authConfig))
|
||||||
|
}
|
||||||
if (isRelativePath(sasJob) && !this.rootFolderName) {
|
if (isRelativePath(sasJob) && !this.rootFolderName) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
'Relative paths cannot be used without specifying a root folder name.'
|
'Relative paths cannot be used without specifying a root folder name.'
|
||||||
@@ -934,7 +1002,7 @@ export class SASViyaApiClient {
|
|||||||
const fullFolderPath = isRelativePath(sasJob)
|
const fullFolderPath = isRelativePath(sasJob)
|
||||||
? `${this.rootFolderName}/${folderPath}`
|
? `${this.rootFolderName}/${folderPath}`
|
||||||
: folderPath
|
: folderPath
|
||||||
await this.populateFolderMap(fullFolderPath, accessToken)
|
await this.populateFolderMap(fullFolderPath, access_token)
|
||||||
|
|
||||||
const jobFolder = this.folderMap.get(fullFolderPath)
|
const jobFolder = this.folderMap.get(fullFolderPath)
|
||||||
if (!jobFolder) {
|
if (!jobFolder) {
|
||||||
@@ -947,7 +1015,7 @@ export class SASViyaApiClient {
|
|||||||
|
|
||||||
let files: any[] = []
|
let files: any[] = []
|
||||||
if (data && Object.keys(data).length) {
|
if (data && Object.keys(data).length) {
|
||||||
files = await this.uploadTables(data, accessToken)
|
files = await this.uploadTables(data, access_token)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!jobToExecute) {
|
if (!jobToExecute) {
|
||||||
@@ -959,7 +1027,7 @@ export class SASViyaApiClient {
|
|||||||
|
|
||||||
const { result: jobDefinition } = await this.requestClient.get<Job>(
|
const { result: jobDefinition } = await this.requestClient.get<Job>(
|
||||||
`${this.serverUrl}${jobDefinitionLink}`,
|
`${this.serverUrl}${jobDefinitionLink}`,
|
||||||
accessToken
|
access_token
|
||||||
)
|
)
|
||||||
|
|
||||||
const jobArguments: { [key: string]: any } = {
|
const jobArguments: { [key: string]: any } = {
|
||||||
@@ -995,18 +1063,18 @@ export class SASViyaApiClient {
|
|||||||
const { result: postedJob, etag } = await this.requestClient.post<Job>(
|
const { result: postedJob, etag } = await this.requestClient.post<Job>(
|
||||||
`${this.serverUrl}/jobExecution/jobs?_action=wait`,
|
`${this.serverUrl}/jobExecution/jobs?_action=wait`,
|
||||||
postJobRequestBody,
|
postJobRequestBody,
|
||||||
accessToken
|
access_token
|
||||||
)
|
)
|
||||||
const jobStatus = await this.pollJobState(
|
const jobStatus = await this.pollJobState(
|
||||||
postedJob,
|
postedJob,
|
||||||
etag,
|
etag,
|
||||||
accessToken
|
authConfig
|
||||||
).catch((err) => {
|
).catch((err) => {
|
||||||
throw prefixMessage(err, 'Error while polling job status. ')
|
throw prefixMessage(err, 'Error while polling job status. ')
|
||||||
})
|
})
|
||||||
const { result: currentJob } = await this.requestClient.get<Job>(
|
const { result: currentJob } = await this.requestClient.get<Job>(
|
||||||
`${this.serverUrl}/jobExecution/jobs/${postedJob.id}`,
|
`${this.serverUrl}/jobExecution/jobs/${postedJob.id}`,
|
||||||
accessToken
|
access_token
|
||||||
)
|
)
|
||||||
|
|
||||||
let jobResult
|
let jobResult
|
||||||
@@ -1017,13 +1085,13 @@ export class SASViyaApiClient {
|
|||||||
if (resultLink) {
|
if (resultLink) {
|
||||||
jobResult = await this.requestClient.get<any>(
|
jobResult = await this.requestClient.get<any>(
|
||||||
`${this.serverUrl}${resultLink}/content`,
|
`${this.serverUrl}${resultLink}/content`,
|
||||||
accessToken,
|
access_token,
|
||||||
'text/plain'
|
'text/plain'
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
if (debug && logLink) {
|
if (debug && logLink) {
|
||||||
log = await this.requestClient
|
log = await this.requestClient
|
||||||
.get<any>(`${this.serverUrl}${logLink.href}/content`, accessToken)
|
.get<any>(`${this.serverUrl}${logLink.href}/content`, access_token)
|
||||||
.then((res: any) => res.result.items.map((i: any) => i.line).join('\n'))
|
.then((res: any) => res.result.items.map((i: any) => i.line).join('\n'))
|
||||||
}
|
}
|
||||||
if (jobStatus === 'failed') {
|
if (jobStatus === 'failed') {
|
||||||
@@ -1073,12 +1141,18 @@ export class SASViyaApiClient {
|
|||||||
private async pollJobState(
|
private async pollJobState(
|
||||||
postedJob: any,
|
postedJob: any,
|
||||||
etag: string | null,
|
etag: string | null,
|
||||||
accessToken?: string,
|
authConfig?: AuthConfig,
|
||||||
pollOptions?: PollOptions
|
pollOptions?: PollOptions
|
||||||
) {
|
) {
|
||||||
|
const logger = process.logger || console
|
||||||
|
|
||||||
let POLL_INTERVAL = 300
|
let POLL_INTERVAL = 300
|
||||||
let MAX_POLL_COUNT = 1000
|
let MAX_POLL_COUNT = 1000
|
||||||
let MAX_ERROR_COUNT = 5
|
let MAX_ERROR_COUNT = 5
|
||||||
|
let access_token = (authConfig || {}).access_token
|
||||||
|
if (authConfig) {
|
||||||
|
;({ access_token } = await this.getTokens(authConfig))
|
||||||
|
}
|
||||||
|
|
||||||
if (pollOptions) {
|
if (pollOptions) {
|
||||||
POLL_INTERVAL = pollOptions.POLL_INTERVAL || POLL_INTERVAL
|
POLL_INTERVAL = pollOptions.POLL_INTERVAL || POLL_INTERVAL
|
||||||
@@ -1092,8 +1166,8 @@ export class SASViyaApiClient {
|
|||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
'If-None-Match': etag
|
'If-None-Match': etag
|
||||||
}
|
}
|
||||||
if (accessToken) {
|
if (access_token) {
|
||||||
headers.Authorization = `Bearer ${accessToken}`
|
headers.Authorization = `Bearer ${access_token}`
|
||||||
}
|
}
|
||||||
const stateLink = postedJob.links.find((l: any) => l.rel === 'state')
|
const stateLink = postedJob.links.find((l: any) => l.rel === 'state')
|
||||||
if (!stateLink) {
|
if (!stateLink) {
|
||||||
@@ -1103,7 +1177,7 @@ export class SASViyaApiClient {
|
|||||||
const { result: state } = await this.requestClient
|
const { result: state } = await this.requestClient
|
||||||
.get<string>(
|
.get<string>(
|
||||||
`${this.serverUrl}${stateLink.href}?_action=wait&wait=300`,
|
`${this.serverUrl}${stateLink.href}?_action=wait&wait=300`,
|
||||||
accessToken,
|
access_token,
|
||||||
'text/plain',
|
'text/plain',
|
||||||
{},
|
{},
|
||||||
this.debug
|
this.debug
|
||||||
@@ -1131,11 +1205,15 @@ export class SASViyaApiClient {
|
|||||||
postedJobState === 'pending' ||
|
postedJobState === 'pending' ||
|
||||||
postedJobState === 'unavailable'
|
postedJobState === 'unavailable'
|
||||||
) {
|
) {
|
||||||
|
if (authConfig) {
|
||||||
|
;({ access_token } = await this.getTokens(authConfig))
|
||||||
|
}
|
||||||
|
|
||||||
if (stateLink) {
|
if (stateLink) {
|
||||||
const { result: jobState } = await this.requestClient
|
const { result: jobState } = await this.requestClient
|
||||||
.get<string>(
|
.get<string>(
|
||||||
`${this.serverUrl}${stateLink.href}?_action=wait&wait=300`,
|
`${this.serverUrl}${stateLink.href}?_action=wait&wait=300`,
|
||||||
accessToken,
|
access_token,
|
||||||
'text/plain',
|
'text/plain',
|
||||||
{},
|
{},
|
||||||
this.debug
|
this.debug
|
||||||
@@ -1164,8 +1242,8 @@ export class SASViyaApiClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (this.debug && printedState !== postedJobState) {
|
if (this.debug && printedState !== postedJobState) {
|
||||||
console.log('Polling job status...')
|
logger.info('Polling job status...')
|
||||||
console.log(`Current job state: ${postedJobState}`)
|
logger.info(`Current job state: ${postedJobState}`)
|
||||||
|
|
||||||
printedState = postedJobState
|
printedState = postedJobState
|
||||||
}
|
}
|
||||||
@@ -1355,6 +1433,9 @@ export class SASViyaApiClient {
|
|||||||
accessToken
|
accessToken
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if (!sourceFolderUri) {
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
const sourceFolderId = sourceFolderUri?.split('/').pop()
|
const sourceFolderId = sourceFolderUri?.split('/').pop()
|
||||||
|
|
||||||
const { result: folder } = await this.requestClient
|
const { result: folder } = await this.requestClient
|
||||||
@@ -1409,4 +1490,21 @@ export class SASViyaApiClient {
|
|||||||
|
|
||||||
return movedFolder
|
return movedFolder
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async getTokens(authConfig: AuthConfig): Promise<AuthConfig> {
|
||||||
|
const logger = process.logger || console
|
||||||
|
let { access_token, refresh_token, client, secret } = authConfig
|
||||||
|
if (
|
||||||
|
isAccessTokenExpiring(access_token) ||
|
||||||
|
isRefreshTokenExpiring(refresh_token)
|
||||||
|
) {
|
||||||
|
logger.info('Refreshing access and refresh tokens.')
|
||||||
|
;({ access_token, refresh_token } = await this.refreshTokens(
|
||||||
|
client,
|
||||||
|
secret,
|
||||||
|
refresh_token
|
||||||
|
))
|
||||||
|
}
|
||||||
|
return { access_token, refresh_token, client, secret }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
236
src/SASjs.ts
236
src/SASjs.ts
@@ -4,15 +4,17 @@ import { SASViyaApiClient } from './SASViyaApiClient'
|
|||||||
import { SAS9ApiClient } from './SAS9ApiClient'
|
import { SAS9ApiClient } from './SAS9ApiClient'
|
||||||
import { FileUploader } from './FileUploader'
|
import { FileUploader } from './FileUploader'
|
||||||
import { AuthManager } from './auth'
|
import { AuthManager } from './auth'
|
||||||
import { ServerType } from '@sasjs/utils/types'
|
import { ServerType, MacroVar, AuthConfig } from '@sasjs/utils/types'
|
||||||
import { RequestClient } from './request/RequestClient'
|
import { RequestClient } from './request/RequestClient'
|
||||||
import {
|
import {
|
||||||
JobExecutor,
|
JobExecutor,
|
||||||
WebJobExecutor,
|
WebJobExecutor,
|
||||||
ComputeJobExecutor,
|
ComputeJobExecutor,
|
||||||
JesJobExecutor
|
JesJobExecutor,
|
||||||
|
Sas9JobExecutor
|
||||||
} from './job-execution'
|
} from './job-execution'
|
||||||
import { ErrorResponse } from './types/errors'
|
import { ErrorResponse } from './types/errors'
|
||||||
|
import { ExtraResponseAttributes } from '@sasjs/utils/types'
|
||||||
|
|
||||||
const defaultConfig: SASjsConfig = {
|
const defaultConfig: SASjsConfig = {
|
||||||
serverUrl: '',
|
serverUrl: '',
|
||||||
@@ -22,7 +24,7 @@ const defaultConfig: SASjsConfig = {
|
|||||||
serverType: ServerType.SasViya,
|
serverType: ServerType.SasViya,
|
||||||
debug: false,
|
debug: false,
|
||||||
contextName: 'SAS Job Execution compute context',
|
contextName: 'SAS Job Execution compute context',
|
||||||
useComputeApi: false,
|
useComputeApi: null,
|
||||||
allowInsecureRequests: false
|
allowInsecureRequests: false
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -41,6 +43,7 @@ export default class SASjs {
|
|||||||
private webJobExecutor: JobExecutor | null = null
|
private webJobExecutor: JobExecutor | null = null
|
||||||
private computeJobExecutor: JobExecutor | null = null
|
private computeJobExecutor: JobExecutor | null = null
|
||||||
private jesJobExecutor: JobExecutor | null = null
|
private jesJobExecutor: JobExecutor | null = null
|
||||||
|
private sas9JobExecutor: JobExecutor | null = null
|
||||||
|
|
||||||
constructor(config?: any) {
|
constructor(config?: any) {
|
||||||
this.sasjsConfig = {
|
this.sasjsConfig = {
|
||||||
@@ -57,15 +60,15 @@ export default class SASjs {
|
|||||||
|
|
||||||
public async executeScriptSAS9(
|
public async executeScriptSAS9(
|
||||||
linesOfCode: string[],
|
linesOfCode: string[],
|
||||||
serverName: string,
|
userName: string,
|
||||||
repositoryName: string
|
password: string
|
||||||
) {
|
) {
|
||||||
this.isMethodSupported('executeScriptSAS9', ServerType.Sas9)
|
this.isMethodSupported('executeScriptSAS9', ServerType.Sas9)
|
||||||
|
|
||||||
return await this.sas9ApiClient?.executeScript(
|
return await this.sas9ApiClient?.executeScript(
|
||||||
linesOfCode,
|
linesOfCode,
|
||||||
serverName,
|
userName,
|
||||||
repositoryName
|
password
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -100,12 +103,12 @@ export default class SASjs {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets executable compute contexts.
|
* Gets executable compute contexts.
|
||||||
* @param accessToken - an access token for an authorized user.
|
* @param authConfig - an access token, refresh token, client and secret for an authorized user.
|
||||||
*/
|
*/
|
||||||
public async getExecutableContexts(accessToken: string) {
|
public async getExecutableContexts(authConfig: AuthConfig) {
|
||||||
this.isMethodSupported('getExecutableContexts', ServerType.SasViya)
|
this.isMethodSupported('getExecutableContexts', ServerType.SasViya)
|
||||||
|
|
||||||
return await this.sasViyaApiClient!.getExecutableContexts(accessToken)
|
return await this.sasViyaApiClient!.getExecutableContexts(authConfig)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -237,14 +240,14 @@ export default class SASjs {
|
|||||||
* @param fileName - name of the file to run. It will be converted to path to the file being submitted for execution.
|
* @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 linesOfCode - lines of sas code from the file to run.
|
||||||
* @param contextName - context name on which code will be run on the server.
|
* @param contextName - context name on which code will be run on the server.
|
||||||
* @param accessToken - (optional) the access token for authorizing the request.
|
* @param authConfig - (optional) the access token, refresh token, client and secret for authorizing the request.
|
||||||
* @param debug - (optional) if true, global debug config will be overriden
|
* @param debug - (optional) if true, global debug config will be overriden
|
||||||
*/
|
*/
|
||||||
public async executeScriptSASViya(
|
public async executeScriptSASViya(
|
||||||
fileName: string,
|
fileName: string,
|
||||||
linesOfCode: string[],
|
linesOfCode: string[],
|
||||||
contextName: string,
|
contextName: string,
|
||||||
accessToken?: string,
|
authConfig?: AuthConfig,
|
||||||
debug?: boolean
|
debug?: boolean
|
||||||
) {
|
) {
|
||||||
this.isMethodSupported('executeScriptSASViya', ServerType.SasViya)
|
this.isMethodSupported('executeScriptSASViya', ServerType.SasViya)
|
||||||
@@ -258,14 +261,14 @@ export default class SASjs {
|
|||||||
fileName,
|
fileName,
|
||||||
linesOfCode,
|
linesOfCode,
|
||||||
contextName,
|
contextName,
|
||||||
accessToken,
|
authConfig,
|
||||||
null,
|
null,
|
||||||
debug ? debug : this.sasjsConfig.debug
|
debug ? debug : this.sasjsConfig.debug
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a folder at SAS file system.
|
* Creates a folder in the logical SAS folder tree
|
||||||
* @param folderName - name of the folder to be created.
|
* @param folderName - name of the folder to be created.
|
||||||
* @param parentFolderPath - the full path (eg `/Public/example/myFolder`) of the parent folder.
|
* @param parentFolderPath - the full path (eg `/Public/example/myFolder`) of the parent folder.
|
||||||
* @param parentFolderUri - the URI of the parent folder.
|
* @param parentFolderUri - the URI of the parent folder.
|
||||||
@@ -297,6 +300,40 @@ export default class SASjs {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a file in the logical SAS folder tree
|
||||||
|
* @param fileName - name of the file to be created.
|
||||||
|
* @param content - content of the file to be created.
|
||||||
|
* @param parentFolderPath - the full path (eg `/Public/example/myFolder`) of the parent folder.
|
||||||
|
* @param parentFolderUri - the URI of the parent folder.
|
||||||
|
* @param accessToken - the access token to authorizing the request.
|
||||||
|
* @param sasApiClient - a client for interfacing with SAS API.
|
||||||
|
*/
|
||||||
|
public async createFile(
|
||||||
|
fileName: string,
|
||||||
|
content: Buffer,
|
||||||
|
parentFolderPath: string,
|
||||||
|
parentFolderUri?: string,
|
||||||
|
accessToken?: string,
|
||||||
|
sasApiClient?: SASViyaApiClient
|
||||||
|
) {
|
||||||
|
if (sasApiClient)
|
||||||
|
return await sasApiClient.createFile(
|
||||||
|
fileName,
|
||||||
|
content,
|
||||||
|
parentFolderPath,
|
||||||
|
parentFolderUri,
|
||||||
|
accessToken
|
||||||
|
)
|
||||||
|
return await this.sasViyaApiClient!.createFile(
|
||||||
|
fileName,
|
||||||
|
content,
|
||||||
|
parentFolderPath,
|
||||||
|
parentFolderUri,
|
||||||
|
accessToken
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fetches a folder from the SAS file system.
|
* Fetches a folder from the SAS file system.
|
||||||
* @param folderPath - path of the folder to be fetched.
|
* @param folderPath - path of the folder to be fetched.
|
||||||
@@ -507,12 +544,7 @@ export default class SASjs {
|
|||||||
public uploadFile(sasJob: string, files: UploadFile[], params: any) {
|
public uploadFile(sasJob: string, files: UploadFile[], params: any) {
|
||||||
const fileUploader =
|
const fileUploader =
|
||||||
this.fileUploader ||
|
this.fileUploader ||
|
||||||
new FileUploader(
|
new FileUploader(this.sasjsConfig, this.jobsPath, this.requestClient!)
|
||||||
this.sasjsConfig.appLoc,
|
|
||||||
this.sasjsConfig.serverUrl,
|
|
||||||
this.jobsPath,
|
|
||||||
this.requestClient!
|
|
||||||
)
|
|
||||||
|
|
||||||
return fileUploader.uploadFile(sasJob, files, params)
|
return fileUploader.uploadFile(sasJob, files, params)
|
||||||
}
|
}
|
||||||
@@ -538,44 +570,130 @@ export default class SASjs {
|
|||||||
* `await request(sasJobPath, data, config, () => setIsLoggedIn(false))`
|
* `await request(sasJobPath, data, config, () => setIsLoggedIn(false))`
|
||||||
* If you are not passing in any data and configuration, it will look like so:
|
* If you are not passing in any data and configuration, it will look like so:
|
||||||
* `await request(sasJobPath, {}, {}, () => setIsLoggedIn(false))`
|
* `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(
|
public async request(
|
||||||
sasJob: string,
|
sasJob: string,
|
||||||
data: { [key: string]: any },
|
data: { [key: string]: any } | null,
|
||||||
config: { [key: string]: any } = {},
|
config: { [key: string]: any } = {},
|
||||||
loginRequiredCallback?: () => any,
|
loginRequiredCallback?: () => any,
|
||||||
accessToken?: string
|
authConfig?: AuthConfig,
|
||||||
|
extraResponseAttributes: ExtraResponseAttributes[] = []
|
||||||
) {
|
) {
|
||||||
config = {
|
config = {
|
||||||
...this.sasjsConfig,
|
...this.sasjsConfig,
|
||||||
...config
|
...config
|
||||||
}
|
}
|
||||||
|
|
||||||
if (config.serverType === ServerType.SasViya && config.contextName) {
|
const validationResult = this.validateInput(data)
|
||||||
if (config.useComputeApi) {
|
|
||||||
return await this.computeJobExecutor!.execute(
|
if (validationResult.status) {
|
||||||
sasJob,
|
if (
|
||||||
data,
|
config.serverType !== ServerType.Sas9 &&
|
||||||
config,
|
config.useComputeApi !== undefined &&
|
||||||
loginRequiredCallback,
|
config.useComputeApi !== null
|
||||||
accessToken
|
) {
|
||||||
)
|
if (config.useComputeApi) {
|
||||||
|
return await this.computeJobExecutor!.execute(
|
||||||
|
sasJob,
|
||||||
|
data,
|
||||||
|
config,
|
||||||
|
loginRequiredCallback,
|
||||||
|
authConfig
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
return await this.jesJobExecutor!.execute(
|
||||||
|
sasJob,
|
||||||
|
data,
|
||||||
|
config,
|
||||||
|
loginRequiredCallback,
|
||||||
|
authConfig,
|
||||||
|
extraResponseAttributes
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else if (
|
||||||
|
config.serverType === ServerType.Sas9 &&
|
||||||
|
config.username &&
|
||||||
|
config.password
|
||||||
|
) {
|
||||||
|
return await this.sas9JobExecutor!.execute(sasJob, data, config)
|
||||||
} else {
|
} else {
|
||||||
return await this.jesJobExecutor!.execute(
|
return await this.webJobExecutor!.execute(
|
||||||
sasJob,
|
sasJob,
|
||||||
data,
|
data,
|
||||||
config,
|
config,
|
||||||
loginRequiredCallback,
|
loginRequiredCallback,
|
||||||
accessToken
|
authConfig,
|
||||||
|
extraResponseAttributes
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
return await this.webJobExecutor!.execute(
|
return Promise.reject(new ErrorResponse(validationResult.msg))
|
||||||
sasJob,
|
}
|
||||||
data,
|
}
|
||||||
config,
|
|
||||||
loginRequiredCallback
|
/**
|
||||||
)
|
* This function validates the input data structure and table naming convention
|
||||||
|
*
|
||||||
|
* @param data A json object that contains one or more tables, it can also be null
|
||||||
|
* @returns An object which contains two attributes: 1) status: boolean, 2) msg: string
|
||||||
|
*/
|
||||||
|
private validateInput(data: { [key: string]: any } | null): {
|
||||||
|
status: boolean
|
||||||
|
msg: string
|
||||||
|
} {
|
||||||
|
if (data === null) return { status: true, msg: '' }
|
||||||
|
for (const key in data) {
|
||||||
|
if (!key.match(/^[a-zA-Z_]/)) {
|
||||||
|
return {
|
||||||
|
status: false,
|
||||||
|
msg: 'First letter of table should be alphabet or underscore.'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!key.match(/^[a-zA-Z_][a-zA-Z0-9_]*$/)) {
|
||||||
|
return { status: false, msg: 'Table name should be alphanumeric.' }
|
||||||
|
}
|
||||||
|
|
||||||
|
if (key.length > 32) {
|
||||||
|
return {
|
||||||
|
status: false,
|
||||||
|
msg: 'Maximum length for table name could be 32 characters.'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.getType(data[key]) !== 'Array') {
|
||||||
|
return {
|
||||||
|
status: false,
|
||||||
|
msg: 'Parameter data contains invalid table structure.'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let i = 0; i < data[key].length; i++) {
|
||||||
|
if (this.getType(data[key][i]) !== 'object') {
|
||||||
|
return {
|
||||||
|
status: false,
|
||||||
|
msg: `Table ${key} contains invalid structure.`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return { status: true, msg: '' }
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* this function returns the type of variable
|
||||||
|
*
|
||||||
|
* @param data it could be anything, like string, array, object etc.
|
||||||
|
* @returns a string which tells the type of input parameter
|
||||||
|
*/
|
||||||
|
private getType(data: any): string {
|
||||||
|
if (Array.isArray(data)) {
|
||||||
|
return 'Array'
|
||||||
|
} else {
|
||||||
|
return typeof data
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -616,7 +734,7 @@ export default class SASjs {
|
|||||||
)
|
)
|
||||||
sasApiClient.debug = this.sasjsConfig.debug
|
sasApiClient.debug = this.sasjsConfig.debug
|
||||||
} else if (this.sasjsConfig.serverType === ServerType.Sas9) {
|
} else if (this.sasjsConfig.serverType === ServerType.Sas9) {
|
||||||
sasApiClient = new SAS9ApiClient(serverUrl)
|
sasApiClient = new SAS9ApiClient(serverUrl, this.jobsPath)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
let sasClientConfig: any = null
|
let sasClientConfig: any = null
|
||||||
@@ -658,20 +776,22 @@ export default class SASjs {
|
|||||||
* @param config - provide any changes to the config here, for instance to
|
* @param config - provide any changes to the config here, for instance to
|
||||||
* enable/disable `debug`. Any change provided will override the global config,
|
* enable/disable `debug`. Any change provided will override the global config,
|
||||||
* for that particular function call.
|
* for that particular function call.
|
||||||
* @param accessToken - a valid access token that is authorised to execute compute jobs.
|
* @param authConfig - a valid client, secret, refresh and access tokens that are authorised to execute compute jobs.
|
||||||
* The access token is not required when the user is authenticated via the browser.
|
* The access token is not required when the user is authenticated via the browser.
|
||||||
* @param waitForResult - a boolean that indicates whether the function needs to wait for execution to complete.
|
* @param waitForResult - a boolean that indicates whether the function needs to wait for execution to complete.
|
||||||
* @param pollOptions - an object that represents poll interval(milliseconds) and maximum amount of attempts. Object example: { MAX_POLL_COUNT: 24 * 60 * 60, POLL_INTERVAL: 1000 }.
|
* @param pollOptions - an object that represents poll interval(milliseconds) and maximum amount of attempts. Object example: { MAX_POLL_COUNT: 24 * 60 * 60, POLL_INTERVAL: 1000 }.
|
||||||
* @param printPid - a boolean that indicates whether the function should print (PID) of the started job.
|
* @param printPid - a boolean that indicates whether the function should print (PID) of the started job.
|
||||||
|
* @param variables - an object that represents macro variables.
|
||||||
*/
|
*/
|
||||||
public async startComputeJob(
|
public async startComputeJob(
|
||||||
sasJob: string,
|
sasJob: string,
|
||||||
data: any,
|
data: any,
|
||||||
config: any = {},
|
config: any = {},
|
||||||
accessToken?: string,
|
authConfig?: AuthConfig,
|
||||||
waitForResult?: boolean,
|
waitForResult?: boolean,
|
||||||
pollOptions?: PollOptions,
|
pollOptions?: PollOptions,
|
||||||
printPid = false
|
printPid = false,
|
||||||
|
variables?: MacroVar
|
||||||
) {
|
) {
|
||||||
config = {
|
config = {
|
||||||
...this.sasjsConfig,
|
...this.sasjsConfig,
|
||||||
@@ -690,11 +810,12 @@ export default class SASjs {
|
|||||||
config.contextName,
|
config.contextName,
|
||||||
config.debug,
|
config.debug,
|
||||||
data,
|
data,
|
||||||
accessToken,
|
authConfig,
|
||||||
!!waitForResult,
|
!!waitForResult,
|
||||||
false,
|
false,
|
||||||
pollOptions,
|
pollOptions,
|
||||||
printPid
|
printPid,
|
||||||
|
variables
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -805,12 +926,15 @@ export default class SASjs {
|
|||||||
if (this.sasjsConfig.serverType === ServerType.Sas9) {
|
if (this.sasjsConfig.serverType === ServerType.Sas9) {
|
||||||
if (this.sas9ApiClient)
|
if (this.sas9ApiClient)
|
||||||
this.sas9ApiClient!.setConfig(this.sasjsConfig.serverUrl)
|
this.sas9ApiClient!.setConfig(this.sasjsConfig.serverUrl)
|
||||||
else this.sas9ApiClient = new SAS9ApiClient(this.sasjsConfig.serverUrl)
|
else
|
||||||
|
this.sas9ApiClient = new SAS9ApiClient(
|
||||||
|
this.sasjsConfig.serverUrl,
|
||||||
|
this.jobsPath
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
this.fileUploader = new FileUploader(
|
this.fileUploader = new FileUploader(
|
||||||
this.sasjsConfig.appLoc,
|
this.sasjsConfig,
|
||||||
this.sasjsConfig.serverUrl,
|
|
||||||
this.jobsPath,
|
this.jobsPath,
|
||||||
this.requestClient
|
this.requestClient
|
||||||
)
|
)
|
||||||
@@ -823,6 +947,12 @@ export default class SASjs {
|
|||||||
this.sasViyaApiClient!
|
this.sasViyaApiClient!
|
||||||
)
|
)
|
||||||
|
|
||||||
|
this.sas9JobExecutor = new Sas9JobExecutor(
|
||||||
|
this.sasjsConfig.serverUrl,
|
||||||
|
this.sasjsConfig.serverType!,
|
||||||
|
this.jobsPath
|
||||||
|
)
|
||||||
|
|
||||||
this.computeJobExecutor = new ComputeJobExecutor(
|
this.computeJobExecutor = new ComputeJobExecutor(
|
||||||
this.sasjsConfig.serverUrl,
|
this.sasjsConfig.serverUrl,
|
||||||
this.sasViyaApiClient!
|
this.sasViyaApiClient!
|
||||||
@@ -853,6 +983,16 @@ export default class SASjs {
|
|||||||
isForced
|
isForced
|
||||||
)
|
)
|
||||||
break
|
break
|
||||||
|
case 'file':
|
||||||
|
await this.createFile(
|
||||||
|
member.name,
|
||||||
|
member.code,
|
||||||
|
parentFolder,
|
||||||
|
undefined,
|
||||||
|
accessToken,
|
||||||
|
sasApiClient
|
||||||
|
)
|
||||||
|
break
|
||||||
case 'service':
|
case 'service':
|
||||||
await this.createJobDefinition(
|
await this.createJobDefinition(
|
||||||
member.name,
|
member.name,
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { Session, Context, CsrfToken, SessionVariable } from './types'
|
import { Session, Context, SessionVariable } from './types'
|
||||||
|
import { NoSessionStateError } from './types/errors'
|
||||||
import { asyncForEach, isUrl } from './utils'
|
import { asyncForEach, isUrl } from './utils'
|
||||||
import { prefixMessage } from '@sasjs/utils/error'
|
import { prefixMessage } from '@sasjs/utils/error'
|
||||||
import { RequestClient } from './request/RequestClient'
|
import { RequestClient } from './request/RequestClient'
|
||||||
@@ -6,10 +7,6 @@ import { RequestClient } from './request/RequestClient'
|
|||||||
const MAX_SESSION_COUNT = 1
|
const MAX_SESSION_COUNT = 1
|
||||||
const RETRY_LIMIT: number = 3
|
const RETRY_LIMIT: number = 3
|
||||||
let RETRY_COUNT: number = 0
|
let RETRY_COUNT: number = 0
|
||||||
const INTERNAL_SAS_ERROR = {
|
|
||||||
status: 304,
|
|
||||||
message: 'Not Modified'
|
|
||||||
}
|
|
||||||
|
|
||||||
export class SessionManager {
|
export class SessionManager {
|
||||||
constructor(
|
constructor(
|
||||||
@@ -158,11 +155,13 @@ export class SessionManager {
|
|||||||
etag: string | null,
|
etag: string | null,
|
||||||
accessToken?: string
|
accessToken?: string
|
||||||
) {
|
) {
|
||||||
|
const logger = process.logger || console
|
||||||
|
|
||||||
let sessionState = session.state
|
let sessionState = session.state
|
||||||
|
|
||||||
const stateLink = session.links.find((l: any) => l.rel === 'state')
|
const stateLink = session.links.find((l: any) => l.rel === 'state')
|
||||||
|
|
||||||
return new Promise(async (resolve, _) => {
|
return new Promise(async (resolve, reject) => {
|
||||||
if (
|
if (
|
||||||
sessionState === 'pending' ||
|
sessionState === 'pending' ||
|
||||||
sessionState === 'running' ||
|
sessionState === 'running' ||
|
||||||
@@ -170,23 +169,24 @@ export class SessionManager {
|
|||||||
) {
|
) {
|
||||||
if (stateLink) {
|
if (stateLink) {
|
||||||
if (this.debug && !this.printedSessionState.printed) {
|
if (this.debug && !this.printedSessionState.printed) {
|
||||||
console.log('Polling session status...')
|
logger.info('Polling session status...')
|
||||||
|
|
||||||
this.printedSessionState.printed = true
|
this.printedSessionState.printed = true
|
||||||
}
|
}
|
||||||
|
|
||||||
const state = await this.getSessionState(
|
const { result: state, responseStatus: responseStatus } =
|
||||||
`${this.serverUrl}${stateLink.href}?wait=30`,
|
await this.getSessionState(
|
||||||
etag!,
|
`${this.serverUrl}${stateLink.href}?wait=30`,
|
||||||
accessToken
|
etag!,
|
||||||
).catch((err) => {
|
accessToken
|
||||||
throw err
|
).catch((err) => {
|
||||||
})
|
throw prefixMessage(err, 'Error while getting session state.')
|
||||||
|
})
|
||||||
|
|
||||||
sessionState = state.trim()
|
sessionState = state.trim()
|
||||||
|
|
||||||
if (this.debug && this.printedSessionState.state !== sessionState) {
|
if (this.debug && this.printedSessionState.state !== sessionState) {
|
||||||
console.log(`Current session state is '${sessionState}'`)
|
logger.info(`Current session state is '${sessionState}'`)
|
||||||
|
|
||||||
this.printedSessionState.state = sessionState
|
this.printedSessionState.state = sessionState
|
||||||
this.printedSessionState.printed = false
|
this.printedSessionState.printed = false
|
||||||
@@ -194,13 +194,21 @@ export class SessionManager {
|
|||||||
|
|
||||||
// There is an internal error present in SAS Viya 3.5
|
// 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
|
// Retry to wait for a session status in such case of SAS internal error
|
||||||
if (
|
if (!sessionState) {
|
||||||
sessionState === INTERNAL_SAS_ERROR.message &&
|
if (RETRY_COUNT < RETRY_LIMIT) {
|
||||||
RETRY_COUNT < RETRY_LIMIT
|
RETRY_COUNT++
|
||||||
) {
|
|
||||||
RETRY_COUNT++
|
|
||||||
|
|
||||||
resolve(this.waitForSession(session, etag, accessToken))
|
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)
|
resolve(sessionState)
|
||||||
@@ -218,11 +226,11 @@ export class SessionManager {
|
|||||||
) {
|
) {
|
||||||
return await this.requestClient
|
return await this.requestClient
|
||||||
.get(url, accessToken, 'text/plain', { 'If-None-Match': etag })
|
.get(url, accessToken, 'text/plain', { 'If-None-Match': etag })
|
||||||
.then((res) => res.result as string)
|
.then((res) => ({
|
||||||
|
result: res.result as string,
|
||||||
|
responseStatus: res.status
|
||||||
|
}))
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
if (err.status === INTERNAL_SAS_ERROR.status)
|
|
||||||
return INTERNAL_SAS_ERROR.message
|
|
||||||
|
|
||||||
throw err
|
throw err
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import { ServerType } from '@sasjs/utils/types'
|
import { ServerType } from '@sasjs/utils/types'
|
||||||
import { isAuthorizeFormRequired } from '.'
|
|
||||||
import { RequestClient } from '../request/RequestClient'
|
import { RequestClient } from '../request/RequestClient'
|
||||||
import { serialize } from '../utils'
|
import { serialize } from '../utils'
|
||||||
|
|
||||||
@@ -35,6 +34,7 @@ export class AuthManager {
|
|||||||
this.userName = loginParams.username
|
this.userName = loginParams.username
|
||||||
|
|
||||||
const { isLoggedIn, loginForm } = await this.checkSession()
|
const { isLoggedIn, loginForm } = await this.checkSession()
|
||||||
|
|
||||||
if (isLoggedIn) {
|
if (isLoggedIn) {
|
||||||
await this.loginCallback()
|
await this.loginCallback()
|
||||||
|
|
||||||
@@ -44,6 +44,44 @@ export class AuthManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let loginResponse = await this.sendLoginRequest(loginForm, loginParams)
|
||||||
|
|
||||||
|
let loggedIn = isLogInSuccess(loginResponse)
|
||||||
|
|
||||||
|
if (!loggedIn) {
|
||||||
|
if (isCredentialsVerifyError(loginResponse)) {
|
||||||
|
const newLoginForm = await this.getLoginForm(loginResponse)
|
||||||
|
|
||||||
|
loginResponse = await this.sendLoginRequest(newLoginForm, loginParams)
|
||||||
|
}
|
||||||
|
|
||||||
|
const currentSession = await this.checkSession()
|
||||||
|
loggedIn = currentSession.isLoggedIn
|
||||||
|
}
|
||||||
|
|
||||||
|
if (loggedIn) {
|
||||||
|
if (this.serverType === ServerType.Sas9) {
|
||||||
|
const casAuthenticationUrl = `${this.serverUrl}/SASStoredProcess/j_spring_cas_security_check`
|
||||||
|
|
||||||
|
await this.requestClient.get<string>(
|
||||||
|
`/SASLogon/login?service=${casAuthenticationUrl}`,
|
||||||
|
undefined
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
this.loginCallback()
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
isLoggedIn: !!loggedIn,
|
||||||
|
userName: this.userName
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async sendLoginRequest(
|
||||||
|
loginForm: { [key: string]: any },
|
||||||
|
loginParams: { [key: string]: any }
|
||||||
|
) {
|
||||||
for (const key in loginForm) {
|
for (const key in loginForm) {
|
||||||
loginParams[key] = loginForm[key]
|
loginParams[key] = loginForm[key]
|
||||||
}
|
}
|
||||||
@@ -60,21 +98,7 @@ export class AuthManager {
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
let loggedIn = isLogInSuccess(loginResponse)
|
return loginResponse
|
||||||
|
|
||||||
if (!loggedIn) {
|
|
||||||
const currentSession = await this.checkSession()
|
|
||||||
loggedIn = currentSession.isLoggedIn
|
|
||||||
}
|
|
||||||
|
|
||||||
if (loggedIn) {
|
|
||||||
this.loginCallback()
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
isLoggedIn: !!loggedIn,
|
|
||||||
userName: this.userName
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -168,5 +192,10 @@ export class AuthManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const isCredentialsVerifyError = (response: string): boolean =>
|
||||||
|
/An error occurred while the system was verifying your credentials. Please enter your credentials again./gm.test(
|
||||||
|
response
|
||||||
|
)
|
||||||
|
|
||||||
const isLogInSuccess = (response: string): boolean =>
|
const isLogInSuccess = (response: string): boolean =>
|
||||||
/You have signed in/gm.test(response)
|
/You have signed in/gm.test(response)
|
||||||
|
|||||||
@@ -57,7 +57,7 @@ describe('AuthManager', () => {
|
|||||||
expect((authManager as any).logoutUrl).toEqual('/SASLogon/logout?')
|
expect((authManager as any).logoutUrl).toEqual('/SASLogon/logout?')
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should call the auth callback and return when already logged in', async (done) => {
|
it('should call the auth callback and return when already logged in', async () => {
|
||||||
const authManager = new AuthManager(
|
const authManager = new AuthManager(
|
||||||
serverUrl,
|
serverUrl,
|
||||||
serverType,
|
serverType,
|
||||||
@@ -77,10 +77,9 @@ describe('AuthManager', () => {
|
|||||||
expect(loginResponse.isLoggedIn).toBeTruthy()
|
expect(loginResponse.isLoggedIn).toBeTruthy()
|
||||||
expect(loginResponse.userName).toEqual(userName)
|
expect(loginResponse.userName).toEqual(userName)
|
||||||
expect(authCallback).toHaveBeenCalledTimes(1)
|
expect(authCallback).toHaveBeenCalledTimes(1)
|
||||||
done()
|
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should post a login request to the server if not logged in', async (done) => {
|
it('should post a login request to the server if not logged in', async () => {
|
||||||
const authManager = new AuthManager(
|
const authManager = new AuthManager(
|
||||||
serverUrl,
|
serverUrl,
|
||||||
serverType,
|
serverType,
|
||||||
@@ -121,10 +120,9 @@ describe('AuthManager', () => {
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
expect(authCallback).toHaveBeenCalledTimes(1)
|
expect(authCallback).toHaveBeenCalledTimes(1)
|
||||||
done()
|
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should parse and submit the authorisation form when necessary', async (done) => {
|
it('should parse and submit the authorisation form when necessary', async () => {
|
||||||
const authManager = new AuthManager(
|
const authManager = new AuthManager(
|
||||||
serverUrl,
|
serverUrl,
|
||||||
serverType,
|
serverType,
|
||||||
@@ -160,10 +158,9 @@ describe('AuthManager', () => {
|
|||||||
expect(requestClient.authorize).toHaveBeenCalledWith(
|
expect(requestClient.authorize).toHaveBeenCalledWith(
|
||||||
mockLoginAuthoriseRequiredResponse
|
mockLoginAuthoriseRequiredResponse
|
||||||
)
|
)
|
||||||
done()
|
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should check and return session information if logged in', async (done) => {
|
it('should check and return session information if logged in', async () => {
|
||||||
const authManager = new AuthManager(
|
const authManager = new AuthManager(
|
||||||
serverUrl,
|
serverUrl,
|
||||||
serverType,
|
serverType,
|
||||||
@@ -189,7 +186,5 @@ describe('AuthManager', () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
done()
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { ServerType } from '@sasjs/utils/types'
|
import { AuthConfig, ServerType } from '@sasjs/utils/types'
|
||||||
import { SASViyaApiClient } from '../SASViyaApiClient'
|
import { SASViyaApiClient } from '../SASViyaApiClient'
|
||||||
import {
|
import {
|
||||||
ErrorResponse,
|
ErrorResponse,
|
||||||
@@ -17,7 +17,7 @@ export class ComputeJobExecutor extends BaseJobExecutor {
|
|||||||
data: any,
|
data: any,
|
||||||
config: any,
|
config: any,
|
||||||
loginRequiredCallback?: any,
|
loginRequiredCallback?: any,
|
||||||
accessToken?: string
|
authConfig?: AuthConfig
|
||||||
) {
|
) {
|
||||||
const loginCallback = loginRequiredCallback || (() => Promise.resolve())
|
const loginCallback = loginRequiredCallback || (() => Promise.resolve())
|
||||||
const waitForResult = true
|
const waitForResult = true
|
||||||
@@ -30,7 +30,7 @@ export class ComputeJobExecutor extends BaseJobExecutor {
|
|||||||
config.contextName,
|
config.contextName,
|
||||||
config.debug,
|
config.debug,
|
||||||
data,
|
data,
|
||||||
accessToken,
|
authConfig,
|
||||||
waitForResult,
|
waitForResult,
|
||||||
expectWebout
|
expectWebout
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
import { ServerType } from '@sasjs/utils/types'
|
import { AuthConfig, ServerType } from '@sasjs/utils/types'
|
||||||
import { SASViyaApiClient } from '../SASViyaApiClient'
|
import { SASViyaApiClient } from '../SASViyaApiClient'
|
||||||
import {
|
import {
|
||||||
ErrorResponse,
|
ErrorResponse,
|
||||||
JobExecutionError,
|
JobExecutionError,
|
||||||
LoginRequiredError
|
LoginRequiredError
|
||||||
} from '../types/errors'
|
} from '../types/errors'
|
||||||
|
import { ExtraResponseAttributes } from '@sasjs/utils/types'
|
||||||
import { BaseJobExecutor } from './JobExecutor'
|
import { BaseJobExecutor } from './JobExecutor'
|
||||||
|
|
||||||
export class JesJobExecutor extends BaseJobExecutor {
|
export class JesJobExecutor extends BaseJobExecutor {
|
||||||
@@ -17,23 +18,34 @@ export class JesJobExecutor extends BaseJobExecutor {
|
|||||||
data: any,
|
data: any,
|
||||||
config: any,
|
config: any,
|
||||||
loginRequiredCallback?: any,
|
loginRequiredCallback?: any,
|
||||||
accessToken?: string
|
authConfig?: AuthConfig,
|
||||||
|
extraResponseAttributes: ExtraResponseAttributes[] = []
|
||||||
) {
|
) {
|
||||||
const loginCallback = loginRequiredCallback || (() => Promise.resolve())
|
const loginCallback = loginRequiredCallback || (() => Promise.resolve())
|
||||||
|
|
||||||
const requestPromise = new Promise((resolve, reject) => {
|
const requestPromise = new Promise((resolve, reject) => {
|
||||||
this.sasViyaApiClient
|
this.sasViyaApiClient
|
||||||
?.executeJob(
|
?.executeJob(sasJob, config.contextName, config.debug, data, authConfig)
|
||||||
sasJob,
|
.then((response: any) => {
|
||||||
config.contextName,
|
|
||||||
config.debug,
|
|
||||||
data,
|
|
||||||
accessToken
|
|
||||||
)
|
|
||||||
.then((response) => {
|
|
||||||
this.appendRequest(response, sasJob, config.debug)
|
this.appendRequest(response, sasJob, config.debug)
|
||||||
|
|
||||||
resolve(response)
|
let responseObject = {}
|
||||||
|
|
||||||
|
if (extraResponseAttributes && extraResponseAttributes.length > 0) {
|
||||||
|
const extraAttributes = extraResponseAttributes.reduce(
|
||||||
|
(map: any, obj: any) => ((map[obj] = response[obj]), map),
|
||||||
|
{}
|
||||||
|
)
|
||||||
|
|
||||||
|
responseObject = {
|
||||||
|
result: response.result,
|
||||||
|
...extraAttributes
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
responseObject = response.result
|
||||||
|
}
|
||||||
|
|
||||||
|
resolve(responseObject)
|
||||||
})
|
})
|
||||||
.catch(async (e: Error) => {
|
.catch(async (e: Error) => {
|
||||||
if (e instanceof JobExecutionError) {
|
if (e instanceof JobExecutionError) {
|
||||||
@@ -50,7 +62,9 @@ export class JesJobExecutor extends BaseJobExecutor {
|
|||||||
sasJob,
|
sasJob,
|
||||||
data,
|
data,
|
||||||
config,
|
config,
|
||||||
loginRequiredCallback
|
loginRequiredCallback,
|
||||||
|
authConfig,
|
||||||
|
extraResponseAttributes
|
||||||
).then(
|
).then(
|
||||||
(res: any) => {
|
(res: any) => {
|
||||||
resolve(res)
|
resolve(res)
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { ServerType } from '@sasjs/utils/types'
|
import { AuthConfig, ServerType } from '@sasjs/utils/types'
|
||||||
import { SASjsRequest } from '../types'
|
import { SASjsRequest } from '../types'
|
||||||
|
import { ExtraResponseAttributes } from '@sasjs/utils/types'
|
||||||
import { asyncForEach, parseGeneratedCode, parseSourceCode } from '../utils'
|
import { asyncForEach, parseGeneratedCode, parseSourceCode } from '../utils'
|
||||||
|
|
||||||
export type ExecuteFunction = () => Promise<any>
|
export type ExecuteFunction = () => Promise<any>
|
||||||
@@ -10,7 +11,8 @@ export interface JobExecutor {
|
|||||||
data: any,
|
data: any,
|
||||||
config: any,
|
config: any,
|
||||||
loginRequiredCallback?: any,
|
loginRequiredCallback?: any,
|
||||||
accessToken?: string
|
authConfig?: AuthConfig,
|
||||||
|
extraResponseAttributes?: ExtraResponseAttributes[]
|
||||||
) => Promise<any>
|
) => Promise<any>
|
||||||
resendWaitingRequests: () => Promise<void>
|
resendWaitingRequests: () => Promise<void>
|
||||||
getRequests: () => SASjsRequest[]
|
getRequests: () => SASjsRequest[]
|
||||||
@@ -28,7 +30,8 @@ export abstract class BaseJobExecutor implements JobExecutor {
|
|||||||
data: any,
|
data: any,
|
||||||
config: any,
|
config: any,
|
||||||
loginRequiredCallback?: any,
|
loginRequiredCallback?: any,
|
||||||
accessToken?: string | undefined
|
authConfig?: AuthConfig | undefined,
|
||||||
|
extraResponseAttributes?: ExtraResponseAttributes[]
|
||||||
): Promise<any>
|
): Promise<any>
|
||||||
|
|
||||||
resendWaitingRequests = async () => {
|
resendWaitingRequests = async () => {
|
||||||
@@ -59,14 +62,14 @@ export abstract class BaseJobExecutor implements JobExecutor {
|
|||||||
let sasWork = null
|
let sasWork = null
|
||||||
|
|
||||||
if (debug) {
|
if (debug) {
|
||||||
if (response?.result && response?.log) {
|
if (response?.log) {
|
||||||
sourceCode = parseSourceCode(response.log)
|
sourceCode = parseSourceCode(response.log)
|
||||||
generatedCode = parseGeneratedCode(response.log)
|
generatedCode = parseGeneratedCode(response.log)
|
||||||
|
|
||||||
if (response.log) {
|
if (response?.result) {
|
||||||
sasWork = response.log
|
|
||||||
} else {
|
|
||||||
sasWork = response.result.WORK
|
sasWork = response.result.WORK
|
||||||
|
} else {
|
||||||
|
sasWork = response.log
|
||||||
}
|
}
|
||||||
} else if (response?.result) {
|
} else if (response?.result) {
|
||||||
sourceCode = parseSourceCode(response.result)
|
sourceCode = parseSourceCode(response.result)
|
||||||
|
|||||||
110
src/job-execution/Sas9JobExecutor.ts
Normal file
110
src/job-execution/Sas9JobExecutor.ts
Normal file
@@ -0,0 +1,110 @@
|
|||||||
|
import { ServerType } from '@sasjs/utils/types'
|
||||||
|
import * as NodeFormData from 'form-data'
|
||||||
|
import { ErrorResponse } from '../types/errors'
|
||||||
|
import { convertToCSV, isRelativePath } from '../utils'
|
||||||
|
import { BaseJobExecutor } from './JobExecutor'
|
||||||
|
import { Sas9RequestClient } from '../request/Sas9RequestClient'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Job executor for SAS9 servers for use in Node.js environments.
|
||||||
|
* Initiates login with the provided username and password from the config
|
||||||
|
* The cookies are stored in the request client and used in subsequent
|
||||||
|
* job execution requests.
|
||||||
|
*/
|
||||||
|
export class Sas9JobExecutor extends BaseJobExecutor {
|
||||||
|
private requestClient: Sas9RequestClient
|
||||||
|
constructor(
|
||||||
|
serverUrl: string,
|
||||||
|
serverType: ServerType,
|
||||||
|
private jobsPath: string
|
||||||
|
) {
|
||||||
|
super(serverUrl, serverType)
|
||||||
|
this.requestClient = new Sas9RequestClient(serverUrl, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
async execute(sasJob: string, data: any, config: any) {
|
||||||
|
const program = isRelativePath(sasJob)
|
||||||
|
? config.appLoc
|
||||||
|
? config.appLoc.replace(/\/?$/, '/') + sasJob.replace(/^\//, '')
|
||||||
|
: sasJob
|
||||||
|
: sasJob
|
||||||
|
let apiUrl = `${config.serverUrl}${this.jobsPath}?${'_program=' + program}`
|
||||||
|
apiUrl = `${apiUrl}${
|
||||||
|
config.username && config.password
|
||||||
|
? '&_username=' + config.username + '&_password=' + config.password
|
||||||
|
: ''
|
||||||
|
}`
|
||||||
|
|
||||||
|
let requestParams = {
|
||||||
|
...this.getRequestParams(config)
|
||||||
|
}
|
||||||
|
|
||||||
|
let formData = new NodeFormData()
|
||||||
|
|
||||||
|
if (data) {
|
||||||
|
try {
|
||||||
|
formData = generateFileUploadForm(formData, data)
|
||||||
|
} catch (e) {
|
||||||
|
return Promise.reject(new ErrorResponse(e?.message, e))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const key in requestParams) {
|
||||||
|
if (requestParams.hasOwnProperty(key)) {
|
||||||
|
formData.append(key, requestParams[key])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.requestClient.login(
|
||||||
|
config.username,
|
||||||
|
config.password,
|
||||||
|
this.jobsPath
|
||||||
|
)
|
||||||
|
const contentType =
|
||||||
|
data && Object.keys(data).length
|
||||||
|
? 'multipart/form-data; boundary=' + (formData as any)._boundary
|
||||||
|
: 'text/plain'
|
||||||
|
return await this.requestClient!.post(
|
||||||
|
apiUrl,
|
||||||
|
formData,
|
||||||
|
undefined,
|
||||||
|
contentType,
|
||||||
|
{
|
||||||
|
Accept: '*/*',
|
||||||
|
Connection: 'Keep-Alive'
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private getRequestParams(config: any): any {
|
||||||
|
const requestParams: any = {}
|
||||||
|
|
||||||
|
if (config.debug) {
|
||||||
|
requestParams['_debug'] = 131
|
||||||
|
}
|
||||||
|
|
||||||
|
return requestParams
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const generateFileUploadForm = (
|
||||||
|
formData: NodeFormData,
|
||||||
|
data: any
|
||||||
|
): NodeFormData => {
|
||||||
|
for (const tableName in data) {
|
||||||
|
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.'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
formData.append(name, csv, {
|
||||||
|
filename: `${name}.csv`,
|
||||||
|
contentType: 'application/csv'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return formData
|
||||||
|
}
|
||||||
@@ -8,8 +8,9 @@ import { generateFileUploadForm } from '../file/generateFileUploadForm'
|
|||||||
import { generateTableUploadForm } from '../file/generateTableUploadForm'
|
import { generateTableUploadForm } from '../file/generateTableUploadForm'
|
||||||
import { RequestClient } from '../request/RequestClient'
|
import { RequestClient } from '../request/RequestClient'
|
||||||
import { SASViyaApiClient } from '../SASViyaApiClient'
|
import { SASViyaApiClient } from '../SASViyaApiClient'
|
||||||
import { isRelativePath } from '../utils'
|
import { isRelativePath, isValidJson } from '../utils'
|
||||||
import { BaseJobExecutor } from './JobExecutor'
|
import { BaseJobExecutor } from './JobExecutor'
|
||||||
|
import { parseWeboutResponse } from '../utils/parseWeboutResponse'
|
||||||
|
|
||||||
export interface WaitingRequstPromise {
|
export interface WaitingRequstPromise {
|
||||||
promise: Promise<any> | null
|
promise: Promise<any> | null
|
||||||
@@ -39,15 +40,18 @@ export class WebJobExecutor extends BaseJobExecutor {
|
|||||||
? config.appLoc.replace(/\/?$/, '/') + sasJob.replace(/^\//, '')
|
? config.appLoc.replace(/\/?$/, '/') + sasJob.replace(/^\//, '')
|
||||||
: sasJob
|
: sasJob
|
||||||
: sasJob
|
: sasJob
|
||||||
const jobUri =
|
let apiUrl = `${config.serverUrl}${this.jobsPath}/?${'_program=' + program}`
|
||||||
config.serverType === ServerType.SasViya
|
|
||||||
? await this.getJobUri(sasJob)
|
if (config.serverType === ServerType.SasViya) {
|
||||||
: ''
|
const jobUri =
|
||||||
const apiUrl = `${config.serverUrl}${this.jobsPath}/?${
|
config.serverType === ServerType.SasViya
|
||||||
jobUri.length > 0
|
? await this.getJobUri(sasJob)
|
||||||
? '__program=' + program + '&_job=' + jobUri
|
: ''
|
||||||
: '_program=' + program
|
|
||||||
}`
|
apiUrl += jobUri.length > 0 ? '&_job=' + jobUri : ''
|
||||||
|
|
||||||
|
apiUrl += config.contextName ? `&_contextname=${config.contextName}` : ''
|
||||||
|
}
|
||||||
|
|
||||||
let requestParams = {
|
let requestParams = {
|
||||||
...this.getRequestParams(config)
|
...this.getRequestParams(config)
|
||||||
@@ -97,6 +101,19 @@ export class WebJobExecutor extends BaseJobExecutor {
|
|||||||
this.appendRequest(res, sasJob, config.debug)
|
this.appendRequest(res, sasJob, config.debug)
|
||||||
resolve(jsonResponse)
|
resolve(jsonResponse)
|
||||||
}
|
}
|
||||||
|
if (this.serverType === ServerType.Sas9 && config.debug) {
|
||||||
|
const jsonResponse = parseWeboutResponse(res.result as string)
|
||||||
|
if (jsonResponse === '') {
|
||||||
|
throw new Error(
|
||||||
|
'Valid JSON could not be extracted from response.'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
isValidJson(jsonResponse)
|
||||||
|
this.appendRequest(res, sasJob, config.debug)
|
||||||
|
resolve(res.result)
|
||||||
|
}
|
||||||
|
isValidJson(res.result as string)
|
||||||
this.appendRequest(res, sasJob, config.debug)
|
this.appendRequest(res, sasJob, config.debug)
|
||||||
resolve(res.result)
|
resolve(res.result)
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
export * from './ComputeJobExecutor'
|
export * from './ComputeJobExecutor'
|
||||||
export * from './JesJobExecutor'
|
export * from './JesJobExecutor'
|
||||||
export * from './JobExecutor'
|
export * from './JobExecutor'
|
||||||
|
export * from './Sas9JobExecutor'
|
||||||
export * from './WebJobExecutor'
|
export * from './WebJobExecutor'
|
||||||
|
|||||||
@@ -10,6 +10,8 @@ import {
|
|||||||
} from '../types/errors'
|
} from '../types/errors'
|
||||||
import { parseWeboutResponse } from '../utils/parseWeboutResponse'
|
import { parseWeboutResponse } from '../utils/parseWeboutResponse'
|
||||||
import { prefixMessage } from '@sasjs/utils/error'
|
import { prefixMessage } from '@sasjs/utils/error'
|
||||||
|
import { SAS9AuthError } from '../types/errors/SAS9AuthError'
|
||||||
|
import { isValidJson } from '../utils'
|
||||||
|
|
||||||
export interface HttpClient {
|
export interface HttpClient {
|
||||||
get<T>(
|
get<T>(
|
||||||
@@ -44,11 +46,11 @@ export interface HttpClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export class RequestClient implements HttpClient {
|
export class RequestClient implements HttpClient {
|
||||||
private csrfToken: CsrfToken = { headerName: '', value: '' }
|
protected csrfToken: CsrfToken = { headerName: '', value: '' }
|
||||||
private fileUploadCsrfToken: CsrfToken | undefined
|
protected fileUploadCsrfToken: CsrfToken | undefined
|
||||||
private httpClient: AxiosInstance
|
protected httpClient: AxiosInstance
|
||||||
|
|
||||||
constructor(private baseUrl: string, allowInsecure = false) {
|
constructor(protected baseUrl: string, allowInsecure = false) {
|
||||||
const https = require('https')
|
const https = require('https')
|
||||||
if (allowInsecure && https.Agent) {
|
if (allowInsecure && https.Agent) {
|
||||||
this.httpClient = axios.create({
|
this.httpClient = axios.create({
|
||||||
@@ -62,6 +64,9 @@ export class RequestClient implements HttpClient {
|
|||||||
baseURL: baseUrl
|
baseURL: baseUrl
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.httpClient.defaults.validateStatus = (status) =>
|
||||||
|
status >= 200 && status < 305
|
||||||
}
|
}
|
||||||
|
|
||||||
public getCsrfToken(type: 'general' | 'file' = 'general') {
|
public getCsrfToken(type: 'general' | 'file' = 'general') {
|
||||||
@@ -79,7 +84,7 @@ export class RequestClient implements HttpClient {
|
|||||||
contentType: string = 'application/json',
|
contentType: string = 'application/json',
|
||||||
overrideHeaders: { [key: string]: string | number } = {},
|
overrideHeaders: { [key: string]: string | number } = {},
|
||||||
debug: boolean = false
|
debug: boolean = false
|
||||||
): Promise<{ result: T; etag: string }> {
|
): Promise<{ result: T; etag: string; status: number }> {
|
||||||
const headers = {
|
const headers = {
|
||||||
...this.getHeaders(accessToken, contentType),
|
...this.getHeaders(accessToken, contentType),
|
||||||
...overrideHeaders
|
...overrideHeaders
|
||||||
@@ -286,11 +291,12 @@ export class RequestClient implements HttpClient {
|
|||||||
})
|
})
|
||||||
.then((res) => res.data)
|
.then((res) => res.data)
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
console.log(error)
|
const logger = process.logger || console
|
||||||
|
logger.error(error)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
private getHeaders = (
|
protected getHeaders = (
|
||||||
accessToken: string | undefined,
|
accessToken: string | undefined,
|
||||||
contentType: string
|
contentType: string
|
||||||
) => {
|
) => {
|
||||||
@@ -315,7 +321,7 @@ export class RequestClient implements HttpClient {
|
|||||||
return headers
|
return headers
|
||||||
}
|
}
|
||||||
|
|
||||||
private parseAndSetFileUploadCsrfToken = (response: AxiosResponse) => {
|
protected parseAndSetFileUploadCsrfToken = (response: AxiosResponse) => {
|
||||||
const token = this.parseCsrfToken(response)
|
const token = this.parseCsrfToken(response)
|
||||||
|
|
||||||
if (token) {
|
if (token) {
|
||||||
@@ -323,7 +329,7 @@ export class RequestClient implements HttpClient {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private parseAndSetCsrfToken = (response: AxiosResponse) => {
|
protected parseAndSetCsrfToken = (response: AxiosResponse) => {
|
||||||
const token = this.parseCsrfToken(response)
|
const token = this.parseCsrfToken(response)
|
||||||
|
|
||||||
if (token) {
|
if (token) {
|
||||||
@@ -347,7 +353,7 @@ export class RequestClient implements HttpClient {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private handleError = async (
|
protected handleError = async (
|
||||||
e: any,
|
e: any,
|
||||||
callback: any,
|
callback: any,
|
||||||
debug: boolean = false
|
debug: boolean = false
|
||||||
@@ -405,7 +411,7 @@ export class RequestClient implements HttpClient {
|
|||||||
throw e
|
throw e
|
||||||
}
|
}
|
||||||
|
|
||||||
private parseResponse<T>(response: AxiosResponse<any>) {
|
protected parseResponse<T>(response: AxiosResponse<any>) {
|
||||||
const etag = response?.headers ? response.headers['etag'] : ''
|
const etag = response?.headers ? response.headers['etag'] : ''
|
||||||
let parsedResponse
|
let parsedResponse
|
||||||
let includeSAS9Log: boolean = false
|
let includeSAS9Log: boolean = false
|
||||||
@@ -418,7 +424,13 @@ export class RequestClient implements HttpClient {
|
|||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
try {
|
try {
|
||||||
parsedResponse = JSON.parse(parseWeboutResponse(response.data))
|
const weboutResponse = parseWeboutResponse(response.data)
|
||||||
|
if (weboutResponse === '') {
|
||||||
|
throw new Error('Valid JSON could not be extracted from response.')
|
||||||
|
}
|
||||||
|
|
||||||
|
const jsonResponse = isValidJson(weboutResponse)
|
||||||
|
parsedResponse = jsonResponse
|
||||||
} catch {
|
} catch {
|
||||||
parsedResponse = response.data
|
parsedResponse = response.data
|
||||||
}
|
}
|
||||||
@@ -426,9 +438,15 @@ export class RequestClient implements HttpClient {
|
|||||||
includeSAS9Log = true
|
includeSAS9Log = true
|
||||||
}
|
}
|
||||||
|
|
||||||
let responseToReturn: { result: T; etag: any; log?: string } = {
|
let responseToReturn: {
|
||||||
|
result: T
|
||||||
|
etag: any
|
||||||
|
log?: string
|
||||||
|
status: number
|
||||||
|
} = {
|
||||||
result: parsedResponse as T,
|
result: parsedResponse as T,
|
||||||
etag
|
etag,
|
||||||
|
status: response.status
|
||||||
}
|
}
|
||||||
|
|
||||||
if (includeSAS9Log) {
|
if (includeSAS9Log) {
|
||||||
@@ -439,7 +457,7 @@ export class RequestClient implements HttpClient {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const throwIfError = (response: AxiosResponse) => {
|
export const throwIfError = (response: AxiosResponse) => {
|
||||||
if (response.status === 401) {
|
if (response.status === 401) {
|
||||||
throw new LoginRequiredError()
|
throw new LoginRequiredError()
|
||||||
}
|
}
|
||||||
@@ -470,6 +488,10 @@ const throwIfError = (response: AxiosResponse) => {
|
|||||||
throw new AuthorizeError(response.data.message, authorizeRequestUrl)
|
throw new AuthorizeError(response.data.message, authorizeRequestUrl)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (response.config?.url?.includes('sasAuthError')) {
|
||||||
|
throw new SAS9AuthError()
|
||||||
|
}
|
||||||
|
|
||||||
const error = parseError(response.data as string)
|
const error = parseError(response.data as string)
|
||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
|
|||||||
121
src/request/Sas9RequestClient.ts
Normal file
121
src/request/Sas9RequestClient.ts
Normal file
@@ -0,0 +1,121 @@
|
|||||||
|
import { AxiosRequestConfig } from 'axios'
|
||||||
|
import axiosCookieJarSupport from 'axios-cookiejar-support'
|
||||||
|
import * as tough from 'tough-cookie'
|
||||||
|
import { prefixMessage } from '@sasjs/utils/error'
|
||||||
|
import { RequestClient, throwIfError } from './RequestClient'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Specific request client for SAS9 in Node.js environments.
|
||||||
|
* Handles redirects and cookie management.
|
||||||
|
*/
|
||||||
|
export class Sas9RequestClient extends RequestClient {
|
||||||
|
constructor(baseUrl: string, allowInsecure = false) {
|
||||||
|
super(baseUrl, allowInsecure)
|
||||||
|
this.httpClient.defaults.maxRedirects = 0
|
||||||
|
this.httpClient.defaults.validateStatus = (status) =>
|
||||||
|
status >= 200 && status < 303
|
||||||
|
|
||||||
|
if (axiosCookieJarSupport) {
|
||||||
|
axiosCookieJarSupport(this.httpClient)
|
||||||
|
this.httpClient.defaults.jar = new tough.CookieJar()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async login(username: string, password: string, jobsPath: string) {
|
||||||
|
const codeInjectorPath = `/User Folders/${username}/My Folder/sasjs/runner`
|
||||||
|
if (this.httpClient.defaults.jar) {
|
||||||
|
;(this.httpClient.defaults.jar as tough.CookieJar).removeAllCookies()
|
||||||
|
await this.get(
|
||||||
|
`${jobsPath}?_program=${codeInjectorPath}&_username=${username}&_password=${password}`,
|
||||||
|
undefined,
|
||||||
|
'text/plain'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async get<T>(
|
||||||
|
url: string,
|
||||||
|
accessToken: string | undefined,
|
||||||
|
contentType: string = 'application/json',
|
||||||
|
overrideHeaders: { [key: string]: string | number } = {},
|
||||||
|
debug: boolean = false
|
||||||
|
): Promise<{ result: T; etag: string; status: number }> {
|
||||||
|
const headers = {
|
||||||
|
...this.getHeaders(accessToken, contentType),
|
||||||
|
...overrideHeaders
|
||||||
|
}
|
||||||
|
|
||||||
|
const requestConfig: AxiosRequestConfig = {
|
||||||
|
headers,
|
||||||
|
responseType: contentType === 'text/plain' ? 'text' : 'json',
|
||||||
|
withCredentials: true
|
||||||
|
}
|
||||||
|
if (contentType === 'text/plain') {
|
||||||
|
requestConfig.transformResponse = undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.httpClient
|
||||||
|
.get<T>(url, requestConfig)
|
||||||
|
.then((response) => {
|
||||||
|
if (response.status === 302) {
|
||||||
|
return this.get(
|
||||||
|
response.headers['location'],
|
||||||
|
accessToken,
|
||||||
|
contentType
|
||||||
|
)
|
||||||
|
}
|
||||||
|
throwIfError(response)
|
||||||
|
return this.parseResponse<T>(response)
|
||||||
|
})
|
||||||
|
.catch(async (e) => {
|
||||||
|
return await this.handleError(
|
||||||
|
e,
|
||||||
|
() =>
|
||||||
|
this.get<T>(url, accessToken, contentType, overrideHeaders).catch(
|
||||||
|
(err) => {
|
||||||
|
throw prefixMessage(
|
||||||
|
err,
|
||||||
|
'Error while executing handle error callback. '
|
||||||
|
)
|
||||||
|
}
|
||||||
|
),
|
||||||
|
debug
|
||||||
|
).catch((err) => {
|
||||||
|
throw prefixMessage(err, 'Error while handling error. ')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
public post<T>(
|
||||||
|
url: string,
|
||||||
|
data: any,
|
||||||
|
accessToken: string | undefined,
|
||||||
|
contentType = 'application/json',
|
||||||
|
overrideHeaders: { [key: string]: string | number } = {}
|
||||||
|
): Promise<{ result: T; etag: string }> {
|
||||||
|
const headers = {
|
||||||
|
...this.getHeaders(accessToken, contentType),
|
||||||
|
...overrideHeaders
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.httpClient
|
||||||
|
.post<T>(url, data, { headers, withCredentials: true })
|
||||||
|
.then(async (response) => {
|
||||||
|
if (response.status === 302) {
|
||||||
|
return await this.get(
|
||||||
|
response.headers['location'],
|
||||||
|
undefined,
|
||||||
|
contentType,
|
||||||
|
overrideHeaders
|
||||||
|
)
|
||||||
|
}
|
||||||
|
throwIfError(response)
|
||||||
|
return this.parseResponse<T>(response)
|
||||||
|
})
|
||||||
|
.catch(async (e) => {
|
||||||
|
return await this.handleError(e, () =>
|
||||||
|
this.post<T>(url, data, accessToken, contentType, overrideHeaders)
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,9 @@
|
|||||||
|
/**
|
||||||
|
* @jest-environment jsdom
|
||||||
|
*/
|
||||||
|
|
||||||
import { FileUploader } from '../FileUploader'
|
import { FileUploader } from '../FileUploader'
|
||||||
import { UploadFile } from '../types'
|
import { SASjsConfig, UploadFile } from '../types'
|
||||||
import { RequestClient } from '../request/RequestClient'
|
import { RequestClient } from '../request/RequestClient'
|
||||||
import axios from 'axios'
|
import axios from 'axios'
|
||||||
jest.mock('axios')
|
jest.mock('axios')
|
||||||
@@ -28,48 +32,51 @@ const prepareFilesAndParams = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
describe('FileUploader', () => {
|
describe('FileUploader', () => {
|
||||||
|
const config: SASjsConfig = {
|
||||||
|
...new SASjsConfig(),
|
||||||
|
appLoc: '/sample/apploc'
|
||||||
|
}
|
||||||
|
|
||||||
const fileUploader = new FileUploader(
|
const fileUploader = new FileUploader(
|
||||||
'/sample/apploc',
|
config,
|
||||||
'https://sample.server.com',
|
|
||||||
'/jobs/path',
|
'/jobs/path',
|
||||||
new RequestClient('https://sample.server.com')
|
new RequestClient('https://sample.server.com')
|
||||||
)
|
)
|
||||||
|
|
||||||
it('should upload successfully', async (done) => {
|
it('should upload successfully', async () => {
|
||||||
const sasJob = 'test/upload'
|
const sasJob = 'test/upload'
|
||||||
const { files, params } = prepareFilesAndParams()
|
const { files, params } = prepareFilesAndParams()
|
||||||
mockedAxios.post.mockImplementation(() =>
|
mockedAxios.post.mockImplementation(() =>
|
||||||
Promise.resolve({ data: sampleResponse })
|
Promise.resolve({ data: sampleResponse })
|
||||||
)
|
)
|
||||||
|
|
||||||
fileUploader.uploadFile(sasJob, files, params).then((res: any) => {
|
const res = await fileUploader.uploadFile(sasJob, files, params)
|
||||||
expect(res).toEqual(JSON.parse(sampleResponse))
|
|
||||||
done()
|
expect(res).toEqual(JSON.parse(sampleResponse))
|
||||||
})
|
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should an error when no files are provided', async (done) => {
|
it('should an error when no files are provided', async () => {
|
||||||
const sasJob = 'test/upload'
|
const sasJob = 'test/upload'
|
||||||
const files: UploadFile[] = []
|
const files: UploadFile[] = []
|
||||||
const params = { table: 'libtable' }
|
const params = { table: 'libtable' }
|
||||||
|
|
||||||
fileUploader.uploadFile(sasJob, files, params).catch((err: any) => {
|
const err = await fileUploader
|
||||||
expect(err.error.message).toEqual('At least one file must be provided.')
|
.uploadFile(sasJob, files, params)
|
||||||
done()
|
.catch((err: any) => err)
|
||||||
})
|
expect(err.error.message).toEqual('At least one file must be provided.')
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should throw an error when no sasJob is provided', async (done) => {
|
it('should throw an error when no sasJob is provided', async () => {
|
||||||
const sasJob = ''
|
const sasJob = ''
|
||||||
const { files, params } = prepareFilesAndParams()
|
const { files, params } = prepareFilesAndParams()
|
||||||
|
|
||||||
fileUploader.uploadFile(sasJob, files, params).catch((err: any) => {
|
const err = await fileUploader
|
||||||
expect(err.error.message).toEqual('sasJob must be provided.')
|
.uploadFile(sasJob, files, params)
|
||||||
done()
|
.catch((err: any) => err)
|
||||||
})
|
expect(err.error.message).toEqual('sasJob must be provided.')
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should throw an error when login is required', async (done) => {
|
it('should throw an error when login is required', async () => {
|
||||||
mockedAxios.post.mockImplementation(() =>
|
mockedAxios.post.mockImplementation(() =>
|
||||||
Promise.resolve({ data: '<form action="Logon">' })
|
Promise.resolve({ data: '<form action="Logon">' })
|
||||||
)
|
)
|
||||||
@@ -77,15 +84,13 @@ describe('FileUploader', () => {
|
|||||||
const sasJob = 'test'
|
const sasJob = 'test'
|
||||||
const { files, params } = prepareFilesAndParams()
|
const { files, params } = prepareFilesAndParams()
|
||||||
|
|
||||||
fileUploader.uploadFile(sasJob, files, params).catch((err: any) => {
|
const err = await fileUploader
|
||||||
expect(err.error.message).toEqual(
|
.uploadFile(sasJob, files, params)
|
||||||
'You must be logged in to upload a file.'
|
.catch((err: any) => err)
|
||||||
)
|
expect(err.error.message).toEqual('You must be logged in to upload a file.')
|
||||||
done()
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should throw an error when invalid JSON is returned by the server', async (done) => {
|
it('should throw an error when invalid JSON is returned by the server', async () => {
|
||||||
mockedAxios.post.mockImplementation(() =>
|
mockedAxios.post.mockImplementation(() =>
|
||||||
Promise.resolve({ data: '{invalid: "json"' })
|
Promise.resolve({ data: '{invalid: "json"' })
|
||||||
)
|
)
|
||||||
@@ -93,13 +98,13 @@ describe('FileUploader', () => {
|
|||||||
const sasJob = 'test'
|
const sasJob = 'test'
|
||||||
const { files, params } = prepareFilesAndParams()
|
const { files, params } = prepareFilesAndParams()
|
||||||
|
|
||||||
fileUploader.uploadFile(sasJob, files, params).catch((err: any) => {
|
const err = await fileUploader
|
||||||
expect(err.error.message).toEqual('File upload request failed.')
|
.uploadFile(sasJob, files, params)
|
||||||
done()
|
.catch((err: any) => err)
|
||||||
})
|
expect(err.error.message).toEqual('File upload request failed.')
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should throw an error when the server request fails', async (done) => {
|
it('should throw an error when the server request fails', async () => {
|
||||||
mockedAxios.post.mockImplementation(() =>
|
mockedAxios.post.mockImplementation(() =>
|
||||||
Promise.reject({ data: '{message: "Server error"}' })
|
Promise.reject({ data: '{message: "Server error"}' })
|
||||||
)
|
)
|
||||||
@@ -107,10 +112,9 @@ describe('FileUploader', () => {
|
|||||||
const sasJob = 'test'
|
const sasJob = 'test'
|
||||||
const { files, params } = prepareFilesAndParams()
|
const { files, params } = prepareFilesAndParams()
|
||||||
|
|
||||||
fileUploader.uploadFile(sasJob, files, params).catch((err: any) => {
|
const err = await fileUploader
|
||||||
expect(err.error.message).toEqual('File upload request failed.')
|
.uploadFile(sasJob, files, params)
|
||||||
|
.catch((err: any) => err)
|
||||||
done()
|
expect(err.error.message).toEqual('File upload request failed.')
|
||||||
})
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ describe('FolderOperations', () => {
|
|||||||
|
|
||||||
beforeEach(() => {})
|
beforeEach(() => {})
|
||||||
|
|
||||||
it('should move and rename folder', async (done) => {
|
it('should move and rename folder', async () => {
|
||||||
mockFetchResponse(false)
|
mockFetchResponse(false)
|
||||||
|
|
||||||
let res: any = await sasViyaApiClient.moveFolder(
|
let res: any = await sasViyaApiClient.moveFolder(
|
||||||
@@ -26,11 +26,9 @@ describe('FolderOperations', () => {
|
|||||||
|
|
||||||
expect(res.folder.name).toEqual('newName')
|
expect(res.folder.name).toEqual('newName')
|
||||||
expect(res.folder.parentFolderUri.split('=')[1]).toEqual('/Test/toFolder')
|
expect(res.folder.parentFolderUri.split('=')[1]).toEqual('/Test/toFolder')
|
||||||
|
|
||||||
done()
|
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should move and keep the name of folder', async (done) => {
|
it('should move and keep the name of folder', async () => {
|
||||||
mockFetchResponse(true)
|
mockFetchResponse(true)
|
||||||
|
|
||||||
let res: any = await sasViyaApiClient.moveFolder(
|
let res: any = await sasViyaApiClient.moveFolder(
|
||||||
@@ -42,11 +40,9 @@ describe('FolderOperations', () => {
|
|||||||
|
|
||||||
expect(res.folder.name).toEqual('oldName')
|
expect(res.folder.name).toEqual('oldName')
|
||||||
expect(res.folder.parentFolderUri.split('=')[1]).toEqual('/Test/toFolder')
|
expect(res.folder.parentFolderUri.split('=')[1]).toEqual('/Test/toFolder')
|
||||||
|
|
||||||
done()
|
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should only rename folder', async (done) => {
|
it('should only rename folder', async () => {
|
||||||
mockFetchResponse(false)
|
mockFetchResponse(false)
|
||||||
|
|
||||||
let res: any = await sasViyaApiClient.moveFolder(
|
let res: any = await sasViyaApiClient.moveFolder(
|
||||||
@@ -58,8 +54,6 @@ describe('FolderOperations', () => {
|
|||||||
|
|
||||||
expect(res.folder.name).toEqual('newName')
|
expect(res.folder.name).toEqual('newName')
|
||||||
expect(res.folder.parentFolderUri.split('=')[1]).toEqual('/Test/toFolder')
|
expect(res.folder.parentFolderUri.split('=')[1]).toEqual('/Test/toFolder')
|
||||||
|
|
||||||
done()
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
import { SessionManager } from '../SessionManager'
|
import { SessionManager } from '../SessionManager'
|
||||||
import * as dotenv from 'dotenv'
|
|
||||||
import { RequestClient } from '../request/RequestClient'
|
import { RequestClient } from '../request/RequestClient'
|
||||||
|
import { NoSessionStateError } from '../types/errors'
|
||||||
|
import * as dotenv from 'dotenv'
|
||||||
import axios from 'axios'
|
import axios from 'axios'
|
||||||
|
|
||||||
jest.mock('axios')
|
jest.mock('axios')
|
||||||
const mockedAxios = axios as jest.Mocked<typeof axios>
|
const mockedAxios = axios as jest.Mocked<typeof axios>
|
||||||
|
|
||||||
@@ -43,4 +45,38 @@ describe('SessionManager', () => {
|
|||||||
).resolves.toEqual(expectedResponse)
|
).resolves.toEqual(expectedResponse)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe('waitForSession', () => {
|
||||||
|
it('should reject with NoSessionStateError if SAS server did not provide session state', async () => {
|
||||||
|
const responseStatus = 304
|
||||||
|
|
||||||
|
mockedAxios.get.mockImplementation(() =>
|
||||||
|
Promise.resolve({ data: '', status: responseStatus })
|
||||||
|
)
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
sessionManager['waitForSession'](
|
||||||
|
{
|
||||||
|
id: 'id',
|
||||||
|
state: '',
|
||||||
|
links: [
|
||||||
|
{ rel: 'state', href: '', uri: '', type: '', method: 'GET' }
|
||||||
|
],
|
||||||
|
attributes: {
|
||||||
|
sessionInactiveTimeout: 0
|
||||||
|
},
|
||||||
|
creationTimeStamp: ''
|
||||||
|
},
|
||||||
|
null,
|
||||||
|
'access_token'
|
||||||
|
)
|
||||||
|
).rejects.toEqual(
|
||||||
|
new NoSessionStateError(
|
||||||
|
responseStatus,
|
||||||
|
process.env.SERVER_URL as string,
|
||||||
|
'logUrl'
|
||||||
|
)
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
31
src/test/utils/isValidJson.spec.ts
Normal file
31
src/test/utils/isValidJson.spec.ts
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
import { isValidJson } from '../../utils'
|
||||||
|
|
||||||
|
describe('jsonValidator', () => {
|
||||||
|
it('should not throw an error with an valid json', () => {
|
||||||
|
const json = {
|
||||||
|
test: 'test'
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(isValidJson(json)).toBe(json)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should not throw an error with an valid json string', () => {
|
||||||
|
const json = {
|
||||||
|
test: 'test'
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(isValidJson(JSON.stringify(json))).toStrictEqual(json)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should throw an error with an invalid json', () => {
|
||||||
|
const json = `{\"test\":\"test\"\"test2\":\"test\"}`
|
||||||
|
|
||||||
|
expect(() => {
|
||||||
|
try {
|
||||||
|
isValidJson(json)
|
||||||
|
} catch (err) {
|
||||||
|
throw new Error()
|
||||||
|
}
|
||||||
|
}).toThrowError
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import { parseGeneratedCode } from '../../utils/index'
|
import { parseGeneratedCode } from '../../utils/index'
|
||||||
|
|
||||||
it('should parse generated code', async (done) => {
|
it('should parse generated code', () => {
|
||||||
expect(sampleResponse).toBeTruthy()
|
expect(sampleResponse).toBeTruthy()
|
||||||
|
|
||||||
const parsedGeneratedCode = parseGeneratedCode(sampleResponse)
|
const parsedGeneratedCode = parseGeneratedCode(sampleResponse)
|
||||||
@@ -15,8 +15,6 @@ it('should parse generated code', async (done) => {
|
|||||||
expect(generatedCodeLines[2].startsWith('MPRINT(MM_WEBOUT)')).toBeTruthy()
|
expect(generatedCodeLines[2].startsWith('MPRINT(MM_WEBOUT)')).toBeTruthy()
|
||||||
expect(generatedCodeLines[3].startsWith('MPRINT(MM_WEBRIGHT)')).toBeTruthy()
|
expect(generatedCodeLines[3].startsWith('MPRINT(MM_WEBRIGHT)')).toBeTruthy()
|
||||||
expect(generatedCodeLines[4].startsWith('MPRINT(MM_WEBOUT)')).toBeTruthy()
|
expect(generatedCodeLines[4].startsWith('MPRINT(MM_WEBOUT)')).toBeTruthy()
|
||||||
|
|
||||||
done()
|
|
||||||
})
|
})
|
||||||
|
|
||||||
/* tslint:disable */
|
/* tslint:disable */
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { parseSourceCode } from '../../utils/index'
|
import { parseSourceCode } from '../../utils/index'
|
||||||
|
|
||||||
it('should parse SAS9 source code', async (done) => {
|
it('should parse SAS9 source code', async () => {
|
||||||
expect(sampleResponse).toBeTruthy()
|
expect(sampleResponse).toBeTruthy()
|
||||||
|
|
||||||
const parsedSourceCode = parseSourceCode(sampleResponse)
|
const parsedSourceCode = parseSourceCode(sampleResponse)
|
||||||
@@ -15,8 +15,6 @@ it('should parse SAS9 source code', async (done) => {
|
|||||||
expect(sourceCodeLines[2].startsWith('8')).toBeTruthy()
|
expect(sourceCodeLines[2].startsWith('8')).toBeTruthy()
|
||||||
expect(sourceCodeLines[3].startsWith('9')).toBeTruthy()
|
expect(sourceCodeLines[3].startsWith('9')).toBeTruthy()
|
||||||
expect(sourceCodeLines[4].startsWith('10')).toBeTruthy()
|
expect(sourceCodeLines[4].startsWith('10')).toBeTruthy()
|
||||||
|
|
||||||
done()
|
|
||||||
})
|
})
|
||||||
|
|
||||||
/* tslint:disable */
|
/* tslint:disable */
|
||||||
|
|||||||
8
src/types/File.ts
Normal file
8
src/types/File.ts
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
import { Link } from './Link'
|
||||||
|
|
||||||
|
export interface File {
|
||||||
|
id: string
|
||||||
|
name: string
|
||||||
|
parentUri: string
|
||||||
|
links: Link[]
|
||||||
|
}
|
||||||
5
src/types/Process.d.ts
vendored
Normal file
5
src/types/Process.d.ts
vendored
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
declare namespace NodeJS {
|
||||||
|
export interface Process {
|
||||||
|
logger?: import('@sasjs/utils/logger').Logger
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -40,23 +40,19 @@ export class SASjsConfig {
|
|||||||
*/
|
*/
|
||||||
debug: boolean = true
|
debug: boolean = true
|
||||||
/**
|
/**
|
||||||
* The name of the compute context to use when calling the Viya APIs directly.
|
* The name of the compute context to use when calling the Viya services directly.
|
||||||
* Example value: 'SAS Job Execution compute context'
|
* Example value: 'SAS Job Execution compute context'
|
||||||
* If set to missing or empty, and useComputeApi is true, the adapter will use
|
|
||||||
* the JES APIs. If provided, the Job Code will be executed in pooled
|
|
||||||
* compute sessions on this named context.
|
|
||||||
*/
|
*/
|
||||||
contextName: string = ''
|
contextName: string = ''
|
||||||
/**
|
/**
|
||||||
* Set to `false` to use the Job Execution Web Service. To enhance VIYA
|
* If it's `false` adapter will use the JES API as connection approach. To enhance VIYA
|
||||||
* performance, set to `true` and provide a `contextName` on which to run
|
* performance, set to `true` and provide a `contextName` on which to run
|
||||||
* the code. When running on a named context, the code executes under the
|
* 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
|
* user identity. When running as a Job Execution service, the code runs
|
||||||
* under the identity in the JES context. If no `contextName` is provided,
|
* under the identity in the JES context. If `useComputeApi` is `null` or `undefined`, the service will run as a Job, except
|
||||||
* and `useComputeApi` is `true`, then the service will run as a Job, except
|
|
||||||
* triggered using the APIs instead of the Job Execution Web Service broker.
|
* triggered using the APIs instead of the Job Execution Web Service broker.
|
||||||
*/
|
*/
|
||||||
useComputeApi = false
|
useComputeApi: boolean | null = null
|
||||||
/**
|
/**
|
||||||
* Defaults to `false`.
|
* Defaults to `false`.
|
||||||
* When set to `true`, the adapter will allow requests to SAS servers that use a self-signed SSL certificate.
|
* When set to `true`, the adapter will allow requests to SAS servers that use a self-signed SSL certificate.
|
||||||
|
|||||||
15
src/types/errors/NoSessionStateError.ts
Normal file
15
src/types/errors/NoSessionStateError.ts
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
export class NoSessionStateError extends Error {
|
||||||
|
constructor(
|
||||||
|
public serverResponseStatus: number,
|
||||||
|
public sessionStateUrl: string,
|
||||||
|
public logUrl: string
|
||||||
|
) {
|
||||||
|
super(
|
||||||
|
`Could not get session state. Server responded with ${serverResponseStatus} whilst checking state: ${sessionStateUrl}`
|
||||||
|
)
|
||||||
|
|
||||||
|
this.name = 'NoSessionStatus'
|
||||||
|
|
||||||
|
Object.setPrototypeOf(this, NoSessionStateError.prototype)
|
||||||
|
}
|
||||||
|
}
|
||||||
9
src/types/errors/SAS9AuthError.ts
Normal file
9
src/types/errors/SAS9AuthError.ts
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
export class SAS9AuthError extends Error {
|
||||||
|
constructor() {
|
||||||
|
super(
|
||||||
|
'The credentials you provided cannot be authenticated. Please provide a valid set of credentials.'
|
||||||
|
)
|
||||||
|
this.name = 'AuthorizeError'
|
||||||
|
Object.setPrototypeOf(this, SAS9AuthError.prototype)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,3 +5,4 @@ export * from './JobExecutionError'
|
|||||||
export * from './LoginRequiredError'
|
export * from './LoginRequiredError'
|
||||||
export * from './NotFoundError'
|
export * from './NotFoundError'
|
||||||
export * from './ErrorResponse'
|
export * from './ErrorResponse'
|
||||||
|
export * from './NoSessionStateError'
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
export * from './Context'
|
export * from './Context'
|
||||||
export * from './CsrfToken'
|
export * from './CsrfToken'
|
||||||
export * from './Folder'
|
export * from './Folder'
|
||||||
|
export * from './File'
|
||||||
export * from './Job'
|
export * from './Job'
|
||||||
export * from './JobDefinition'
|
export * from './JobDefinition'
|
||||||
export * from './JobResult'
|
export * from './JobResult'
|
||||||
|
|||||||
@@ -15,12 +15,14 @@ export const fetchLogByChunks = async (
|
|||||||
logUrl: string,
|
logUrl: string,
|
||||||
logCount: number
|
logCount: number
|
||||||
): Promise<string> => {
|
): Promise<string> => {
|
||||||
|
const logger = process.logger || console
|
||||||
|
|
||||||
let log: string = ''
|
let log: string = ''
|
||||||
|
|
||||||
const loglimit = logCount < 10000 ? logCount : 10000
|
const loglimit = logCount < 10000 ? logCount : 10000
|
||||||
let start = 0
|
let start = 0
|
||||||
do {
|
do {
|
||||||
console.log(
|
logger.info(
|
||||||
`Fetching logs from line no: ${start + 1} to ${
|
`Fetching logs from line no: ${start + 1} to ${
|
||||||
start + loglimit
|
start + loglimit
|
||||||
} of ${logCount}.`
|
} of ${logCount}.`
|
||||||
|
|||||||
@@ -12,3 +12,4 @@ export * from './serialize'
|
|||||||
export * from './splitChunks'
|
export * from './splitChunks'
|
||||||
export * from './parseWeboutResponse'
|
export * from './parseWeboutResponse'
|
||||||
export * from './fetchLogByChunks'
|
export * from './fetchLogByChunks'
|
||||||
|
export * from './isValidJson'
|
||||||
|
|||||||
13
src/utils/isValidJson.ts
Normal file
13
src/utils/isValidJson.ts
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
/**
|
||||||
|
* Checks if string is in valid JSON format else throw error.
|
||||||
|
* @param str - string to check.
|
||||||
|
*/
|
||||||
|
export const isValidJson = (str: string | object) => {
|
||||||
|
try {
|
||||||
|
if (typeof str === 'object') return str
|
||||||
|
|
||||||
|
return JSON.parse(str)
|
||||||
|
} catch (e) {
|
||||||
|
throw new Error('Invalid JSON response.')
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,21 +1,32 @@
|
|||||||
const path = require('path')
|
const path = require('path')
|
||||||
const webpack = require('webpack')
|
const webpack = require('webpack')
|
||||||
const terserPlugin = require('terser-webpack-plugin')
|
const terserPlugin = require('terser-webpack-plugin')
|
||||||
|
const nodePolyfillPlugin = require('node-polyfill-webpack-plugin')
|
||||||
|
|
||||||
|
const defaultPlugins = [
|
||||||
|
new webpack.ContextReplacementPlugin(/moment[\/\\]locale$/, /en/),
|
||||||
|
new webpack.SourceMapDevToolPlugin({
|
||||||
|
filename: null,
|
||||||
|
exclude: [/node_modules/],
|
||||||
|
test: /\.ts($|\?)/i
|
||||||
|
})
|
||||||
|
]
|
||||||
|
|
||||||
|
const optimization = {
|
||||||
|
minimize: true,
|
||||||
|
minimizer: [
|
||||||
|
new terserPlugin({
|
||||||
|
parallel: true,
|
||||||
|
terserOptions: {}
|
||||||
|
})
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
const browserConfig = {
|
const browserConfig = {
|
||||||
entry: './src/index.ts',
|
entry: './src/index.ts',
|
||||||
devtool: 'inline-source-map',
|
devtool: 'inline-source-map',
|
||||||
mode: 'production',
|
mode: 'production',
|
||||||
optimization: {
|
optimization: optimization,
|
||||||
minimizer: [
|
|
||||||
new terserPlugin({
|
|
||||||
cache: true,
|
|
||||||
parallel: true,
|
|
||||||
sourceMap: true,
|
|
||||||
terserOptions: {}
|
|
||||||
})
|
|
||||||
]
|
|
||||||
},
|
|
||||||
module: {
|
module: {
|
||||||
rules: [
|
rules: [
|
||||||
{
|
{
|
||||||
@@ -27,7 +38,7 @@ const browserConfig = {
|
|||||||
},
|
},
|
||||||
resolve: {
|
resolve: {
|
||||||
extensions: ['.ts', '.js'],
|
extensions: ['.ts', '.js'],
|
||||||
fallback: { https: false }
|
fallback: { https: false, fs: false, readline: false }
|
||||||
},
|
},
|
||||||
output: {
|
output: {
|
||||||
filename: 'index.js',
|
filename: 'index.js',
|
||||||
@@ -36,17 +47,27 @@ const browserConfig = {
|
|||||||
library: 'SASjs'
|
library: 'SASjs'
|
||||||
},
|
},
|
||||||
plugins: [
|
plugins: [
|
||||||
new webpack.ContextReplacementPlugin(/moment[\/\\]locale$/, /en/),
|
...defaultPlugins,
|
||||||
new webpack.SourceMapDevToolPlugin({
|
new webpack.ProvidePlugin({
|
||||||
filename: null,
|
process: 'process/browser'
|
||||||
exclude: [/node_modules/],
|
}),
|
||||||
test: /\.ts($|\?)/i
|
new nodePolyfillPlugin()
|
||||||
})
|
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const browserConfigWithoutProcessPlugin = {
|
||||||
|
entry: browserConfig.entry,
|
||||||
|
devtool: browserConfig.devtool,
|
||||||
|
mode: browserConfig.mode,
|
||||||
|
optimization: optimization,
|
||||||
|
module: browserConfig.module,
|
||||||
|
resolve: browserConfig.resolve,
|
||||||
|
output: browserConfig.output,
|
||||||
|
plugins: defaultPlugins
|
||||||
|
}
|
||||||
|
|
||||||
const nodeConfig = {
|
const nodeConfig = {
|
||||||
...browserConfig,
|
...browserConfigWithoutProcessPlugin,
|
||||||
target: 'node',
|
target: 'node',
|
||||||
entry: './node/index.ts',
|
entry: './node/index.ts',
|
||||||
output: {
|
output: {
|
||||||
|
|||||||
Reference in New Issue
Block a user