Files
dc/client/src/app/services/sas.service.ts
T
sead bb80476767
Lighthouse Checks / lighthouse (pull_request) Failing after 3m28s
Build / Build-and-ng-test (pull_request) Failing after 3m51s
Build / Build-and-test-development (pull_request) Has been skipped
fix: use correct debug param for runAsTask
2026-05-14 11:21:01 +02:00

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)
}
}