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

Compare commits

..

1 Commits

Author SHA1 Message Date
sabir_hassan
591e1ebc09 fix: callback type checking fixes #304 2021-05-27 21:59:40 +05:00
43 changed files with 2758 additions and 41269 deletions

View File

@@ -6,7 +6,7 @@ GREEN="\033[1;32m"
# temporary file which holds the message).
commit_message=$(cat "$1")
if (echo "$commit_message" | grep -Eq "^(build|chore|ci|docs|feat|fix|perf|refactor|revert|style|test)(\([a-z0-9 \-\*]+\))?!?: .+$") then
if (echo "$commit_message" | grep -Eq "^(build|chore|ci|docs|feat|fix|perf|refactor|revert|style|test)(\([a-z \-]+\))?!?: .+$") then
echo "${GREEN} ✔ Commit message meets Conventional Commit standards"
exit 0
fi

View File

@@ -1,7 +0,0 @@
version: 2
updates:
- package-ecosystem: npm
directory: "/"
schedule:
interval: daily
open-pull-requests-limit: 10

View File

@@ -7,8 +7,3 @@ groups:
- saadjutt01
- medjedovicm
- allanbowe
- sabhas
- name: SASjs QA
reviewers: 1
usernames:
- VladislavParhomchik

View File

@@ -12,8 +12,6 @@ What code changes have been made to achieve the intent.
## Checks
No PR (that involves a non-trivial code change) should be merged, unless all four of the items below are confirmed! If an urgent fix is needed - use a tar file.
- [ ] Code is formatted correctly (`npm run lint:fix`).
- [ ] All unit tests are passing (`npm test`).
- [ ] All `sasjs-cli` unit tests are passing (`npm test`).

View File

@@ -172,39 +172,35 @@ 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.
* `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`.
* `useComputeApi` - if `true` and the serverType is `SASVIYA` then the REST APIs will be called directly (rather than using the JES web service).
* `contextName` - if missing or blank, and `useComputeApi` is `true` and `serverType` is `SASVIYA` then the JES API will be used.
The adapter supports a number of approaches for interfacing with Viya (`serverType` is `SASVIYA`). For maximum performance, be sure to [configure your compute context](https://sasjs.io/guide-viya/#shared-account-and-server-re-use) with `reuseServerProcesses` as `true` and a system account in `runServerAs`. This functionality is available since Viya 3.5. This configuration is supported when [creating contexts using the CLI](https://sasjs.io/sasjs-cli-context/#sasjs-context-create).
### Using JES Web App
In this setup, all requests are routed through the JES web app, at `YOURSERVER/SASJobExecution?_program=/your/program`. This is the most reliable method, and also the slowest. One request is made to the JES app, and remaining requests (getting job uri, session spawning, passing parameters, running the program, fetching the log) are handled by the SAS server inside the JES app.
In this setup, all requests are routed through the JES web app, at `YOURSERVER/SASJobExecution`. This is the most reliable method, and also the slowest. One request is made to the JES app, and remaining requests (getting job uri, session spawning, passing parameters, running the program, fetching the log) are made on the SAS server by the JES app.
```
{
appLoc:"/Your/Path",
serverType:"SASVIYA",
contextName: 'yourComputeContext'
serverType:"SASVIYA"
}
```
Note - to use the web approach, the `useComputeApi` property must be `undefined` or `null`.
### Using the JES API
Here we are running Jobs using the Job Execution Service except this time we are making the requests directly using the REST API instead of through the JES Web App. This is helpful when we need to call web services outside of a browser (eg with the SASjs CLI or other commandline tools). To save one network request, the adapter prefetches the JOB URIs and passes them in the `__job` parameter. Depending on your network bandwidth, it may or may not be faster than the JES Web approach.
Here we are running Jobs using the Job Execution Service except this time we are making the requests directly using the REST API instead of through the JES Web App. This is helpful when we need to call web services outside of a browser (eg with the SASjs CLI or other commandline tools). To save one network request, the adapter prefetches the JOB URIs and passes them in the `__job` parameter.
```
{
appLoc:"/Your/Path",
serverType:"SASVIYA",
useComputeApi: false,
contextName: 'yourComputeContext'
useComputeApi: true
}
```
### Using the Compute API
This approach is by far the fastest, as a result of the optimisations we have built into the adapter. With this configuration, in the first sasjs request, we take a URI map of the services in the target folder, and create a session manager. This manager will spawn a additional session every time a request is made. Subsequent requests will use the existing 'hot' session, if it exists. Sessions are always deleted after every use, which actually makes this _less_ resource intensive than a typical JES web app, in which all sessions are kept alive by default for 15 minutes.
This approach is by far the fastest, as a result of the optimisations we have built into the adapter. With this configuration, in the first sasjs request, we take a URI map of the services in the target folder, and create a session manager - which spawns an extra session. The next time a request is made, the adapter will use the 'hot' session. Sessions are deleted after every use, which actually makes this _less_ resource intensive than a typical JES web app, in which all sessions are kept alive by default for 15 minutes.
```
{
@@ -229,4 +225,4 @@ If you are a SAS 9 or SAS Viya customer you can also request a copy of [Data Con
If you find this library useful, help us grow our star graph!
![](https://starchart.cc/sasjs/adapter.svg)
![](https://starchart.cc/sasjs/adapter.svg)

20397
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -3,17 +3,17 @@
"description": "JavaScript adapter for SAS",
"homepage": "https://adapter.sasjs.io",
"scripts": {
"build": "rimraf build && rimraf node && mkdir node && copyfiles -u 1 \"./src/**/*\" ./node && webpack && rimraf build/src && rimraf node",
"package:lib": "npm run build && copyfiles ./package.json build && cd build && npm version \"5.0.0\" && npm pack",
"build": "rimraf build && rimraf node && mkdir node && cp -r src/* node && webpack && rimraf build/src && rimraf node",
"package:lib": "npm run build && cp ./package.json build && cd build && npm version \"5.0.0\" && npm pack",
"publish:lib": "npm run build && cd build && npm publish",
"lint:fix": "npx prettier --write \"src/**/*.{ts,tsx,js,jsx,html,css,sass,less,json,yml,md,graphql}\" && npx prettier --write \"sasjs-tests/src/**/*.{ts,tsx,js,jsx,html,css,sass,less,json,yml,md,graphql}\"",
"lint": "npx prettier --check \"src/**/*.{ts,tsx,js,jsx,html,css,sass,less,json,yml,md,graphql}\" && npx prettier --check \"sasjs-tests/src/**/*.{ts,tsx,js,jsx,html,css,sass,less,json,yml,md,graphql}\"",
"lint:fix": "npx prettier --write 'src/**/*.{ts,tsx,js,jsx,html,css,sass,less,json,yml,md,graphql}' && npx prettier --write 'sasjs-tests/src/**/*.{ts,tsx,js,jsx,html,css,sass,less,json,yml,md,graphql}'",
"lint": "npx prettier --check 'src/**/*.{ts,tsx,js,jsx,html,css,sass,less,json,yml,md,graphql}' && npx prettier --check 'sasjs-tests/src/**/*.{ts,tsx,js,jsx,html,css,sass,less,json,yml,md,graphql}'",
"test": "jest --silent --coverage",
"prepublishOnly": "cp -r ./build/* . && rm -rf ./build",
"postpublish": "git clean -fd",
"semantic-release": "semantic-release",
"typedoc": "typedoc",
"prepare": "git rev-parse --git-dir && git config core.hooksPath ./.git-hooks && git config core.autocrlf false || true"
"postinstall": "[ -d .git ] && git config core.hooksPath ./.git-hooks || true"
},
"publishConfig": {
"access": "public"
@@ -38,40 +38,31 @@
},
"license": "ISC",
"devDependencies": {
"@types/jest": "^26.0.23",
"@types/mime": "^2.0.3",
"@types/tough-cookie": "^4.0.0",
"copyfiles": "^2.4.1",
"@types/jest": "^26.0.22",
"cp": "^0.2.0",
"dotenv": "^10.0.0",
"jest": "^27.0.6",
"dotenv": "^8.2.0",
"jest": "^26.6.3",
"jest-extended": "^0.11.5",
"mime": "^2.5.2",
"node-polyfill-webpack-plugin": "^1.1.4",
"path": "^0.12.7",
"process": "^0.11.10",
"rimraf": "^3.0.2",
"semantic-release": "^17.4.4",
"terser-webpack-plugin": "^5.1.4",
"ts-jest": "^27.0.3",
"ts-loader": "^9.2.2",
"semantic-release": "^17.4.2",
"terser-webpack-plugin": "^4.2.3",
"ts-jest": "^25.5.1",
"ts-loader": "^9.1.2",
"tslint": "^6.1.3",
"tslint-config-prettier": "^1.18.0",
"typedoc": "^0.21.2",
"typedoc-neo-theme": "^1.1.1",
"typedoc": "^0.20.35",
"typedoc-neo-theme": "^1.1.0",
"typedoc-plugin-external-module-name": "^4.0.6",
"typescript": "^4.3.4",
"webpack": "^5.41.1",
"webpack-cli": "^4.7.2"
"typescript": "^3.9.9",
"webpack": "^5.33.2",
"webpack-cli": "^4.7.0"
},
"main": "index.js",
"dependencies": {
"@sasjs/utils": "^2.23.2",
"@sasjs/utils": "^2.10.2",
"axios": "^0.21.1",
"axios-cookiejar-support": "^1.0.1",
"form-data": "^4.0.0",
"https": "^1.0.0",
"tough-cookie": "^4.0.0",
"url": "^0.11.0"
"https": "^1.0.0"
}
}

View File

@@ -6,7 +6,7 @@ When developing on `@sasjs/adapter`, it's good practice to run the test suite ag
You can use the provided `update:adapter` NPM script for this.
```bash
```
npm run update:adapter
```
@@ -37,7 +37,7 @@ To be able to run the `deploy` script, two environment variables need to be set:
So you can run the script like so:
```bash
```
SSH_ACCOUNT=me@my-sas-server.com DEPLOY_PATH=/var/www/html/my-folder/sasjs-tests npm run deploy
```
@@ -49,7 +49,8 @@ The below services need to be created on your SAS server, at the location specif
### SAS 9
```sas
```
filename mc url "https://raw.githubusercontent.com/sasjs/core/main/all.sas";
%inc mc;
filename ft15f001 temp;
@@ -71,24 +72,11 @@ parmcards4;
%webout(CLOSE)
;;;;
%mm_createwebservice(path=/Public/app/common,name=sendArr)
parmcards4;
let he who hath understanding, reckon the number of the beast
;;;;
%mm_createwebservice(path=/Public/app/common,name=makeErr)
parmcards4;
%webout(OPEN)
data _null_;
file _webout;
put ' the discovery channel ';
run;
%webout(CLOSE)
;;;;
%mm_createwebservice(path=/Public/app/common,name=invalidJSON)
```
### SAS Viya
```sas
```
filename mc url "https://raw.githubusercontent.com/sasjs/core/main/all.sas";
%inc mc;
filename ft15f001 temp;
@@ -127,15 +115,6 @@ If you can trust yourself when all men doubt you,
But make allowance for their doubting too;
;;;;
%mp_createwebservice(path=/Public/app/common,name=makeErr)
parmcards4;
%webout(OPEN)
data _null_;
file _webout;
put ' the discovery channel ';
run;
%webout(CLOSE)
;;;;
%mp_createwebservice(path=/Public/app/common,name=invalidJSON)
```
You should now be able to access the tests in your browser at the deployed path on your server.

File diff suppressed because it is too large Load Diff

View File

@@ -7,7 +7,7 @@
"@sasjs/adapter": "file:../build/sasjs-adapter-5.0.0.tgz",
"@sasjs/test-framework": "^1.4.0",
"@types/jest": "^26.0.20",
"@types/node": "^14.14.41",
"@types/node": "^14.14.25",
"@types/react": "^17.0.1",
"@types/react-dom": "^17.0.0",
"@types/react-router-dom": "^5.1.7",

View File

@@ -13,19 +13,14 @@ const App = (): ReactElement<{}> => {
useEffect(() => {
if (adapter) {
const testSuites = [
setTestSuites([
basicTests(adapter, config.userName, config.password),
sendArrTests(adapter),
sendObjTests(adapter),
specialCaseTests(adapter),
sasjsRequestTests(adapter)
]
if (adapter.getSasjsConfig().serverType === 'SASVIYA') {
testSuites.push(computeTests(adapter))
}
setTestSuites(testSuites)
sasjsRequestTests(adapter),
computeTests(adapter)
])
}
}, [adapter, config])

View File

@@ -145,29 +145,6 @@ export const basicTests = (
sasjsConfig.debug === false
)
}
},
{
title: 'Request with extra attributes on JES approach',
description:
'Should complete successful request with extra attributes present in response',
test: async () => {
const config = {
useComputeApi: false
}
return await adapter.request(
'common/sendArr',
stringData,
config,
undefined,
undefined,
['file', 'data']
)
},
assertion: (response: any) => {
const responseKeys: any = Object.keys(response)
return responseKeys.includes('file') && responseKeys.includes('data')
}
}
]
})

View File

@@ -25,7 +25,7 @@ export const computeTests = (adapter: SASjs): TestSuite => ({
'/Public/app/common/sendArr',
data,
{},
undefined,
'',
true
)
},

View File

@@ -176,59 +176,11 @@ export const sendObjTests = (adapter: SASjs): TestSuite => ({
name: 'sendObj',
tests: [
{
title: 'Table name starts with numeric',
title: 'Invalid column name',
description: 'Should throw an error',
test: async () => {
const invalidData: any = {
'1InvalidTable': [{ col1: 42 }]
}
return adapter.request('common/sendObj', invalidData).catch((e) => e)
},
assertion: (error: any) =>
!!error && !!error.error && !!error.error.message
},
{
title: 'Table name contains a space',
description: 'Should throw an error',
test: async () => {
const invalidData: any = {
'an invalidTable': [{ col1: 42 }]
}
return adapter.request('common/sendObj', invalidData).catch((e) => e)
},
assertion: (error: any) =>
!!error && !!error.error && !!error.error.message
},
{
title: 'Table name contains a special character',
description: 'Should throw an error',
test: async () => {
const invalidData: any = {
'anInvalidTable#': [{ col1: 42 }]
}
return adapter.request('common/sendObj', invalidData).catch((e) => e)
},
assertion: (error: any) =>
!!error && !!error.error && !!error.error.message
},
{
title: 'Table name exceeds max length of 32 characters',
description: 'Should throw an error',
test: async () => {
const invalidData: any = {
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx: [{ col1: 42 }]
}
return adapter.request('common/sendObj', invalidData).catch((e) => e)
},
assertion: (error: any) =>
!!error && !!error.error && !!error.error.message
},
{
title: "Invalid data object's structure",
description: 'Should throw an error',
test: async () => {
const invalidData: any = {
inData: [[{ data: 'value' }]]
'1 invalid table': [{ col1: 42 }]
}
return adapter.request('common/sendObj', invalidData).catch((e) => e)
},

View File

@@ -2,7 +2,6 @@ import { Context, EditContextInput, ContextAllAttributes } from './types'
import { isUrl } from './utils'
import { prefixMessage } from '@sasjs/utils/error'
import { RequestClient } from './request/RequestClient'
import { AuthConfig } from '@sasjs/utils/types'
export class ContextManager {
private defaultComputeContexts = [
@@ -329,12 +328,12 @@ export class ContextManager {
public async getExecutableContexts(
executeScript: Function,
authConfig?: AuthConfig
accessToken?: string
) {
const { result: contexts } = await this.requestClient
.get<{ items: Context[] }>(
`${this.serverUrl}/compute/contexts?limit=10000`,
authConfig?.access_token
accessToken
)
.catch((err) => {
throw prefixMessage(err, 'Error while fetching compute contexts.')
@@ -351,7 +350,7 @@ export class ContextManager {
`test-${context.name}`,
linesOfCode,
context.name,
authConfig,
accessToken,
null,
false,
true,

View File

@@ -2,19 +2,15 @@ import { isUrl } from './utils'
import { UploadFile } from './types/UploadFile'
import { ErrorResponse, LoginRequiredError } from './types/errors'
import { RequestClient } from './request/RequestClient'
import { ServerType } from '@sasjs/utils/types'
import SASjs from './SASjs'
import { Server } from 'https'
import { SASjsConfig } from './types'
import { config } from 'process'
export class FileUploader {
constructor(
private sasjsConfig: SASjsConfig,
private appLoc: string,
serverUrl: string,
private jobsPath: string,
private requestClient: RequestClient
) {
if (this.sasjsConfig.serverUrl) isUrl(this.sasjsConfig.serverUrl)
if (serverUrl) isUrl(serverUrl)
}
public uploadFile(sasJob: string, files: UploadFile[], params: any) {
@@ -33,8 +29,8 @@ export class FileUploader {
}
}
const program = this.sasjsConfig.appLoc
? this.sasjsConfig.appLoc.replace(/\/?$/, '/') + sasJob.replace(/^\//, '')
const program = this.appLoc
? this.appLoc.replace(/\/?$/, '/') + sasJob.replace(/^\//, '')
: sasJob
const uploadUrl = `${this.jobsPath}/?${
'_program=' + program
@@ -48,12 +44,6 @@ export class FileUploader {
const csrfToken = this.requestClient.getCsrfToken('file')
if (csrfToken) formData.append('_csrf', csrfToken.value)
if (this.sasjsConfig.debug) formData.append('_debug', '131')
if (
this.sasjsConfig.serverType === ServerType.SasViya &&
this.sasjsConfig.contextName
)
formData.append('_contextname', this.sasjsConfig.contextName)
const headers = {
'cache-control': 'no-cache',
@@ -63,15 +53,9 @@ export class FileUploader {
return this.requestClient
.post(uploadUrl, formData, undefined, 'application/json', headers)
.then((res) => {
let result
result =
typeof res.result === 'string' ? JSON.parse(res.result) : res.result
return result
//TODO: append to SASjs requests
})
.then((res) =>
typeof res.result === 'string' ? JSON.parse(res.result) : res.result
)
.catch((err: Error) => {
if (err instanceof LoginRequiredError) {
return Promise.reject(

View File

@@ -1,6 +1,4 @@
import { generateTimestamp } from '@sasjs/utils/time'
import * as NodeFormData from 'form-data'
import { Sas9RequestClient } from './request/Sas9RequestClient'
import axios, { AxiosInstance } from 'axios'
import { isUrl } from './utils'
/**
@@ -8,11 +6,11 @@ import { isUrl } from './utils'
*
*/
export class SAS9ApiClient {
private requestClient: Sas9RequestClient
private httpClient: AxiosInstance
constructor(private serverUrl: string, private jobsPath: string) {
constructor(private serverUrl: string) {
if (serverUrl) isUrl(serverUrl)
this.requestClient = new Sas9RequestClient(serverUrl, false)
this.httpClient = axios.create({ baseURL: this.serverUrl })
}
/**
@@ -35,61 +33,27 @@ export class SAS9ApiClient {
/**
* Executes code on a SAS9 server.
* @param linesOfCode - an array of code lines to execute.
* @param userName - the user name to log into the current SAS server.
* @param password - the password to log into the current SAS server.
* @param serverName - the server to execute the code on.
* @param repositoryName - the repository to execute the code in.
*/
public async executeScript(
linesOfCode: string[],
userName: string,
password: string
serverName: string,
repositoryName: string
) {
await this.requestClient.login(userName, password, this.jobsPath)
const requestPayload = linesOfCode.join('\n')
// This piece of code forces a webout to prevent Stored Process Errors.
const forceOutputCode = [
'data _null_;',
'file _webout;',
`put 'Executed sasjs run';`,
'run;'
]
const formData = generateFileUploadForm(
[...linesOfCode, ...forceOutputCode].join('\n')
const executeScriptResponse = await this.httpClient.put(
`/sas/servers/${serverName}/cmd?repositoryName=${repositoryName}`,
`command=${requestPayload}`,
{
headers: {
Accept: 'application/json'
},
responseType: 'text'
}
)
const codeInjectorPath = `/User Folders/${userName}/My Folder/sasjs/runner`
const contentType =
'multipart/form-data; boundary=' + formData.getBoundary()
const contentLength = formData.getLengthSync()
const headers = {
'cache-control': 'no-cache',
Accept: '*/*',
'Content-Type': contentType,
'Content-Length': contentLength,
Connection: 'keep-alive'
}
const storedProcessUrl = `${this.jobsPath}/?${
'_program=' + codeInjectorPath + '&_debug=log'
}`
const response = await this.requestClient.post(
storedProcessUrl,
formData,
undefined,
contentType,
headers
)
return response.result as string
return executeScriptResponse.data
}
}
const generateFileUploadForm = (data: any): NodeFormData => {
const formData = new NodeFormData()
const filename = `sasjs-execute-sas9-${generateTimestamp('')}.sas`
formData.append(filename, data, {
filename,
contentType: 'text/plain'
})
return formData
}

View File

@@ -12,7 +12,6 @@ import {
Context,
ContextAllAttributes,
Folder,
File,
EditContextInput,
JobDefinition,
PollOptions
@@ -25,20 +24,12 @@ import {
import { formatDataForRequest } from './utils/formatDataForRequest'
import { SessionManager } from './SessionManager'
import { ContextManager } from './ContextManager'
import {
timestampToYYYYMMDDHHMMSS,
isAccessTokenExpiring,
isRefreshTokenExpiring,
Logger,
LogLevel,
SasAuthResponse,
MacroVar,
AuthConfig
} from '@sasjs/utils'
import { timestampToYYYYMMDDHHMMSS } from '@sasjs/utils/time'
import { Logger, LogLevel } from '@sasjs/utils/logger'
import { isAuthorizeFormRequired } from './auth/isAuthorizeFormRequired'
import { RequestClient } from './request/RequestClient'
import { SasAuthResponse } from '@sasjs/utils/types'
import { prefixMessage } from '@sasjs/utils/error'
import * as mime from 'mime'
/**
* A client for interfacing with the SAS Viya REST API.
@@ -137,14 +128,14 @@ export class SASViyaApiClient {
/**
* Returns all compute contexts on this server that the user has access to.
* @param authConfig - an access token, refresh token, client and secret for an authorized user.
* @param accessToken - an access token for an authorized user.
*/
public async getExecutableContexts(authConfig?: AuthConfig) {
public async getExecutableContexts(accessToken?: string) {
const bindedExecuteScript = this.executeScript.bind(this)
return await this.contextManager.getExecutableContexts(
bindedExecuteScript,
authConfig
accessToken
)
}
@@ -273,40 +264,37 @@ export class SASViyaApiClient {
* @param jobPath - the path to the file being submitted for execution.
* @param linesOfCode - an array of code lines to execute.
* @param contextName - the context to execute the code in.
* @param authConfig - an object containing an access token, refresh token, client ID and secret.
* @param accessToken - an access token for an authorized user.
* @param data - execution data.
* @param debug - when set to true, the log will be returned.
* @param expectWebout - when set to true, the automatic _webout fileref will be checked for content, and that content returned. This fileref is used when the Job contains a SASjs web request (as opposed to executing arbitrary SAS code).
* @param waitForResult - when set to true, function will return the session
* @param pollOptions - an object that represents poll interval(milliseconds) and maximum amount of attempts. Object example: { MAX_POLL_COUNT: 24 * 60 * 60, POLL_INTERVAL: 1000 }.
* @param printPid - a boolean that indicates whether the function should print (PID) of the started job.
* @param variables - an object that represents macro variables.
*/
public async executeScript(
jobPath: string,
linesOfCode: string[],
contextName: string,
authConfig?: AuthConfig,
accessToken?: string,
data = null,
debug: boolean = false,
expectWebout = false,
waitForResult = true,
pollOptions?: PollOptions,
printPid = false,
variables?: MacroVar
printPid = false
): Promise<any> {
let access_token = (authConfig || {}).access_token
if (authConfig) {
;({ access_token } = await this.getTokens(authConfig))
}
const logger = process.logger || console
try {
const headers: any = {
'Content-Type': 'application/json'
}
if (accessToken) headers.Authorization = `Bearer ${accessToken}`
let executionSessionId: string
const session = await this.sessionManager
.getSession(access_token)
.getSession(accessToken)
.catch((err) => {
throw prefixMessage(err, 'Error while getting session. ')
})
@@ -315,7 +303,7 @@ export class SASViyaApiClient {
if (printPid) {
const { result: jobIdVariable } = await this.sessionManager
.getVariable(executionSessionId, 'SYSJOBID', access_token)
.getVariable(executionSessionId, 'SYSJOBID', accessToken)
.catch((err) => {
throw prefixMessage(err, 'Error while getting session variable. ')
})
@@ -347,6 +335,7 @@ export class SASViyaApiClient {
if (debug) {
jobArguments['_OMITTEXTLOG'] = false
jobArguments['_OMITSESSIONRESULTS'] = false
jobArguments['_DEBUG'] = 131
}
let fileName
@@ -367,15 +356,11 @@ export class SASViyaApiClient {
: jobPath
}
if (variables) jobVariables = { ...jobVariables, ...variables }
if (debug) jobVariables = { ...jobVariables, _DEBUG: 131 }
let files: any[] = []
if (data) {
if (JSON.stringify(data).includes(';')) {
files = await this.uploadTables(data, access_token).catch((err) => {
files = await this.uploadTables(data, accessToken).catch((err) => {
throw prefixMessage(err, 'Error while uploading tables. ')
})
@@ -405,7 +390,7 @@ export class SASViyaApiClient {
.post<Job>(
`/compute/sessions/${executionSessionId}/jobs`,
jobRequestBody,
access_token
accessToken
)
.catch((err) => {
throw prefixMessage(err, 'Error while posting job. ')
@@ -414,8 +399,8 @@ export class SASViyaApiClient {
if (!waitForResult) return session
if (debug) {
logger.info(`Job has been submitted for '${fileName}'.`)
logger.info(
console.log(`Job has been submitted for '${fileName}'.`)
console.log(
`You can monitor the job progress at '${this.serverUrl}${
postedJob.links.find((l: any) => l.rel === 'state')!.href
}'.`
@@ -425,7 +410,7 @@ export class SASViyaApiClient {
const jobStatus = await this.pollJobState(
postedJob,
etag,
authConfig,
accessToken,
pollOptions
).catch(async (err) => {
const error = err?.response?.data
@@ -438,7 +423,7 @@ export class SASViyaApiClient {
const logCount = 1000000
err.log = await fetchLogByChunks(
this.requestClient,
access_token!,
accessToken!,
sessionLogUrl,
logCount
)
@@ -446,14 +431,10 @@ export class SASViyaApiClient {
throw prefixMessage(err, 'Error while polling job status. ')
})
if (authConfig) {
;({ access_token } = await this.getTokens(authConfig))
}
const { result: currentJob } = await this.requestClient
.get<Job>(
`/compute/sessions/${executionSessionId}/jobs/${postedJob.id}`,
access_token
accessToken
)
.catch((err) => {
throw prefixMessage(err, 'Error while getting job. ')
@@ -469,7 +450,7 @@ export class SASViyaApiClient {
const logCount = currentJob.logStatistics?.lineCount ?? 1000000
log = await fetchLogByChunks(
this.requestClient,
access_token!,
accessToken!,
logUrl,
logCount
)
@@ -489,7 +470,7 @@ export class SASViyaApiClient {
if (resultLink) {
jobResult = await this.requestClient
.get<any>(resultLink, access_token, 'text/plain')
.get<any>(resultLink, accessToken, 'text/plain')
.catch(async (e) => {
if (e instanceof NotFoundError) {
if (logLink) {
@@ -497,7 +478,7 @@ export class SASViyaApiClient {
const logCount = currentJob.logStatistics?.lineCount ?? 1000000
log = await fetchLogByChunks(
this.requestClient,
access_token!,
accessToken!,
logUrl,
logCount
)
@@ -516,7 +497,7 @@ export class SASViyaApiClient {
}
await this.sessionManager
.clearSession(executionSessionId, access_token)
.clearSession(executionSessionId, accessToken)
.catch((err) => {
throw prefixMessage(err, 'Error while clearing session. ')
})
@@ -528,7 +509,7 @@ export class SASViyaApiClient {
jobPath,
linesOfCode,
contextName,
authConfig,
accessToken,
data,
debug,
false,
@@ -551,53 +532,6 @@ export class SASViyaApiClient {
.then((res) => res.result)
}
/**
* Creates a file. Path to or URI of the parent folder is required.
* @param fileName - the name of the new file.
* @param contentBuffer - the content of the new file in Buffer.
* @param parentFolderPath - the full path to the parent folder. If not
* provided, the parentFolderUri must be provided.
* @param parentFolderUri - the URI (eg /folders/folders/UUID) of the parent
* folder. If not provided, the parentFolderPath must be provided.
* @param accessToken - an access token for authorizing the request.
*/
public async createFile(
fileName: string,
contentBuffer: Buffer,
parentFolderPath?: string,
parentFolderUri?: string,
accessToken?: string
): Promise<File> {
if (!parentFolderPath && !parentFolderUri) {
throw new Error('Path or URI of the parent folder is required.')
}
if (!parentFolderUri && parentFolderPath) {
parentFolderUri = await this.getFolderUri(parentFolderPath, accessToken)
}
const headers = {
Accept: 'application/vnd.sas.file+json',
'Content-Disposition': `filename="${fileName}";`
}
const formData = new NodeFormData()
formData.append('file', contentBuffer, fileName)
const mimeType =
mime.getType(fileName.match(/\.[0-9a-z]+$/i)?.[0] || '') ?? 'text/plain'
return (
await this.requestClient.post<File>(
`/files/files?parentFolderUri=${parentFolderUri}&typeDefName=file#rawUpload`,
formData,
accessToken,
'multipart/form-data; boundary=' + (formData as any)._boundary,
headers
)
).result
}
/**
* Creates a folder. Path to or URI of the parent folder is required.
* @param folderName - the name of the new folder.
@@ -615,7 +549,6 @@ export class SASViyaApiClient {
accessToken?: string,
isForced?: boolean
): Promise<Folder> {
const logger = process.logger || console
if (!parentFolderPath && !parentFolderUri) {
throw new Error('Path or URI of the parent folder is required.')
}
@@ -623,7 +556,7 @@ export class SASViyaApiClient {
if (!parentFolderUri && parentFolderPath) {
parentFolderUri = await this.getFolderUri(parentFolderPath, accessToken)
if (!parentFolderUri) {
logger.info(
console.log(
`Parent folder at path '${parentFolderPath}' is not present.`
)
@@ -635,7 +568,7 @@ export class SASViyaApiClient {
if (newParentFolderPath === '') {
throw new Error('Root folder has to be present on the server.')
}
logger.info(
console.log(
`Creating parent folder:\n'${newFolderName}' in '${newParentFolderPath}'`
)
const parentFolder = await this.createFolder(
@@ -644,7 +577,7 @@ export class SASViyaApiClient {
undefined,
accessToken
)
logger.info(
console.log(
`Parent folder '${newFolderName}' has been successfully created.`
)
parentFolderUri = `/folders/folders/${parentFolder.id}`
@@ -786,11 +719,13 @@ export class SASViyaApiClient {
let formData
if (typeof FormData === 'undefined') {
formData = new NodeFormData()
formData.append('grant_type', 'authorization_code')
formData.append('code', authCode)
} else {
formData = new FormData()
formData.append('grant_type', 'authorization_code')
formData.append('code', authCode)
}
formData.append('grant_type', 'authorization_code')
formData.append('code', authCode)
const authResponse = await this.requestClient
.post(
@@ -879,25 +814,18 @@ export class SASViyaApiClient {
* @param expectWebout - a boolean indicating whether to expect a _webout response.
* @param pollOptions - an object that represents poll interval(milliseconds) and maximum amount of attempts. Object example: { MAX_POLL_COUNT: 24 * 60 * 60, POLL_INTERVAL: 1000 }.
* @param printPid - a boolean that indicates whether the function should print (PID) of the started job.
* @param variables - an object that represents macro variables.
*/
public async executeComputeJob(
sasJob: string,
contextName: string,
debug?: boolean,
data?: any,
authConfig?: AuthConfig,
accessToken?: string,
waitForResult = true,
expectWebout = false,
pollOptions?: PollOptions,
printPid = false,
variables?: MacroVar
printPid = false
) {
let access_token = (authConfig || {}).access_token
if (authConfig) {
;({ access_token } = await this.getTokens(authConfig))
}
if (isRelativePath(sasJob) && !this.rootFolderName) {
throw new Error(
'Relative paths cannot be used without specifying a root folder name'
@@ -911,7 +839,7 @@ export class SASViyaApiClient {
? `${this.rootFolderName}/${folderPath}`
: folderPath
await this.populateFolderMap(fullFolderPath, access_token).catch((err) => {
await this.populateFolderMap(fullFolderPath, accessToken).catch((err) => {
throw prefixMessage(err, 'Error while populating folder map. ')
})
@@ -923,6 +851,12 @@ export class SASViyaApiClient {
)
}
const headers: any = { 'Content-Type': 'application/json' }
if (!!accessToken) {
headers.Authorization = `Bearer ${accessToken}`
}
const jobToExecute = jobFolder?.find((item) => item.name === jobName)
if (!jobToExecute) {
@@ -943,7 +877,7 @@ export class SASViyaApiClient {
const { result: jobDefinition } = await this.requestClient
.get<JobDefinition>(
`${this.serverUrl}${jobDefinitionLink.href}`,
access_token
accessToken
)
.catch((err) => {
throw prefixMessage(err, 'Error while getting job definition. ')
@@ -963,14 +897,13 @@ export class SASViyaApiClient {
sasJob,
linesToExecute,
contextName,
authConfig,
accessToken,
data,
debug,
expectWebout,
waitForResult,
pollOptions,
printPid,
variables
printPid
)
}
@@ -987,12 +920,8 @@ export class SASViyaApiClient {
contextName: string,
debug: boolean,
data?: any,
authConfig?: AuthConfig
accessToken?: string
) {
let access_token = (authConfig || {}).access_token
if (authConfig) {
;({ access_token } = await this.getTokens(authConfig))
}
if (isRelativePath(sasJob) && !this.rootFolderName) {
throw new Error(
'Relative paths cannot be used without specifying a root folder name.'
@@ -1005,7 +934,7 @@ export class SASViyaApiClient {
const fullFolderPath = isRelativePath(sasJob)
? `${this.rootFolderName}/${folderPath}`
: folderPath
await this.populateFolderMap(fullFolderPath, access_token)
await this.populateFolderMap(fullFolderPath, accessToken)
const jobFolder = this.folderMap.get(fullFolderPath)
if (!jobFolder) {
@@ -1018,7 +947,7 @@ export class SASViyaApiClient {
let files: any[] = []
if (data && Object.keys(data).length) {
files = await this.uploadTables(data, access_token)
files = await this.uploadTables(data, accessToken)
}
if (!jobToExecute) {
@@ -1030,7 +959,7 @@ export class SASViyaApiClient {
const { result: jobDefinition } = await this.requestClient.get<Job>(
`${this.serverUrl}${jobDefinitionLink}`,
access_token
accessToken
)
const jobArguments: { [key: string]: any } = {
@@ -1066,18 +995,18 @@ export class SASViyaApiClient {
const { result: postedJob, etag } = await this.requestClient.post<Job>(
`${this.serverUrl}/jobExecution/jobs?_action=wait`,
postJobRequestBody,
access_token
accessToken
)
const jobStatus = await this.pollJobState(
postedJob,
etag,
authConfig
accessToken
).catch((err) => {
throw prefixMessage(err, 'Error while polling job status. ')
})
const { result: currentJob } = await this.requestClient.get<Job>(
`${this.serverUrl}/jobExecution/jobs/${postedJob.id}`,
access_token
accessToken
)
let jobResult
@@ -1088,13 +1017,13 @@ export class SASViyaApiClient {
if (resultLink) {
jobResult = await this.requestClient.get<any>(
`${this.serverUrl}${resultLink}/content`,
access_token,
accessToken,
'text/plain'
)
}
if (debug && logLink) {
log = await this.requestClient
.get<any>(`${this.serverUrl}${logLink.href}/content`, access_token)
.get<any>(`${this.serverUrl}${logLink.href}/content`, accessToken)
.then((res: any) => res.result.items.map((i: any) => i.line).join('\n'))
}
if (jobStatus === 'failed') {
@@ -1144,18 +1073,12 @@ export class SASViyaApiClient {
private async pollJobState(
postedJob: any,
etag: string | null,
authConfig?: AuthConfig,
accessToken?: string,
pollOptions?: PollOptions
) {
const logger = process.logger || console
let POLL_INTERVAL = 300
let MAX_POLL_COUNT = 1000
let MAX_ERROR_COUNT = 5
let access_token = (authConfig || {}).access_token
if (authConfig) {
;({ access_token } = await this.getTokens(authConfig))
}
if (pollOptions) {
POLL_INTERVAL = pollOptions.POLL_INTERVAL || POLL_INTERVAL
@@ -1169,8 +1092,8 @@ export class SASViyaApiClient {
'Content-Type': 'application/json',
'If-None-Match': etag
}
if (access_token) {
headers.Authorization = `Bearer ${access_token}`
if (accessToken) {
headers.Authorization = `Bearer ${accessToken}`
}
const stateLink = postedJob.links.find((l: any) => l.rel === 'state')
if (!stateLink) {
@@ -1180,7 +1103,7 @@ export class SASViyaApiClient {
const { result: state } = await this.requestClient
.get<string>(
`${this.serverUrl}${stateLink.href}?_action=wait&wait=300`,
access_token,
accessToken,
'text/plain',
{},
this.debug
@@ -1208,15 +1131,11 @@ export class SASViyaApiClient {
postedJobState === 'pending' ||
postedJobState === 'unavailable'
) {
if (authConfig) {
;({ access_token } = await this.getTokens(authConfig))
}
if (stateLink) {
const { result: jobState } = await this.requestClient
.get<string>(
`${this.serverUrl}${stateLink.href}?_action=wait&wait=300`,
access_token,
accessToken,
'text/plain',
{},
this.debug
@@ -1245,8 +1164,8 @@ export class SASViyaApiClient {
}
if (this.debug && printedState !== postedJobState) {
logger.info('Polling job status...')
logger.info(`Current job state: ${postedJobState}`)
console.log('Polling job status...')
console.log(`Current job state: ${postedJobState}`)
printedState = postedJobState
}
@@ -1436,9 +1355,6 @@ export class SASViyaApiClient {
accessToken
)
if (!sourceFolderUri) {
return undefined
}
const sourceFolderId = sourceFolderUri?.split('/').pop()
const { result: folder } = await this.requestClient
@@ -1493,21 +1409,4 @@ export class SASViyaApiClient {
return movedFolder
}
private async getTokens(authConfig: AuthConfig): Promise<AuthConfig> {
const logger = process.logger || console
let { access_token, refresh_token, client, secret } = authConfig
if (
isAccessTokenExpiring(access_token) ||
isRefreshTokenExpiring(refresh_token)
) {
logger.info('Refreshing access and refresh tokens.')
;({ access_token, refresh_token } = await this.refreshTokens(
client,
secret,
refresh_token
))
}
return { access_token, refresh_token, client, secret }
}
}

View File

@@ -4,17 +4,15 @@ import { SASViyaApiClient } from './SASViyaApiClient'
import { SAS9ApiClient } from './SAS9ApiClient'
import { FileUploader } from './FileUploader'
import { AuthManager } from './auth'
import { ServerType, MacroVar, AuthConfig } from '@sasjs/utils/types'
import { ServerType } from '@sasjs/utils/types'
import { RequestClient } from './request/RequestClient'
import {
JobExecutor,
WebJobExecutor,
ComputeJobExecutor,
JesJobExecutor,
Sas9JobExecutor
JesJobExecutor
} from './job-execution'
import { ErrorResponse } from './types/errors'
import { ExtraResponseAttributes } from '@sasjs/utils/types'
const defaultConfig: SASjsConfig = {
serverUrl: '',
@@ -24,7 +22,7 @@ const defaultConfig: SASjsConfig = {
serverType: ServerType.SasViya,
debug: false,
contextName: 'SAS Job Execution compute context',
useComputeApi: null,
useComputeApi: false,
allowInsecureRequests: false
}
@@ -43,7 +41,6 @@ export default class SASjs {
private webJobExecutor: JobExecutor | null = null
private computeJobExecutor: JobExecutor | null = null
private jesJobExecutor: JobExecutor | null = null
private sas9JobExecutor: JobExecutor | null = null
constructor(config?: any) {
this.sasjsConfig = {
@@ -60,15 +57,15 @@ export default class SASjs {
public async executeScriptSAS9(
linesOfCode: string[],
userName: string,
password: string
serverName: string,
repositoryName: string
) {
this.isMethodSupported('executeScriptSAS9', ServerType.Sas9)
return await this.sas9ApiClient?.executeScript(
linesOfCode,
userName,
password
serverName,
repositoryName
)
}
@@ -103,12 +100,12 @@ export default class SASjs {
/**
* Gets executable compute contexts.
* @param authConfig - an access token, refresh token, client and secret for an authorized user.
* @param accessToken - an access token for an authorized user.
*/
public async getExecutableContexts(authConfig: AuthConfig) {
public async getExecutableContexts(accessToken: string) {
this.isMethodSupported('getExecutableContexts', ServerType.SasViya)
return await this.sasViyaApiClient!.getExecutableContexts(authConfig)
return await this.sasViyaApiClient!.getExecutableContexts(accessToken)
}
/**
@@ -240,14 +237,14 @@ export default class SASjs {
* @param fileName - name of the file to run. It will be converted to path to the file being submitted for execution.
* @param linesOfCode - lines of sas code from the file to run.
* @param contextName - context name on which code will be run on the server.
* @param authConfig - (optional) the access token, refresh token, client and secret for authorizing the request.
* @param accessToken - (optional) the access token for authorizing the request.
* @param debug - (optional) if true, global debug config will be overriden
*/
public async executeScriptSASViya(
fileName: string,
linesOfCode: string[],
contextName: string,
authConfig?: AuthConfig,
accessToken?: string,
debug?: boolean
) {
this.isMethodSupported('executeScriptSASViya', ServerType.SasViya)
@@ -261,14 +258,14 @@ export default class SASjs {
fileName,
linesOfCode,
contextName,
authConfig,
accessToken,
null,
debug ? debug : this.sasjsConfig.debug
)
}
/**
* Creates a folder in the logical SAS folder tree
* Creates a folder at SAS file system.
* @param folderName - name of the folder to be created.
* @param parentFolderPath - the full path (eg `/Public/example/myFolder`) of the parent folder.
* @param parentFolderUri - the URI of the parent folder.
@@ -300,40 +297,6 @@ export default class SASjs {
)
}
/**
* Creates a file in the logical SAS folder tree
* @param fileName - name of the file to be created.
* @param content - content of the file to be created.
* @param parentFolderPath - the full path (eg `/Public/example/myFolder`) of the parent folder.
* @param parentFolderUri - the URI of the parent folder.
* @param accessToken - the access token to authorizing the request.
* @param sasApiClient - a client for interfacing with SAS API.
*/
public async createFile(
fileName: string,
content: Buffer,
parentFolderPath: string,
parentFolderUri?: string,
accessToken?: string,
sasApiClient?: SASViyaApiClient
) {
if (sasApiClient)
return await sasApiClient.createFile(
fileName,
content,
parentFolderPath,
parentFolderUri,
accessToken
)
return await this.sasViyaApiClient!.createFile(
fileName,
content,
parentFolderPath,
parentFolderUri,
accessToken
)
}
/**
* Fetches a folder from the SAS file system.
* @param folderPath - path of the folder to be fetched.
@@ -544,7 +507,12 @@ export default class SASjs {
public uploadFile(sasJob: string, files: UploadFile[], params: any) {
const fileUploader =
this.fileUploader ||
new FileUploader(this.sasjsConfig, this.jobsPath, this.requestClient!)
new FileUploader(
this.sasjsConfig.appLoc,
this.sasjsConfig.serverUrl,
this.jobsPath,
this.requestClient!
)
return fileUploader.uploadFile(sasJob, files, params)
}
@@ -570,38 +538,30 @@ export default class SASjs {
* `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,
data: { [key: string]: any },
config: { [key: string]: any } = {},
loginRequiredCallback?: () => any,
authConfig?: AuthConfig,
extraResponseAttributes: ExtraResponseAttributes[] = []
accessToken?: string
) {
config = {
...this.sasjsConfig,
...config
}
const validationResult = this.validateInput(data)
if (validationResult.status) {
if (
config.serverType !== ServerType.Sas9 &&
config.useComputeApi !== undefined &&
config.useComputeApi !== null
) {
if (
typeof loginRequiredCallback === 'function' ||
typeof loginRequiredCallback === 'undefined'
) {
if (config.serverType === ServerType.SasViya && config.contextName) {
if (config.useComputeApi) {
return await this.computeJobExecutor!.execute(
sasJob,
data,
config,
loginRequiredCallback,
authConfig
accessToken
)
} else {
return await this.jesJobExecutor!.execute(
@@ -609,91 +569,23 @@ export default class SASjs {
data,
config,
loginRequiredCallback,
authConfig,
extraResponseAttributes
accessToken
)
}
} else if (
config.serverType === ServerType.Sas9 &&
config.username &&
config.password
) {
return await this.sas9JobExecutor!.execute(sasJob, data, config)
} else {
return await this.webJobExecutor!.execute(
sasJob,
data,
config,
loginRequiredCallback,
authConfig,
extraResponseAttributes
loginRequiredCallback
)
}
} else {
return Promise.reject(new ErrorResponse(validationResult.msg))
}
}
/**
* This function validates the input data structure and table naming convention
*
* @param data A json object that contains one or more tables, it can also be null
* @returns An object which contains two attributes: 1) status: boolean, 2) msg: string
*/
private validateInput(data: { [key: string]: any } | null): {
status: boolean
msg: string
} {
if (data === null) return { status: true, msg: '' }
for (const key in data) {
if (!key.match(/^[a-zA-Z_]/)) {
return {
status: false,
msg: 'First letter of table should be alphabet or underscore.'
}
}
if (!key.match(/^[a-zA-Z_][a-zA-Z0-9_]*$/)) {
return { status: false, msg: 'Table name should be alphanumeric.' }
}
if (key.length > 32) {
return {
status: false,
msg: 'Maximum length for table name could be 32 characters.'
}
}
if (this.getType(data[key]) !== 'Array') {
return {
status: false,
msg: 'Parameter data contains invalid table structure.'
}
}
for (let i = 0; i < data[key].length; i++) {
if (this.getType(data[key][i]) !== 'object') {
return {
status: false,
msg: `Table ${key} contains invalid structure.`
}
}
}
}
return { status: true, msg: '' }
}
/**
* this function returns the type of variable
*
* @param data it could be anything, like string, array, object etc.
* @returns a string which tells the type of input parameter
*/
private getType(data: any): string {
if (Array.isArray(data)) {
return 'Array'
} else {
return typeof data
return Promise.reject(
new ErrorResponse(
`Invalid loginRequiredCallback parameter was provided. Expected Callback function but found ${typeof loginRequiredCallback}`
)
)
}
}
@@ -734,7 +626,7 @@ export default class SASjs {
)
sasApiClient.debug = this.sasjsConfig.debug
} else if (this.sasjsConfig.serverType === ServerType.Sas9) {
sasApiClient = new SAS9ApiClient(serverUrl, this.jobsPath)
sasApiClient = new SAS9ApiClient(serverUrl)
}
} else {
let sasClientConfig: any = null
@@ -776,22 +668,20 @@ export default class SASjs {
* @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 authConfig - a valid client, secret, refresh and access tokens that are authorised to execute compute jobs.
* @param accessToken - a valid access token that is authorised to execute compute jobs.
* The access token is not required when the user is authenticated via the browser.
* @param waitForResult - a boolean that indicates whether the function needs to wait for execution to complete.
* @param pollOptions - an object that represents poll interval(milliseconds) and maximum amount of attempts. Object example: { MAX_POLL_COUNT: 24 * 60 * 60, POLL_INTERVAL: 1000 }.
* @param printPid - a boolean that indicates whether the function should print (PID) of the started job.
* @param variables - an object that represents macro variables.
*/
public async startComputeJob(
sasJob: string,
data: any,
config: any = {},
authConfig?: AuthConfig,
accessToken?: string,
waitForResult?: boolean,
pollOptions?: PollOptions,
printPid = false,
variables?: MacroVar
printPid = false
) {
config = {
...this.sasjsConfig,
@@ -810,12 +700,11 @@ export default class SASjs {
config.contextName,
config.debug,
data,
authConfig,
accessToken,
!!waitForResult,
false,
pollOptions,
printPid,
variables
printPid
)
}
@@ -926,15 +815,12 @@ export default class SASjs {
if (this.sasjsConfig.serverType === ServerType.Sas9) {
if (this.sas9ApiClient)
this.sas9ApiClient!.setConfig(this.sasjsConfig.serverUrl)
else
this.sas9ApiClient = new SAS9ApiClient(
this.sasjsConfig.serverUrl,
this.jobsPath
)
else this.sas9ApiClient = new SAS9ApiClient(this.sasjsConfig.serverUrl)
}
this.fileUploader = new FileUploader(
this.sasjsConfig,
this.sasjsConfig.appLoc,
this.sasjsConfig.serverUrl,
this.jobsPath,
this.requestClient
)
@@ -947,12 +833,6 @@ export default class SASjs {
this.sasViyaApiClient!
)
this.sas9JobExecutor = new Sas9JobExecutor(
this.sasjsConfig.serverUrl,
this.sasjsConfig.serverType!,
this.jobsPath
)
this.computeJobExecutor = new ComputeJobExecutor(
this.sasjsConfig.serverUrl,
this.sasViyaApiClient!
@@ -983,16 +863,6 @@ export default class SASjs {
isForced
)
break
case 'file':
await this.createFile(
member.name,
member.code,
parentFolder,
undefined,
accessToken,
sasApiClient
)
break
case 'service':
await this.createJobDefinition(
member.name,

View File

@@ -6,6 +6,10 @@ import { RequestClient } from './request/RequestClient'
const MAX_SESSION_COUNT = 1
const RETRY_LIMIT: number = 3
let RETRY_COUNT: number = 0
const INTERNAL_SAS_ERROR = {
status: 304,
message: 'Not Modified'
}
export class SessionManager {
constructor(
@@ -154,13 +158,11 @@ export class SessionManager {
etag: string | null,
accessToken?: string
) {
const logger = process.logger || console
let sessionState = session.state
const stateLink = session.links.find((l: any) => l.rel === 'state')
return new Promise(async (resolve, reject) => {
return new Promise(async (resolve, _) => {
if (
sessionState === 'pending' ||
sessionState === 'running' ||
@@ -168,7 +170,7 @@ export class SessionManager {
) {
if (stateLink) {
if (this.debug && !this.printedSessionState.printed) {
logger.info('Polling session status...')
console.log('Polling session status...')
this.printedSessionState.printed = true
}
@@ -178,13 +180,13 @@ export class SessionManager {
etag!,
accessToken
).catch((err) => {
throw prefixMessage(err, 'Error while getting session state.')
throw err
})
sessionState = state.trim()
if (this.debug && this.printedSessionState.state !== sessionState) {
logger.info(`Current session state is '${sessionState}'`)
console.log(`Current session state is '${sessionState}'`)
this.printedSessionState.state = sessionState
this.printedSessionState.printed = false
@@ -192,14 +194,13 @@ export class SessionManager {
// There is an internal error present in SAS Viya 3.5
// Retry to wait for a session status in such case of SAS internal error
if (!sessionState) {
if (RETRY_COUNT < RETRY_LIMIT) {
RETRY_COUNT++
if (
sessionState === INTERNAL_SAS_ERROR.message &&
RETRY_COUNT < RETRY_LIMIT
) {
RETRY_COUNT++
resolve(this.waitForSession(session, etag, accessToken))
} else {
reject('Could not get session state.')
}
resolve(this.waitForSession(session, etag, accessToken))
}
resolve(sessionState)
@@ -219,6 +220,9 @@ export class SessionManager {
.get(url, accessToken, 'text/plain', { 'If-None-Match': etag })
.then((res) => res.result as string)
.catch((err) => {
if (err.status === INTERNAL_SAS_ERROR.status)
return INTERNAL_SAS_ERROR.message
throw err
})
}

View File

@@ -1,4 +1,5 @@
import { ServerType } from '@sasjs/utils/types'
import { isAuthorizeFormRequired } from '.'
import { RequestClient } from '../request/RequestClient'
import { serialize } from '../utils'
@@ -34,7 +35,6 @@ export class AuthManager {
this.userName = loginParams.username
const { isLoggedIn, loginForm } = await this.checkSession()
if (isLoggedIn) {
await this.loginCallback()
@@ -44,44 +44,6 @@ export class AuthManager {
}
}
let loginResponse = await this.sendLoginRequest(loginForm, loginParams)
let loggedIn = isLogInSuccess(loginResponse)
if (!loggedIn) {
if (isCredentialsVerifyError(loginResponse)) {
const newLoginForm = await this.getLoginForm(loginResponse)
loginResponse = await this.sendLoginRequest(newLoginForm, loginParams)
}
const currentSession = await this.checkSession()
loggedIn = currentSession.isLoggedIn
}
if (loggedIn) {
if (this.serverType === ServerType.Sas9) {
const casAuthenticationUrl = `${this.serverUrl}/SASStoredProcess/j_spring_cas_security_check`
await this.requestClient.get<string>(
`/SASLogon/login?service=${casAuthenticationUrl}`,
undefined
)
}
this.loginCallback()
}
return {
isLoggedIn: !!loggedIn,
userName: this.userName
}
}
private async sendLoginRequest(
loginForm: { [key: string]: any },
loginParams: { [key: string]: any }
) {
for (const key in loginForm) {
loginParams[key] = loginForm[key]
}
@@ -98,7 +60,21 @@ export class AuthManager {
}
)
return loginResponse
let loggedIn = isLogInSuccess(loginResponse)
if (!loggedIn) {
const currentSession = await this.checkSession()
loggedIn = currentSession.isLoggedIn
}
if (loggedIn) {
this.loginCallback()
}
return {
isLoggedIn: !!loggedIn,
userName: this.userName
}
}
/**
@@ -192,10 +168,5 @@ export class AuthManager {
}
}
const isCredentialsVerifyError = (response: string): boolean =>
/An error occurred while the system was verifying your credentials. Please enter your credentials again./gm.test(
response
)
const isLogInSuccess = (response: string): boolean =>
/You have signed in/gm.test(response)

View File

@@ -57,7 +57,7 @@ describe('AuthManager', () => {
expect((authManager as any).logoutUrl).toEqual('/SASLogon/logout?')
})
it('should call the auth callback and return when already logged in', async () => {
it('should call the auth callback and return when already logged in', async (done) => {
const authManager = new AuthManager(
serverUrl,
serverType,
@@ -77,9 +77,10 @@ describe('AuthManager', () => {
expect(loginResponse.isLoggedIn).toBeTruthy()
expect(loginResponse.userName).toEqual(userName)
expect(authCallback).toHaveBeenCalledTimes(1)
done()
})
it('should post a login request to the server if not logged in', async () => {
it('should post a login request to the server if not logged in', async (done) => {
const authManager = new AuthManager(
serverUrl,
serverType,
@@ -120,9 +121,10 @@ describe('AuthManager', () => {
}
)
expect(authCallback).toHaveBeenCalledTimes(1)
done()
})
it('should parse and submit the authorisation form when necessary', async () => {
it('should parse and submit the authorisation form when necessary', async (done) => {
const authManager = new AuthManager(
serverUrl,
serverType,
@@ -158,9 +160,10 @@ describe('AuthManager', () => {
expect(requestClient.authorize).toHaveBeenCalledWith(
mockLoginAuthoriseRequiredResponse
)
done()
})
it('should check and return session information if logged in', async () => {
it('should check and return session information if logged in', async (done) => {
const authManager = new AuthManager(
serverUrl,
serverType,
@@ -186,5 +189,7 @@ describe('AuthManager', () => {
}
}
)
done()
})
})

View File

@@ -1,4 +1,4 @@
import { AuthConfig, ServerType } from '@sasjs/utils/types'
import { ServerType } from '@sasjs/utils/types'
import { SASViyaApiClient } from '../SASViyaApiClient'
import {
ErrorResponse,
@@ -17,7 +17,7 @@ export class ComputeJobExecutor extends BaseJobExecutor {
data: any,
config: any,
loginRequiredCallback?: any,
authConfig?: AuthConfig
accessToken?: string
) {
const loginCallback = loginRequiredCallback || (() => Promise.resolve())
const waitForResult = true
@@ -30,7 +30,7 @@ export class ComputeJobExecutor extends BaseJobExecutor {
config.contextName,
config.debug,
data,
authConfig,
accessToken,
waitForResult,
expectWebout
)

View File

@@ -1,11 +1,10 @@
import { AuthConfig, ServerType } from '@sasjs/utils/types'
import { ServerType } from '@sasjs/utils/types'
import { SASViyaApiClient } from '../SASViyaApiClient'
import {
ErrorResponse,
JobExecutionError,
LoginRequiredError
} from '../types/errors'
import { ExtraResponseAttributes } from '@sasjs/utils/types'
import { BaseJobExecutor } from './JobExecutor'
export class JesJobExecutor extends BaseJobExecutor {
@@ -18,34 +17,23 @@ export class JesJobExecutor extends BaseJobExecutor {
data: any,
config: any,
loginRequiredCallback?: any,
authConfig?: AuthConfig,
extraResponseAttributes: ExtraResponseAttributes[] = []
accessToken?: string
) {
const loginCallback = loginRequiredCallback || (() => Promise.resolve())
const requestPromise = new Promise((resolve, reject) => {
this.sasViyaApiClient
?.executeJob(sasJob, config.contextName, config.debug, data, authConfig)
.then((response: any) => {
?.executeJob(
sasJob,
config.contextName,
config.debug,
data,
accessToken
)
.then((response) => {
this.appendRequest(response, sasJob, config.debug)
let responseObject = {}
if (extraResponseAttributes && extraResponseAttributes.length > 0) {
const extraAttributes = extraResponseAttributes.reduce(
(map: any, obj: any) => ((map[obj] = response[obj]), map),
{}
)
responseObject = {
result: response.result,
...extraAttributes
}
} else {
responseObject = response.result
}
resolve(responseObject)
resolve(response)
})
.catch(async (e: Error) => {
if (e instanceof JobExecutionError) {
@@ -62,9 +50,7 @@ export class JesJobExecutor extends BaseJobExecutor {
sasJob,
data,
config,
loginRequiredCallback,
authConfig,
extraResponseAttributes
loginRequiredCallback
).then(
(res: any) => {
resolve(res)

View File

@@ -1,6 +1,5 @@
import { AuthConfig, ServerType } from '@sasjs/utils/types'
import { ServerType } from '@sasjs/utils/types'
import { SASjsRequest } from '../types'
import { ExtraResponseAttributes } from '@sasjs/utils/types'
import { asyncForEach, parseGeneratedCode, parseSourceCode } from '../utils'
export type ExecuteFunction = () => Promise<any>
@@ -11,8 +10,7 @@ export interface JobExecutor {
data: any,
config: any,
loginRequiredCallback?: any,
authConfig?: AuthConfig,
extraResponseAttributes?: ExtraResponseAttributes[]
accessToken?: string
) => Promise<any>
resendWaitingRequests: () => Promise<void>
getRequests: () => SASjsRequest[]
@@ -30,8 +28,7 @@ export abstract class BaseJobExecutor implements JobExecutor {
data: any,
config: any,
loginRequiredCallback?: any,
authConfig?: AuthConfig | undefined,
extraResponseAttributes?: ExtraResponseAttributes[]
accessToken?: string | undefined
): Promise<any>
resendWaitingRequests = async () => {
@@ -62,14 +59,14 @@ export abstract class BaseJobExecutor implements JobExecutor {
let sasWork = null
if (debug) {
if (response?.log) {
if (response?.result && response?.log) {
sourceCode = parseSourceCode(response.log)
generatedCode = parseGeneratedCode(response.log)
if (response?.result) {
sasWork = response.result.WORK
} else {
if (response.log) {
sasWork = response.log
} else {
sasWork = response.result.WORK
}
} else if (response?.result) {
sourceCode = parseSourceCode(response.result)

View File

@@ -1,110 +0,0 @@
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'
/**
* Job executor for SAS9 servers for use in Node.js environments.
* Initiates login with the provided username and password from the config
* The cookies are stored in the request client and used in subsequent
* job execution requests.
*/
export class Sas9JobExecutor extends BaseJobExecutor {
private requestClient: Sas9RequestClient
constructor(
serverUrl: string,
serverType: ServerType,
private jobsPath: string
) {
super(serverUrl, serverType)
this.requestClient = new Sas9RequestClient(serverUrl, false)
}
async execute(sasJob: string, data: any, config: any) {
const program = isRelativePath(sasJob)
? config.appLoc
? config.appLoc.replace(/\/?$/, '/') + sasJob.replace(/^\//, '')
: sasJob
: sasJob
let apiUrl = `${config.serverUrl}${this.jobsPath}?${'_program=' + program}`
apiUrl = `${apiUrl}${
config.username && config.password
? '&_username=' + config.username + '&_password=' + config.password
: ''
}`
let requestParams = {
...this.getRequestParams(config)
}
let formData = new NodeFormData()
if (data) {
try {
formData = generateFileUploadForm(formData, data)
} catch (e) {
return Promise.reject(new ErrorResponse(e?.message, e))
}
}
for (const key in requestParams) {
if (requestParams.hasOwnProperty(key)) {
formData.append(key, requestParams[key])
}
}
await this.requestClient.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(
apiUrl,
formData,
undefined,
contentType,
{
Accept: '*/*',
Connection: 'Keep-Alive'
}
)
}
private getRequestParams(config: any): any {
const requestParams: any = {}
if (config.debug) {
requestParams['_debug'] = 131
}
return requestParams
}
}
const generateFileUploadForm = (
formData: NodeFormData,
data: any
): NodeFormData => {
for (const tableName in data) {
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.'
)
}
formData.append(name, csv, {
filename: `${name}.csv`,
contentType: 'application/csv'
})
}
return formData
}

View File

@@ -8,9 +8,8 @@ import { generateFileUploadForm } from '../file/generateFileUploadForm'
import { generateTableUploadForm } from '../file/generateTableUploadForm'
import { RequestClient } from '../request/RequestClient'
import { SASViyaApiClient } from '../SASViyaApiClient'
import { isRelativePath, isValidJson } from '../utils'
import { isRelativePath } from '../utils'
import { BaseJobExecutor } from './JobExecutor'
import { parseWeboutResponse } from '../utils/parseWeboutResponse'
export interface WaitingRequstPromise {
promise: Promise<any> | null
@@ -40,18 +39,15 @@ export class WebJobExecutor extends BaseJobExecutor {
? config.appLoc.replace(/\/?$/, '/') + sasJob.replace(/^\//, '')
: sasJob
: sasJob
let apiUrl = `${config.serverUrl}${this.jobsPath}/?${'_program=' + program}`
if (config.serverType === ServerType.SasViya) {
const jobUri =
config.serverType === ServerType.SasViya
? await this.getJobUri(sasJob)
: ''
apiUrl += jobUri.length > 0 ? '&_job=' + jobUri : ''
apiUrl += config.contextName ? `&_contextname=${config.contextName}` : ''
}
const jobUri =
config.serverType === ServerType.SasViya
? await this.getJobUri(sasJob)
: ''
const apiUrl = `${config.serverUrl}${this.jobsPath}/?${
jobUri.length > 0
? '__program=' + program + '&_job=' + jobUri
: '_program=' + program
}`
let requestParams = {
...this.getRequestParams(config)
@@ -101,19 +97,6 @@ export class WebJobExecutor extends BaseJobExecutor {
this.appendRequest(res, sasJob, config.debug)
resolve(jsonResponse)
}
if (this.serverType === ServerType.Sas9 && config.debug) {
const jsonResponse = parseWeboutResponse(res.result as string)
if (jsonResponse === '') {
throw new Error(
'Valid JSON could not be extracted from response.'
)
}
isValidJson(jsonResponse)
this.appendRequest(res, sasJob, config.debug)
resolve(res.result)
}
isValidJson(res.result as string)
this.appendRequest(res, sasJob, config.debug)
resolve(res.result)
})

View File

@@ -1,5 +1,4 @@
export * from './ComputeJobExecutor'
export * from './JesJobExecutor'
export * from './JobExecutor'
export * from './Sas9JobExecutor'
export * from './WebJobExecutor'

View File

@@ -10,8 +10,6 @@ import {
} from '../types/errors'
import { parseWeboutResponse } from '../utils/parseWeboutResponse'
import { prefixMessage } from '@sasjs/utils/error'
import { SAS9AuthError } from '../types/errors/SAS9AuthError'
import { isValidJson } from '../utils'
export interface HttpClient {
get<T>(
@@ -46,11 +44,11 @@ export interface HttpClient {
}
export class RequestClient implements HttpClient {
protected csrfToken: CsrfToken = { headerName: '', value: '' }
protected fileUploadCsrfToken: CsrfToken | undefined
protected httpClient: AxiosInstance
private csrfToken: CsrfToken = { headerName: '', value: '' }
private fileUploadCsrfToken: CsrfToken | undefined
private httpClient: AxiosInstance
constructor(protected baseUrl: string, allowInsecure = false) {
constructor(private baseUrl: string, allowInsecure = false) {
const https = require('https')
if (allowInsecure && https.Agent) {
this.httpClient = axios.create({
@@ -64,9 +62,6 @@ export class RequestClient implements HttpClient {
baseURL: baseUrl
})
}
this.httpClient.defaults.validateStatus = (status) =>
status >= 200 && status < 305
}
public getCsrfToken(type: 'general' | 'file' = 'general') {
@@ -291,12 +286,11 @@ export class RequestClient implements HttpClient {
})
.then((res) => res.data)
.catch((error) => {
const logger = process.logger || console
logger.error(error)
console.log(error)
})
}
protected getHeaders = (
private getHeaders = (
accessToken: string | undefined,
contentType: string
) => {
@@ -321,7 +315,7 @@ export class RequestClient implements HttpClient {
return headers
}
protected parseAndSetFileUploadCsrfToken = (response: AxiosResponse) => {
private parseAndSetFileUploadCsrfToken = (response: AxiosResponse) => {
const token = this.parseCsrfToken(response)
if (token) {
@@ -329,7 +323,7 @@ export class RequestClient implements HttpClient {
}
}
protected parseAndSetCsrfToken = (response: AxiosResponse) => {
private parseAndSetCsrfToken = (response: AxiosResponse) => {
const token = this.parseCsrfToken(response)
if (token) {
@@ -353,7 +347,7 @@ export class RequestClient implements HttpClient {
}
}
protected handleError = async (
private handleError = async (
e: any,
callback: any,
debug: boolean = false
@@ -411,7 +405,7 @@ export class RequestClient implements HttpClient {
throw e
}
protected parseResponse<T>(response: AxiosResponse<any>) {
private parseResponse<T>(response: AxiosResponse<any>) {
const etag = response?.headers ? response.headers['etag'] : ''
let parsedResponse
let includeSAS9Log: boolean = false
@@ -424,13 +418,7 @@ export class RequestClient implements HttpClient {
}
} catch {
try {
const weboutResponse = parseWeboutResponse(response.data)
if (weboutResponse === '') {
throw new Error('Valid JSON could not be extracted from response.')
}
isValidJson(weboutResponse)
parsedResponse = JSON.parse(weboutResponse)
parsedResponse = JSON.parse(parseWeboutResponse(response.data))
} catch {
parsedResponse = response.data
}
@@ -451,7 +439,7 @@ export class RequestClient implements HttpClient {
}
}
export const throwIfError = (response: AxiosResponse) => {
const throwIfError = (response: AxiosResponse) => {
if (response.status === 401) {
throw new LoginRequiredError()
}
@@ -482,10 +470,6 @@ export const throwIfError = (response: AxiosResponse) => {
throw new AuthorizeError(response.data.message, authorizeRequestUrl)
}
if (response.config?.url?.includes('sasAuthError')) {
throw new SAS9AuthError()
}
const error = parseError(response.data as string)
if (error) {

View File

@@ -1,121 +0,0 @@
import { AxiosRequestConfig } from 'axios'
import axiosCookieJarSupport from 'axios-cookiejar-support'
import * as tough from 'tough-cookie'
import { prefixMessage } from '@sasjs/utils/error'
import { RequestClient, throwIfError } from './RequestClient'
/**
* Specific request client for SAS9 in Node.js environments.
* Handles redirects and cookie management.
*/
export class Sas9RequestClient extends RequestClient {
constructor(baseUrl: string, allowInsecure = false) {
super(baseUrl, allowInsecure)
this.httpClient.defaults.maxRedirects = 0
this.httpClient.defaults.validateStatus = (status) =>
status >= 200 && status < 303
if (axiosCookieJarSupport) {
axiosCookieJarSupport(this.httpClient)
this.httpClient.defaults.jar = new tough.CookieJar()
}
}
public async login(username: string, password: string, jobsPath: string) {
const codeInjectorPath = `/User Folders/${username}/My Folder/sasjs/runner`
if (this.httpClient.defaults.jar) {
;(this.httpClient.defaults.jar as tough.CookieJar).removeAllCookies()
await this.get(
`${jobsPath}?_program=${codeInjectorPath}&_username=${username}&_password=${password}`,
undefined,
'text/plain'
)
}
}
public async get<T>(
url: string,
accessToken: string | undefined,
contentType: string = 'application/json',
overrideHeaders: { [key: string]: string | number } = {},
debug: boolean = false
): Promise<{ result: T; etag: string }> {
const headers = {
...this.getHeaders(accessToken, contentType),
...overrideHeaders
}
const requestConfig: AxiosRequestConfig = {
headers,
responseType: contentType === 'text/plain' ? 'text' : 'json',
withCredentials: true
}
if (contentType === 'text/plain') {
requestConfig.transformResponse = undefined
}
return this.httpClient
.get<T>(url, requestConfig)
.then((response) => {
if (response.status === 302) {
return this.get(
response.headers['location'],
accessToken,
contentType
)
}
throwIfError(response)
return this.parseResponse<T>(response)
})
.catch(async (e) => {
return await this.handleError(
e,
() =>
this.get<T>(url, accessToken, contentType, overrideHeaders).catch(
(err) => {
throw prefixMessage(
err,
'Error while executing handle error callback. '
)
}
),
debug
).catch((err) => {
throw prefixMessage(err, 'Error while handling error. ')
})
})
}
public post<T>(
url: string,
data: any,
accessToken: string | undefined,
contentType = 'application/json',
overrideHeaders: { [key: string]: string | number } = {}
): Promise<{ result: T; etag: string }> {
const headers = {
...this.getHeaders(accessToken, contentType),
...overrideHeaders
}
return this.httpClient
.post<T>(url, data, { headers, withCredentials: true })
.then(async (response) => {
if (response.status === 302) {
return await this.get(
response.headers['location'],
undefined,
contentType,
overrideHeaders
)
}
throwIfError(response)
return this.parseResponse<T>(response)
})
.catch(async (e) => {
return await this.handleError(e, () =>
this.post<T>(url, data, accessToken, contentType, overrideHeaders)
)
})
}
}

View File

@@ -1,9 +1,5 @@
/**
* @jest-environment jsdom
*/
import { FileUploader } from '../FileUploader'
import { SASjsConfig, UploadFile } from '../types'
import { UploadFile } from '../types'
import { RequestClient } from '../request/RequestClient'
import axios from 'axios'
jest.mock('axios')
@@ -32,51 +28,48 @@ const prepareFilesAndParams = () => {
}
describe('FileUploader', () => {
const config: SASjsConfig = {
...new SASjsConfig(),
appLoc: '/sample/apploc'
}
const fileUploader = new FileUploader(
config,
'/sample/apploc',
'https://sample.server.com',
'/jobs/path',
new RequestClient('https://sample.server.com')
)
it('should upload successfully', async () => {
it('should upload successfully', async (done) => {
const sasJob = 'test/upload'
const { files, params } = prepareFilesAndParams()
mockedAxios.post.mockImplementation(() =>
Promise.resolve({ data: sampleResponse })
)
const res = await fileUploader.uploadFile(sasJob, files, params)
expect(res).toEqual(JSON.parse(sampleResponse))
fileUploader.uploadFile(sasJob, files, params).then((res: any) => {
expect(res).toEqual(JSON.parse(sampleResponse))
done()
})
})
it('should an error when no files are provided', async () => {
it('should an error when no files are provided', async (done) => {
const sasJob = 'test/upload'
const files: UploadFile[] = []
const params = { table: 'libtable' }
const err = await fileUploader
.uploadFile(sasJob, files, params)
.catch((err: any) => err)
expect(err.error.message).toEqual('At least one file must be provided.')
fileUploader.uploadFile(sasJob, files, params).catch((err: any) => {
expect(err.error.message).toEqual('At least one file must be provided.')
done()
})
})
it('should throw an error when no sasJob is provided', async () => {
it('should throw an error when no sasJob is provided', async (done) => {
const sasJob = ''
const { files, params } = prepareFilesAndParams()
const err = await fileUploader
.uploadFile(sasJob, files, params)
.catch((err: any) => err)
expect(err.error.message).toEqual('sasJob must be provided.')
fileUploader.uploadFile(sasJob, files, params).catch((err: any) => {
expect(err.error.message).toEqual('sasJob must be provided.')
done()
})
})
it('should throw an error when login is required', async () => {
it('should throw an error when login is required', async (done) => {
mockedAxios.post.mockImplementation(() =>
Promise.resolve({ data: '<form action="Logon">' })
)
@@ -84,13 +77,15 @@ describe('FileUploader', () => {
const sasJob = 'test'
const { files, params } = prepareFilesAndParams()
const err = await fileUploader
.uploadFile(sasJob, files, params)
.catch((err: any) => err)
expect(err.error.message).toEqual('You must be logged in to upload a file.')
fileUploader.uploadFile(sasJob, files, params).catch((err: any) => {
expect(err.error.message).toEqual(
'You must be logged in to upload a file.'
)
done()
})
})
it('should throw an error when invalid JSON is returned by the server', async () => {
it('should throw an error when invalid JSON is returned by the server', async (done) => {
mockedAxios.post.mockImplementation(() =>
Promise.resolve({ data: '{invalid: "json"' })
)
@@ -98,13 +93,13 @@ describe('FileUploader', () => {
const sasJob = 'test'
const { files, params } = prepareFilesAndParams()
const err = await fileUploader
.uploadFile(sasJob, files, params)
.catch((err: any) => err)
expect(err.error.message).toEqual('File upload request failed.')
fileUploader.uploadFile(sasJob, files, params).catch((err: any) => {
expect(err.error.message).toEqual('File upload request failed.')
done()
})
})
it('should throw an error when the server request fails', async () => {
it('should throw an error when the server request fails', async (done) => {
mockedAxios.post.mockImplementation(() =>
Promise.reject({ data: '{message: "Server error"}' })
)
@@ -112,9 +107,10 @@ describe('FileUploader', () => {
const sasJob = 'test'
const { files, params } = prepareFilesAndParams()
const err = await fileUploader
.uploadFile(sasJob, files, params)
.catch((err: any) => err)
expect(err.error.message).toEqual('File upload request failed.')
fileUploader.uploadFile(sasJob, files, params).catch((err: any) => {
expect(err.error.message).toEqual('File upload request failed.')
done()
})
})
})

View File

@@ -14,7 +14,7 @@ describe('FolderOperations', () => {
beforeEach(() => {})
it('should move and rename folder', async () => {
it('should move and rename folder', async (done) => {
mockFetchResponse(false)
let res: any = await sasViyaApiClient.moveFolder(
@@ -26,9 +26,11 @@ describe('FolderOperations', () => {
expect(res.folder.name).toEqual('newName')
expect(res.folder.parentFolderUri.split('=')[1]).toEqual('/Test/toFolder')
done()
})
it('should move and keep the name of folder', async () => {
it('should move and keep the name of folder', async (done) => {
mockFetchResponse(true)
let res: any = await sasViyaApiClient.moveFolder(
@@ -40,9 +42,11 @@ describe('FolderOperations', () => {
expect(res.folder.name).toEqual('oldName')
expect(res.folder.parentFolderUri.split('=')[1]).toEqual('/Test/toFolder')
done()
})
it('should only rename folder', async () => {
it('should only rename folder', async (done) => {
mockFetchResponse(false)
let res: any = await sasViyaApiClient.moveFolder(
@@ -54,6 +58,8 @@ describe('FolderOperations', () => {
expect(res.folder.name).toEqual('newName')
expect(res.folder.parentFolderUri.split('=')[1]).toEqual('/Test/toFolder')
done()
})
})

View File

@@ -1,6 +1,6 @@
import { parseGeneratedCode } from '../../utils/index'
it('should parse generated code', () => {
it('should parse generated code', async (done) => {
expect(sampleResponse).toBeTruthy()
const parsedGeneratedCode = parseGeneratedCode(sampleResponse)
@@ -15,6 +15,8 @@ it('should parse generated code', () => {
expect(generatedCodeLines[2].startsWith('MPRINT(MM_WEBOUT)')).toBeTruthy()
expect(generatedCodeLines[3].startsWith('MPRINT(MM_WEBRIGHT)')).toBeTruthy()
expect(generatedCodeLines[4].startsWith('MPRINT(MM_WEBOUT)')).toBeTruthy()
done()
})
/* tslint:disable */

View File

@@ -1,6 +1,6 @@
import { parseSourceCode } from '../../utils/index'
it('should parse SAS9 source code', async () => {
it('should parse SAS9 source code', async (done) => {
expect(sampleResponse).toBeTruthy()
const parsedSourceCode = parseSourceCode(sampleResponse)
@@ -15,6 +15,8 @@ it('should parse SAS9 source code', async () => {
expect(sourceCodeLines[2].startsWith('8')).toBeTruthy()
expect(sourceCodeLines[3].startsWith('9')).toBeTruthy()
expect(sourceCodeLines[4].startsWith('10')).toBeTruthy()
done()
})
/* tslint:disable */

View File

@@ -1,8 +0,0 @@
import { Link } from './Link'
export interface File {
id: string
name: string
parentUri: string
links: Link[]
}

View File

@@ -1,5 +0,0 @@
declare namespace NodeJS {
export interface Process {
logger?: import('@sasjs/utils/logger').Logger
}
}

View File

@@ -40,19 +40,23 @@ export class SASjsConfig {
*/
debug: boolean = true
/**
* The name of the compute context to use when calling the Viya services directly.
* The name of the compute context to use when calling the Viya APIs directly.
* Example value: 'SAS Job Execution compute context'
* If set to missing or empty, and useComputeApi is true, the adapter will use
* the JES APIs. If provided, the Job Code will be executed in pooled
* compute sessions on this named context.
*/
contextName: string = ''
/**
* If it's `false` adapter will use the JES API as connection approach. To enhance VIYA
* Set to `false` to use the Job Execution Web Service. To enhance VIYA
* performance, set to `true` and provide a `contextName` on which to run
* the code. When running on a named context, the code executes under the
* user identity. When running as a Job Execution service, the code runs
* under the identity in the JES context. If `useComputeApi` is `null` or `undefined`, the service will run as a Job, except
* under the identity in the JES context. If no `contextName` is provided,
* and `useComputeApi` is `true`, then the service will run as a Job, except
* triggered using the APIs instead of the Job Execution Web Service broker.
*/
useComputeApi: boolean | null = null
useComputeApi = false
/**
* Defaults to `false`.
* When set to `true`, the adapter will allow requests to SAS servers that use a self-signed SSL certificate.

View File

@@ -1,9 +0,0 @@
export class SAS9AuthError extends Error {
constructor() {
super(
'The credentials you provided cannot be authenticated. Please provide a valid set of credentials.'
)
this.name = 'AuthorizeError'
Object.setPrototypeOf(this, SAS9AuthError.prototype)
}
}

View File

@@ -1,7 +1,6 @@
export * from './Context'
export * from './CsrfToken'
export * from './Folder'
export * from './File'
export * from './Job'
export * from './JobDefinition'
export * from './JobResult'

View File

@@ -15,14 +15,12 @@ export const fetchLogByChunks = async (
logUrl: string,
logCount: number
): Promise<string> => {
const logger = process.logger || console
let log: string = ''
const loglimit = logCount < 10000 ? logCount : 10000
let start = 0
do {
logger.info(
console.log(
`Fetching logs from line no: ${start + 1} to ${
start + loglimit
} of ${logCount}.`

View File

@@ -12,4 +12,3 @@ export * from './serialize'
export * from './splitChunks'
export * from './parseWeboutResponse'
export * from './fetchLogByChunks'
export * from './isValidJson'

View File

@@ -1,11 +0,0 @@
/**
* Checks if string is in valid JSON format else throw error.
* @param str - string to check.
*/
export const isValidJson = (str: string) => {
try {
JSON.parse(str)
} catch (e) {
throw new Error('Invalid JSON response.')
}
}

View File

@@ -1,32 +1,21 @@
const path = require('path')
const webpack = require('webpack')
const terserPlugin = require('terser-webpack-plugin')
const nodePolyfillPlugin = require('node-polyfill-webpack-plugin')
const defaultPlugins = [
new webpack.ContextReplacementPlugin(/moment[\/\\]locale$/, /en/),
new webpack.SourceMapDevToolPlugin({
filename: null,
exclude: [/node_modules/],
test: /\.ts($|\?)/i
})
]
const optimization = {
minimize: true,
minimizer: [
new terserPlugin({
parallel: true,
terserOptions: {}
})
]
}
const browserConfig = {
entry: './src/index.ts',
devtool: 'inline-source-map',
mode: 'production',
optimization: optimization,
optimization: {
minimizer: [
new terserPlugin({
cache: true,
parallel: true,
sourceMap: true,
terserOptions: {}
})
]
},
module: {
rules: [
{
@@ -38,7 +27,7 @@ const browserConfig = {
},
resolve: {
extensions: ['.ts', '.js'],
fallback: { https: false, fs: false, readline: false }
fallback: { https: false }
},
output: {
filename: 'index.js',
@@ -47,27 +36,17 @@ const browserConfig = {
library: 'SASjs'
},
plugins: [
...defaultPlugins,
new webpack.ProvidePlugin({
process: 'process/browser'
}),
new nodePolyfillPlugin()
new webpack.ContextReplacementPlugin(/moment[\/\\]locale$/, /en/),
new webpack.SourceMapDevToolPlugin({
filename: null,
exclude: [/node_modules/],
test: /\.ts($|\?)/i
})
]
}
const browserConfigWithoutProcessPlugin = {
entry: browserConfig.entry,
devtool: browserConfig.devtool,
mode: browserConfig.mode,
optimization: optimization,
module: browserConfig.module,
resolve: browserConfig.resolve,
output: browserConfig.output,
plugins: defaultPlugins
}
const nodeConfig = {
...browserConfigWithoutProcessPlugin,
...browserConfig,
target: 'node',
entry: './node/index.ts',
output: {