From b1c0e26c237b6d09e2820844a1c2ac77c3f28795 Mon Sep 17 00:00:00 2001 From: Mihajlo Medjedovic Date: Wed, 4 Jun 2025 17:04:27 +0200 Subject: [PATCH] feat: added methods to GET and UPDATE file content on viya --- src/SASViyaApiClient.ts | 133 +++++++++++++++++++++++++++++++-- src/SASjs.ts | 39 +++++++++- src/types/FileContentUpdate.ts | 33 ++++++++ 3 files changed, 198 insertions(+), 7 deletions(-) create mode 100644 src/types/FileContentUpdate.ts diff --git a/src/SASViyaApiClient.ts b/src/SASViyaApiClient.ts index ad35aea..75fecb0 100644 --- a/src/SASViyaApiClient.ts +++ b/src/SASViyaApiClient.ts @@ -28,6 +28,7 @@ import { uploadTables } from './api/viya/uploadTables' import { executeOnComputeApi } from './api/viya/executeOnComputeApi' import { getAccessTokenForViya } from './auth/getAccessTokenForViya' import { refreshTokensForViya } from './auth/refreshTokensForViya' +import { FileContentUpdate } from './types/FileContentUpdate' interface JobExecutionResult { result?: { result: object } @@ -311,6 +312,72 @@ export class SASViyaApiClient { ) } + /** + * Fetches the file content for a file in the specified folder. + * + * @param folderPath - the full path to the folder containing the file. eg: /Public/folder1/folder2 + * @param fileName - the name of the file in the `folderPath` + * @param accessToken - an access token for authorizing the request + */ + public async getFileContent( + folderPath: string, + fileName: string, + accessToken?: string + ) { + const { fileUri } = await this.getFileUri( + folderPath, + fileName, + accessToken + ).catch((err) => { + throw prefixMessage( + err, + `Error while getting file URI for: ${fileName} in folder: ${folderPath}. ` + ) + }) + + return await this.requestClient + .get(`${this.serverUrl}${fileUri}/content`, accessToken) + .then((res) => res.result) + } + + /** + * Updates the file content for a file in the specified folder. + * + * @param folderPath - the full path to the folder containing the file. eg: /Public/folder1/folder2 + * @param fileName - the name of the file in the `folderPath` + * @param content - the new content to be written to the file + * @param accessToken - an access token for authorizing the request + */ + public async updateFileContent( + folderPath: string, + fileName: string, + content: string, + accessToken?: string + ) { + const { fileUri, etag } = await this.getFileUri( + folderPath, + fileName, + accessToken + ).catch((err) => { + throw prefixMessage( + err, + `Error while getting file URI for: ${fileName} in folder: ${folderPath}. ` + ) + }) + + return await this.requestClient + .put( + `${this.serverUrl}${fileUri}/content`, + content, + accessToken, + { + 'If-Match': etag, + 'Content-Type': 'text/plain' + } + ) + .then((res) => res.result) + } + /** * Fetches a folder. Path to the folder is required. * @param folderPath - the absolute path to the folder. @@ -941,6 +1008,7 @@ export class SASViyaApiClient { }) if (!folder) return undefined + return folder } @@ -952,6 +1020,47 @@ export class SASViyaApiClient { return `/folders/folders/${folderDetails.id}` } + private async getFileUri( + folderPath: string, + fileName: string, + accessToken?: string + ): Promise<{ + fileUri: string + etag: string + }> { + const folderMembers = await this.listFolder(folderPath, accessToken, 1000, { + returnDetails: true + }).catch((err) => { + throw prefixMessage(err, `Error while listing folder: ${folderPath}. `) + }) + + if (!folderMembers || !folderMembers.length) + throw new Error(`No members found in folder: ${folderPath}`) + + const fileUri = folderMembers.find( + (member) => member.name === fileName + )?.uri + + if (!fileUri) + throw new Error(`File ${fileName} not found in folder: ${folderPath}`) + + // Fetch the file details to get the resource ETag + const { etag } = await this.requestClient.get( + `${this.serverUrl}${fileUri}`, + accessToken + ) + + if (!etag) + throw new Error( + `File ${fileName} does not have an ETag, or request failed.` + ) + + return { + fileUri, + etag: etag || '' + } + } + private async getRecycleBinUri(accessToken?: string) { const url = '/folders/folders/@myRecycleBin' @@ -999,14 +1108,19 @@ export class SASViyaApiClient { } /** - * Lists children folders for given Viya folder. + * Lists children folders/files for given Viya folder. * @param sourceFolder - the full path (eg `/Public/example/myFolder`) or URI of the source folder listed. Providing URI instead of path will save one extra request. * @param accessToken - an access token for authorizing the request. + * @param {Object} [options] - Additional options. + * @param {boolean} [options.returnDetails=false] - when set to true, the function will return an array of objects with member details, otherwise it will return an array of member names. */ public async listFolder( sourceFolder: string, accessToken?: string, - limit: number = 20 + limit: number = 20, + options?: { + returnDetails?: boolean + } ) { // checks if 'sourceFolder' is already a URI const sourceFolderUri = isUri(sourceFolder) @@ -1018,11 +1132,20 @@ export class SASViyaApiClient { accessToken ) + let membersToReturn = [] + if (members && members.items) { - return members.items.map((item: any) => item.name) - } else { - return [] + // If returnDetails is true, return full member details + if (options?.returnDetails) { + membersToReturn = members.items + } else { + // If returnDetails is false, return only member names + membersToReturn = members.items.map((item: any) => item.name) + } } + + // Return members without Etag + return membersToReturn } /** diff --git a/src/SASjs.ts b/src/SASjs.ts index 5a4a5c2..71db4c8 100644 --- a/src/SASjs.ts +++ b/src/SASjs.ts @@ -411,6 +411,36 @@ export default class SASjs { ) } + public async getFileContent( + folderPath: string, + fileName: string, + accessToken?: string + ) { + this.isMethodSupported('getFileContent', [ServerType.SasViya]) + + return await this.sasViyaApiClient!.getFileContent( + folderPath, + fileName, + accessToken + ) + } + + public async updateFileContent( + folderPath: string, + fileName: string, + content: string, + accessToken?: string + ) { + this.isMethodSupported('updateFileContent', [ServerType.SasViya]) + + return await this.sasViyaApiClient!.updateFileContent( + folderPath, + fileName, + content, + accessToken + ) + } + /** * Fetches a folder from the SAS file system. * @param folderPath - path of the folder to be fetched. @@ -436,18 +466,23 @@ export default class SASjs { * Lists children folders for given Viya folder. * @param sourceFolder - the full path (eg `/Public/example/myFolder`) or URI of the source folder listed. Providing URI instead of path will save one extra request. * @param accessToken - an access token for authorizing the request. + * @param returnDetails - when set to true, the function will return an array of objects with member details, otherwise it will return an array of member names. */ public async listFolder( sourceFolder: string, accessToken?: string, - limit?: number + limit?: number, + returnDetails = false ) { this.isMethodSupported('listFolder', [ServerType.SasViya]) return await this.sasViyaApiClient?.listFolder( sourceFolder, accessToken, - limit + limit, + { + returnDetails + } ) } diff --git a/src/types/FileContentUpdate.ts b/src/types/FileContentUpdate.ts new file mode 100644 index 0000000..48ab227 --- /dev/null +++ b/src/types/FileContentUpdate.ts @@ -0,0 +1,33 @@ +export interface FileContentUpdate { + creationTimeStamp: string + modifiedTimeStamp: string + createdBy: string + modifiedBy: string + id: string + properties: Properties + contentDisposition: string + contentType: string + encoding: string + links: Link[] + name: string + size: number + searchable: boolean + fileStatus: string + fileVersion: number + typeDefName: string + version: number + virusDetected: boolean + urlDetected: boolean + quarantine: boolean +} + +export interface Link { + method: string + rel: string + href: string + uri: string + type?: string + responseType?: string +} + +export interface Properties {}