mirror of
https://github.com/sasjs/adapter.git
synced 2025-12-11 01:14:36 +00:00
Compare commits
42 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6aee95b21d | ||
| 3d281abbf8 | |||
| 99d783e174 | |||
| 17a3d1b8a9 | |||
|
|
01af5eb634 | ||
| 0c3797e2de | |||
| c33c509207 | |||
| af351d7375 | |||
| 2b53406cac | |||
| 99cfb8b2af | |||
|
|
22fa185715 | ||
|
|
dad99557a7 | ||
| c7cc2e5fa4 | |||
|
|
2bd7544051 | ||
| 1fb972d88a | |||
| 64f8f8c893 | |||
|
|
ddb4a51c55 | ||
| 921d6ef364 | |||
|
|
105675d46a | ||
|
|
e4addba762 | ||
| 8203e918fd | |||
|
|
2210e43880 | ||
|
|
b04df0bc6d | ||
| 98e851b4d8 | |||
| 84306bea3d | |||
| 89d32262f8 | |||
| 257010f57d | |||
| eb9991015b | |||
|
|
9d17e87a09 | ||
| 55f309e998 | |||
| 3d9b40398c | |||
|
|
e0badae973 | ||
|
|
524c561390 | ||
| e7ceac1b78 | |||
|
|
72ddd424a5 | ||
| 85f771d1ed | |||
|
|
1a781c3a56 | ||
|
|
296d4efdfb | ||
| 8df09d01de | |||
|
|
2d4a9d6dee | ||
|
|
38c30f6342 | ||
|
|
dd72304bc7 |
12
.github/issue_template.md
vendored
Normal file
12
.github/issue_template.md
vendored
Normal file
@@ -0,0 +1,12 @@
|
||||
## Expected behaviour
|
||||
*Describe what should be happening*
|
||||
|
||||
## Current behaviour
|
||||
*Describe what is actually happening*
|
||||
|
||||
## Environment info
|
||||
**Client tech stack**: *Angular, React, Vue, VanillaJS, NodeJS etc.*
|
||||
**Server type**: SASJS|SASVIYA|SAS9
|
||||
**Login mechanism**: Default|Redirected
|
||||
**Debug**: true|false
|
||||
**Use Compute Api (relevant only on VIYA)**: true|false
|
||||
30
.github/vpn/config.ovpn
vendored
30
.github/vpn/config.ovpn
vendored
@@ -1,30 +0,0 @@
|
||||
cipher AES-256-CBC
|
||||
setenv FORWARD_COMPATIBLE 1
|
||||
client
|
||||
server-poll-timeout 4
|
||||
nobind
|
||||
remote vpn.analytium.co.uk 1194 udp
|
||||
remote vpn.analytium.co.uk 1194 udp
|
||||
remote vpn.analytium.co.uk 443 tcp
|
||||
remote vpn.analytium.co.uk 1194 udp
|
||||
remote vpn.analytium.co.uk 1194 udp
|
||||
remote vpn.analytium.co.uk 1194 udp
|
||||
remote vpn.analytium.co.uk 1194 udp
|
||||
remote vpn.analytium.co.uk 1194 udp
|
||||
dev tun
|
||||
dev-type tun
|
||||
ns-cert-type server
|
||||
setenv opt tls-version-min 1.0 or-highest
|
||||
reneg-sec 604800
|
||||
sndbuf 0
|
||||
rcvbuf 0
|
||||
# NOTE: LZO commands are pushed by the Access Server at connect time.
|
||||
# NOTE: The below line doesn't disable LZO.
|
||||
comp-lzo no
|
||||
verb 3
|
||||
setenv PUSH_PEER_INFO
|
||||
|
||||
ca ca.crt
|
||||
cert user.crt
|
||||
key user.key
|
||||
tls-auth tls.key 1
|
||||
25
.github/workflows/build.yml
vendored
25
.github/workflows/build.yml
vendored
@@ -45,31 +45,6 @@ jobs:
|
||||
key: ${{ secrets.DCGITLAB_KEY }}
|
||||
known_hosts: 'placeholder'
|
||||
|
||||
- name: Write VPN Files
|
||||
run: |
|
||||
echo "$CA_CRT" > .github/vpn/ca.crt
|
||||
echo "$USER_CRT" > .github/vpn/user.crt
|
||||
echo "$USER_KEY" > .github/vpn/user.key
|
||||
echo "$TLS_KEY" > .github/vpn/tls.key
|
||||
shell: bash
|
||||
env:
|
||||
CA_CRT: ${{ secrets.CA_CRT}}
|
||||
USER_CRT: ${{ secrets.USER_CRT }}
|
||||
USER_KEY: ${{ secrets.USER_KEY }}
|
||||
TLS_KEY: ${{ secrets.TLS_KEY }}
|
||||
|
||||
- name: Install Open VPN
|
||||
run: |
|
||||
sudo apt install apt-transport-https
|
||||
sudo wget https://swupdate.openvpn.net/repos/openvpn-repo-pkg-key.pub
|
||||
sudo apt-key add openvpn-repo-pkg-key.pub
|
||||
sudo wget -O /etc/apt/sources.list.d/openvpn3.list https://swupdate.openvpn.net/community/openvpn3/repos/openvpn3-focal.list
|
||||
sudo apt update
|
||||
sudo apt install openvpn3=16~beta+focal
|
||||
|
||||
- name: Start Open VPN 3
|
||||
run: openvpn3 session-start --config .github/vpn/config.ovpn
|
||||
|
||||
- name: Deploy sasjs-tests
|
||||
run: |
|
||||
npm install -g replace-in-files-cli
|
||||
|
||||
4
.github/workflows/npmpublish.yml
vendored
4
.github/workflows/npmpublish.yml
vendored
@@ -34,10 +34,10 @@ jobs:
|
||||
run: npm run build
|
||||
|
||||
- name: Semantic Release
|
||||
uses: cycjimmy/semantic-release-action@v2
|
||||
uses: cycjimmy/semantic-release-action@v3
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GH_TOKEN }}
|
||||
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
|
||||
- name: Send Matrix message
|
||||
run: curl -XPOST -d "{\"msgtype\":\"m.text\", \"body\":\"New version of @sasjs/adapter has been released! \n Please deploy and run 'dctests' with new adapter to make sure everything is still in place.\"}" https://matrix.4gl.io/_matrix/client/r0/rooms/!BDUPBPEGVvRLKLQUxY:4gl.io/send/m.room.message?access_token=${{ secrets.MATRIX_TOKEN }}
|
||||
run: curl -XPOST -d "{\"msgtype\":\"m.text\", \"body\":\"New version of @sasjs/adapter has been released! \n Please deploy and run 'dctests' with new adapter to make sure everything is still in place.\"}" https://matrix.4gl.io/_matrix/client/r0/rooms/!jRebyiGmHZlpfDwYXN:4gl.io/send/m.room.message?access_token=${{ secrets.MATRIX_TOKEN }}
|
||||
|
||||
@@ -20,7 +20,7 @@ SASjs is a open-source framework for building Web Apps on SAS® platforms. You c
|
||||
|
||||
1 - `npm install @sasjs/adapter` - for use in a nodeJS project (recommended)
|
||||
|
||||
2 - [Download](https://cdn.jsdelivr.net/npm/@sasjs/adapter@3/index.min.js) and use a copy of the latest JS file
|
||||
2 - [Download](https://cdn.jsdelivr.net/npm/@sasjs/adapter@4/index.min.js) and use a copy of the latest JS file
|
||||
|
||||
3 - Reference directly from the CDN - in which case click [here](https://www.jsdelivr.com/package/npm/@sasjs/adapter?tab=collection) and select "SRI" to get the script tag with the integrity hash.
|
||||
|
||||
|
||||
7326
package-lock.json
generated
7326
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
16
package.json
16
package.json
@@ -45,9 +45,7 @@
|
||||
"license": "ISC",
|
||||
"devDependencies": {
|
||||
"@cypress/webpack-preprocessor": "5.9.1",
|
||||
"@types/axios": "0.14.0",
|
||||
"@types/express": "4.17.13",
|
||||
"@types/form-data": "2.5.0",
|
||||
"@types/jest": "27.4.0",
|
||||
"@types/mime": "2.0.3",
|
||||
"@types/pem": "1.9.6",
|
||||
@@ -61,26 +59,26 @@
|
||||
"jest-extended": "2.0.0",
|
||||
"node-polyfill-webpack-plugin": "1.1.4",
|
||||
"path": "0.12.7",
|
||||
"pem": "1.14.6",
|
||||
"pem": "1.14.5",
|
||||
"prettier": "2.7.1",
|
||||
"process": "0.11.10",
|
||||
"rimraf": "3.0.2",
|
||||
"semantic-release": "19.0.3",
|
||||
"terser-webpack-plugin": "5.3.1",
|
||||
"terser-webpack-plugin": "5.3.6",
|
||||
"ts-jest": "27.1.3",
|
||||
"ts-loader": "9.4.0",
|
||||
"tslint": "6.1.3",
|
||||
"tslint-config-prettier": "1.18.0",
|
||||
"typedoc": "0.23.15",
|
||||
"typedoc-plugin-rename-defaults": "0.4.0",
|
||||
"typedoc": "0.23.24",
|
||||
"typedoc-plugin-rename-defaults": "0.6.4",
|
||||
"typescript": "4.8.3",
|
||||
"webpack": "5.69.0",
|
||||
"webpack": "5.76.2",
|
||||
"webpack-cli": "4.9.2"
|
||||
},
|
||||
"main": "index.js",
|
||||
"dependencies": {
|
||||
"@sasjs/utils": "2.48.2",
|
||||
"axios": "0.26.0",
|
||||
"@sasjs/utils": "2.52.0",
|
||||
"axios": "0.27.2",
|
||||
"axios-cookiejar-support": "1.0.1",
|
||||
"form-data": "4.0.0",
|
||||
"https": "1.0.0",
|
||||
|
||||
863
sasjs-tests/package-lock.json
generated
863
sasjs-tests/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -5,7 +5,7 @@
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@sasjs/adapter": "file:../build/sasjs-adapter-5.0.0.tgz",
|
||||
"@sasjs/test-framework": "^1.5.6",
|
||||
"@sasjs/test-framework": "1.5.7",
|
||||
"@types/jest": "^26.0.20",
|
||||
"@types/node": "^14.14.41",
|
||||
"@types/react": "^17.0.1",
|
||||
@@ -43,6 +43,6 @@
|
||||
]
|
||||
},
|
||||
"devDependencies": {
|
||||
"node-sass": "^7.0.1"
|
||||
"node-sass": "7.0.3"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ if npm run cy:run -- --spec "cypress/integration/sasjs.tests.ts" ; then
|
||||
echo "Cypress sasjs testing passed!"
|
||||
else
|
||||
echo '{"msgtype":"m.text", "body":"Automated sasjs-tests failed on the @sasjs/adapter PR: '$2'"}'
|
||||
curl -XPOST -d '{"msgtype":"m.text", "body":"Automated sasjs-tests failed on the @sasjs/adapter PR: '$2'"}' https://matrix.4gl.io/_matrix/client/r0/rooms/%21BDUPBPEGVvRLKLQUxY:4gl.io/send/m.room.message?access_token=$1
|
||||
curl -XPOST -d '{"msgtype":"m.text", "body":"Automated sasjs-tests failed on the @sasjs/adapter PR: '$2'"}' https://matrix.4gl.io/_matrix/client/r0/rooms/%21jRebyiGmHZlpfDwYXN:4gl.io:4gl.io/send/m.room.message?access_token=$1
|
||||
echo "Cypress sasjs testing failed!"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
@@ -4,8 +4,7 @@ import {
|
||||
UploadFile,
|
||||
EditContextInput,
|
||||
PollOptions,
|
||||
LoginMechanism,
|
||||
ExecutionQuery
|
||||
LoginMechanism
|
||||
} from './types'
|
||||
import { SASViyaApiClient } from './SASViyaApiClient'
|
||||
import { SAS9ApiClient } from './SAS9ApiClient'
|
||||
@@ -17,7 +16,6 @@ import {
|
||||
AuthConfig,
|
||||
ExtraResponseAttributes,
|
||||
SasAuthResponse,
|
||||
ServicePackSASjs,
|
||||
AuthConfigSas9
|
||||
} from '@sasjs/utils/types'
|
||||
import { RequestClient } from './request/RequestClient'
|
||||
@@ -895,6 +893,7 @@ export default class SASjs {
|
||||
await this.computeJobExecutor?.resendWaitingRequests()
|
||||
await this.jesJobExecutor?.resendWaitingRequests()
|
||||
await this.fileUploader?.resendWaitingRequests()
|
||||
await this.sasjsJobExecutor?.resendWaitingRequests()
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import * as NodeFormData from 'form-data'
|
||||
import { AuthConfig, ServerType, ServicePackSASjs } from '@sasjs/utils/types'
|
||||
import { ExecutionQuery } from './types'
|
||||
import { RequestClient } from './request/RequestClient'
|
||||
@@ -8,6 +9,18 @@ import { getTokens } from './auth/getTokens'
|
||||
export class SASjsApiClient {
|
||||
constructor(private requestClient: RequestClient) {}
|
||||
|
||||
private async getAccessTokenForRequest(authConfig?: AuthConfig) {
|
||||
if (authConfig) {
|
||||
const { access_token } = await getTokens(
|
||||
this.requestClient,
|
||||
authConfig,
|
||||
ServerType.Sasjs
|
||||
)
|
||||
|
||||
return access_token
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates the folders and services at the given location `appLoc` on the given server `serverUrl`.
|
||||
* @param dataJson - the JSON specifying the folders and files to be created, can also includes
|
||||
@@ -20,15 +33,7 @@ export class SASjsApiClient {
|
||||
appLoc: string,
|
||||
authConfig?: AuthConfig
|
||||
) {
|
||||
let access_token = (authConfig || {}).access_token
|
||||
if (authConfig) {
|
||||
;({ access_token } = await getTokens(
|
||||
this.requestClient,
|
||||
authConfig,
|
||||
ServerType.Sasjs
|
||||
))
|
||||
}
|
||||
|
||||
const access_token = await this.getAccessTokenForRequest(authConfig)
|
||||
dataJson.appLoc = dataJson.appLoc || appLoc
|
||||
|
||||
const { result } = await this.requestClient.post<{
|
||||
@@ -48,6 +53,40 @@ export class SASjsApiClient {
|
||||
return Promise.resolve(result)
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates/updates files within SASjs drive using uploaded json compressed file.
|
||||
* @param zipFilePath - Compressed file path; file should only contain one JSON file and
|
||||
* should have same name as of compressed file e.g. deploy.JSON should be compressed to deploy.JSON.zip
|
||||
* Any other file or JSON file in zipped will be ignored!
|
||||
* @param authConfig - (optional) a valid client, secret, refresh and access tokens that are authorised to execute compute jobs.
|
||||
*/
|
||||
public async deployZipFile(zipFilePath: string, authConfig?: AuthConfig) {
|
||||
const { createReadStream } = require('@sasjs/utils/file')
|
||||
const access_token = await this.getAccessTokenForRequest(authConfig)
|
||||
|
||||
const file = await createReadStream(zipFilePath)
|
||||
const formData = new NodeFormData()
|
||||
formData.append('file', file)
|
||||
|
||||
const contentType = `multipart/form-data; boundary=${formData.getBoundary()}`
|
||||
|
||||
const { result } = await this.requestClient.post<{
|
||||
status: string
|
||||
message: string
|
||||
streamServiceName?: string
|
||||
example?: {}
|
||||
}>(
|
||||
'SASjsApi/drive/deploy/upload',
|
||||
formData,
|
||||
access_token,
|
||||
contentType,
|
||||
{},
|
||||
{ maxContentLength: Infinity, maxBodyLength: Infinity }
|
||||
)
|
||||
|
||||
return Promise.resolve(result)
|
||||
}
|
||||
|
||||
public async executeJob(
|
||||
query: ExecutionQuery,
|
||||
appLoc: string,
|
||||
@@ -80,14 +119,7 @@ export class SASjsApiClient {
|
||||
runTime: string = 'sas',
|
||||
authConfig?: AuthConfig
|
||||
) {
|
||||
let access_token = (authConfig || {}).access_token
|
||||
if (authConfig) {
|
||||
;({ access_token } = await getTokens(
|
||||
this.requestClient,
|
||||
authConfig,
|
||||
ServerType.Sasjs
|
||||
))
|
||||
}
|
||||
const access_token = await this.getAccessTokenForRequest(authConfig)
|
||||
|
||||
let parsedSasjsServerLog = ''
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { ServerType } from '@sasjs/utils/types'
|
||||
import { RequestClient } from '../request/RequestClient'
|
||||
import { NotFoundError } from '../types/errors'
|
||||
import { LoginOptions, LoginResult, LoginResultInternal } from '../types/Login'
|
||||
import { serialize } from '../utils'
|
||||
import { extractUserLongNameSas9 } from '../utils/sas9/extractUserLongNameSas9'
|
||||
@@ -42,6 +43,9 @@ export class AuthManager {
|
||||
} = await this.fetchUserName()
|
||||
|
||||
if (isLoggedInAlready) {
|
||||
const logger = process.logger || console
|
||||
logger.log('login was not attempted as a valid session already exists')
|
||||
|
||||
await this.loginCallback()
|
||||
|
||||
return {
|
||||
@@ -109,6 +113,9 @@ export class AuthManager {
|
||||
} = await this.checkSession()
|
||||
|
||||
if (isLoggedInAlready) {
|
||||
const logger = process.logger || console
|
||||
logger.log('login was not attempted as a valid session already exists')
|
||||
|
||||
await this.loginCallback()
|
||||
|
||||
this.userName = loginParams.username
|
||||
@@ -155,10 +162,12 @@ export class AuthManager {
|
||||
private async performCASSecurityCheck() {
|
||||
const casAuthenticationUrl = `${this.serverUrl}/SASStoredProcess/j_spring_cas_security_check`
|
||||
|
||||
await this.requestClient.get<string>(
|
||||
`/SASLogon/login?service=${casAuthenticationUrl}`,
|
||||
undefined
|
||||
)
|
||||
await this.requestClient
|
||||
.get<string>(`/SASLogon/login?service=${casAuthenticationUrl}`, undefined)
|
||||
.catch((err) => {
|
||||
// ignore if resource not found error
|
||||
if (!(err instanceof NotFoundError)) throw err
|
||||
})
|
||||
}
|
||||
|
||||
private async sendLoginRequest(
|
||||
@@ -239,7 +248,7 @@ export class AuthManager {
|
||||
}
|
||||
|
||||
const { result: formResponse } = await this.requestClient.get<string>(
|
||||
this.loginUrl.replace('.do', ''),
|
||||
this.loginUrl.replace('/SASLogon/login.do', '/SASLogon/login'),
|
||||
undefined,
|
||||
'text/plain'
|
||||
)
|
||||
@@ -312,12 +321,13 @@ export class AuthManager {
|
||||
}
|
||||
|
||||
private getLoginForm(response: any) {
|
||||
const pattern: RegExp = /<form.+action="(.*Logon[^"]*).*>/
|
||||
const pattern: RegExp = /<form.+action="(.*(Logon)|(login)[^"]*).*>/
|
||||
const matches = pattern.exec(response)
|
||||
const formInputs: any = {}
|
||||
|
||||
if (matches && matches.length) {
|
||||
this.setLoginUrl(matches)
|
||||
response = response.replace(/<input/g, '\n<input')
|
||||
const inputs = response.match(/<input.*"hidden"[^>]*>/g)
|
||||
|
||||
if (inputs) {
|
||||
@@ -348,7 +358,7 @@ export class AuthManager {
|
||||
this.loginUrl =
|
||||
this.serverType === ServerType.SasViya
|
||||
? tempLoginLink
|
||||
: loginUrl.replace('.do', '')
|
||||
: loginUrl.replace('/SASLogon/login.do', '/SASLogon/login')
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
export const isLogInRequired = (response: string): boolean => {
|
||||
const pattern: RegExp = /<form.+action="(.*Logon[^"]*).*>/gm
|
||||
const pattern: RegExp = /<form.+action="(.*(Logon)|(login)[^"]*).*>/gm
|
||||
const matches = pattern.test(response)
|
||||
return matches
|
||||
}
|
||||
|
||||
273
src/minified/sas9/SASjs.ts
Normal file
273
src/minified/sas9/SASjs.ts
Normal file
@@ -0,0 +1,273 @@
|
||||
import { validateInput, compareTimestamps } from '../../utils'
|
||||
import { SASjsConfig, UploadFile, LoginMechanism } from '../../types'
|
||||
import { AuthManager } from '../../auth'
|
||||
import {
|
||||
ServerType,
|
||||
AuthConfig,
|
||||
ExtraResponseAttributes
|
||||
} from '@sasjs/utils/types'
|
||||
import { RequestClient } from '../../request/RequestClient'
|
||||
import { FileUploader } from '../../job-execution/FileUploader'
|
||||
import { WebJobExecutor } from './WebJobExecutor'
|
||||
import { ErrorResponse } from '../../types/errors/ErrorResponse'
|
||||
import { LoginOptions, LoginResult } from '../../types/Login'
|
||||
|
||||
const defaultConfig: SASjsConfig = {
|
||||
serverUrl: '',
|
||||
pathSASJS: '/SASjsApi/stp/execute',
|
||||
pathSAS9: '/SASStoredProcess/do',
|
||||
pathSASViya: '/SASJobExecution',
|
||||
appLoc: '/Public/seedapp',
|
||||
serverType: ServerType.Sas9,
|
||||
debug: false,
|
||||
contextName: 'SAS Job Execution compute context',
|
||||
useComputeApi: null,
|
||||
loginMechanism: LoginMechanism.Default
|
||||
}
|
||||
|
||||
/**
|
||||
* SASjs is a JavaScript adapter for SAS.
|
||||
*
|
||||
*/
|
||||
export default class SASjs {
|
||||
private sasjsConfig: SASjsConfig = new SASjsConfig()
|
||||
private jobsPath: string = ''
|
||||
private fileUploader: FileUploader | null = null
|
||||
private authManager: AuthManager | null = null
|
||||
private requestClient: RequestClient | null = null
|
||||
private webJobExecutor: WebJobExecutor | null = null
|
||||
|
||||
constructor(config?: Partial<SASjsConfig>) {
|
||||
this.sasjsConfig = {
|
||||
...defaultConfig,
|
||||
...config
|
||||
}
|
||||
|
||||
this.setupConfiguration()
|
||||
}
|
||||
|
||||
/**
|
||||
* Logs into the SAS server with the supplied credentials.
|
||||
* @param username - a string representing the username.
|
||||
* @param password - a string representing the password.
|
||||
* @param clientId - a string representing the client ID.
|
||||
*/
|
||||
public async logIn(
|
||||
username?: string,
|
||||
password?: string,
|
||||
clientId?: string,
|
||||
options: LoginOptions = {}
|
||||
): Promise<LoginResult> {
|
||||
if (this.sasjsConfig.loginMechanism === LoginMechanism.Default) {
|
||||
if (!username || !password)
|
||||
throw new Error(
|
||||
'A username and password are required when using the default login mechanism.'
|
||||
)
|
||||
|
||||
return this.authManager!.logIn(username, password)
|
||||
}
|
||||
|
||||
if (typeof window === typeof undefined) {
|
||||
throw new Error(
|
||||
'The redirected login mechanism is only available for use in the browser.'
|
||||
)
|
||||
}
|
||||
|
||||
return this.authManager!.redirectedLogIn(options)
|
||||
}
|
||||
|
||||
/**
|
||||
* Logs out of the configured SAS server.
|
||||
*/
|
||||
public logOut() {
|
||||
return this.authManager!.logOut()
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the current SASjs configuration.
|
||||
*
|
||||
*/
|
||||
public getSasjsConfig() {
|
||||
return this.sasjsConfig
|
||||
}
|
||||
|
||||
/**
|
||||
* this method returns an array of SASjsRequest
|
||||
* @returns SASjsRequest[]
|
||||
*/
|
||||
public getSasRequests() {
|
||||
const requests = [...this.requestClient!.getRequests()]
|
||||
const sortedRequests = requests.sort(compareTimestamps)
|
||||
return sortedRequests
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the debug state. Turning this on will enable additional logging in the adapter.
|
||||
* @param value - boolean indicating debug state (on/off).
|
||||
*/
|
||||
public setDebugState(value: boolean) {
|
||||
this.sasjsConfig.debug = value
|
||||
}
|
||||
|
||||
/**
|
||||
* Uploads a file to the given service.
|
||||
* @param sasJob - the path to the SAS program (ultimately resolves to
|
||||
* the SAS `_program` parameter to run a Job Definition or SAS 9 Stored
|
||||
* 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 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 async uploadFile(
|
||||
sasJob: string,
|
||||
files: UploadFile[],
|
||||
params: { [key: string]: any } | null,
|
||||
config: { [key: string]: any } = {},
|
||||
loginRequiredCallback?: () => any
|
||||
) {
|
||||
config = {
|
||||
...this.sasjsConfig,
|
||||
...config
|
||||
}
|
||||
const data = { files, params }
|
||||
|
||||
return await this.fileUploader!.execute(
|
||||
sasJob,
|
||||
data,
|
||||
config,
|
||||
loginRequiredCallback
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Makes a request to program specified in `SASjob` (could be a Viya Job, a
|
||||
* SAS 9 Stored Process, or a SASjs Server Stored Program). The response
|
||||
* object will always contain table names in lowercase, and column names in
|
||||
* uppercase. Values are returned formatted by default, unformatted
|
||||
* values can be configured as an option in the `%webout` macro.
|
||||
*
|
||||
* @param sasJob - the path to the SAS program (ultimately resolves to
|
||||
* the SAS `_program` parameter to run a Job Definition or SAS 9 Stored
|
||||
* Process). Is prepended at runtime with the value of `appLoc`.
|
||||
* @param data - a JSON object containing one or more tables to be sent to
|
||||
* SAS. For an example of the table structure, see the project README. This
|
||||
* value can be `null` if no inputs are required.
|
||||
* @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.
|
||||
* When using a `loginRequiredCallback`, the call to the request will look, for example, like so:
|
||||
* `await request(sasJobPath, data, config, () => setIsLoggedIn(false))`
|
||||
* If you are not passing in any data and configuration, it will look like so:
|
||||
* `await request(sasJobPath, {}, {}, () => setIsLoggedIn(false))`
|
||||
* @param extraResponseAttributes - a array of predefined values that are used
|
||||
* to provide extra attributes (same names as those values) to be added in response
|
||||
* Supported values are declared in ExtraResponseAttributes type.
|
||||
*/
|
||||
public async request(
|
||||
sasJob: string,
|
||||
data: { [key: string]: any } | null,
|
||||
config: { [key: string]: any } = {},
|
||||
loginRequiredCallback?: () => any,
|
||||
authConfig?: AuthConfig,
|
||||
extraResponseAttributes: ExtraResponseAttributes[] = []
|
||||
) {
|
||||
config = {
|
||||
...this.sasjsConfig,
|
||||
...config
|
||||
}
|
||||
|
||||
const validationResult = validateInput(data)
|
||||
|
||||
// status is true if the data passes validation checks above
|
||||
if (validationResult.status) {
|
||||
return await this.webJobExecutor!.execute(
|
||||
sasJob,
|
||||
data,
|
||||
config,
|
||||
loginRequiredCallback,
|
||||
authConfig,
|
||||
extraResponseAttributes
|
||||
)
|
||||
} else {
|
||||
return Promise.reject(new ErrorResponse(validationResult.msg))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether a session is active, or login is required.
|
||||
* @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()
|
||||
}
|
||||
|
||||
private setupConfiguration() {
|
||||
if (
|
||||
this.sasjsConfig.serverUrl === undefined ||
|
||||
this.sasjsConfig.serverUrl === ''
|
||||
) {
|
||||
if (typeof location !== 'undefined') {
|
||||
let url = `${location.protocol}//${location.hostname}`
|
||||
|
||||
if (location.port) url = `${url}:${location.port}`
|
||||
|
||||
this.sasjsConfig.serverUrl = url
|
||||
} else {
|
||||
this.sasjsConfig.serverUrl = ''
|
||||
}
|
||||
}
|
||||
|
||||
if (this.sasjsConfig.serverUrl.slice(-1) === '/') {
|
||||
this.sasjsConfig.serverUrl = this.sasjsConfig.serverUrl.slice(0, -1)
|
||||
}
|
||||
|
||||
if (!this.requestClient) {
|
||||
this.requestClient = new RequestClient(
|
||||
this.sasjsConfig.serverUrl,
|
||||
this.sasjsConfig.httpsAgentOptions,
|
||||
this.sasjsConfig.requestHistoryLimit
|
||||
)
|
||||
} else {
|
||||
this.requestClient.setConfig(
|
||||
this.sasjsConfig.serverUrl,
|
||||
this.sasjsConfig.httpsAgentOptions
|
||||
)
|
||||
}
|
||||
|
||||
this.jobsPath = this.sasjsConfig.pathSAS9
|
||||
|
||||
this.authManager = new AuthManager(
|
||||
this.sasjsConfig.serverUrl,
|
||||
this.sasjsConfig.serverType!,
|
||||
this.requestClient,
|
||||
this.resendWaitingRequests
|
||||
)
|
||||
|
||||
this.fileUploader = new FileUploader(
|
||||
this.sasjsConfig.serverUrl,
|
||||
this.sasjsConfig.serverType!,
|
||||
this.jobsPath,
|
||||
this.requestClient
|
||||
)
|
||||
|
||||
this.webJobExecutor = new WebJobExecutor(
|
||||
this.sasjsConfig.serverUrl,
|
||||
this.sasjsConfig.serverType!,
|
||||
this.jobsPath,
|
||||
this.requestClient
|
||||
)
|
||||
}
|
||||
|
||||
private resendWaitingRequests = async () => {
|
||||
await this.webJobExecutor?.resendWaitingRequests()
|
||||
await this.fileUploader?.resendWaitingRequests()
|
||||
}
|
||||
}
|
||||
157
src/minified/sas9/WebJobExecutor.ts
Normal file
157
src/minified/sas9/WebJobExecutor.ts
Normal file
@@ -0,0 +1,157 @@
|
||||
import {
|
||||
AuthConfig,
|
||||
ExtraResponseAttributes,
|
||||
ServerType
|
||||
} from '@sasjs/utils/types'
|
||||
import {
|
||||
ErrorResponse,
|
||||
JobExecutionError,
|
||||
LoginRequiredError
|
||||
} from '../../types/errors'
|
||||
import { RequestClient } from '../../request/RequestClient'
|
||||
import {
|
||||
isRelativePath,
|
||||
parseSasViyaDebugResponse,
|
||||
appendExtraResponseAttributes,
|
||||
convertToCSV
|
||||
} from '../../utils'
|
||||
import { BaseJobExecutor } from '../../job-execution/JobExecutor'
|
||||
import { parseWeboutResponse } from '../../utils/parseWeboutResponse'
|
||||
|
||||
export interface WaitingRequstPromise {
|
||||
promise: Promise<any> | null
|
||||
resolve: any
|
||||
reject: any
|
||||
}
|
||||
export class WebJobExecutor extends BaseJobExecutor {
|
||||
constructor(
|
||||
serverUrl: string,
|
||||
serverType: ServerType,
|
||||
private jobsPath: string,
|
||||
private requestClient: RequestClient
|
||||
) {
|
||||
super(serverUrl, serverType)
|
||||
}
|
||||
|
||||
async execute(
|
||||
sasJob: string,
|
||||
data: any,
|
||||
config: any,
|
||||
loginRequiredCallback?: any,
|
||||
authConfig?: AuthConfig,
|
||||
extraResponseAttributes: ExtraResponseAttributes[] = []
|
||||
) {
|
||||
const loginCallback = loginRequiredCallback
|
||||
const program = isRelativePath(sasJob)
|
||||
? config.appLoc
|
||||
? config.appLoc.replace(/\/?$/, '/') + sasJob.replace(/^\//, '')
|
||||
: sasJob
|
||||
: sasJob
|
||||
let apiUrl = `${config.serverUrl}${this.jobsPath}/?${'_program=' + program}`
|
||||
|
||||
let requestParams = {
|
||||
...this.getRequestParams(config)
|
||||
}
|
||||
|
||||
let formData = new FormData()
|
||||
|
||||
if (data) {
|
||||
try {
|
||||
formData = generateFileUploadForm(formData, data)
|
||||
} catch (e: any) {
|
||||
return Promise.reject(new ErrorResponse(e?.message, e))
|
||||
}
|
||||
}
|
||||
|
||||
for (const key in requestParams) {
|
||||
if (requestParams.hasOwnProperty(key)) {
|
||||
formData.append(key, requestParams[key])
|
||||
}
|
||||
}
|
||||
|
||||
const requestPromise = new Promise((resolve, reject) => {
|
||||
this.requestClient!.post(apiUrl, formData, authConfig?.access_token)
|
||||
.then(async (res: any) => {
|
||||
this.requestClient!.appendRequest(res, sasJob, config.debug)
|
||||
|
||||
const jsonResponse =
|
||||
config.debug && typeof res.result === 'string'
|
||||
? parseWeboutResponse(res.result, apiUrl)
|
||||
: res.result
|
||||
|
||||
const responseObject = appendExtraResponseAttributes(
|
||||
{ result: jsonResponse, log: res.log },
|
||||
extraResponseAttributes
|
||||
)
|
||||
resolve(responseObject)
|
||||
})
|
||||
.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) {
|
||||
if (!loginRequiredCallback) {
|
||||
reject(
|
||||
new ErrorResponse(
|
||||
'Request is not authenticated. Make sure .env file exists with valid credentials.',
|
||||
e
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
this.appendWaitingRequest(() => {
|
||||
return this.execute(
|
||||
sasJob,
|
||||
data,
|
||||
config,
|
||||
loginRequiredCallback,
|
||||
authConfig,
|
||||
extraResponseAttributes
|
||||
).then(
|
||||
(res: any) => {
|
||||
resolve(res)
|
||||
},
|
||||
(err: any) => {
|
||||
reject(err)
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
if (loginCallback) await loginCallback()
|
||||
} else reject(new ErrorResponse(e?.message, e))
|
||||
})
|
||||
})
|
||||
|
||||
return requestPromise
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* One of the approaches SASjs takes to send tables-formatted JSON (see README)
|
||||
* to SAS is as multipart form data, where each table is provided as a specially
|
||||
* formatted CSV file.
|
||||
*/
|
||||
const generateFileUploadForm = (formData: FormData, data: any): FormData => {
|
||||
for (const tableName in data) {
|
||||
if (!Array.isArray(data[tableName])) continue
|
||||
|
||||
const name = tableName
|
||||
const csv = convertToCSV(data, tableName)
|
||||
|
||||
if (csv === 'ERROR: LARGE STRING LENGTH') {
|
||||
throw new Error(
|
||||
'The max length of a string value in SASjs is 32765 characters.'
|
||||
)
|
||||
}
|
||||
|
||||
const file = new Blob([csv], {
|
||||
type: 'application/csv'
|
||||
})
|
||||
|
||||
formData.append(name, file, `${name}.csv`)
|
||||
}
|
||||
|
||||
return formData
|
||||
}
|
||||
3
src/minified/sas9/index.ts
Normal file
3
src/minified/sas9/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
import SASjs from './SASjs'
|
||||
export * from '../../types'
|
||||
export default SASjs
|
||||
@@ -691,7 +691,9 @@ const parseError = (data: string) => {
|
||||
const parts = data.split(/stored process not found: /i)
|
||||
if (parts.length > 1) {
|
||||
const storedProcessPath = parts[1].split('<i>')[1].split('</i>')[0]
|
||||
const message = `Stored process not found: ${storedProcessPath}`
|
||||
const message = storedProcessPath.endsWith('runner')
|
||||
? `SASJS runner not found. Here's the link (https://cli.sasjs.io/auth/#sasjs-runner) to the SAS code for registering the SASjs runner`
|
||||
: `Stored process not found: ${storedProcessPath}`
|
||||
return new JobExecutionError(500, message, '')
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import axiosCookieJarSupport from 'axios-cookiejar-support'
|
||||
import * as tough from 'tough-cookie'
|
||||
import { prefixMessage } from '@sasjs/utils/error'
|
||||
import { RequestClient, throwIfError } from './RequestClient'
|
||||
import { JobExecutionError } from '../types/errors'
|
||||
|
||||
/**
|
||||
* Specific request client for SAS9 in Node.js environments.
|
||||
@@ -69,6 +70,8 @@ export class Sas9RequestClient extends RequestClient {
|
||||
return this.parseResponse<T>(response)
|
||||
})
|
||||
.catch(async (e: any) => {
|
||||
if (e instanceof JobExecutionError) throw e
|
||||
|
||||
return await this.handleError(
|
||||
e,
|
||||
() =>
|
||||
|
||||
@@ -23,8 +23,16 @@ const optimization = {
|
||||
}
|
||||
|
||||
const browserConfig = {
|
||||
entry: './src/index.ts',
|
||||
devtool: 'inline-source-map',
|
||||
entry: {
|
||||
index: './src/index.ts',
|
||||
minified_sas9: './src/minified/sas9/index.ts'
|
||||
},
|
||||
output: {
|
||||
filename: '[name].js',
|
||||
path: path.resolve(__dirname, 'build'),
|
||||
libraryTarget: 'umd',
|
||||
library: 'SASjs'
|
||||
},
|
||||
mode: 'production',
|
||||
optimization: optimization,
|
||||
module: {
|
||||
@@ -40,12 +48,6 @@ const browserConfig = {
|
||||
extensions: ['.ts', '.js'],
|
||||
fallback: { https: false, fs: false, readline: false }
|
||||
},
|
||||
output: {
|
||||
filename: 'index.js',
|
||||
path: path.resolve(__dirname, 'build'),
|
||||
libraryTarget: 'umd',
|
||||
library: 'SASjs'
|
||||
},
|
||||
plugins: [
|
||||
...defaultPlugins,
|
||||
new webpack.ProvidePlugin({
|
||||
@@ -55,6 +57,18 @@ const browserConfig = {
|
||||
]
|
||||
}
|
||||
|
||||
const browserConfigWithDevTool = {
|
||||
...browserConfig,
|
||||
entry: './src/index.ts',
|
||||
output: {
|
||||
filename: 'index-dev.js',
|
||||
path: path.resolve(__dirname, 'build'),
|
||||
libraryTarget: 'umd',
|
||||
library: 'SASjs'
|
||||
},
|
||||
devtool: 'inline-source-map'
|
||||
}
|
||||
|
||||
const browserConfigWithoutProcessPlugin = {
|
||||
entry: browserConfig.entry,
|
||||
devtool: browserConfig.devtool,
|
||||
@@ -76,4 +90,4 @@ const nodeConfig = {
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = [browserConfig, nodeConfig]
|
||||
module.exports = [browserConfig, browserConfigWithDevTool, nodeConfig]
|
||||
|
||||
Reference in New Issue
Block a user