704 lines
21 KiB
TypeScript
704 lines
21 KiB
TypeScript
import { Injectable, EventEmitter } from '@angular/core'
|
|
|
|
import SASjs, { UploadFile } from '@sasjs/adapter'
|
|
import { BehaviorSubject } from 'rxjs'
|
|
import { UserService } from '../shared/user.service'
|
|
|
|
import { Router } from '@angular/router'
|
|
import { EventService } from './event.service'
|
|
import { SasjsService } from './sasjs.service'
|
|
import { SASjsApiDriveFolderContents } from '../models/sasjs-api/SASjsApiDriveFolderContents.model'
|
|
import { ServerType } from '@sasjs/utils/types/serverType'
|
|
import { DcAdapterSettings } from '../models/DcAdapterSettings'
|
|
import { AppStoreService } from './app-store.service'
|
|
import { LoggerService } from './logger.service'
|
|
import { RequestWrapperOptions } from '../models/request-wrapper/RequestWrapperOptions'
|
|
import { ErrorBody } from '../models/ErrorBody'
|
|
import { UploadFileResponse } from '../models/UploadFile'
|
|
import { RequestWrapperResponse } from '../models/request-wrapper/RequestWrapperResponse'
|
|
import { SasViyaService } from './sas-viya.service'
|
|
import { ViyaApiFolder } from '../viya-api-explorer/models/viya-api-folder.model'
|
|
import { ViyaApiFolderMembers } from '../viya-api-explorer/models/viya-api-folder-content.model'
|
|
|
|
@Injectable({
|
|
providedIn: 'root'
|
|
})
|
|
export class SasService {
|
|
public loadStartupServiceEmitter: EventEmitter<any> = new EventEmitter()
|
|
public incorrectSiteIdEmitter: EventEmitter<string> = new EventEmitter()
|
|
public requestSiteIdEmitter: EventEmitter<string> = new EventEmitter()
|
|
|
|
private sasjsAdapter: SASjs = new SASjs()
|
|
private dcAdapterSettings: DcAdapterSettings | undefined
|
|
public serverType: any
|
|
private appLocCheckPending: boolean = false
|
|
|
|
public shouldLogin = new BehaviorSubject(false)
|
|
private license_site_id = new BehaviorSubject<string[] | null>(null)
|
|
|
|
constructor(
|
|
private appStoreService: AppStoreService,
|
|
private userService: UserService,
|
|
private eventService: EventService,
|
|
private sasjsService: SasjsService,
|
|
private sasViyaService: SasViyaService,
|
|
private loggerService: LoggerService,
|
|
private router: Router
|
|
) {}
|
|
|
|
/**
|
|
* Same as `setup` function in the sasjs.service, this is the constructor replacement.
|
|
* This function is being called by `app.service`.
|
|
* Because of timing and dependency issues
|
|
*/
|
|
public sasServiceInit() {
|
|
this.dcAdapterSettings = this.appStoreService.getDcAdapterSettings()
|
|
|
|
this.sasjsService.setup()
|
|
this.sasViyaService.setup()
|
|
|
|
if (!this.dcAdapterSettings) {
|
|
this.eventService.showInfoModal(
|
|
'Error',
|
|
'Adapter settings (index.html) are not present.'
|
|
)
|
|
return
|
|
}
|
|
|
|
this.sasjsAdapter = new SASjs(this.dcAdapterSettings)
|
|
|
|
switch (this.dcAdapterSettings.serverType) {
|
|
case ServerType.SasViya: {
|
|
this.checkViyaDeploy(this.dcAdapterSettings.appLoc || '')
|
|
break
|
|
}
|
|
case ServerType.Sas9: {
|
|
this.loadStartupServiceEmitter.emit()
|
|
break
|
|
}
|
|
case ServerType.Sasjs: {
|
|
this.checkSasjsDeploy()
|
|
break
|
|
}
|
|
}
|
|
|
|
if (this.getSasjsConfig().loginMechanism === 'Redirected') {
|
|
this.shouldLogin.subscribe((shouldLogin) => {
|
|
if (shouldLogin) {
|
|
this.sasjsAdapter.logIn().then((res) => {
|
|
console.log('res', res)
|
|
})
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Runing a backend request against a service.
|
|
* Function also handles the displaying of success or error modals.
|
|
*
|
|
* @param url service to run reuqest against
|
|
* @param data to be sent to backend service
|
|
* @param config additional parameters to force eg. { debug: false }
|
|
* @param wrapperOptions used to provide options to the request wrapper function
|
|
* for example to suppress error or success abort modals after request is finished
|
|
* @returns adapter response or an error. It will return the `log` as well.
|
|
* The log could be potentially be wrong if multiple requests happen because the log this
|
|
* function return is the last request in the Adapter Array for the given URL.
|
|
*/
|
|
public request<responseType = any>(
|
|
url: string,
|
|
data: any,
|
|
config?: any,
|
|
wrapperOptions?: RequestWrapperOptions
|
|
): Promise<RequestWrapperResponse<responseType>> {
|
|
url = 'services/' + url
|
|
|
|
if (!wrapperOptions) wrapperOptions = {}
|
|
|
|
// If debug is on it will print what is going inside the adapter
|
|
this.loggerService.logRequestData(url, data)
|
|
|
|
return new Promise((resolve, reject) => {
|
|
this.sasjsAdapter
|
|
.request(url, data, config, () => {
|
|
this.shouldLogin.next(true)
|
|
})
|
|
.then(
|
|
(res: any) => {
|
|
const sasRequest = this.sasjsAdapter
|
|
.getSasRequests()
|
|
.find((rq) => rq.serviceLink === url)
|
|
|
|
if (res.login === false) {
|
|
this.shouldLogin.next(true)
|
|
reject({
|
|
adapterResponse: false,
|
|
log: sasRequest?.logFile
|
|
})
|
|
}
|
|
|
|
if (!this.userService.user && res.MF_GETUSER) {
|
|
this.userService.user = {
|
|
username: res.MF_GETUSER
|
|
}
|
|
}
|
|
|
|
if (res.SYSSITE) {
|
|
this.requestSiteIdEmitter.emit(res.SYSSITE)
|
|
|
|
const licenseSiteId = this.getLicenseSiteId()
|
|
|
|
if (licenseSiteId.length > 0) {
|
|
if (!this.getLicenseSiteId().includes(res.SYSSITE)) {
|
|
this.incorrectSiteIdEmitter.emit(res.SYSSITE)
|
|
}
|
|
}
|
|
}
|
|
|
|
if (res.status === 404) {
|
|
reject({
|
|
adapterResponse: {
|
|
MESSAGE: res.body || 'SAS Responded with error'
|
|
},
|
|
log: sasRequest?.logFile
|
|
})
|
|
}
|
|
|
|
if (typeof res.sasjsAbort !== 'undefined') {
|
|
const abortRes = res
|
|
const abortMsg = abortRes.sasjsAbort[0].MSG
|
|
const macMsg = abortRes.sasjsAbort[0].MAC
|
|
|
|
if (
|
|
abortMsg.includes(
|
|
'Data_Controller_Settings(StoredProcess) not found'
|
|
)
|
|
) {
|
|
this.eventService.startupDataLoaded()
|
|
this.router.navigateByUrl('/deploy')
|
|
|
|
reject({
|
|
adapterResponse: {
|
|
error: abortMsg
|
|
},
|
|
log: sasRequest?.logFile
|
|
})
|
|
|
|
return
|
|
}
|
|
|
|
if (!wrapperOptions?.suppressSuccessAbortModal) {
|
|
this.eventService.showAbortModal(
|
|
url.replace('services/', ''),
|
|
abortMsg,
|
|
{
|
|
SYSWARNINGTEXT: abortRes.SYSWARNINGTEXT,
|
|
SYSERRORTEXT: abortRes.SYSERRORTEXT,
|
|
MAC: macMsg
|
|
}
|
|
)
|
|
}
|
|
|
|
reject({
|
|
adapterResponse: {
|
|
error: abortMsg
|
|
},
|
|
log: sasRequest?.logFile
|
|
})
|
|
}
|
|
|
|
resolve({
|
|
adapterResponse: res,
|
|
log: sasRequest?.logFile
|
|
})
|
|
},
|
|
(err: { error: ErrorBody | undefined }) => {
|
|
console.error(err)
|
|
|
|
const sasRequest = this.sasjsAdapter
|
|
.getSasRequests()
|
|
.find((rq) => rq.serviceLink === url)
|
|
|
|
if (err.error) {
|
|
let errorMessage: string | undefined = err.error.message
|
|
let log: string | undefined
|
|
|
|
if (err.error.details && err.error.details.log) {
|
|
log = err.error.details.log
|
|
}
|
|
|
|
// If not a single useful info is returned from adapter
|
|
// We display that it's `unknown` SAS service error
|
|
if (!errorMessage || errorMessage.trim().length < 1) {
|
|
errorMessage = 'SAS Service error ocurred'
|
|
}
|
|
|
|
// Otherwise we display error message from adapter
|
|
if (!wrapperOptions?.suppressErrorAbortModal) {
|
|
this.eventService.showAbortModal(
|
|
url,
|
|
errorMessage,
|
|
{ LOG: log },
|
|
'Request error'
|
|
)
|
|
}
|
|
reject({
|
|
adapterResponse: {
|
|
error: errorMessage
|
|
},
|
|
log: sasRequest?.logFile
|
|
})
|
|
}
|
|
|
|
reject({
|
|
adapterResponse: err,
|
|
log: sasRequest?.logFile
|
|
})
|
|
}
|
|
)
|
|
})
|
|
}
|
|
|
|
/**
|
|
* Uploads a file to the backend, using the adapter upload function.
|
|
*
|
|
* @param sasService Service to which the file will be sent
|
|
* @param files Files to be sent
|
|
* @param params Aditional parameters eg. { debug: false }
|
|
* @returns HTTP Response
|
|
*/
|
|
public uploadFile(
|
|
sasService: string,
|
|
files: UploadFile[],
|
|
params: any
|
|
): Promise<UploadFileResponse> {
|
|
return new Promise((resolve, reject) => {
|
|
this.sasjsAdapter.uploadFile(sasService, files, params).then(
|
|
(res) => {
|
|
const sasRequest = this.sasjsAdapter
|
|
.getSasRequests()
|
|
.find((rq) => rq.serviceLink === 'services/editors/loadfile')
|
|
|
|
resolve({
|
|
adapterResponse: res,
|
|
log: sasRequest?.logFile
|
|
})
|
|
},
|
|
(err) => {
|
|
const sasRequest = this.sasjsAdapter
|
|
.getSasRequests()
|
|
.find((rq) => rq.serviceLink === 'services/editors/loadfile')
|
|
|
|
reject({
|
|
response: err,
|
|
log: sasRequest?.logFile
|
|
})
|
|
}
|
|
)
|
|
})
|
|
}
|
|
|
|
public async login(username: string, password: string) {
|
|
const clientId =
|
|
this.getServerType() === ServerType.Sasjs ? 'clientID1' : undefined
|
|
|
|
return this.sasjsAdapter
|
|
.logIn(username, password, clientId)
|
|
.then(
|
|
(res: { isLoggedIn: boolean; userName: string }) => {
|
|
if (res.isLoggedIn) {
|
|
this.userService.user = { username: res.userName }
|
|
|
|
if (this.appLocCheckPending) {
|
|
this.checkViyaDeploy(this.dcAdapterSettings?.appLoc || '')
|
|
this.appLocCheckPending = false
|
|
}
|
|
}
|
|
|
|
this.shouldLogin.next(!res.isLoggedIn)
|
|
return res.isLoggedIn
|
|
},
|
|
(err: any) => {
|
|
console.error(err)
|
|
this.shouldLogin.next(true)
|
|
return false
|
|
}
|
|
)
|
|
.catch((e: any) => {
|
|
if (e === 403) {
|
|
console.error('Invalid host')
|
|
}
|
|
return false
|
|
})
|
|
}
|
|
|
|
public reloadStartupData() {
|
|
this.loadStartupServiceEmitter.emit()
|
|
}
|
|
|
|
public getLicenseSiteId(): string[] {
|
|
return this.license_site_id.value || []
|
|
}
|
|
|
|
public setLicenseSiteId(value: string | string[]) {
|
|
if (typeof value === 'object') {
|
|
this.license_site_id.next(value)
|
|
} else {
|
|
this.license_site_id.next([value])
|
|
}
|
|
}
|
|
|
|
public async checkSasjsDeploy() {
|
|
const sasjsConfig = this.getSasjsConfig()
|
|
const configuratorFolder = `${sasjsConfig.appLoc}/services/admin`
|
|
|
|
this.sasjsService.getFolderContentsFromDrive(configuratorFolder).subscribe(
|
|
(contents: SASjsApiDriveFolderContents) => {
|
|
if (contents.files.includes('makedata.sas')) {
|
|
this.eventService.startupDataLoaded()
|
|
this.router.navigateByUrl('/deploy')
|
|
} else {
|
|
this.loadStartupServiceEmitter.emit()
|
|
|
|
if (this.router.url.includes('deploy')) this.router.navigateByUrl('/')
|
|
}
|
|
},
|
|
(err: any) => {
|
|
const errorMessage =
|
|
typeof err.error === 'string'
|
|
? err.error
|
|
: JSON.stringify(err.error || err)
|
|
|
|
if (errorMessage.includes('Unauthorized')) {
|
|
this.shouldLogin.next(true)
|
|
|
|
this.shouldLogin.subscribe((res: boolean) => {
|
|
if (res === false) location.reload()
|
|
})
|
|
} else if (errorMessage.includes(`Folder doesn't exist.`)) {
|
|
console.warn(
|
|
'SASjs SAS services are not present on the current appLoc.'
|
|
)
|
|
this.eventService.startupDataLoaded()
|
|
this.router.navigateByUrl('/deploy')
|
|
}
|
|
}
|
|
)
|
|
}
|
|
|
|
// Required type is NodeJS.Timeout
|
|
// But NodeJS is not available in browser so we have to go with any
|
|
checkingInterval: any
|
|
public async sasjsMakedataChecking(): Promise<boolean> {
|
|
return new Promise(async (resolve, reject) => {
|
|
this.checkingInterval = setInterval(async () => {
|
|
this.sasjsMakedataSuccessfull()
|
|
.then((success: boolean) => {
|
|
if (!!success) {
|
|
clearInterval(this.checkingInterval)
|
|
resolve(success)
|
|
}
|
|
})
|
|
.catch((err: any) => {
|
|
clearInterval(this.checkingInterval)
|
|
reject(err)
|
|
})
|
|
}, 1000)
|
|
})
|
|
}
|
|
|
|
private async sasjsMakedataSuccessfull(): Promise<boolean> {
|
|
return new Promise((resolve, reject) => {
|
|
const sasjsConfig = this.getSasjsConfig()
|
|
const configuratorFolder = `${sasjsConfig.appLoc}/services/admin`
|
|
|
|
this.sasjsService
|
|
.getFolderContentsFromDrive(configuratorFolder)
|
|
.subscribe(
|
|
(contents: SASjsApiDriveFolderContents) => {
|
|
if (!contents.files.includes('makedata.sas')) {
|
|
resolve(true)
|
|
} else {
|
|
resolve(false)
|
|
}
|
|
},
|
|
(err: any) => {
|
|
const errorMessage =
|
|
typeof err.error === 'string'
|
|
? err.error
|
|
: JSON.stringify(err.error || err)
|
|
if (errorMessage.includes(`Folder doesn't exist.`)) {
|
|
reject()
|
|
}
|
|
}
|
|
)
|
|
})
|
|
}
|
|
|
|
public async checkViyaDeploy(path: string) {
|
|
const getFolderExistsInAdapter =
|
|
typeof this.sasjsAdapter.getFolder !== 'undefined'
|
|
|
|
let appLocExists: boolean = false
|
|
let errorMessage: string | undefined = undefined
|
|
|
|
if (getFolderExistsInAdapter) {
|
|
const results = await this.appLocCheck(path)
|
|
|
|
appLocExists = results.found
|
|
errorMessage = results.errorMessage
|
|
} else {
|
|
appLocExists = await this.appLocCheckPreAxiosdAdapter(path)
|
|
}
|
|
|
|
if (appLocExists) {
|
|
// Check if there is appLoc/services/admin/makedata.sas present
|
|
// if yes, it needs to be run, so we redirect to /deploy
|
|
// if not, we load the startup service
|
|
|
|
this.viyaMakedataSuccessfull().then(
|
|
(success: boolean) => {
|
|
if (success) {
|
|
this.loadStartupServiceEmitter.emit()
|
|
} else {
|
|
this.eventService.startupDataLoaded()
|
|
this.router.navigateByUrl('/deploy')
|
|
}
|
|
},
|
|
(error: any) => {
|
|
console.error('Error while looking for the file: makedata.sas', error)
|
|
}
|
|
)
|
|
} else {
|
|
const errorMessageToShow =
|
|
(errorMessage ||
|
|
'Viya services are not present on the current appLoc, or API not reachable. Check the ADAPTER configuration.') +
|
|
`\nAppLoc: ${path}`
|
|
|
|
this.eventService.showInfoModal('Error', errorMessageToShow)
|
|
}
|
|
}
|
|
|
|
private async viyaMakedataSuccessfull(): Promise<boolean> {
|
|
return new Promise((resolve, reject) => {
|
|
const sasjsConfig = this.getSasjsConfig()
|
|
const configuratorFolder = `${sasjsConfig.appLoc}/services/admin`
|
|
|
|
this.sasViyaService.getFolderByPath(configuratorFolder).subscribe(
|
|
(folderInfo: ViyaApiFolder) => {
|
|
const folderId = folderInfo.id
|
|
|
|
if (!folderId) {
|
|
console.error(
|
|
`Folder ID is not present. ${configuratorFolder}`,
|
|
sasjsConfig
|
|
)
|
|
resolve(false)
|
|
}
|
|
|
|
this.sasViyaService.getFolderMembers(folderId).subscribe(
|
|
(members: ViyaApiFolderMembers) => {
|
|
if (
|
|
!members.items.some((item: any) => item.name === 'makedata')
|
|
) {
|
|
// Makedata.sas is not present, which means it was run
|
|
resolve(true)
|
|
} else {
|
|
// Makedata.sas is present, which means it was not run
|
|
resolve(false)
|
|
}
|
|
},
|
|
(err: any) => {
|
|
console.error('Error getting folder contents', err)
|
|
reject()
|
|
}
|
|
)
|
|
},
|
|
(err: any) => {
|
|
console.warn('Error getting folder info', err)
|
|
reject(err)
|
|
}
|
|
)
|
|
})
|
|
}
|
|
|
|
public appLocCheck(
|
|
path: string
|
|
): Promise<{ found: boolean; errorMessage?: string }> {
|
|
return new Promise(async (resolve, reject) => {
|
|
let fetchError: string = ''
|
|
|
|
let res: any
|
|
|
|
try {
|
|
res = await this.sasjsAdapter.getFolder(path)
|
|
} catch (err: any) {
|
|
if (err.name === 'LoginRequiredError') {
|
|
this.appLocCheckPending = true
|
|
this.shouldLogin.next(true)
|
|
|
|
resolve({ found: false })
|
|
} else if (err.name === 'NotFoundeError') {
|
|
fetchError = err.message
|
|
} else {
|
|
fetchError =
|
|
'Viya services are not present on the current appLoc, or API not reachable. Check the ADAPTER configuration.'
|
|
}
|
|
}
|
|
|
|
if (fetchError.length) {
|
|
console.warn(fetchError)
|
|
return resolve({ found: false, errorMessage: fetchError })
|
|
}
|
|
|
|
resolve({ found: true })
|
|
})
|
|
}
|
|
|
|
/**
|
|
* This is a function written before axios adapter where we
|
|
* are getting the folder directly from DC. axios adapter
|
|
* has getFolder() function that provides the folder.
|
|
* @param path The path of the folder of which details we are getting
|
|
*/
|
|
public appLocCheckPreAxiosdAdapter(path: string): Promise<boolean> {
|
|
return new Promise((resolve, reject) => {
|
|
let url = `/folders/folders/@item?path=${path}`
|
|
let statusNotFound: boolean = false
|
|
|
|
return fetch(url)
|
|
.then((res) => {
|
|
if (res.status === 404) {
|
|
statusNotFound = true
|
|
}
|
|
|
|
return res.text()
|
|
})
|
|
.then((res) => {
|
|
if (this.isLoginRequired(res)) {
|
|
this.appLocCheckPending = true
|
|
this.shouldLogin.next(true)
|
|
} else {
|
|
if (statusNotFound) {
|
|
console.warn(
|
|
'Viya services are not present on the current appLoc.'
|
|
)
|
|
this.eventService.startupDataLoaded()
|
|
this.router.navigateByUrl('/deploy')
|
|
return resolve(false)
|
|
}
|
|
|
|
let jsonResponse: any = null
|
|
|
|
try {
|
|
jsonResponse = JSON.parse(res)
|
|
} catch (ex) {}
|
|
|
|
if (jsonResponse) {
|
|
if (jsonResponse.httpStatusCode) {
|
|
if (jsonResponse.httpStatusCode === 404) {
|
|
console.warn(
|
|
'Viya services are not present on the current appLoc.'
|
|
)
|
|
this.eventService.startupDataLoaded()
|
|
this.router.navigateByUrl('/deploy')
|
|
return resolve(false)
|
|
}
|
|
}
|
|
}
|
|
|
|
resolve(true)
|
|
}
|
|
})
|
|
.catch((error: any) => {
|
|
resolve(false)
|
|
})
|
|
})
|
|
}
|
|
|
|
private isLoginRequired(response: string) {
|
|
const pattern: RegExp = /<form.+action="(.*Logon[^"]*).*>/gm
|
|
const matches = pattern.test(response)
|
|
return matches
|
|
}
|
|
|
|
public logout() {
|
|
this.sasjsAdapter.logOut().then(() => {
|
|
location.reload()
|
|
})
|
|
}
|
|
|
|
public getSasjsConfig() {
|
|
return this.sasjsAdapter.getSasjsConfig()
|
|
}
|
|
|
|
public getSasRequests() {
|
|
return this.sasjsAdapter.getSasRequests()
|
|
}
|
|
|
|
public setDebugState(state: boolean) {
|
|
this.sasjsAdapter.setDebugState(state)
|
|
}
|
|
|
|
/**
|
|
* Returns the `&_debug=...` URL segment honoring the live adapter
|
|
* config. Empty string when debug is off. `128` on the Viya WEB JES path
|
|
* with `runAsTask` enabled, `131` otherwise.
|
|
*/
|
|
public getDebugUrlParam(): string {
|
|
const config = this.sasjsAdapter.getSasjsConfig()
|
|
if (!config.debug) return ''
|
|
const value =
|
|
config.serverType === ServerType.SasViya &&
|
|
config.useComputeApi === null &&
|
|
config.runAsTask === true
|
|
? 128
|
|
: 131
|
|
return `&_debug=${value}`
|
|
}
|
|
|
|
public getSasjsInstance() {
|
|
return this.sasjsAdapter
|
|
}
|
|
|
|
public getServerType(): string {
|
|
const sasjsConfig = this.sasjsAdapter.getSasjsConfig()
|
|
|
|
if (sasjsConfig.serverType) {
|
|
return sasjsConfig.serverType
|
|
}
|
|
|
|
return 'SASVIYA'
|
|
}
|
|
|
|
public getExecutionPath() {
|
|
const sasjsConfig = this.sasjsAdapter.getSasjsConfig()
|
|
|
|
switch (sasjsConfig.serverType) {
|
|
case ServerType.SasViya: {
|
|
return sasjsConfig.pathSASViya
|
|
}
|
|
case ServerType.Sas9: {
|
|
return sasjsConfig.pathSAS9
|
|
}
|
|
case ServerType.Sasjs: {
|
|
return sasjsConfig.pathSASJS
|
|
}
|
|
}
|
|
}
|
|
|
|
// Viya specific functions
|
|
public getFileContent(folderPath: string, fileName: string) {
|
|
return this.sasjsAdapter.getFileContent(folderPath, fileName)
|
|
}
|
|
|
|
public updateFileContent(
|
|
folderPath: string,
|
|
fileName: string,
|
|
content: string
|
|
) {
|
|
return this.sasjsAdapter.updateFileContent(folderPath, fileName, content)
|
|
}
|
|
}
|