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

Compare commits

..

1 Commits

Author SHA1 Message Date
2c6198ae25 chore: for krishna 2021-09-06 19:28:28 +05:00
147 changed files with 10611 additions and 56218 deletions

View File

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

View File

@@ -13,15 +13,14 @@ jobs:
strategy:
matrix:
node-version: [lts/fermium]
node-version: [15.x]
steps:
- uses: actions/checkout@v2
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v2
uses: actions/setup-node@v1
with:
node-version: ${{ matrix.node-version }}
cache: npm
- name: Install Dependencies
run: npm ci
- name: Check code style

2
.gitignore vendored
View File

@@ -5,4 +5,4 @@ build
/coverage
.DS_Store
.DS_Store

View File

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

View File

@@ -36,7 +36,7 @@ Ok ok. Deploy this [example.html](https://raw.githubusercontent.com/sasjs/adapte
The backend part can be deployed as follows:
```sas
```
%let appLoc=/Public/app/readme; /* Metadata or Viya Folder per SASjs config */
filename mc url "https://raw.githubusercontent.com/sasjs/core/main/all.sas";
%inc mc; /* compile macros (can also be downloaded & compiled seperately) */
@@ -85,11 +85,11 @@ let sasJs = new SASjs.default(
);
```
If you've installed it via NPM, you can import it as a default import like so:
```js
```
import SASjs from '@sasjs/adapter';
```
You can then instantiate it with:
```js
```
const sasJs = new SASjs({your config})
```
@@ -119,7 +119,6 @@ sasJs.request("/path/to/my/service", dataObject)
console.log(response.tablewith2cols1row[0].COL1.value)
})
```
We supply the path to the SAS service, and a data object. The data object can be null (for services with no input), or can contain one or more tables in the following format:
```javascript
@@ -147,7 +146,6 @@ The adapter will also cache the logs (if debug enabled) and even the work tables
The SAS side is handled by a number of macros in the [macro core](https://github.com/sasjs/core) library.
The following snippet shows the process of SAS tables arriving / leaving:
```sas
/* fetch all input tables sent from frontend - they arrive as work tables */
%webout(FETCH)
@@ -163,6 +161,7 @@ run;
%webout(OBJ,tables,fmt=N) /* unformatted (raw) data */
%webout(OBJ,tables,label=newtable) /* rename tables on export */
%webout(CLOSE) /* close the JSON and send some extra useful variables too */
```
## Configuration
@@ -173,7 +172,6 @@ Configuration on the client side involves passing an object on startup, which ca
* `serverType` - either `SAS9` or `SASVIYA`.
* `serverUrl` - the location (including http protocol and port) of the SAS Server. Can be omitted, eg if serving directly from the SAS Web Server, or in streaming mode.
* `debug` - if `true` then SAS Logs and extra debug information is returned.
* `LoginMechanism` - either `Default` or `Redirected`. If `Redirected` then authentication occurs through the injection of an additional screen, which contains the SASLogon prompt. This allows for more complex authentication flows (such as 2FA) and avoids the need to handle passwords in the application itself. The styling of the redirect flow can also be modified. If left at "Default" then the developer must capture the username and password and use these with the `.login()` method.
* `useComputeApi` - Only relevant when the serverType is `SASVIYA`. If `true` the [Compute API](#using-the-compute-api) is used. If `false` the [JES API](#using-the-jes-api) is used. If `null` or `undefined` the [Web](#using-jes-web-app) approach is used.
* `contextName` - Compute context on which the requests will be called. If missing or not provided, defaults to `Job Execution Compute context`.
@@ -198,7 +196,7 @@ Here we are running Jobs using the Job Execution Service except this time we are
This approach (`useComputeApi: false`) also ensures that jobs are displayed in Environment Manager.
```json
```
{
appLoc:"/Your/Path",
serverType:"SASVIYA",
@@ -212,12 +210,12 @@ This approach is by far the fastest, as a result of the optimisations we have bu
With this approach (`useComputeApi: true`), the requests/logs will _not_ appear in the list in Environment manager.
```json
```
{
appLoc:"/Your/Path",
serverType:"SASVIYA",
useComputeApi: true,
contextName: "yourComputeContext"
contextName: 'yourComputeContext'
}
```

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

20050
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -41,40 +41,36 @@
"license": "ISC",
"devDependencies": {
"@types/axios": "^0.14.0",
"@types/express": "^4.17.13",
"@types/form-data": "^2.5.0",
"@types/jest": "^27.0.2",
"@types/jest": "^27.0.1",
"@types/mime": "^2.0.3",
"@types/pem": "^1.9.6",
"@types/tough-cookie": "^4.0.1",
"copyfiles": "^2.4.1",
"cp": "^0.2.0",
"dotenv": "^10.0.0",
"express": "^4.17.1",
"jest": "^27.2.0",
"jest": "^27.1.0",
"jest-extended": "^0.11.5",
"node-polyfill-webpack-plugin": "^1.1.4",
"path": "^0.12.7",
"pem": "^1.14.4",
"process": "^0.11.10",
"rimraf": "^3.0.2",
"semantic-release": "^18.0.0",
"terser-webpack-plugin": "^5.2.4",
"semantic-release": "^17.4.7",
"terser-webpack-plugin": "^5.2.0",
"ts-jest": "^27.0.3",
"ts-loader": "^9.2.6",
"ts-loader": "^9.2.2",
"tslint": "^6.1.3",
"tslint-config-prettier": "^1.18.0",
"typedoc": "0.19.2",
"typedoc": "^0.21.9",
"typedoc-neo-theme": "^1.1.1",
"typedoc-plugin-external-module-name": "^4.0.6",
"typescript": "4.3.5",
"webpack": "^5.56.0",
"webpack": "^5.44.0",
"webpack-cli": "^4.7.2"
},
"main": "index.js",
"dependencies": {
"@sasjs/utils": "^2.32.0",
"axios": "^0.21.4",
"@sasjs/utils": "^2.30.0",
"axios": "^0.21.1",
"axios-cookiejar-support": "^1.0.1",
"form-data": "^4.0.0",
"https": "^1.0.0",

File diff suppressed because it is too large Load Diff

View File

@@ -5,7 +5,7 @@
"private": true,
"dependencies": {
"@sasjs/adapter": "file:../build/sasjs-adapter-5.0.0.tgz",
"@sasjs/test-framework": "^1.4.2",
"@sasjs/test-framework": "^1.4.0",
"@types/jest": "^26.0.20",
"@types/node": "^14.14.41",
"@types/react": "^17.0.1",
@@ -23,7 +23,7 @@
"test": "react-scripts test",
"eject": "react-scripts eject",
"update:adapter": "cd .. && npm run package:lib && cd sasjs-tests && npm i ../build/sasjs-adapter-5.0.0.tgz",
"deploy:tests": "rsync -avhe ssh ./build/* --delete $SSH_ACCOUNT:$DEPLOY_PATH || npm run deploy:tests-win",
"deploy:tests": "rsync -avhe ssh ./build/* --delete sabhas@sas.analytium.co.uk:/var/www/html/sabhas/sasjs-test || npm run deploy:tests-win",
"deploy:tests-win": "scp %DEPLOY_PATH% ./build/*",
"deploy": "npm run update:adapter && npm run build && npm run deploy:tests"
},

View File

@@ -2,7 +2,7 @@
"userName": "",
"password": "",
"sasJsConfig": {
"serverUrl": "",
"serverUrl": "https://sas.analytium.co.uk/",
"appLoc": "/Public/app",
"serverType": "SASVIYA",
"debug": false,

View File

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

View File

@@ -1,4 +1,4 @@
import SASjs, { LoginMechanism, SASjsConfig } from '@sasjs/adapter'
import SASjs, { SASjsConfig } from '@sasjs/adapter'
import { TestSuite } from '@sasjs/test-framework'
import { ServerType } from '@sasjs/utils/types'
@@ -13,7 +13,7 @@ const defaultConfig: SASjsConfig = {
debug: false,
contextName: 'SAS Job Execution compute context',
useComputeApi: false,
loginMechanism: LoginMechanism.Default
allowInsecureRequests: false
}
const customConfig = {
@@ -41,19 +41,6 @@ export const basicTests = (
assertion: (response: any) =>
response && response.isLoggedIn && response.userName === userName
},
{
title: 'Fetch username for already logged in user',
description: 'Should log the user in',
test: async () => {
await adapter.logIn(userName, password)
const newAdapterIns = new SASjs(adapter.getSasjsConfig())
return await newAdapterIns.checkSession()
},
assertion: (response: any) =>
response?.isLoggedIn && response?.userName === userName
},
{
title: 'Multiple Log in attempts',
description:
@@ -61,7 +48,7 @@ export const basicTests = (
test: async () => {
await adapter.logOut()
await adapter.logIn('invalid', 'invalid')
return await adapter.logIn(userName, password)
return adapter.logIn(userName, password)
},
assertion: (response: any) =>
response && response.isLoggedIn && response.userName === userName
@@ -77,8 +64,8 @@ export const basicTests = (
'common/sendArr',
stringData,
undefined,
async () => {
await adapter.logIn(userName, password)
() => {
adapter.logIn(userName, password)
}
)
},
@@ -164,7 +151,7 @@ export const basicTests = (
description:
'Should complete successful request with extra attributes present in response',
test: async () => {
const config: Partial<SASjsConfig> = {
const config = {
useComputeApi: false
}

107
src/FileUploader.ts Normal file
View File

@@ -0,0 +1,107 @@
import { isUrl, getValidJson, parseSasViyaDebugResponse } from './utils'
import { UploadFile } from './types/UploadFile'
import { ErrorResponse, LoginRequiredError } from './types/errors'
import { RequestClient } from './request/RequestClient'
import { ServerType } from '@sasjs/utils/types'
import SASjs from './SASjs'
import { Server } from 'https'
import { SASjsConfig } from './types'
import { config } from 'process'
export class FileUploader {
constructor(
private sasjsConfig: SASjsConfig,
private jobsPath: string,
private requestClient: RequestClient
) {
if (this.sasjsConfig.serverUrl) isUrl(this.sasjsConfig.serverUrl)
}
public uploadFile(sasJob: string, files: UploadFile[], params: any) {
if (files?.length < 1)
return Promise.reject(
new ErrorResponse('At least one file must be provided.')
)
if (!sasJob || sasJob === '')
return Promise.reject(new ErrorResponse('sasJob must be provided.'))
let paramsString = ''
for (let param in params) {
if (params.hasOwnProperty(param)) {
paramsString += `&${param}=${params[param]}`
}
}
const program = this.sasjsConfig.appLoc
? this.sasjsConfig.appLoc.replace(/\/?$/, '/') + sasJob.replace(/^\//, '')
: sasJob
const uploadUrl = `${this.jobsPath}/?${
'_program=' + program
}${paramsString}`
const formData = new FormData()
for (let file of files) {
formData.append('file', file.file, file.fileName)
}
const csrfToken = this.requestClient.getCsrfToken('file')
if (csrfToken) formData.append('_csrf', csrfToken.value)
if (this.sasjsConfig.debug) formData.append('_debug', '131')
if (
this.sasjsConfig.serverType === ServerType.SasViya &&
this.sasjsConfig.contextName
)
formData.append('_contextname', this.sasjsConfig.contextName)
const headers = {
'cache-control': 'no-cache',
Accept: '*/*',
'Content-Type': 'text/plain'
}
// currently only web approach is supported for file upload
// therefore log is part of response with debug enabled and must be parsed
return this.requestClient
.post(
uploadUrl,
formData,
undefined,
'application/json',
headers,
this.sasjsConfig.debug,
true,
sasJob
)
.then(async (res) => {
if (
this.sasjsConfig.serverType === ServerType.SasViya &&
this.sasjsConfig.debug
) {
const jsonResponse = await parseSasViyaDebugResponse(
res.result as string,
this.requestClient,
this.sasjsConfig.serverUrl
)
return jsonResponse
}
return typeof res.result === 'string'
? getValidJson(res.result)
: res.result
//TODO: append to SASjs requests
})
.catch((err: Error) => {
if (err instanceof LoginRequiredError) {
return Promise.reject(
new ErrorResponse('You must be logged in to upload a file.', err)
)
}
return Promise.reject(
new ErrorResponse('File upload request failed.', err)
)
})
}
}

View File

@@ -1,4 +1,3 @@
import * as https from 'https'
import { generateTimestamp } from '@sasjs/utils/time'
import * as NodeFormData from 'form-data'
import { Sas9RequestClient } from './request/Sas9RequestClient'
@@ -14,10 +13,10 @@ export class SAS9ApiClient {
constructor(
private serverUrl: string,
private jobsPath: string,
httpsAgentOptions?: https.AgentOptions
allowInsecureRequests: boolean
) {
if (serverUrl) isUrl(serverUrl)
this.requestClient = new Sas9RequestClient(serverUrl, httpsAgentOptions)
this.requestClient = new Sas9RequestClient(serverUrl, allowInsecureRequests)
}
/**

View File

@@ -22,8 +22,8 @@ import { pollJobState } from './api/viya/pollJobState'
import { getTokens } from './auth/getTokens'
import { uploadTables } from './api/viya/uploadTables'
import { executeScript } from './api/viya/executeScript'
import { getAccessTokenForViya } from './auth/getAccessTokenForViya'
import { refreshTokensForViya } from './auth/refreshTokensForViya'
import { getAccessToken } from './auth/getAccessToken'
import { refreshTokens } from './auth/refreshTokens'
/**
* A client for interfacing with the SAS Viya REST API.
@@ -51,16 +51,6 @@ export class SASViyaApiClient {
)
private folderMap = new Map<string, Job[]>()
/**
* A helper method used to call appendRequest method of RequestClient
* @param response - response from sasjs request
* @param program - name of program
* @param debug - a boolean that indicates whether debug was enabled or not
*/
public appendRequest(response: any, program: string, debug: boolean) {
this.requestClient!.appendRequest(response, program, debug)
}
public get debug() {
return this._debug
}
@@ -534,26 +524,21 @@ export class SASViyaApiClient {
clientSecret: string,
authCode: string
): Promise<SasAuthResponse> {
return getAccessTokenForViya(
this.requestClient,
clientId,
clientSecret,
authCode
)
return getAccessToken(this.requestClient, clientId, clientSecret, authCode)
}
/**
* Exchanges the refresh token for an access token for the given client.
* @param clientId - the client ID to authenticate with.
* @param clientSecret - the client secret to authenticate with.
* @param refreshToken - the refresh token received from the server.
* @param authCode - the refresh token received from the server.
*/
public async refreshTokens(
clientId: string,
clientSecret: string,
refreshToken: string
) {
return refreshTokensForViya(
return refreshTokens(
this.requestClient,
clientId,
clientSecret,
@@ -797,12 +782,24 @@ export class SASViyaApiClient {
jobResult = await this.requestClient.get<any>(
`${this.serverUrl}${resultLink}/content`,
access_token,
'text/plain'
'text/plain',
{},
debug,
true,
sasJob
)
}
if (debug && logLink) {
log = await this.requestClient
.get<any>(`${this.serverUrl}${logLink.href}/content`, access_token)
.get<any>(
`${this.serverUrl}${logLink.href}/content`,
access_token,
'application/json',
{},
debug,
true,
sasJob
)
.then((res: any) => res.result.items.map((i: any) => i.line).join('\n'))
}
if (jobStatus === 'failed') {

View File

@@ -1,41 +1,27 @@
import { compareTimestamps, asyncForEach } from './utils'
import {
SASjsConfig,
UploadFile,
EditContextInput,
PollOptions,
LoginMechanism,
FolderMember,
ServiceMember,
ExecutionQuery
} from './types'
import { SASjsConfig, UploadFile, EditContextInput, PollOptions } from './types'
import { SASViyaApiClient } from './SASViyaApiClient'
import { SAS9ApiClient } from './SAS9ApiClient'
import { SASjsApiClient, SASjsAuthResponse } from './SASjsApiClient'
import { FileUploader } from './FileUploader'
import { AuthManager } from './auth'
import {
ServerType,
MacroVar,
AuthConfig,
ExtraResponseAttributes,
SasAuthResponse
ExtraResponseAttributes
} from '@sasjs/utils/types'
import { RequestClient } from './request/RequestClient'
import { SasjsRequestClient } from './request/SasjsRequestClient'
import {
JobExecutor,
WebJobExecutor,
ComputeJobExecutor,
JesJobExecutor,
Sas9JobExecutor,
FileUploader
Sas9JobExecutor
} from './job-execution'
import { ErrorResponse } from './types/errors'
import { LoginOptions, LoginResult } from './types/Login'
const defaultConfig: SASjsConfig = {
serverUrl: '',
pathSASJS: '/SASjsApi/stp/execute',
pathSAS9: '/SASStoredProcess/do',
pathSASViya: '/SASJobExecution',
appLoc: '/Public/seedapp',
@@ -43,7 +29,7 @@ const defaultConfig: SASjsConfig = {
debug: false,
contextName: 'SAS Job Execution compute context',
useComputeApi: null,
loginMechanism: LoginMechanism.Default
allowInsecureRequests: false
}
/**
@@ -55,7 +41,6 @@ export default class SASjs {
private jobsPath: string = ''
private sasViyaApiClient: SASViyaApiClient | null = null
private sas9ApiClient: SAS9ApiClient | null = null
private sasJSApiClient: SASjsApiClient | null = null
private fileUploader: FileUploader | null = null
private authManager: AuthManager | null = null
private requestClient: RequestClient | null = null
@@ -64,7 +49,8 @@ export default class SASjs {
private jesJobExecutor: JobExecutor | null = null
private sas9JobExecutor: JobExecutor | null = null
constructor(config?: Partial<SASjsConfig>) {
constructor(config?: any) {
console.log('from SASjs constructor')
this.sasjsConfig = {
...defaultConfig,
...config
@@ -82,7 +68,7 @@ export default class SASjs {
userName: string,
password: string
) {
this.isMethodSupported('executeScriptSAS9', [ServerType.Sas9])
this.isMethodSupported('executeScriptSAS9', ServerType.Sas9)
return await this.sas9ApiClient?.executeScript(
linesOfCode,
@@ -96,7 +82,7 @@ export default class SASjs {
* @param accessToken - an access token for an authorized user.
*/
public async getComputeContexts(accessToken: string) {
this.isMethodSupported('getComputeContexts', [ServerType.SasViya])
this.isMethodSupported('getComputeContexts', ServerType.SasViya)
return await this.sasViyaApiClient!.getComputeContexts(accessToken)
}
@@ -106,7 +92,7 @@ export default class SASjs {
* @param accessToken - an access token for an authorized user.
*/
public async getLauncherContexts(accessToken: string) {
this.isMethodSupported('getLauncherContexts', [ServerType.SasViya])
this.isMethodSupported('getLauncherContexts', ServerType.SasViya)
return await this.sasViyaApiClient!.getLauncherContexts(accessToken)
}
@@ -115,7 +101,7 @@ export default class SASjs {
* Gets default(system) launcher contexts.
*/
public getDefaultComputeContexts() {
this.isMethodSupported('getDefaultComputeContexts', [ServerType.SasViya])
this.isMethodSupported('getDefaultComputeContexts', ServerType.SasViya)
return this.sasViyaApiClient!.getDefaultComputeContexts()
}
@@ -125,7 +111,7 @@ export default class SASjs {
* @param authConfig - an access token, refresh token, client and secret for an authorized user.
*/
public async getExecutableContexts(authConfig: AuthConfig) {
this.isMethodSupported('getExecutableContexts', [ServerType.SasViya])
this.isMethodSupported('getExecutableContexts', ServerType.SasViya)
return await this.sasViyaApiClient!.getExecutableContexts(authConfig)
}
@@ -147,7 +133,7 @@ export default class SASjs {
accessToken: string,
authorizedUsers?: string[]
) {
this.isMethodSupported('createComputeContext', [ServerType.SasViya])
this.isMethodSupported('createComputeContext', ServerType.SasViya)
return await this.sasViyaApiClient!.createComputeContext(
contextName,
@@ -172,7 +158,7 @@ export default class SASjs {
launchType: string,
accessToken: string
) {
this.isMethodSupported('createLauncherContext', [ServerType.SasViya])
this.isMethodSupported('createLauncherContext', ServerType.SasViya)
return await this.sasViyaApiClient!.createLauncherContext(
contextName,
@@ -193,7 +179,7 @@ export default class SASjs {
editedContext: EditContextInput,
accessToken?: string
) {
this.isMethodSupported('editComputeContext', [ServerType.SasViya])
this.isMethodSupported('editComputeContext', ServerType.SasViya)
return await this.sasViyaApiClient!.editComputeContext(
contextName,
@@ -208,7 +194,7 @@ export default class SASjs {
* @param accessToken - an access token for an authorized user.
*/
public async deleteComputeContext(contextName: string, accessToken?: string) {
this.isMethodSupported('deleteComputeContext', [ServerType.SasViya])
this.isMethodSupported('deleteComputeContext', ServerType.SasViya)
return await this.sasViyaApiClient!.deleteComputeContext(
contextName,
@@ -226,7 +212,7 @@ export default class SASjs {
contextName: string,
accessToken?: string
) {
this.isMethodSupported('getComputeContextByName', [ServerType.SasViya])
this.isMethodSupported('getComputeContextByName', ServerType.SasViya)
return await this.sasViyaApiClient!.getComputeContextByName(
contextName,
@@ -240,7 +226,7 @@ export default class SASjs {
* @param accessToken - an access token for an authorized user.
*/
public async getComputeContextById(contextId: string, accessToken?: string) {
this.isMethodSupported('getComputeContextById', [ServerType.SasViya])
this.isMethodSupported('getComputeContextById', ServerType.SasViya)
return await this.sasViyaApiClient!.getComputeContextById(
contextId,
@@ -249,7 +235,7 @@ export default class SASjs {
}
public async createSession(contextName: string, accessToken: string) {
this.isMethodSupported('createSession', [ServerType.SasViya])
this.isMethodSupported('createSession', ServerType.SasViya)
return await this.sasViyaApiClient!.createSession(contextName, accessToken)
}
@@ -269,7 +255,7 @@ export default class SASjs {
authConfig?: AuthConfig,
debug?: boolean
) {
this.isMethodSupported('executeScriptSASViya', [ServerType.SasViya])
this.isMethodSupported('executeScriptSASViya', ServerType.SasViya)
if (!contextName) {
throw new Error(
'Context name is undefined. Please set a `contextName` in your SASjs or override config.'
@@ -359,7 +345,7 @@ export default class SASjs {
* @param accessToken - the access token to authorize the request.
*/
public async getFolder(folderPath: string, accessToken?: string) {
this.isMethodSupported('getFolder', [ServerType.SasViya])
this.isMethodSupported('getFolder', ServerType.SasViya)
return await this.sasViyaApiClient!.getFolder(folderPath, accessToken)
}
@@ -369,7 +355,7 @@ export default class SASjs {
* @param accessToken - an access token for authorizing the request.
*/
public async deleteFolder(folderPath: string, accessToken: string) {
this.isMethodSupported('deleteFolder', [ServerType.SasViya])
this.isMethodSupported('deleteFolder', ServerType.SasViya)
return await this.sasViyaApiClient?.deleteFolder(folderPath, accessToken)
}
@@ -384,7 +370,7 @@ export default class SASjs {
accessToken?: string,
limit?: number
) {
this.isMethodSupported('listFolder', [ServerType.SasViya])
this.isMethodSupported('listFolder', ServerType.SasViya)
return await this.sasViyaApiClient?.listFolder(
sourceFolder,
@@ -406,7 +392,7 @@ export default class SASjs {
targetFolderName: string,
accessToken: string
) {
this.isMethodSupported('moveFolder', [ServerType.SasViya])
this.isMethodSupported('moveFolder', ServerType.SasViya)
return await this.sasViyaApiClient?.moveFolder(
sourceFolder,
@@ -424,7 +410,7 @@ export default class SASjs {
accessToken?: string,
sasApiClient?: SASViyaApiClient
) {
this.isMethodSupported('createJobDefinition', [ServerType.SasViya])
this.isMethodSupported('createJobDefinition', ServerType.SasViya)
if (sasApiClient)
return await sasApiClient!.createJobDefinition(
@@ -444,7 +430,7 @@ export default class SASjs {
}
public async getAuthCode(clientId: string) {
this.isMethodSupported('getAuthCode', [ServerType.SasViya])
this.isMethodSupported('getAuthCode', ServerType.SasViya)
return await this.sasViyaApiClient!.getAuthCode(clientId)
}
@@ -459,14 +445,8 @@ export default class SASjs {
clientId: string,
clientSecret: string,
authCode: string
): Promise<SasAuthResponse | SASjsAuthResponse> {
this.isMethodSupported('getAccessToken', [
ServerType.SasViya,
ServerType.Sasjs
])
if (this.sasjsConfig.serverType === ServerType.Sasjs)
return await this.sasJSApiClient!.getAccessToken(clientId, authCode)
) {
this.isMethodSupported('getAccessToken', ServerType.SasViya)
return await this.sasViyaApiClient!.getAccessToken(
clientId,
@@ -475,24 +455,12 @@ export default class SASjs {
)
}
/**
* Exchanges the refresh token for an access token for the given client.
* @param clientId - the client ID to authenticate with.
* @param clientSecret - the client secret to authenticate with.
* @param refreshToken - the refresh token received from the server.
*/
public async refreshTokens(
clientId: string,
clientSecret: string,
refreshToken: string
): Promise<SasAuthResponse | SASjsAuthResponse> {
this.isMethodSupported('refreshTokens', [
ServerType.SasViya,
ServerType.Sasjs
])
if (this.sasjsConfig.serverType === ServerType.Sasjs)
return await this.sasJSApiClient!.refreshTokens(refreshToken)
) {
this.isMethodSupported('refreshTokens', ServerType.SasViya)
return await this.sasViyaApiClient!.refreshTokens(
clientId,
@@ -502,7 +470,7 @@ export default class SASjs {
}
public async deleteClient(clientId: string, accessToken: string) {
this.isMethodSupported('deleteClient', [ServerType.SasViya])
this.isMethodSupported('deleteClient', ServerType.SasViya)
return await this.sasViyaApiClient!.deleteClient(clientId, accessToken)
}
@@ -532,7 +500,7 @@ export default class SASjs {
...this.sasjsConfig,
...config
}
this.setupConfiguration()
await this.setupConfiguration()
}
/**
@@ -558,39 +526,9 @@ export default class SASjs {
* 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.'
)
if (this.sasjsConfig.serverType === ServerType.Sasjs) {
if (!clientId)
throw new Error(
'A username, password and clientId are required when using the default login mechanism with server type SASJS.'
)
return this.authManager!.logInSasjs(username, password, clientId)
}
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)
public async logIn(username: string, password: string) {
return this.authManager!.logIn(username, password)
}
/**
@@ -607,32 +545,24 @@ export default class SASjs {
* Process). Is prepended at runtime with the value of `appLoc`.
* @param files - array of files to be uploaded, including File object and file name.
* @param params - request URL parameters.
* @param 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.
* @param overrideSasjsConfig - object to override existing config (optional)
*/
public async uploadFile(
public uploadFile(
sasJob: string,
files: UploadFile[],
params: { [key: string]: any } | null,
config: { [key: string]: any } = {},
loginRequiredCallback?: () => any
params: any,
overrideSasjsConfig?: any
) {
config = {
...this.sasjsConfig,
...config
}
const data = { files, params }
const fileUploader = overrideSasjsConfig
? new FileUploader(
{ ...this.sasjsConfig, ...overrideSasjsConfig },
this.jobsPath,
this.requestClient!
)
: this.fileUploader ||
new FileUploader(this.sasjsConfig, this.jobsPath, this.requestClient!)
return await this.fileUploader!.execute(
sasJob,
data,
config,
loginRequiredCallback
)
return fileUploader.uploadFile(sasJob, files, params)
}
/**
@@ -682,6 +612,7 @@ export default class SASjs {
config.useComputeApi !== null
) {
if (config.useComputeApi) {
console.log(615)
return await this.computeJobExecutor!.execute(
sasJob,
data,
@@ -806,7 +737,7 @@ export default class SASjs {
accessToken?: string,
isForced = false
) {
this.isMethodSupported('deployServicePack', [ServerType.SasViya])
this.isMethodSupported('deployServicePack', ServerType.SasViya)
let sasApiClient: any = null
if (serverUrl || appLoc) {
@@ -828,7 +759,7 @@ export default class SASjs {
sasApiClient = new SAS9ApiClient(
serverUrl,
this.jobsPath,
this.sasjsConfig.httpsAgentOptions
this.sasjsConfig.allowInsecureRequests
)
}
} else {
@@ -860,14 +791,6 @@ export default class SASjs {
)
}
public async deployToSASjs(members: [FolderMember, ServiceMember]) {
return await this.sasJSApiClient?.deploy(members, this.sasjsConfig.appLoc)
}
public async executeJobSASjs(query: ExecutionQuery) {
return await this.sasJSApiClient?.executeJob(query)
}
/**
* Kicks off execution of the given job via the compute API.
* @returns an object representing the compute session created for the given job.
@@ -901,7 +824,7 @@ export default class SASjs {
...config
}
this.isMethodSupported('startComputeJob', [ServerType.SasViya])
this.isMethodSupported('startComputeJob', ServerType.SasViya)
if (!config.contextName) {
throw new Error(
'Context name is undefined. Please set a `contextName` in your SASjs or override config.'
@@ -926,7 +849,6 @@ export default class SASjs {
await this.webJobExecutor?.resendWaitingRequests()
await this.computeJobExecutor?.resendWaitingRequests()
await this.jesJobExecutor?.resendWaitingRequests()
await this.fileUploader?.resendWaitingRequests()
}
/**
@@ -963,8 +885,10 @@ export default class SASjs {
* @returns SASjsRequest[]
*/
public getSasRequests() {
const requests = [...this.requestClient!.getRequests()]
console.log('from getSASRequests')
const requests = this.requestClient!.getRequests()
const sortedRequests = requests.sort(compareTimestamps)
console.log('sortedRequests', sortedRequests)
return sortedRequests
}
@@ -992,28 +916,15 @@ export default class SASjs {
this.sasjsConfig.serverUrl = this.sasjsConfig.serverUrl.slice(0, -1)
}
if (!this.requestClient) {
const RequestClientClass =
this.sasjsConfig.serverType === ServerType.Sasjs
? SasjsRequestClient
: RequestClient
this.requestClient = new RequestClientClass(
this.sasjsConfig.serverUrl,
this.sasjsConfig.httpsAgentOptions
)
} else {
this.requestClient.setConfig(
this.sasjsConfig.serverUrl,
this.sasjsConfig.httpsAgentOptions
)
}
this.requestClient = new RequestClient(
this.sasjsConfig.serverUrl,
this.sasjsConfig.allowInsecureRequests
)
this.jobsPath =
this.sasjsConfig.serverType === ServerType.SasViya
? this.sasjsConfig.pathSASViya
: this.sasjsConfig.serverType === ServerType.Sas9
? this.sasjsConfig.pathSAS9
: this.sasjsConfig.pathSASJS || ''
: this.sasjsConfig.pathSAS9
this.authManager = new AuthManager(
this.sasjsConfig.serverUrl,
@@ -1023,49 +934,34 @@ export default class SASjs {
)
if (this.sasjsConfig.serverType === ServerType.SasViya) {
if (this.sasViyaApiClient) {
if (this.sasViyaApiClient)
this.sasViyaApiClient!.setConfig(
this.sasjsConfig.serverUrl,
this.sasjsConfig.appLoc
)
} else {
else
this.sasViyaApiClient = new SASViyaApiClient(
this.sasjsConfig.serverUrl,
this.sasjsConfig.appLoc,
this.sasjsConfig.contextName,
this.requestClient
)
}
this.sasViyaApiClient.debug = this.sasjsConfig.debug
}
if (this.sasjsConfig.serverType === ServerType.Sas9) {
if (this.sas9ApiClient) {
if (this.sas9ApiClient)
this.sas9ApiClient!.setConfig(this.sasjsConfig.serverUrl)
} else {
else
this.sas9ApiClient = new SAS9ApiClient(
this.sasjsConfig.serverUrl,
this.jobsPath,
this.sasjsConfig.httpsAgentOptions
this.sasjsConfig.allowInsecureRequests
)
}
}
if (this.sasjsConfig.serverType === ServerType.Sasjs) {
if (this.sasJSApiClient) {
this.sasJSApiClient.setConfig(this.sasjsConfig.serverUrl)
} else {
this.sasJSApiClient = new SASjsApiClient(
this.sasjsConfig.serverUrl,
this.requestClient
)
}
}
this.fileUploader = new FileUploader(
this.sasjsConfig.serverUrl,
this.sasjsConfig.serverType!,
this.sasjsConfig,
this.jobsPath,
this.requestClient
)
@@ -1082,8 +978,7 @@ export default class SASjs {
this.sasjsConfig.serverUrl,
this.sasjsConfig.serverType!,
this.jobsPath,
this.requestClient,
this.sasjsConfig.httpsAgentOptions
this.sasjsConfig.allowInsecureRequests
)
this.computeJobExecutor = new ComputeJobExecutor(
@@ -1150,15 +1045,12 @@ export default class SASjs {
})
}
private isMethodSupported(method: string, serverTypes: ServerType[]) {
if (
!this.sasjsConfig.serverType ||
!serverTypes.includes(this.sasjsConfig.serverType)
) {
private isMethodSupported(method: string, serverType: string) {
if (this.sasjsConfig.serverType !== serverType) {
throw new Error(
`Method '${method}' is only supported on ${serverTypes.join(
', '
)} servers.`
`Method '${method}' is only supported on ${
serverType === ServerType.Sas9 ? 'SAS9' : 'SAS Viya'
} servers.`
)
}
}

View File

@@ -1,82 +0,0 @@
import { FolderMember, ServiceMember, ExecutionQuery } from './types'
import { RequestClient } from './request/RequestClient'
import { getAccessTokenForSasjs } from './auth/getAccessTokenForSasjs'
import { refreshTokensForSasjs } from './auth/refreshTokensForSasjs'
import { getAuthCodeForSasjs } from './auth/getAuthCodeForSasjs'
export class SASjsApiClient {
constructor(
private serverUrl: string,
private requestClient: RequestClient
) {}
public setConfig(serverUrl: string) {
if (serverUrl) this.serverUrl = serverUrl
}
public async deploy(members: [FolderMember, ServiceMember], appLoc: string) {
const { result } = await this.requestClient.post<{
status: string
message: string
example?: {}
}>(
'SASjsApi/drive/deploy',
{ fileTree: members, appLoc: appLoc },
undefined
)
return Promise.resolve(result)
}
public async executeJob(query: ExecutionQuery) {
const { result } = await this.requestClient.post<{
status: string
message: string
log?: string
logPath?: string
error?: {}
}>('SASjsApi/stp/execute', query, undefined)
return Promise.resolve(result)
}
/**
* Exchanges the auth code for an access token for the given client.
* @param clientId - the client ID to authenticate with.
* @param authCode - the auth code received from the server.
*/
public async getAccessToken(
clientId: string,
authCode: string
): Promise<SASjsAuthResponse> {
return getAccessTokenForSasjs(this.requestClient, clientId, authCode)
}
/**
* Exchanges the refresh token for an access token.
* @param refreshToken - the refresh token received from the server.
*/
public async refreshTokens(refreshToken: string): Promise<SASjsAuthResponse> {
return refreshTokensForSasjs(this.requestClient, refreshToken)
}
/**
* Performs a login authenticate and returns an auth code for the given client.
* @param username - a string representing the username.
* @param password - a string representing the password.
* @param clientId - the client ID to authenticate with.
*/
public async getAuthCode(
username: string,
password: string,
clientId: string
) {
return getAuthCodeForSasjs(this.requestClient, username, password, clientId)
}
}
// todo move to sasjs/utils
export interface SASjsAuthResponse {
access_token: string
refresh_token: string
}

View File

@@ -168,7 +168,7 @@ export class SessionManager {
) {
if (stateLink) {
if (this.debug && !this.printedSessionState.printed) {
logger.info(`Polling: ${this.serverUrl + stateLink.href}`)
logger.info('Polling session status...')
this.printedSessionState.printed = true
}

View File

@@ -239,7 +239,15 @@ export async function executeScript(
const resultLink = `/compute/sessions/${executionSessionId}/filerefs/_webout/content`
jobResult = await requestClient
.get<any>(resultLink, access_token, 'text/plain')
.get<any>(
resultLink,
access_token,
'text/plain',
{},
debug,
true,
jobPath
)
.catch(async (e) => {
if (e instanceof NotFoundError) {
if (logLink) {

View File

@@ -4,7 +4,7 @@ import { getTokens } from '../../auth/getTokens'
import { RequestClient } from '../../request/RequestClient'
import { JobStatePollError } from '../../types/errors'
import { Link, WriteStream } from '../../types'
import { delay, isNode } from '../../utils'
import { isNode } from '../../utils'
export async function pollJobState(
requestClient: RequestClient,
@@ -206,11 +206,10 @@ const doPoll = async (
pollCount++
const jobHref = postedJob.links.find((l: Link) => l.rel === 'self')!.href
if (pollOptions?.streamLog) {
const jobUrl = postedJob.links.find((l: Link) => l.rel === 'self')
const { result: job } = await requestClient.get<Job>(
jobHref,
jobUrl!.href,
authConfig?.access_token
)
@@ -232,7 +231,7 @@ const doPoll = async (
}
if (debug && printedState !== state) {
logger.info(`Polling: ${requestClient.getBaseUrl() + jobHref}/state`)
logger.info('Polling job status...')
logger.info(`Current job state: ${state}`)
printedState = state
@@ -247,3 +246,5 @@ const doPoll = async (
return { state, pollCount }
}
const delay = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms))

View File

@@ -9,10 +9,7 @@ import * as isNodeModule from '../../../utils/isNode'
import { PollOptions } from '../../../types'
import { WriteStream } from 'fs'
const baseUrl = 'http://localhost'
const requestClient = new (<jest.Mock<RequestClient>>RequestClient)()
requestClient['httpClient'].defaults.baseURL = baseUrl
const defaultPollOptions: PollOptions = {
maxPollCount: 100,
pollInterval: 500,
@@ -198,7 +195,7 @@ describe('pollJobState', () => {
expect((process as any).logger.info).toHaveBeenCalledTimes(4)
expect((process as any).logger.info).toHaveBeenNthCalledWith(
1,
`Polling: ${baseUrl}/job/state`
'Polling job status...'
)
expect((process as any).logger.info).toHaveBeenNthCalledWith(
2,
@@ -206,7 +203,7 @@ describe('pollJobState', () => {
)
expect((process as any).logger.info).toHaveBeenNthCalledWith(
3,
`Polling: ${baseUrl}/job/state`
'Polling job status...'
)
expect((process as any).logger.info).toHaveBeenNthCalledWith(
4,

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