Skip to content
Merged
Show file tree
Hide file tree
Changes from 9 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
};
213 changes: 213 additions & 0 deletions lib/object-storage/object-storage.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,213 @@
import { Bucket, File, Storage } from '@google-cloud/storage';

import { InternalServerError } from 'errors/apps-sdk-error';
import {
DeleteFileResponse,
DownloadFileResponse,
FileInfo,
GetFileInfoResponse,
ListFilesOptions,
ListFilesResponse,
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}`
Comment on lines 208 to 212
Copy link
Contributor

Choose a reason for hiding this comment

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

most of this can be extracted as well

};
}
}
}
45 changes: 45 additions & 0 deletions lib/types/object-storage.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
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;
}
Comment on lines 6 to 56
Copy link
Contributor

Choose a reason for hiding this comment

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

let's extract { success; boolean, error?: string } into a shared base response

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