mirror of
https://github.com/sasjs/adapter.git
synced 2025-12-11 17:34:34 +00:00
Compare commits
83 Commits
v2.11.1
...
error-stat
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
596c1de5cb | ||
|
|
b645d1495b | ||
| 3a4a4c3460 | |||
|
|
182de51f9b | ||
|
|
712d1549c7 | ||
|
|
5a478c8936 | ||
| c9ecc1dde4 | |||
| bdf9e2fd5b | |||
|
|
0b795b26c0 | ||
| 96aac0cfa2 | |||
|
|
a82e1f33e3 | ||
|
|
058c887cd3 | ||
|
|
81d959c7c1 | ||
| 7c5adeabb5 | |||
|
|
cb88376bda | ||
|
|
4c8ddeca25 | ||
|
|
d264a3f239 | ||
|
|
840b1aa1bf | ||
|
|
e26fd307c8 | ||
|
|
62deaf9f03 | ||
|
|
865bf71f7d | ||
|
|
734c5bccaa | ||
|
|
bc82cb5f5e | ||
|
|
f25c76fdfd | ||
|
|
e649b41e9e | ||
|
|
a962979765 | ||
|
|
01d76fa66f | ||
|
|
49cfde9f7d | ||
|
|
ce04ffea05 | ||
| 9dc0499f66 | |||
|
|
bc1a7dc54f | ||
|
|
93c267fd4e | ||
|
|
0457eb6663 | ||
|
|
519494718b | ||
|
|
de5c38f0fb | ||
|
|
208470e7d9 | ||
|
|
0321f77451 | ||
|
|
6c5fdc01eb | ||
|
|
2aa0cd8d7a | ||
|
|
397bc4524f | ||
|
|
8dce9f3e48 | ||
| 8e9f1df1ce | |||
|
|
ff4915f7f3 | ||
|
|
6ff8eece7b | ||
|
|
2849e6ed07 | ||
|
|
90b11fe3fa | ||
|
|
147609842d | ||
|
|
dd6f9cd617 | ||
|
|
7f590c35da | ||
|
|
a38de108e3 | ||
|
|
e975e7de97 | ||
|
|
d418a7e971 | ||
|
|
a5b5052a5f | ||
|
|
7638595523 | ||
|
|
70d64f6eec | ||
|
|
f0ecfa57e5 | ||
|
|
5f3416ecd7 | ||
|
|
d8b1a72da2 | ||
|
|
7e64819eb2 | ||
|
|
2f1d403af4 | ||
|
|
075d410f7d | ||
|
|
f964bcef9e | ||
|
|
5784232d4e | ||
|
|
70ecc8b50e | ||
|
|
369a035e8a | ||
|
|
e5655033c1 | ||
|
|
c7af30bfa3 | ||
|
|
c8da3a54cf | ||
|
|
100da16803 | ||
|
|
dc91679040 | ||
| 28c8ebfc65 | |||
|
|
0c4d30afe3 | ||
|
|
bc015b72b6 | ||
| 085a3f84e9 | |||
|
|
f241d75f0a | ||
|
|
8a883c09f6 | ||
|
|
42d01b4044 | ||
|
|
15ff90025a | ||
|
|
10cf4998f5 | ||
|
|
f714f20f29 | ||
|
|
19adcc3115 | ||
|
|
bb6b25bac7 | ||
|
|
ec9dbd7ad6 |
10
.github/dependabot.yml
vendored
10
.github/dependabot.yml
vendored
@@ -1,7 +1,7 @@
|
||||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: npm
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: daily
|
||||
open-pull-requests-limit: 10
|
||||
- package-ecosystem: npm
|
||||
directory: '/'
|
||||
schedule:
|
||||
interval: monthly
|
||||
open-pull-requests-limit: 10
|
||||
|
||||
5
.github/workflows/build.yml
vendored
5
.github/workflows/build.yml
vendored
@@ -13,14 +13,15 @@ jobs:
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
node-version: [15.x]
|
||||
node-version: [lts/fermium]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Use Node.js ${{ matrix.node-version }}
|
||||
uses: actions/setup-node@v1
|
||||
uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
cache: npm
|
||||
- name: Install Dependencies
|
||||
run: npm ci
|
||||
- name: Check code style
|
||||
|
||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -5,4 +5,4 @@ build
|
||||
|
||||
/coverage
|
||||
|
||||
.DS_Store
|
||||
.DS_Store
|
||||
|
||||
@@ -3,3 +3,4 @@ docs/
|
||||
.github/
|
||||
*.md
|
||||
*.spec.ts
|
||||
.all-contributorsrc
|
||||
|
||||
16
README.md
16
README.md
@@ -36,7 +36,7 @@ Ok ok. Deploy this [example.html](https://raw.githubusercontent.com/sasjs/adapte
|
||||
|
||||
The backend part can be deployed as follows:
|
||||
|
||||
```
|
||||
```sas
|
||||
%let appLoc=/Public/app/readme; /* Metadata or Viya Folder per SASjs config */
|
||||
filename mc url "https://raw.githubusercontent.com/sasjs/core/main/all.sas";
|
||||
%inc mc; /* compile macros (can also be downloaded & compiled seperately) */
|
||||
@@ -85,11 +85,11 @@ let sasJs = new SASjs.default(
|
||||
);
|
||||
```
|
||||
If you've installed it via NPM, you can import it as a default import like so:
|
||||
```
|
||||
```js
|
||||
import SASjs from '@sasjs/adapter';
|
||||
```
|
||||
You can then instantiate it with:
|
||||
```
|
||||
```js
|
||||
const sasJs = new SASjs({your config})
|
||||
```
|
||||
|
||||
@@ -119,6 +119,7 @@ sasJs.request("/path/to/my/service", dataObject)
|
||||
console.log(response.tablewith2cols1row[0].COL1.value)
|
||||
})
|
||||
```
|
||||
|
||||
We supply the path to the SAS service, and a data object. The data object can be null (for services with no input), or can contain one or more tables in the following format:
|
||||
|
||||
```javascript
|
||||
@@ -146,6 +147,7 @@ The adapter will also cache the logs (if debug enabled) and even the work tables
|
||||
The SAS side is handled by a number of macros in the [macro core](https://github.com/sasjs/core) library.
|
||||
|
||||
The following snippet shows the process of SAS tables arriving / leaving:
|
||||
|
||||
```sas
|
||||
/* fetch all input tables sent from frontend - they arrive as work tables */
|
||||
%webout(FETCH)
|
||||
@@ -161,7 +163,6 @@ run;
|
||||
%webout(OBJ,tables,fmt=N) /* unformatted (raw) data */
|
||||
%webout(OBJ,tables,label=newtable) /* rename tables on export */
|
||||
%webout(CLOSE) /* close the JSON and send some extra useful variables too */
|
||||
|
||||
```
|
||||
|
||||
## Configuration
|
||||
@@ -172,6 +173,7 @@ Configuration on the client side involves passing an object on startup, which ca
|
||||
* `serverType` - either `SAS9` or `SASVIYA`.
|
||||
* `serverUrl` - the location (including http protocol and port) of the SAS Server. Can be omitted, eg if serving directly from the SAS Web Server, or in streaming mode.
|
||||
* `debug` - if `true` then SAS Logs and extra debug information is returned.
|
||||
* `LoginMechanism` - either `Default` or `Redirected`. If `Redirected` then authentication occurs through the injection of an additional screen, which contains the SASLogon prompt. This allows for more complex authentication flows (such as 2FA) and avoids the need to handle passwords in the application itself. The styling of the redirect flow can also be modified. If left at "Default" then the developer must capture the username and password and use these with the `.login()` method.
|
||||
* `useComputeApi` - Only relevant when the serverType is `SASVIYA`. If `true` the [Compute API](#using-the-compute-api) is used. If `false` the [JES API](#using-the-jes-api) is used. If `null` or `undefined` the [Web](#using-jes-web-app) approach is used.
|
||||
* `contextName` - Compute context on which the requests will be called. If missing or not provided, defaults to `Job Execution Compute context`.
|
||||
|
||||
@@ -196,7 +198,7 @@ Here we are running Jobs using the Job Execution Service except this time we are
|
||||
|
||||
This approach (`useComputeApi: false`) also ensures that jobs are displayed in Environment Manager.
|
||||
|
||||
```
|
||||
```json
|
||||
{
|
||||
appLoc:"/Your/Path",
|
||||
serverType:"SASVIYA",
|
||||
@@ -210,12 +212,12 @@ This approach is by far the fastest, as a result of the optimisations we have bu
|
||||
|
||||
With this approach (`useComputeApi: true`), the requests/logs will _not_ appear in the list in Environment manager.
|
||||
|
||||
```
|
||||
```json
|
||||
{
|
||||
appLoc:"/Your/Path",
|
||||
serverType:"SASVIYA",
|
||||
useComputeApi: true,
|
||||
contextName: 'yourComputeContext'
|
||||
contextName: "yourComputeContext"
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
225
docs/classes/api_viya_spec.mockstream.html
Normal file
225
docs/classes/api_viya_spec.mockstream.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
358
docs/classes/job_execution.fileuploader.html
Normal file
358
docs/classes/job_execution.fileuploader.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
363
docs/classes/job_execution.sas9jobexecutor.html
Normal file
363
docs/classes/job_execution.sas9jobexecutor.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
1102
docs/classes/request.sas9requestclient.html
Normal file
1102
docs/classes/request.sas9requestclient.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
282
docs/classes/root.sasjsapiclient.html
Normal file
282
docs/classes/root.sasjsapiclient.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
281
docs/classes/types_errors.invalidjsonerror.html
Normal file
281
docs/classes/types_errors.invalidjsonerror.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
308
docs/classes/types_errors.jobstatepollerror.html
Normal file
308
docs/classes/types_errors.jobstatepollerror.html
Normal file
File diff suppressed because one or more lines are too long
281
docs/classes/types_errors.jsonparsearrayerror.html
Normal file
281
docs/classes/types_errors.jsonparsearrayerror.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
347
docs/classes/types_errors.nosessionstateerror.html
Normal file
347
docs/classes/types_errors.nosessionstateerror.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
293
docs/classes/types_errors.rootfoldernotfounderror.html
Normal file
293
docs/classes/types_errors.rootfoldernotfounderror.html
Normal file
File diff suppressed because one or more lines are too long
281
docs/classes/types_errors.sas9autherror.html
Normal file
281
docs/classes/types_errors.sas9autherror.html
Normal file
File diff suppressed because one or more lines are too long
305
docs/classes/types_errors.weboutresponseerror.html
Normal file
305
docs/classes/types_errors.weboutresponseerror.html
Normal file
File diff suppressed because one or more lines are too long
235
docs/enums/types.loginmechanism.html
Normal file
235
docs/enums/types.loginmechanism.html
Normal file
File diff suppressed because one or more lines are too long
235
docs/enums/types.membertype.html
Normal file
235
docs/enums/types.membertype.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
139
docs/index.html
139
docs/index.html
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
243
docs/interfaces/types.executionquery.html
Normal file
243
docs/interfaces/types.executionquery.html
Normal file
File diff suppressed because one or more lines are too long
279
docs/interfaces/types.file.html
Normal file
279
docs/interfaces/types.file.html
Normal file
File diff suppressed because one or more lines are too long
225
docs/interfaces/types.filetree.html
Normal file
225
docs/interfaces/types.filetree.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
261
docs/interfaces/types.foldermember.html
Normal file
261
docs/interfaces/types.foldermember.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
225
docs/interfaces/types.loginoptions.html
Normal file
225
docs/interfaces/types.loginoptions.html
Normal file
File diff suppressed because one or more lines are too long
243
docs/interfaces/types.loginresult.html
Normal file
243
docs/interfaces/types.loginresult.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
147
docs/interfaces/types.process.html
Normal file
147
docs/interfaces/types.process.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
261
docs/interfaces/types.servicemember.html
Normal file
261
docs/interfaces/types.servicemember.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
285
docs/interfaces/types.writestream.html
Normal file
285
docs/interfaces/types.writestream.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
455
docs/modules/api_viya.html
Normal file
455
docs/modules/api_viya.html
Normal file
File diff suppressed because one or more lines are too long
452
docs/modules/api_viya_spec.html
Normal file
452
docs/modules/api_viya_spec.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
251
docs/modules/test.html
Normal file
251
docs/modules/test.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
20061
package-lock.json
generated
20061
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
22
package.json
22
package.json
@@ -41,36 +41,40 @@
|
||||
"license": "ISC",
|
||||
"devDependencies": {
|
||||
"@types/axios": "^0.14.0",
|
||||
"@types/express": "^4.17.13",
|
||||
"@types/form-data": "^2.5.0",
|
||||
"@types/jest": "^27.0.1",
|
||||
"@types/jest": "^27.0.2",
|
||||
"@types/mime": "^2.0.3",
|
||||
"@types/pem": "^1.9.6",
|
||||
"@types/tough-cookie": "^4.0.1",
|
||||
"copyfiles": "^2.4.1",
|
||||
"cp": "^0.2.0",
|
||||
"dotenv": "^10.0.0",
|
||||
"jest": "^27.1.0",
|
||||
"express": "^4.17.1",
|
||||
"jest": "^27.2.0",
|
||||
"jest-extended": "^0.11.5",
|
||||
"node-polyfill-webpack-plugin": "^1.1.4",
|
||||
"path": "^0.12.7",
|
||||
"pem": "^1.14.4",
|
||||
"process": "^0.11.10",
|
||||
"rimraf": "^3.0.2",
|
||||
"semantic-release": "^17.4.7",
|
||||
"terser-webpack-plugin": "^5.2.0",
|
||||
"semantic-release": "^18.0.0",
|
||||
"terser-webpack-plugin": "^5.2.4",
|
||||
"ts-jest": "^27.0.3",
|
||||
"ts-loader": "^9.2.2",
|
||||
"ts-loader": "^9.2.6",
|
||||
"tslint": "^6.1.3",
|
||||
"tslint-config-prettier": "^1.18.0",
|
||||
"typedoc": "^0.21.9",
|
||||
"typedoc": "0.19.2",
|
||||
"typedoc-neo-theme": "^1.1.1",
|
||||
"typedoc-plugin-external-module-name": "^4.0.6",
|
||||
"typescript": "4.3.5",
|
||||
"webpack": "^5.44.0",
|
||||
"webpack": "^5.56.0",
|
||||
"webpack-cli": "^4.7.2"
|
||||
},
|
||||
"main": "index.js",
|
||||
"dependencies": {
|
||||
"@sasjs/utils": "^2.30.0",
|
||||
"axios": "^0.21.1",
|
||||
"@sasjs/utils": "^2.32.0",
|
||||
"axios": "^0.21.4",
|
||||
"axios-cookiejar-support": "^1.0.1",
|
||||
"form-data": "^4.0.0",
|
||||
"https": "^1.0.0",
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@sasjs/adapter": "file:../build/sasjs-adapter-5.0.0.tgz",
|
||||
"@sasjs/test-framework": "^1.4.0",
|
||||
"@sasjs/test-framework": "^1.4.2",
|
||||
"@types/jest": "^26.0.20",
|
||||
"@types/node": "^14.14.41",
|
||||
"@types/react": "^17.0.1",
|
||||
|
||||
@@ -13,7 +13,6 @@ const defaultConfig: SASjsConfig = {
|
||||
debug: false,
|
||||
contextName: 'SAS Job Execution compute context',
|
||||
useComputeApi: false,
|
||||
allowInsecureRequests: false,
|
||||
loginMechanism: LoginMechanism.Default
|
||||
}
|
||||
|
||||
@@ -78,8 +77,8 @@ export const basicTests = (
|
||||
'common/sendArr',
|
||||
stringData,
|
||||
undefined,
|
||||
() => {
|
||||
adapter.logIn(userName, password)
|
||||
async () => {
|
||||
await adapter.logIn(userName, password)
|
||||
}
|
||||
)
|
||||
},
|
||||
|
||||
@@ -1,99 +0,0 @@
|
||||
import { isUrl, getValidJson, parseSasViyaDebugResponse } from './utils'
|
||||
import { UploadFile } from './types/UploadFile'
|
||||
import { ErrorResponse, LoginRequiredError } from './types/errors'
|
||||
import { RequestClient } from './request/RequestClient'
|
||||
import { ServerType } from '@sasjs/utils/types'
|
||||
import SASjs from './SASjs'
|
||||
import { Server } from 'https'
|
||||
import { SASjsConfig } from './types'
|
||||
import { config } from 'process'
|
||||
|
||||
export class FileUploader {
|
||||
constructor(
|
||||
private sasjsConfig: SASjsConfig,
|
||||
private jobsPath: string,
|
||||
private requestClient: RequestClient
|
||||
) {
|
||||
if (this.sasjsConfig.serverUrl) isUrl(this.sasjsConfig.serverUrl)
|
||||
}
|
||||
|
||||
public uploadFile(sasJob: string, files: UploadFile[], params: any) {
|
||||
if (files?.length < 1)
|
||||
return Promise.reject(
|
||||
new ErrorResponse('At least one file must be provided.')
|
||||
)
|
||||
if (!sasJob || sasJob === '')
|
||||
return Promise.reject(new ErrorResponse('sasJob must be provided.'))
|
||||
|
||||
let paramsString = ''
|
||||
|
||||
for (let param in params) {
|
||||
if (params.hasOwnProperty(param)) {
|
||||
paramsString += `&${param}=${params[param]}`
|
||||
}
|
||||
}
|
||||
|
||||
const program = this.sasjsConfig.appLoc
|
||||
? this.sasjsConfig.appLoc.replace(/\/?$/, '/') + sasJob.replace(/^\//, '')
|
||||
: sasJob
|
||||
const uploadUrl = `${this.jobsPath}/?${
|
||||
'_program=' + program
|
||||
}${paramsString}`
|
||||
|
||||
const formData = new FormData()
|
||||
|
||||
for (let file of files) {
|
||||
formData.append('file', file.file, file.fileName)
|
||||
}
|
||||
|
||||
const csrfToken = this.requestClient.getCsrfToken('file')
|
||||
if (csrfToken) formData.append('_csrf', csrfToken.value)
|
||||
if (this.sasjsConfig.debug) formData.append('_debug', '131')
|
||||
if (
|
||||
this.sasjsConfig.serverType === ServerType.SasViya &&
|
||||
this.sasjsConfig.contextName
|
||||
)
|
||||
formData.append('_contextname', this.sasjsConfig.contextName)
|
||||
|
||||
const headers = {
|
||||
'cache-control': 'no-cache',
|
||||
Accept: '*/*',
|
||||
'Content-Type': 'text/plain'
|
||||
}
|
||||
|
||||
// currently only web approach is supported for file upload
|
||||
// therefore log is part of response with debug enabled and must be parsed
|
||||
return this.requestClient
|
||||
.post(uploadUrl, formData, undefined, 'application/json', headers)
|
||||
.then(async (res) => {
|
||||
this.requestClient!.appendRequest(res, sasJob, this.sasjsConfig.debug)
|
||||
if (
|
||||
this.sasjsConfig.serverType === ServerType.SasViya &&
|
||||
this.sasjsConfig.debug
|
||||
) {
|
||||
const jsonResponse = await parseSasViyaDebugResponse(
|
||||
res.result as string,
|
||||
this.requestClient,
|
||||
this.sasjsConfig.serverUrl
|
||||
)
|
||||
return jsonResponse
|
||||
}
|
||||
|
||||
return typeof res.result === 'string'
|
||||
? getValidJson(res.result)
|
||||
: res.result
|
||||
|
||||
//TODO: append to SASjs requests
|
||||
})
|
||||
.catch((err: Error) => {
|
||||
if (err instanceof LoginRequiredError) {
|
||||
return Promise.reject(
|
||||
new ErrorResponse('You must be logged in to upload a file.', err)
|
||||
)
|
||||
}
|
||||
return Promise.reject(
|
||||
new ErrorResponse('File upload request failed.', err)
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
import * as https from 'https'
|
||||
import { generateTimestamp } from '@sasjs/utils/time'
|
||||
import * as NodeFormData from 'form-data'
|
||||
import { Sas9RequestClient } from './request/Sas9RequestClient'
|
||||
@@ -13,10 +14,10 @@ export class SAS9ApiClient {
|
||||
constructor(
|
||||
private serverUrl: string,
|
||||
private jobsPath: string,
|
||||
allowInsecureRequests: boolean
|
||||
httpsAgentOptions?: https.AgentOptions
|
||||
) {
|
||||
if (serverUrl) isUrl(serverUrl)
|
||||
this.requestClient = new Sas9RequestClient(serverUrl, allowInsecureRequests)
|
||||
this.requestClient = new Sas9RequestClient(serverUrl, httpsAgentOptions)
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
110
src/SASjs.ts
110
src/SASjs.ts
@@ -4,11 +4,14 @@ import {
|
||||
UploadFile,
|
||||
EditContextInput,
|
||||
PollOptions,
|
||||
LoginMechanism
|
||||
LoginMechanism,
|
||||
FolderMember,
|
||||
ServiceMember,
|
||||
ExecutionQuery
|
||||
} from './types'
|
||||
import { SASViyaApiClient } from './SASViyaApiClient'
|
||||
import { SAS9ApiClient } from './SAS9ApiClient'
|
||||
import { FileUploader } from './FileUploader'
|
||||
import { SASjsApiClient } from './SASjsApiClient'
|
||||
import { AuthManager } from './auth'
|
||||
import {
|
||||
ServerType,
|
||||
@@ -22,13 +25,15 @@ import {
|
||||
WebJobExecutor,
|
||||
ComputeJobExecutor,
|
||||
JesJobExecutor,
|
||||
Sas9JobExecutor
|
||||
Sas9JobExecutor,
|
||||
FileUploader
|
||||
} from './job-execution'
|
||||
import { ErrorResponse } from './types/errors'
|
||||
import { LoginOptions, LoginResult } from './types/Login'
|
||||
|
||||
const defaultConfig: SASjsConfig = {
|
||||
serverUrl: '',
|
||||
pathSASJS: '/SASjsApi/stp/execute',
|
||||
pathSAS9: '/SASStoredProcess/do',
|
||||
pathSASViya: '/SASJobExecution',
|
||||
appLoc: '/Public/seedapp',
|
||||
@@ -36,7 +41,6 @@ const defaultConfig: SASjsConfig = {
|
||||
debug: false,
|
||||
contextName: 'SAS Job Execution compute context',
|
||||
useComputeApi: null,
|
||||
allowInsecureRequests: false,
|
||||
loginMechanism: LoginMechanism.Default
|
||||
}
|
||||
|
||||
@@ -49,6 +53,7 @@ export default class SASjs {
|
||||
private jobsPath: string = ''
|
||||
private sasViyaApiClient: SASViyaApiClient | null = null
|
||||
private sas9ApiClient: SAS9ApiClient | null = null
|
||||
private SASjsApiClient: SASjsApiClient | null = null
|
||||
private fileUploader: FileUploader | null = null
|
||||
private authManager: AuthManager | null = null
|
||||
private requestClient: RequestClient | null = null
|
||||
@@ -57,7 +62,7 @@ export default class SASjs {
|
||||
private jesJobExecutor: JobExecutor | null = null
|
||||
private sas9JobExecutor: JobExecutor | null = null
|
||||
|
||||
constructor(config?: any) {
|
||||
constructor(config?: Partial<SASjsConfig>) {
|
||||
this.sasjsConfig = {
|
||||
...defaultConfig,
|
||||
...config
|
||||
@@ -507,7 +512,7 @@ export default class SASjs {
|
||||
...this.sasjsConfig,
|
||||
...config
|
||||
}
|
||||
await this.setupConfiguration()
|
||||
this.setupConfiguration()
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -523,10 +528,11 @@ export default class SASjs {
|
||||
|
||||
/**
|
||||
* Checks whether a session is active, or login is required.
|
||||
* @param accessToken - an optional access token is required for SASjs server type.
|
||||
* @returns - a promise which resolves with an object containing two values - a boolean `isLoggedIn`, and a string `userName`.
|
||||
*/
|
||||
public async checkSession() {
|
||||
return this.authManager!.checkSession()
|
||||
public async checkSession(accessToken?: string) {
|
||||
return this.authManager!.checkSession(accessToken)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -559,9 +565,10 @@ export default class SASjs {
|
||||
|
||||
/**
|
||||
* Logs out of the configured SAS server.
|
||||
* @param accessToken - an optional access token is required for SASjs server type.
|
||||
*/
|
||||
public logOut() {
|
||||
return this.authManager!.logOut()
|
||||
public logOut(accessToken?: string) {
|
||||
return this.authManager!.logOut(accessToken)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -571,24 +578,32 @@ export default class SASjs {
|
||||
* Process). Is prepended at runtime with the value of `appLoc`.
|
||||
* @param files - array of files to be uploaded, including File object and file name.
|
||||
* @param params - request URL parameters.
|
||||
* @param overrideSasjsConfig - object to override existing config (optional)
|
||||
* @param config - provide any changes to the config here, for instance to
|
||||
* enable/disable `debug`. Any change provided will override the global config,
|
||||
* for that particular function call.
|
||||
* @param loginRequiredCallback - a function that is called if the
|
||||
* user is not logged in (eg to display a login form). The request will be
|
||||
* resubmitted after successful login.
|
||||
*/
|
||||
public uploadFile(
|
||||
public async uploadFile(
|
||||
sasJob: string,
|
||||
files: UploadFile[],
|
||||
params: any,
|
||||
overrideSasjsConfig?: any
|
||||
params: { [key: string]: any } | null,
|
||||
config: { [key: string]: any } = {},
|
||||
loginRequiredCallback?: () => any
|
||||
) {
|
||||
const fileUploader = overrideSasjsConfig
|
||||
? new FileUploader(
|
||||
{ ...this.sasjsConfig, ...overrideSasjsConfig },
|
||||
this.jobsPath,
|
||||
this.requestClient!
|
||||
)
|
||||
: this.fileUploader ||
|
||||
new FileUploader(this.sasjsConfig, this.jobsPath, this.requestClient!)
|
||||
config = {
|
||||
...this.sasjsConfig,
|
||||
...config
|
||||
}
|
||||
const data = { files, params }
|
||||
|
||||
return fileUploader.uploadFile(sasJob, files, params)
|
||||
return await this.fileUploader!.execute(
|
||||
sasJob,
|
||||
data,
|
||||
config,
|
||||
loginRequiredCallback
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -784,7 +799,7 @@ export default class SASjs {
|
||||
sasApiClient = new SAS9ApiClient(
|
||||
serverUrl,
|
||||
this.jobsPath,
|
||||
this.sasjsConfig.allowInsecureRequests
|
||||
this.sasjsConfig.httpsAgentOptions
|
||||
)
|
||||
}
|
||||
} else {
|
||||
@@ -816,6 +831,14 @@ export default class SASjs {
|
||||
)
|
||||
}
|
||||
|
||||
public async deployToSASjs(members: [FolderMember, ServiceMember]) {
|
||||
return await this.SASjsApiClient?.deploy(members, this.sasjsConfig.appLoc)
|
||||
}
|
||||
|
||||
public async executeJobSASjs(query: ExecutionQuery) {
|
||||
return await this.SASjsApiClient?.executeJob(query)
|
||||
}
|
||||
|
||||
/**
|
||||
* Kicks off execution of the given job via the compute API.
|
||||
* @returns an object representing the compute session created for the given job.
|
||||
@@ -874,6 +897,7 @@ export default class SASjs {
|
||||
await this.webJobExecutor?.resendWaitingRequests()
|
||||
await this.computeJobExecutor?.resendWaitingRequests()
|
||||
await this.jesJobExecutor?.resendWaitingRequests()
|
||||
await this.fileUploader?.resendWaitingRequests()
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -942,19 +966,21 @@ export default class SASjs {
|
||||
if (!this.requestClient) {
|
||||
this.requestClient = new RequestClient(
|
||||
this.sasjsConfig.serverUrl,
|
||||
this.sasjsConfig.allowInsecureRequests
|
||||
this.sasjsConfig.httpsAgentOptions
|
||||
)
|
||||
} else {
|
||||
this.requestClient.setConfig(
|
||||
this.sasjsConfig.serverUrl,
|
||||
this.sasjsConfig.allowInsecureRequests
|
||||
this.sasjsConfig.httpsAgentOptions
|
||||
)
|
||||
}
|
||||
|
||||
this.jobsPath =
|
||||
this.sasjsConfig.serverType === ServerType.SasViya
|
||||
? this.sasjsConfig.pathSASViya
|
||||
: this.sasjsConfig.pathSAS9
|
||||
: this.sasjsConfig.serverType === ServerType.Sas9
|
||||
? this.sasjsConfig.pathSAS9
|
||||
: this.sasjsConfig.pathSASJS || ''
|
||||
|
||||
this.authManager = new AuthManager(
|
||||
this.sasjsConfig.serverUrl,
|
||||
@@ -964,34 +990,49 @@ export default class SASjs {
|
||||
)
|
||||
|
||||
if (this.sasjsConfig.serverType === ServerType.SasViya) {
|
||||
if (this.sasViyaApiClient)
|
||||
if (this.sasViyaApiClient) {
|
||||
this.sasViyaApiClient!.setConfig(
|
||||
this.sasjsConfig.serverUrl,
|
||||
this.sasjsConfig.appLoc
|
||||
)
|
||||
else
|
||||
} else {
|
||||
this.sasViyaApiClient = new SASViyaApiClient(
|
||||
this.sasjsConfig.serverUrl,
|
||||
this.sasjsConfig.appLoc,
|
||||
this.sasjsConfig.contextName,
|
||||
this.requestClient
|
||||
)
|
||||
}
|
||||
|
||||
this.sasViyaApiClient.debug = this.sasjsConfig.debug
|
||||
}
|
||||
|
||||
if (this.sasjsConfig.serverType === ServerType.Sas9) {
|
||||
if (this.sas9ApiClient)
|
||||
if (this.sas9ApiClient) {
|
||||
this.sas9ApiClient!.setConfig(this.sasjsConfig.serverUrl)
|
||||
else
|
||||
} else {
|
||||
this.sas9ApiClient = new SAS9ApiClient(
|
||||
this.sasjsConfig.serverUrl,
|
||||
this.jobsPath,
|
||||
this.sasjsConfig.allowInsecureRequests
|
||||
this.sasjsConfig.httpsAgentOptions
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (this.sasjsConfig.serverType === ServerType.Sasjs) {
|
||||
if (this.SASjsApiClient) {
|
||||
this.SASjsApiClient.setConfig(this.sasjsConfig.serverUrl)
|
||||
} else {
|
||||
this.SASjsApiClient = new SASjsApiClient(
|
||||
this.sasjsConfig.serverUrl,
|
||||
this.requestClient
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
this.fileUploader = new FileUploader(
|
||||
this.sasjsConfig,
|
||||
this.sasjsConfig.serverUrl,
|
||||
this.sasjsConfig.serverType!,
|
||||
this.jobsPath,
|
||||
this.requestClient
|
||||
)
|
||||
@@ -1008,7 +1049,8 @@ export default class SASjs {
|
||||
this.sasjsConfig.serverUrl,
|
||||
this.sasjsConfig.serverType!,
|
||||
this.jobsPath,
|
||||
this.sasjsConfig.allowInsecureRequests
|
||||
this.requestClient,
|
||||
this.sasjsConfig.httpsAgentOptions
|
||||
)
|
||||
|
||||
this.computeJobExecutor = new ComputeJobExecutor(
|
||||
|
||||
39
src/SASjsApiClient.ts
Normal file
39
src/SASjsApiClient.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
import { FolderMember, ServiceMember, ExecutionQuery } from './types'
|
||||
import { RequestClient } from './request/RequestClient'
|
||||
|
||||
export class SASjsApiClient {
|
||||
constructor(
|
||||
private serverUrl: string,
|
||||
private requestClient: RequestClient
|
||||
) {}
|
||||
|
||||
public setConfig(serverUrl: string) {
|
||||
if (serverUrl) this.serverUrl = serverUrl
|
||||
}
|
||||
|
||||
public async deploy(members: [FolderMember, ServiceMember], appLoc: string) {
|
||||
const { result } = await this.requestClient.post<{
|
||||
status: string
|
||||
message: string
|
||||
example?: {}
|
||||
}>(
|
||||
'SASjsApi/drive/deploy',
|
||||
{ fileTree: members, appLoc: appLoc },
|
||||
undefined
|
||||
)
|
||||
|
||||
return Promise.resolve(result)
|
||||
}
|
||||
|
||||
public async executeJob(query: ExecutionQuery) {
|
||||
const { result } = await this.requestClient.post<{
|
||||
status: string
|
||||
message: string
|
||||
log?: string
|
||||
logPath?: string
|
||||
error?: {}
|
||||
}>('SASjsApi/stp/execute', query, undefined)
|
||||
|
||||
return Promise.resolve(result)
|
||||
}
|
||||
}
|
||||
@@ -21,7 +21,9 @@ export class AuthManager {
|
||||
this.logoutUrl =
|
||||
this.serverType === ServerType.Sas9
|
||||
? '/SASLogon/logout?'
|
||||
: '/SASLogon/logout.do?'
|
||||
: this.serverType === ServerType.SasViya
|
||||
? '/SASLogon/logout.do?'
|
||||
: '/SASjsApi/auth/logout'
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -180,20 +182,21 @@ export class AuthManager {
|
||||
|
||||
/**
|
||||
* Checks whether a session is active, or login is required.
|
||||
* @param accessToken - an optional access token is required for SASjs server type.
|
||||
* @returns - a promise which resolves with an object containing three values
|
||||
* - a boolean `isLoggedIn`
|
||||
* - a string `userName` and
|
||||
* - a form `loginForm` if not loggedin.
|
||||
*/
|
||||
public async checkSession(): Promise<{
|
||||
public async checkSession(accessToken?: string): Promise<{
|
||||
isLoggedIn: boolean
|
||||
userName: string
|
||||
loginForm?: any
|
||||
}> {
|
||||
const { isLoggedIn, userName } = await this.fetchUserName()
|
||||
const { isLoggedIn, userName } = await this.fetchUserName(accessToken)
|
||||
let loginForm = null
|
||||
|
||||
if (!isLoggedIn) {
|
||||
if (!isLoggedIn && this.serverType !== ServerType.Sasjs) {
|
||||
//We will logout to make sure cookies are removed and login form is presented
|
||||
//Residue can happen in case of session expiration
|
||||
await this.logOut()
|
||||
@@ -218,19 +221,20 @@ export class AuthManager {
|
||||
return await this.getLoginForm(formResponse)
|
||||
}
|
||||
|
||||
private async fetchUserName(): Promise<{
|
||||
private async fetchUserName(accessToken?: string): Promise<{
|
||||
isLoggedIn: boolean
|
||||
userName: 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 === ServerType.SasViya
|
||||
? `${this.serverUrl}/identities/users/@currentUser`
|
||||
: `${this.serverUrl}/SASStoredProcess`
|
||||
: this.serverType === ServerType.Sas9
|
||||
? `${this.serverUrl}/SASStoredProcess`
|
||||
: `${this.serverUrl}/SASjsApi/session`
|
||||
|
||||
// Access token is required for server type `SASjs`
|
||||
const { result: loginResponse } = await this.requestClient
|
||||
.get<string>(url, undefined, 'text/plain')
|
||||
.get<string>(url, accessToken, 'text/plain')
|
||||
.catch((err: any) => {
|
||||
return { result: 'authErr' }
|
||||
})
|
||||
@@ -256,6 +260,13 @@ export class AuthManager {
|
||||
.split(' ')
|
||||
.map((name: string) => name.slice(0, 3).toLowerCase())
|
||||
.join('')
|
||||
|
||||
case ServerType.Sasjs:
|
||||
return response?.username
|
||||
|
||||
default:
|
||||
console.error('Server Type not found in extractUserName function')
|
||||
return ''
|
||||
}
|
||||
}
|
||||
|
||||
@@ -302,8 +313,12 @@ export class AuthManager {
|
||||
|
||||
/**
|
||||
* Logs out of the configured SAS server.
|
||||
* @param accessToken - an optional access token is required for SASjs server type.
|
||||
*/
|
||||
public logOut() {
|
||||
public logOut(accessToken?: string) {
|
||||
if (this.serverType === ServerType.Sasjs) {
|
||||
return this.requestClient.post(this.logoutUrl, undefined, accessToken)
|
||||
}
|
||||
this.requestClient.clearCsrfTokens()
|
||||
return this.requestClient.get(this.logoutUrl, undefined).then(() => true)
|
||||
}
|
||||
|
||||
@@ -46,7 +46,7 @@ export async function getAccessToken(
|
||||
)
|
||||
.then((res) => res.result as SasAuthResponse)
|
||||
.catch((err) => {
|
||||
throw prefixMessage(err, 'Error while getting access token')
|
||||
throw prefixMessage(err, 'Error while getting access token. ')
|
||||
})
|
||||
|
||||
return authResponse
|
||||
|
||||
@@ -45,7 +45,6 @@ export class ComputeJobExecutor extends BaseJobExecutor {
|
||||
}
|
||||
|
||||
if (e instanceof LoginRequiredError) {
|
||||
await loginCallback()
|
||||
this.appendWaitingRequest(() => {
|
||||
return this.execute(
|
||||
sasJob,
|
||||
@@ -61,6 +60,8 @@ export class ComputeJobExecutor extends BaseJobExecutor {
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
await loginCallback()
|
||||
} else {
|
||||
reject(new ErrorResponse(e?.message, e))
|
||||
}
|
||||
|
||||
143
src/job-execution/FileUploader.ts
Normal file
143
src/job-execution/FileUploader.ts
Normal file
@@ -0,0 +1,143 @@
|
||||
import {
|
||||
getValidJson,
|
||||
parseSasViyaDebugResponse,
|
||||
parseWeboutResponse
|
||||
} from '../utils'
|
||||
import { UploadFile } from '../types/UploadFile'
|
||||
import {
|
||||
ErrorResponse,
|
||||
JobExecutionError,
|
||||
LoginRequiredError
|
||||
} from '../types/errors'
|
||||
import { RequestClient } from '../request/RequestClient'
|
||||
import { ServerType } from '@sasjs/utils/types'
|
||||
import { BaseJobExecutor } from './JobExecutor'
|
||||
|
||||
interface dataFileUpload {
|
||||
files: UploadFile[]
|
||||
params: { [key: string]: any } | null
|
||||
}
|
||||
|
||||
export class FileUploader extends BaseJobExecutor {
|
||||
constructor(
|
||||
serverUrl: string,
|
||||
serverType: ServerType,
|
||||
private jobsPath: string,
|
||||
private requestClient: RequestClient
|
||||
) {
|
||||
super(serverUrl, serverType)
|
||||
}
|
||||
|
||||
public async execute(
|
||||
sasJob: string,
|
||||
data: any,
|
||||
config: any,
|
||||
loginRequiredCallback?: any
|
||||
) {
|
||||
const { files, params }: dataFileUpload = data
|
||||
const loginCallback = loginRequiredCallback || (() => Promise.resolve())
|
||||
|
||||
if (!files?.length)
|
||||
throw new ErrorResponse('At least one file must be provided.')
|
||||
|
||||
if (!sasJob || sasJob === '')
|
||||
throw new ErrorResponse('sasJob must be provided.')
|
||||
|
||||
let paramsString = ''
|
||||
|
||||
for (let param in params)
|
||||
if (params.hasOwnProperty(param))
|
||||
paramsString += `&${param}=${params[param]}`
|
||||
|
||||
const program = config.appLoc
|
||||
? config.appLoc.replace(/\/?$/, '/') + sasJob.replace(/^\//, '')
|
||||
: sasJob
|
||||
const uploadUrl = `${this.jobsPath}/?${
|
||||
'_program=' + program
|
||||
}${paramsString}`
|
||||
|
||||
const formData = new FormData()
|
||||
|
||||
for (let file of files) {
|
||||
formData.append('file', file.file, file.fileName)
|
||||
}
|
||||
|
||||
const csrfToken = this.requestClient.getCsrfToken('file')
|
||||
if (csrfToken) formData.append('_csrf', csrfToken.value)
|
||||
if (config.debug) formData.append('_debug', '131')
|
||||
if (config.serverType === ServerType.SasViya && config.contextName)
|
||||
formData.append('_contextname', config.contextName)
|
||||
|
||||
const headers = {
|
||||
'cache-control': 'no-cache',
|
||||
Accept: '*/*',
|
||||
'Content-Type': 'text/plain'
|
||||
}
|
||||
|
||||
// currently only web approach is supported for file upload
|
||||
// therefore log is part of response with debug enabled and must be parsed
|
||||
const requestPromise = new Promise((resolve, reject) => {
|
||||
this.requestClient
|
||||
.post(uploadUrl, formData, undefined, 'application/json', headers)
|
||||
.then(async (res: any) => {
|
||||
this.requestClient.appendRequest(res, sasJob, config.debug)
|
||||
|
||||
let jsonResponse = res.result
|
||||
|
||||
if (config.debug) {
|
||||
switch (this.serverType) {
|
||||
case ServerType.SasViya:
|
||||
jsonResponse = await parseSasViyaDebugResponse(
|
||||
res.result,
|
||||
this.requestClient,
|
||||
config.serverUrl
|
||||
)
|
||||
break
|
||||
case ServerType.Sas9:
|
||||
jsonResponse =
|
||||
typeof res.result === 'string'
|
||||
? parseWeboutResponse(res.result, uploadUrl)
|
||||
: res.result
|
||||
break
|
||||
}
|
||||
} else {
|
||||
jsonResponse =
|
||||
typeof res.result === 'string'
|
||||
? getValidJson(res.result)
|
||||
: res.result
|
||||
}
|
||||
|
||||
resolve(jsonResponse)
|
||||
})
|
||||
.catch(async (e: Error) => {
|
||||
if (e instanceof JobExecutionError) {
|
||||
this.requestClient!.appendRequest(e, sasJob, config.debug)
|
||||
reject(new ErrorResponse(e?.message, e))
|
||||
}
|
||||
|
||||
if (e instanceof LoginRequiredError) {
|
||||
this.appendWaitingRequest(() => {
|
||||
return this.execute(
|
||||
sasJob,
|
||||
data,
|
||||
config,
|
||||
loginRequiredCallback
|
||||
).then(
|
||||
(res: any) => {
|
||||
resolve(res)
|
||||
},
|
||||
(err: any) => {
|
||||
reject(err)
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
await loginCallback()
|
||||
} else {
|
||||
reject(new ErrorResponse('File upload request failed.', e))
|
||||
}
|
||||
})
|
||||
})
|
||||
return requestPromise
|
||||
}
|
||||
}
|
||||
@@ -45,8 +45,6 @@ export class JesJobExecutor extends BaseJobExecutor {
|
||||
}
|
||||
|
||||
if (e instanceof LoginRequiredError) {
|
||||
await loginCallback()
|
||||
|
||||
this.appendWaitingRequest(() => {
|
||||
return this.execute(
|
||||
sasJob,
|
||||
@@ -64,6 +62,8 @@ export class JesJobExecutor extends BaseJobExecutor {
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
await loginCallback()
|
||||
} else {
|
||||
reject(new ErrorResponse(e?.message, e))
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { AuthConfig, ServerType } from '@sasjs/utils/types'
|
||||
import { SASjsRequest } from '../types'
|
||||
import { ExtraResponseAttributes } from '@sasjs/utils/types'
|
||||
import { asyncForEach, parseGeneratedCode, parseSourceCode } from '../utils'
|
||||
import { asyncForEach } from '../utils'
|
||||
|
||||
export type ExecuteFunction = () => Promise<any>
|
||||
|
||||
@@ -21,7 +20,6 @@ export abstract class BaseJobExecutor implements JobExecutor {
|
||||
constructor(protected serverUrl: string, protected serverType: ServerType) {}
|
||||
|
||||
private waitingRequests: ExecuteFunction[] = []
|
||||
private requests: SASjsRequest[] = []
|
||||
|
||||
abstract execute(
|
||||
sasJob: string,
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import * as https from 'https'
|
||||
import { ServerType } from '@sasjs/utils/types'
|
||||
import * as NodeFormData from 'form-data'
|
||||
import { ErrorResponse } from '../types/errors'
|
||||
import { convertToCSV, isRelativePath } from '../utils'
|
||||
import { BaseJobExecutor } from './JobExecutor'
|
||||
import { Sas9RequestClient } from '../request/Sas9RequestClient'
|
||||
import { RequestClient } from '../request/RequestClient'
|
||||
|
||||
/**
|
||||
* Job executor for SAS9 servers for use in Node.js environments.
|
||||
@@ -12,15 +14,16 @@ import { Sas9RequestClient } from '../request/Sas9RequestClient'
|
||||
* job execution requests.
|
||||
*/
|
||||
export class Sas9JobExecutor extends BaseJobExecutor {
|
||||
private requestClient: Sas9RequestClient
|
||||
private sas9RequestClient: Sas9RequestClient
|
||||
constructor(
|
||||
serverUrl: string,
|
||||
serverType: ServerType,
|
||||
private jobsPath: string,
|
||||
allowInsecureRequests: boolean
|
||||
private requestClient: RequestClient,
|
||||
httpsAgentOptions?: https.AgentOptions
|
||||
) {
|
||||
super(serverUrl, serverType)
|
||||
this.requestClient = new Sas9RequestClient(serverUrl, allowInsecureRequests)
|
||||
this.sas9RequestClient = new Sas9RequestClient(serverUrl, httpsAgentOptions)
|
||||
}
|
||||
|
||||
async execute(sasJob: string, data: any, config: any) {
|
||||
@@ -36,6 +39,8 @@ export class Sas9JobExecutor extends BaseJobExecutor {
|
||||
: ''
|
||||
}`
|
||||
|
||||
apiUrl = `${apiUrl}${config.debug ? '&_debug=131' : ''}`
|
||||
|
||||
let requestParams = {
|
||||
...this.getRequestParams(config)
|
||||
}
|
||||
@@ -45,9 +50,11 @@ export class Sas9JobExecutor extends BaseJobExecutor {
|
||||
if (data) {
|
||||
try {
|
||||
formData = generateFileUploadForm(formData, data)
|
||||
} catch (e) {
|
||||
} catch (e: any) {
|
||||
return Promise.reject(new ErrorResponse(e?.message, e))
|
||||
}
|
||||
} else {
|
||||
data = ''
|
||||
}
|
||||
|
||||
for (const key in requestParams) {
|
||||
@@ -56,16 +63,18 @@ export class Sas9JobExecutor extends BaseJobExecutor {
|
||||
}
|
||||
}
|
||||
|
||||
await this.requestClient.login(
|
||||
await this.sas9RequestClient.login(
|
||||
config.username,
|
||||
config.password,
|
||||
this.jobsPath
|
||||
)
|
||||
|
||||
const contentType =
|
||||
data && Object.keys(data).length
|
||||
? 'multipart/form-data; boundary=' + (formData as any)._boundary
|
||||
: 'text/plain'
|
||||
return await this.requestClient!.post(
|
||||
|
||||
return await this.sas9RequestClient!.post(
|
||||
apiUrl,
|
||||
formData,
|
||||
undefined,
|
||||
@@ -75,6 +84,28 @@ export class Sas9JobExecutor extends BaseJobExecutor {
|
||||
Connection: 'Keep-Alive'
|
||||
}
|
||||
)
|
||||
.then((res: any) => {
|
||||
let resString = res
|
||||
|
||||
if (typeof res === 'object') {
|
||||
resString = JSON.stringify(res)
|
||||
}
|
||||
|
||||
this.requestClient!.appendRequest(resString, sasJob, config.debug)
|
||||
|
||||
return res
|
||||
})
|
||||
.catch((err: any) => {
|
||||
let errString = err
|
||||
|
||||
if (typeof err === 'object') {
|
||||
errString = JSON.stringify(errString)
|
||||
}
|
||||
|
||||
this.requestClient!.appendRequest(errString, sasJob, config.debug)
|
||||
|
||||
return err
|
||||
})
|
||||
}
|
||||
|
||||
private getRequestParams(config: any): any {
|
||||
|
||||
@@ -15,7 +15,8 @@ import { SASViyaApiClient } from '../SASViyaApiClient'
|
||||
import {
|
||||
isRelativePath,
|
||||
parseSasViyaDebugResponse,
|
||||
appendExtraResponseAttributes
|
||||
appendExtraResponseAttributes,
|
||||
getValidJson
|
||||
} from '../utils'
|
||||
import { BaseJobExecutor } from './JobExecutor'
|
||||
import { parseWeboutResponse } from '../utils/parseWeboutResponse'
|
||||
@@ -53,7 +54,36 @@ export class WebJobExecutor extends BaseJobExecutor {
|
||||
let apiUrl = `${config.serverUrl}${this.jobsPath}/?${'_program=' + program}`
|
||||
|
||||
if (config.serverType === ServerType.SasViya) {
|
||||
const jobUri = await this.getJobUri(sasJob)
|
||||
let jobUri
|
||||
try {
|
||||
jobUri = await this.getJobUri(sasJob)
|
||||
} catch (e: any) {
|
||||
return new Promise(async (resolve, reject) => {
|
||||
if (e instanceof LoginRequiredError) {
|
||||
this.appendWaitingRequest(() => {
|
||||
return this.execute(
|
||||
sasJob,
|
||||
data,
|
||||
config,
|
||||
loginRequiredCallback,
|
||||
authConfig,
|
||||
extraResponseAttributes
|
||||
).then(
|
||||
(res: any) => {
|
||||
resolve(res)
|
||||
},
|
||||
(err: any) => {
|
||||
reject(err)
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
await loginCallback()
|
||||
} else {
|
||||
reject(new ErrorResponse(e?.message, e))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
apiUrl += jobUri.length > 0 ? '&_job=' + jobUri : ''
|
||||
|
||||
@@ -84,13 +114,14 @@ export class WebJobExecutor extends BaseJobExecutor {
|
||||
const stringifiedData = JSON.stringify(data)
|
||||
if (
|
||||
config.serverType === ServerType.Sas9 ||
|
||||
config.serverType === ServerType.Sasjs ||
|
||||
stringifiedData.length > 500000 ||
|
||||
stringifiedData.includes(';')
|
||||
) {
|
||||
// file upload approach
|
||||
try {
|
||||
formData = generateFileUploadForm(formData, data)
|
||||
} catch (e) {
|
||||
} catch (e: any) {
|
||||
return Promise.reject(new ErrorResponse(e?.message, e))
|
||||
}
|
||||
} else {
|
||||
@@ -100,7 +131,7 @@ export class WebJobExecutor extends BaseJobExecutor {
|
||||
generateTableUploadForm(formData, data)
|
||||
formData = newFormData
|
||||
requestParams = { ...requestParams, ...params }
|
||||
} catch (e) {
|
||||
} catch (e: any) {
|
||||
return Promise.reject(new ErrorResponse(e?.message, e))
|
||||
}
|
||||
}
|
||||
@@ -113,12 +144,25 @@ export class WebJobExecutor extends BaseJobExecutor {
|
||||
}
|
||||
|
||||
const requestPromise = new Promise((resolve, reject) => {
|
||||
this.requestClient!.post(apiUrl, formData, undefined)
|
||||
// Access token is required for server type `SASjs`
|
||||
this.requestClient!.post(apiUrl, formData, authConfig?.access_token)
|
||||
.then(async (res: any) => {
|
||||
this.requestClient!.appendRequest(res, sasJob, config.debug)
|
||||
const resObj =
|
||||
this.serverType === ServerType.Sasjs
|
||||
? {
|
||||
result: res.result._webout,
|
||||
log: res.result.log
|
||||
}
|
||||
: res
|
||||
this.requestClient!.appendRequest(resObj, sasJob, config.debug)
|
||||
|
||||
let jsonResponse = res.result
|
||||
|
||||
if (this.serverType === ServerType.Sasjs) {
|
||||
const webout = parseWeboutResponse(res.result._webout, apiUrl)
|
||||
jsonResponse = getValidJson(webout)
|
||||
}
|
||||
|
||||
if (config.debug) {
|
||||
switch (this.serverType) {
|
||||
case ServerType.SasViya:
|
||||
|
||||
@@ -3,3 +3,4 @@ export * from './JesJobExecutor'
|
||||
export * from './JobExecutor'
|
||||
export * from './Sas9JobExecutor'
|
||||
export * from './WebJobExecutor'
|
||||
export * from './FileUploader'
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import axios, { AxiosInstance, AxiosRequestConfig, AxiosResponse } from 'axios'
|
||||
import { AxiosInstance, AxiosRequestConfig, AxiosResponse } from 'axios'
|
||||
import * as https from 'https'
|
||||
import { CsrfToken } from '..'
|
||||
import { isAuthorizeFormRequired, isLogInRequired } from '../auth'
|
||||
import {
|
||||
@@ -12,7 +13,11 @@ import { SASjsRequest } from '../types'
|
||||
import { parseWeboutResponse } from '../utils/parseWeboutResponse'
|
||||
import { prefixMessage } from '@sasjs/utils/error'
|
||||
import { SAS9AuthError } from '../types/errors/SAS9AuthError'
|
||||
import { parseGeneratedCode, parseSourceCode } from '../utils'
|
||||
import {
|
||||
parseGeneratedCode,
|
||||
parseSourceCode,
|
||||
createAxiosInstance
|
||||
} from '../utils'
|
||||
|
||||
export interface HttpClient {
|
||||
get<T>(
|
||||
@@ -54,12 +59,15 @@ export class RequestClient implements HttpClient {
|
||||
protected fileUploadCsrfToken: CsrfToken | undefined
|
||||
protected httpClient!: AxiosInstance
|
||||
|
||||
constructor(protected baseUrl: string, allowInsecure = false) {
|
||||
this.createHttpClient(baseUrl, allowInsecure)
|
||||
constructor(
|
||||
protected baseUrl: string,
|
||||
httpsAgentOptions?: https.AgentOptions
|
||||
) {
|
||||
this.createHttpClient(baseUrl, httpsAgentOptions)
|
||||
}
|
||||
|
||||
public setConfig(baseUrl: string, allowInsecure = false) {
|
||||
this.createHttpClient(baseUrl, allowInsecure)
|
||||
public setConfig(baseUrl: string, httpsAgentOptions?: https.AgentOptions) {
|
||||
this.createHttpClient(baseUrl, httpsAgentOptions)
|
||||
}
|
||||
|
||||
public getCsrfToken(type: 'general' | 'file' = 'general') {
|
||||
@@ -182,7 +190,7 @@ export class RequestClient implements HttpClient {
|
||||
})
|
||||
}
|
||||
|
||||
public post<T>(
|
||||
public async post<T>(
|
||||
url: string,
|
||||
data: any,
|
||||
accessToken: string | undefined,
|
||||
@@ -281,12 +289,16 @@ export class RequestClient implements HttpClient {
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await this.httpClient.post(url, content, { headers })
|
||||
const response = await this.httpClient.post(url, content, {
|
||||
headers,
|
||||
transformRequest: (requestBody) => requestBody
|
||||
})
|
||||
|
||||
return {
|
||||
result: response.data,
|
||||
etag: response.headers['etag'] as string
|
||||
}
|
||||
} catch (e) {
|
||||
} catch (e: any) {
|
||||
const response = e.response as AxiosResponse
|
||||
if (response?.status === 403 || response?.status === 449) {
|
||||
this.parseAndSetFileUploadCsrfToken(response)
|
||||
@@ -507,23 +519,18 @@ export class RequestClient implements HttpClient {
|
||||
return responseToReturn
|
||||
}
|
||||
|
||||
private createHttpClient(baseUrl: string, allowInsecure = false) {
|
||||
const https = require('https')
|
||||
if (allowInsecure && https.Agent) {
|
||||
this.httpClient = axios.create({
|
||||
baseURL: baseUrl,
|
||||
httpsAgent: new https.Agent({
|
||||
rejectUnauthorized: !allowInsecure
|
||||
})
|
||||
})
|
||||
} else {
|
||||
this.httpClient = axios.create({
|
||||
baseURL: baseUrl
|
||||
})
|
||||
}
|
||||
private createHttpClient(
|
||||
baseUrl: string,
|
||||
httpsAgentOptions?: https.AgentOptions
|
||||
) {
|
||||
const httpsAgent = httpsAgentOptions
|
||||
? new https.Agent(httpsAgentOptions)
|
||||
: undefined
|
||||
|
||||
this.httpClient = createAxiosInstance(baseUrl, httpsAgent)
|
||||
|
||||
this.httpClient.defaults.validateStatus = (status) =>
|
||||
status >= 200 && status < 305
|
||||
status >= 200 && status < 401
|
||||
}
|
||||
}
|
||||
|
||||
@@ -620,7 +627,7 @@ const parseError = (data: string) => {
|
||||
if (parts.length > 1) {
|
||||
const log = parts[1].split('<pre>')[1].split('</pre>')[0]
|
||||
const message = `This request completed with errors.`
|
||||
return new JobExecutionError(404, message, log)
|
||||
return new JobExecutionError(500, message, log)
|
||||
}
|
||||
}
|
||||
} catch (_) {}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user