mirror of
https://github.com/sasjs/adapter.git
synced 2025-12-11 09:24:35 +00:00
Compare commits
1 Commits
v3.10.3
...
error-body
| Author | SHA1 | Date | |
|---|---|---|---|
| a54df1e2cb |
@@ -1,12 +0,0 @@
|
||||
#!/bin/sh
|
||||
|
||||
# Using `--silent` helps for showing any errs in the first line of the response
|
||||
# The first line is picked up by the VS Code GIT UI popup when rc is not 0
|
||||
|
||||
if npm run --silent lint:silent ; then
|
||||
exit 0
|
||||
else
|
||||
npm run --silent lint:fix
|
||||
echo "❌ Prettier check failed! We ran lint:fix for you. Please add & commit again."
|
||||
exit 1
|
||||
fi
|
||||
6
.github/dependabot.yml
vendored
6
.github/dependabot.yml
vendored
@@ -1,7 +1,7 @@
|
||||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: "npm"
|
||||
directory: "/"
|
||||
- package-ecosystem: npm
|
||||
directory: '/'
|
||||
schedule:
|
||||
interval: "monthly"
|
||||
interval: monthly
|
||||
open-pull-requests-limit: 2
|
||||
|
||||
30
.github/vpn/config.ovpn
vendored
30
.github/vpn/config.ovpn
vendored
@@ -1,30 +0,0 @@
|
||||
cipher AES-256-CBC
|
||||
setenv FORWARD_COMPATIBLE 1
|
||||
client
|
||||
server-poll-timeout 4
|
||||
nobind
|
||||
remote vpn.analytium.co.uk 1194 udp
|
||||
remote vpn.analytium.co.uk 1194 udp
|
||||
remote vpn.analytium.co.uk 443 tcp
|
||||
remote vpn.analytium.co.uk 1194 udp
|
||||
remote vpn.analytium.co.uk 1194 udp
|
||||
remote vpn.analytium.co.uk 1194 udp
|
||||
remote vpn.analytium.co.uk 1194 udp
|
||||
remote vpn.analytium.co.uk 1194 udp
|
||||
dev tun
|
||||
dev-type tun
|
||||
ns-cert-type server
|
||||
setenv opt tls-version-min 1.0 or-highest
|
||||
reneg-sec 604800
|
||||
sndbuf 0
|
||||
rcvbuf 0
|
||||
# NOTE: LZO commands are pushed by the Access Server at connect time.
|
||||
# NOTE: The below line doesn't disable LZO.
|
||||
comp-lzo no
|
||||
verb 3
|
||||
setenv PUSH_PEER_INFO
|
||||
|
||||
ca ca.crt
|
||||
cert user.crt
|
||||
key user.key
|
||||
tls-auth tls.key 1
|
||||
67
.github/workflows/build.yml
vendored
67
.github/workflows/build.yml
vendored
@@ -4,6 +4,7 @@
|
||||
name: SASjs Build
|
||||
|
||||
on:
|
||||
push:
|
||||
pull_request:
|
||||
|
||||
jobs:
|
||||
@@ -21,77 +22,19 @@ jobs:
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
cache: npm
|
||||
|
||||
- name: Check npm audit
|
||||
run: npm audit --production --audit-level=low
|
||||
|
||||
- name: Install Dependencies
|
||||
run: npm ci
|
||||
|
||||
- name: Check code style
|
||||
run: npm run lint
|
||||
|
||||
- name: Run unit tests
|
||||
run: npm test
|
||||
|
||||
- name: Build Package
|
||||
run: npm run package:lib
|
||||
env:
|
||||
CI: true
|
||||
|
||||
- name: Install SSH Key
|
||||
uses: shimataro/ssh-key-action@v2
|
||||
with:
|
||||
key: ${{ secrets.DCGITLAB_KEY }}
|
||||
known_hosts: 'placeholder'
|
||||
|
||||
- name: Write VPN Files
|
||||
run: |
|
||||
echo "$CA_CRT" > .github/vpn/ca.crt
|
||||
echo "$USER_CRT" > .github/vpn/user.crt
|
||||
echo "$USER_KEY" > .github/vpn/user.key
|
||||
echo "$TLS_KEY" > .github/vpn/tls.key
|
||||
shell: bash
|
||||
env:
|
||||
CA_CRT: ${{ secrets.CA_CRT}}
|
||||
USER_CRT: ${{ secrets.USER_CRT }}
|
||||
USER_KEY: ${{ secrets.USER_KEY }}
|
||||
TLS_KEY: ${{ secrets.TLS_KEY }}
|
||||
|
||||
- name: Install Open VPN
|
||||
run: |
|
||||
sudo apt install apt-transport-https
|
||||
sudo wget https://swupdate.openvpn.net/repos/openvpn-repo-pkg-key.pub
|
||||
sudo apt-key add openvpn-repo-pkg-key.pub
|
||||
sudo wget -O /etc/apt/sources.list.d/openvpn3.list https://swupdate.openvpn.net/community/openvpn3/repos/openvpn3-focal.list
|
||||
sudo apt update
|
||||
sudo apt install openvpn3=16~beta+focal
|
||||
|
||||
- name: Start Open VPN 3
|
||||
run: openvpn3 session-start --config .github/vpn/config.ovpn
|
||||
|
||||
- name: Deploy sasjs-tests
|
||||
run: |
|
||||
npm install -g replace-in-files-cli
|
||||
cd sasjs-tests
|
||||
replace-in-files --regex='"@sasjs/adapter".*' --replacement='"@sasjs/adapter":"latest",' ./package.json
|
||||
npm i
|
||||
replace-in-files --regex='"serverUrl".*' --replacement='"serverUrl":"${{ secrets.SASJS_SERVER_URL }}",' ./public/config.json
|
||||
replace-in-files --regex='"userName".*' --replacement='"userName":"${{ secrets.SASJS_USERNAME }}",' ./public/config.json
|
||||
replace-in-files --regex='"password".*' --replacement='"password":"${{ secrets.SASJS_PASSWORD }}",' ./public/config.json
|
||||
replace-in-files --regex='"serverType".*' --replacement='"serverType":"SASJS",' ./public/config.json
|
||||
npm run update:adapter && npm run build
|
||||
scp -o stricthostkeychecking=no -r ./build/* ${{ secrets.DCGITLAB_DEPLOY_PATH_VIYA }}
|
||||
|
||||
- name: Run cypress on sasjs
|
||||
run: |
|
||||
replace-in-files --regex='"sasjsTestsUrl".*' --replacement='"sasjsTestsUrl":"${{ secrets.SASJS_TEST_URL_VIYA }}",' ./cypress.json
|
||||
replace-in-files --regex='"username".*' --replacement='"username":"${{ secrets.SASJS_USERNAME }}",' ./cypress.json
|
||||
replace-in-files --regex='"password".*' --replacement='"password":"${{ secrets.SASJS_PASSWORD }}",' ./cypress.json
|
||||
sh ./sasjs-cypress-run.sh ${{ secrets.DISCORD_WEBHOOK }} https://github.com/${{github.repository}}/actions/runs/${{github.run_id}}
|
||||
|
||||
# For some reason if coverage report action is run before other commands, those commands can't access the directories and files on which they depend on
|
||||
- name: Generate coverage report
|
||||
uses: artiomtr/jest-coverage-report-action@v2.0-rc.2
|
||||
with:
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: Build Package
|
||||
run: npm run package:lib
|
||||
env:
|
||||
CI: true
|
||||
|
||||
17
.github/workflows/npmpublish.yml
vendored
17
.github/workflows/npmpublish.yml
vendored
@@ -11,28 +11,15 @@ on:
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
node-version: [lts/fermium]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Use Node.js ${{ matrix.node-version }}
|
||||
uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
cache: npm
|
||||
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
- name: Install Dependencies
|
||||
run: npm ci
|
||||
|
||||
- name: Check code style
|
||||
run: npm run lint
|
||||
|
||||
- name: Build Project
|
||||
run: npm run build
|
||||
|
||||
- name: Semantic Release
|
||||
uses: cycjimmy/semantic-release-action@v2
|
||||
env:
|
||||
|
||||
10
.npmignore
10
.npmignore
@@ -4,13 +4,3 @@ docs/
|
||||
*.md
|
||||
*.spec.ts
|
||||
.all-contributorsrc
|
||||
cypress/
|
||||
.gitpod.yml
|
||||
.prettierrc
|
||||
cypress.json
|
||||
jest.config.js
|
||||
sasjs-cypress-run.sh
|
||||
tsconfig.json
|
||||
tslint.json
|
||||
typedoc.json
|
||||
webpack.config.js
|
||||
|
||||
@@ -16,5 +16,5 @@ No PR (that involves a non-trivial code change) should be merged, unless all ite
|
||||
|
||||
|
||||
- [ ] All `sasjs-cli` unit tests are passing (`npm test`).
|
||||
- (CI Runs this) All `sasjs-tests` are passing. If you want to run it manually (instructions available [here](https://github.com/sasjs/adapter/blob/master/sasjs-tests/README.md)).
|
||||
- [ ] All `sasjs-tests` are passing (instructions available [here](https://github.com/sasjs/adapter/blob/master/sasjs-tests/README.md)).
|
||||
- [ ] [Data Controller](https://datacontroller.io) builds and is functional on both SAS 9 and Viya
|
||||
|
||||
12
README.md
12
README.md
@@ -2,6 +2,7 @@
|
||||
|
||||
[![npm package][npm-image]][npm-url]
|
||||
[![Github Workflow][githubworkflow-image]][githubworkflow-url]
|
||||
[![Dependency Status][dependency-image]][dependency-url]
|
||||
[]()
|
||||

|
||||
[](/LICENSE)
|
||||
@@ -15,6 +16,7 @@
|
||||
[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:
|
||||
|
||||
@@ -30,7 +32,7 @@ For more information on building web apps with SAS, check out [sasjs.io](https:/
|
||||
|
||||
## None of this makes sense. How do I build an app with it?
|
||||
|
||||
Ok ok. Deploy this [example.html](https://raw.githubusercontent.com/sasjs/adapter/master/example.html) file to your web server, and update `servertype` to `SAS9`, `SASVIYA`, or `SASJS` depending on your backend.
|
||||
Ok ok. Deploy this [example.html](https://raw.githubusercontent.com/sasjs/adapter/master/example.html) file to your web server, and update `servertype` to `SAS9` or `SASVIYA` depending on your backend.
|
||||
|
||||
The backend part can be deployed as follows:
|
||||
|
||||
@@ -50,7 +52,7 @@ parmcards4;
|
||||
%webout(OBJ,areas)
|
||||
%webout(CLOSE)
|
||||
;;;;
|
||||
%mx_createwebservice(path=&appLoc/common,name=getdata)
|
||||
%mp_createwebservice(path=&appLoc/common,name=getdata)
|
||||
```
|
||||
|
||||
You now have a simple web app with a backend service!
|
||||
@@ -153,7 +155,7 @@ The SAS type (char/numeric) of the values is determined according to a set of ru
|
||||
|
||||
* If the values are numeric, the SAS type is numeric
|
||||
* If the values are all string, the SAS type is character
|
||||
* If the values contain a single character (a-Z + underscore + .) AND a numeric, then the SAS type is numeric (with special missing values).
|
||||
* If the values contain a single character (a-Z + underscore) AND a numeric, then the SAS type is numeric (with special missing values).
|
||||
* `null` is set to either '.' or '' depending on the assigned or derived type per the above rules. If entire column is `null` then the type will be numeric.
|
||||
|
||||
The following table illustrates the formats applied to columns under various scenarios:
|
||||
@@ -219,7 +221,7 @@ The SAS side is handled by a number of macros in the [macro core](https://github
|
||||
The following snippet shows the process of SAS tables arriving / leaving:
|
||||
|
||||
```sas
|
||||
/* convert frontend input tables from into SASWORK datasets */
|
||||
/* fetch all input tables sent from frontend - they arrive as work tables */
|
||||
%webout(FETCH)
|
||||
|
||||
/* some sas code */
|
||||
@@ -312,7 +314,7 @@ For more information and examples specific to this adapter you can check out the
|
||||
|
||||
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.
|
||||
|
||||
As a SAS customer you can also request a copy of [Data Controller](https://datacontroller.io) - free for up to 5 users, this tool makes use of all parts of the SASjs framework.
|
||||
If you are a SAS 9 or SAS Viya customer you can also request a copy of [Data Controller](https://datacontroller.io) - free for up to 5 users, this tool makes use of all parts of the SASjs framework.
|
||||
|
||||
|
||||
## Star Gazing
|
||||
|
||||
11
cypress.json
11
cypress.json
@@ -1,11 +0,0 @@
|
||||
{
|
||||
"chromeWebSecurity": false,
|
||||
"defaultCommandTimeout": 20000,
|
||||
"env": {
|
||||
"sasjsTestsUrl": "",
|
||||
"username": "",
|
||||
"password": "",
|
||||
"screenshotOnRunFailure": false,
|
||||
"testingFinishTimeout": 600000
|
||||
}
|
||||
}
|
||||
@@ -1,78 +0,0 @@
|
||||
const sasjsTestsUrl = Cypress.env('sasjsTestsUrl')
|
||||
const username = Cypress.env('username')
|
||||
const password = Cypress.env('password')
|
||||
const testingFinishTimeout = Cypress.env('testingFinishTimeout')
|
||||
|
||||
context('sasjs-tests', function () {
|
||||
this.beforeAll(() => {
|
||||
cy.visit(sasjsTestsUrl)
|
||||
})
|
||||
|
||||
this.beforeEach(() => {
|
||||
cy.reload()
|
||||
})
|
||||
|
||||
it('Should have all tests successfull', (done) => {
|
||||
cy.get('body').then(($body) => {
|
||||
if ($body.find('input[placeholder="User Name"]').length > 0) {
|
||||
cy.get('input[placeholder="User Name"]').type(username)
|
||||
cy.get('input[placeholder="Password"]').type(password)
|
||||
cy.get('.submit-button').click()
|
||||
}
|
||||
|
||||
cy.get('input[placeholder="User Name"]', { timeout: 40000 })
|
||||
.should('not.exist')
|
||||
.then(() => {
|
||||
cy.get('.ui.massive.icon.primary.left.labeled.button')
|
||||
.click()
|
||||
.then(() => {
|
||||
cy.get('.ui.massive.loading.primary.button', {
|
||||
timeout: testingFinishTimeout
|
||||
})
|
||||
.should('not.exist')
|
||||
.then(() => {
|
||||
cy.get('span.icon.failed')
|
||||
.should('not.exist')
|
||||
.then(() => {
|
||||
done()
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
it('Should have all tests successfull with debug on', (done) => {
|
||||
cy.get('body').then(($body) => {
|
||||
if ($body.find('input[placeholder="User Name"]').length > 0) {
|
||||
cy.get('input[placeholder="User Name"]').type(username)
|
||||
cy.get('input[placeholder="Password"]').type(password)
|
||||
cy.get('.submit-button').click()
|
||||
}
|
||||
|
||||
cy.get('.ui.fitted.toggle.checkbox label')
|
||||
.click()
|
||||
.then(() => {
|
||||
cy.get('input[placeholder="User Name"]', { timeout: 40000 })
|
||||
.should('not.exist')
|
||||
.then(() => {
|
||||
cy.get('.ui.massive.icon.primary.left.labeled.button')
|
||||
.click()
|
||||
.then(() => {
|
||||
cy.get('.ui.massive.loading.primary.button', {
|
||||
timeout: testingFinishTimeout
|
||||
})
|
||||
.should('not.exist')
|
||||
.then(() => {
|
||||
cy.get('span.icon.failed')
|
||||
.should('not.exist')
|
||||
.then(() => {
|
||||
done()
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -1,31 +0,0 @@
|
||||
const wp = require('@cypress/webpack-preprocessor')
|
||||
|
||||
const webpackOptions = {
|
||||
resolve: {
|
||||
extensions: ['.ts', '.js']
|
||||
},
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
test: /\.ts$/,
|
||||
loaders: ['ts-loader'],
|
||||
exclude: [/node_modules/]
|
||||
},
|
||||
{
|
||||
test: /\.(html|css)$/,
|
||||
loader: 'raw-loader',
|
||||
exclude: /\.async\.(html|css)$/
|
||||
},
|
||||
{
|
||||
test: /\.async\.(html|css)$/,
|
||||
loaders: ['file?name=[name].[hash].[ext]', 'extract']
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
const options = {
|
||||
webpackOptions
|
||||
}
|
||||
|
||||
module.exports = wp(options)
|
||||
@@ -1,42 +0,0 @@
|
||||
/// <reference types="cypress" />
|
||||
// ***********************************************************
|
||||
// This example plugins/index.js can be used to load plugins
|
||||
//
|
||||
// You can change the location of this file or turn off loading
|
||||
// the plugins file with the 'pluginsFile' configuration option.
|
||||
//
|
||||
// You can read more here:
|
||||
// https://on.cypress.io/plugins-guide
|
||||
// ***********************************************************
|
||||
|
||||
// This function is called when a project is opened or re-opened (e.g. due to
|
||||
// the project's config changing)
|
||||
|
||||
const wp = require('@cypress/webpack-preprocessor')
|
||||
|
||||
/**
|
||||
* @type {Cypress.PluginConfig}
|
||||
*/
|
||||
module.exports = (on, config) => {
|
||||
// `on` is used to hook into various events Cypress emits
|
||||
// `config` is the resolved Cypress config
|
||||
|
||||
const options = {
|
||||
webpackOptions: require('../webpack.config.js')
|
||||
}
|
||||
on('file:preprocessor', wp(options))
|
||||
|
||||
on('before:browser:launch', (browser = {}, launchOptions) => {
|
||||
if (browser.name === 'chrome') {
|
||||
launchOptions.args.push('--disable-site-isolation-trials')
|
||||
launchOptions.args.push('--auto-open-devtools-for-tabs')
|
||||
launchOptions.args.push('--aggressive-cache-discard')
|
||||
launchOptions.args.push('--disable-cache')
|
||||
launchOptions.args.push('--disable-application-cache')
|
||||
launchOptions.args.push('--disable-offline-load-stale-cache')
|
||||
launchOptions.args.push('--disk-cache-size=0')
|
||||
|
||||
return launchOptions
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"strict": true,
|
||||
"baseUrl": "../node_modules",
|
||||
"target": "es6",
|
||||
"lib": ["es2019", "dom"],
|
||||
"types": ["cypress"]
|
||||
},
|
||||
"include": ["**/*.ts"]
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
module.exports = {
|
||||
mode: 'development',
|
||||
resolve: {
|
||||
extensions: ['.ts', '.js']
|
||||
},
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
test: /\.ts$/,
|
||||
exclude: [/node_modules/],
|
||||
use: [
|
||||
{
|
||||
loader: 'ts-loader',
|
||||
options: {
|
||||
// skip typechecking for speed
|
||||
transpileOnly: true
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
14468
package-lock.json
generated
14468
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
16
package.json
16
package.json
@@ -8,17 +8,14 @@
|
||||
"build": "rimraf build && rimraf node && mkdir node && copyfiles -u 1 \"./src/**/*\" ./node && webpack && rimraf build/src && rimraf node",
|
||||
"package:lib": "npm run build && copyfiles ./package.json ./checkNodeVersion.js build && cd build && npm version \"5.0.0\" && npm pack",
|
||||
"publish:lib": "npm run build && cd build && npm publish",
|
||||
"lint:fix": "npx prettier --loglevel silent --write \"src/**/*.{ts,tsx,js,jsx,html,css,sass,less,json,yml,md,graphql}\" && npx prettier --loglevel silent --write \"sasjs-tests/src/**/*.{ts,tsx,js,jsx,html,css,sass,less,json,yml,md,graphql}\" && npx prettier --loglevel silent --write \"cypress/**/*.{ts,tsx,js,jsx,html,css,sass,less,json,yml,md,graphql}\"",
|
||||
"lint": "npx prettier --check \"src/**/*.{ts,tsx,js,jsx,html,css,sass,less,json,yml,md,graphql}\" && npx prettier --check \"sasjs-tests/src/**/*.{ts,tsx,js,jsx,html,css,sass,less,json,yml,md,graphql}\" && npx prettier --check \"cypress/**/*.{ts,tsx,js,jsx,html,css,sass,less,json,yml,md,graphql}\"",
|
||||
"lint:silent": "npx prettier --loglevel silent --check \"src/**/*.{ts,tsx,js,jsx,html,css,sass,less,json,yml,md,graphql}\" && npx prettier --loglevel silent --check \"sasjs-tests/src/**/*.{ts,tsx,js,jsx,html,css,sass,less,json,yml,md,graphql}\" && npx prettier --loglevel silent --check \"cypress/**/*.{ts,tsx,js,jsx,html,css,sass,less,json,yml,md,graphql}\"",
|
||||
"lint:fix": "npx prettier --write \"src/**/*.{ts,tsx,js,jsx,html,css,sass,less,json,yml,md,graphql}\" && npx prettier --write \"sasjs-tests/src/**/*.{ts,tsx,js,jsx,html,css,sass,less,json,yml,md,graphql}\"",
|
||||
"lint": "npx prettier --check \"src/**/*.{ts,tsx,js,jsx,html,css,sass,less,json,yml,md,graphql}\" && npx prettier --check \"sasjs-tests/src/**/*.{ts,tsx,js,jsx,html,css,sass,less,json,yml,md,graphql}\"",
|
||||
"test": "jest --silent --coverage",
|
||||
"prepublishOnly": "cp -r ./build/* . && rm -rf ./build",
|
||||
"postpublish": "git clean -fd",
|
||||
"semantic-release": "semantic-release",
|
||||
"typedoc": "node createTSDocs",
|
||||
"prepare": "git rev-parse --git-dir && git config core.hooksPath ./.git-hooks && git config core.autocrlf false || true",
|
||||
"cypress": "cypress open",
|
||||
"cy:run": "cypress run"
|
||||
"prepare": "git rev-parse --git-dir && git config core.hooksPath ./.git-hooks && git config core.autocrlf false || true"
|
||||
},
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
@@ -43,7 +40,6 @@
|
||||
},
|
||||
"license": "ISC",
|
||||
"devDependencies": {
|
||||
"@cypress/webpack-preprocessor": "5.9.1",
|
||||
"@types/axios": "0.14.0",
|
||||
"@types/express": "4.17.13",
|
||||
"@types/form-data": "2.5.0",
|
||||
@@ -53,7 +49,6 @@
|
||||
"@types/tough-cookie": "4.0.1",
|
||||
"copyfiles": "2.4.1",
|
||||
"cp": "0.2.0",
|
||||
"cypress": "7.7.0",
|
||||
"dotenv": "16.0.0",
|
||||
"express": "4.17.3",
|
||||
"jest": "27.4.7",
|
||||
@@ -61,7 +56,6 @@
|
||||
"node-polyfill-webpack-plugin": "1.1.4",
|
||||
"path": "0.12.7",
|
||||
"pem": "1.14.6",
|
||||
"prettier": "2.7.1",
|
||||
"process": "0.11.10",
|
||||
"rimraf": "3.0.2",
|
||||
"semantic-release": "18.0.0",
|
||||
@@ -71,8 +65,6 @@
|
||||
"tslint": "6.1.3",
|
||||
"tslint-config-prettier": "1.18.0",
|
||||
"typedoc": "0.22.11",
|
||||
"typedoc-neo-theme": "1.1.1",
|
||||
"typedoc-plugin-external-module-name": "4.0.6",
|
||||
"typedoc-plugin-rename-defaults": "0.4.0",
|
||||
"typescript": "4.5.5",
|
||||
"webpack": "5.69.0",
|
||||
@@ -80,7 +72,7 @@
|
||||
},
|
||||
"main": "index.js",
|
||||
"dependencies": {
|
||||
"@sasjs/utils": "2.44.0",
|
||||
"@sasjs/utils": "2.36.1",
|
||||
"axios": "0.26.0",
|
||||
"axios-cookiejar-support": "1.0.1",
|
||||
"form-data": "4.0.0",
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
if npm run cy:run -- --spec "cypress/integration/sasjs.tests.ts" ; then
|
||||
echo "Cypress sasjs testing passed!"
|
||||
else
|
||||
curl -X POST --header "Content-Type:application/json" --data '{"username":"GitHub CI - Adapter SASJS-TESTS (FAIL)", "content":"Automated sasjs-tests failed on the @sasjs/adapter PR on following link.\n'$2'", "avatar_url":"https://i.ibb.co/Lpk7Xvq/error-outline.png"}' $1
|
||||
|
||||
echo "Cypress sasjs testing failed!"
|
||||
exit 1
|
||||
fi
|
||||
@@ -60,58 +60,34 @@ If you'd like to deploy just `sasjs-tests` without changing the adapter version,
|
||||
|
||||
The below services need to be created on your SAS server, at the location specified as the `appLoc` in the SASjs configuration.
|
||||
|
||||
The code below will work on ALL SAS platforms (Viya, SAS 9 EBI, SASjs Server).
|
||||
### SAS 9
|
||||
|
||||
```sas
|
||||
filename mc url "https://raw.githubusercontent.com/sasjs/core/main/all.sas";
|
||||
%inc mc;
|
||||
filename ft15f001 temp lrecl=1000;
|
||||
filename ft15f001 temp;
|
||||
parmcards4;
|
||||
%webout(FETCH)
|
||||
%webout(OPEN)
|
||||
%macro x();
|
||||
%if %symexist(sasjs_tables) %then %do i=1 %to %sysfunc(countw(&sasjs_tables));
|
||||
%let table=%scan(&sasjs_tables,&i);
|
||||
%webout(OBJ,&table,missing=STRING,showmeta=YES)
|
||||
%end;
|
||||
%else %do i=1 %to &_webin_file_count;
|
||||
%webout(OBJ,&&_webin_name&i,missing=STRING,showmeta=YES)
|
||||
%end;
|
||||
%do i=1 %to &_webin_file_count; %webout(OBJ,&&_webin_name&i,missing=STRING,showmeta=YES) %end;
|
||||
%mend; %x()
|
||||
%webout(CLOSE)
|
||||
;;;;
|
||||
%mx_createwebservice(path=/Public/app/common,name=sendObj)
|
||||
%mm_createwebservice(path=/Public/app/common,name=sendObj)
|
||||
parmcards4;
|
||||
%webout(FETCH)
|
||||
%webout(OPEN)
|
||||
%macro x();
|
||||
%if %symexist(sasjs_tables) %then %do i=1 %to %sysfunc(countw(&sasjs_tables));
|
||||
%let table=%scan(&sasjs_tables,&i);
|
||||
%webout(ARR,&table,missing=STRING,showmeta=YES)
|
||||
%end;
|
||||
%else %do i=1 %to &_webin_file_count;
|
||||
%webout(ARR,&&_webin_name&i,missing=STRING,showmeta=YES)
|
||||
%end;
|
||||
%do i=1 %to &_webin_file_count; %webout(ARR,&&_webin_name&i,missing=STRING,showmeta=YES) %end;
|
||||
%mend; %x()
|
||||
%webout(CLOSE)
|
||||
;;;;
|
||||
%mx_createwebservice(path=/Public/app/common,name=sendArr)
|
||||
%mm_createwebservice(path=/Public/app/common,name=sendArr)
|
||||
parmcards4;
|
||||
data work.macvars;
|
||||
set sashelp.vmacro;
|
||||
run;
|
||||
%webout(OPEN)
|
||||
%webout(OBJ,macvars)
|
||||
%webout(CLOSE)
|
||||
let he who hath understanding, reckon the number of the beast
|
||||
;;;;
|
||||
%mx_createwebservice(path=/Public/app/common,name=sendMacVars)
|
||||
parmcards4;
|
||||
If you can keep your head when all about you
|
||||
Are losing theirs and blaming it on you,
|
||||
If you can trust yourself when all men doubt you,
|
||||
But make allowance for their doubting too;
|
||||
;;;;
|
||||
%mx_createwebservice(path=/Public/app/common,name=makeErr)
|
||||
%mm_createwebservice(path=/Public/app/common,name=makeErr)
|
||||
parmcards4;
|
||||
%webout(OPEN)
|
||||
data _null_;
|
||||
@@ -120,7 +96,59 @@ data _null_;
|
||||
run;
|
||||
%webout(CLOSE)
|
||||
;;;;
|
||||
%mx_createwebservice(path=/Public/app/common,name=invalidJSON)
|
||||
%mm_createwebservice(path=/Public/app/common,name=invalidJSON)
|
||||
```
|
||||
|
||||
### SAS Viya
|
||||
|
||||
```sas
|
||||
filename mc url "https://raw.githubusercontent.com/sasjs/core/main/all.sas";
|
||||
%inc mc;
|
||||
filename ft15f001 temp;
|
||||
parmcards4;
|
||||
%webout(FETCH)
|
||||
%webout(OPEN)
|
||||
%macro x();
|
||||
%do i=1 %to %sysfunc(countw(&sasjs_tables));
|
||||
%let table=%scan(&sasjs_tables,&i);
|
||||
%webout(OBJ,&table,missing=STRING,showmeta=YES)
|
||||
%end;
|
||||
%mend;
|
||||
%x()
|
||||
%webout(CLOSE)
|
||||
;;;;
|
||||
%mp_createwebservice(path=/Public/app/common,name=sendObj)
|
||||
filename ft15f001 temp;
|
||||
parmcards4;
|
||||
%webout(FETCH)
|
||||
%webout(OPEN)
|
||||
%macro x();
|
||||
%do i=1 %to %sysfunc(countw(&sasjs_tables));
|
||||
%let table=%scan(&sasjs_tables,&i);
|
||||
%webout(ARR,&table,missing=STRING,showmeta=YES)
|
||||
%end;
|
||||
%mend;
|
||||
%x()
|
||||
%webout(CLOSE)
|
||||
;;;;
|
||||
%mp_createwebservice(path=/Public/app/common,name=sendArr)
|
||||
filename ft15f001 temp;
|
||||
parmcards4;
|
||||
If you can keep your head when all about you
|
||||
Are losing theirs and blaming it on you,
|
||||
If you can trust yourself when all men doubt you,
|
||||
But make allowance for their doubting too;
|
||||
;;;;
|
||||
%mp_createwebservice(path=/Public/app/common,name=makeErr)
|
||||
parmcards4;
|
||||
%webout(OPEN)
|
||||
data _null_;
|
||||
file _webout;
|
||||
put ' the discovery channel ';
|
||||
run;
|
||||
%webout(CLOSE)
|
||||
;;;;
|
||||
%mp_createwebservice(path=/Public/app/common,name=invalidJSON)
|
||||
```
|
||||
|
||||
You should now be able to access the tests in your browser at the deployed path on your server.
|
||||
|
||||
7668
sasjs-tests/package-lock.json
generated
7668
sasjs-tests/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -6,7 +6,6 @@ import { specialCaseTests } from './testSuites/SpecialCases'
|
||||
import { sasjsRequestTests } from './testSuites/SasjsRequests'
|
||||
import '@sasjs/test-framework/dist/index.css'
|
||||
import { computeTests } from './testSuites/Compute'
|
||||
import { fileUploadTests } from './testSuites/FileUpload'
|
||||
|
||||
const App = (): ReactElement<{}> => {
|
||||
const { adapter, config } = useContext(AppContext)
|
||||
@@ -19,8 +18,7 @@ const App = (): ReactElement<{}> => {
|
||||
sendArrTests(adapter),
|
||||
sendObjTests(adapter),
|
||||
specialCaseTests(adapter),
|
||||
sasjsRequestTests(adapter),
|
||||
fileUploadTests(adapter)
|
||||
sasjsRequestTests(adapter)
|
||||
]
|
||||
|
||||
if (adapter.getSasjsConfig().serverType === 'SASVIYA') {
|
||||
|
||||
@@ -9,7 +9,7 @@ const Login = (): ReactElement<{}> => {
|
||||
const appContext = useContext(AppContext)
|
||||
|
||||
const handleSubmit = useCallback(
|
||||
(e: any) => {
|
||||
(e) => {
|
||||
e.preventDefault()
|
||||
appContext.adapter.logIn(username, password).then((res) => {
|
||||
appContext.setIsLoggedIn(res.isLoggedIn)
|
||||
@@ -28,7 +28,7 @@ const Login = (): ReactElement<{}> => {
|
||||
placeholder="User Name"
|
||||
value={username}
|
||||
required
|
||||
onChange={(e: any) => setUsername(e.target.value)}
|
||||
onChange={(e) => setUsername(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div className="row">
|
||||
@@ -38,7 +38,7 @@ const Login = (): ReactElement<{}> => {
|
||||
type="password"
|
||||
value={password}
|
||||
required
|
||||
onChange={(e: any) => setPassword(e.target.value)}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<button type="submit" className="submit-button">
|
||||
|
||||
@@ -61,7 +61,7 @@ export const basicTests = (
|
||||
'Should fail on first attempt and should log the user in on second attempt',
|
||||
test: async () => {
|
||||
await adapter.logOut()
|
||||
await adapter.logIn('invalid', 'invalid').catch((err: any) => {})
|
||||
await adapter.logIn('invalid', 'invalid')
|
||||
return await adapter.logIn(userName, password)
|
||||
},
|
||||
assertion: (response: any) =>
|
||||
@@ -161,17 +161,26 @@ export const basicTests = (
|
||||
}
|
||||
},
|
||||
{
|
||||
title: 'Web request',
|
||||
description: 'Should run the request with old web approach',
|
||||
title: 'Request with extra attributes on JES approach',
|
||||
description:
|
||||
'Should complete successful request with extra attributes present in response',
|
||||
test: async () => {
|
||||
const config: Partial<SASjsConfig> = {
|
||||
useComputeApi: false
|
||||
}
|
||||
|
||||
return await adapter.request('common/sendArr', stringData, config)
|
||||
return await adapter.request(
|
||||
'common/sendArr',
|
||||
stringData,
|
||||
config,
|
||||
undefined,
|
||||
undefined,
|
||||
['file', 'data']
|
||||
)
|
||||
},
|
||||
assertion: (response: any) => {
|
||||
return response.table1[0][0] === stringData.table1[0].col1
|
||||
const responseKeys: any = Object.keys(response)
|
||||
return responseKeys.includes('file') && responseKeys.includes('data')
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
@@ -1,35 +1,9 @@
|
||||
import SASjs from '@sasjs/adapter'
|
||||
import { TestSuite } from '@sasjs/test-framework'
|
||||
|
||||
const stringData: any = { table1: [{ col1: 'first col value' }] }
|
||||
|
||||
export const computeTests = (adapter: SASjs): TestSuite => ({
|
||||
name: 'Compute',
|
||||
tests: [
|
||||
{
|
||||
title: 'Compute API request',
|
||||
description: 'Should run the request with compute API approach',
|
||||
test: async () => {
|
||||
return await adapter.request('common/sendArr', stringData)
|
||||
},
|
||||
assertion: (response: any) => {
|
||||
return response.table1[0][0] === stringData.table1[0].col1
|
||||
}
|
||||
},
|
||||
{
|
||||
title: 'JES API request',
|
||||
description: 'Should run the request with JES API approach',
|
||||
test: async () => {
|
||||
const config = {
|
||||
useComputeApi: false
|
||||
}
|
||||
|
||||
return await adapter.request('common/sendArr', stringData, config)
|
||||
},
|
||||
assertion: (response: any) => {
|
||||
return response.table1[0][0] === stringData.table1[0].col1
|
||||
}
|
||||
},
|
||||
{
|
||||
title: 'Start Compute Job - not waiting for result',
|
||||
description: 'Should start a compute job and return the session',
|
||||
|
||||
@@ -1,35 +0,0 @@
|
||||
import SASjs from '@sasjs/adapter'
|
||||
import { TestSuite } from '@sasjs/test-framework'
|
||||
|
||||
export const fileUploadTests = (adapter: SASjs): TestSuite => ({
|
||||
name: 'File Upload Tests',
|
||||
tests: [
|
||||
{
|
||||
title: 'Upload File',
|
||||
description: 'Should upload the file to VIYA',
|
||||
test: async () => {
|
||||
let blob: any = new Blob(['test'], { type: 'text/html' })
|
||||
blob['lastModifiedDate'] = ''
|
||||
blob['name'] = 'macvars_testfile'
|
||||
let file = blob
|
||||
|
||||
const filesToUpload = [
|
||||
{
|
||||
file: file,
|
||||
fileName: file.name
|
||||
}
|
||||
]
|
||||
|
||||
return adapter.uploadFile('common/sendMacVars', filesToUpload, null)
|
||||
},
|
||||
assertion: (response: any) =>
|
||||
(response.macvars as any[]).findIndex(
|
||||
(el: any) => el.NAME === '_WEBIN_FILE_COUNT' && el.VALUE === '1'
|
||||
) > -1 &&
|
||||
(response.macvars as any[]).findIndex(
|
||||
(el: any) =>
|
||||
el.NAME === '_WEBIN_FILENAME' && el.VALUE === 'macvars_testfile'
|
||||
) > -1
|
||||
}
|
||||
]
|
||||
})
|
||||
@@ -86,7 +86,7 @@ export const sendArrTests = (adapter: SASjs): TestSuite => ({
|
||||
'Should error out with long string values over 32765 characters',
|
||||
test: () => {
|
||||
const data = getLongStringData(32767)
|
||||
return adapter.request('common/sendArr', data).catch((e: any) => e)
|
||||
return adapter.request('common/sendArr', data).catch((e) => e)
|
||||
},
|
||||
assertion: (error: any) => {
|
||||
return !!error && !!error.error && !!error.error.message
|
||||
@@ -138,7 +138,7 @@ export const sendArrTests = (adapter: SASjs): TestSuite => ({
|
||||
result =
|
||||
result &&
|
||||
res.table1[index][3] ===
|
||||
(multipleRowsWithNulls.table1[index].col4 || '')
|
||||
(multipleRowsWithNulls.table1[index].col4 || ' ')
|
||||
})
|
||||
return result
|
||||
}
|
||||
@@ -164,7 +164,7 @@ export const sendArrTests = (adapter: SASjs): TestSuite => ({
|
||||
result =
|
||||
result &&
|
||||
res.table1[index][3] ===
|
||||
(multipleColumnsWithNulls.table1[index].col4 || '')
|
||||
(multipleColumnsWithNulls.table1[index].col4 || ' ')
|
||||
})
|
||||
return result
|
||||
}
|
||||
@@ -182,9 +182,7 @@ export const sendObjTests = (adapter: SASjs): TestSuite => ({
|
||||
const invalidData: any = {
|
||||
'1InvalidTable': [{ col1: 42 }]
|
||||
}
|
||||
return adapter
|
||||
.request('common/sendObj', invalidData)
|
||||
.catch((e: any) => e)
|
||||
return adapter.request('common/sendObj', invalidData).catch((e) => e)
|
||||
},
|
||||
assertion: (error: any) =>
|
||||
!!error && !!error.error && !!error.error.message
|
||||
@@ -196,9 +194,7 @@ export const sendObjTests = (adapter: SASjs): TestSuite => ({
|
||||
const invalidData: any = {
|
||||
'an invalidTable': [{ col1: 42 }]
|
||||
}
|
||||
return adapter
|
||||
.request('common/sendObj', invalidData)
|
||||
.catch((e: any) => e)
|
||||
return adapter.request('common/sendObj', invalidData).catch((e) => e)
|
||||
},
|
||||
assertion: (error: any) =>
|
||||
!!error && !!error.error && !!error.error.message
|
||||
@@ -210,9 +206,7 @@ export const sendObjTests = (adapter: SASjs): TestSuite => ({
|
||||
const invalidData: any = {
|
||||
'anInvalidTable#': [{ col1: 42 }]
|
||||
}
|
||||
return adapter
|
||||
.request('common/sendObj', invalidData)
|
||||
.catch((e: any) => e)
|
||||
return adapter.request('common/sendObj', invalidData).catch((e) => e)
|
||||
},
|
||||
assertion: (error: any) =>
|
||||
!!error && !!error.error && !!error.error.message
|
||||
@@ -224,10 +218,7 @@ export const sendObjTests = (adapter: SASjs): TestSuite => ({
|
||||
const invalidData: any = {
|
||||
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx: [{ col1: 42 }]
|
||||
}
|
||||
|
||||
return adapter
|
||||
.request('common/sendObj', invalidData)
|
||||
.catch((e: any) => e)
|
||||
return adapter.request('common/sendObj', invalidData).catch((e) => e)
|
||||
},
|
||||
assertion: (error: any) =>
|
||||
!!error && !!error.error && !!error.error.message
|
||||
@@ -239,9 +230,7 @@ export const sendObjTests = (adapter: SASjs): TestSuite => ({
|
||||
const invalidData: any = {
|
||||
inData: [[{ data: 'value' }]]
|
||||
}
|
||||
return adapter
|
||||
.request('common/sendObj', invalidData)
|
||||
.catch((e: any) => e)
|
||||
return adapter.request('common/sendObj', invalidData).catch((e) => e)
|
||||
},
|
||||
assertion: (error: any) =>
|
||||
!!error && !!error.error && !!error.error.message
|
||||
@@ -275,7 +264,7 @@ export const sendObjTests = (adapter: SASjs): TestSuite => ({
|
||||
test: () => {
|
||||
return adapter
|
||||
.request('common/sendObj', getLongStringData(32767))
|
||||
.catch((e: any) => e)
|
||||
.catch((e) => e)
|
||||
},
|
||||
assertion: (error: any) => {
|
||||
return !!error && !!error.error && !!error.error.message
|
||||
@@ -339,7 +328,7 @@ export const sendObjTests = (adapter: SASjs): TestSuite => ({
|
||||
result =
|
||||
result &&
|
||||
res.table1[index].COL4 ===
|
||||
(multipleRowsWithNulls.table1[index].col4 || '')
|
||||
(multipleRowsWithNulls.table1[index].col4 || ' ')
|
||||
})
|
||||
return result
|
||||
}
|
||||
@@ -368,7 +357,7 @@ export const sendObjTests = (adapter: SASjs): TestSuite => ({
|
||||
result =
|
||||
result &&
|
||||
res.table1[index].COL4 ===
|
||||
(multipleColumnsWithNulls.table1[index].col4 || '')
|
||||
(multipleColumnsWithNulls.table1[index].col4 || ' ')
|
||||
})
|
||||
return result
|
||||
}
|
||||
|
||||
@@ -37,8 +37,6 @@ const moreSpecialCharData: any = {
|
||||
]
|
||||
}
|
||||
|
||||
const stringData: any = { table1: [{ col1: 'first col value' }] }
|
||||
|
||||
const getWideData = () => {
|
||||
const cols: any = {}
|
||||
for (let i = 1; i <= 10000; i++) {
|
||||
@@ -271,34 +269,6 @@ export const specialCaseTests = (adapter: SASjs): TestSuite => ({
|
||||
)
|
||||
}
|
||||
},
|
||||
{
|
||||
title: 'Request with extra attributes on JES approach',
|
||||
description:
|
||||
'Should complete successful request with extra attributes present in response',
|
||||
test: async () => {
|
||||
if (adapter.getSasjsConfig().serverType !== 'SASVIYA')
|
||||
return Promise.resolve('skip')
|
||||
|
||||
const config = {
|
||||
useComputeApi: false
|
||||
}
|
||||
|
||||
return await adapter.request(
|
||||
'common/sendArr',
|
||||
stringData,
|
||||
config,
|
||||
undefined,
|
||||
undefined,
|
||||
['file', 'data']
|
||||
)
|
||||
},
|
||||
assertion: (response: any) => {
|
||||
if (response === 'skip') return true
|
||||
|
||||
const responseKeys: any = Object.keys(response)
|
||||
return responseKeys.includes('file') && responseKeys.includes('data')
|
||||
}
|
||||
},
|
||||
{
|
||||
title: 'Special missing values',
|
||||
description: 'Should support special missing values',
|
||||
|
||||
@@ -9,7 +9,7 @@ export const assert = (
|
||||
} else {
|
||||
result = expression()
|
||||
}
|
||||
} catch (e: any) {
|
||||
} catch (e) {
|
||||
console.error(message)
|
||||
throw new Error(message)
|
||||
}
|
||||
|
||||
@@ -31,7 +31,7 @@ describe('SASViyaApiClient', () => {
|
||||
.mockImplementation(() => Promise.reject('Not Found'))
|
||||
const error = await sasViyaApiClient
|
||||
.createFolder('test', '/foo')
|
||||
.catch((e: any) => e)
|
||||
.catch((e) => e)
|
||||
expect(error).toBeInstanceOf(RootFolderNotFoundError)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -11,11 +11,7 @@ import {
|
||||
JobDefinition,
|
||||
PollOptions
|
||||
} from './types'
|
||||
import {
|
||||
CertificateError,
|
||||
JobExecutionError,
|
||||
RootFolderNotFoundError
|
||||
} from './types/errors'
|
||||
import { JobExecutionError, RootFolderNotFoundError } from './types/errors'
|
||||
import { SessionManager } from './SessionManager'
|
||||
import { ContextManager } from './ContextManager'
|
||||
import { SasAuthResponse, MacroVar, AuthConfig } from '@sasjs/utils/types'
|
||||
@@ -882,8 +878,7 @@ export class SASViyaApiClient {
|
||||
|
||||
const { result: folder } = await this.requestClient
|
||||
.get<Folder>(`${this.serverUrl}${url}`, accessToken)
|
||||
.catch((err) => {
|
||||
if (err instanceof CertificateError) throw err
|
||||
.catch(() => {
|
||||
return { result: null }
|
||||
})
|
||||
|
||||
@@ -904,8 +899,7 @@ export class SASViyaApiClient {
|
||||
|
||||
const { result: folder } = await this.requestClient
|
||||
.get<Folder>(`${this.serverUrl}${url}`, accessToken)
|
||||
.catch((err) => {
|
||||
if (err instanceof CertificateError) throw err
|
||||
.catch(() => {
|
||||
return { result: null }
|
||||
})
|
||||
|
||||
|
||||
80
src/SASjs.ts
80
src/SASjs.ts
@@ -5,7 +5,8 @@ import {
|
||||
EditContextInput,
|
||||
PollOptions,
|
||||
LoginMechanism,
|
||||
ExecutionQuery
|
||||
ExecutionQuery,
|
||||
FileTree
|
||||
} from './types'
|
||||
import { SASViyaApiClient } from './SASViyaApiClient'
|
||||
import { SAS9ApiClient } from './SAS9ApiClient'
|
||||
@@ -16,8 +17,7 @@ import {
|
||||
MacroVar,
|
||||
AuthConfig,
|
||||
ExtraResponseAttributes,
|
||||
SasAuthResponse,
|
||||
ServicePackSASjs
|
||||
SasAuthResponse
|
||||
} from '@sasjs/utils/types'
|
||||
import { RequestClient } from './request/RequestClient'
|
||||
import { SasjsRequestClient } from './request/SasjsRequestClient'
|
||||
@@ -77,7 +77,7 @@ export default class SASjs {
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes SAS code on a SAS 9 server. Requires a runner to be present in
|
||||
* Executes code against a SAS 9 server. Requires a runner to be present in
|
||||
* the users home directory in metadata.
|
||||
* @param linesOfCode - lines of sas code from the file to run.
|
||||
* @param username - a string representing the username.
|
||||
@@ -97,17 +97,6 @@ export default class SASjs {
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes SAS code on a SASJS server
|
||||
* @param code - a string of code from the file to run.
|
||||
* @param authConfig - (optional) a valid client, secret, refresh and access tokens that are authorised to execute scripts.
|
||||
*/
|
||||
public async executeScriptSASjs(code: string, authConfig?: AuthConfig) {
|
||||
this.isMethodSupported('executeScriptSASJS', [ServerType.Sasjs])
|
||||
|
||||
return await this.sasJSApiClient?.executeScript(code, authConfig)
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes sas code in a SAS Viya compute session.
|
||||
* @param fileName - name of the file to run. It will be converted to path to the file being submitted for execution.
|
||||
@@ -145,7 +134,7 @@ export default class SASjs {
|
||||
|
||||
/**
|
||||
* Gets compute contexts.
|
||||
* @param accessToken - an access token for an authorised user.
|
||||
* @param accessToken - an access token for an authorized user.
|
||||
*/
|
||||
public async getComputeContexts(accessToken: string) {
|
||||
this.isMethodSupported('getComputeContexts', [ServerType.SasViya])
|
||||
@@ -155,7 +144,7 @@ export default class SASjs {
|
||||
|
||||
/**
|
||||
* Gets launcher contexts.
|
||||
* @param accessToken - an access token for an authorised user.
|
||||
* @param accessToken - an access token for an authorized user.
|
||||
*/
|
||||
public async getLauncherContexts(accessToken: string) {
|
||||
this.isMethodSupported('getLauncherContexts', [ServerType.SasViya])
|
||||
@@ -174,7 +163,7 @@ export default class SASjs {
|
||||
|
||||
/**
|
||||
* Gets executable compute contexts.
|
||||
* @param authConfig - an access token, refresh token, client and secret for an authorised user.
|
||||
* @param authConfig - an access token, refresh token, client and secret for an authorized user.
|
||||
*/
|
||||
public async getExecutableContexts(authConfig: AuthConfig) {
|
||||
this.isMethodSupported('getExecutableContexts', [ServerType.SasViya])
|
||||
@@ -188,8 +177,8 @@ export default class SASjs {
|
||||
* @param launchContextName - the name of the launcher context used by the compute service.
|
||||
* @param sharedAccountId - the ID of the account to run the servers for this context as.
|
||||
* @param autoExecLines - the lines of code to execute during session initialization.
|
||||
* @param accessToken - an access token for an authorised user.
|
||||
* @param authorisedUsers - an optional list of authorised user IDs.
|
||||
* @param accessToken - an access token for an authorized user.
|
||||
* @param authorizedUsers - an optional list of authorized user IDs.
|
||||
*/
|
||||
public async createComputeContext(
|
||||
contextName: string,
|
||||
@@ -197,7 +186,7 @@ export default class SASjs {
|
||||
sharedAccountId: string,
|
||||
autoExecLines: string[],
|
||||
accessToken: string,
|
||||
authorisedUsers?: string[]
|
||||
authorizedUsers?: string[]
|
||||
) {
|
||||
this.isMethodSupported('createComputeContext', [ServerType.SasViya])
|
||||
|
||||
@@ -207,7 +196,7 @@ export default class SASjs {
|
||||
sharedAccountId,
|
||||
autoExecLines,
|
||||
accessToken,
|
||||
authorisedUsers
|
||||
authorizedUsers
|
||||
)
|
||||
}
|
||||
|
||||
@@ -216,7 +205,7 @@ export default class SASjs {
|
||||
* @param contextName - the name of the context to be created.
|
||||
* @param description - the description of the context to be created.
|
||||
* @param launchType - launch type of the context to be created.
|
||||
* @param accessToken - an access token for an authorised user.
|
||||
* @param accessToken - an access token for an authorized user.
|
||||
*/
|
||||
public async createLauncherContext(
|
||||
contextName: string,
|
||||
@@ -238,7 +227,7 @@ export default class SASjs {
|
||||
* Updates a compute context on the given server.
|
||||
* @param contextName - the original name of the context to be deleted.
|
||||
* @param editedContext - an object with the properties to be updated.
|
||||
* @param accessToken - an access token for an authorised user.
|
||||
* @param accessToken - an access token for an authorized user.
|
||||
*/
|
||||
public async editComputeContext(
|
||||
contextName: string,
|
||||
@@ -257,7 +246,7 @@ export default class SASjs {
|
||||
/**
|
||||
* Deletes a compute context on the given server.
|
||||
* @param contextName - the name of the context to be deleted.
|
||||
* @param accessToken - an access token for an authorised user.
|
||||
* @param accessToken - an access token for an authorized user.
|
||||
*/
|
||||
public async deleteComputeContext(contextName: string, accessToken?: string) {
|
||||
this.isMethodSupported('deleteComputeContext', [ServerType.SasViya])
|
||||
@@ -272,7 +261,7 @@ export default class SASjs {
|
||||
* Returns a JSON representation of a compute context.
|
||||
* @example: { "createdBy": "admin", "links": [...], "id": "ID", "version": 2, "name": "context1" }
|
||||
* @param contextName - the name of the context to return.
|
||||
* @param accessToken - an access token for an authorised user.
|
||||
* @param accessToken - an access token for an authorized user.
|
||||
*/
|
||||
public async getComputeContextByName(
|
||||
contextName: string,
|
||||
@@ -289,7 +278,7 @@ export default class SASjs {
|
||||
/**
|
||||
* Returns a JSON representation of a compute context.
|
||||
* @param contextId - an id of the context to return.
|
||||
* @param accessToken - an access token for an authorised user.
|
||||
* @param accessToken - an access token for an authorized user.
|
||||
*/
|
||||
public async getComputeContextById(contextId: string, accessToken?: string) {
|
||||
this.isMethodSupported('getComputeContextById', [ServerType.SasViya])
|
||||
@@ -592,6 +581,15 @@ export default class SASjs {
|
||||
'A username and password are required when using the default login mechanism.'
|
||||
)
|
||||
|
||||
if (this.sasjsConfig.serverType === ServerType.Sasjs) {
|
||||
if (!clientId)
|
||||
throw new Error(
|
||||
'A username, password and clientId are required when using the default login mechanism with server type SASJS.'
|
||||
)
|
||||
|
||||
return this.authManager!.logInSasjs(username, password, clientId)
|
||||
}
|
||||
|
||||
return this.authManager!.logIn(username, password)
|
||||
}
|
||||
|
||||
@@ -890,25 +888,24 @@ export default class SASjs {
|
||||
|
||||
/**
|
||||
* Creates the folders and services at the given location `appLoc` on the given server `serverUrl`.
|
||||
* @param dataJson - the JSON specifying the folders and files to be created, can also includes
|
||||
* appLoc, streamServiceName, streamWebFolder, streamLogo
|
||||
* @param appLoc - (optional) the base folder in which to create the new folders and
|
||||
* services. If not provided, is taken from SASjsConfig. Precedence will be of appLoc present in dataJson.
|
||||
* @param authConfig - (optional) a valid client, secret, refresh and access tokens that are authorised to execute compute jobs.
|
||||
* @param members - the JSON specifying the folders and services to be created.
|
||||
* @param appLoc - the base folder in which to create the new folders and
|
||||
* services. If not provided, is taken from SASjsConfig.
|
||||
* @param authConfig - a valid client, secret, refresh and access tokens that are authorised to execute compute jobs.
|
||||
*/
|
||||
public async deployToSASjs(
|
||||
dataJson: ServicePackSASjs,
|
||||
members: FileTree,
|
||||
appLoc?: string,
|
||||
authConfig?: AuthConfig
|
||||
) {
|
||||
if (!appLoc) {
|
||||
appLoc = this.sasjsConfig.appLoc
|
||||
}
|
||||
return await this.sasJSApiClient?.deploy(dataJson, appLoc, authConfig)
|
||||
return await this.sasJSApiClient?.deploy(members, appLoc, authConfig)
|
||||
}
|
||||
|
||||
public async executeJobSASjs(query: ExecutionQuery, authConfig?: AuthConfig) {
|
||||
return await this.sasJSApiClient?.executeJob(query, authConfig)
|
||||
public async executeJobSASjs(query: ExecutionQuery) {
|
||||
return await this.sasJSApiClient?.executeJob(query)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -975,7 +972,7 @@ export default class SASjs {
|
||||
/**
|
||||
* Fetches content of the log file
|
||||
* @param logUrl - url of the log file.
|
||||
* @param accessToken - an access token for an authorised user.
|
||||
* @param accessToken - an access token for an authorized user.
|
||||
*/
|
||||
public async fetchLogFileContent(logUrl: string, accessToken?: string) {
|
||||
return await this.requestClient!.get(logUrl, accessToken).then((res) => {
|
||||
@@ -1097,8 +1094,13 @@ export default class SASjs {
|
||||
}
|
||||
|
||||
if (this.sasjsConfig.serverType === ServerType.Sasjs) {
|
||||
if (!this.sasJSApiClient) {
|
||||
this.sasJSApiClient = new SASjsApiClient(this.requestClient)
|
||||
if (this.sasJSApiClient) {
|
||||
this.sasJSApiClient.setConfig(this.sasjsConfig.serverUrl)
|
||||
} else {
|
||||
this.sasJSApiClient = new SASjsApiClient(
|
||||
this.sasjsConfig.serverUrl,
|
||||
this.requestClient
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,16 +1,24 @@
|
||||
import { AuthConfig, ServerType, ServicePackSASjs } from '@sasjs/utils/types'
|
||||
import { ExecutionQuery } from './types'
|
||||
import { AuthConfig, ServerType } from '@sasjs/utils/types'
|
||||
import { FileTree, ExecutionQuery } from './types'
|
||||
import { RequestClient } from './request/RequestClient'
|
||||
import { getAccessTokenForSasjs } from './auth/getAccessTokenForSasjs'
|
||||
import { refreshTokensForSasjs } from './auth/refreshTokensForSasjs'
|
||||
import { getAuthCodeForSasjs } from './auth/getAuthCodeForSasjs'
|
||||
import { parseWeboutResponse } from './utils'
|
||||
import { getTokens } from './auth/getTokens'
|
||||
|
||||
export class SASjsApiClient {
|
||||
constructor(private requestClient: RequestClient) {}
|
||||
constructor(
|
||||
private serverUrl: string,
|
||||
private requestClient: RequestClient
|
||||
) {}
|
||||
|
||||
public setConfig(serverUrl: string) {
|
||||
if (serverUrl) this.serverUrl = serverUrl
|
||||
}
|
||||
|
||||
public async deploy(
|
||||
dataJson: ServicePackSASjs,
|
||||
members: FileTree,
|
||||
appLoc: string,
|
||||
authConfig?: AuthConfig
|
||||
) {
|
||||
@@ -22,17 +30,13 @@ export class SASjsApiClient {
|
||||
ServerType.Sasjs
|
||||
))
|
||||
}
|
||||
|
||||
dataJson.appLoc = dataJson.appLoc || appLoc
|
||||
|
||||
const { result } = await this.requestClient.post<{
|
||||
status: string
|
||||
message: string
|
||||
streamServiceName?: string
|
||||
example?: {}
|
||||
}>(
|
||||
'SASjsApi/drive/deploy',
|
||||
dataJson,
|
||||
{ fileTree: members, appLoc: appLoc },
|
||||
access_token,
|
||||
undefined,
|
||||
{},
|
||||
@@ -42,9 +46,7 @@ export class SASjsApiClient {
|
||||
return Promise.resolve(result)
|
||||
}
|
||||
|
||||
public async executeJob(query: ExecutionQuery, authConfig?: AuthConfig) {
|
||||
const access_token = authConfig ? authConfig.access_token : undefined
|
||||
|
||||
public async executeJob(query: ExecutionQuery) {
|
||||
const { result } = await this.requestClient.post<{
|
||||
status: string
|
||||
message: string
|
||||
@@ -52,7 +54,7 @@ export class SASjsApiClient {
|
||||
logPath?: string
|
||||
error?: {}
|
||||
_webout?: string
|
||||
}>('SASjsApi/stp/execute', query, access_token)
|
||||
}>('SASjsApi/stp/execute', query, undefined)
|
||||
|
||||
if (Object.keys(result).includes('_webout')) {
|
||||
result._webout = parseWeboutResponse(result._webout!)
|
||||
@@ -61,39 +63,6 @@ export class SASjsApiClient {
|
||||
return Promise.resolve(result)
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes code on a SASJS server.
|
||||
* @param code - a string of code to execute.
|
||||
* @param authConfig - an object for authentication.
|
||||
*/
|
||||
public async executeScript(code: string, authConfig?: AuthConfig) {
|
||||
let access_token = (authConfig || {}).access_token
|
||||
if (authConfig) {
|
||||
;({ access_token } = await getTokens(
|
||||
this.requestClient,
|
||||
authConfig,
|
||||
ServerType.Sasjs
|
||||
))
|
||||
}
|
||||
|
||||
let parsedSasjsServerLog = ''
|
||||
|
||||
await this.requestClient
|
||||
.post('SASjsApi/code/execute', { code }, access_token)
|
||||
.then((res: any) => {
|
||||
if (res.result?.log) {
|
||||
parsedSasjsServerLog = res.result.log
|
||||
.map((logLine: any) => logLine.line)
|
||||
.join('\n')
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
parsedSasjsServerLog = err
|
||||
})
|
||||
|
||||
return parsedSasjsServerLog
|
||||
}
|
||||
|
||||
/**
|
||||
* Exchanges the auth code for an access token for the given client.
|
||||
* @param clientId - the client ID to authenticate with.
|
||||
@@ -113,6 +82,20 @@ export class SASjsApiClient {
|
||||
public async refreshTokens(refreshToken: string): Promise<SASjsAuthResponse> {
|
||||
return refreshTokensForSasjs(this.requestClient, refreshToken)
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs a login authenticate and returns an auth code for the given client.
|
||||
* @param username - a string representing the username.
|
||||
* @param password - a string representing the password.
|
||||
* @param clientId - the client ID to authenticate with.
|
||||
*/
|
||||
public async getAuthCode(
|
||||
username: string,
|
||||
password: string,
|
||||
clientId: string
|
||||
) {
|
||||
return getAuthCodeForSasjs(this.requestClient, username, password, clientId)
|
||||
}
|
||||
}
|
||||
|
||||
// todo move to sasjs/utils
|
||||
|
||||
@@ -240,7 +240,7 @@ export async function executeScript(
|
||||
|
||||
jobResult = await requestClient
|
||||
.get<any>(resultLink, access_token, 'text/plain')
|
||||
.catch(async (e: any) => {
|
||||
.catch(async (e) => {
|
||||
if (e instanceof NotFoundError) {
|
||||
if (logLink) {
|
||||
const logUrl = `${logLink.href}/content`
|
||||
@@ -271,7 +271,7 @@ export async function executeScript(
|
||||
})
|
||||
|
||||
return { result: jobResult?.result, log }
|
||||
} catch (e: any) {
|
||||
} catch (e) {
|
||||
interface HttpError {
|
||||
status: number
|
||||
}
|
||||
|
||||
@@ -80,7 +80,7 @@ describe('executeScript', () => {
|
||||
'test',
|
||||
['%put hello'],
|
||||
'test context'
|
||||
).catch((e: any) => e)
|
||||
).catch((e) => e)
|
||||
|
||||
expect(error).toContain('Error while getting session.')
|
||||
})
|
||||
@@ -128,7 +128,7 @@ describe('executeScript', () => {
|
||||
false,
|
||||
defaultPollOptions,
|
||||
true
|
||||
).catch((e: any) => e)
|
||||
).catch((e) => e)
|
||||
|
||||
expect(error).toContain('Error while getting session variable.')
|
||||
})
|
||||
@@ -297,7 +297,7 @@ describe('executeScript', () => {
|
||||
false,
|
||||
defaultPollOptions,
|
||||
true
|
||||
).catch((e: any) => e)
|
||||
).catch((e) => e)
|
||||
|
||||
expect(error).toContain('Error while posting job')
|
||||
})
|
||||
@@ -367,7 +367,7 @@ describe('executeScript', () => {
|
||||
true,
|
||||
defaultPollOptions,
|
||||
true
|
||||
).catch((e: any) => e)
|
||||
).catch((e) => e)
|
||||
|
||||
expect(error).toContain('Error while polling job status.')
|
||||
})
|
||||
@@ -393,7 +393,7 @@ describe('executeScript', () => {
|
||||
true,
|
||||
defaultPollOptions,
|
||||
true
|
||||
).catch((e: any) => e)
|
||||
).catch((e) => e)
|
||||
|
||||
expect(fetchLogsModule.fetchLogByChunks).toHaveBeenCalledWith(
|
||||
requestClient,
|
||||
@@ -468,7 +468,7 @@ describe('executeScript', () => {
|
||||
true,
|
||||
defaultPollOptions,
|
||||
true
|
||||
).catch((e: any) => e)
|
||||
).catch((e) => e)
|
||||
|
||||
expect(fetchLogsModule.fetchLogByChunks).toHaveBeenCalledWith(
|
||||
requestClient,
|
||||
@@ -501,7 +501,7 @@ describe('executeScript', () => {
|
||||
true,
|
||||
defaultPollOptions,
|
||||
true
|
||||
).catch((e: any) => e)
|
||||
).catch((e) => e)
|
||||
|
||||
expect(fetchLogsModule.fetchLogByChunks).toHaveBeenCalledWith(
|
||||
requestClient,
|
||||
@@ -561,7 +561,7 @@ describe('executeScript', () => {
|
||||
true,
|
||||
defaultPollOptions,
|
||||
true
|
||||
).catch((e: any) => e)
|
||||
).catch((e) => e)
|
||||
|
||||
expect(requestClient.get).toHaveBeenCalledWith(
|
||||
`/compute/sessions/${mockSession.id}/filerefs/_webout/content`,
|
||||
@@ -622,7 +622,7 @@ describe('executeScript', () => {
|
||||
true,
|
||||
defaultPollOptions,
|
||||
true
|
||||
).catch((e: any) => e)
|
||||
).catch((e) => e)
|
||||
|
||||
expect(error).toContain('Error while clearing session.')
|
||||
})
|
||||
|
||||
@@ -59,7 +59,7 @@ describe('pollJobState', () => {
|
||||
false,
|
||||
undefined,
|
||||
defaultPollOptions
|
||||
).catch((e: any) => e)
|
||||
).catch((e) => e)
|
||||
|
||||
expect((error as Error).message).toContain('Job state link was not found.')
|
||||
})
|
||||
@@ -238,7 +238,7 @@ describe('pollJobState', () => {
|
||||
false,
|
||||
undefined,
|
||||
defaultPollOptions
|
||||
).catch((e: any) => e)
|
||||
).catch((e) => e)
|
||||
|
||||
expect(error.message).toEqual(
|
||||
'Error while polling job state for job j0b: Status Error'
|
||||
|
||||
@@ -17,7 +17,7 @@ describe('saveLog', () => {
|
||||
|
||||
it('should throw an error when a valid access token is not provided', async () => {
|
||||
const error = await saveLog(mockJob, requestClient, 0, 100, stream).catch(
|
||||
(e: any) => e
|
||||
(e) => e
|
||||
)
|
||||
|
||||
expect(error.message).toContain(
|
||||
@@ -33,7 +33,7 @@ describe('saveLog', () => {
|
||||
100,
|
||||
stream,
|
||||
't0k3n'
|
||||
).catch((e: any) => e)
|
||||
).catch((e) => e)
|
||||
|
||||
expect(error.message).toContain(
|
||||
`Log URL for job ${mockJob.id} was not found.`
|
||||
|
||||
@@ -30,7 +30,7 @@ describe('uploadTables', () => {
|
||||
.mockImplementation(() => 'ERROR: LARGE STRING LENGTH')
|
||||
|
||||
const error = await uploadTables(requestClient, data, 't0k3n').catch(
|
||||
(e: any) => e
|
||||
(e) => e
|
||||
)
|
||||
|
||||
expect(requestClient.uploadFile).not.toHaveBeenCalled()
|
||||
@@ -46,7 +46,7 @@ describe('uploadTables', () => {
|
||||
.mockImplementation(() => Promise.reject('Upload Error'))
|
||||
|
||||
const error = await uploadTables(requestClient, data, 't0k3n').catch(
|
||||
(e: any) => e
|
||||
(e) => e
|
||||
)
|
||||
|
||||
expect(error).toContain('Error while uploading file.')
|
||||
|
||||
@@ -28,7 +28,7 @@ describe('writeStream', () => {
|
||||
jest
|
||||
.spyOn(stream, 'write')
|
||||
.mockImplementation((_, callback) => callback(new Error('Test Error')))
|
||||
const error = await writeStream(stream, content).catch((e: any) => e)
|
||||
const error = await writeStream(stream, content).catch((e) => e)
|
||||
|
||||
expect(error.message).toEqual('Test Error')
|
||||
})
|
||||
|
||||
@@ -4,7 +4,7 @@ export const writeStream = async (
|
||||
stream: WriteStream,
|
||||
content: string
|
||||
): Promise<void> =>
|
||||
stream.write(content + '\n', (e: any) => {
|
||||
stream.write(content + '\n', (e) => {
|
||||
if (e) return Promise.reject(e)
|
||||
|
||||
return Promise.resolve()
|
||||
|
||||
@@ -2,6 +2,8 @@ import { ServerType } from '@sasjs/utils/types'
|
||||
import { RequestClient } from '../request/RequestClient'
|
||||
import { LoginOptions, LoginResult } from '../types/Login'
|
||||
import { serialize } from '../utils'
|
||||
import { getAccessTokenForSasjs } from './getAccessTokenForSasjs'
|
||||
import { getAuthCodeForSasjs } from './getAuthCodeForSasjs'
|
||||
import { openWebPage } from './openWebPage'
|
||||
import { verifySas9Login } from './verifySas9Login'
|
||||
import { verifySasViyaLogin } from './verifySasViyaLogin'
|
||||
@@ -81,6 +83,39 @@ export class AuthManager {
|
||||
return { isLoggedIn: false, userName: '' }
|
||||
}
|
||||
|
||||
/**
|
||||
* Logs into the SAS server with the supplied credentials.
|
||||
* @param userName - a string representing the username.
|
||||
* @param password - a string representing the password.
|
||||
* @param clientId - a string representing the client ID.
|
||||
* @returns - a boolean `isLoggedin` and a string `username`
|
||||
*/
|
||||
public async logInSasjs(
|
||||
username: string,
|
||||
password: string,
|
||||
clientId: string
|
||||
): Promise<LoginResult> {
|
||||
const isLoggedIn = await this.sendLoginRequestSasjs(
|
||||
username,
|
||||
password,
|
||||
clientId
|
||||
)
|
||||
.then((res) => {
|
||||
this.userName = username
|
||||
this.requestClient.saveLocalStorageToken(
|
||||
res.access_token,
|
||||
res.refresh_token
|
||||
)
|
||||
return true
|
||||
})
|
||||
.catch(() => false)
|
||||
|
||||
return {
|
||||
isLoggedIn,
|
||||
userName: this.userName
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Logs into the SAS server with the supplied credentials.
|
||||
* @param username - a string representing the username.
|
||||
@@ -117,7 +152,7 @@ export class AuthManager {
|
||||
|
||||
let loginResponse = await this.sendLoginRequest(loginForm, loginParams)
|
||||
|
||||
let isLoggedIn = isLogInSuccess(this.serverType, loginResponse)
|
||||
let isLoggedIn = isLogInSuccess(loginResponse)
|
||||
|
||||
if (!isLoggedIn) {
|
||||
if (isCredentialsVerifyError(loginResponse)) {
|
||||
@@ -161,17 +196,6 @@ export class AuthManager {
|
||||
loginForm: { [key: string]: any },
|
||||
loginParams: { [key: string]: any }
|
||||
) {
|
||||
if (this.serverType === ServerType.Sasjs) {
|
||||
const { username, password } = loginParams
|
||||
const { result: loginResponse } = await this.requestClient.post<string>(
|
||||
this.loginUrl,
|
||||
{ username, password },
|
||||
undefined
|
||||
)
|
||||
|
||||
return loginResponse
|
||||
}
|
||||
|
||||
for (const key in loginForm) {
|
||||
loginParams[key] = loginForm[key]
|
||||
}
|
||||
@@ -191,6 +215,19 @@ export class AuthManager {
|
||||
return loginResponse
|
||||
}
|
||||
|
||||
private async sendLoginRequestSasjs(
|
||||
username: string,
|
||||
password: string,
|
||||
clientId: string
|
||||
) {
|
||||
const authCode = await getAuthCodeForSasjs(
|
||||
this.requestClient,
|
||||
username,
|
||||
password,
|
||||
clientId
|
||||
)
|
||||
return getAccessTokenForSasjs(this.requestClient, clientId, authCode)
|
||||
}
|
||||
/**
|
||||
* Checks whether a session is active, or login is required.
|
||||
* @returns - a promise which resolves with an object containing three values
|
||||
@@ -211,7 +248,8 @@ export class AuthManager {
|
||||
//Residue can happen in case of session expiration
|
||||
await this.logOut()
|
||||
|
||||
loginForm = await this.getNewLoginForm()
|
||||
if (this.serverType !== ServerType.Sasjs)
|
||||
loginForm = await this.getNewLoginForm()
|
||||
}
|
||||
|
||||
return Promise.resolve({
|
||||
@@ -222,12 +260,6 @@ export class AuthManager {
|
||||
}
|
||||
|
||||
private async getNewLoginForm() {
|
||||
if (this.serverType === ServerType.Sasjs) {
|
||||
// server will be sending CSRF cookie,
|
||||
// http client will use it automatically
|
||||
return this.requestClient.get('/', undefined)
|
||||
}
|
||||
|
||||
const { result: formResponse } = await this.requestClient.get<string>(
|
||||
this.loginUrl.replace('.do', ''),
|
||||
undefined,
|
||||
@@ -257,12 +289,6 @@ export class AuthManager {
|
||||
const isLoggedIn = loginResponse !== 'authErr'
|
||||
const userName = isLoggedIn ? this.extractUserName(loginResponse) : ''
|
||||
|
||||
if (!isLoggedIn) {
|
||||
//We will logout to make sure cookies are removed and login form is presented
|
||||
//Residue can happen in case of session expiration
|
||||
await this.logOut()
|
||||
}
|
||||
|
||||
return { isLoggedIn, userName }
|
||||
}
|
||||
|
||||
@@ -358,8 +384,5 @@ const isCredentialsVerifyError = (response: string): boolean =>
|
||||
response
|
||||
)
|
||||
|
||||
const isLogInSuccess = (serverType: ServerType, response: any): boolean => {
|
||||
if (serverType === ServerType.Sasjs) return response?.loggedin
|
||||
|
||||
return /You have signed in/gm.test(response)
|
||||
}
|
||||
const isLogInSuccess = (response: string): boolean =>
|
||||
/You have signed in/gm.test(response)
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { SasAuthResponse } from '@sasjs/utils/types'
|
||||
import { prefixMessage } from '@sasjs/utils/error'
|
||||
import { RequestClient } from '../request/RequestClient'
|
||||
import { CertificateError } from '../types/errors'
|
||||
|
||||
/**
|
||||
* Exchanges the auth code for an access token for the given client.
|
||||
@@ -37,7 +36,6 @@ export async function getAccessTokenForViya(
|
||||
.post(url, data, undefined, 'application/x-www-form-urlencoded', headers)
|
||||
.then((res) => res.result as SasAuthResponse)
|
||||
.catch((err) => {
|
||||
if (err instanceof CertificateError) throw err
|
||||
throw prefixMessage(err, 'Error while getting access token. ')
|
||||
})
|
||||
|
||||
|
||||
31
src/auth/getAuthCodeForSasjs.ts
Normal file
31
src/auth/getAuthCodeForSasjs.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
import { prefixMessage } from '@sasjs/utils/error'
|
||||
import { RequestClient } from '../request/RequestClient'
|
||||
|
||||
/**
|
||||
* Performs a login authenticate and returns an auth code for the given client.
|
||||
* @param requestClient - the pre-configured HTTP request client
|
||||
* @param username - a string representing the username.
|
||||
* @param password - a string representing the password.
|
||||
* @param clientId - the client ID to authenticate with.
|
||||
*/
|
||||
export const getAuthCodeForSasjs = async (
|
||||
requestClient: RequestClient,
|
||||
username: string,
|
||||
password: string,
|
||||
clientId: string
|
||||
) => {
|
||||
const url = '/SASjsApi/auth/authorize'
|
||||
const data = { username, password, clientId }
|
||||
|
||||
const { code: authCode } = await requestClient
|
||||
.post<{ code: string }>(url, data, undefined)
|
||||
.then((res) => res.result)
|
||||
.catch((err) => {
|
||||
throw prefixMessage(
|
||||
err,
|
||||
'Error while authenticating with provided username, password and clientId. '
|
||||
)
|
||||
})
|
||||
|
||||
return authCode
|
||||
}
|
||||
@@ -53,7 +53,7 @@ describe('getAccessTokenForSasjs', () => {
|
||||
requestClient,
|
||||
authConfig.client,
|
||||
authConfig.refresh_token
|
||||
).catch((e: any) => e)
|
||||
).catch((e) => e)
|
||||
|
||||
expect(error).toContain('Error while getting access token')
|
||||
})
|
||||
|
||||
@@ -64,7 +64,7 @@ describe('getAccessTokenForViya', () => {
|
||||
authConfig.client,
|
||||
authConfig.secret,
|
||||
authConfig.refresh_token
|
||||
).catch((e: any) => e)
|
||||
).catch((e) => e)
|
||||
|
||||
expect(error).toContain('Error while getting access token')
|
||||
})
|
||||
|
||||
@@ -62,9 +62,7 @@ describe('getTokens', () => {
|
||||
const expectedError =
|
||||
'Unable to obtain new access token. Your refresh token has expired.'
|
||||
|
||||
const error = await getTokens(requestClient, authConfig).catch(
|
||||
(e: any) => e
|
||||
)
|
||||
const error = await getTokens(requestClient, authConfig).catch((e) => e)
|
||||
|
||||
expect(error.message).toEqual(expectedError)
|
||||
})
|
||||
|
||||
@@ -35,7 +35,7 @@ describe('refreshTokensForSasjs', () => {
|
||||
const error = await refreshTokensForSasjs(
|
||||
requestClient,
|
||||
refresh_token
|
||||
).catch((e: any) => e)
|
||||
).catch((e) => e)
|
||||
|
||||
expect(error).toContain('Error while refreshing tokens')
|
||||
})
|
||||
|
||||
@@ -63,7 +63,7 @@ describe('refreshTokensForViya', () => {
|
||||
authConfig.client,
|
||||
authConfig.secret,
|
||||
authConfig.refresh_token
|
||||
).catch((e: any) => e)
|
||||
).catch((e) => e)
|
||||
|
||||
expect(error).toContain('Error while refreshing tokens')
|
||||
})
|
||||
|
||||
@@ -26,18 +26,11 @@ export const generateFileUploadForm = (
|
||||
)
|
||||
}
|
||||
|
||||
if (typeof FormData === 'undefined' && formData instanceof NodeFormData) {
|
||||
formData.append(name, csv, {
|
||||
filename: `${name}.csv`,
|
||||
contentType: 'application/csv'
|
||||
})
|
||||
} else {
|
||||
const file = new Blob([csv], {
|
||||
type: 'application/csv'
|
||||
})
|
||||
const file = new Blob([csv], {
|
||||
type: 'application/csv'
|
||||
})
|
||||
|
||||
formData.append(name, file, `${name}.csv`)
|
||||
}
|
||||
formData.append(name, file, `${name}.csv`)
|
||||
}
|
||||
|
||||
return formData
|
||||
|
||||
@@ -7,6 +7,7 @@ describe('generateFileUploadForm', () => {
|
||||
}
|
||||
|
||||
const BlobMock = jest.fn()
|
||||
|
||||
;(global as any).FormData = FormDataMock
|
||||
;(global as any).Blob = BlobMock
|
||||
})
|
||||
|
||||
@@ -99,20 +99,7 @@ export class FileUploader extends BaseJobExecutor {
|
||||
? parseWeboutResponse(res.result, uploadUrl)
|
||||
: res.result
|
||||
break
|
||||
case ServerType.Sasjs:
|
||||
if (typeof res.result._webout === 'object') {
|
||||
jsonResponse = res.result._webout
|
||||
} else {
|
||||
const webout = parseWeboutResponse(
|
||||
res.result._webout,
|
||||
uploadUrl
|
||||
)
|
||||
jsonResponse = getValidJson(webout)
|
||||
}
|
||||
break
|
||||
}
|
||||
} else if (this.serverType === ServerType.Sasjs) {
|
||||
jsonResponse = getValidJson(res.result._webout)
|
||||
} else {
|
||||
jsonResponse =
|
||||
typeof res.result === 'string'
|
||||
|
||||
@@ -21,7 +21,6 @@ import {
|
||||
} from '../utils'
|
||||
import { BaseJobExecutor } from './JobExecutor'
|
||||
import { parseWeboutResponse } from '../utils/parseWeboutResponse'
|
||||
import { Server } from 'https'
|
||||
|
||||
export interface WaitingRequstPromise {
|
||||
promise: Promise<any> | null
|
||||
@@ -47,7 +46,7 @@ export class WebJobExecutor extends BaseJobExecutor {
|
||||
authConfig?: AuthConfig,
|
||||
extraResponseAttributes: ExtraResponseAttributes[] = []
|
||||
) {
|
||||
const loginCallback = loginRequiredCallback
|
||||
const loginCallback = loginRequiredCallback || (() => Promise.resolve())
|
||||
const program = isRelativePath(sasJob)
|
||||
? config.appLoc
|
||||
? config.appLoc.replace(/\/?$/, '/') + sasJob.replace(/^\//, '')
|
||||
@@ -80,7 +79,7 @@ export class WebJobExecutor extends BaseJobExecutor {
|
||||
)
|
||||
})
|
||||
|
||||
if (loginCallback) await loginCallback()
|
||||
await loginCallback()
|
||||
} else {
|
||||
reject(new ErrorResponse(e?.message, e))
|
||||
}
|
||||
@@ -92,7 +91,7 @@ export class WebJobExecutor extends BaseJobExecutor {
|
||||
if (jobUri.length > 0) {
|
||||
apiUrl += '&_job=' + jobUri
|
||||
/**
|
||||
* Using both _job and _program parameters will cause a conflict in the JES web app, as it's not clear whether or not the server should make the extra fetch for the job uri.
|
||||
* Using both _job and _program parameters will cause a conflict in the JES web app, as it’s not clear whether or not the server should make the extra fetch for the job uri.
|
||||
* To handle this, we add the extra underscore and recreate the _program variable in the SAS side of the SASjs adapter so it remains available for backend developers.
|
||||
*/
|
||||
apiUrl = apiUrl.replace('_program=', '__program=')
|
||||
@@ -176,18 +175,6 @@ export class WebJobExecutor extends BaseJobExecutor {
|
||||
log: parsedSasjsServerLog
|
||||
}
|
||||
: res
|
||||
|
||||
if (
|
||||
this.serverType === ServerType.Sasjs &&
|
||||
res.result._webout.length < 1
|
||||
) {
|
||||
throw new JobExecutionError(
|
||||
0,
|
||||
`No webout was returned by job ${program}. Server type is SASJS and the calling function is WebJobExecutor. Please check the SAS log for more info.`,
|
||||
parsedSasjsServerLog
|
||||
)
|
||||
}
|
||||
|
||||
this.requestClient!.appendRequest(resObj, sasJob, config.debug)
|
||||
|
||||
let jsonResponse = res.result
|
||||
@@ -233,15 +220,6 @@ export class WebJobExecutor extends BaseJobExecutor {
|
||||
}
|
||||
|
||||
if (e instanceof LoginRequiredError) {
|
||||
if (!loginRequiredCallback) {
|
||||
reject(
|
||||
new ErrorResponse(
|
||||
'Request is not authenticated. Make sure .env file exists with valid credentials.',
|
||||
e
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
this.appendWaitingRequest(() => {
|
||||
return this.execute(
|
||||
sasJob,
|
||||
@@ -260,7 +238,7 @@ export class WebJobExecutor extends BaseJobExecutor {
|
||||
)
|
||||
})
|
||||
|
||||
if (loginCallback) await loginCallback()
|
||||
await loginCallback()
|
||||
} else {
|
||||
reject(new ErrorResponse(e?.message, e))
|
||||
}
|
||||
|
||||
@@ -7,8 +7,7 @@ import {
|
||||
LoginRequiredError,
|
||||
NotFoundError,
|
||||
InternalServerError,
|
||||
JobExecutionError,
|
||||
CertificateError
|
||||
JobExecutionError
|
||||
} from '../types/errors'
|
||||
import { SASjsRequest } from '../types'
|
||||
import { parseWeboutResponse } from '../utils/parseWeboutResponse'
|
||||
@@ -19,7 +18,6 @@ import {
|
||||
parseSourceCode,
|
||||
createAxiosInstance
|
||||
} from '../utils'
|
||||
import { InvalidCsrfError } from '../types/errors/InvalidCsrfError'
|
||||
|
||||
export interface HttpClient {
|
||||
get<T>(
|
||||
@@ -133,26 +131,6 @@ export class RequestClient implements HttpClient {
|
||||
} else {
|
||||
sasWork = response.log
|
||||
}
|
||||
} else if (response?.result?.log) {
|
||||
//In this scenario we know we got the response from SASJS server
|
||||
//Log is array of `{ line: '' }` so we need to convert it back to text
|
||||
//To be able to parse it with current functions.
|
||||
let log: string = ''
|
||||
|
||||
if (typeof log !== 'string') {
|
||||
log = response.result.log
|
||||
.map((logLine: any) => logLine.line)
|
||||
.join('\n')
|
||||
}
|
||||
|
||||
sourceCode = parseSourceCode(log)
|
||||
generatedCode = parseGeneratedCode(log)
|
||||
|
||||
if (response?.result?._webout) {
|
||||
sasWork = response.result._webout.WORK
|
||||
} else {
|
||||
sasWork = log
|
||||
}
|
||||
} else if (response?.result) {
|
||||
sourceCode = parseSourceCode(response.result)
|
||||
generatedCode = parseGeneratedCode(response.result)
|
||||
@@ -207,7 +185,7 @@ export class RequestClient implements HttpClient {
|
||||
|
||||
return this.parseResponse<T>(response)
|
||||
})
|
||||
.catch(async (e: any) => {
|
||||
.catch(async (e) => {
|
||||
return await this.handleError(
|
||||
e,
|
||||
() =>
|
||||
@@ -248,7 +226,7 @@ export class RequestClient implements HttpClient {
|
||||
|
||||
return this.parseResponse<T>(response)
|
||||
})
|
||||
.catch(async (e: any) => {
|
||||
.catch(async (e) => {
|
||||
return await this.handleError(e, () =>
|
||||
this.post<T>(url, data, accessToken, contentType, overrideHeaders)
|
||||
)
|
||||
@@ -272,7 +250,7 @@ export class RequestClient implements HttpClient {
|
||||
throwIfError(response)
|
||||
return this.parseResponse<T>(response)
|
||||
})
|
||||
.catch(async (e: any) => {
|
||||
.catch(async (e) => {
|
||||
return await this.handleError(e, () =>
|
||||
this.put<T>(url, data, accessToken, overrideHeaders)
|
||||
)
|
||||
@@ -291,7 +269,7 @@ export class RequestClient implements HttpClient {
|
||||
throwIfError(response)
|
||||
return this.parseResponse<T>(response)
|
||||
})
|
||||
.catch(async (e: any) => {
|
||||
.catch(async (e) => {
|
||||
return await this.handleError(e, () => this.delete<T>(url, accessToken))
|
||||
})
|
||||
}
|
||||
@@ -309,7 +287,7 @@ export class RequestClient implements HttpClient {
|
||||
throwIfError(response)
|
||||
return this.parseResponse<T>(response)
|
||||
})
|
||||
.catch(async (e: any) => {
|
||||
.catch(async (e) => {
|
||||
return await this.handleError(e, () =>
|
||||
this.patch<T>(url, data, accessToken)
|
||||
)
|
||||
@@ -499,24 +477,6 @@ export class RequestClient implements HttpClient {
|
||||
throw e
|
||||
}
|
||||
|
||||
if (e instanceof InvalidCsrfError) {
|
||||
// Fetching root will inject CSRF token in cookie
|
||||
await this.httpClient
|
||||
.get('/', {
|
||||
withCredentials: true
|
||||
})
|
||||
.catch((err) => {
|
||||
throw prefixMessage(err, 'Error while re-fetching CSRF token.')
|
||||
})
|
||||
|
||||
return await callback().catch((err: any) => {
|
||||
throw prefixMessage(
|
||||
err,
|
||||
'Error while executing callback in handleError. '
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
if (response?.status === 403 || response?.status === 449) {
|
||||
this.parseAndSetCsrfToken(response)
|
||||
|
||||
@@ -537,10 +497,6 @@ export class RequestClient implements HttpClient {
|
||||
else return
|
||||
}
|
||||
|
||||
if (e.isAxiosError && e.code === 'UNABLE_TO_VERIFY_LEAF_SIGNATURE') {
|
||||
throw new CertificateError(e.message)
|
||||
}
|
||||
|
||||
if (e.message) throw e
|
||||
else throw prefixMessage(e, 'Error while handling error. ')
|
||||
}
|
||||
@@ -603,17 +559,9 @@ export class RequestClient implements HttpClient {
|
||||
export const throwIfError = (response: AxiosResponse) => {
|
||||
switch (response.status) {
|
||||
case 400:
|
||||
if (
|
||||
typeof response.data === 'object' &&
|
||||
response.data.error === 'invalid_grant'
|
||||
) {
|
||||
// In SASVIYA when trying to get access token, if auth code is wrong status code will be 400 so in such case we return login required error.
|
||||
if (typeof response.data === 'object') {
|
||||
throw new LoginRequiredError(response.data)
|
||||
}
|
||||
|
||||
if (response.data.toLowerCase() === 'invalid csrf token!') {
|
||||
throw new InvalidCsrfError()
|
||||
}
|
||||
break
|
||||
case 401:
|
||||
if (typeof response.data === 'object') {
|
||||
|
||||
@@ -68,7 +68,7 @@ export class Sas9RequestClient extends RequestClient {
|
||||
throwIfError(response)
|
||||
return this.parseResponse<T>(response)
|
||||
})
|
||||
.catch(async (e: any) => {
|
||||
.catch(async (e) => {
|
||||
return await this.handleError(
|
||||
e,
|
||||
() =>
|
||||
@@ -113,7 +113,7 @@ export class Sas9RequestClient extends RequestClient {
|
||||
throwIfError(response)
|
||||
return this.parseResponse<T>(response)
|
||||
})
|
||||
.catch(async (e: any) => {
|
||||
.catch(async (e) => {
|
||||
return await this.handleError(e, () =>
|
||||
this.post<T>(url, data, accessToken, contentType, overrideHeaders)
|
||||
)
|
||||
|
||||
@@ -54,17 +54,6 @@ describe('formatDataForRequest', () => {
|
||||
expect(formatDataForRequest(data)).toEqual(expectedOutput)
|
||||
})
|
||||
|
||||
it('should accept . as special missing value', () => {
|
||||
let tableWithMissingValues = {
|
||||
[testTable]: [{ var: '.' }, { var: 0 }],
|
||||
[`$${testTable}`]: { formats: { var: 'best.' } }
|
||||
}
|
||||
|
||||
expect(() =>
|
||||
formatDataForRequest(tableWithMissingValues)
|
||||
).not.toThrowError()
|
||||
})
|
||||
|
||||
it('should throw an error if special missing values is not valid', () => {
|
||||
let tableWithMissingValues = {
|
||||
[testTable]: [{ var: 'AA' }, { var: 0 }],
|
||||
@@ -73,7 +62,7 @@ describe('formatDataForRequest', () => {
|
||||
|
||||
expect(() => formatDataForRequest(tableWithMissingValues)).toThrow(
|
||||
new Error(
|
||||
`A Special missing value can only be a single character from 'A' to 'Z', '_', '.[a-z]', '._'`
|
||||
'Special missing value can only be a single character from A to Z or _'
|
||||
)
|
||||
)
|
||||
})
|
||||
|
||||
47
src/types/FileTree.ts
Normal file
47
src/types/FileTree.ts
Normal file
@@ -0,0 +1,47 @@
|
||||
export interface FileTree {
|
||||
members: [FolderMember, ServiceMember]
|
||||
}
|
||||
|
||||
export enum MemberType {
|
||||
folder = 'folder',
|
||||
service = 'service'
|
||||
}
|
||||
|
||||
export interface FolderMember {
|
||||
name: string
|
||||
type: MemberType.folder
|
||||
members: [FolderMember, ServiceMember]
|
||||
}
|
||||
|
||||
export interface ServiceMember {
|
||||
name: string
|
||||
type: MemberType.service
|
||||
code: string
|
||||
}
|
||||
|
||||
export const isFileTree = (arg: any): arg is FileTree =>
|
||||
arg &&
|
||||
arg.members &&
|
||||
Array.isArray(arg.members) &&
|
||||
arg.members.filter(
|
||||
(member: FolderMember | ServiceMember) =>
|
||||
!isFolderMember(member) && !isServiceMember(member)
|
||||
).length === 0
|
||||
|
||||
const isFolderMember = (arg: any): arg is FolderMember =>
|
||||
arg &&
|
||||
typeof arg.name === 'string' &&
|
||||
arg.type === MemberType.folder &&
|
||||
arg.members &&
|
||||
Array.isArray(arg.members) &&
|
||||
arg.members.filter(
|
||||
(member: FolderMember | ServiceMember) =>
|
||||
!isFolderMember(member) && !isServiceMember(member)
|
||||
).length === 0
|
||||
|
||||
const isServiceMember = (arg: any): arg is ServiceMember =>
|
||||
arg &&
|
||||
typeof arg.name === 'string' &&
|
||||
arg.type === MemberType.service &&
|
||||
arg.code &&
|
||||
typeof arg.code === 'string'
|
||||
@@ -1,12 +0,0 @@
|
||||
const instructionsToFix =
|
||||
'https://github.com/sasjs/cli/issues/1181#issuecomment-1090638584'
|
||||
|
||||
export class CertificateError extends Error {
|
||||
constructor(message: string) {
|
||||
super(
|
||||
`${message}\nPlease visit the link below for further information on this issue:\n- ${instructionsToFix}\n`
|
||||
)
|
||||
this.name = 'CertificateError'
|
||||
Object.setPrototypeOf(this, CertificateError.prototype)
|
||||
}
|
||||
}
|
||||
@@ -21,7 +21,7 @@ export class ErrorResponse {
|
||||
}
|
||||
}
|
||||
|
||||
interface ErrorBody {
|
||||
export interface ErrorBody {
|
||||
message: string
|
||||
details: string
|
||||
raw: any
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
export class InvalidCsrfError extends Error {
|
||||
constructor() {
|
||||
const message = 'Invalid CSRF token!'
|
||||
|
||||
super(`Auth error: ${message}`)
|
||||
this.name = 'InvalidCsrfError'
|
||||
Object.setPrototypeOf(this, InvalidCsrfError.prototype)
|
||||
}
|
||||
}
|
||||
@@ -1,14 +1,14 @@
|
||||
export * from './AuthorizeError'
|
||||
export * from './CertificateError'
|
||||
export * from './ComputeJobExecutionError'
|
||||
export * from './ErrorResponse'
|
||||
export * from './InternalServerError'
|
||||
export * from './InvalidJsonError'
|
||||
export * from './JobExecutionError'
|
||||
export * from './JobStatePollError'
|
||||
export * from './JsonParseArrayError'
|
||||
export * from './LoginRequiredError'
|
||||
export * from './NoSessionStateError'
|
||||
export * from './NotFoundError'
|
||||
export * from './ErrorResponse'
|
||||
export * from './NoSessionStateError'
|
||||
export * from './RootFolderNotFoundError'
|
||||
export * from './JsonParseArrayError'
|
||||
export * from './WeboutResponseError'
|
||||
export * from './InvalidJsonError'
|
||||
export * from './ErrorResponse'
|
||||
|
||||
@@ -12,4 +12,5 @@ export * from './Session'
|
||||
export * from './UploadFile'
|
||||
export * from './PollOptions'
|
||||
export * from './WriteStream'
|
||||
export * from './FileTree'
|
||||
export * from './ExecuteScript'
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import { isSpecialMissing } from '@sasjs/utils/input/validators'
|
||||
|
||||
/**
|
||||
* Converts the given JSON object array to a CSV string.
|
||||
* @param data - the array of JSON objects to convert.
|
||||
@@ -20,6 +18,7 @@ export const convertToCSV = (
|
||||
let headers: string[] = []
|
||||
let csvTest
|
||||
let invalidString = false
|
||||
const specialMissingValueRegExp = /^[a-z_]{1}$/i
|
||||
|
||||
if (formats) {
|
||||
headers = Object.keys(formats).map((key) => `${key}:${formats![key]}`)
|
||||
@@ -37,7 +36,7 @@ export const convertToCSV = (
|
||||
hasNullOrNumber = true
|
||||
} else if (
|
||||
typeof row[field] === 'string' &&
|
||||
isSpecialMissing(row[field])
|
||||
specialMissingValueRegExp.test(row[field])
|
||||
) {
|
||||
hasSpecialMissingString = true
|
||||
}
|
||||
@@ -131,9 +130,10 @@ export const convertToCSV = (
|
||||
value = currentCell === null ? '' : currentCell
|
||||
|
||||
if (formats && formats[fieldName] === 'best.') {
|
||||
if (value && !isSpecialMissing(value)) {
|
||||
if (value && !specialMissingValueRegExp.test(value)) {
|
||||
console.log(`🤖[value]🤖`, value)
|
||||
throw new Error(
|
||||
`A Special missing value can only be a single character from 'A' to 'Z', '_', '.[a-z]', '._'`
|
||||
'Special missing value can only be a single character from A to Z or _'
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ export const getValidJson = (str: string | object): object => {
|
||||
if (str === '') return {}
|
||||
|
||||
return JSON.parse(str)
|
||||
} catch (e: any) {
|
||||
} catch (e) {
|
||||
if (e instanceof JsonParseArrayError) throw e
|
||||
throw new InvalidJsonError()
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ export const parseSasViyaLog = (logResponse: { items: any[] }) => {
|
||||
log = logResponse.items
|
||||
? logResponse.items.map((i) => i.line).join('\n')
|
||||
: JSON.stringify(logResponse)
|
||||
} catch (e: any) {
|
||||
} catch (e) {
|
||||
console.error('An error has occurred while parsing the log response', e)
|
||||
log = logResponse
|
||||
}
|
||||
|
||||
@@ -16,15 +16,10 @@ export const parseSasViyaDebugResponse = async (
|
||||
requestClient: RequestClient,
|
||||
serverUrl: string
|
||||
) => {
|
||||
// On viya 3.5, iframe is like <iframe style="width: 99%; height: 500px" src="..."></iframe>
|
||||
// On viya 4, iframe is like <iframe style="width: 99%; height: 500px; background-color:Canvas;" src=...></iframe>
|
||||
|
||||
const iframeStart = response.split(
|
||||
/<iframe style="width: 99%; height: 500px" src="|<iframe style="width: 99%; height: 500px; background-color:Canvas;" src=/
|
||||
'<iframe style="width: 99%; height: 500px" src="'
|
||||
)[1]
|
||||
const jsonUrl = iframeStart
|
||||
? iframeStart.split(/"><\/iframe>|><\/iframe>/)[0]
|
||||
: null
|
||||
const jsonUrl = iframeStart ? iframeStart.split('"></iframe>')[0] : null
|
||||
if (!jsonUrl) {
|
||||
throw new Error('Unable to find webout file URL.')
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ export const parseWeboutResponse = (response: string, url?: string): string => {
|
||||
sasResponse = response
|
||||
.split('>>weboutBEGIN<<')[1]
|
||||
.split('>>weboutEND<<')[0]
|
||||
} catch (e: any) {
|
||||
} catch (e) {
|
||||
if (url) throw new WeboutResponseError(url)
|
||||
|
||||
sasResponse = ''
|
||||
|
||||
Reference in New Issue
Block a user