1
0
mirror of https://github.com/sasjs/adapter.git synced 2026-01-16 00:20:06 +00:00

chore(merge): Merge branch 'master' into service-pack-with-file-resource

This commit is contained in:
Saad Jutt
2021-06-10 16:55:35 +05:00
20 changed files with 2524 additions and 15861 deletions

View File

@@ -1,4 +1,6 @@
import axios, { AxiosInstance } from 'axios'
import { generateTimestamp } from '@sasjs/utils/time'
import * as NodeFormData from 'form-data'
import { Sas9RequestClient } from './request/Sas9RequestClient'
import { isUrl } from './utils'
/**
@@ -6,11 +8,11 @@ import { isUrl } from './utils'
*
*/
export class SAS9ApiClient {
private httpClient: AxiosInstance
private requestClient: Sas9RequestClient
constructor(private serverUrl: string) {
constructor(private serverUrl: string, private jobsPath: string) {
if (serverUrl) isUrl(serverUrl)
this.httpClient = axios.create({ baseURL: this.serverUrl })
this.requestClient = new Sas9RequestClient(serverUrl, false)
}
/**
@@ -33,27 +35,52 @@ export class SAS9ApiClient {
/**
* Executes code on a SAS9 server.
* @param linesOfCode - an array of code lines to execute.
* @param serverName - the server to execute the code on.
* @param repositoryName - the repository to execute the code in.
* @param userName - the user name to log into the current SAS server.
* @param password - the password to log into the current SAS server.
*/
public async executeScript(
linesOfCode: string[],
serverName: string,
repositoryName: string
userName: string,
password: string
) {
const requestPayload = linesOfCode.join('\n')
await this.requestClient.login(userName, password, this.jobsPath)
const executeScriptResponse = await this.httpClient.put(
`/sas/servers/${serverName}/cmd?repositoryName=${repositoryName}`,
`command=${requestPayload}`,
{
headers: {
Accept: 'application/json'
},
responseType: 'text'
}
const formData = generateFileUploadForm(linesOfCode.join('\n'))
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 executeScriptResponse.data
return response.result as string
}
}
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

@@ -29,7 +29,7 @@ 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 { SasAuthResponse, MacroVar } from '@sasjs/utils/types'
import { prefixMessage } from '@sasjs/utils/error'
import * as mime from 'mime'
@@ -273,6 +273,7 @@ export class SASViyaApiClient {
* @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,
@@ -284,7 +285,8 @@ export class SASViyaApiClient {
expectWebout = false,
waitForResult = true,
pollOptions?: PollOptions,
printPid = false
printPid = false,
variables?: MacroVar
): Promise<any> {
try {
const headers: any = {
@@ -358,6 +360,8 @@ export class SASViyaApiClient {
: jobPath
}
if (variables) jobVariables = { ...jobVariables, ...variables }
let files: any[] = []
if (data) {
@@ -768,13 +772,11 @@ 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(
@@ -863,6 +865,7 @@ 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,
@@ -873,7 +876,8 @@ export class SASViyaApiClient {
waitForResult = true,
expectWebout = false,
pollOptions?: PollOptions,
printPid = false
printPid = false,
variables?: MacroVar
) {
if (isRelativePath(sasJob) && !this.rootFolderName) {
throw new Error(
@@ -952,7 +956,8 @@ export class SASViyaApiClient {
expectWebout,
waitForResult,
pollOptions,
printPid
printPid,
variables
)
}

View File

@@ -4,7 +4,7 @@ import { SASViyaApiClient } from './SASViyaApiClient'
import { SAS9ApiClient } from './SAS9ApiClient'
import { FileUploader } from './FileUploader'
import { AuthManager } from './auth'
import { ServerType } from '@sasjs/utils/types'
import { ServerType, MacroVar } from '@sasjs/utils/types'
import { RequestClient } from './request/RequestClient'
import {
JobExecutor,
@@ -59,15 +59,15 @@ export default class SASjs {
public async executeScriptSAS9(
linesOfCode: string[],
serverName: string,
repositoryName: string
userName: string,
password: string
) {
this.isMethodSupported('executeScriptSAS9', ServerType.Sas9)
return await this.sas9ApiClient?.executeScript(
linesOfCode,
serverName,
repositoryName
userName,
password
)
}
@@ -659,7 +659,7 @@ export default class SASjs {
)
sasApiClient.debug = this.sasjsConfig.debug
} else if (this.sasjsConfig.serverType === ServerType.Sas9) {
sasApiClient = new SAS9ApiClient(serverUrl)
sasApiClient = new SAS9ApiClient(serverUrl, this.jobsPath)
}
} else {
let sasClientConfig: any = null
@@ -706,6 +706,7 @@ export default class SASjs {
* @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,
@@ -714,7 +715,8 @@ export default class SASjs {
accessToken?: string,
waitForResult?: boolean,
pollOptions?: PollOptions,
printPid = false
printPid = false,
variables?: MacroVar
) {
config = {
...this.sasjsConfig,
@@ -737,7 +739,8 @@ export default class SASjs {
!!waitForResult,
false,
pollOptions,
printPid
printPid,
variables
)
}
@@ -848,7 +851,11 @@ 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)
else
this.sas9ApiClient = new SAS9ApiClient(
this.sasjsConfig.serverUrl,
this.jobsPath
)
}
this.fileUploader = new FileUploader(

View File

@@ -35,6 +35,7 @@ export class AuthManager {
this.userName = loginParams.username
const { isLoggedIn, loginForm } = await this.checkSession()
if (isLoggedIn) {
await this.loginCallback()
@@ -44,6 +45,35 @@ 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) {
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]
}
@@ -60,21 +90,7 @@ export class AuthManager {
}
)
let loggedIn = isLogInSuccess(loginResponse)
if (!loggedIn) {
const currentSession = await this.checkSession()
loggedIn = currentSession.isLoggedIn
}
if (loggedIn) {
this.loginCallback()
}
return {
isLoggedIn: !!loggedIn,
userName: this.userName
}
return loginResponse
}
/**
@@ -168,5 +184,10 @@ 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 (done) => {
it('should call the auth callback and return when already logged in', async () => {
const authManager = new AuthManager(
serverUrl,
serverType,
@@ -77,10 +77,9 @@ 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 (done) => {
it('should post a login request to the server if not logged in', async () => {
const authManager = new AuthManager(
serverUrl,
serverType,
@@ -121,10 +120,9 @@ describe('AuthManager', () => {
}
)
expect(authCallback).toHaveBeenCalledTimes(1)
done()
})
it('should parse and submit the authorisation form when necessary', async (done) => {
it('should parse and submit the authorisation form when necessary', async () => {
const authManager = new AuthManager(
serverUrl,
serverType,
@@ -160,10 +158,9 @@ describe('AuthManager', () => {
expect(requestClient.authorize).toHaveBeenCalledWith(
mockLoginAuthoriseRequiredResponse
)
done()
})
it('should check and return session information if logged in', async (done) => {
it('should check and return session information if logged in', async () => {
const authManager = new AuthManager(
serverUrl,
serverType,
@@ -189,7 +186,5 @@ describe('AuthManager', () => {
}
}
)
done()
})
})

View File

@@ -10,6 +10,7 @@ import {
} from '../types/errors'
import { parseWeboutResponse } from '../utils/parseWeboutResponse'
import { prefixMessage } from '@sasjs/utils/error'
import { SAS9AuthError } from '../types/errors/SAS9AuthError'
export interface HttpClient {
get<T>(
@@ -470,6 +471,10 @@ 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,3 +1,7 @@
/**
* @jest-environment jsdom
*/
import { FileUploader } from '../FileUploader'
import { UploadFile } from '../types'
import { RequestClient } from '../request/RequestClient'
@@ -35,41 +39,40 @@ describe('FileUploader', () => {
new RequestClient('https://sample.server.com')
)
it('should upload successfully', async (done) => {
it('should upload successfully', async () => {
const sasJob = 'test/upload'
const { files, params } = prepareFilesAndParams()
mockedAxios.post.mockImplementation(() =>
Promise.resolve({ data: sampleResponse })
)
fileUploader.uploadFile(sasJob, files, params).then((res: any) => {
expect(res).toEqual(JSON.parse(sampleResponse))
done()
})
const res = await fileUploader.uploadFile(sasJob, files, params)
expect(res).toEqual(JSON.parse(sampleResponse))
})
it('should an error when no files are provided', async (done) => {
it('should an error when no files are provided', async () => {
const sasJob = 'test/upload'
const files: UploadFile[] = []
const params = { table: 'libtable' }
fileUploader.uploadFile(sasJob, files, params).catch((err: any) => {
expect(err.error.message).toEqual('At least one file must be provided.')
done()
})
const err = await fileUploader
.uploadFile(sasJob, files, params)
.catch((err: any) => err)
expect(err.error.message).toEqual('At least one file must be provided.')
})
it('should throw an error when no sasJob is provided', async (done) => {
it('should throw an error when no sasJob is provided', async () => {
const sasJob = ''
const { files, params } = prepareFilesAndParams()
fileUploader.uploadFile(sasJob, files, params).catch((err: any) => {
expect(err.error.message).toEqual('sasJob must be provided.')
done()
})
const err = await fileUploader
.uploadFile(sasJob, files, params)
.catch((err: any) => err)
expect(err.error.message).toEqual('sasJob must be provided.')
})
it('should throw an error when login is required', async (done) => {
it('should throw an error when login is required', async () => {
mockedAxios.post.mockImplementation(() =>
Promise.resolve({ data: '<form action="Logon">' })
)
@@ -77,15 +80,13 @@ describe('FileUploader', () => {
const sasJob = 'test'
const { files, params } = prepareFilesAndParams()
fileUploader.uploadFile(sasJob, files, params).catch((err: any) => {
expect(err.error.message).toEqual(
'You must be logged in to upload a file.'
)
done()
})
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.')
})
it('should throw an error when invalid JSON is returned by the server', async (done) => {
it('should throw an error when invalid JSON is returned by the server', async () => {
mockedAxios.post.mockImplementation(() =>
Promise.resolve({ data: '{invalid: "json"' })
)
@@ -93,13 +94,13 @@ describe('FileUploader', () => {
const sasJob = 'test'
const { files, params } = prepareFilesAndParams()
fileUploader.uploadFile(sasJob, files, params).catch((err: any) => {
expect(err.error.message).toEqual('File upload request failed.')
done()
})
const err = await fileUploader
.uploadFile(sasJob, files, params)
.catch((err: any) => err)
expect(err.error.message).toEqual('File upload request failed.')
})
it('should throw an error when the server request fails', async (done) => {
it('should throw an error when the server request fails', async () => {
mockedAxios.post.mockImplementation(() =>
Promise.reject({ data: '{message: "Server error"}' })
)
@@ -107,10 +108,9 @@ describe('FileUploader', () => {
const sasJob = 'test'
const { files, params } = prepareFilesAndParams()
fileUploader.uploadFile(sasJob, files, params).catch((err: any) => {
expect(err.error.message).toEqual('File upload request failed.')
done()
})
const err = await fileUploader
.uploadFile(sasJob, files, params)
.catch((err: any) => err)
expect(err.error.message).toEqual('File upload request failed.')
})
})

View File

@@ -14,7 +14,7 @@ describe('FolderOperations', () => {
beforeEach(() => {})
it('should move and rename folder', async (done) => {
it('should move and rename folder', async () => {
mockFetchResponse(false)
let res: any = await sasViyaApiClient.moveFolder(
@@ -26,11 +26,9 @@ 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 (done) => {
it('should move and keep the name of folder', async () => {
mockFetchResponse(true)
let res: any = await sasViyaApiClient.moveFolder(
@@ -42,11 +40,9 @@ describe('FolderOperations', () => {
expect(res.folder.name).toEqual('oldName')
expect(res.folder.parentFolderUri.split('=')[1]).toEqual('/Test/toFolder')
done()
})
it('should only rename folder', async (done) => {
it('should only rename folder', async () => {
mockFetchResponse(false)
let res: any = await sasViyaApiClient.moveFolder(
@@ -58,8 +54,6 @@ 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', async (done) => {
it('should parse generated code', () => {
expect(sampleResponse).toBeTruthy()
const parsedGeneratedCode = parseGeneratedCode(sampleResponse)
@@ -15,8 +15,6 @@ it('should parse generated code', async (done) => {
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 (done) => {
it('should parse SAS9 source code', async () => {
expect(sampleResponse).toBeTruthy()
const parsedSourceCode = parseSourceCode(sampleResponse)
@@ -15,8 +15,6 @@ it('should parse SAS9 source code', async (done) => {
expect(sourceCodeLines[2].startsWith('8')).toBeTruthy()
expect(sourceCodeLines[3].startsWith('9')).toBeTruthy()
expect(sourceCodeLines[4].startsWith('10')).toBeTruthy()
done()
})
/* tslint:disable */

View File

@@ -0,0 +1,9 @@
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)
}
}