Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions lib/index.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import { EnvironmentVariablesManager } from 'lib/environment-variables-manager';
import { Logger } from 'lib/logger';
import { ObjectStorage } from 'lib/object-storage';
import { Queue } from 'lib/queue';
import { SecretsManager } from 'lib/secrets-manager';
import { SecureStorage } from 'lib/secure-storage';
import { Period, Storage } from 'lib/storage';

export {
ObjectStorage,
SecureStorage,
Storage,
Period,
Expand Down
2 changes: 1 addition & 1 deletion lib/minimal-package.ts
Original file line number Diff line number Diff line change
@@ -1 +1 @@
export default { name: '@mondaycom/apps-sdk', version: '3.2.1' };
export default { name: '@mondaycom/apps-sdk', version: '3.3.0-beta.4' };
5 changes: 5 additions & 0 deletions lib/object-storage/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { ObjectStorage } from './object-storage';

export {
ObjectStorage
};
251 changes: 251 additions & 0 deletions lib/object-storage/object-storage.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,251 @@
import { Bucket, File, Storage } from '@google-cloud/storage';

import { InternalServerError } from 'errors/apps-sdk-error';
import { TIME_IN_MILLISECOND } from 'lib/utils/time-enum';
import {
DeleteFileResponse,
DownloadFileResponse,
FileInfo,
GetFileInfoResponse,
ListFilesOptions,
ListFilesResponse,
PresignedUrlOptions,
PresignedUrlResponse,
UploadFileOptions,
UploadFileResponse
} from 'types/object-storage';
import { Logger } from 'utils/logger';

const logger = new Logger('ObjectStorage', { mondayInternal: true });

export class ObjectStorage {
private storage: Storage;
private bucketName: string;

constructor() {
if (!process.env.OBJECT_STORAGE_BUCKET) {
throw new InternalServerError('OBJECT_STORAGE_BUCKET is not set');
}

this.storage = new Storage();
this.bucketName = process.env.OBJECT_STORAGE_BUCKET;
logger.info(`ObjectStorage initialized with bucket: ${this.bucketName}`);
}

private getBucket(): Bucket {
return this.storage.bucket(this.bucketName);
}

private handleError(error: unknown, operation: string): { errorMessage: string; errorObj: Error } {
const errorMessage = error instanceof Error ? error.message : String(error);
const errorObj = error instanceof Error ? error : new Error(String(error));
logger.error(`Failed to ${operation}:`, { error: errorObj });
return { errorMessage, errorObj };
}

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@maorb-dev add another method that will provide the dev with a pre-signed url for uploading

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

once added, also expose it with the mcode-sdk-api, simlar to this: https://github.com/DaPulse/mcode-sdk-api/pull/35/files
and also generate the python + PHP clients

async uploadFile(fileName: string, content: Buffer | string, options: UploadFileOptions = {}): Promise<UploadFileResponse> {
try {
const bucket = this.getBucket();
const file: File = bucket.file(fileName);

const uploadOptions = {
metadata: {
contentType: options.contentType || 'application/octet-stream',
metadata: options.metadata || {}
}
};

await file.save(content, uploadOptions);

const fileUrl = `gs://${this.bucketName}/${fileName}`;

logger.info(`File uploaded successfully: ${fileName}`);

return {
success: true,
fileName,
fileUrl
};
} catch (error) {
const { errorMessage } = this.handleError(error, 'upload file');
return {
success: false,
error: `Failed to upload file: ${errorMessage}`
};
}
}

async downloadFile(fileName: string): Promise<DownloadFileResponse> {
try {
const bucket = this.getBucket();
const file: File = bucket.file(fileName);

const [exists] = await file.exists();
if (!exists) {
return {
success: false,
error: 'File not found'
};
}

const [content] = await file.download();
const [metadata] = await file.getMetadata();

return {
success: true,
content,
contentType: metadata.contentType || 'application/octet-stream'
};
} catch (error) {
const { errorMessage } = this.handleError(error, 'download file');
return {
success: false,
error: `Failed to download file: ${errorMessage}`
};
}
}

async deleteFile(fileName: string): Promise<DeleteFileResponse> {
try {
const bucket = this.getBucket();
const file: File = bucket.file(fileName);

const [exists] = await file.exists();
if (!exists) {
return {
success: false,
error: 'File not found'
};
}

await file.delete();

logger.info(`File deleted successfully: ${fileName}`);

return { success: true };
} catch (error) {
const { errorMessage } = this.handleError(error, 'delete file');
return {
success: false,
error: `Failed to delete file: ${errorMessage}`
};
}
}

async listFiles(options: ListFilesOptions = {}): Promise<ListFilesResponse> {
try {
const bucket = this.getBucket();

const queryOptions = {
maxResults: options.maxResults || 100,
...(options.prefix && { prefix: options.prefix }),
...(options.pageToken && { pageToken: options.pageToken })
};

const [files, , apiResponse] = await bucket.getFiles(queryOptions);

const fileInfos: Array<FileInfo> = files.map((file: File) => ({
name: file.name,
size: parseInt(String(file.metadata.size || '0'), 10) || 0,
contentType: file.metadata.contentType || 'application/octet-stream',
lastModified: new Date(file.metadata.updated || Date.now()),
etag: file.metadata.etag || '',
metadata: Object.fromEntries(
Object.entries(file.metadata.metadata || {}).map(([key, value]) => [
key,
String(value || '')
])
)
}));

return {
success: true,
files: fileInfos,
nextPageToken: (apiResponse as { nextPageToken?: string })?.nextPageToken
};
} catch (error) {
const { errorMessage } = this.handleError(error, 'list files');
return {
success: false,
error: `Failed to list files: ${errorMessage}`
};
}
}

async getFileInfo(fileName: string): Promise<GetFileInfoResponse> {
try {
const bucket = this.getBucket();
const file: File = bucket.file(fileName);

const [exists] = await file.exists();
if (!exists) {
return {
success: false,
error: 'File not found'
};
}

const [metadata] = await file.getMetadata();

const fileInfo: FileInfo = {
name: file.name,
size: parseInt(String(metadata.size || '0'), 10) || 0,
contentType: metadata.contentType || 'application/octet-stream',
lastModified: new Date(metadata.updated || Date.now()),
etag: metadata.etag || '',
metadata: Object.fromEntries(
Object.entries(metadata.metadata || {}).map(([key, value]) => [
key,
String(value || '')
])
)
};

return {
success: true,
fileInfo
};
} catch (error) {
const { errorMessage } = this.handleError(error, 'get file info');
return {
success: false,
error: `Failed to get file info: ${errorMessage}`
};
}
}

async getPresignedUploadUrl(fileName: string, options: PresignedUrlOptions = {}): Promise<PresignedUrlResponse> {
try {
const bucket = this.getBucket();
const file: File = bucket.file(fileName);

const fifteenMinutesFromNow = new Date(Date.now() + TIME_IN_MILLISECOND.MINUTE * 15);
const expires = options.expires || fifteenMinutesFromNow;

const signedUrlOptions = {
version: 'v4' as const,
action: 'write' as const,
expires,
...(options.contentType && {
contentType: options.contentType
})
};

const [presignedUrl] = await file.getSignedUrl(signedUrlOptions);

logger.info(`Presigned upload URL generated for file: ${fileName}`);

return {
success: true,
presignedUrl,
fileName
};
} catch (error) {
const { errorMessage } = this.handleError(error, 'generate presigned upload URL');
return {
success: false,
error: `Failed to generate presigned upload URL: ${errorMessage}`
};
}
}
}
55 changes: 55 additions & 0 deletions lib/types/object-storage.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
export type BaseResponse = {
success: boolean;
error?: string;
}

export type UploadFileOptions = {
contentType?: string;
metadata?: Record<string, string>;
}

export type UploadFileResponse = BaseResponse & {
fileName?: string;
fileUrl?: string;
}

export type DownloadFileResponse = BaseResponse & {
content?: Buffer;
contentType?: string;
}

export type DeleteFileResponse = BaseResponse;

export type ListFilesOptions = {
prefix?: string;
maxResults?: number;
pageToken?: string;
}

export type FileInfo = {
name: string;
size: number;
contentType: string;
lastModified: Date;
etag: string;
metadata: Record<string, string>;
}

export type ListFilesResponse = BaseResponse & {
files?: Array<FileInfo>;
nextPageToken?: string;
}

export type GetFileInfoResponse = BaseResponse & {
fileInfo?: FileInfo;
}

export type PresignedUrlOptions = {
expires?: Date;
contentType?: string;
}

export type PresignedUrlResponse = BaseResponse & {
presignedUrl?: string;
fileName?: string;
}
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@mondaycom/apps-sdk",
"version": "3.2.1",
"version": "3.3.0-beta.4",
"description": "monday apps SDK for NodeJS",
"main": "./dist/cjs/index.js",
"module": "./dist/esm/index.js",
Expand Down Expand Up @@ -69,6 +69,7 @@
},
"dependencies": {
"@google-cloud/pubsub": "^4.4.0",
"@google-cloud/storage": "^7.7.0",
"app-root-path": "^3.1.0",
"google-auth-library": "^9.10.0",
"http-status-codes": "^2.2.0",
Expand Down
Loading
Loading