1
0
mirror of https://github.com/sasjs/adapter.git synced 2026-01-03 18:50:05 +00:00
Files
adapter/src/SASjs.ts
2020-09-16 11:34:59 +03:00

1361 lines
38 KiB
TypeScript

import { isIEorEdgeOrOldFirefox } from './utils/isIeOrEdge'
import * as e6p from 'es6-promise'
;(e6p as any).polyfill()
if (isIEorEdgeOrOldFirefox()) {
if (window) {
window.fetch = undefined as any // ensure the polyfill runs
}
}
// tslint:disable-next-line
require('isomorphic-fetch')
import {
convertToCSV,
compareTimestamps,
serialize,
isAuthorizeFormRequired,
parseAndSubmitAuthorizeForm,
splitChunks,
isLogInRequired,
isLogInSuccess,
parseSourceCode,
parseGeneratedCode,
parseWeboutResponse,
needsRetry,
asyncForEach
} from './utils'
import {
SASjsConfig,
SASjsRequest,
SASjsWaitingRequest,
ServerType,
CsrfToken,
UploadFile,
EditContextInput
} from './types'
import { SASViyaApiClient } from './SASViyaApiClient'
import { SAS9ApiClient } from './SAS9ApiClient'
import { FileUploader } from './FileUploader'
const defaultConfig: SASjsConfig = {
serverUrl: '',
pathSAS9: '/SASStoredProcess/do',
pathSASViya: '/SASJobExecution',
appLoc: '/Public/seedapp',
serverType: ServerType.SASViya,
debug: true,
contextName: 'SAS Job Execution compute context',
useComputeApi: false
}
const requestRetryLimit = 5
/**
* SASjs is a JavaScript adapter for SAS.
*
*/
export default class SASjs {
private sasjsConfig: SASjsConfig = new SASjsConfig()
private jobsPath: string = ''
private logoutUrl: string = ''
private loginUrl: string = ''
private csrfTokenApi: CsrfToken | null = null
private csrfTokenWeb: CsrfToken | null = null
private retryCountWeb: number = 0
private retryCountComputeApi: number = 0
private retryCountJeseApi: number = 0
private sasjsRequests: SASjsRequest[] = []
private sasjsWaitingRequests: SASjsWaitingRequest[] = []
private userName: string = ''
private sasViyaApiClient: SASViyaApiClient | null = null
private sas9ApiClient: SAS9ApiClient | null = null
private fileUploader: FileUploader | null = null
constructor(config?: any) {
this.sasjsConfig = {
...defaultConfig,
...config
}
this.setupConfiguration()
}
public async executeScriptSAS9(
linesOfCode: string[],
serverName: string,
repositoryName: string
) {
this.isMethodSupported('executeScriptSAS9', ServerType.SAS9)
return await this.sas9ApiClient?.executeScript(
linesOfCode,
serverName,
repositoryName
)
}
public async getAllContexts(accessToken: string) {
this.isMethodSupported('getAllContexts', ServerType.SASViya)
return await this.sasViyaApiClient!.getAllContexts(accessToken)
}
public async getExecutableContexts(accessToken: string) {
this.isMethodSupported('getExecutableContexts', ServerType.SASViya)
return await this.sasViyaApiClient!.getExecutableContexts(accessToken)
}
/**
* Creates a compute context on the given server.
* @param contextName - the name of the context to be created.
* @param launchContextName - the name of the launcher context used by the compute service.
* @param sharedAccountId - the ID of the account to run the servers for this context as.
* @param autoExecLines - the lines of code to execute during session initialization.
* @param authorizedUsers - an optional list of authorized user IDs.
* @param accessToken - an access token for an authorized user.
*/
public async createContext(
contextName: string,
launchContextName: string,
sharedAccountId: string,
autoExecLines: string[],
authorizedUsers: string[],
accessToken: string
) {
this.isMethodSupported('createContext', ServerType.SASViya)
return await this.sasViyaApiClient!.createContext(
contextName,
launchContextName,
sharedAccountId,
autoExecLines,
authorizedUsers,
accessToken
)
}
/**
* Updates a compute context on the given server.
* @param contextName - the original name of the context to be deleted.
* @param editedContext - an object with the properties to be updated.
* @param accessToken - an access token for an authorized user.
*/
public async editContext(
contextName: string,
editedContext: EditContextInput,
accessToken?: string
) {
this.isMethodSupported('editContext', ServerType.SASViya)
return await this.sasViyaApiClient!.editContext(
contextName,
editedContext,
accessToken
)
}
/**
* Deletes a compute context on the given server.
* @param contextName - the name of the context to be deleted.
* @param accessToken - an access token for an authorized user.
*/
public async deleteContext(contextName: string, accessToken?: string) {
this.isMethodSupported('deleteContext', ServerType.SASViya)
return await this.sasViyaApiClient!.deleteContext(contextName, accessToken)
}
public async createSession(contextName: string, accessToken: string) {
this.isMethodSupported('createSession', ServerType.SASViya)
return await this.sasViyaApiClient!.createSession(contextName, accessToken)
}
public async executeScriptSASViya(
fileName: string,
linesOfCode: string[],
contextName: string,
accessToken?: string,
sessionId = '',
silent = false
) {
this.isMethodSupported('executeScriptSASViya', ServerType.SASViya)
return await this.sasViyaApiClient!.executeScript(
fileName,
linesOfCode,
contextName,
accessToken,
silent,
null,
this.sasjsConfig.debug
)
}
/**
* 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.
* @param accessToken - the access token to authorizing the request.
* @param sasApiClient - a client for interfacing with SAS API.
* @param isForced - flag that indicates if target folder already exists, it and all subfolders have to be deleted. Applicable for SAS VIYA only.
*/
public async createFolder(
folderName: string,
parentFolderPath: string,
parentFolderUri?: string,
accessToken?: string,
sasApiClient?: SASViyaApiClient,
isForced?: boolean
) {
this.isMethodSupported('createFolder', ServerType.SASViya)
if (sasApiClient)
return await sasApiClient.createFolder(
folderName,
parentFolderPath,
parentFolderUri,
accessToken
)
return await this.sasViyaApiClient!.createFolder(
folderName,
parentFolderPath,
parentFolderUri,
accessToken,
isForced
)
}
public async createJobDefinition(
jobName: string,
code: string,
parentFolderPath?: string,
parentFolderUri?: string,
accessToken?: string,
sasApiClient?: SASViyaApiClient
) {
this.isMethodSupported('createJobDefinition', ServerType.SASViya)
if (sasApiClient)
return await sasApiClient!.createJobDefinition(
jobName,
code,
parentFolderPath,
parentFolderUri,
accessToken
)
return await this.sasViyaApiClient!.createJobDefinition(
jobName,
code,
parentFolderPath,
parentFolderUri,
accessToken
)
}
public async getAuthCode(clientId: string) {
this.isMethodSupported('getAuthCode', ServerType.SASViya)
return await this.sasViyaApiClient!.getAuthCode(clientId)
}
public async getAccessToken(
clientId: string,
clientSecret: string,
authCode: string
) {
this.isMethodSupported('getAccessToken', ServerType.SASViya)
return await this.sasViyaApiClient!.getAccessToken(
clientId,
clientSecret,
authCode
)
}
public async refreshTokens(
clientId: string,
clientSecret: string,
refreshToken: string
) {
this.isMethodSupported('refreshTokens', ServerType.SASViya)
return await this.sasViyaApiClient!.refreshTokens(
clientId,
clientSecret,
refreshToken
)
}
public async deleteClient(clientId: string, accessToken: string) {
this.isMethodSupported('deleteClient', ServerType.SASViya)
return await this.sasViyaApiClient!.deleteClient(clientId, accessToken)
}
/**
* Returns the current SASjs configuration.
*
*/
public getSasjsConfig() {
return this.sasjsConfig
}
/**
* Returns the username of the user currently logged in.
*
*/
public getUserName() {
return this.userName
}
/**
* Returns the _csrf token of the current session for the API approach.
*
*/
public getCsrfApi() {
return this.csrfTokenApi?.value
}
/**
* Returns the _csrf token of the current session for the WEB approach.
*
*/
public getCsrfWeb() {
return this.csrfTokenWeb?.value
}
/**
* Sets the SASjs configuration.
* @param config - SASjs configuration.
*/
public async setSASjsConfig(config: SASjsConfig) {
this.sasjsConfig = {
...this.sasjsConfig,
...config
}
await this.setupConfiguration()
}
/**
* Sets the debug state. Turning this on will enable additional logging in the adapter.
* @param value - boolean indicating debug state (on/off).
*/
public setDebugState(value: boolean) {
this.sasjsConfig.debug = value
}
/**
* Checks whether a session is active, or login is required.
* @returns - a promise which resolves with an object containing two values - a boolean `isLoggedIn`, and a string `userName`.
*/
public async checkSession() {
const loginResponse = await fetch(this.loginUrl.replace('.do', ''))
const responseText = await loginResponse.text()
const isLoggedIn = /<button.+onClick.+logout/gm.test(responseText)
return Promise.resolve({
isLoggedIn,
userName: this.userName
})
}
/**
* Logs into the SAS server with the supplied credentials.
* @param username - a string representing the username.
* @param password - a string representing the password.
*/
public async logIn(username: string, password: string) {
const loginParams: any = {
_service: 'default',
username,
password
}
this.userName = loginParams.username
const { isLoggedIn } = await this.checkSession()
if (isLoggedIn) {
this.resendWaitingRequests()
return Promise.resolve({
isLoggedIn,
userName: this.userName
})
}
const loginForm = await this.getLoginForm()
for (const key in loginForm) {
loginParams[key] = loginForm[key]
}
const loginParamsStr = serialize(loginParams)
return fetch(this.loginUrl, {
method: 'post',
credentials: 'include',
referrerPolicy: 'same-origin',
body: loginParamsStr,
headers: new Headers({
'Content-Type': 'application/x-www-form-urlencoded'
})
})
.then((response) => response.text())
.then(async (responseText) => {
let authFormRes: any
let loggedIn
if (isAuthorizeFormRequired(responseText)) {
authFormRes = await parseAndSubmitAuthorizeForm(
responseText,
this.sasjsConfig.serverUrl
)
} else {
loggedIn = isLogInSuccess(responseText)
}
if (!loggedIn) {
const currentSession = await this.checkSession()
loggedIn = currentSession.isLoggedIn
}
if (loggedIn) {
this.resendWaitingRequests()
}
return {
isLoggedIn: loggedIn,
userName: this.userName
}
})
.catch((e) => Promise.reject(e))
}
/**
* Logs out of the configured SAS server.
*/
public logOut() {
return new Promise((resolve, reject) => {
const logOutURL = `${this.sasjsConfig.serverUrl}${this.logoutUrl}`
fetch(logOutURL)
.then(() => {
resolve(true)
})
.catch((err: Error) => reject(err))
})
}
/**
* Uploads a file to the given service.
* @param sasJob - the path to the SAS program (ultimately resolves to
* the SAS `_program` parameter to run a Job Definition or SAS 9 Stored
* Process). Is prepended at runtime with the value of `appLoc`.
* @param files - array of files to be uploaded, including File object and file name.
* @param params - request URL parameters.
*/
public uploadFile(sasJob: string, files: UploadFile[], params: any) {
const fileUploader =
this.fileUploader ||
new FileUploader(
this.sasjsConfig.appLoc,
this.sasjsConfig.serverUrl,
this.jobsPath,
this.setCsrfTokenWeb,
this.csrfTokenWeb
)
return fileUploader.uploadFile(sasJob, files, params)
}
/**
* Makes a request to the SAS Service specified in `SASjob`. The response
* object will always contain table names in lowercase, and column names in
* uppercase. Values are returned formatted by default, unformatted
* values can be configured as an option in the `%webout` macro.
*
* @param sasJob - the path to the SAS program (ultimately resolves to
* the SAS `_program` parameter to run a Job Definition or SAS 9 Stored
* Process). Is prepended at runtime with the value of `appLoc`.
* @param data - a JSON object containing one or more tables to be sent to
* SAS. Can be `null` if no inputs required.
* @param config - provide any changes to the config here, for instance to
* enable/disable `debug`. Any change provided will override the global config,
* for that particular function call.
* @param loginRequiredCallback - provide a function here to be called if the
* user is not logged in (eg to display a login form). The request will be
* resubmitted after logon.
*/
public async request(
sasJob: string,
data: any,
config: any = {},
loginRequiredCallback?: any,
accessToken?: string
) {
let requestResponse
config = {
...this.sasjsConfig,
...config
}
sasJob = sasJob.startsWith('/') ? sasJob.replace('/', '') : sasJob
if (config.serverType === ServerType.SASViya && config.contextName) {
if (config.useComputeApi) {
requestResponse = await this.executeJobViaComputeApi(
sasJob,
data,
config,
loginRequiredCallback,
accessToken
)
this.retryCountComputeApi = 0
} else {
requestResponse = await this.executeJobViaJesApi(
sasJob,
data,
config,
loginRequiredCallback,
accessToken
)
this.retryCountJeseApi = 0
}
} else {
requestResponse = await this.executeJobViaWeb(
sasJob,
data,
config,
loginRequiredCallback
)
}
return requestResponse
}
/**
* Creates the folders and services at the given location `appLoc` on the given server `serverUrl`.
* @param serviceJson - the JSON specifying the folders and services to be created.
* @param appLoc - the base folder in which to create the new folders and
* services. If not provided, is taken from SASjsConfig.
* @param serverUrl - the server on which to deploy the folders and services.
* If not provided, is taken from SASjsConfig.
* @param accessToken - an optional access token to be passed in when
* using this function from the command line.
* @param isForced - flag that indicates if target folder already exists, it and all subfolders have to be deleted.
*/
public async deployServicePack(
serviceJson: any,
appLoc?: string,
serverUrl?: string,
accessToken?: string,
isForced = false
) {
this.isMethodSupported('deployServicePack', ServerType.SASViya)
let sasApiClient: any = null
if (serverUrl || appLoc) {
if (!serverUrl) {
serverUrl = this.sasjsConfig.serverUrl
}
if (!appLoc) {
appLoc = this.sasjsConfig.appLoc
}
if (this.sasjsConfig.serverType === ServerType.SASViya) {
sasApiClient = new SASViyaApiClient(
serverUrl,
appLoc,
this.sasjsConfig.contextName,
this.setCsrfTokenApi
)
} else if (this.sasjsConfig.serverType === ServerType.SAS9) {
sasApiClient = new SAS9ApiClient(serverUrl)
}
} else {
let sasClientConfig: any = null
if (this.sasjsConfig.serverType === ServerType.SASViya) {
sasClientConfig = this.sasViyaApiClient!.getConfig()
} else if (this.sasjsConfig.serverType === ServerType.SAS9) {
sasClientConfig = this.sas9ApiClient!.getConfig()
}
serverUrl = sasClientConfig.serverUrl
appLoc = sasClientConfig.rootFolderName as string
}
// members of type 'folder' should be processed first
if (serviceJson.members[0].members) {
serviceJson.members[0].members.sort((member: { type: string }) =>
member.type === 'folder' ? -1 : 1
)
}
const members =
serviceJson.members[0].name === 'services'
? serviceJson.members[0].members
: serviceJson.members
await this.createFoldersAndServices(
appLoc,
members,
accessToken,
sasApiClient,
isForced
)
}
private async executeJobViaComputeApi(
sasJob: string,
data: any,
config: any,
loginRequiredCallback?: any,
accessToken?: string
) {
const sasjsWaitingRequest: SASjsWaitingRequest = {
requestPromise: {
promise: null,
resolve: null,
reject: null
},
SASjob: sasJob,
data
}
sasjsWaitingRequest.requestPromise.promise = new Promise(
async (resolve, reject) => {
this.sasViyaApiClient
?.executeComputeJob(
sasJob,
config.contextName,
config.debug,
data,
accessToken
)
.then((response) => {
if (!config.debug) {
this.appendSasjsRequest(null, sasJob, null)
} else {
this.appendSasjsRequest(response, sasJob, null)
}
let responseJson
try {
responseJson = JSON.parse(response!.result)
} catch {
responseJson = JSON.parse(parseWeboutResponse(response!.result))
}
resolve(responseJson)
})
.catch(async (response) => {
let error = response.error || response
if (needsRetry(JSON.stringify(error))) {
if (this.retryCountComputeApi < requestRetryLimit) {
let retryResponse = await this.executeJobViaComputeApi(
sasJob,
data,
config,
loginRequiredCallback,
accessToken
)
this.retryCountComputeApi++
resolve(retryResponse)
} else {
this.retryCountComputeApi = 0
reject({ MESSAGE: 'Compute API retry requests limit reached.' })
}
}
if (error && error.status === 401) {
if (loginRequiredCallback) loginRequiredCallback(true)
sasjsWaitingRequest.requestPromise.resolve = resolve
sasjsWaitingRequest.requestPromise.reject = reject
sasjsWaitingRequest.config = config
this.sasjsWaitingRequests.push(sasjsWaitingRequest)
} else {
reject({ MESSAGE: error || 'Job execution failed.' })
}
this.appendSasjsRequest(response.log, sasJob, null)
})
}
)
return sasjsWaitingRequest.requestPromise.promise
}
private async executeJobViaJesApi(
sasJob: string,
data: any,
config: any,
loginRequiredCallback?: any,
accessToken?: string
) {
const sasjsWaitingRequest: SASjsWaitingRequest = {
requestPromise: {
promise: null,
resolve: null,
reject: null
},
SASjob: sasJob,
data
}
sasjsWaitingRequest.requestPromise.promise = new Promise(
async (resolve, reject) => {
const session = await this.checkSession()
if (!session.isLoggedIn && !accessToken) {
if (loginRequiredCallback) loginRequiredCallback(true)
sasjsWaitingRequest.requestPromise.resolve = resolve
sasjsWaitingRequest.requestPromise.reject = reject
sasjsWaitingRequest.config = config
this.sasjsWaitingRequests.push(sasjsWaitingRequest)
} else {
resolve(
await this.sasViyaApiClient
?.executeJob(
sasJob,
config.contextName,
config.debug,
data,
accessToken
)
.then((response) => {
if (!config.debug) {
this.appendSasjsRequest(null, sasJob, null)
} else {
this.appendSasjsRequest(response, sasJob, null)
}
let responseJson
try {
responseJson = JSON.parse(response!.result)
} catch {
responseJson = JSON.parse(
parseWeboutResponse(response!.result)
)
}
return responseJson
})
.catch(async (e) => {
if (needsRetry(JSON.stringify(e))) {
if (this.retryCountJeseApi < requestRetryLimit) {
let retryResponse = await this.executeJobViaJesApi(
sasJob,
data,
config,
loginRequiredCallback,
accessToken
)
this.retryCountJeseApi++
resolve(retryResponse)
} else {
this.retryCountJeseApi = 0
reject({ MESSAGE: 'JES API retry requests limit reached' })
}
}
reject({ MESSAGE: (e && e.message) || 'Job execution failed.' })
})
)
}
}
)
return sasjsWaitingRequest.requestPromise.promise
}
private async executeJobViaWeb(
sasJob: string,
data: any,
config: any,
loginRequiredCallback?: any
) {
const sasjsWaitingRequest: SASjsWaitingRequest = {
requestPromise: {
promise: null,
resolve: null,
reject: null
},
SASjob: sasJob,
data
}
const program = config.appLoc
? config.appLoc.replace(/\/?$/, '/') + sasJob.replace(/^\//, '')
: sasJob
const jobUri =
config.serverType === 'SASVIYA' ? await this.getJobUri(sasJob) : ''
const apiUrl = `${config.serverUrl}${this.jobsPath}/?${
jobUri.length > 0
? '__program=' + program + '&_job=' + jobUri
: '_program=' + program
}`
const requestParams = {
...this.getRequestParamsWeb(config)
}
const formData = new FormData()
let isError = false
let errorMsg = ''
if (data) {
const stringifiedData = JSON.stringify(data)
if (
config.serverType === ServerType.SAS9 ||
stringifiedData.length > 500000 ||
stringifiedData.includes(';')
) {
// file upload approach
for (const tableName in data) {
if (isError) {
return
}
const name = tableName
const csv = convertToCSV(data[tableName])
if (csv === 'ERROR: LARGE STRING LENGTH') {
isError = true
errorMsg =
'The max length of a string value in SASjs is 32765 characters.'
}
const file = new Blob([csv], {
type: 'application/csv'
})
formData.append(name, file, `${name}.csv`)
}
} else {
// param based approach
const sasjsTables = []
let tableCounter = 0
for (const tableName in data) {
if (isError) {
return
}
tableCounter++
sasjsTables.push(tableName)
const csv = convertToCSV(data[tableName])
if (csv === 'ERROR: LARGE STRING LENGTH') {
isError = true
errorMsg =
'The max length of a string value in SASjs is 32765 characters.'
}
// if csv has length more then 16k, send in chunks
if (csv.length > 16000) {
const csvChunks = splitChunks(csv)
// append chunks to form data with same key
csvChunks.map((chunk) => {
formData.append(`sasjs${tableCounter}data`, chunk)
})
} else {
requestParams[`sasjs${tableCounter}data`] = csv
}
}
requestParams['sasjs_tables'] = sasjsTables.join(' ')
}
}
for (const key in requestParams) {
if (requestParams.hasOwnProperty(key)) {
formData.append(key, requestParams[key])
}
}
let isRedirected = false
sasjsWaitingRequest.requestPromise.promise = new Promise(
(resolve, reject) => {
if (isError) {
reject({ MESSAGE: errorMsg })
}
const headers: any = {}
if (this.csrfTokenWeb) {
headers[this.csrfTokenWeb.headerName] = this.csrfTokenWeb.value
}
fetch(apiUrl, {
method: 'POST',
body: formData,
referrerPolicy: 'same-origin',
headers
})
.then(async (response) => {
if (!response.ok) {
if (response.status === 403) {
const tokenHeader = response.headers.get('X-CSRF-HEADER')
if (tokenHeader) {
const token = response.headers.get(tokenHeader)
this.csrfTokenWeb = {
headerName: tokenHeader,
value: token || ''
}
}
}
}
if (response.redirected && config.serverType === ServerType.SAS9) {
isRedirected = true
}
return response.text()
})
.then((responseText) => {
if (
(needsRetry(responseText) || isRedirected) &&
!isLogInRequired(responseText)
) {
if (this.retryCountWeb < requestRetryLimit) {
this.retryCountWeb++
this.request(sasJob, data, config, loginRequiredCallback).then(
(res: any) => resolve(res),
(err: any) => reject(err)
)
} else {
this.retryCountWeb = 0
reject(responseText)
}
} else {
this.retryCountWeb = 0
this.parseLogFromResponse(responseText, program)
if (isLogInRequired(responseText)) {
if (loginRequiredCallback) loginRequiredCallback(true)
sasjsWaitingRequest.requestPromise.resolve = resolve
sasjsWaitingRequest.requestPromise.reject = reject
sasjsWaitingRequest.config = config
this.sasjsWaitingRequests.push(sasjsWaitingRequest)
} else {
if (config.serverType === ServerType.SAS9 && config.debug) {
this.updateUsername(responseText)
const jsonResponseText = parseWeboutResponse(responseText)
if (jsonResponseText !== '') {
resolve(JSON.parse(jsonResponseText))
} else {
reject({
MESSAGE: this.parseSAS9ErrorResponse(responseText)
})
}
} else if (
config.serverType === ServerType.SASViya &&
config.debug
) {
try {
this.parseSASVIYADebugResponse(responseText).then(
(resText: any) => {
this.updateUsername(resText)
try {
resolve(JSON.parse(resText))
} catch (e) {
reject({ MESSAGE: resText })
}
},
(err: any) => {
reject({ MESSAGE: err })
}
)
} catch (e) {
reject({ MESSAGE: responseText })
}
} else {
this.updateUsername(responseText)
try {
const parsedJson = JSON.parse(responseText)
resolve(parsedJson)
} catch (e) {
reject({ MESSAGE: responseText })
}
}
}
}
})
.catch((e: Error) => {
reject(e)
})
}
)
return sasjsWaitingRequest.requestPromise.promise
}
private setCsrfTokenWeb = (csrfToken: CsrfToken) => {
this.csrfTokenWeb = csrfToken
}
private setCsrfTokenApi = (csrfToken: CsrfToken) => {
this.csrfTokenApi = csrfToken
}
private async resendWaitingRequests() {
for (const sasjsWaitingRequest of this.sasjsWaitingRequests) {
this.request(sasjsWaitingRequest.SASjob, sasjsWaitingRequest.data).then(
(res: any) => {
sasjsWaitingRequest.requestPromise.resolve(res)
},
(err: any) => {
sasjsWaitingRequest.requestPromise.reject(err)
}
)
}
this.sasjsWaitingRequests = []
}
private getRequestParamsWeb(config: any): any {
const requestParams: any = {}
if (this.csrfTokenWeb) {
requestParams['_csrf'] = this.csrfTokenWeb.value
}
if (config.debug) {
requestParams['_omittextlog'] = 'false'
requestParams['_omitsessionresults'] = 'false'
requestParams['_debug'] = 131
}
return requestParams
}
private updateUsername(response: any) {
try {
const responseJson = JSON.parse(response)
if (this.sasjsConfig.serverType === ServerType.SAS9) {
this.userName = responseJson['_METAUSER']
} else {
this.userName = responseJson['SYSUSERID']
}
} catch (e) {
this.userName = ''
}
}
private parseSASVIYADebugResponse(response: string) {
return new Promise((resolve, reject) => {
const iframeStart = response.split(
'<iframe style="width: 99%; height: 500px" src="'
)[1]
const jsonUrl = iframeStart ? iframeStart.split('"></iframe>')[0] : null
if (jsonUrl) {
fetch(this.sasjsConfig.serverUrl + jsonUrl)
.then((res) => res.text())
.then((resText) => {
resolve(resText)
})
} else {
reject('No debug info found in response.')
}
})
}
private async getJobUri(sasJob: string) {
if (!this.sasViyaApiClient) return ''
const jobMap: any = await this.sasViyaApiClient.getAppLocMap()
let uri = ''
if (jobMap.size) {
const jobKey = sasJob.split('/')[0]
const jobName = sasJob.split('/')[1]
const locJobs = jobMap.get(jobKey)
if (locJobs) {
const job = locJobs.find(
(el: any) => el.name === jobName && el.contentType === 'jobDefinition'
)
if (job) {
uri = job.uri
}
}
}
return uri
}
private parseSAS9ErrorResponse(response: string) {
const logLines = response.split('\n')
const parsedLines: string[] = []
let firstErrorLineIndex: number = -1
logLines.map((line: string, index: number) => {
if (
line.toLowerCase().includes('error') &&
!line.toLowerCase().includes('this request completed with errors.') &&
firstErrorLineIndex === -1
) {
firstErrorLineIndex = index
}
})
for (let i = firstErrorLineIndex - 10; i <= firstErrorLineIndex + 10; i++) {
parsedLines.push(logLines[i])
}
return parsedLines.join(', ')
}
private parseLogFromResponse(response: any, program: string) {
if (this.sasjsConfig.serverType === ServerType.SAS9) {
this.appendSasjsRequest(response, program, null)
} else {
if (!this.sasjsConfig.debug) {
this.appendSasjsRequest(null, program, null)
} else {
this.appendSasjsRequest(response, program, null)
}
}
}
private fetchLogFileContent(logLink: string) {
return new Promise((resolve, reject) => {
fetch(logLink, {
method: 'GET'
})
.then((response: any) => response.text())
.then((response: any) => resolve(response))
.catch((err: Error) => reject(err))
})
}
private async appendSasjsRequest(
response: any,
program: string,
pgmData: any
) {
let sourceCode = ''
let generatedCode = ''
let sasWork = null
if (response && response.result && response.log) {
sourceCode = parseSourceCode(response.log)
generatedCode = parseGeneratedCode(response.log)
if (this.sasjsConfig.debug) {
if (response.log) {
sasWork = response.log
} else {
sasWork = JSON.parse(parseWeboutResponse(response.result)).WORK
}
} else {
sasWork = JSON.parse(response.result).WORK
}
} else {
if (response) {
sourceCode = parseSourceCode(response)
generatedCode = parseGeneratedCode(response)
sasWork = await this.parseSasWork(response)
}
}
this.sasjsRequests.push({
logFile: (response && response.log) || response,
serviceLink: program,
timestamp: new Date(),
sourceCode,
generatedCode,
SASWORK: sasWork
})
if (this.sasjsRequests.length > 20) {
this.sasjsRequests.splice(0, 1)
}
}
private async parseSasWork(response: any) {
if (this.sasjsConfig.debug) {
let jsonResponse
if (this.sasjsConfig.serverType === ServerType.SAS9) {
try {
jsonResponse = JSON.parse(parseWeboutResponse(response))
} catch (e) {
console.error(e)
}
} else {
await this.parseSASVIYADebugResponse(response).then(
(resText: any) => {
try {
jsonResponse = JSON.parse(resText)
} catch (e) {
console.error(e)
}
},
(err: any) => {
console.error(err)
}
)
}
if (jsonResponse) {
return jsonResponse.WORK
}
}
return null
}
public getSasRequests() {
const sortedRequests = this.sasjsRequests.sort(compareTimestamps)
return sortedRequests
}
public clearSasRequests() {
this.sasjsRequests = []
}
private setupConfiguration() {
if (
this.sasjsConfig.serverUrl === undefined ||
this.sasjsConfig.serverUrl === ''
) {
let url = `${location.protocol}//${location.hostname}`
if (location.port) {
url = `${url}:${location.port}`
}
this.sasjsConfig.serverUrl = url
}
if (this.sasjsConfig.serverUrl.slice(-1) === '/') {
this.sasjsConfig.serverUrl = this.sasjsConfig.serverUrl.slice(0, -1)
}
this.jobsPath =
this.sasjsConfig.serverType === ServerType.SASViya
? this.sasjsConfig.pathSASViya
: this.sasjsConfig.pathSAS9
this.loginUrl = `${this.sasjsConfig.serverUrl}/SASLogon/login`
this.logoutUrl =
this.sasjsConfig.serverType === ServerType.SAS9
? '/SASLogon/logout?'
: '/SASLogon/logout.do?'
if (this.sasjsConfig.serverType === ServerType.SASViya) {
if (this.sasViyaApiClient)
this.sasViyaApiClient!.setConfig(
this.sasjsConfig.serverUrl,
this.sasjsConfig.appLoc
)
else
this.sasViyaApiClient = new SASViyaApiClient(
this.sasjsConfig.serverUrl,
this.sasjsConfig.appLoc,
this.sasjsConfig.contextName,
this.setCsrfTokenApi
)
}
if (this.sasjsConfig.serverType === ServerType.SAS9) {
if (this.sas9ApiClient)
this.sas9ApiClient!.setConfig(this.sasjsConfig.serverUrl)
else this.sas9ApiClient = new SAS9ApiClient(this.sasjsConfig.serverUrl)
}
this.fileUploader = new FileUploader(
this.sasjsConfig.appLoc,
this.sasjsConfig.serverUrl,
this.jobsPath,
this.setCsrfTokenWeb
)
}
private setLoginUrl = (matches: RegExpExecArray) => {
let parsedURL = matches[1].replace(/\?.*/, '')
if (parsedURL[0] === '/') {
parsedURL = parsedURL.substr(1)
const tempLoginLink = this.sasjsConfig.serverUrl
? `${this.sasjsConfig.serverUrl}/${parsedURL}`
: `${parsedURL}`
const loginUrl = tempLoginLink
this.loginUrl =
this.sasjsConfig.serverType === ServerType.SASViya
? tempLoginLink
: loginUrl.replace('.do', '')
}
}
private async getLoginForm() {
const pattern: RegExp = /<form.+action="(.*Logon[^"]*).*>/
const response = await fetch(this.loginUrl).then((r) => r.text())
const matches = pattern.exec(response)
const formInputs: any = {}
if (matches && matches.length) {
this.setLoginUrl(matches)
const inputs = response.match(/<input.*"hidden"[^>]*>/g)
if (inputs) {
inputs.forEach((inputStr: string) => {
const valueMatch = inputStr.match(/name="([^"]*)"\svalue="([^"]*)/)
if (valueMatch && valueMatch.length) {
formInputs[valueMatch[1]] = valueMatch[2]
}
})
}
}
return Object.keys(formInputs).length ? formInputs : null
}
private async createFoldersAndServices(
parentFolder: string,
membersJson: any[],
accessToken?: string,
sasApiClient?: SASViyaApiClient,
isForced?: boolean
) {
await asyncForEach(membersJson, async (member: any) => {
switch (member.type) {
case 'folder':
await this.createFolder(
member.name,
parentFolder,
undefined,
accessToken,
sasApiClient,
isForced
)
break
case 'service':
await this.createJobDefinition(
member.name,
member.code,
parentFolder,
undefined,
accessToken,
sasApiClient
)
break
default:
throw new Error(`Unidentified member '${member.name}' provided.`)
}
if (member.type === 'folder' && member.members && member.members.length)
await this.createFoldersAndServices(
`${parentFolder}/${member.name}`,
member.members,
accessToken,
sasApiClient,
isForced
)
})
}
private isMethodSupported(method: string, serverType: string) {
if (this.sasjsConfig.serverType !== serverType) {
throw new Error(
`Method '${method}' is only supported on ${
serverType === ServerType.SAS9 ? 'SAS9' : 'SAS Viya'
} servers.`
)
}
}
}