From 7e64819eb234aab82411c1ef17e6236aa2f3f6e1 Mon Sep 17 00:00:00 2001 From: Yury Shkoda Date: Tue, 28 Sep 2021 10:38:29 +0300 Subject: [PATCH] feat(sasjs/server): add SASBaseApiClient class --- src/SASBaseApiClient.ts | 105 +++++++++++++++++++++++++++++++++++++ src/SASjs.ts | 37 +++++++++++-- src/auth/AuthManager.ts | 2 + src/types/ExecuteScript.ts | 8 +++ src/types/SASjsConfig.ts | 1 + 5 files changed, 148 insertions(+), 5 deletions(-) create mode 100644 src/SASBaseApiClient.ts create mode 100644 src/types/ExecuteScript.ts diff --git a/src/SASBaseApiClient.ts b/src/SASBaseApiClient.ts new file mode 100644 index 0000000..30684fb --- /dev/null +++ b/src/SASBaseApiClient.ts @@ -0,0 +1,105 @@ +import { + MemberType, + FolderMember, + ServiceMember, + ExecutionQuery, + ExecutionResult +} from './types' +import { + createFolder, + createFile, + fileExists, + readFile, + asyncForEach, + generateTimestamp +} from '@sasjs/utils' +import { getTmpFilesFolderPath, getTmpLogFolderPath } from './utils' +import * as path from 'path' +import { promisify } from 'util' +import { execFile } from 'child_process' +const execFilePromise = promisify(execFile) + +export class SASBaseApiClient { + public async deploy(members: [FolderMember, ServiceMember]) { + await this.createFileTree(members) + } + + constructor(private pathSASBase: string) {} + + public setConfig(pathSASBase: string) { + if (pathSASBase) this.pathSASBase = pathSASBase + } + + // TODO: make public + private async createFileTree( + members: [FolderMember, ServiceMember], + parentFolders: string[] = [] + ) { + const destinationPath = path.join( + await getTmpFilesFolderPath(), + path.join(...parentFolders) + ) + + await asyncForEach( + members, + async (member: FolderMember | ServiceMember) => { + const name = member.name + + if (member.type === MemberType.folder) { + await createFolder(path.join(destinationPath, name)).catch((err) => + Promise.reject({ error: err, failedToCreate: name }) + ) + + await this.createFileTree(member.members, [ + ...parentFolders, + name + ]).catch((err) => + Promise.reject({ error: err, failedToCreate: name }) + ) + } else { + await createFile(path.join(destinationPath, name), member.code).catch( + (err) => Promise.reject({ error: err, failedToCreate: name }) + ) + } + } + ) + + return Promise.resolve(true) + } + + public async executeScript( + query: ExecutionQuery + ): Promise { + let sasCodePath = path.join(await getTmpFilesFolderPath(), query._program) + + sasCodePath = sasCodePath.replace(new RegExp('/', 'g'), path.sep) + + if (!(await fileExists(sasCodePath))) { + return Promise.reject(`${query._program} does not exist.`) + } + + const sasFile: string = sasCodePath.split(path.sep).pop() || 'default' + + const sasLogPath = path.join( + await getTmpLogFolderPath(), + [sasFile.replace(/\.sas/g, ''), '-', generateTimestamp(), '.log'].join('') + ) + + const { stdout, stderr } = await execFilePromise(this.pathSASBase, [ + '-SYSIN', + sasCodePath, + '-log', + sasLogPath, + '-nosplash' + ]) + + if (stderr) return Promise.reject(stderr) + + if (await fileExists(sasLogPath)) { + return Promise.resolve({ + log: await readFile(sasLogPath), + logPath: sasLogPath + }) + } + } +} diff --git a/src/SASjs.ts b/src/SASjs.ts index 74831a5..16b6651 100644 --- a/src/SASjs.ts +++ b/src/SASjs.ts @@ -4,10 +4,14 @@ import { UploadFile, EditContextInput, PollOptions, - LoginMechanism + LoginMechanism, + FolderMember, + ServiceMember, + ExecutionQuery } from './types' import { SASViyaApiClient } from './SASViyaApiClient' import { SAS9ApiClient } from './SAS9ApiClient' +import { SASBaseApiClient } from './SASBaseApiClient' import { AuthManager } from './auth' import { ServerType, @@ -29,6 +33,7 @@ import { LoginOptions, LoginResult } from './types/Login' const defaultConfig: SASjsConfig = { serverUrl: '', + pathSASBase: '', pathSAS9: '/SASStoredProcess/do', pathSASViya: '/SASJobExecution', appLoc: '/Public/seedapp', @@ -49,6 +54,7 @@ export default class SASjs { private jobsPath: string = '' private sasViyaApiClient: SASViyaApiClient | null = null private sas9ApiClient: SAS9ApiClient | null = null + private sasBaseApiClient: SASBaseApiClient | null = null private fileUploader: FileUploader | null = null private authManager: AuthManager | null = null private requestClient: RequestClient | null = null @@ -824,6 +830,14 @@ export default class SASjs { ) } + public async deployToSASBase(members: [FolderMember, ServiceMember]) { + return await this.sasBaseApiClient?.deploy(members) + } + + public async executeScriptSASBase(query: ExecutionQuery) { + return await this.sasBaseApiClient?.executeScript(query) + } + /** * Kicks off execution of the given job via the compute API. * @returns an object representing the compute session created for the given job. @@ -973,30 +987,43 @@ 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.allowInsecureRequests ) + } + } + + if (this.sasjsConfig.serverType === ServerType.Sasjs) { + if (this.sasBaseApiClient) { + this.sasBaseApiClient.setConfig(this.sasjsConfig.pathSASBase) + } else { + this.sasBaseApiClient = new SASBaseApiClient( + this.sasjsConfig.pathSASBase + ) + } } this.fileUploader = new FileUploader( diff --git a/src/auth/AuthManager.ts b/src/auth/AuthManager.ts index 288d4ae..f050949 100644 --- a/src/auth/AuthManager.ts +++ b/src/auth/AuthManager.ts @@ -256,6 +256,8 @@ export class AuthManager { .split(' ') .map((name: string) => name.slice(0, 3).toLowerCase()) .join('') + default: + return '' } } diff --git a/src/types/ExecuteScript.ts b/src/types/ExecuteScript.ts new file mode 100644 index 0000000..f7a38d1 --- /dev/null +++ b/src/types/ExecuteScript.ts @@ -0,0 +1,8 @@ +export interface ExecutionQuery { + _program: string +} + +export interface ExecutionResult { + log: string + logPath: string +} diff --git a/src/types/SASjsConfig.ts b/src/types/SASjsConfig.ts index b562dcb..f08590c 100644 --- a/src/types/SASjsConfig.ts +++ b/src/types/SASjsConfig.ts @@ -21,6 +21,7 @@ export class SASjsConfig { * will use '/SASJobExecution' on SAS Viya. */ pathSASViya: string = '' + pathSASBase: string = '' /** * The appLoc is the parent folder under which the SAS services (STPs or Job * Execution Services) are stored. We recommend that each app is stored in