mirror of
https://github.com/sasjs/adapter.git
synced 2025-12-11 09:24:35 +00:00
Compare commits
62 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 | ||
|
|
42d01b4044 |
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"
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
18869
package-lock.json
generated
18869
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.22.4",
|
||||
"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,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)
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
50
src/SASjs.ts
50
src/SASjs.ts
@@ -4,10 +4,14 @@ import {
|
||||
UploadFile,
|
||||
EditContextInput,
|
||||
PollOptions,
|
||||
LoginMechanism
|
||||
LoginMechanism,
|
||||
FolderMember,
|
||||
ServiceMember,
|
||||
ExecutionQuery
|
||||
} from './types'
|
||||
import { SASViyaApiClient } from './SASViyaApiClient'
|
||||
import { SAS9ApiClient } from './SAS9ApiClient'
|
||||
import { SASjsApiClient } from './SASjsApiClient'
|
||||
import { AuthManager } from './auth'
|
||||
import {
|
||||
ServerType,
|
||||
@@ -36,7 +40,6 @@ const defaultConfig: SASjsConfig = {
|
||||
debug: false,
|
||||
contextName: 'SAS Job Execution compute context',
|
||||
useComputeApi: null,
|
||||
allowInsecureRequests: false,
|
||||
loginMechanism: LoginMechanism.Default
|
||||
}
|
||||
|
||||
@@ -49,6 +52,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 +61,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
|
||||
@@ -792,7 +796,7 @@ export default class SASjs {
|
||||
sasApiClient = new SAS9ApiClient(
|
||||
serverUrl,
|
||||
this.jobsPath,
|
||||
this.sasjsConfig.allowInsecureRequests
|
||||
this.sasjsConfig.httpsAgentOptions
|
||||
)
|
||||
}
|
||||
} else {
|
||||
@@ -824,6 +828,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.
|
||||
@@ -951,12 +963,12 @@ 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
|
||||
)
|
||||
}
|
||||
|
||||
@@ -973,30 +985,44 @@ 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(
|
||||
@@ -1018,7 +1044,7 @@ export default class SASjs {
|
||||
this.sasjsConfig.serverUrl,
|
||||
this.sasjsConfig.serverType!,
|
||||
this.jobsPath,
|
||||
this.sasjsConfig.allowInsecureRequests
|
||||
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)
|
||||
}
|
||||
}
|
||||
@@ -256,6 +256,10 @@ export class AuthManager {
|
||||
.split(' ')
|
||||
.map((name: string) => name.slice(0, 3).toLowerCase())
|
||||
.join('')
|
||||
|
||||
default:
|
||||
console.error('Server Type not found in extractUserName function')
|
||||
return ''
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
|
||||
@@ -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,3 +1,4 @@
|
||||
import * as https from 'https'
|
||||
import { ServerType } from '@sasjs/utils/types'
|
||||
import * as NodeFormData from 'form-data'
|
||||
import { ErrorResponse } from '../types/errors'
|
||||
@@ -17,10 +18,10 @@ export class Sas9JobExecutor extends BaseJobExecutor {
|
||||
serverUrl: string,
|
||||
serverType: ServerType,
|
||||
private jobsPath: string,
|
||||
allowInsecureRequests: boolean
|
||||
httpsAgentOptions?: https.AgentOptions
|
||||
) {
|
||||
super(serverUrl, serverType)
|
||||
this.requestClient = new Sas9RequestClient(serverUrl, allowInsecureRequests)
|
||||
this.requestClient = new Sas9RequestClient(serverUrl, httpsAgentOptions)
|
||||
}
|
||||
|
||||
async execute(sasJob: string, data: any, config: any) {
|
||||
|
||||
@@ -53,7 +53,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 : ''
|
||||
|
||||
|
||||
@@ -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') {
|
||||
@@ -281,7 +289,11 @@ 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
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import * as https from 'https'
|
||||
import { AxiosRequestConfig } from 'axios'
|
||||
import axiosCookieJarSupport from 'axios-cookiejar-support'
|
||||
import * as tough from 'tough-cookie'
|
||||
@@ -9,8 +10,8 @@ import { RequestClient, throwIfError } from './RequestClient'
|
||||
* Handles redirects and cookie management.
|
||||
*/
|
||||
export class Sas9RequestClient extends RequestClient {
|
||||
constructor(baseUrl: string, allowInsecure = false) {
|
||||
super(baseUrl, allowInsecure)
|
||||
constructor(baseUrl: string, httpsAgentOptions?: https.AgentOptions) {
|
||||
super(baseUrl, httpsAgentOptions)
|
||||
this.httpClient.defaults.maxRedirects = 0
|
||||
this.httpClient.defaults.validateStatus = (status) =>
|
||||
status >= 200 && status < 303
|
||||
|
||||
167
src/test/RequestClient.spec.ts
Normal file
167
src/test/RequestClient.spec.ts
Normal file
@@ -0,0 +1,167 @@
|
||||
import * as pem from 'pem'
|
||||
import * as http from 'http'
|
||||
import * as https from 'https'
|
||||
import { app, mockedAuthResponse } from './SAS_server_app'
|
||||
import { ServerType } from '@sasjs/utils'
|
||||
import SASjs from '../SASjs'
|
||||
import * as axiosModules from '../utils/createAxiosInstance'
|
||||
|
||||
const axiosActual = jest.requireActual('axios')
|
||||
|
||||
jest
|
||||
.spyOn(axiosModules, 'createAxiosInstance')
|
||||
.mockImplementation((baseURL: string, httpsAgent?: https.Agent) =>
|
||||
axiosActual.create({ baseURL, httpsAgent })
|
||||
)
|
||||
|
||||
const PORT = 8000
|
||||
const SERVER_URL = `https://localhost:${PORT}/`
|
||||
|
||||
const ERROR_MESSAGES = {
|
||||
selfSigned: 'self signed certificate',
|
||||
CCA: 'unable to verify the first certificate'
|
||||
}
|
||||
|
||||
describe('RequestClient', () => {
|
||||
let server: http.Server
|
||||
|
||||
const adapter = new SASjs({
|
||||
serverUrl: `http://localhost:${PORT}/`,
|
||||
serverType: ServerType.SasViya
|
||||
})
|
||||
|
||||
beforeAll(async () => {
|
||||
await new Promise((resolve: any, reject: any) => {
|
||||
server = app
|
||||
.listen(PORT, () => resolve())
|
||||
.on('error', (err: any) => reject(err))
|
||||
})
|
||||
})
|
||||
|
||||
afterAll(() => {
|
||||
server.close()
|
||||
})
|
||||
|
||||
it('should response the POST method', async () => {
|
||||
const authResponse = await adapter.getAccessToken(
|
||||
'clientId',
|
||||
'clientSecret',
|
||||
'authCode'
|
||||
)
|
||||
|
||||
expect(authResponse.access_token).toBe(mockedAuthResponse.access_token)
|
||||
})
|
||||
|
||||
it('should response the POST method with Unauthorized', async () => {
|
||||
await expect(
|
||||
adapter.getAccessToken('clientId', 'clientSecret', 'incorrect')
|
||||
).rejects.toThrow(
|
||||
'Error while getting access token. Request failed with status code 401'
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('RequestClient - Self Signed Server', () => {
|
||||
let adapter: SASjs
|
||||
|
||||
let httpsServer: https.Server
|
||||
let sslConfig: pem.CertificateCreationResult
|
||||
|
||||
beforeAll(async () => {
|
||||
;({ httpsServer, keys: sslConfig } = await setupSelfSignedServer())
|
||||
await new Promise((resolve: any, reject: any) => {
|
||||
httpsServer
|
||||
.listen(PORT, () => resolve())
|
||||
.on('error', (err: any) => reject(err))
|
||||
})
|
||||
|
||||
adapter = new SASjs({
|
||||
serverUrl: SERVER_URL,
|
||||
serverType: ServerType.SasViya,
|
||||
httpsAgentOptions: { ca: [sslConfig.certificate] }
|
||||
})
|
||||
})
|
||||
|
||||
afterAll(() => {
|
||||
httpsServer.close()
|
||||
})
|
||||
|
||||
it('should throw error for not providing certificate', async () => {
|
||||
const adapterWithoutCertificate = new SASjs({
|
||||
serverUrl: SERVER_URL,
|
||||
serverType: ServerType.SasViya
|
||||
})
|
||||
|
||||
await expect(
|
||||
adapterWithoutCertificate.getAccessToken(
|
||||
'clientId',
|
||||
'clientSecret',
|
||||
'authCode'
|
||||
)
|
||||
).rejects.toThrow(
|
||||
`Error while getting access token. ${ERROR_MESSAGES.selfSigned}`
|
||||
)
|
||||
})
|
||||
|
||||
it('should response the POST method using insecure flag', async () => {
|
||||
const adapterAllowInsecure = new SASjs({
|
||||
serverUrl: SERVER_URL,
|
||||
serverType: ServerType.SasViya,
|
||||
httpsAgentOptions: { rejectUnauthorized: false }
|
||||
})
|
||||
|
||||
const authResponse = await adapterAllowInsecure.getAccessToken(
|
||||
'clientId',
|
||||
'clientSecret',
|
||||
'authCode'
|
||||
)
|
||||
|
||||
expect(authResponse.access_token).toBe(mockedAuthResponse.access_token)
|
||||
})
|
||||
|
||||
it('should response the POST method', async () => {
|
||||
const authResponse = await adapter.getAccessToken(
|
||||
'clientId',
|
||||
'clientSecret',
|
||||
'authCode'
|
||||
)
|
||||
|
||||
expect(authResponse.access_token).toBe(mockedAuthResponse.access_token)
|
||||
})
|
||||
|
||||
it('should response the POST method with Unauthorized', async () => {
|
||||
await expect(
|
||||
adapter.getAccessToken('clientId', 'clientSecret', 'incorrect')
|
||||
).rejects.toThrow(
|
||||
'Error while getting access token. Request failed with status code 401'
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
const setupSelfSignedServer = async (): Promise<{
|
||||
httpsServer: https.Server
|
||||
keys: pem.CertificateCreationResult
|
||||
}> => {
|
||||
return await new Promise(async (resolve) => {
|
||||
const keys = await createCertificate()
|
||||
|
||||
const httpsServer = https.createServer(
|
||||
{ key: keys.clientKey, cert: keys.certificate },
|
||||
app
|
||||
)
|
||||
|
||||
resolve({ httpsServer, keys })
|
||||
})
|
||||
}
|
||||
|
||||
const createCertificate = async (): Promise<pem.CertificateCreationResult> => {
|
||||
return await new Promise((resolve, reject) => {
|
||||
pem.createCertificate(
|
||||
{ days: 1, selfSigned: true },
|
||||
(error: any, keys: pem.CertificateCreationResult) => {
|
||||
if (error) reject(false)
|
||||
resolve(keys)
|
||||
}
|
||||
)
|
||||
})
|
||||
}
|
||||
38
src/test/SAS_server_app.ts
Normal file
38
src/test/SAS_server_app.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
import express = require('express')
|
||||
|
||||
export const app = express()
|
||||
|
||||
export const mockedAuthResponse = {
|
||||
access_token: 'access_token',
|
||||
token_type: 'bearer',
|
||||
id_token: 'id_token',
|
||||
refresh_token: 'refresh_token',
|
||||
expires_in: 43199,
|
||||
scope: 'openid',
|
||||
jti: 'jti'
|
||||
}
|
||||
|
||||
app.get('/', function (req: any, res: any) {
|
||||
res.send('Hello World')
|
||||
})
|
||||
|
||||
app.post('/SASLogon/oauth/token', function (req: any, res: any) {
|
||||
let valid = true
|
||||
// capture the encoded form data
|
||||
req.on('data', (data: any) => {
|
||||
const resData = data.toString()
|
||||
|
||||
if (resData.includes('incorrect')) valid = false
|
||||
})
|
||||
|
||||
// send a response when finished reading
|
||||
// the encoded form data
|
||||
req.on('end', () => {
|
||||
if (valid) res.status(200).send(mockedAuthResponse)
|
||||
else
|
||||
res.status(401).send({
|
||||
error: 'unauthorized',
|
||||
error_description: 'Bad credentials'
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -1,6 +1,5 @@
|
||||
import { SessionManager } from '../SessionManager'
|
||||
import { RequestClient } from '../request/RequestClient'
|
||||
import { NoSessionStateError } from '../types/errors'
|
||||
import * as dotenv from 'dotenv'
|
||||
import axios from 'axios'
|
||||
import { Logger, LogLevel } from '@sasjs/utils'
|
||||
|
||||
4
src/types/ExecuteScript.ts
Normal file
4
src/types/ExecuteScript.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export interface ExecutionQuery {
|
||||
_program: string
|
||||
_debug?: number
|
||||
}
|
||||
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,3 +1,4 @@
|
||||
import * as https from 'https'
|
||||
import { ServerType } from '@sasjs/utils/types'
|
||||
|
||||
/**
|
||||
@@ -54,11 +55,11 @@ export class SASjsConfig {
|
||||
*/
|
||||
useComputeApi: boolean | null = null
|
||||
/**
|
||||
* Defaults to `false`.
|
||||
* When set to `true`, the adapter will allow requests to SAS servers that use a self-signed SSL certificate.
|
||||
* Changing this setting is not recommended.
|
||||
* Optional settings to configure HTTPS Agent.
|
||||
* By providing `key`, `cert`, `ca` to connect with server
|
||||
* Other options can be set `rejectUnauthorized` and `requestCert`
|
||||
*/
|
||||
allowInsecureRequests = false
|
||||
httpsAgentOptions?: https.AgentOptions
|
||||
/**
|
||||
* Supported login mechanisms are - Redirected and Default
|
||||
*/
|
||||
|
||||
@@ -12,3 +12,5 @@ export * from './Session'
|
||||
export * from './UploadFile'
|
||||
export * from './PollOptions'
|
||||
export * from './WriteStream'
|
||||
export * from './FileTree'
|
||||
export * from './ExecuteScript'
|
||||
|
||||
7
src/utils/createAxiosInstance.ts
Normal file
7
src/utils/createAxiosInstance.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import axios from 'axios'
|
||||
import * as https from 'https'
|
||||
|
||||
export const createAxiosInstance = (
|
||||
baseURL: string,
|
||||
httpsAgent?: https.Agent
|
||||
) => axios.create({ baseURL, httpsAgent })
|
||||
@@ -1,6 +1,7 @@
|
||||
export * from './asyncForEach'
|
||||
export * from './compareTimestamps'
|
||||
export * from './convertToCsv'
|
||||
export * from './createAxiosInstance'
|
||||
export * from './delay'
|
||||
export * from './isNode'
|
||||
export * from './isRelativePath'
|
||||
|
||||
@@ -25,6 +25,6 @@ export const parseSasViyaDebugResponse = async (
|
||||
}
|
||||
|
||||
return requestClient
|
||||
.get(serverUrl + jsonUrl, undefined)
|
||||
.get(serverUrl + jsonUrl, undefined, 'text/plain')
|
||||
.then((res: any) => getValidJson(res.result))
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user