1
0
mirror of https://github.com/sasjs/adapter.git synced 2025-12-11 17:34:34 +00:00

Compare commits

...

62 Commits

Author SHA1 Message Date
Krishna Acondy
596c1de5cb fix(request-client): return Internal Server Error code in case of Stored Process Error 2021-12-12 20:37:19 +00:00
Allan Bowe
b645d1495b Merge pull request #586 from sasjs/handle-sasjs-server-response
fix: handle sasjs server response
2021-12-08 09:09:42 +00:00
3a4a4c3460 chore: sasjs path optional 2021-12-06 17:44:08 +01:00
Saad Jutt
182de51f9b chore: added comments 2021-12-05 19:27:07 +05:00
Saad Jutt
712d1549c7 feat(server): added sasjs server support 2021-12-05 18:31:36 +05:00
Allan Bowe
5a478c8936 Merge pull request #594 from sasjs/issue-588
fix: sas9JobExecutor issues
2021-12-03 11:13:15 +00:00
c9ecc1dde4 chore: requestClient naming 2021-12-03 11:54:52 +01:00
bdf9e2fd5b fix: sas9JobExecutor not returning response 2021-11-26 15:32:10 +01:00
Vladislav Parhomchik
0b795b26c0 Merge pull request #589 from sasjs/issue-587
Pinned typedoc version to 0.19.2
2021-11-26 12:47:21 +03:00
96aac0cfa2 fix: Sas9JobExecutor appendRequest 2021-11-24 20:26:02 +01:00
Yury Shkoda
a82e1f33e3 docs: update docs 2021-11-24 11:23:27 +03:00
Yury Shkoda
058c887cd3 chore(docs): pinned typedoc version 2021-11-24 11:19:41 +03:00
Allan Bowe
81d959c7c1 fix: parsing _webout from response regardless of debug status 2021-11-18 19:56:12 +00:00
7c5adeabb5 feat: parse response in webJobExecutor when server type is sasjs server 2021-11-19 00:35:02 +05:00
Yury Shkoda
cb88376bda Merge pull request #576 from sasjs/update-dependencies
chore(deps): update dependencies
2021-11-01 15:50:51 +03:00
Yury Shkoda
4c8ddeca25 chore(deps): regenerate package-lock with --legacy-peer-deps option 2021-11-01 15:26:31 +03:00
Yury Shkoda
d264a3f239 chore(ci/cd): used node lts/fermium for GH actions 2021-11-01 14:58:27 +03:00
Yury Shkoda
840b1aa1bf chore(deps): regenerate package-lock.json 2021-11-01 14:47:48 +03:00
Yury Shkoda
e26fd307c8 chore(git): merge branch 'master' into update-dependencies 2021-11-01 14:45:38 +03:00
Allan Bowe
62deaf9f03 Merge pull request #581 from sasjs/package-lock-and-node-version
fix: updated package-lock + updated node version for workflow
2021-10-29 09:35:50 +01:00
Saad Jutt
865bf71f7d fix: updated package-lock + updated node version for workflow 2021-10-29 13:32:59 +05:00
Allan Bowe
734c5bccaa Merge pull request #564 from sasjs/issue-555
BREAKING CHANGE: boolean allowInsecure is replaced with Https Agent Config
2021-10-29 09:12:56 +01:00
Saad Jutt
bc82cb5f5e chore: TSDoc comment updated 2021-10-29 00:29:14 +05:00
Allan Bowe
f25c76fdfd fix: merge 2021-10-28 13:50:29 +00:00
Allan Bowe
e649b41e9e Merge pull request #579 from sasjs/sasjs/server-support
chore: removed legacy code
2021-10-28 14:02:17 +01:00
Yury Shkoda
a962979765 chore: removed legacy code 2021-10-28 15:44:17 +03:00
Yury Shkoda
01d76fa66f Merge pull request #574 from sasjs/sasjs/server
Add sasjs/server support
2021-10-28 15:36:45 +03:00
Yury Shkoda
49cfde9f7d chore(sasjs/server): fix deploy endpoint 2021-10-28 15:13:14 +03:00
Yury Shkoda
ce04ffea05 fix(SASjsApiClient): change SASjs Server endpoints 2021-10-28 10:21:15 +03:00
9dc0499f66 chore: allow insecure remove 2021-10-27 13:20:59 +02:00
Allan Bowe
bc1a7dc54f chore: lint fix 2021-10-27 09:53:42 +00:00
Allan Bowe
93c267fd4e Update AuthManager.ts 2021-10-27 12:20:40 +03:00
Yury Shkoda
0457eb6663 fix: fix calls to SASjsApi endpoints 2021-10-27 11:16:35 +03:00
Yury Shkoda
519494718b chore: address PR comments 2021-10-27 11:15:28 +03:00
Vladislav Parhomchik
de5c38f0fb chore(deps): versions fix, authmanager default returns 2021-10-26 15:46:51 +03:00
Vladislav Parhomchik
208470e7d9 chore(deps): update dependencies 2021-10-26 13:06:03 +03:00
Yury Shkoda
0321f77451 chore: update dependencies 2021-10-20 15:08:58 +03:00
Yury Shkoda
6c5fdc01eb chore: merge branch 'sasjs/server' of https://github.com/sasjs/adapter into sasjs/server 2021-10-20 15:05:03 +03:00
Yury Shkoda
2aa0cd8d7a chore: remove tmp utilities 2021-10-20 15:01:21 +03:00
Yury Shkoda
397bc4524f chore: change 'SASBase' to 'SASjs' 2021-10-20 15:00:52 +03:00
Yury Shkoda
8dce9f3e48 chore(npm): update @sasjs/utils version 2021-10-20 14:59:02 +03:00
8e9f1df1ce chore: fixing the error when using in angular, added isNode check for imports 2021-10-12 16:45:39 +02:00
Saad Jutt
ff4915f7f3 chore: comment file removed 2021-10-07 13:47:28 +05:00
Saad Jutt
6ff8eece7b chore: removed httpsAgent type + clean up 2021-10-07 13:45:50 +05:00
Saad Jutt
2849e6ed07 test(RequestClient): updated 2021-10-06 14:06:00 +05:00
Yury Shkoda
90b11fe3fa feat(deploy): add appLoc 2021-10-04 17:01:25 +03:00
Yury Shkoda
147609842d Merge pull request #565 from sasjs/modifying-npmignore
chore: add .all-contributorsrc to .npmignore
2021-10-04 09:17:54 +03:00
Yury Shkoda
dd6f9cd617 chore(npm): add empty line to the end of .npmignore 2021-10-04 09:10:37 +03:00
Saad Jutt
7f590c35da test(RequestClient): fixed to use actual axios 2021-10-01 15:42:04 +05:00
Vladislav Parhomchik
a38de108e3 chore: add .all-contributorsrc to .npmignore 2021-10-01 10:31:30 +03:00
Saad Jutt
e975e7de97 test(RequestClient): specs for communicating with self-signed server 2021-10-01 12:03:44 +05:00
Yury Shkoda
d418a7e971 fix(http): extend valid responce statuses up to 400 2021-09-30 14:33:33 +03:00
Yury Shkoda
a5b5052a5f fix(baseSAS): removed sasjs/server logic 2021-09-30 14:31:54 +03:00
Yury Shkoda
7638595523 chore(git): fix .gitignore 2021-09-30 14:30:47 +03:00
Allan Bowe
70d64f6eec Merge pull request #556 from sasjs/test-framework
chore: bump up test-framework
2021-09-29 22:42:54 +01:00
Saad Jutt
f0ecfa57e5 BREAKING CHANGE: boolean allowInsecure is replaced configuration of Https Agent 2021-09-28 16:02:12 +05:00
Yury Shkoda
5f3416ecd7 chore(utils): add tmpFolder utils 2021-09-28 10:39:54 +03:00
Yury Shkoda
d8b1a72da2 chore(types): add FileTree types 2021-09-28 10:39:18 +03:00
Yury Shkoda
7e64819eb2 feat(sasjs/server): add SASBaseApiClient class 2021-09-28 10:38:29 +03:00
Yury Shkoda
2f1d403af4 chore(deps): use @sasjs/utils tarball 2021-09-28 10:33:11 +03:00
Yury Shkoda
075d410f7d chore(git): ignore tmp folder 2021-09-28 10:31:36 +03:00
085a3f84e9 chore: bump up test-framework 2021-09-16 10:37:56 +02:00
104 changed files with 29784 additions and 3881 deletions

View File

@@ -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

View File

@@ -3,3 +3,4 @@ docs/
.github/
*.md
*.spec.ts
.all-contributorsrc

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

455
docs/modules/api_viya.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

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

20539
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -41,35 +41,39 @@
"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",
"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",
"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.22.3",
"typedoc": "0.19.2",
"typedoc-neo-theme": "^1.1.1",
"typedoc-plugin-external-module-name": "^4.0.6",
"typescript": "4.3.5",
"webpack": "^5.52.1",
"webpack": "^5.56.0",
"webpack-cli": "^4.7.2"
},
"main": "index.js",
"dependencies": {
"@sasjs/utils": "^2.30.0",
"@sasjs/utils": "^2.32.0",
"axios": "^0.21.4",
"axios-cookiejar-support": "^1.0.1",
"form-data": "^4.0.0",

View File

@@ -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",

View File

@@ -13,7 +13,6 @@ const defaultConfig: SASjsConfig = {
debug: false,
contextName: 'SAS Job Execution compute context',
useComputeApi: false,
allowInsecureRequests: false,
loginMechanism: LoginMechanism.Default
}

View File

@@ -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)
}
/**

View File

@@ -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,
@@ -29,6 +33,7 @@ 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
@@ -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)
}
/**
@@ -792,7 +799,7 @@ export default class SASjs {
sasApiClient = new SAS9ApiClient(
serverUrl,
this.jobsPath,
this.sasjsConfig.allowInsecureRequests
this.sasjsConfig.httpsAgentOptions
)
}
} else {
@@ -824,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.
@@ -951,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,
@@ -973,31 +990,45 @@ 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.serverUrl,
@@ -1018,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
View 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)
}
}

View File

@@ -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)
}

View File

@@ -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

View File

@@ -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)
}
@@ -48,6 +53,8 @@ export class Sas9JobExecutor extends BaseJobExecutor {
} 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 {

View File

@@ -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'
@@ -113,6 +114,7 @@ export class WebJobExecutor extends BaseJobExecutor {
const stringifiedData = JSON.stringify(data)
if (
config.serverType === ServerType.Sas9 ||
config.serverType === ServerType.Sasjs ||
stringifiedData.length > 500000 ||
stringifiedData.includes(';')
) {
@@ -142,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:

View File

@@ -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') {
@@ -511,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
}
}
@@ -624,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 (_) {}

View File

@@ -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

View 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)
}
)
})
}

View 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'
})
})
})

View File

@@ -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'

View File

@@ -0,0 +1,4 @@
export interface ExecutionQuery {
_program: string
_debug?: number
}

47
src/types/FileTree.ts Normal file
View 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'

View File

@@ -1,3 +1,4 @@
import * as https from 'https'
import { ServerType } from '@sasjs/utils/types'
/**
@@ -11,6 +12,11 @@ export class SASjsConfig {
* streamed.
*/
serverUrl: string = ''
/**
* The location of the STP Process Web Application. By default the adapter
* will use '/SASjsApi/stp/execute' on SAS JS.
*/
pathSASJS?: string = ''
/**
* The location of the Stored Process Web Application. By default the adapter
* will use '/SASStoredProcess/do' on SAS 9.
@@ -54,11 +60,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
*/

View File

@@ -12,3 +12,5 @@ export * from './Session'
export * from './UploadFile'
export * from './PollOptions'
export * from './WriteStream'
export * from './FileTree'
export * from './ExecuteScript'

View 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 })

Some files were not shown because too many files have changed in this diff Show More