mirror of
https://github.com/sasjs/adapter.git
synced 2025-12-12 09:44:36 +00:00
Compare commits
124 Commits
v2.2.5
...
callback-v
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
591e1ebc09 | ||
|
|
bd19457c2a | ||
|
|
b0570e1cd9 | ||
|
|
a5f1b59f7b | ||
|
|
01ca29fc01 | ||
|
|
ed9648fdf9 | ||
|
|
7e17aa6eb3 | ||
|
|
9caee9941a | ||
|
|
e309e7a4f4 | ||
|
|
c47441d6d4 | ||
|
|
1844bc48ac | ||
|
|
7a5adebdb5 | ||
|
|
b39f0c577b | ||
|
|
15f4065cd8 | ||
|
|
4c67665b4d | ||
|
|
76d0b82b4c | ||
|
|
95d65d270d | ||
|
|
4e5c9c1ccd | ||
|
|
3267af0724 | ||
|
|
75120424d0 | ||
|
|
f13c7e5cf1 | ||
|
|
53a7b1c9e6 | ||
|
|
8c30cbff13 | ||
|
|
8f3a7f33f8 | ||
|
|
67ec27bab7 | ||
|
|
c1b200b0d8 | ||
|
|
e03ec996d6 | ||
|
|
ad8dbfd4ec | ||
|
|
15a774ff81 | ||
|
|
98114c5591 | ||
|
|
dffcb66d54 | ||
|
|
67c7147e62 | ||
|
|
50d1b4d824 | ||
|
|
dc98ce3b0b | ||
|
|
cf1e3f3835 | ||
|
|
2f913e9363 | ||
|
|
05a9864df8 | ||
|
|
3a0d764dfa | ||
|
|
310087b895 | ||
|
|
dc39ecd4a8 | ||
|
|
99e192c5de | ||
|
|
b86658ef9b | ||
|
|
88f08e8864 | ||
|
|
80e5de5d65 | ||
|
|
665734b168 | ||
|
|
5543f467e6 | ||
|
|
a32c0879b3 | ||
|
|
bb2ad5bb9a | ||
|
|
6f2f11d112 | ||
|
|
fef65bbfd2 | ||
|
|
efeba71612 | ||
|
|
8f54002b1e | ||
|
|
9d6882799d | ||
|
|
73a3acee68 | ||
|
|
0a88220e04 | ||
|
|
c8e1779272 | ||
|
|
8bd3580e23 | ||
|
|
f732b32873 | ||
|
|
65b18f9148 | ||
|
|
10b1676a35 | ||
|
|
b9bd09d3e8 | ||
|
|
537f687b94 | ||
|
|
bfd532f813 | ||
|
|
4f2b4f46a8 | ||
|
|
077cc9458d | ||
|
|
0a7ab394a4 | ||
|
|
f873febfde | ||
|
|
55e8ce359b | ||
|
|
99d7c8f119 | ||
|
|
b3c90f09d6 | ||
|
|
2401962c53 | ||
|
|
362b4d4db3 | ||
|
|
8aea325139 | ||
|
|
bb370061a2 | ||
|
|
48442f7769 | ||
|
|
e67a8531ce | ||
|
|
ef4f020e2a | ||
|
|
2feceeb2f9 | ||
|
|
eaec922fea | ||
|
|
de94777fff | ||
|
|
0aa0ae65e0 | ||
|
|
4b0d62d59b | ||
|
|
b3ef50e9eb | ||
|
|
d30a1890a1 | ||
|
|
f1c2569de3 | ||
|
|
4826388cdd | ||
| e88736056a | |||
| 9da2a29a72 | |||
| dce8a08a86 | |||
| 1fabb9e610 | |||
| 23db0ac80d | |||
| 28370341d8 | |||
|
|
a023ffe850 | ||
|
|
a4e77ecf6e | ||
|
|
7efc0a1fb2 | ||
|
|
c3e2b2ce70 | ||
|
|
dde1228b1d | ||
|
|
b3474b6dfb | ||
|
|
179a04ae31 | ||
|
|
2bdcbda54c | ||
|
|
a123392c56 | ||
|
|
719135e366 | ||
|
|
ba619554b7 | ||
|
|
6a3ab7032f | ||
|
|
d818d14cb4 | ||
|
|
599c130395 | ||
|
|
9ef2759e27 | ||
|
|
43355c88d4 | ||
|
|
15e1acaf4f | ||
|
|
ec77ffdd88 | ||
|
|
9797c1ca84 | ||
|
|
bbe9633dc8 | ||
|
|
6f60ac5cc7 | ||
|
|
e7ba09793c | ||
|
|
c0c0800e61 | ||
|
|
0bd9d8f93f | ||
|
|
214fc2d5cd | ||
|
|
55b0e2f934 | ||
|
|
609cd4ed6d | ||
|
|
2b20bbdcc8 | ||
|
|
946a95bea1 | ||
|
|
7ec1c152e3 | ||
|
|
0cf1110018 | ||
|
|
51a09d049c |
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-z \-]+\))?!?: .+$") 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
|
||||||
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
|
||||||
|
|||||||
186
README.md
186
README.md
@@ -1,12 +1,28 @@
|
|||||||
[](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
|
||||||
|
|
||||||
2 - [Download](https://cdn.jsdelivr.net/npm/@sasjs/adapter@1/index.js) and use a copy of the latest JS file
|
2 - [Download](https://cdn.jsdelivr.net/npm/@sasjs/adapter@2/index.js) and use a copy of the latest JS file
|
||||||
|
|
||||||
3 - Reference directly from the CDN - in which case click [here](https://www.jsdelivr.com/package/npm/@sasjs/adapter?tab=collection) and select "SRI" to get the script tag with the integrity hash.
|
3 - Reference directly from the CDN - in which case click [here](https://www.jsdelivr.com/package/npm/@sasjs/adapter?tab=collection) and select "SRI" to get the script tag with the integrity hash.
|
||||||
|
|
||||||
@@ -41,8 +57,172 @@ parmcards4;
|
|||||||
|
|
||||||
You now have a simple web app with a backend service!
|
You now have a simple web app with a backend service!
|
||||||
|
|
||||||
|
## Detailed Overview
|
||||||
|
|
||||||
|
The SASjs adapter is a JS library and a set of SAS Macros that handle the communication between the frontend app and backend SAS services.
|
||||||
|
|
||||||
|
There are three parts to consider:
|
||||||
|
|
||||||
|
1. JS request / response
|
||||||
|
2. SAS inputs / outputs
|
||||||
|
3. Configuration
|
||||||
|
|
||||||
|
### JS Request / Response
|
||||||
|
|
||||||
|
To install the library you can simply run `npm i @sasjs/adapter` or include a `<script>` tag with a reference to our [CDN](https://www.jsdelivr.com/package/npm/@sasjs/adapter).
|
||||||
|
|
||||||
|
Full technical documentation is available [here](https://adapter.sasjs.io). The main parts are:
|
||||||
|
|
||||||
|
### Instantiation
|
||||||
|
The following code will instantiate an instance of the adapter:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
let sasJs = new SASjs.default(
|
||||||
|
{
|
||||||
|
appLoc: "/Your/SAS/Folder",
|
||||||
|
serverType:"SAS9"
|
||||||
|
}
|
||||||
|
);
|
||||||
|
```
|
||||||
|
If you've installed it via NPM, you can import it as a default import like so:
|
||||||
|
```
|
||||||
|
import SASjs from '@sasjs/adapter';
|
||||||
|
```
|
||||||
|
You can then instantiate it with:
|
||||||
|
```
|
||||||
|
const sasJs = new SASjs({your config})
|
||||||
|
```
|
||||||
|
|
||||||
|
More on the config later.
|
||||||
|
|
||||||
|
### SAS Logon
|
||||||
|
The login process can be handled directly, as below, or as a callback function to a SAS request.
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
sasJs.logIn('USERNAME','PASSWORD'
|
||||||
|
).then((response) => {
|
||||||
|
if (response.isLoggedIn === true) {
|
||||||
|
console.log('do stuff')
|
||||||
|
} else {
|
||||||
|
console.log('do other stuff')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Request / Response
|
||||||
|
A simple request can be sent to SAS in the following fashion:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
sasJs.request("/path/to/my/service", dataObject)
|
||||||
|
.then((response) => {
|
||||||
|
// all tables are in the response object, eg:
|
||||||
|
console.log(response.tablewith2cols1row[0].COL1.value)
|
||||||
|
})
|
||||||
|
```
|
||||||
|
We supply the path to the SAS service, and a data object. The data object can be null (for services with no input), or can contain one or more tables in the following format:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
let dataObject={
|
||||||
|
"tablewith2cols1row": [{
|
||||||
|
"col1": "val1",
|
||||||
|
"col2": 42
|
||||||
|
}],
|
||||||
|
"tablewith1col2rows": [{
|
||||||
|
"col": "row1"
|
||||||
|
}, {
|
||||||
|
"col": "row2"
|
||||||
|
}]
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
There are optional parameters such as a config object and a callback login function.
|
||||||
|
|
||||||
|
The response object will contain returned tables and columns. Table names are always lowercase, and column names uppercase.
|
||||||
|
|
||||||
|
The adapter will also cache the logs (if debug enabled) and even the work tables. For performance, it is best to keep debug mode off.
|
||||||
|
|
||||||
|
## SAS Inputs / Outputs
|
||||||
|
|
||||||
|
The SAS side is handled by a number of macros in the [macro core](https://github.com/sasjs/core) library.
|
||||||
|
|
||||||
|
The following snippet shows the process of SAS tables arriving / leaving:
|
||||||
|
```sas
|
||||||
|
/* fetch all input tables sent from frontend - they arrive as work tables */
|
||||||
|
%webout(FETCH)
|
||||||
|
|
||||||
|
/* some sas code */
|
||||||
|
data some sas tables;
|
||||||
|
set from js;
|
||||||
|
run;
|
||||||
|
|
||||||
|
%webout(OPEN) /* open the JSON to be returned */
|
||||||
|
%webout(OBJ,some) /* `some` table is sent in object format */
|
||||||
|
%webout(ARR,sas) /* `sas` table is sent in array format, smaller filesize */
|
||||||
|
%webout(OBJ,tables,fmt=N) /* unformatted (raw) data */
|
||||||
|
%webout(OBJ,tables,label=newtable) /* rename tables on export */
|
||||||
|
%webout(CLOSE) /* close the JSON and send some extra useful variables too */
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
Configuration on the client side involves passing an object on startup, which can also be passed with each request. Technical documentation on the SASjsConfig class is available [here](https://adapter.sasjs.io/classes/types.sasjsconfig.html). The main config items are:
|
||||||
|
|
||||||
|
* `appLoc` - this is the folder under which the SAS services will be created.
|
||||||
|
* `serverType` - either `SAS9` or `SASVIYA`.
|
||||||
|
* `serverUrl` - the location (including http protocol and port) of the SAS Server. Can be omitted, eg if serving directly from the SAS Web Server, or in streaming mode.
|
||||||
|
* `debug` - if `true` then SAS Logs and extra debug information is returned.
|
||||||
|
* `useComputeApi` - if `true` and the serverType is `SASVIYA` then the REST APIs will be called directly (rather than using the JES web service).
|
||||||
|
* `contextName` - if missing or blank, and `useComputeApi` is `true` and `serverType` is `SASVIYA` then the JES API will be used.
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
```
|
||||||
|
{
|
||||||
|
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",
|
||||||
|
serverType:"SASVIYA",
|
||||||
|
useComputeApi: true
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 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.
|
||||||
|
|
||||||
|
```
|
||||||
|
{
|
||||||
|
appLoc:"/Your/Path",
|
||||||
|
serverType:"SASVIYA",
|
||||||
|
useComputeApi: true,
|
||||||
|
contextName: 'yourComputeContext'
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
# 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.
|
||||||
|
|
||||||
|
|
||||||
|
## Star Gazing
|
||||||
|
|
||||||
|
If you find this library useful, help us grow our star graph!
|
||||||
|
|
||||||
|

|
||||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
286
docs/classes/types_errors.authorizeerror.html
Normal file
286
docs/classes/types_errors.authorizeerror.html
Normal file
File diff suppressed because one or more lines are too long
304
docs/classes/types_errors.computejobexecutionerror.html
Normal file
304
docs/classes/types_errors.computejobexecutionerror.html
Normal file
File diff suppressed because one or more lines are too long
209
docs/classes/types_errors.errorresponse.html
Normal file
209
docs/classes/types_errors.errorresponse.html
Normal file
File diff suppressed because one or more lines are too long
259
docs/classes/types_errors.internalservererror.html
Normal file
259
docs/classes/types_errors.internalservererror.html
Normal file
File diff suppressed because one or more lines are too long
325
docs/classes/types_errors.jobexecutionerror.html
Normal file
325
docs/classes/types_errors.jobexecutionerror.html
Normal file
File diff suppressed because one or more lines are too long
259
docs/classes/types_errors.loginrequirederror.html
Normal file
259
docs/classes/types_errors.loginrequirederror.html
Normal file
File diff suppressed because one or more lines are too long
283
docs/classes/types_errors.notfounderror.html
Normal file
283
docs/classes/types_errors.notfounderror.html
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
133
docs/index.html
133
docs/index.html
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
200
docs/interfaces/job_execution.waitingrequstpromise.html
Normal file
200
docs/interfaces/job_execution.waitingrequstpromise.html
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
209
docs/interfaces/types.logstatistics.html
Normal file
209
docs/interfaces/types.logstatistics.html
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
133
docs/modules/types_errors.html
Normal file
133
docs/modules/types_errors.html
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
21159
package-lock.json
generated
21159
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
24
package.json
24
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 && cp -r 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 && cp ./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}'",
|
"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}'",
|
"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",
|
||||||
|
"postinstall": "[ -d .git ] && git config core.hooksPath ./.git-hooks || true"
|
||||||
},
|
},
|
||||||
"publishConfig": {
|
"publishConfig": {
|
||||||
"access": "public"
|
"access": "public"
|
||||||
@@ -36,31 +38,31 @@
|
|||||||
},
|
},
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/jest": "^26.0.20",
|
"@types/jest": "^26.0.22",
|
||||||
"cp": "^0.2.0",
|
"cp": "^0.2.0",
|
||||||
"dotenv": "^8.2.0",
|
"dotenv": "^8.2.0",
|
||||||
"jest": "^26.6.3",
|
"jest": "^26.6.3",
|
||||||
"jest-extended": "^0.11.5",
|
"jest-extended": "^0.11.5",
|
||||||
"path": "^0.12.7",
|
"path": "^0.12.7",
|
||||||
"rimraf": "^3.0.2",
|
"rimraf": "^3.0.2",
|
||||||
"semantic-release": "^17.3.9",
|
"semantic-release": "^17.4.2",
|
||||||
"terser-webpack-plugin": "^4.2.3",
|
"terser-webpack-plugin": "^4.2.3",
|
||||||
"ts-jest": "^25.5.1",
|
"ts-jest": "^25.5.1",
|
||||||
"ts-loader": "^8.0.17",
|
"ts-loader": "^9.1.2",
|
||||||
"tslint": "^6.1.3",
|
"tslint": "^6.1.3",
|
||||||
"tslint-config-prettier": "^1.18.0",
|
"tslint-config-prettier": "^1.18.0",
|
||||||
"typedoc": "^0.19.2",
|
"typedoc": "^0.20.35",
|
||||||
"typedoc-neo-theme": "^1.1.0",
|
"typedoc-neo-theme": "^1.1.0",
|
||||||
"typedoc-plugin-external-module-name": "^4.0.6",
|
"typedoc-plugin-external-module-name": "^4.0.6",
|
||||||
"typescript": "^3.9.9",
|
"typescript": "^3.9.9",
|
||||||
"webpack": "^5.21.2",
|
"webpack": "^5.33.2",
|
||||||
"webpack-cli": "^4.5.0"
|
"webpack-cli": "^4.7.0"
|
||||||
},
|
},
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@sasjs/utils": "^2.5.0",
|
"@sasjs/utils": "^2.10.2",
|
||||||
"axios": "^0.21.1",
|
"axios": "^0.21.1",
|
||||||
"form-data": "^3.0.0",
|
"form-data": "^4.0.0",
|
||||||
"https": "^1.0.0"
|
"https": "^1.0.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"trailingComma": "none",
|
"trailingComma": "none",
|
||||||
"tabWidth": 2,
|
"tabWidth": 2,
|
||||||
"semi": true,
|
"semi": false,
|
||||||
"singleQuote": false
|
"singleQuote": true
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -55,6 +55,7 @@ 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 +64,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;
|
||||||
|
|||||||
97
sasjs-tests/package-lock.json
generated
97
sasjs-tests/package-lock.json
generated
@@ -2005,18 +2005,18 @@
|
|||||||
},
|
},
|
||||||
"@sasjs/adapter": {
|
"@sasjs/adapter": {
|
||||||
"version": "file:../build/sasjs-adapter-5.0.0.tgz",
|
"version": "file:../build/sasjs-adapter-5.0.0.tgz",
|
||||||
"integrity": "sha512-1t+3LIL2BFw8HpZUPI9QM24801+JH4DCAu4eHoLLmytYhN72asMi1aVtgSDb1xiJYgpbTG7EK3qRpHIV8cEN8w==",
|
"integrity": "sha512-DxoQbdJqzqOTIuT7qwSfAbmNTWdpOx5zGkiMuZBSwoi9lSsRNoARiWnJq5Vl6h4RXJlc/FVdBFt35RZm4Mc0ZQ==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"@sasjs/utils": "^2.5.0",
|
"@sasjs/utils": "^2.10.2",
|
||||||
"axios": "^0.21.1",
|
"axios": "^0.21.1",
|
||||||
"form-data": "^3.0.0",
|
"form-data": "^4.0.0",
|
||||||
"https": "^1.0.0"
|
"https": "^1.0.0"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"form-data": {
|
"form-data": {
|
||||||
"version": "3.0.1",
|
"version": "4.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/form-data/-/form-data-3.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz",
|
||||||
"integrity": "sha512-RHkBKtLWUVwd7SqRIvCZMEvAMoGUp0XU+seQiZejj0COz3RI3hWP4sCv3gZWWLjJTd7rGwcsF5eKZGii0r/hbg==",
|
"integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"asynckit": "^0.4.0",
|
"asynckit": "^0.4.0",
|
||||||
"combined-stream": "^1.0.8",
|
"combined-stream": "^1.0.8",
|
||||||
@@ -2046,14 +2046,70 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"@sasjs/utils": {
|
"@sasjs/utils": {
|
||||||
"version": "2.5.1",
|
"version": "2.12.1",
|
||||||
"resolved": "https://registry.npmjs.org/@sasjs/utils/-/utils-2.5.1.tgz",
|
"resolved": "https://registry.npmjs.org/@sasjs/utils/-/utils-2.12.1.tgz",
|
||||||
"integrity": "sha512-a3ISiUX8Yz7au4XYxq2KWf9ODT6nsIDbE4FEqS+AQ3McxZkfuAk4v+REXjOmIlcyQd4R4bufEK8XoB6AROn9sA==",
|
"integrity": "sha512-6gZS5zW0J70P7XaVuEczyfHVaVa8Ks/aWr4PIlpJcxWD0enJtCEmos2DdnezdSoNvODkPq/8rzMPqko5jaXK1Q==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"@types/prompts": "^2.0.9",
|
"@types/prompts": "^2.0.11",
|
||||||
|
"chalk": "^4.1.1",
|
||||||
|
"cli-table": "^0.3.6",
|
||||||
"consola": "^2.15.0",
|
"consola": "^2.15.0",
|
||||||
"prompts": "^2.4.0",
|
"prompts": "^2.4.1",
|
||||||
"valid-url": "^1.0.9"
|
"valid-url": "^1.0.9"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"ansi-styles": {
|
||||||
|
"version": "4.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
|
||||||
|
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
|
||||||
|
"requires": {
|
||||||
|
"color-convert": "^2.0.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"chalk": {
|
||||||
|
"version": "4.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.1.tgz",
|
||||||
|
"integrity": "sha512-diHzdDKxcU+bAsUboHLPEDQiw0qEe0qd7SYUn3HgcFlWgbDcfLGswOHYeGrHKzG9z6UYf01d9VFMfZxPM1xZSg==",
|
||||||
|
"requires": {
|
||||||
|
"ansi-styles": "^4.1.0",
|
||||||
|
"supports-color": "^7.1.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"color-convert": {
|
||||||
|
"version": "2.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
|
||||||
|
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
|
||||||
|
"requires": {
|
||||||
|
"color-name": "~1.1.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"color-name": {
|
||||||
|
"version": "1.1.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
|
||||||
|
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="
|
||||||
|
},
|
||||||
|
"has-flag": {
|
||||||
|
"version": "4.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
|
||||||
|
"integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="
|
||||||
|
},
|
||||||
|
"prompts": {
|
||||||
|
"version": "2.4.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.1.tgz",
|
||||||
|
"integrity": "sha512-EQyfIuO2hPDsX1L/blblV+H7I0knhgAd82cVneCwcdND9B8AuCDuRcBH6yIcG4dFzlOUqbazQqwGjx5xmsNLuQ==",
|
||||||
|
"requires": {
|
||||||
|
"kleur": "^3.0.3",
|
||||||
|
"sisteransi": "^1.0.5"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"supports-color": {
|
||||||
|
"version": "7.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
|
||||||
|
"integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
|
||||||
|
"requires": {
|
||||||
|
"has-flag": "^4.0.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"@semantic-ui-react/event-stack": {
|
"@semantic-ui-react/event-stack": {
|
||||||
@@ -2366,9 +2422,9 @@
|
|||||||
"integrity": "sha512-6gOkRe7OIioWAXfnO/2lFiv+SJichKVSys1mSsgyrYHSEjk8Ctv4tSR/Odvnu+HWlH2C8j53dahU03XmQdd5fA=="
|
"integrity": "sha512-6gOkRe7OIioWAXfnO/2lFiv+SJichKVSys1mSsgyrYHSEjk8Ctv4tSR/Odvnu+HWlH2C8j53dahU03XmQdd5fA=="
|
||||||
},
|
},
|
||||||
"@types/prompts": {
|
"@types/prompts": {
|
||||||
"version": "2.0.9",
|
"version": "2.0.11",
|
||||||
"resolved": "https://registry.npmjs.org/@types/prompts/-/prompts-2.0.9.tgz",
|
"resolved": "https://registry.npmjs.org/@types/prompts/-/prompts-2.0.11.tgz",
|
||||||
"integrity": "sha512-TORZP+FSjTYMWwKadftmqEn6bziN5RnfygehByGsjxoK5ydnClddtv6GikGWPvCm24oI+YBwck5WDxIIyNxUrA==",
|
"integrity": "sha512-dcF5L3rU9VfpLEJIV++FEyhGhuIpJllNEwllVuJ5g8eoVqjf048tW9+spivIwjzgPbtaGAl7mIZW3cmhDAq2UQ==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"@types/node": "*"
|
"@types/node": "*"
|
||||||
}
|
}
|
||||||
@@ -4460,6 +4516,14 @@
|
|||||||
"resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz",
|
||||||
"integrity": "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A=="
|
"integrity": "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A=="
|
||||||
},
|
},
|
||||||
|
"cli-table": {
|
||||||
|
"version": "0.3.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/cli-table/-/cli-table-0.3.6.tgz",
|
||||||
|
"integrity": "sha512-ZkNZbnZjKERTY5NwC2SeMeLeifSPq/pubeRoTpdr3WchLlnZg6hEgvHkK5zL7KNFdd9PmHN8lxrENUwI3cE8vQ==",
|
||||||
|
"requires": {
|
||||||
|
"colors": "1.0.3"
|
||||||
|
}
|
||||||
|
},
|
||||||
"cliui": {
|
"cliui": {
|
||||||
"version": "5.0.0",
|
"version": "5.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/cliui/-/cliui-5.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/cliui/-/cliui-5.0.0.tgz",
|
||||||
@@ -4568,6 +4632,11 @@
|
|||||||
"resolved": "https://registry.npmjs.org/colorette/-/colorette-1.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/colorette/-/colorette-1.2.1.tgz",
|
||||||
"integrity": "sha512-puCDz0CzydiSYOrnXpz/PKd69zRrribezjtE9yd4zvytoRc8+RY/KJPvtPFKZS3E3wP6neGyMe0vOTlHO5L3Pw=="
|
"integrity": "sha512-puCDz0CzydiSYOrnXpz/PKd69zRrribezjtE9yd4zvytoRc8+RY/KJPvtPFKZS3E3wP6neGyMe0vOTlHO5L3Pw=="
|
||||||
},
|
},
|
||||||
|
"colors": {
|
||||||
|
"version": "1.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/colors/-/colors-1.0.3.tgz",
|
||||||
|
"integrity": "sha1-BDP0TYCWgP3rYO0mDxsMJi6CpAs="
|
||||||
|
},
|
||||||
"combined-stream": {
|
"combined-stream": {
|
||||||
"version": "1.0.8",
|
"version": "1.0.8",
|
||||||
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
|
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
"homepage": ".",
|
"homepage": ".",
|
||||||
"private": true,
|
"private": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@sasjs/adapter": "^2.2.4",
|
"@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.25",
|
||||||
@@ -23,8 +23,8 @@
|
|||||||
"test": "react-scripts test",
|
"test": "react-scripts test",
|
||||||
"eject": "react-scripts eject",
|
"eject": "react-scripts eject",
|
||||||
"update:adapter": "cd .. && npm run package:lib && cd sasjs-tests && npm i ../build/sasjs-adapter-5.0.0.tgz",
|
"update:adapter": "cd .. && npm run package:lib && cd sasjs-tests && npm i ../build/sasjs-adapter-5.0.0.tgz",
|
||||||
"deploy:tests": "npm run build && rsync -avhe ssh ./build/* --delete $SSH_ACCOUNT:$DEPLOY_PATH",
|
"deploy:tests": "rsync -avhe ssh ./build/* --delete $SSH_ACCOUNT:$DEPLOY_PATH",
|
||||||
"deploy": "npm run update:adapter && npm run deploy:tests"
|
"deploy": "npm run update:adapter && npm run build && npm run deploy:tests"
|
||||||
},
|
},
|
||||||
"eslintConfig": {
|
"eslintConfig": {
|
||||||
"extends": "react-app"
|
"extends": "react-app"
|
||||||
|
|||||||
@@ -1,15 +1,15 @@
|
|||||||
import React, { ReactElement, useState, useContext, useEffect } from "react";
|
import React, { ReactElement, useState, useContext, useEffect } from 'react'
|
||||||
import { TestSuiteRunner, TestSuite, AppContext } from "@sasjs/test-framework";
|
import { TestSuiteRunner, TestSuite, AppContext } from '@sasjs/test-framework'
|
||||||
import { basicTests } from "./testSuites/Basic";
|
import { basicTests } from './testSuites/Basic'
|
||||||
import { sendArrTests, sendObjTests } from "./testSuites/RequestData";
|
import { sendArrTests, sendObjTests } from './testSuites/RequestData'
|
||||||
import { specialCaseTests } from "./testSuites/SpecialCases";
|
import { specialCaseTests } from './testSuites/SpecialCases'
|
||||||
import { sasjsRequestTests } from "./testSuites/SasjsRequests";
|
import { sasjsRequestTests } from './testSuites/SasjsRequests'
|
||||||
import "@sasjs/test-framework/dist/index.css";
|
import '@sasjs/test-framework/dist/index.css'
|
||||||
import { computeTests } from "./testSuites/Compute";
|
import { computeTests } from './testSuites/Compute'
|
||||||
|
|
||||||
const App = (): ReactElement<{}> => {
|
const App = (): ReactElement<{}> => {
|
||||||
const { adapter, config } = useContext(AppContext);
|
const { adapter, config } = useContext(AppContext)
|
||||||
const [testSuites, setTestSuites] = useState<TestSuite[]>([]);
|
const [testSuites, setTestSuites] = useState<TestSuite[]>([])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (adapter) {
|
if (adapter) {
|
||||||
@@ -20,15 +20,15 @@ const App = (): ReactElement<{}> => {
|
|||||||
specialCaseTests(adapter),
|
specialCaseTests(adapter),
|
||||||
sasjsRequestTests(adapter),
|
sasjsRequestTests(adapter),
|
||||||
computeTests(adapter)
|
computeTests(adapter)
|
||||||
]);
|
])
|
||||||
}
|
}
|
||||||
}, [adapter, config]);
|
}, [adapter, config])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="app">
|
<div className="app">
|
||||||
{adapter && testSuites && <TestSuiteRunner testSuites={testSuites} />}
|
{adapter && testSuites && <TestSuiteRunner testSuites={testSuites} />}
|
||||||
</div>
|
</div>
|
||||||
);
|
)
|
||||||
};
|
}
|
||||||
|
|
||||||
export default App;
|
export default App
|
||||||
|
|||||||
@@ -1,22 +1,22 @@
|
|||||||
import React, { ReactElement, useState, useCallback, useContext } from "react";
|
import React, { ReactElement, useState, useCallback, useContext } from 'react'
|
||||||
import "./Login.scss";
|
import './Login.scss'
|
||||||
import { AppContext } from "@sasjs/test-framework";
|
import { AppContext } from '@sasjs/test-framework'
|
||||||
import { Redirect } from "react-router-dom";
|
import { Redirect } from 'react-router-dom'
|
||||||
|
|
||||||
const Login = (): ReactElement<{}> => {
|
const Login = (): ReactElement<{}> => {
|
||||||
const [username, setUsername] = useState("");
|
const [username, setUsername] = useState('')
|
||||||
const [password, setPassword] = useState("");
|
const [password, setPassword] = useState('')
|
||||||
const appContext = useContext(AppContext);
|
const appContext = useContext(AppContext)
|
||||||
|
|
||||||
const handleSubmit = useCallback(
|
const handleSubmit = useCallback(
|
||||||
(e) => {
|
(e) => {
|
||||||
e.preventDefault();
|
e.preventDefault()
|
||||||
appContext.adapter.logIn(username, password).then((res) => {
|
appContext.adapter.logIn(username, password).then((res) => {
|
||||||
appContext.setIsLoggedIn(res.isLoggedIn);
|
appContext.setIsLoggedIn(res.isLoggedIn)
|
||||||
});
|
})
|
||||||
},
|
},
|
||||||
[username, password, appContext]
|
[username, password, appContext]
|
||||||
);
|
)
|
||||||
|
|
||||||
return !appContext.isLoggedIn ? (
|
return !appContext.isLoggedIn ? (
|
||||||
<div className="login-container">
|
<div className="login-container">
|
||||||
@@ -48,7 +48,7 @@ const Login = (): ReactElement<{}> => {
|
|||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<Redirect to="/" />
|
<Redirect to="/" />
|
||||||
);
|
)
|
||||||
};
|
}
|
||||||
|
|
||||||
export default Login;
|
export default Login
|
||||||
|
|||||||
@@ -1,23 +1,23 @@
|
|||||||
import React, { ReactElement, useContext, FunctionComponent } from "react";
|
import React, { ReactElement, useContext, FunctionComponent } from 'react'
|
||||||
import { Redirect, Route } from "react-router-dom";
|
import { Redirect, Route } from 'react-router-dom'
|
||||||
import { AppContext } from "@sasjs/test-framework";
|
import { AppContext } from '@sasjs/test-framework'
|
||||||
|
|
||||||
interface PrivateRouteProps {
|
interface PrivateRouteProps {
|
||||||
component: FunctionComponent;
|
component: FunctionComponent
|
||||||
exact?: boolean;
|
exact?: boolean
|
||||||
path: string;
|
path: string
|
||||||
}
|
}
|
||||||
|
|
||||||
const PrivateRoute = (
|
const PrivateRoute = (
|
||||||
props: PrivateRouteProps
|
props: PrivateRouteProps
|
||||||
): ReactElement<PrivateRouteProps> => {
|
): ReactElement<PrivateRouteProps> => {
|
||||||
const { component, path, exact } = props;
|
const { component, path, exact } = props
|
||||||
const appContext = useContext(AppContext);
|
const appContext = useContext(AppContext)
|
||||||
return appContext.isLoggedIn ? (
|
return appContext.isLoggedIn ? (
|
||||||
<Route component={component} path={path} exact={exact} />
|
<Route component={component} path={path} exact={exact} />
|
||||||
) : (
|
) : (
|
||||||
<Redirect to="/login" />
|
<Redirect to="/login" />
|
||||||
);
|
)
|
||||||
};
|
}
|
||||||
|
|
||||||
export default PrivateRoute;
|
export default PrivateRoute
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
import React from "react";
|
import React from 'react'
|
||||||
import ReactDOM from "react-dom";
|
import ReactDOM from 'react-dom'
|
||||||
import { Route, HashRouter, Switch } from "react-router-dom";
|
import { Route, HashRouter, Switch } from 'react-router-dom'
|
||||||
import "./index.scss";
|
import './index.scss'
|
||||||
import * as serviceWorker from "./serviceWorker";
|
import * as serviceWorker from './serviceWorker'
|
||||||
import { AppProvider } from "@sasjs/test-framework";
|
import { AppProvider } from '@sasjs/test-framework'
|
||||||
import PrivateRoute from "./PrivateRoute";
|
import PrivateRoute from './PrivateRoute'
|
||||||
import Login from "./Login";
|
import Login from './Login'
|
||||||
import App from "./App";
|
import App from './App'
|
||||||
|
|
||||||
ReactDOM.render(
|
ReactDOM.render(
|
||||||
<AppProvider>
|
<AppProvider>
|
||||||
@@ -17,10 +17,10 @@ ReactDOM.render(
|
|||||||
</Switch>
|
</Switch>
|
||||||
</HashRouter>
|
</HashRouter>
|
||||||
</AppProvider>,
|
</AppProvider>,
|
||||||
document.getElementById("root")
|
document.getElementById('root')
|
||||||
);
|
)
|
||||||
|
|
||||||
// If you want your app to work offline and load faster, you can change
|
// If you want your app to work offline and load faster, you can change
|
||||||
// unregister() to register() below. Note this comes with some pitfalls.
|
// unregister() to register() below. Note this comes with some pitfalls.
|
||||||
// Learn more about service workers: https://bit.ly/CRA-PWA
|
// Learn more about service workers: https://bit.ly/CRA-PWA
|
||||||
serviceWorker.unregister();
|
serviceWorker.unregister()
|
||||||
|
|||||||
@@ -11,46 +11,46 @@
|
|||||||
// opt-in, read https://bit.ly/CRA-PWA
|
// opt-in, read https://bit.ly/CRA-PWA
|
||||||
|
|
||||||
const isLocalhost = Boolean(
|
const isLocalhost = Boolean(
|
||||||
window.location.hostname === "localhost" ||
|
window.location.hostname === 'localhost' ||
|
||||||
// [::1] is the IPv6 localhost address.
|
// [::1] is the IPv6 localhost address.
|
||||||
window.location.hostname === "[::1]" ||
|
window.location.hostname === '[::1]' ||
|
||||||
// 127.0.0.0/8 are considered localhost for IPv4.
|
// 127.0.0.0/8 are considered localhost for IPv4.
|
||||||
window.location.hostname.match(
|
window.location.hostname.match(
|
||||||
/^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/
|
/^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/
|
||||||
)
|
)
|
||||||
);
|
)
|
||||||
|
|
||||||
export function register(config) {
|
export function register(config) {
|
||||||
if (process.env.NODE_ENV === "production" && "serviceWorker" in navigator) {
|
if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) {
|
||||||
// The URL constructor is available in all browsers that support SW.
|
// The URL constructor is available in all browsers that support SW.
|
||||||
const publicUrl = new URL(process.env.PUBLIC_URL, window.location.href);
|
const publicUrl = new URL(process.env.PUBLIC_URL, window.location.href)
|
||||||
if (publicUrl.origin !== window.location.origin) {
|
if (publicUrl.origin !== window.location.origin) {
|
||||||
// Our service worker won't work if PUBLIC_URL is on a different origin
|
// Our service worker won't work if PUBLIC_URL is on a different origin
|
||||||
// from what our page is served on. This might happen if a CDN is used to
|
// from what our page is served on. This might happen if a CDN is used to
|
||||||
// serve assets; see https://github.com/facebook/create-react-app/issues/2374
|
// serve assets; see https://github.com/facebook/create-react-app/issues/2374
|
||||||
return;
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
window.addEventListener("load", () => {
|
window.addEventListener('load', () => {
|
||||||
const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`;
|
const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`
|
||||||
|
|
||||||
if (isLocalhost) {
|
if (isLocalhost) {
|
||||||
// This is running on localhost. Let's check if a service worker still exists or not.
|
// This is running on localhost. Let's check if a service worker still exists or not.
|
||||||
checkValidServiceWorker(swUrl, config);
|
checkValidServiceWorker(swUrl, config)
|
||||||
|
|
||||||
// Add some additional logging to localhost, pointing developers to the
|
// Add some additional logging to localhost, pointing developers to the
|
||||||
// service worker/PWA documentation.
|
// service worker/PWA documentation.
|
||||||
navigator.serviceWorker.ready.then(() => {
|
navigator.serviceWorker.ready.then(() => {
|
||||||
console.log(
|
console.log(
|
||||||
"This web app is being served cache-first by a service " +
|
'This web app is being served cache-first by a service ' +
|
||||||
"worker. To learn more, visit https://bit.ly/CRA-PWA"
|
'worker. To learn more, visit https://bit.ly/CRA-PWA'
|
||||||
);
|
)
|
||||||
});
|
})
|
||||||
} else {
|
} else {
|
||||||
// Is not localhost. Just register service worker
|
// Is not localhost. Just register service worker
|
||||||
registerValidSW(swUrl, config);
|
registerValidSW(swUrl, config)
|
||||||
}
|
}
|
||||||
});
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -59,83 +59,83 @@ function registerValidSW(swUrl, config) {
|
|||||||
.register(swUrl)
|
.register(swUrl)
|
||||||
.then((registration) => {
|
.then((registration) => {
|
||||||
registration.onupdatefound = () => {
|
registration.onupdatefound = () => {
|
||||||
const installingWorker = registration.installing;
|
const installingWorker = registration.installing
|
||||||
if (installingWorker == null) {
|
if (installingWorker == null) {
|
||||||
return;
|
return
|
||||||
}
|
}
|
||||||
installingWorker.onstatechange = () => {
|
installingWorker.onstatechange = () => {
|
||||||
if (installingWorker.state === "installed") {
|
if (installingWorker.state === 'installed') {
|
||||||
if (navigator.serviceWorker.controller) {
|
if (navigator.serviceWorker.controller) {
|
||||||
// At this point, the updated precached content has been fetched,
|
// At this point, the updated precached content has been fetched,
|
||||||
// but the previous service worker will still serve the older
|
// but the previous service worker will still serve the older
|
||||||
// content until all client tabs are closed.
|
// content until all client tabs are closed.
|
||||||
console.log(
|
console.log(
|
||||||
"New content is available and will be used when all " +
|
'New content is available and will be used when all ' +
|
||||||
"tabs for this page are closed. See https://bit.ly/CRA-PWA."
|
'tabs for this page are closed. See https://bit.ly/CRA-PWA.'
|
||||||
);
|
)
|
||||||
|
|
||||||
// Execute callback
|
// Execute callback
|
||||||
if (config && config.onUpdate) {
|
if (config && config.onUpdate) {
|
||||||
config.onUpdate(registration);
|
config.onUpdate(registration)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// At this point, everything has been precached.
|
// At this point, everything has been precached.
|
||||||
// It's the perfect time to display a
|
// It's the perfect time to display a
|
||||||
// "Content is cached for offline use." message.
|
// "Content is cached for offline use." message.
|
||||||
console.log("Content is cached for offline use.");
|
console.log('Content is cached for offline use.')
|
||||||
|
|
||||||
// Execute callback
|
// Execute callback
|
||||||
if (config && config.onSuccess) {
|
if (config && config.onSuccess) {
|
||||||
config.onSuccess(registration);
|
config.onSuccess(registration)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
|
||||||
};
|
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
console.error("Error during service worker registration:", error);
|
console.error('Error during service worker registration:', error)
|
||||||
});
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function checkValidServiceWorker(swUrl, config) {
|
function checkValidServiceWorker(swUrl, config) {
|
||||||
// Check if the service worker can be found. If it can't reload the page.
|
// Check if the service worker can be found. If it can't reload the page.
|
||||||
fetch(swUrl, {
|
fetch(swUrl, {
|
||||||
headers: { "Service-Worker": "script" }
|
headers: { 'Service-Worker': 'script' }
|
||||||
})
|
})
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
// Ensure service worker exists, and that we really are getting a JS file.
|
// Ensure service worker exists, and that we really are getting a JS file.
|
||||||
const contentType = response.headers.get("content-type");
|
const contentType = response.headers.get('content-type')
|
||||||
if (
|
if (
|
||||||
response.status === 404 ||
|
response.status === 404 ||
|
||||||
(contentType != null && contentType.indexOf("javascript") === -1)
|
(contentType != null && contentType.indexOf('javascript') === -1)
|
||||||
) {
|
) {
|
||||||
// No service worker found. Probably a different app. Reload the page.
|
// No service worker found. Probably a different app. Reload the page.
|
||||||
navigator.serviceWorker.ready.then((registration) => {
|
navigator.serviceWorker.ready.then((registration) => {
|
||||||
registration.unregister().then(() => {
|
registration.unregister().then(() => {
|
||||||
window.location.reload();
|
window.location.reload()
|
||||||
});
|
})
|
||||||
});
|
})
|
||||||
} else {
|
} else {
|
||||||
// Service worker found. Proceed as normal.
|
// Service worker found. Proceed as normal.
|
||||||
registerValidSW(swUrl, config);
|
registerValidSW(swUrl, config)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
console.log(
|
console.log(
|
||||||
"No internet connection found. App is running in offline mode."
|
'No internet connection found. App is running in offline mode.'
|
||||||
);
|
)
|
||||||
});
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
export function unregister() {
|
export function unregister() {
|
||||||
if ("serviceWorker" in navigator) {
|
if ('serviceWorker' in navigator) {
|
||||||
navigator.serviceWorker.ready
|
navigator.serviceWorker.ready
|
||||||
.then((registration) => {
|
.then((registration) => {
|
||||||
registration.unregister();
|
registration.unregister()
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
console.error(error.message);
|
console.error(error.message)
|
||||||
});
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,4 +2,4 @@
|
|||||||
// allows you to do things like:
|
// allows you to do things like:
|
||||||
// expect(element).toHaveTextContent(/react/i)
|
// expect(element).toHaveTextContent(/react/i)
|
||||||
// learn more: https://github.com/testing-library/jest-dom
|
// learn more: https://github.com/testing-library/jest-dom
|
||||||
import "@testing-library/jest-dom/extend-expect";
|
import '@testing-library/jest-dom/extend-expect'
|
||||||
|
|||||||
@@ -1,97 +1,102 @@
|
|||||||
import SASjs, { SASjsConfig } from "@sasjs/adapter";
|
import SASjs, { SASjsConfig } from '@sasjs/adapter'
|
||||||
import { TestSuite } from "@sasjs/test-framework";
|
import { TestSuite } from '@sasjs/test-framework'
|
||||||
import { ServerType } from "@sasjs/utils/types";
|
import { ServerType } from '@sasjs/utils/types'
|
||||||
|
|
||||||
const stringData: any = { table1: [{ col1: "first col value" }] };
|
const stringData: any = { table1: [{ col1: 'first col value' }] }
|
||||||
|
|
||||||
const defaultConfig: SASjsConfig = {
|
const defaultConfig: SASjsConfig = {
|
||||||
serverUrl: window.location.origin,
|
serverUrl: window.location.origin,
|
||||||
pathSAS9: "/SASStoredProcess/do",
|
pathSAS9: '/SASStoredProcess/do',
|
||||||
pathSASViya: "/SASJobExecution",
|
pathSASViya: '/SASJobExecution',
|
||||||
appLoc: "/Public/seedapp",
|
appLoc: '/Public/seedapp',
|
||||||
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: false,
|
||||||
allowInsecureRequests: false
|
allowInsecureRequests: false
|
||||||
};
|
}
|
||||||
|
|
||||||
const customConfig = {
|
const customConfig = {
|
||||||
serverUrl: "http://url.com",
|
serverUrl: 'http://url.com',
|
||||||
pathSAS9: "sas9",
|
pathSAS9: 'sas9',
|
||||||
pathSASViya: "viya",
|
pathSASViya: 'viya',
|
||||||
appLoc: "/Public/seedapp",
|
appLoc: '/Public/seedapp',
|
||||||
serverType: ServerType.Sas9,
|
serverType: ServerType.Sas9,
|
||||||
debug: false
|
debug: false
|
||||||
};
|
}
|
||||||
|
|
||||||
export const basicTests = (
|
export const basicTests = (
|
||||||
adapter: SASjs,
|
adapter: SASjs,
|
||||||
userName: string,
|
userName: string,
|
||||||
password: string
|
password: string
|
||||||
): TestSuite => ({
|
): TestSuite => ({
|
||||||
name: "Basic Tests",
|
name: 'Basic Tests',
|
||||||
tests: [
|
tests: [
|
||||||
{
|
{
|
||||||
title: "Log in",
|
title: 'Log in',
|
||||||
description: "Should log the user in",
|
description: 'Should log the user in',
|
||||||
test: async () => {
|
test: async () => {
|
||||||
return adapter.logIn(userName, password);
|
return adapter.logIn(userName, password)
|
||||||
},
|
},
|
||||||
assertion: (response: any) =>
|
assertion: (response: any) =>
|
||||||
response && response.isLoggedIn && response.userName === userName
|
response && response.isLoggedIn && response.userName === userName
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "Multiple Log in attempts",
|
title: 'Multiple Log in attempts',
|
||||||
description:
|
description:
|
||||||
"Should fail on first attempt and should log the user in on second attempt",
|
'Should fail on first attempt and should log the user in on second attempt',
|
||||||
test: async () => {
|
test: async () => {
|
||||||
await adapter.logOut();
|
await adapter.logOut()
|
||||||
await adapter.logIn("invalid", "invalid");
|
await adapter.logIn('invalid', 'invalid')
|
||||||
return adapter.logIn(userName, password);
|
return adapter.logIn(userName, password)
|
||||||
},
|
},
|
||||||
assertion: (response: any) =>
|
assertion: (response: any) =>
|
||||||
response && response.isLoggedIn && response.userName === userName
|
response && response.isLoggedIn && response.userName === userName
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "Trigger login callback",
|
title: 'Trigger login callback',
|
||||||
description:
|
description:
|
||||||
"Should trigger required login callback and after successful login, it should finish the request",
|
'Should trigger required login callback and after successful login, it should finish the request',
|
||||||
test: async () => {
|
test: async () => {
|
||||||
await adapter.logOut();
|
await adapter.logOut()
|
||||||
|
|
||||||
return await adapter.request("common/sendArr", stringData, null, () => {
|
return await adapter.request(
|
||||||
adapter.logIn(userName, password);
|
'common/sendArr',
|
||||||
});
|
stringData,
|
||||||
|
undefined,
|
||||||
|
() => {
|
||||||
|
adapter.logIn(userName, password)
|
||||||
|
}
|
||||||
|
)
|
||||||
},
|
},
|
||||||
assertion: (response: any) => {
|
assertion: (response: any) => {
|
||||||
return response.table1[0][0] === stringData.table1[0].col1;
|
return response.table1[0][0] === stringData.table1[0].col1
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "Request with debug on",
|
title: 'Request with debug on',
|
||||||
description:
|
description:
|
||||||
"Should complete successful request with debugging switched on",
|
'Should complete successful request with debugging switched on',
|
||||||
test: async () => {
|
test: async () => {
|
||||||
const config = {
|
const config = {
|
||||||
debug: true
|
debug: true
|
||||||
}
|
}
|
||||||
|
|
||||||
return await adapter.request("common/sendArr", stringData, config)
|
return await adapter.request('common/sendArr', stringData, config)
|
||||||
},
|
},
|
||||||
assertion: (response: any) => {
|
assertion: (response: any) => {
|
||||||
return response.table1[0][0] === stringData.table1[0].col1;
|
return response.table1[0][0] === stringData.table1[0].col1
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "Default config",
|
title: 'Default config',
|
||||||
description:
|
description:
|
||||||
"Should instantiate with default config when none is provided",
|
'Should instantiate with default config when none is provided',
|
||||||
test: async () => {
|
test: async () => {
|
||||||
return Promise.resolve(new SASjs());
|
return Promise.resolve(new SASjs())
|
||||||
},
|
},
|
||||||
assertion: (sasjsInstance: SASjs) => {
|
assertion: (sasjsInstance: SASjs) => {
|
||||||
const sasjsConfig = sasjsInstance.getSasjsConfig();
|
const sasjsConfig = sasjsInstance.getSasjsConfig()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
sasjsConfig.serverUrl === defaultConfig.serverUrl &&
|
sasjsConfig.serverUrl === defaultConfig.serverUrl &&
|
||||||
@@ -100,17 +105,17 @@ export const basicTests = (
|
|||||||
sasjsConfig.appLoc === defaultConfig.appLoc &&
|
sasjsConfig.appLoc === defaultConfig.appLoc &&
|
||||||
sasjsConfig.serverType === defaultConfig.serverType &&
|
sasjsConfig.serverType === defaultConfig.serverType &&
|
||||||
sasjsConfig.debug === defaultConfig.debug
|
sasjsConfig.debug === defaultConfig.debug
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "Custom config",
|
title: 'Custom config',
|
||||||
description: "Should use fully custom config whenever supplied",
|
description: 'Should use fully custom config whenever supplied',
|
||||||
test: async () => {
|
test: async () => {
|
||||||
return Promise.resolve(new SASjs(customConfig));
|
return Promise.resolve(new SASjs(customConfig))
|
||||||
},
|
},
|
||||||
assertion: (sasjsInstance: SASjs) => {
|
assertion: (sasjsInstance: SASjs) => {
|
||||||
const sasjsConfig = sasjsInstance.getSasjsConfig();
|
const sasjsConfig = sasjsInstance.getSasjsConfig()
|
||||||
return (
|
return (
|
||||||
sasjsConfig.serverUrl === customConfig.serverUrl &&
|
sasjsConfig.serverUrl === customConfig.serverUrl &&
|
||||||
sasjsConfig.pathSAS9 === customConfig.pathSAS9 &&
|
sasjsConfig.pathSAS9 === customConfig.pathSAS9 &&
|
||||||
@@ -118,28 +123,28 @@ export const basicTests = (
|
|||||||
sasjsConfig.appLoc === customConfig.appLoc &&
|
sasjsConfig.appLoc === customConfig.appLoc &&
|
||||||
sasjsConfig.serverType === customConfig.serverType &&
|
sasjsConfig.serverType === customConfig.serverType &&
|
||||||
sasjsConfig.debug === customConfig.debug
|
sasjsConfig.debug === customConfig.debug
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "Config overrides",
|
title: 'Config overrides',
|
||||||
description: "Should override default config with supplied properties",
|
description: 'Should override default config with supplied properties',
|
||||||
test: async () => {
|
test: async () => {
|
||||||
return Promise.resolve(
|
return Promise.resolve(
|
||||||
new SASjs({ serverUrl: "http://test.com", debug: false })
|
new SASjs({ serverUrl: 'http://test.com', debug: false })
|
||||||
);
|
)
|
||||||
},
|
},
|
||||||
assertion: (sasjsInstance: SASjs) => {
|
assertion: (sasjsInstance: SASjs) => {
|
||||||
const sasjsConfig = sasjsInstance.getSasjsConfig();
|
const sasjsConfig = sasjsInstance.getSasjsConfig()
|
||||||
return (
|
return (
|
||||||
sasjsConfig.serverUrl === "http://test.com" &&
|
sasjsConfig.serverUrl === 'http://test.com' &&
|
||||||
sasjsConfig.pathSAS9 === defaultConfig.pathSAS9 &&
|
sasjsConfig.pathSAS9 === defaultConfig.pathSAS9 &&
|
||||||
sasjsConfig.pathSASViya === defaultConfig.pathSASViya &&
|
sasjsConfig.pathSASViya === defaultConfig.pathSASViya &&
|
||||||
sasjsConfig.appLoc === defaultConfig.appLoc &&
|
sasjsConfig.appLoc === defaultConfig.appLoc &&
|
||||||
sasjsConfig.serverType === defaultConfig.serverType &&
|
sasjsConfig.serverType === defaultConfig.serverType &&
|
||||||
sasjsConfig.debug === false
|
sasjsConfig.debug === false
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
});
|
})
|
||||||
|
|||||||
@@ -1,106 +1,100 @@
|
|||||||
import SASjs from "@sasjs/adapter";
|
import SASjs from '@sasjs/adapter'
|
||||||
import { TestSuite } from "@sasjs/test-framework";
|
import { TestSuite } from '@sasjs/test-framework'
|
||||||
|
|
||||||
export const computeTests = (adapter: SASjs): TestSuite => ({
|
export const computeTests = (adapter: SASjs): TestSuite => ({
|
||||||
name: "Compute",
|
name: 'Compute',
|
||||||
tests: [
|
tests: [
|
||||||
{
|
{
|
||||||
title: "Start Compute Job - not waiting for result",
|
title: 'Start Compute Job - not waiting for result',
|
||||||
description: "Should start a compute job and return the session",
|
description: 'Should start a compute job and return the session',
|
||||||
test: () => {
|
test: () => {
|
||||||
const data: any = { table1: [{ col1: "first col value" }] };
|
const data: any = { table1: [{ col1: 'first col value' }] }
|
||||||
return adapter.startComputeJob("/Public/app/common/sendArr", data);
|
return adapter.startComputeJob('/Public/app/common/sendArr', data)
|
||||||
},
|
},
|
||||||
assertion: (res: any) => {
|
assertion: (res: any) => {
|
||||||
const expectedProperties = ["id", "applicationName", "attributes"];
|
const expectedProperties = ['id', 'applicationName', 'attributes']
|
||||||
return validate(expectedProperties, res);
|
return validate(expectedProperties, res)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "Start Compute Job - waiting for result",
|
title: 'Start Compute Job - waiting for result',
|
||||||
description: "Should start a compute job and return the job",
|
description: 'Should start a compute job and return the job',
|
||||||
test: () => {
|
test: () => {
|
||||||
const data: any = { table1: [{ col1: "first col value" }] };
|
const data: any = { table1: [{ col1: 'first col value' }] }
|
||||||
return adapter.startComputeJob(
|
return adapter.startComputeJob(
|
||||||
"/Public/app/common/sendArr",
|
'/Public/app/common/sendArr',
|
||||||
data,
|
data,
|
||||||
{},
|
{},
|
||||||
"",
|
'',
|
||||||
true
|
true
|
||||||
);
|
)
|
||||||
},
|
},
|
||||||
assertion: (res: any) => {
|
assertion: (res: any) => {
|
||||||
const expectedProperties = [
|
const expectedProperties = [
|
||||||
"id",
|
'id',
|
||||||
"state",
|
'state',
|
||||||
"creationTimeStamp",
|
'creationTimeStamp',
|
||||||
"jobConditionCode"
|
'jobConditionCode'
|
||||||
];
|
]
|
||||||
return validate(expectedProperties, res.job);
|
return validate(expectedProperties, res.job)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "Execute Script Viya - complete job",
|
title: 'Execute Script Viya - complete job',
|
||||||
description: "Should execute sas file and return log",
|
description: 'Should execute sas file and return log',
|
||||||
test: () => {
|
test: () => {
|
||||||
const fileLines = [
|
const fileLines = [`data;`, `do x=1 to 100;`, `output;`, `end;`, `run;`]
|
||||||
`data;`,
|
|
||||||
`do x=1 to 100;`,
|
|
||||||
`output;`,
|
|
||||||
`end;`,
|
|
||||||
`run;`
|
|
||||||
];
|
|
||||||
|
|
||||||
return adapter.executeScriptSASViya(
|
return adapter.executeScriptSASViya(
|
||||||
"sasCode.sas",
|
'sasCode.sas',
|
||||||
fileLines,
|
fileLines,
|
||||||
"SAS Studio compute context",
|
'SAS Studio compute context',
|
||||||
undefined,
|
|
||||||
true
|
|
||||||
);
|
|
||||||
},
|
|
||||||
assertion: (res: any) => {
|
|
||||||
const expectedLogContent = `1 data;\\n2 do x=1 to 100;\\n3 output;\\n4 end;\\n5 run;\\n\\n`;
|
|
||||||
|
|
||||||
return validateLog(expectedLogContent, res.log);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "Execute Script Viya - failed job",
|
|
||||||
description: "Should execute sas file and return log",
|
|
||||||
test: () => {
|
|
||||||
const fileLines = [`%abort;`];
|
|
||||||
|
|
||||||
return adapter
|
|
||||||
.executeScriptSASViya(
|
|
||||||
"sasCode.sas",
|
|
||||||
fileLines,
|
|
||||||
"SAS Studio compute context",
|
|
||||||
undefined,
|
undefined,
|
||||||
true
|
true
|
||||||
)
|
)
|
||||||
.catch((err: any) => err);
|
|
||||||
},
|
},
|
||||||
assertion: (res: any) => {
|
assertion: (res: any) => {
|
||||||
const expectedLogContent = `1 %abort;\\nERROR: The %ABORT statement is not valid in open code.\\n`;
|
const expectedLogContent = `1 data;\\n2 do x=1 to 100;\\n3 output;\\n4 end;\\n5 run;\\n\\n`
|
||||||
|
|
||||||
return validateLog(expectedLogContent, res.log);
|
return validateLog(expectedLogContent, res.log)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Execute Script Viya - failed job',
|
||||||
|
description: 'Should execute sas file and return log',
|
||||||
|
test: () => {
|
||||||
|
const fileLines = [`%abort;`]
|
||||||
|
|
||||||
|
return adapter
|
||||||
|
.executeScriptSASViya(
|
||||||
|
'sasCode.sas',
|
||||||
|
fileLines,
|
||||||
|
'SAS Studio compute context',
|
||||||
|
undefined,
|
||||||
|
true
|
||||||
|
)
|
||||||
|
.catch((err: any) => err)
|
||||||
|
},
|
||||||
|
assertion: (res: any) => {
|
||||||
|
const expectedLogContent = `1 %abort;\\nERROR: The %ABORT statement is not valid in open code.\\n`
|
||||||
|
|
||||||
|
return validateLog(expectedLogContent, res.log)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
});
|
})
|
||||||
|
|
||||||
const validateLog = (text: string, log: string): boolean => {
|
const validateLog = (text: string, log: string): boolean => {
|
||||||
const isValid = JSON.stringify(log).includes(text);
|
const isValid = JSON.stringify(log).includes(text)
|
||||||
|
|
||||||
return isValid;
|
return isValid
|
||||||
};
|
}
|
||||||
|
|
||||||
const validate = (expectedProperties: string[], data: any): boolean => {
|
const validate = (expectedProperties: string[], data: any): boolean => {
|
||||||
const actualProperties = Object.keys(data);
|
const actualProperties = Object.keys(data)
|
||||||
|
|
||||||
const isValid = expectedProperties.every((property) =>
|
const isValid = expectedProperties.every((property) =>
|
||||||
actualProperties.includes(property)
|
actualProperties.includes(property)
|
||||||
);
|
)
|
||||||
return isValid;
|
return isValid
|
||||||
};
|
}
|
||||||
|
|||||||
@@ -1,111 +1,112 @@
|
|||||||
import SASjs from "@sasjs/adapter";
|
import SASjs from '@sasjs/adapter'
|
||||||
import { TestSuite } from "@sasjs/test-framework";
|
import { TestSuite } from '@sasjs/test-framework'
|
||||||
|
|
||||||
const stringData: any = { table1: [{ col1: "first col value" }] };
|
const stringData: any = { table1: [{ col1: 'first col value' }] }
|
||||||
const numericData: any = { table1: [{ col1: 3.14159265 }] };
|
const numericData: any = { table1: [{ col1: 3.14159265 }] }
|
||||||
const multiColumnData: any = {
|
const multiColumnData: any = {
|
||||||
table1: [{ col1: 42, col2: 1.618, col3: "x", col4: "x" }]
|
table1: [{ col1: 42, col2: 1.618, col3: 'x', col4: 'x' }]
|
||||||
};
|
}
|
||||||
const multipleRowsWithNulls: any = {
|
const multipleRowsWithNulls: any = {
|
||||||
table1: [
|
table1: [
|
||||||
{ col1: 42, col2: null, col3: "x", col4: "" },
|
{ col1: 42, col2: null, col3: 'x', col4: '' },
|
||||||
{ col1: 42, col2: null, col3: "x", col4: "" },
|
{ col1: 42, col2: null, col3: 'x', col4: '' },
|
||||||
{ col1: 42, col2: null, col3: "x", col4: "" },
|
{ col1: 42, col2: null, col3: 'x', col4: '' },
|
||||||
{ col1: 42, col2: 1.62, col3: "x", col4: "x" },
|
{ col1: 42, col2: 1.62, col3: 'x', col4: 'x' },
|
||||||
{ col1: 42, col2: 1.62, col3: "x", col4: "x" }
|
{ col1: 42, col2: 1.62, col3: 'x', col4: 'x' }
|
||||||
]
|
]
|
||||||
};
|
}
|
||||||
|
|
||||||
const multipleColumnsWithNulls: any = {
|
const multipleColumnsWithNulls: any = {
|
||||||
table1: [
|
table1: [
|
||||||
{ col1: 42, col2: null, col3: "x", col4: null },
|
{ col1: 42, col2: null, col3: 'x', col4: null },
|
||||||
{ col1: 42, col2: null, col3: "x", col4: null },
|
{ col1: 42, col2: null, col3: 'x', col4: null },
|
||||||
{ col1: 42, col2: null, col3: "x", col4: null },
|
{ col1: 42, col2: null, col3: 'x', col4: null },
|
||||||
{ col1: 42, col2: null, col3: "x", col4: "" },
|
{ col1: 42, col2: null, col3: 'x', col4: '' },
|
||||||
{ col1: 42, col2: null, col3: "x", col4: "" }
|
{ col1: 42, col2: null, col3: 'x', col4: '' }
|
||||||
]
|
]
|
||||||
};
|
}
|
||||||
|
|
||||||
const getLongStringData = (length = 32764) => {
|
const getLongStringData = (length = 32764) => {
|
||||||
let x = "X";
|
let x = 'X'
|
||||||
for (let i = 1; i <= length; i++) {
|
for (let i = 1; i <= length; i++) {
|
||||||
x = x + "X";
|
x = x + 'X'
|
||||||
}
|
}
|
||||||
const data: any = { table1: [{ col1: x }] };
|
const data: any = { table1: [{ col1: x }] }
|
||||||
return data;
|
return data
|
||||||
};
|
}
|
||||||
|
|
||||||
const getLargeObjectData = () => {
|
const getLargeObjectData = () => {
|
||||||
const data = { table1: [{ big: "data" }] };
|
const data = { table1: [{ big: 'data' }] }
|
||||||
|
|
||||||
for (let i = 1; i < 10000; i++) {
|
for (let i = 1; i < 10000; i++) {
|
||||||
data.table1.push(data.table1[0]);
|
data.table1.push(data.table1[0])
|
||||||
}
|
}
|
||||||
|
|
||||||
return data;
|
return data
|
||||||
};
|
}
|
||||||
|
|
||||||
export const sendArrTests = (adapter: SASjs): TestSuite => ({
|
export const sendArrTests = (adapter: SASjs): TestSuite => ({
|
||||||
name: "sendArr",
|
name: 'sendArr',
|
||||||
tests: [
|
tests: [
|
||||||
{
|
{
|
||||||
title: "Absolute paths",
|
title: 'Absolute paths',
|
||||||
description: "Should work with absolute paths to SAS jobs",
|
description: 'Should work with absolute paths to SAS jobs',
|
||||||
test: () => {
|
test: () => {
|
||||||
return adapter.request("/Public/app/common/sendArr", stringData);
|
return adapter.request('/Public/app/common/sendArr', stringData)
|
||||||
},
|
},
|
||||||
assertion: (res: any) => {
|
assertion: (res: any) => {
|
||||||
return res.table1[0][0] === stringData.table1[0].col1;
|
return res.table1[0][0] === stringData.table1[0].col1
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "Single string value",
|
title: 'Single string value',
|
||||||
description: "Should send an array with a single string value",
|
description: 'Should send an array with a single string value',
|
||||||
test: () => {
|
test: () => {
|
||||||
return adapter.request("common/sendArr", stringData);
|
return adapter.request('common/sendArr', stringData)
|
||||||
},
|
},
|
||||||
assertion: (res: any) => {
|
assertion: (res: any) => {
|
||||||
return res.table1[0][0] === stringData.table1[0].col1;
|
return res.table1[0][0] === stringData.table1[0].col1
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "Long string value",
|
title: 'Long string value',
|
||||||
description:
|
description:
|
||||||
"Should send an array with a long string value under 32765 characters",
|
'Should send an array with a long string value under 32765 characters',
|
||||||
test: () => {
|
test: () => {
|
||||||
return adapter.request("common/sendArr", getLongStringData());
|
return adapter.request('common/sendArr', getLongStringData())
|
||||||
},
|
},
|
||||||
assertion: (res: any) => {
|
assertion: (res: any) => {
|
||||||
const longStringData = getLongStringData();
|
const longStringData = getLongStringData()
|
||||||
return res.table1[0][0] === longStringData.table1[0].col1;
|
return res.table1[0][0] === longStringData.table1[0].col1
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "Overly long string value",
|
title: 'Overly long string value',
|
||||||
description:
|
description:
|
||||||
"Should error out with long string values over 32765 characters",
|
'Should error out with long string values over 32765 characters',
|
||||||
test: () => {
|
test: () => {
|
||||||
const data = getLongStringData(32767);
|
const data = getLongStringData(32767)
|
||||||
return adapter.request("common/sendArr", data).catch((e) => e);
|
return adapter.request('common/sendArr', data).catch((e) => e)
|
||||||
},
|
},
|
||||||
assertion: (error: any) => {
|
assertion: (error: any) => {
|
||||||
return !!error && !!error.error && !!error.error.message;
|
return !!error && !!error.error && !!error.error.message
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "Single numeric value",
|
title: 'Single numeric value',
|
||||||
description: "Should send an array with a single numeric value",
|
description: 'Should send an array with a single numeric value',
|
||||||
test: () => {
|
test: () => {
|
||||||
return adapter.request("common/sendArr", numericData);
|
return adapter.request('common/sendArr', numericData)
|
||||||
},
|
},
|
||||||
assertion: (res: any) => {
|
assertion: (res: any) => {
|
||||||
return res.table1[0][0] === numericData.table1[0].col1;
|
return res.table1[0][0] === numericData.table1[0].col1
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "Multiple columns",
|
title: 'Multiple columns',
|
||||||
description: "Should handle data with multiple columns",
|
description: 'Should handle data with multiple columns',
|
||||||
test: () => {
|
test: () => {
|
||||||
return adapter.request("common/sendArr", multiColumnData);
|
return adapter.request('common/sendArr', multiColumnData)
|
||||||
},
|
},
|
||||||
assertion: (res: any) => {
|
assertion: (res: any) => {
|
||||||
return (
|
return (
|
||||||
@@ -113,143 +114,141 @@ export const sendArrTests = (adapter: SASjs): TestSuite => ({
|
|||||||
res.table1[0][1] === multiColumnData.table1[0].col2 &&
|
res.table1[0][1] === multiColumnData.table1[0].col2 &&
|
||||||
res.table1[0][2] === multiColumnData.table1[0].col3 &&
|
res.table1[0][2] === multiColumnData.table1[0].col3 &&
|
||||||
res.table1[0][3] === multiColumnData.table1[0].col4
|
res.table1[0][3] === multiColumnData.table1[0].col4
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "Multiple rows with nulls",
|
title: 'Multiple rows with nulls',
|
||||||
description: "Should handle data with multiple rows with null values",
|
description: 'Should handle data with multiple rows with null values',
|
||||||
test: () => {
|
test: () => {
|
||||||
return adapter.request("common/sendArr", multipleRowsWithNulls);
|
return adapter.request('common/sendArr', multipleRowsWithNulls)
|
||||||
},
|
},
|
||||||
assertion: (res: any) => {
|
assertion: (res: any) => {
|
||||||
let result = true;
|
let result = true
|
||||||
multipleRowsWithNulls.table1.forEach((_: any, index: number) => {
|
multipleRowsWithNulls.table1.forEach((_: any, index: number) => {
|
||||||
result =
|
result =
|
||||||
result &&
|
result &&
|
||||||
res.table1[index][0] === multipleRowsWithNulls.table1[index].col1;
|
res.table1[index][0] === multipleRowsWithNulls.table1[index].col1
|
||||||
result =
|
result =
|
||||||
result &&
|
result &&
|
||||||
res.table1[index][1] === multipleRowsWithNulls.table1[index].col2;
|
res.table1[index][1] === multipleRowsWithNulls.table1[index].col2
|
||||||
result =
|
result =
|
||||||
result &&
|
result &&
|
||||||
res.table1[index][2] === multipleRowsWithNulls.table1[index].col3;
|
res.table1[index][2] === multipleRowsWithNulls.table1[index].col3
|
||||||
result =
|
|
||||||
result &&
|
|
||||||
res.table1[index][3] === multipleRowsWithNulls.table1[index].col4;
|
|
||||||
});
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "Multiple columns with nulls",
|
|
||||||
description: "Should handle data with multiple columns with null values",
|
|
||||||
test: () => {
|
|
||||||
return adapter.request("common/sendArr", multipleColumnsWithNulls);
|
|
||||||
},
|
|
||||||
assertion: (res: any) => {
|
|
||||||
let result = true;
|
|
||||||
multipleColumnsWithNulls.table1.forEach((_: any, index: number) => {
|
|
||||||
result =
|
|
||||||
result &&
|
|
||||||
res.table1[index][0] ===
|
|
||||||
multipleColumnsWithNulls.table1[index].col1;
|
|
||||||
result =
|
|
||||||
result &&
|
|
||||||
res.table1[index][1] ===
|
|
||||||
multipleColumnsWithNulls.table1[index].col2;
|
|
||||||
result =
|
|
||||||
result &&
|
|
||||||
res.table1[index][2] ===
|
|
||||||
multipleColumnsWithNulls.table1[index].col3;
|
|
||||||
result =
|
result =
|
||||||
result &&
|
result &&
|
||||||
res.table1[index][3] ===
|
res.table1[index][3] ===
|
||||||
(multipleColumnsWithNulls.table1[index].col4 || "");
|
(multipleRowsWithNulls.table1[index].col4 || ' ')
|
||||||
});
|
})
|
||||||
return result;
|
return result
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Multiple columns with nulls',
|
||||||
|
description: 'Should handle data with multiple columns with null values',
|
||||||
|
test: () => {
|
||||||
|
return adapter.request('common/sendArr', multipleColumnsWithNulls)
|
||||||
|
},
|
||||||
|
assertion: (res: any) => {
|
||||||
|
let result = true
|
||||||
|
multipleColumnsWithNulls.table1.forEach((_: any, index: number) => {
|
||||||
|
result =
|
||||||
|
result &&
|
||||||
|
res.table1[index][0] === multipleColumnsWithNulls.table1[index].col1
|
||||||
|
result =
|
||||||
|
result &&
|
||||||
|
res.table1[index][1] === multipleColumnsWithNulls.table1[index].col2
|
||||||
|
result =
|
||||||
|
result &&
|
||||||
|
res.table1[index][2] === multipleColumnsWithNulls.table1[index].col3
|
||||||
|
result =
|
||||||
|
result &&
|
||||||
|
res.table1[index][3] ===
|
||||||
|
(multipleColumnsWithNulls.table1[index].col4 || ' ')
|
||||||
|
})
|
||||||
|
return result
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
});
|
})
|
||||||
|
|
||||||
export const sendObjTests = (adapter: SASjs): TestSuite => ({
|
export const sendObjTests = (adapter: SASjs): TestSuite => ({
|
||||||
name: "sendObj",
|
name: 'sendObj',
|
||||||
tests: [
|
tests: [
|
||||||
{
|
{
|
||||||
title: "Invalid column name",
|
title: 'Invalid column name',
|
||||||
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 }]
|
'1 invalid table': [{ col1: 42 }]
|
||||||
};
|
}
|
||||||
return adapter.request("common/sendObj", invalidData).catch((e) => e);
|
return adapter.request('common/sendObj', invalidData).catch((e) => e)
|
||||||
},
|
},
|
||||||
assertion: (error: any) =>
|
assertion: (error: any) =>
|
||||||
!!error && !!error.error && !!error.error.message
|
!!error && !!error.error && !!error.error.message
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "Single string value",
|
title: 'Single string value',
|
||||||
description: "Should send an object with a single string value",
|
description: 'Should send an object with a single string value',
|
||||||
test: () => {
|
test: () => {
|
||||||
return adapter.request("common/sendObj", stringData);
|
return adapter.request('common/sendObj', stringData)
|
||||||
},
|
},
|
||||||
assertion: (res: any) => {
|
assertion: (res: any) => {
|
||||||
return res.table1[0].COL1 === stringData.table1[0].col1;
|
return res.table1[0].COL1 === stringData.table1[0].col1
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "Long string value",
|
title: 'Long string value',
|
||||||
description:
|
description:
|
||||||
"Should send an object with a long string value under 32765 characters",
|
'Should send an object with a long string value under 32765 characters',
|
||||||
test: () => {
|
test: () => {
|
||||||
return adapter.request("common/sendObj", getLongStringData());
|
return adapter.request('common/sendObj', getLongStringData())
|
||||||
},
|
},
|
||||||
assertion: (res: any) => {
|
assertion: (res: any) => {
|
||||||
const longStringData = getLongStringData();
|
const longStringData = getLongStringData()
|
||||||
return res.table1[0].COL1 === longStringData.table1[0].col1;
|
return res.table1[0].COL1 === longStringData.table1[0].col1
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "Overly long string value",
|
title: 'Overly long string value',
|
||||||
description:
|
description:
|
||||||
"Should error out with long string values over 32765 characters",
|
'Should error out with long string values over 32765 characters',
|
||||||
test: () => {
|
test: () => {
|
||||||
return adapter
|
return adapter
|
||||||
.request("common/sendObj", getLongStringData(32767))
|
.request('common/sendObj', getLongStringData(32767))
|
||||||
.catch((e) => e);
|
.catch((e) => e)
|
||||||
},
|
},
|
||||||
assertion: (error: any) => {
|
assertion: (error: any) => {
|
||||||
return !!error && !!error.error && !!error.error.message;
|
return !!error && !!error.error && !!error.error.message
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "Single numeric value",
|
title: 'Single numeric value',
|
||||||
description: "Should send an object with a single numeric value",
|
description: 'Should send an object with a single numeric value',
|
||||||
test: () => {
|
test: () => {
|
||||||
return adapter.request("common/sendObj", numericData);
|
return adapter.request('common/sendObj', numericData)
|
||||||
},
|
},
|
||||||
assertion: (res: any) => {
|
assertion: (res: any) => {
|
||||||
return res.table1[0].COL1 === numericData.table1[0].col1;
|
return res.table1[0].COL1 === numericData.table1[0].col1
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
title: "Large data volume",
|
title: 'Large data volume',
|
||||||
description: "Should send an object with a large amount of data",
|
description: 'Should send an object with a large amount of data',
|
||||||
test: () => {
|
test: () => {
|
||||||
return adapter.request("common/sendObj", getLargeObjectData());
|
return adapter.request('common/sendObj', getLargeObjectData())
|
||||||
},
|
},
|
||||||
assertion: (res: any) => {
|
assertion: (res: any) => {
|
||||||
const data = getLargeObjectData();
|
const data = getLargeObjectData()
|
||||||
return res.table1[9000].BIG === data.table1[9000].big;
|
return res.table1[9000].BIG === data.table1[9000].big
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "Multiple columns",
|
title: 'Multiple columns',
|
||||||
description: "Should handle data with multiple columns",
|
description: 'Should handle data with multiple columns',
|
||||||
test: () => {
|
test: () => {
|
||||||
return adapter.request("common/sendObj", multiColumnData);
|
return adapter.request('common/sendObj', multiColumnData)
|
||||||
},
|
},
|
||||||
assertion: (res: any) => {
|
assertion: (res: any) => {
|
||||||
return (
|
return (
|
||||||
@@ -257,62 +256,63 @@ export const sendObjTests = (adapter: SASjs): TestSuite => ({
|
|||||||
res.table1[0].COL2 === multiColumnData.table1[0].col2 &&
|
res.table1[0].COL2 === multiColumnData.table1[0].col2 &&
|
||||||
res.table1[0].COL3 === multiColumnData.table1[0].col3 &&
|
res.table1[0].COL3 === multiColumnData.table1[0].col3 &&
|
||||||
res.table1[0].COL4 === multiColumnData.table1[0].col4
|
res.table1[0].COL4 === multiColumnData.table1[0].col4
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "Multiple rows with nulls",
|
title: 'Multiple rows with nulls',
|
||||||
description: "Should handle data with multiple rows with null values",
|
description: 'Should handle data with multiple rows with null values',
|
||||||
test: () => {
|
test: () => {
|
||||||
return adapter.request("common/sendObj", multipleRowsWithNulls);
|
return adapter.request('common/sendObj', multipleRowsWithNulls)
|
||||||
},
|
},
|
||||||
assertion: (res: any) => {
|
assertion: (res: any) => {
|
||||||
let result = true;
|
let result = true
|
||||||
multipleRowsWithNulls.table1.forEach((_: any, index: number) => {
|
multipleRowsWithNulls.table1.forEach((_: any, index: number) => {
|
||||||
result =
|
result =
|
||||||
result &&
|
result &&
|
||||||
res.table1[index].COL1 === multipleRowsWithNulls.table1[index].col1;
|
res.table1[index].COL1 === multipleRowsWithNulls.table1[index].col1
|
||||||
result =
|
result =
|
||||||
result &&
|
result &&
|
||||||
res.table1[index].COL2 === multipleRowsWithNulls.table1[index].col2;
|
res.table1[index].COL2 === multipleRowsWithNulls.table1[index].col2
|
||||||
result =
|
result =
|
||||||
result &&
|
result &&
|
||||||
res.table1[index].COL3 === multipleRowsWithNulls.table1[index].col3;
|
res.table1[index].COL3 === multipleRowsWithNulls.table1[index].col3
|
||||||
result =
|
result =
|
||||||
result &&
|
result &&
|
||||||
res.table1[index].COL4 === multipleRowsWithNulls.table1[index].col4;
|
res.table1[index].COL4 ===
|
||||||
});
|
(multipleRowsWithNulls.table1[index].col4 || ' ')
|
||||||
return result;
|
})
|
||||||
|
return result
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "Multiple columns with nulls",
|
title: 'Multiple columns with nulls',
|
||||||
description: "Should handle data with multiple columns with null values",
|
description: 'Should handle data with multiple columns with null values',
|
||||||
test: () => {
|
test: () => {
|
||||||
return adapter.request("common/sendObj", multipleColumnsWithNulls);
|
return adapter.request('common/sendObj', multipleColumnsWithNulls)
|
||||||
},
|
},
|
||||||
assertion: (res: any) => {
|
assertion: (res: any) => {
|
||||||
let result = true;
|
let result = true
|
||||||
multipleColumnsWithNulls.table1.forEach((_: any, index: number) => {
|
multipleColumnsWithNulls.table1.forEach((_: any, index: number) => {
|
||||||
result =
|
result =
|
||||||
result &&
|
result &&
|
||||||
res.table1[index].COL1 ===
|
res.table1[index].COL1 ===
|
||||||
multipleColumnsWithNulls.table1[index].col1;
|
multipleColumnsWithNulls.table1[index].col1
|
||||||
result =
|
result =
|
||||||
result &&
|
result &&
|
||||||
res.table1[index].COL2 ===
|
res.table1[index].COL2 ===
|
||||||
multipleColumnsWithNulls.table1[index].col2;
|
multipleColumnsWithNulls.table1[index].col2
|
||||||
result =
|
result =
|
||||||
result &&
|
result &&
|
||||||
res.table1[index].COL3 ===
|
res.table1[index].COL3 ===
|
||||||
multipleColumnsWithNulls.table1[index].col3;
|
multipleColumnsWithNulls.table1[index].col3
|
||||||
result =
|
result =
|
||||||
result &&
|
result &&
|
||||||
res.table1[index].COL4 ===
|
res.table1[index].COL4 ===
|
||||||
(multipleColumnsWithNulls.table1[index].col4 || "");
|
(multipleColumnsWithNulls.table1[index].col4 || ' ')
|
||||||
});
|
})
|
||||||
return result;
|
return result
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
});
|
})
|
||||||
|
|||||||
@@ -1,49 +1,49 @@
|
|||||||
import SASjs from "@sasjs/adapter";
|
import SASjs from '@sasjs/adapter'
|
||||||
import { TestSuite } from "@sasjs/test-framework";
|
import { TestSuite } from '@sasjs/test-framework'
|
||||||
|
|
||||||
const data: any = { table1: [{ col1: "first col value" }] };
|
const data: any = { table1: [{ col1: 'first col value' }] }
|
||||||
|
|
||||||
export const sasjsRequestTests = (adapter: SASjs): TestSuite => ({
|
export const sasjsRequestTests = (adapter: SASjs): TestSuite => ({
|
||||||
name: "SASjs Requests",
|
name: 'SASjs Requests',
|
||||||
tests: [
|
tests: [
|
||||||
{
|
{
|
||||||
title: "WORK tables",
|
title: 'WORK tables',
|
||||||
description: "Should get WORK tables after request",
|
description: 'Should get WORK tables after request',
|
||||||
test: async () => {
|
test: async () => {
|
||||||
return adapter.request("common/sendArr", data);
|
return adapter.request('common/sendArr', data)
|
||||||
},
|
},
|
||||||
assertion: () => {
|
assertion: () => {
|
||||||
const requests = adapter.getSasRequests();
|
const requests = adapter.getSasRequests()
|
||||||
if (adapter.getSasjsConfig().debug) {
|
if (adapter.getSasjsConfig().debug) {
|
||||||
return requests[0].SASWORK !== null;
|
return requests[0].SASWORK !== null
|
||||||
} else {
|
} else {
|
||||||
return requests[0].SASWORK === null;
|
return requests[0].SASWORK === null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "Make error and capture log",
|
title: 'Make error and capture log',
|
||||||
description:
|
description:
|
||||||
"Should make an error and capture log, in the same time it is testing if debug override is working",
|
'Should make an error and capture log, in the same time it is testing if debug override is working',
|
||||||
test: async () => {
|
test: async () => {
|
||||||
return adapter
|
return adapter
|
||||||
.request("common/makeErr", data, { debug: true })
|
.request('common/makeErr', data, { debug: true })
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
const sasRequests = adapter.getSasRequests();
|
const sasRequests = adapter.getSasRequests()
|
||||||
const makeErrRequest: any =
|
const makeErrRequest: any =
|
||||||
sasRequests.find((req) => req.serviceLink.includes("makeErr")) ||
|
sasRequests.find((req) => req.serviceLink.includes('makeErr')) ||
|
||||||
null;
|
null
|
||||||
|
|
||||||
if (!makeErrRequest) return false;
|
if (!makeErrRequest) return false
|
||||||
|
|
||||||
return !!(
|
return !!(
|
||||||
makeErrRequest.logFile && makeErrRequest.logFile.length > 0
|
makeErrRequest.logFile && makeErrRequest.logFile.length > 0
|
||||||
);
|
)
|
||||||
});
|
})
|
||||||
},
|
},
|
||||||
assertion: (response) => {
|
assertion: (response) => {
|
||||||
return response;
|
return response
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
});
|
})
|
||||||
|
|||||||
@@ -1,91 +1,92 @@
|
|||||||
import SASjs from "@sasjs/adapter";
|
import SASjs from '@sasjs/adapter'
|
||||||
import { TestSuite } from "@sasjs/test-framework";
|
import { TestSuite } from '@sasjs/test-framework'
|
||||||
|
|
||||||
const specialCharData: any = {
|
const specialCharData: any = {
|
||||||
table1: [
|
table1: [
|
||||||
{
|
{
|
||||||
tab: "\t",
|
tab: '\t',
|
||||||
lf: "\n",
|
lf: '\n',
|
||||||
cr: "\r",
|
cr: '\r',
|
||||||
semicolon: ";semi",
|
semicolon: ';semi',
|
||||||
percent: "%",
|
percent: '%',
|
||||||
singleQuote: "'",
|
singleQuote: "'",
|
||||||
doubleQuote: '"',
|
doubleQuote: '"',
|
||||||
crlf: "\r\n",
|
crlf: '\r\n',
|
||||||
euro: "€euro",
|
euro: '€euro',
|
||||||
banghash: "!#banghash"
|
banghash: '!#banghash',
|
||||||
|
dot: '.'
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
};
|
}
|
||||||
|
|
||||||
const moreSpecialCharData: any = {
|
const moreSpecialCharData: any = {
|
||||||
table1: [
|
table1: [
|
||||||
{
|
{
|
||||||
speech0: '"speech',
|
speech0: '"speech',
|
||||||
pct: "%percent",
|
pct: '%percent',
|
||||||
speech: '"speech',
|
speech: '"speech',
|
||||||
slash: "\\slash",
|
slash: '\\slash',
|
||||||
slashWithSpecial: "\\\tslash",
|
slashWithSpecial: '\\\tslash',
|
||||||
macvar: "&sysuserid",
|
macvar: '&sysuserid',
|
||||||
chinese: "传/傳chinese",
|
chinese: '传/傳chinese',
|
||||||
sigma: "Σsigma",
|
sigma: 'Σsigma',
|
||||||
at: "@at",
|
at: '@at',
|
||||||
serbian: "Српски",
|
serbian: 'Српски',
|
||||||
dollar: "$"
|
dollar: '$'
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
};
|
}
|
||||||
|
|
||||||
const getWideData = () => {
|
const getWideData = () => {
|
||||||
const cols: any = {};
|
const cols: any = {}
|
||||||
for (let i = 1; i <= 10000; i++) {
|
for (let i = 1; i <= 10000; i++) {
|
||||||
cols["col" + i] = "test" + i;
|
cols['col' + i] = 'test' + i
|
||||||
}
|
}
|
||||||
|
|
||||||
const data: any = {
|
const data: any = {
|
||||||
table1: [cols]
|
table1: [cols]
|
||||||
};
|
}
|
||||||
|
|
||||||
return data;
|
return data
|
||||||
};
|
}
|
||||||
|
|
||||||
const getTables = () => {
|
const getTables = () => {
|
||||||
const tables: any = {};
|
const tables: any = {}
|
||||||
|
|
||||||
for (let i = 1; i <= 100; i++) {
|
for (let i = 1; i <= 100; i++) {
|
||||||
tables["table" + i] = [{ col1: "x", col2: "x", col3: "x", col4: "x" }];
|
tables['table' + i] = [{ col1: 'x', col2: 'x', col3: 'x', col4: 'x' }]
|
||||||
}
|
}
|
||||||
return tables;
|
return tables
|
||||||
};
|
}
|
||||||
|
|
||||||
const getLargeDataset = () => {
|
const getLargeDataset = () => {
|
||||||
const rows: any = [];
|
const rows: any = []
|
||||||
const colData: string =
|
const colData: string =
|
||||||
"xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx";
|
'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx'
|
||||||
for (let i = 1; i <= 10000; i++) {
|
for (let i = 1; i <= 10000; i++) {
|
||||||
rows.push({ col1: colData, col2: colData, col3: colData, col4: colData });
|
rows.push({ col1: colData, col2: colData, col3: colData, col4: colData })
|
||||||
}
|
}
|
||||||
|
|
||||||
const data: any = {
|
const data: any = {
|
||||||
table1: rows
|
table1: rows
|
||||||
};
|
}
|
||||||
|
|
||||||
return data;
|
return data
|
||||||
};
|
}
|
||||||
|
|
||||||
const errorAndCsrfData: any = {
|
const errorAndCsrfData: any = {
|
||||||
error: [{ col1: "q", col2: "w", col3: "e", col4: "r" }],
|
error: [{ col1: 'q', col2: 'w', col3: 'e', col4: 'r' }],
|
||||||
_csrf: [{ col1: "q", col2: "w", col3: "e", col4: "r" }]
|
_csrf: [{ col1: 'q', col2: 'w', col3: 'e', col4: 'r' }]
|
||||||
};
|
}
|
||||||
|
|
||||||
export const specialCaseTests = (adapter: SASjs): TestSuite => ({
|
export const specialCaseTests = (adapter: SASjs): TestSuite => ({
|
||||||
name: "Special Cases",
|
name: 'Special Cases',
|
||||||
tests: [
|
tests: [
|
||||||
{
|
{
|
||||||
title: "Common special characters",
|
title: 'Common special characters',
|
||||||
description: "Should handle common special characters",
|
description: 'Should handle common special characters',
|
||||||
test: () => {
|
test: () => {
|
||||||
return adapter.request("common/sendArr", specialCharData);
|
return adapter.request('common/sendArr', specialCharData)
|
||||||
},
|
},
|
||||||
assertion: (res: any) => {
|
assertion: (res: any) => {
|
||||||
return (
|
return (
|
||||||
@@ -96,17 +97,18 @@ export const specialCaseTests = (adapter: SASjs): TestSuite => ({
|
|||||||
res.table1[0][4] === specialCharData.table1[0].percent &&
|
res.table1[0][4] === specialCharData.table1[0].percent &&
|
||||||
res.table1[0][5] === specialCharData.table1[0].singleQuote &&
|
res.table1[0][5] === specialCharData.table1[0].singleQuote &&
|
||||||
res.table1[0][6] === specialCharData.table1[0].doubleQuote &&
|
res.table1[0][6] === specialCharData.table1[0].doubleQuote &&
|
||||||
res.table1[0][7] === "\n" &&
|
res.table1[0][7] === '\n' &&
|
||||||
res.table1[0][8] === specialCharData.table1[0].euro &&
|
res.table1[0][8] === specialCharData.table1[0].euro &&
|
||||||
res.table1[0][9] === specialCharData.table1[0].banghash
|
res.table1[0][9] === specialCharData.table1[0].banghash &&
|
||||||
);
|
res.table1[0][10] === specialCharData.table1[0].dot
|
||||||
|
)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "Other special characters",
|
title: 'Other special characters',
|
||||||
description: "Should handle other special characters",
|
description: 'Should handle other special characters',
|
||||||
test: () => {
|
test: () => {
|
||||||
return adapter.request("common/sendArr", moreSpecialCharData);
|
return adapter.request('common/sendArr', moreSpecialCharData)
|
||||||
},
|
},
|
||||||
assertion: (res: any) => {
|
assertion: (res: any) => {
|
||||||
return (
|
return (
|
||||||
@@ -121,50 +123,50 @@ export const specialCaseTests = (adapter: SASjs): TestSuite => ({
|
|||||||
res.table1[0][8] === moreSpecialCharData.table1[0].at &&
|
res.table1[0][8] === moreSpecialCharData.table1[0].at &&
|
||||||
res.table1[0][9] === moreSpecialCharData.table1[0].serbian &&
|
res.table1[0][9] === moreSpecialCharData.table1[0].serbian &&
|
||||||
res.table1[0][10] === moreSpecialCharData.table1[0].dollar
|
res.table1[0][10] === moreSpecialCharData.table1[0].dollar
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "Wide table with sendArr",
|
title: 'Wide table with sendArr',
|
||||||
description: "Should handle data with 10000 columns",
|
description: 'Should handle data with 10000 columns',
|
||||||
test: () => {
|
test: () => {
|
||||||
return adapter.request("common/sendArr", getWideData());
|
return adapter.request('common/sendArr', getWideData())
|
||||||
},
|
},
|
||||||
assertion: (res: any) => {
|
assertion: (res: any) => {
|
||||||
const data = getWideData();
|
const data = getWideData()
|
||||||
let result = true;
|
let result = true
|
||||||
for (let i = 0; i <= 10; i++) {
|
for (let i = 0; i <= 10; i++) {
|
||||||
result =
|
result =
|
||||||
result && res.table1[0][i] === data.table1[0]["col" + (i + 1)];
|
result && res.table1[0][i] === data.table1[0]['col' + (i + 1)]
|
||||||
}
|
}
|
||||||
return result;
|
return result
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "Wide table with sendObj",
|
title: 'Wide table with sendObj',
|
||||||
description: "Should handle data with 10000 columns",
|
description: 'Should handle data with 10000 columns',
|
||||||
test: () => {
|
test: () => {
|
||||||
return adapter.request("common/sendObj", getWideData());
|
return adapter.request('common/sendObj', getWideData())
|
||||||
},
|
},
|
||||||
assertion: (res: any) => {
|
assertion: (res: any) => {
|
||||||
const data = getWideData();
|
const data = getWideData()
|
||||||
let result = true;
|
let result = true
|
||||||
for (let i = 0; i <= 10; i++) {
|
for (let i = 0; i <= 10; i++) {
|
||||||
result =
|
result =
|
||||||
result &&
|
result &&
|
||||||
res.table1[0]["COL" + (i + 1)] === data.table1[0]["col" + (i + 1)];
|
res.table1[0]['COL' + (i + 1)] === data.table1[0]['col' + (i + 1)]
|
||||||
}
|
}
|
||||||
return result;
|
return result
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "Multiple tables",
|
title: 'Multiple tables',
|
||||||
description: "Should handle data with 100 tables",
|
description: 'Should handle data with 100 tables',
|
||||||
test: () => {
|
test: () => {
|
||||||
return adapter.request("common/sendArr", getTables());
|
return adapter.request('common/sendArr', getTables())
|
||||||
},
|
},
|
||||||
assertion: (res: any) => {
|
assertion: (res: any) => {
|
||||||
const data = getTables();
|
const data = getTables()
|
||||||
return (
|
return (
|
||||||
res.table1[0][0] === data.table1[0].col1 &&
|
res.table1[0][0] === data.table1[0].col1 &&
|
||||||
res.table1[0][1] === data.table1[0].col2 &&
|
res.table1[0][1] === data.table1[0].col2 &&
|
||||||
@@ -174,45 +176,45 @@ export const specialCaseTests = (adapter: SASjs): TestSuite => ({
|
|||||||
res.table50[0][1] === data.table50[0].col2 &&
|
res.table50[0][1] === data.table50[0].col2 &&
|
||||||
res.table50[0][2] === data.table50[0].col3 &&
|
res.table50[0][2] === data.table50[0].col3 &&
|
||||||
res.table50[0][3] === data.table50[0].col4
|
res.table50[0][3] === data.table50[0].col4
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "Large dataset with sendObj",
|
title: 'Large dataset with sendObj',
|
||||||
description: "Should handle 5mb of data",
|
description: 'Should handle 5mb of data',
|
||||||
test: () => {
|
test: () => {
|
||||||
return adapter.request("common/sendObj", getLargeDataset());
|
return adapter.request('common/sendObj', getLargeDataset())
|
||||||
},
|
},
|
||||||
assertion: (res: any) => {
|
assertion: (res: any) => {
|
||||||
const data = getLargeDataset();
|
const data = getLargeDataset()
|
||||||
let result = true;
|
let result = true
|
||||||
for (let i = 0; i <= 10; i++) {
|
for (let i = 0; i <= 10; i++) {
|
||||||
result = result && res.table1[i][0] === data.table1[i][0];
|
result = result && res.table1[i][0] === data.table1[i][0]
|
||||||
}
|
}
|
||||||
return result;
|
return result
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "Large dataset with sendArr",
|
title: 'Large dataset with sendArr',
|
||||||
description: "Should handle 5mb of data",
|
description: 'Should handle 5mb of data',
|
||||||
test: () => {
|
test: () => {
|
||||||
return adapter.request("common/sendArr", getLargeDataset());
|
return adapter.request('common/sendArr', getLargeDataset())
|
||||||
},
|
},
|
||||||
assertion: (res: any) => {
|
assertion: (res: any) => {
|
||||||
const data = getLargeDataset();
|
const data = getLargeDataset()
|
||||||
let result = true;
|
let result = true
|
||||||
for (let i = 0; i <= 10; i++) {
|
for (let i = 0; i <= 10; i++) {
|
||||||
result =
|
result =
|
||||||
result && res.table1[i][0] === Object.values(data.table1[i])[0];
|
result && res.table1[i][0] === Object.values(data.table1[i])[0]
|
||||||
}
|
}
|
||||||
return result;
|
return result
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "Error and _csrf tables with sendArr",
|
title: 'Error and _csrf tables with sendArr',
|
||||||
description: "Should handle error and _csrf tables",
|
description: 'Should handle error and _csrf tables',
|
||||||
test: () => {
|
test: () => {
|
||||||
return adapter.request("common/sendArr", errorAndCsrfData);
|
return adapter.request('common/sendArr', errorAndCsrfData)
|
||||||
},
|
},
|
||||||
assertion: (res: any) => {
|
assertion: (res: any) => {
|
||||||
return (
|
return (
|
||||||
@@ -224,14 +226,14 @@ export const specialCaseTests = (adapter: SASjs): TestSuite => ({
|
|||||||
res._csrf[0][1] === errorAndCsrfData._csrf[0].col2 &&
|
res._csrf[0][1] === errorAndCsrfData._csrf[0].col2 &&
|
||||||
res._csrf[0][2] === errorAndCsrfData._csrf[0].col3 &&
|
res._csrf[0][2] === errorAndCsrfData._csrf[0].col3 &&
|
||||||
res._csrf[0][3] === errorAndCsrfData._csrf[0].col4
|
res._csrf[0][3] === errorAndCsrfData._csrf[0].col4
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "Error and _csrf tables with sendObj",
|
title: 'Error and _csrf tables with sendObj',
|
||||||
description: "Should handle error and _csrf tables",
|
description: 'Should handle error and _csrf tables',
|
||||||
test: () => {
|
test: () => {
|
||||||
return adapter.request("common/sendObj", errorAndCsrfData);
|
return adapter.request('common/sendObj', errorAndCsrfData)
|
||||||
},
|
},
|
||||||
assertion: (res: any) => {
|
assertion: (res: any) => {
|
||||||
return (
|
return (
|
||||||
@@ -243,8 +245,8 @@ export const specialCaseTests = (adapter: SASjs): TestSuite => ({
|
|||||||
res._csrf[0].COL2 === errorAndCsrfData._csrf[0].col2 &&
|
res._csrf[0].COL2 === errorAndCsrfData._csrf[0].col2 &&
|
||||||
res._csrf[0].COL3 === errorAndCsrfData._csrf[0].col3 &&
|
res._csrf[0].COL3 === errorAndCsrfData._csrf[0].col3 &&
|
||||||
res._csrf[0].COL4 === errorAndCsrfData._csrf[0].col4
|
res._csrf[0].COL4 === errorAndCsrfData._csrf[0].col4
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
});
|
})
|
||||||
|
|||||||
@@ -1,22 +1,22 @@
|
|||||||
export const assert = (
|
export const assert = (
|
||||||
expression: boolean | (() => boolean),
|
expression: boolean | (() => boolean),
|
||||||
message = "Assertion failed"
|
message = 'Assertion failed'
|
||||||
) => {
|
) => {
|
||||||
let result;
|
let result
|
||||||
try {
|
try {
|
||||||
if (typeof expression === "boolean") {
|
if (typeof expression === 'boolean') {
|
||||||
result = expression;
|
result = expression
|
||||||
} else {
|
} else {
|
||||||
result = expression();
|
result = expression()
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(message);
|
console.error(message)
|
||||||
throw new Error(message);
|
throw new Error(message)
|
||||||
}
|
}
|
||||||
if (!!result) {
|
if (!!result) {
|
||||||
return;
|
return
|
||||||
} else {
|
} else {
|
||||||
console.error(message);
|
console.error(message)
|
||||||
throw new Error(message);
|
throw new Error(message)
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|||||||
@@ -314,9 +314,7 @@ export class ContextManager {
|
|||||||
contextId: string,
|
contextId: string,
|
||||||
accessToken?: string
|
accessToken?: string
|
||||||
): Promise<ContextAllAttributes> {
|
): Promise<ContextAllAttributes> {
|
||||||
const {
|
const { result: context } = await this.requestClient
|
||||||
result: context
|
|
||||||
} = await this.requestClient
|
|
||||||
.get<ContextAllAttributes>(
|
.get<ContextAllAttributes>(
|
||||||
`${this.serverUrl}/compute/contexts/${contextId}`,
|
`${this.serverUrl}/compute/contexts/${contextId}`,
|
||||||
accessToken
|
accessToken
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { isUrl } from './utils'
|
import { isUrl } from './utils'
|
||||||
import { UploadFile } from './types/UploadFile'
|
import { UploadFile } from './types/UploadFile'
|
||||||
import { ErrorResponse, LoginRequiredError } from './types'
|
import { ErrorResponse, LoginRequiredError } from './types/errors'
|
||||||
import { RequestClient } from './request/RequestClient'
|
import { RequestClient } from './request/RequestClient'
|
||||||
|
|
||||||
export class FileUploader {
|
export class FileUploader {
|
||||||
|
|||||||
@@ -1,4 +1,10 @@
|
|||||||
import { convertToCSV, isRelativePath, isUri, isUrl } from './utils'
|
import {
|
||||||
|
convertToCSV,
|
||||||
|
isRelativePath,
|
||||||
|
isUri,
|
||||||
|
isUrl,
|
||||||
|
fetchLogByChunks
|
||||||
|
} from './utils'
|
||||||
import * as NodeFormData from 'form-data'
|
import * as NodeFormData from 'form-data'
|
||||||
import {
|
import {
|
||||||
Job,
|
Job,
|
||||||
@@ -8,10 +14,13 @@ import {
|
|||||||
Folder,
|
Folder,
|
||||||
EditContextInput,
|
EditContextInput,
|
||||||
JobDefinition,
|
JobDefinition,
|
||||||
PollOptions,
|
PollOptions
|
||||||
ComputeJobExecutionError,
|
|
||||||
JobExecutionError
|
|
||||||
} from './types'
|
} from './types'
|
||||||
|
import {
|
||||||
|
ComputeJobExecutionError,
|
||||||
|
JobExecutionError,
|
||||||
|
NotFoundError
|
||||||
|
} from './types/errors'
|
||||||
import { formatDataForRequest } from './utils/formatDataForRequest'
|
import { formatDataForRequest } from './utils/formatDataForRequest'
|
||||||
import { SessionManager } from './SessionManager'
|
import { SessionManager } from './SessionManager'
|
||||||
import { ContextManager } from './ContextManager'
|
import { ContextManager } from './ContextManager'
|
||||||
@@ -19,7 +28,6 @@ import { timestampToYYYYMMDDHHMMSS } from '@sasjs/utils/time'
|
|||||||
import { Logger, LogLevel } from '@sasjs/utils/logger'
|
import { Logger, LogLevel } from '@sasjs/utils/logger'
|
||||||
import { isAuthorizeFormRequired } from './auth/isAuthorizeFormRequired'
|
import { isAuthorizeFormRequired } from './auth/isAuthorizeFormRequired'
|
||||||
import { RequestClient } from './request/RequestClient'
|
import { RequestClient } from './request/RequestClient'
|
||||||
import { NotFoundError } from './types/NotFoundError'
|
|
||||||
import { SasAuthResponse } from '@sasjs/utils/types'
|
import { SasAuthResponse } from '@sasjs/utils/types'
|
||||||
import { prefixMessage } from '@sasjs/utils/error'
|
import { prefixMessage } from '@sasjs/utils/error'
|
||||||
|
|
||||||
@@ -404,7 +412,22 @@ export class SASViyaApiClient {
|
|||||||
etag,
|
etag,
|
||||||
accessToken,
|
accessToken,
|
||||||
pollOptions
|
pollOptions
|
||||||
).catch((err) => {
|
).catch(async (err) => {
|
||||||
|
const error = err?.response?.data
|
||||||
|
const result = /err=[0-9]*,/.exec(error)
|
||||||
|
|
||||||
|
const errorCode = '5113'
|
||||||
|
if (result?.[0]?.slice(4, -1) === errorCode) {
|
||||||
|
const sessionLogUrl =
|
||||||
|
postedJob.links.find((l: any) => l.rel === 'up')!.href + '/log'
|
||||||
|
const logCount = 1000000
|
||||||
|
err.log = await fetchLogByChunks(
|
||||||
|
this.requestClient,
|
||||||
|
accessToken!,
|
||||||
|
sessionLogUrl,
|
||||||
|
logCount
|
||||||
|
)
|
||||||
|
}
|
||||||
throw prefixMessage(err, 'Error while polling job status. ')
|
throw prefixMessage(err, 'Error while polling job status. ')
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -418,19 +441,19 @@ export class SASViyaApiClient {
|
|||||||
})
|
})
|
||||||
|
|
||||||
let jobResult
|
let jobResult
|
||||||
let log
|
let log = ''
|
||||||
|
|
||||||
const logLink = currentJob.links.find((l) => l.rel === 'log')
|
const logLink = currentJob.links.find((l) => l.rel === 'log')
|
||||||
|
|
||||||
if (debug && logLink) {
|
if (debug && logLink) {
|
||||||
log = await this.requestClient
|
const logUrl = `${logLink.href}/content`
|
||||||
.get<any>(`${logLink.href}/content?limit=10000`, accessToken)
|
const logCount = currentJob.logStatistics?.lineCount ?? 1000000
|
||||||
.then((res: any) =>
|
log = await fetchLogByChunks(
|
||||||
res.result.items.map((i: any) => i.line).join('\n')
|
this.requestClient,
|
||||||
|
accessToken!,
|
||||||
|
logUrl,
|
||||||
|
logCount
|
||||||
)
|
)
|
||||||
.catch((err) => {
|
|
||||||
throw prefixMessage(err, 'Error while getting log. ')
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (jobStatus === 'failed' || jobStatus === 'error') {
|
if (jobStatus === 'failed' || jobStatus === 'error') {
|
||||||
@@ -451,14 +474,14 @@ export class SASViyaApiClient {
|
|||||||
.catch(async (e) => {
|
.catch(async (e) => {
|
||||||
if (e instanceof NotFoundError) {
|
if (e instanceof NotFoundError) {
|
||||||
if (logLink) {
|
if (logLink) {
|
||||||
log = await this.requestClient
|
const logUrl = `${logLink.href}/content`
|
||||||
.get<any>(`${logLink.href}/content?limit=10000`, accessToken)
|
const logCount = currentJob.logStatistics?.lineCount ?? 1000000
|
||||||
.then((res: any) =>
|
log = await fetchLogByChunks(
|
||||||
res.result.items.map((i: any) => i.line).join('\n')
|
this.requestClient,
|
||||||
|
accessToken!,
|
||||||
|
logUrl,
|
||||||
|
logCount
|
||||||
)
|
)
|
||||||
.catch((err) => {
|
|
||||||
throw prefixMessage(err, 'Error while getting log. ')
|
|
||||||
})
|
|
||||||
|
|
||||||
return Promise.reject({
|
return Promise.reject({
|
||||||
status: 500,
|
status: 500,
|
||||||
@@ -571,9 +594,8 @@ export class SASViyaApiClient {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const {
|
const { result: createFolderResponse } =
|
||||||
result: createFolderResponse
|
await this.requestClient.post<Folder>(
|
||||||
} = await this.requestClient.post<Folder>(
|
|
||||||
`/folders/folders?parentFolderUri=${parentFolderUri}`,
|
`/folders/folders?parentFolderUri=${parentFolderUri}`,
|
||||||
{
|
{
|
||||||
name: folderName,
|
name: folderName,
|
||||||
@@ -852,9 +874,7 @@ export class SASViyaApiClient {
|
|||||||
throw new Error(`URI of job definition was not found.`)
|
throw new Error(`URI of job definition was not found.`)
|
||||||
}
|
}
|
||||||
|
|
||||||
const {
|
const { result: jobDefinition } = await this.requestClient
|
||||||
result: jobDefinition
|
|
||||||
} = await this.requestClient
|
|
||||||
.get<JobDefinition>(
|
.get<JobDefinition>(
|
||||||
`${this.serverUrl}${jobDefinitionLink.href}`,
|
`${this.serverUrl}${jobDefinitionLink.href}`,
|
||||||
accessToken
|
accessToken
|
||||||
@@ -1058,6 +1078,7 @@ export class SASViyaApiClient {
|
|||||||
) {
|
) {
|
||||||
let POLL_INTERVAL = 300
|
let POLL_INTERVAL = 300
|
||||||
let MAX_POLL_COUNT = 1000
|
let MAX_POLL_COUNT = 1000
|
||||||
|
let MAX_ERROR_COUNT = 5
|
||||||
|
|
||||||
if (pollOptions) {
|
if (pollOptions) {
|
||||||
POLL_INTERVAL = pollOptions.POLL_INTERVAL || POLL_INTERVAL
|
POLL_INTERVAL = pollOptions.POLL_INTERVAL || POLL_INTERVAL
|
||||||
@@ -1066,6 +1087,7 @@ export class SASViyaApiClient {
|
|||||||
|
|
||||||
let postedJobState = ''
|
let postedJobState = ''
|
||||||
let pollCount = 0
|
let pollCount = 0
|
||||||
|
let errorCount = 0
|
||||||
const headers: any = {
|
const headers: any = {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
'If-None-Match': etag
|
'If-None-Match': etag
|
||||||
@@ -1080,12 +1102,18 @@ 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=30`,
|
`${this.serverUrl}${stateLink.href}?_action=wait&wait=300`,
|
||||||
accessToken,
|
accessToken,
|
||||||
'text/plain'
|
'text/plain',
|
||||||
|
{},
|
||||||
|
this.debug
|
||||||
)
|
)
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
throw prefixMessage(err, 'Error while getting job state. ')
|
console.error(
|
||||||
|
`Error fetching job state from ${this.serverUrl}${stateLink.href}. Starting poll, assuming job to be running.`,
|
||||||
|
err
|
||||||
|
)
|
||||||
|
return { result: 'unavailable' }
|
||||||
})
|
})
|
||||||
|
|
||||||
const currentState = state.trim()
|
const currentState = state.trim()
|
||||||
@@ -1100,20 +1128,40 @@ export class SASViyaApiClient {
|
|||||||
if (
|
if (
|
||||||
postedJobState === 'running' ||
|
postedJobState === 'running' ||
|
||||||
postedJobState === '' ||
|
postedJobState === '' ||
|
||||||
postedJobState === 'pending'
|
postedJobState === 'pending' ||
|
||||||
|
postedJobState === 'unavailable'
|
||||||
) {
|
) {
|
||||||
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=30`,
|
`${this.serverUrl}${stateLink.href}?_action=wait&wait=300`,
|
||||||
accessToken,
|
accessToken,
|
||||||
'text/plain'
|
'text/plain',
|
||||||
|
{},
|
||||||
|
this.debug
|
||||||
)
|
)
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
throw prefixMessage(err, 'Error while getting job state. ')
|
errorCount++
|
||||||
|
if (
|
||||||
|
pollCount >= MAX_POLL_COUNT ||
|
||||||
|
errorCount >= MAX_ERROR_COUNT
|
||||||
|
) {
|
||||||
|
throw prefixMessage(
|
||||||
|
err,
|
||||||
|
'Error while getting job state after interval. '
|
||||||
|
)
|
||||||
|
}
|
||||||
|
console.error(
|
||||||
|
`Error fetching job state from ${this.serverUrl}${stateLink.href}. Resuming poll, assuming job to be running.`,
|
||||||
|
err
|
||||||
|
)
|
||||||
|
return { result: 'unavailable' }
|
||||||
})
|
})
|
||||||
|
|
||||||
postedJobState = jobState.trim()
|
postedJobState = jobState.trim()
|
||||||
|
if (postedJobState != 'unavailable' && errorCount > 0) {
|
||||||
|
errorCount = 0
|
||||||
|
}
|
||||||
|
|
||||||
if (this.debug && printedState !== postedJobState) {
|
if (this.debug && printedState !== postedJobState) {
|
||||||
console.log('Polling job status...')
|
console.log('Polling job status...')
|
||||||
|
|||||||
49
src/SASjs.ts
49
src/SASjs.ts
@@ -12,6 +12,7 @@ import {
|
|||||||
ComputeJobExecutor,
|
ComputeJobExecutor,
|
||||||
JesJobExecutor
|
JesJobExecutor
|
||||||
} from './job-execution'
|
} from './job-execution'
|
||||||
|
import { ErrorResponse } from './types/errors'
|
||||||
|
|
||||||
const defaultConfig: SASjsConfig = {
|
const defaultConfig: SASjsConfig = {
|
||||||
serverUrl: '',
|
serverUrl: '',
|
||||||
@@ -530,22 +531,29 @@ 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 loginRequiredCallback - provide a function here to be called if the
|
* @param loginRequiredCallback - a function that is called if the
|
||||||
* user is not logged in (eg to display a login form). The request will be
|
* user is not logged in (eg to display a login form). The request will be
|
||||||
* resubmitted after logon.
|
* resubmitted after successful login.
|
||||||
|
* When using a `loginRequiredCallback`, the call to the request will look, for example, like so:
|
||||||
|
* `await request(sasJobPath, data, config, () => setIsLoggedIn(false))`
|
||||||
|
* If you are not passing in any data and configuration, it will look like so:
|
||||||
|
* `await request(sasJobPath, {}, {}, () => setIsLoggedIn(false))`
|
||||||
*/
|
*/
|
||||||
public async request(
|
public async request(
|
||||||
sasJob: string,
|
sasJob: string,
|
||||||
data: any,
|
data: { [key: string]: any },
|
||||||
config: any = {},
|
config: { [key: string]: any } = {},
|
||||||
loginRequiredCallback?: any,
|
loginRequiredCallback?: () => any,
|
||||||
accessToken?: string
|
accessToken?: string
|
||||||
) {
|
) {
|
||||||
config = {
|
config = {
|
||||||
...this.sasjsConfig,
|
...this.sasjsConfig,
|
||||||
...config
|
...config
|
||||||
}
|
}
|
||||||
|
if (
|
||||||
|
typeof loginRequiredCallback === 'function' ||
|
||||||
|
typeof loginRequiredCallback === 'undefined'
|
||||||
|
) {
|
||||||
if (config.serverType === ServerType.SasViya && config.contextName) {
|
if (config.serverType === ServerType.SasViya && config.contextName) {
|
||||||
if (config.useComputeApi) {
|
if (config.useComputeApi) {
|
||||||
return await this.computeJobExecutor!.execute(
|
return await this.computeJobExecutor!.execute(
|
||||||
@@ -572,6 +580,13 @@ export default class SASjs {
|
|||||||
loginRequiredCallback
|
loginRequiredCallback
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
return Promise.reject(
|
||||||
|
new ErrorResponse(
|
||||||
|
`Invalid loginRequiredCallback parameter was provided. Expected Callback function but found ${typeof loginRequiredCallback}`
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -705,9 +720,27 @@ export default class SASjs {
|
|||||||
* @param accessToken - an access token for an authorized user.
|
* @param accessToken - an access token for an authorized user.
|
||||||
*/
|
*/
|
||||||
public async fetchLogFileContent(logUrl: string, accessToken?: string) {
|
public async fetchLogFileContent(logUrl: string, accessToken?: string) {
|
||||||
return await this.requestClient!.get(logUrl, accessToken).then((res) =>
|
return await this.requestClient!.get(logUrl, accessToken).then((res) => {
|
||||||
JSON.stringify(res.result)
|
if (!res)
|
||||||
|
return Promise.reject(
|
||||||
|
new ErrorResponse(
|
||||||
|
'Error while fetching log. Response was not provided.'
|
||||||
)
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = JSON.stringify(res.result)
|
||||||
|
|
||||||
|
return result
|
||||||
|
} catch (err) {
|
||||||
|
return Promise.reject(
|
||||||
|
new ErrorResponse(
|
||||||
|
'Error while fetching log. The result is not valid.',
|
||||||
|
err
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
public getSasRequests() {
|
public getSasRequests() {
|
||||||
|
|||||||
@@ -91,10 +91,7 @@ export class SessionManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async createAndWaitForSession(accessToken?: string) {
|
private async createAndWaitForSession(accessToken?: string) {
|
||||||
const {
|
const { result: createdSession, etag } = await this.requestClient
|
||||||
result: createdSession,
|
|
||||||
etag
|
|
||||||
} = await this.requestClient
|
|
||||||
.post<Session>(
|
.post<Session>(
|
||||||
`${this.serverUrl}/compute/contexts/${
|
`${this.serverUrl}/compute/contexts/${
|
||||||
this.currentContext!.id
|
this.currentContext!.id
|
||||||
|
|||||||
@@ -82,17 +82,33 @@ export class AuthManager {
|
|||||||
* @returns - a promise which resolves with an object containing two values - a boolean `isLoggedIn`, and a string `userName`.
|
* @returns - a promise which resolves with an object containing two values - a boolean `isLoggedIn`, and a string `userName`.
|
||||||
*/
|
*/
|
||||||
public async checkSession() {
|
public async checkSession() {
|
||||||
const { result: loginResponse } = await this.requestClient.get<string>(
|
//For VIYA we will send request on API endpoint. Which is faster then pinging SASJobExecution.
|
||||||
|
//For SAS9 we will send request on SASStoredProcess
|
||||||
|
const url =
|
||||||
|
this.serverType === 'SASVIYA'
|
||||||
|
? `${this.serverUrl}/identities`
|
||||||
|
: `${this.serverUrl}/SASStoredProcess`
|
||||||
|
|
||||||
|
const { result: loginResponse } = await this.requestClient
|
||||||
|
.get<string>(url, undefined, 'text/plain')
|
||||||
|
.catch((err: any) => {
|
||||||
|
return { result: 'authErr' }
|
||||||
|
})
|
||||||
|
|
||||||
|
const isLoggedIn = loginResponse !== 'authErr'
|
||||||
|
let loginForm = null
|
||||||
|
|
||||||
|
if (!isLoggedIn) {
|
||||||
|
//We will logout to make sure cookies are removed and login form is presented
|
||||||
|
this.logOut()
|
||||||
|
|
||||||
|
const { result: formResponse } = await this.requestClient.get<string>(
|
||||||
this.loginUrl.replace('.do', ''),
|
this.loginUrl.replace('.do', ''),
|
||||||
undefined,
|
undefined,
|
||||||
'text/plain'
|
'text/plain'
|
||||||
)
|
)
|
||||||
const responseText = loginResponse
|
|
||||||
const isLoggedIn = /<button.+onClick.+logout/gm.test(responseText)
|
|
||||||
let loginForm: any = null
|
|
||||||
|
|
||||||
if (!isLoggedIn) {
|
loginForm = await this.getLoginForm(formResponse)
|
||||||
loginForm = await this.getLoginForm(responseText)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return Promise.resolve({
|
return Promise.resolve({
|
||||||
|
|||||||
@@ -176,7 +176,10 @@ describe('AuthManager', () => {
|
|||||||
|
|
||||||
const response = await authManager.checkSession()
|
const response = await authManager.checkSession()
|
||||||
expect(response.isLoggedIn).toBeTruthy()
|
expect(response.isLoggedIn).toBeTruthy()
|
||||||
expect(mockedAxios.get).toHaveBeenNthCalledWith(1, `/SASLogon/login`, {
|
expect(mockedAxios.get).toHaveBeenNthCalledWith(
|
||||||
|
1,
|
||||||
|
`http://test-server.com/identities`,
|
||||||
|
{
|
||||||
withCredentials: true,
|
withCredentials: true,
|
||||||
responseType: 'text',
|
responseType: 'text',
|
||||||
transformResponse: undefined,
|
transformResponse: undefined,
|
||||||
@@ -184,33 +187,8 @@ describe('AuthManager', () => {
|
|||||||
Accept: '*/*',
|
Accept: '*/*',
|
||||||
'Content-Type': 'text/plain'
|
'Content-Type': 'text/plain'
|
||||||
}
|
}
|
||||||
})
|
|
||||||
|
|
||||||
done()
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should check and return session information if logged in', async (done) => {
|
|
||||||
const authManager = new AuthManager(
|
|
||||||
serverUrl,
|
|
||||||
serverType,
|
|
||||||
requestClient,
|
|
||||||
authCallback
|
|
||||||
)
|
|
||||||
mockedAxios.get.mockImplementation(() =>
|
|
||||||
Promise.resolve({ data: '<button onClick="logout">' })
|
|
||||||
)
|
|
||||||
|
|
||||||
const response = await authManager.checkSession()
|
|
||||||
expect(response.isLoggedIn).toBeTruthy()
|
|
||||||
expect(mockedAxios.get).toHaveBeenNthCalledWith(1, `/SASLogon/login`, {
|
|
||||||
withCredentials: true,
|
|
||||||
responseType: 'text',
|
|
||||||
transformResponse: undefined,
|
|
||||||
headers: {
|
|
||||||
Accept: '*/*',
|
|
||||||
'Content-Type': 'text/plain'
|
|
||||||
}
|
}
|
||||||
})
|
)
|
||||||
|
|
||||||
done()
|
done()
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import SASjs from './SASjs'
|
import SASjs from './SASjs'
|
||||||
export * from './types'
|
export * from './types'
|
||||||
|
export * from './types/errors'
|
||||||
export * from './SASViyaApiClient'
|
export * from './SASViyaApiClient'
|
||||||
export * from './SAS9ApiClient'
|
export * from './SAS9ApiClient'
|
||||||
export default SASjs
|
export default SASjs
|
||||||
|
|||||||
@@ -1,7 +1,10 @@
|
|||||||
import { ServerType } from '@sasjs/utils/types'
|
import { ServerType } from '@sasjs/utils/types'
|
||||||
import { ErrorResponse } from '..'
|
|
||||||
import { SASViyaApiClient } from '../SASViyaApiClient'
|
import { SASViyaApiClient } from '../SASViyaApiClient'
|
||||||
import { ComputeJobExecutionError, LoginRequiredError } from '../types'
|
import {
|
||||||
|
ErrorResponse,
|
||||||
|
ComputeJobExecutionError,
|
||||||
|
LoginRequiredError
|
||||||
|
} from '../types/errors'
|
||||||
import { BaseJobExecutor } from './JobExecutor'
|
import { BaseJobExecutor } from './JobExecutor'
|
||||||
|
|
||||||
export class ComputeJobExecutor extends BaseJobExecutor {
|
export class ComputeJobExecutor extends BaseJobExecutor {
|
||||||
|
|||||||
@@ -1,7 +1,10 @@
|
|||||||
import { ServerType } from '@sasjs/utils/types'
|
import { ServerType } from '@sasjs/utils/types'
|
||||||
import { ErrorResponse } from '..'
|
|
||||||
import { SASViyaApiClient } from '../SASViyaApiClient'
|
import { SASViyaApiClient } from '../SASViyaApiClient'
|
||||||
import { JobExecutionError, LoginRequiredError } from '../types'
|
import {
|
||||||
|
ErrorResponse,
|
||||||
|
JobExecutionError,
|
||||||
|
LoginRequiredError
|
||||||
|
} from '../types/errors'
|
||||||
import { BaseJobExecutor } from './JobExecutor'
|
import { BaseJobExecutor } from './JobExecutor'
|
||||||
|
|
||||||
export class JesJobExecutor extends BaseJobExecutor {
|
export class JesJobExecutor extends BaseJobExecutor {
|
||||||
@@ -30,7 +33,7 @@ export class JesJobExecutor extends BaseJobExecutor {
|
|||||||
.then((response) => {
|
.then((response) => {
|
||||||
this.appendRequest(response, sasJob, config.debug)
|
this.appendRequest(response, sasJob, config.debug)
|
||||||
|
|
||||||
resolve(response.result)
|
resolve(response)
|
||||||
})
|
})
|
||||||
.catch(async (e: Error) => {
|
.catch(async (e: Error) => {
|
||||||
if (e instanceof JobExecutionError) {
|
if (e instanceof JobExecutionError) {
|
||||||
|
|||||||
@@ -1,5 +1,9 @@
|
|||||||
import { ServerType } from '@sasjs/utils/types'
|
import { ServerType } from '@sasjs/utils/types'
|
||||||
import { ErrorResponse, JobExecutionError, LoginRequiredError } from '..'
|
import {
|
||||||
|
ErrorResponse,
|
||||||
|
JobExecutionError,
|
||||||
|
LoginRequiredError
|
||||||
|
} from '../types/errors'
|
||||||
import { generateFileUploadForm } from '../file/generateFileUploadForm'
|
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'
|
||||||
@@ -67,10 +71,8 @@ export class WebJobExecutor extends BaseJobExecutor {
|
|||||||
} else {
|
} else {
|
||||||
// param based approach
|
// param based approach
|
||||||
try {
|
try {
|
||||||
const {
|
const { formData: newFormData, requestParams: params } =
|
||||||
formData: newFormData,
|
generateTableUploadForm(formData, data)
|
||||||
requestParams: params
|
|
||||||
} = generateTableUploadForm(formData, data)
|
|
||||||
formData = newFormData
|
formData = newFormData
|
||||||
requestParams = { ...requestParams, ...params }
|
requestParams = { ...requestParams, ...params }
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|||||||
@@ -1,9 +1,13 @@
|
|||||||
import axios, { AxiosInstance, AxiosRequestConfig, AxiosResponse } from 'axios'
|
import axios, { AxiosInstance, AxiosRequestConfig, AxiosResponse } from 'axios'
|
||||||
import { CsrfToken, JobExecutionError } from '..'
|
import { CsrfToken } from '..'
|
||||||
import { isAuthorizeFormRequired, isLogInRequired } from '../auth'
|
import { isAuthorizeFormRequired, isLogInRequired } from '../auth'
|
||||||
import { LoginRequiredError } from '../types'
|
import {
|
||||||
import { AuthorizeError } from '../types/AuthorizeError'
|
AuthorizeError,
|
||||||
import { NotFoundError } from '../types/NotFoundError'
|
LoginRequiredError,
|
||||||
|
NotFoundError,
|
||||||
|
InternalServerError,
|
||||||
|
JobExecutionError
|
||||||
|
} 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'
|
||||||
|
|
||||||
@@ -73,7 +77,8 @@ export class RequestClient implements HttpClient {
|
|||||||
url: string,
|
url: string,
|
||||||
accessToken: string | undefined,
|
accessToken: string | undefined,
|
||||||
contentType: string = 'application/json',
|
contentType: string = 'application/json',
|
||||||
overrideHeaders: { [key: string]: string | number } = {}
|
overrideHeaders: { [key: string]: string | number } = {},
|
||||||
|
debug: boolean = false
|
||||||
): Promise<{ result: T; etag: string }> {
|
): Promise<{ result: T; etag: string }> {
|
||||||
const headers = {
|
const headers = {
|
||||||
...this.getHeaders(accessToken, contentType),
|
...this.getHeaders(accessToken, contentType),
|
||||||
@@ -93,10 +98,13 @@ export class RequestClient implements HttpClient {
|
|||||||
.get<T>(url, requestConfig)
|
.get<T>(url, requestConfig)
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
throwIfError(response)
|
throwIfError(response)
|
||||||
|
|
||||||
return this.parseResponse<T>(response)
|
return this.parseResponse<T>(response)
|
||||||
})
|
})
|
||||||
.catch(async (e) => {
|
.catch(async (e) => {
|
||||||
return await this.handleError(e, () =>
|
return await this.handleError(
|
||||||
|
e,
|
||||||
|
() =>
|
||||||
this.get<T>(url, accessToken, contentType, overrideHeaders).catch(
|
this.get<T>(url, accessToken, contentType, overrideHeaders).catch(
|
||||||
(err) => {
|
(err) => {
|
||||||
throw prefixMessage(
|
throw prefixMessage(
|
||||||
@@ -104,7 +112,8 @@ export class RequestClient implements HttpClient {
|
|||||||
'Error while executing handle error callback. '
|
'Error while executing handle error callback. '
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
)
|
),
|
||||||
|
debug
|
||||||
).catch((err) => {
|
).catch((err) => {
|
||||||
throw prefixMessage(err, 'Error while handling error. ')
|
throw prefixMessage(err, 'Error while handling error. ')
|
||||||
})
|
})
|
||||||
@@ -205,9 +214,8 @@ export class RequestClient implements HttpClient {
|
|||||||
const headers = this.getHeaders(accessToken, 'application/json')
|
const headers = this.getHeaders(accessToken, 'application/json')
|
||||||
|
|
||||||
if (this.fileUploadCsrfToken?.value) {
|
if (this.fileUploadCsrfToken?.value) {
|
||||||
headers[
|
headers[this.fileUploadCsrfToken.headerName] =
|
||||||
this.fileUploadCsrfToken.headerName
|
this.fileUploadCsrfToken.value
|
||||||
] = this.fileUploadCsrfToken.value
|
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -324,9 +332,9 @@ export class RequestClient implements HttpClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private parseCsrfToken = (response: AxiosResponse): CsrfToken | undefined => {
|
private parseCsrfToken = (response: AxiosResponse): CsrfToken | undefined => {
|
||||||
const tokenHeader = (response.headers[
|
const tokenHeader = (
|
||||||
'x-csrf-header'
|
response.headers['x-csrf-header'] as string
|
||||||
] as string)?.toLowerCase()
|
)?.toLowerCase()
|
||||||
|
|
||||||
if (tokenHeader) {
|
if (tokenHeader) {
|
||||||
const token = response.headers[tokenHeader]
|
const token = response.headers[tokenHeader]
|
||||||
@@ -339,7 +347,11 @@ export class RequestClient implements HttpClient {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private handleError = async (e: any, callback: any) => {
|
private handleError = async (
|
||||||
|
e: any,
|
||||||
|
callback: any,
|
||||||
|
debug: boolean = false
|
||||||
|
) => {
|
||||||
const response = e.response as AxiosResponse
|
const response = e.response as AxiosResponse
|
||||||
|
|
||||||
if (e instanceof AuthorizeError) {
|
if (e instanceof AuthorizeError) {
|
||||||
@@ -385,6 +397,9 @@ export class RequestClient implements HttpClient {
|
|||||||
throw e
|
throw e
|
||||||
} else if (response?.status === 404) {
|
} else if (response?.status === 404) {
|
||||||
throw new NotFoundError(response.config.url!)
|
throw new NotFoundError(response.config.url!)
|
||||||
|
} else if (response?.status === 502) {
|
||||||
|
if (debug) throw new InternalServerError()
|
||||||
|
else return
|
||||||
}
|
}
|
||||||
|
|
||||||
throw e
|
throw e
|
||||||
@@ -456,6 +471,7 @@ const throwIfError = (response: AxiosResponse) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const error = parseError(response.data as string)
|
const error = parseError(response.data as string)
|
||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
throw error
|
throw error
|
||||||
}
|
}
|
||||||
|
|||||||
39
src/test/utils/isUrl.spec.ts
Normal file
39
src/test/utils/isUrl.spec.ts
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
import { isUrl } from '../../utils/isUrl'
|
||||||
|
|
||||||
|
describe('urlValidator', () => {
|
||||||
|
it('should return true with an HTTP URL', () => {
|
||||||
|
const url = 'http://google.com'
|
||||||
|
|
||||||
|
expect(isUrl(url)).toEqual(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should return true with an HTTPS URL', () => {
|
||||||
|
const url = 'https://google.com'
|
||||||
|
|
||||||
|
expect(isUrl(url)).toEqual(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should return true when the URL is blank', () => {
|
||||||
|
const url = ''
|
||||||
|
|
||||||
|
expect(isUrl(url)).toEqual(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should return false when the URL has not supported protocol', () => {
|
||||||
|
const url = 'htpps://google.com'
|
||||||
|
|
||||||
|
expect(isUrl(url)).toEqual(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should return false when the URL is null', () => {
|
||||||
|
const url = null
|
||||||
|
|
||||||
|
expect(isUrl(url as unknown as string)).toEqual(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should return false when the URL is undefined', () => {
|
||||||
|
const url = undefined
|
||||||
|
|
||||||
|
expect(isUrl(url as unknown as string)).toEqual(false)
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
import { Link } from './Link'
|
import { Link } from './Link'
|
||||||
import { JobResult } from './JobResult'
|
import { JobResult } from './JobResult'
|
||||||
|
import { LogStatistics } from './LogStatistics'
|
||||||
|
|
||||||
export interface Job {
|
export interface Job {
|
||||||
id: string
|
id: string
|
||||||
@@ -10,4 +11,5 @@ export interface Job {
|
|||||||
links: Link[]
|
links: Link[]
|
||||||
results: JobResult
|
results: JobResult
|
||||||
error?: any
|
error?: any
|
||||||
|
logStatistics: LogStatistics
|
||||||
}
|
}
|
||||||
|
|||||||
4
src/types/LogStatistics.ts
Normal file
4
src/types/LogStatistics.ts
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
export interface LogStatistics {
|
||||||
|
lineCount: number
|
||||||
|
modifiedTimeStamp: string
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Job } from './Job'
|
import { Job } from '../Job'
|
||||||
|
|
||||||
export class ComputeJobExecutionError extends Error {
|
export class ComputeJobExecutionError extends Error {
|
||||||
constructor(public job: Job, public log: string) {
|
constructor(public job: Job, public log: string) {
|
||||||
9
src/types/errors/InternalServerError.ts
Normal file
9
src/types/errors/InternalServerError.ts
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
export class InternalServerError extends Error {
|
||||||
|
constructor() {
|
||||||
|
super('Error: Internal server error.')
|
||||||
|
|
||||||
|
this.name = 'InternalServerError'
|
||||||
|
|
||||||
|
Object.setPrototypeOf(this, InternalServerError.prototype)
|
||||||
|
}
|
||||||
|
}
|
||||||
7
src/types/errors/index.ts
Normal file
7
src/types/errors/index.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
export * from './AuthorizeError'
|
||||||
|
export * from './ComputeJobExecutionError'
|
||||||
|
export * from './InternalServerError'
|
||||||
|
export * from './JobExecutionError'
|
||||||
|
export * from './LoginRequiredError'
|
||||||
|
export * from './NotFoundError'
|
||||||
|
export * from './ErrorResponse'
|
||||||
@@ -1,14 +1,10 @@
|
|||||||
export * from './ComputeJobExecutionError'
|
|
||||||
export * from './Context'
|
export * from './Context'
|
||||||
export * from './CsrfToken'
|
export * from './CsrfToken'
|
||||||
export * from './ErrorResponse'
|
|
||||||
export * from './Folder'
|
export * from './Folder'
|
||||||
export * from './Job'
|
export * from './Job'
|
||||||
export * from './JobExecutionError'
|
|
||||||
export * from './JobDefinition'
|
export * from './JobDefinition'
|
||||||
export * from './JobResult'
|
export * from './JobResult'
|
||||||
export * from './Link'
|
export * from './Link'
|
||||||
export * from './LoginRequiredError'
|
|
||||||
export * from './SASjsConfig'
|
export * from './SASjsConfig'
|
||||||
export * from './SASjsRequest'
|
export * from './SASjsRequest'
|
||||||
export * from './Session'
|
export * from './Session'
|
||||||
|
|||||||
170
src/utils/convertToCsv.spec.ts
Normal file
170
src/utils/convertToCsv.spec.ts
Normal file
@@ -0,0 +1,170 @@
|
|||||||
|
import { convertToCSV } from './convertToCsv'
|
||||||
|
|
||||||
|
describe('convertToCsv', () => {
|
||||||
|
it('should convert single quoted values', () => {
|
||||||
|
const data = [
|
||||||
|
{ foo: `'bar'`, bar: 'abc' },
|
||||||
|
{ foo: 'sadf', bar: 'def' },
|
||||||
|
{ foo: 'asd', bar: `'qwert'` }
|
||||||
|
]
|
||||||
|
|
||||||
|
const expectedOutput = `foo:$char5. bar:$char7.\r\n"'bar'",abc\r\nsadf,def\r\nasd,"'qwert'"`
|
||||||
|
|
||||||
|
expect(convertToCSV(data)).toEqual(expectedOutput)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should convert double quoted values', () => {
|
||||||
|
const data = [
|
||||||
|
{ foo: `"bar"`, bar: 'abc' },
|
||||||
|
{ foo: 'sadf', bar: 'def' },
|
||||||
|
{ foo: 'asd', bar: `"qwert"` }
|
||||||
|
]
|
||||||
|
|
||||||
|
const expectedOutput = `foo:$char5. bar:$char7.\r\n"""bar""",abc\r\nsadf,def\r\nasd,"""qwert"""`
|
||||||
|
|
||||||
|
expect(convertToCSV(data)).toEqual(expectedOutput)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should convert values with mixed quotes', () => {
|
||||||
|
const data = [{ foo: `'blah'`, bar: `"blah"` }]
|
||||||
|
|
||||||
|
const expectedOutput = `foo:$char6. bar:$char6.\r\n"'blah'","""blah"""`
|
||||||
|
|
||||||
|
expect(convertToCSV(data)).toEqual(expectedOutput)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should convert values with mixed quotes', () => {
|
||||||
|
const data = [{ foo: `'blah,"'`, bar: `"blah,blah" "` }]
|
||||||
|
|
||||||
|
const expectedOutput = `foo:$char8. bar:$char13.\r\n"'blah,""'","""blah,blah"" """`
|
||||||
|
|
||||||
|
expect(convertToCSV(data)).toEqual(expectedOutput)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should convert values with mixed quotes', () => {
|
||||||
|
const data = [{ foo: `',''`, bar: `","` }]
|
||||||
|
|
||||||
|
const expectedOutput = `foo:$char4. bar:$char3.\r\n"',''",""","""`
|
||||||
|
|
||||||
|
expect(convertToCSV(data)).toEqual(expectedOutput)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should convert values with mixed quotes', () => {
|
||||||
|
const data = [{ foo: `','`, bar: `,"` }]
|
||||||
|
|
||||||
|
const expectedOutput = `foo:$char3. bar:$char2.\r\n"','",","""`
|
||||||
|
|
||||||
|
expect(convertToCSV(data)).toEqual(expectedOutput)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should convert values with mixed quotes', () => {
|
||||||
|
const data = [{ foo: `"`, bar: `'` }]
|
||||||
|
|
||||||
|
const expectedOutput = `foo:$char1. bar:$char1.\r\n"""","'"`
|
||||||
|
|
||||||
|
expect(convertToCSV(data)).toEqual(expectedOutput)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should convert values with mixed quotes', () => {
|
||||||
|
const data = [{ foo: `,`, bar: `',` }]
|
||||||
|
|
||||||
|
const expectedOutput = `foo:$char1. bar:$char2.\r\n",","',"`
|
||||||
|
|
||||||
|
expect(convertToCSV(data)).toEqual(expectedOutput)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should convert values with number cases 1', () => {
|
||||||
|
const data = [
|
||||||
|
{ col1: 42, col2: null, col3: 'x', col4: null },
|
||||||
|
{ col1: 42, col2: null, col3: 'x', col4: null },
|
||||||
|
{ col1: 42, col2: null, col3: 'x', col4: null },
|
||||||
|
{ col1: 42, col2: null, col3: 'x', col4: '' },
|
||||||
|
{ col1: 42, col2: null, col3: 'x', col4: '' }
|
||||||
|
]
|
||||||
|
|
||||||
|
const expectedOutput = `col1:best. col2:best. col3:$char1. col4:$char1.\r\n42,.,x,\r\n42,.,x,\r\n42,.,x,\r\n42,.,x,\r\n42,.,x,`
|
||||||
|
|
||||||
|
expect(convertToCSV(data)).toEqual(expectedOutput)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should convert values with number cases 2', () => {
|
||||||
|
const data = [
|
||||||
|
{ col1: 42, col2: null, col3: 'x', col4: '' },
|
||||||
|
{ col1: 42, col2: null, col3: 'x', col4: '' },
|
||||||
|
{ col1: 42, col2: null, col3: 'x', col4: '' },
|
||||||
|
{ col1: 42, col2: 1.62, col3: 'x', col4: 'x' },
|
||||||
|
{ col1: 42, col2: 1.62, col3: 'x', col4: 'x' }
|
||||||
|
]
|
||||||
|
|
||||||
|
const expectedOutput = `col1:best. col2:best. col3:$char1. col4:$char1.\r\n42,.,x,\r\n42,.,x,\r\n42,.,x,\r\n42,1.62,x,x\r\n42,1.62,x,x`
|
||||||
|
|
||||||
|
expect(convertToCSV(data)).toEqual(expectedOutput)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should convert values with common special characters', () => {
|
||||||
|
expect(convertToCSV([{ tab: '\t' }])).toEqual(`tab:$char1.\r\n\"\t\"`)
|
||||||
|
expect(convertToCSV([{ lf: '\n' }])).toEqual(`lf:$char1.\r\n\"\n\"`)
|
||||||
|
expect(convertToCSV([{ semicolon: ';semi' }])).toEqual(
|
||||||
|
`semicolon:$char5.\r\n;semi`
|
||||||
|
)
|
||||||
|
expect(convertToCSV([{ percent: '%' }])).toEqual(`percent:$char1.\r\n%`)
|
||||||
|
expect(convertToCSV([{ singleQuote: "'" }])).toEqual(
|
||||||
|
`singleQuote:$char1.\r\n\"'\"`
|
||||||
|
)
|
||||||
|
expect(convertToCSV([{ doubleQuote: '"' }])).toEqual(
|
||||||
|
`doubleQuote:$char1.\r\n""""`
|
||||||
|
)
|
||||||
|
expect(convertToCSV([{ crlf: '\r\n' }])).toEqual(`crlf:$char2.\r\n\"\n\"`)
|
||||||
|
expect(convertToCSV([{ euro: '€euro' }])).toEqual(`euro:$char7.\r\n€euro`)
|
||||||
|
expect(convertToCSV([{ banghash: '!#banghash' }])).toEqual(
|
||||||
|
`banghash:$char10.\r\n!#banghash`
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should convert values with other special characters', () => {
|
||||||
|
const data = [
|
||||||
|
{
|
||||||
|
speech0: '"speech',
|
||||||
|
pct: '%percent',
|
||||||
|
speech: '"speech',
|
||||||
|
slash: '\\slash',
|
||||||
|
slashWithSpecial: '\\\tslash',
|
||||||
|
macvar: '&sysuserid',
|
||||||
|
chinese: '传/傳chinese',
|
||||||
|
sigma: 'Σsigma',
|
||||||
|
at: '@at',
|
||||||
|
serbian: 'Српски',
|
||||||
|
dollar: '$'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
const expectedOutput = `speech0:$char7. pct:$char8. speech:$char7. slash:$char6. slashWithSpecial:$char7. macvar:$char10. chinese:$char14. sigma:$char7. at:$char3. serbian:$char12. dollar:$char1.\r\n"""speech",%percent,"""speech",\\slash,\"\\\tslash\",&sysuserid,传/傳chinese,Σsigma,@at,Српски,$`
|
||||||
|
|
||||||
|
expect(convertToCSV(data)).toEqual(expectedOutput)
|
||||||
|
|
||||||
|
expect(convertToCSV([{ speech: 'menext' }])).toEqual(
|
||||||
|
`speech:$char6.\r\nmenext`
|
||||||
|
)
|
||||||
|
expect(convertToCSV([{ speech: 'me\nnext' }])).toEqual(
|
||||||
|
`speech:$char7.\r\n\"me\nnext\"`
|
||||||
|
)
|
||||||
|
expect(convertToCSV([{ speech: `me'next` }])).toEqual(
|
||||||
|
`speech:$char7.\r\n\"me'next\"`
|
||||||
|
)
|
||||||
|
expect(convertToCSV([{ speech: `me"next` }])).toEqual(
|
||||||
|
`speech:$char7.\r\n\"me""next\"`
|
||||||
|
)
|
||||||
|
expect(convertToCSV([{ speech: `me""next` }])).toEqual(
|
||||||
|
`speech:$char8.\r\n\"me""""next\"`
|
||||||
|
)
|
||||||
|
expect(convertToCSV([{ slashWithSpecial: '\\\tslash' }])).toEqual(
|
||||||
|
`slashWithSpecial:$char7.\r\n\"\\\tslash\"`
|
||||||
|
)
|
||||||
|
expect(convertToCSV([{ slashWithSpecial: '\\ \tslash' }])).toEqual(
|
||||||
|
`slashWithSpecial:$char8.\r\n\"\\ \tslash\"`
|
||||||
|
)
|
||||||
|
expect(
|
||||||
|
convertToCSV([{ slashWithSpecialExtra: '\\\ts\tl\ta\ts\t\th\t' }])
|
||||||
|
).toEqual(`slashWithSpecialExtra:$char13.\r\n\"\\\ts\tl\ta\ts\t\th\t\"`)
|
||||||
|
})
|
||||||
|
})
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user