From e7a36d7df11101e0bb5317579e3af762ab42ac3d Mon Sep 17 00:00:00 2001 From: Daniel Vainshtein Date: Mon, 30 Jun 2025 14:21:52 +0200 Subject: [PATCH 01/17] [beta] add object storage --- lib/index.ts | 2 + lib/object-storage/index.ts | 5 + lib/object-storage/object-storage.ts | 226 ++++++++++++++++++++ lib/types/object-storage.ts | 52 +++++ package.json | 3 +- tests/object-storage/object-storage.test.ts | 122 +++++++++++ 6 files changed, 409 insertions(+), 1 deletion(-) create mode 100644 lib/object-storage/index.ts create mode 100644 lib/object-storage/object-storage.ts create mode 100644 lib/types/object-storage.ts create mode 100644 tests/object-storage/object-storage.test.ts diff --git a/lib/index.ts b/lib/index.ts index 0ad54f5..38168d7 100644 --- a/lib/index.ts +++ b/lib/index.ts @@ -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, diff --git a/lib/object-storage/index.ts b/lib/object-storage/index.ts new file mode 100644 index 0000000..cd97fe8 --- /dev/null +++ b/lib/object-storage/index.ts @@ -0,0 +1,5 @@ +import { ObjectStorage } from './object-storage'; + +export { + ObjectStorage +}; \ No newline at end of file diff --git a/lib/object-storage/object-storage.ts b/lib/object-storage/object-storage.ts new file mode 100644 index 0000000..cd53255 --- /dev/null +++ b/lib/object-storage/object-storage.ts @@ -0,0 +1,226 @@ +import { Storage } from '@google-cloud/storage'; +import { GoogleAuth } from 'google-auth-library'; + +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'); + } + + const auth = new GoogleAuth({ + scopes: ['https://www.googleapis.com/auth/cloud-platform'] + }); + this.storage = new Storage({ auth }); + + this.bucketName = process.env.OBJECT_STORAGE_BUCKET; + logger.info(`ObjectStorage initialized with bucket: ${this.bucketName}`); + } + + /** + * Upload a file to the object storage bucket + */ + async uploadFile(fileName: string, content: Buffer | string, options: UploadFileOptions = {}): Promise { + try { + const bucket = this.storage.bucket(this.bucketName); + const file = bucket.file(fileName); + + const uploadOptions: any = { + metadata: { + contentType: options.contentType || 'application/octet-stream', + metadata: options.metadata || {} + } + }; + + if (options.public) { + uploadOptions.predefinedAcl = 'publicRead'; + } + + await file.save(content, uploadOptions); + + const fileUrl = options.public + ? `https://storage.googleapis.com/${this.bucketName}/${fileName}` + : `gs://${this.bucketName}/${fileName}`; + + logger.info(`File uploaded successfully: ${fileName}`); + + return { + success: true, + fileName, + fileUrl + }; + } catch (error: any) { + logger.error('Failed to upload file:', error); + return { + success: false, + error: `Failed to upload file: ${error.message}` + }; + } + } + + /** + * Download a file from the object storage bucket + */ + async downloadFile(fileName: string): Promise { + try { + const bucket = this.storage.bucket(this.bucketName); + const 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 + }; + } catch (error: any) { + logger.error('Failed to download file:', error); + return { + success: false, + error: `Failed to download file: ${error.message}` + }; + } + } + + /** + * Delete a file from the object storage bucket + */ + async deleteFile(fileName: string): Promise { + try { + const bucket = this.storage.bucket(this.bucketName); + const 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: any) { + logger.error('Failed to delete file:', error); + return { + success: false, + error: `Failed to delete file: ${error.message}` + }; + } + } + + /** + * List files in the object storage bucket + */ + async listFiles(options: ListFilesOptions = {}): Promise { + try { + const bucket = this.storage.bucket(this.bucketName); + + const queryOptions: any = { + maxResults: options.maxResults || 100 + }; + + if (options.prefix) { + queryOptions.prefix = options.prefix; + } + + if (options.pageToken) { + queryOptions.pageToken = options.pageToken; + } + + const [files, , metadata] = await bucket.getFiles(queryOptions); + + const fileInfos: FileInfo[] = files.map(file => ({ + name: file.name, + size: parseInt(file.metadata.size) || 0, + contentType: file.metadata.contentType || 'application/octet-stream', + lastModified: new Date(file.metadata.updated), + etag: file.metadata.etag, + metadata: file.metadata.metadata || {} + })); + + return { + success: true, + files: fileInfos, + nextPageToken: metadata?.nextPageToken + }; + } catch (error: any) { + logger.error('Failed to list files:', error); + return { + success: false, + error: `Failed to list files: ${error.message}` + }; + } + } + + /** + * Get information about a specific file + */ + async getFileInfo(fileName: string): Promise { + try { + const bucket = this.storage.bucket(this.bucketName); + const 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(metadata.size) || 0, + contentType: metadata.contentType || 'application/octet-stream', + lastModified: new Date(metadata.updated), + etag: metadata.etag, + metadata: metadata.metadata || {} + }; + + return { + success: true, + fileInfo + }; + } catch (error: any) { + logger.error('Failed to get file info:', error); + return { + success: false, + error: `Failed to get file info: ${error.message}` + }; + } + } + } \ No newline at end of file diff --git a/lib/types/object-storage.ts b/lib/types/object-storage.ts new file mode 100644 index 0000000..abec869 --- /dev/null +++ b/lib/types/object-storage.ts @@ -0,0 +1,52 @@ +export interface UploadFileOptions { + contentType?: string; + metadata?: Record; + public?: boolean; +} + +export interface UploadFileResponse { + success: boolean; + fileName?: string; + fileUrl?: string; + error?: string; +} + +export interface DownloadFileResponse { + success: boolean; + content?: Buffer; + contentType?: string; + error?: string; +} + +export interface DeleteFileResponse { + success: boolean; + error?: string; +} + +export interface ListFilesOptions { + prefix?: string; + maxResults?: number; + pageToken?: string; +} + +export interface FileInfo { + name: string; + size: number; + contentType: string; + lastModified: Date; + etag: string; + metadata?: Record; +} + +export interface ListFilesResponse { + success: boolean; + files?: FileInfo[]; + nextPageToken?: string; + error?: string; +} + +export interface GetFileInfoResponse { + success: boolean; + fileInfo?: FileInfo; + error?: string; +} \ No newline at end of file diff --git a/package.json b/package.json index 73b5975..78fcaca 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@mondaycom/apps-sdk", - "version": "3.2.1", + "version": "3.3.0-beta", "description": "monday apps SDK for NodeJS", "main": "./dist/cjs/index.js", "module": "./dist/esm/index.js", @@ -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", diff --git a/tests/object-storage/object-storage.test.ts b/tests/object-storage/object-storage.test.ts new file mode 100644 index 0000000..18185d3 --- /dev/null +++ b/tests/object-storage/object-storage.test.ts @@ -0,0 +1,122 @@ +import { ObjectStorage } from 'lib/object-storage'; + +describe('ObjectStorage', () => { + let objectStorage: ObjectStorage; + + beforeEach(() => { + objectStorage = new ObjectStorage(); + }); + + describe('uploadFile', () => { + it('should upload a file successfully', async () => { + const fileName = 'test-file.txt'; + const content = 'Hello, World!'; + + const result = await objectStorage.uploadFile(fileName, content, { + contentType: 'text/plain', + metadata: { 'uploaded-by': 'test' } + }); + + expect(result.success).toBe(true); + expect(result.fileName).toBe(fileName); + expect(result.fileUrl).toContain(fileName); + }); + + it('should handle upload failure gracefully', async () => { + const fileName = ''; + const content = 'Hello, World!'; + + const result = await objectStorage.uploadFile(fileName, content); + + expect(result.success).toBe(false); + expect(result.error).toBeDefined(); + }); + }); + + describe('downloadFile', () => { + it('should download an existing file', async () => { + const fileName = 'existing-file.txt'; + + const result = await objectStorage.downloadFile(fileName); + + expect(result.success).toBe(true); + expect(result.content).toBeDefined(); + expect(result.contentType).toBeDefined(); + }); + + it('should handle file not found', async () => { + const fileName = 'non-existent-file.txt'; + + const result = await objectStorage.downloadFile(fileName); + + expect(result.success).toBe(false); + expect(result.error).toBe('File not found'); + }); + }); + + describe('listFiles', () => { + it('should list files with default options', async () => { + const result = await objectStorage.listFiles(); + + expect(result.success).toBe(true); + expect(Array.isArray(result.files)).toBe(true); + }); + + it('should list files with prefix filter', async () => { + const result = await objectStorage.listFiles({ prefix: 'test-' }); + + expect(result.success).toBe(true); + expect(Array.isArray(result.files)).toBe(true); + }); + + it('should list files with pagination', async () => { + const result = await objectStorage.listFiles({ maxResults: 10 }); + + expect(result.success).toBe(true); + expect(Array.isArray(result.files)).toBe(true); + }); + }); + + describe('deleteFile', () => { + it('should delete an existing file', async () => { + const fileName = 'file-to-delete.txt'; + + const result = await objectStorage.deleteFile(fileName); + + expect(result.success).toBe(true); + }); + + it('should handle file not found during deletion', async () => { + const fileName = 'non-existent-file.txt'; + + const result = await objectStorage.deleteFile(fileName); + + expect(result.success).toBe(false); + expect(result.error).toBe('File not found'); + }); + }); + + describe('getFileInfo', () => { + it('should return file information', async () => { + const fileName = 'info-file.txt'; + + const result = await objectStorage.getFileInfo(fileName); + + expect(result.success).toBe(true); + expect(result.fileInfo).toBeDefined(); + expect(result.fileInfo?.name).toBe(fileName); + expect(typeof result.fileInfo?.size).toBe('number'); + expect(result.fileInfo?.contentType).toBeDefined(); + expect(result.fileInfo?.lastModified).toBeInstanceOf(Date); + }); + + it('should handle file not found', async () => { + const fileName = 'non-existent-file.txt'; + + const result = await objectStorage.getFileInfo(fileName); + + expect(result.success).toBe(false); + expect(result.error).toBe('File not found'); + }); + }); +}); \ No newline at end of file From 260dd5afbaa29609b728b4af287e11f6f7155792 Mon Sep 17 00:00:00 2001 From: Daniel Vainshtein Date: Mon, 30 Jun 2025 14:41:40 +0200 Subject: [PATCH 02/17] feat(sdk): update lock [beta] --- lib/minimal-package.ts | 2 +- lib/object-storage/index.ts | 2 +- lib/object-storage/object-storage.ts | 105 ++++++++++---------- lib/types/object-storage.ts | 23 ++--- tests/object-storage/object-storage.test.ts | 20 ++-- yarn.lock | 91 ++++++++++++++++- 6 files changed, 164 insertions(+), 79 deletions(-) diff --git a/lib/minimal-package.ts b/lib/minimal-package.ts index cf4f033..ef4bebe 100644 --- a/lib/minimal-package.ts +++ b/lib/minimal-package.ts @@ -1 +1 @@ -export default { name: '@mondaycom/apps-sdk', version: '3.2.1' }; +export default { name: '@mondaycom/apps-sdk', version: '3.3.0-beta' }; diff --git a/lib/object-storage/index.ts b/lib/object-storage/index.ts index cd97fe8..fb88f8c 100644 --- a/lib/object-storage/index.ts +++ b/lib/object-storage/index.ts @@ -2,4 +2,4 @@ import { ObjectStorage } from './object-storage'; export { ObjectStorage -}; \ No newline at end of file +}; diff --git a/lib/object-storage/object-storage.ts b/lib/object-storage/object-storage.ts index cd53255..a0c50ca 100644 --- a/lib/object-storage/object-storage.ts +++ b/lib/object-storage/object-storage.ts @@ -1,5 +1,4 @@ import { Storage } from '@google-cloud/storage'; -import { GoogleAuth } from 'google-auth-library'; import { InternalServerError } from 'errors/apps-sdk-error'; import { @@ -25,11 +24,7 @@ export class ObjectStorage { throw new InternalServerError('OBJECT_STORAGE_BUCKET is not set'); } - const auth = new GoogleAuth({ - scopes: ['https://www.googleapis.com/auth/cloud-platform'] - }); - this.storage = new Storage({ auth }); - + this.storage = new Storage(); this.bucketName = process.env.OBJECT_STORAGE_BUCKET; logger.info(`ObjectStorage initialized with bucket: ${this.bucketName}`); } @@ -42,22 +37,16 @@ export class ObjectStorage { const bucket = this.storage.bucket(this.bucketName); const file = bucket.file(fileName); - const uploadOptions: any = { + const uploadOptions = { metadata: { contentType: options.contentType || 'application/octet-stream', metadata: options.metadata || {} } }; - if (options.public) { - uploadOptions.predefinedAcl = 'publicRead'; - } - await file.save(content, uploadOptions); - const fileUrl = options.public - ? `https://storage.googleapis.com/${this.bucketName}/${fileName}` - : `gs://${this.bucketName}/${fileName}`; + const fileUrl = `gs://${this.bucketName}/${fileName}`; logger.info(`File uploaded successfully: ${fileName}`); @@ -66,11 +55,13 @@ export class ObjectStorage { fileName, fileUrl }; - } catch (error: any) { - logger.error('Failed to upload file:', error); + } catch (error) { + const errorMessage = error instanceof Error ? error.message : String(error); + const errorObj = error instanceof Error ? error : new Error(String(error)); + logger.error('Failed to upload file:', { error: errorObj }); return { success: false, - error: `Failed to upload file: ${error.message}` + error: `Failed to upload file: ${errorMessage}` }; } } @@ -99,11 +90,13 @@ export class ObjectStorage { content, contentType: metadata.contentType }; - } catch (error: any) { - logger.error('Failed to download file:', error); + } catch (error) { + const errorMessage = error instanceof Error ? error.message : String(error); + const errorObj = error instanceof Error ? error : new Error(String(error)); + logger.error('Failed to download file:', { error: errorObj }); return { success: false, - error: `Failed to download file: ${error.message}` + error: `Failed to download file: ${errorMessage}` }; } } @@ -131,11 +124,13 @@ export class ObjectStorage { return { success: true }; - } catch (error: any) { - logger.error('Failed to delete file:', error); + } catch (error) { + const errorMessage = error instanceof Error ? error.message : String(error); + const errorObj = error instanceof Error ? error : new Error(String(error)); + logger.error('Failed to delete file:', { error: errorObj }); return { success: false, - error: `Failed to delete file: ${error.message}` + error: `Failed to delete file: ${errorMessage}` }; } } @@ -147,39 +142,40 @@ export class ObjectStorage { try { const bucket = this.storage.bucket(this.bucketName); - const queryOptions: any = { - maxResults: options.maxResults || 100 + const queryOptions = { + maxResults: options.maxResults || 100, + ...(options.prefix && { prefix: options.prefix }), + ...(options.pageToken && { pageToken: options.pageToken }) }; - - if (options.prefix) { - queryOptions.prefix = options.prefix; - } - - if (options.pageToken) { - queryOptions.pageToken = options.pageToken; - } - const [files, , metadata] = await bucket.getFiles(queryOptions); + const [files, , apiResponse] = await bucket.getFiles(queryOptions); - const fileInfos: FileInfo[] = files.map(file => ({ + const fileInfos: Array = files.map(file => ({ name: file.name, - size: parseInt(file.metadata.size) || 0, + size: parseInt(file.metadata.size?.toString() || '0') || 0, contentType: file.metadata.contentType || 'application/octet-stream', - lastModified: new Date(file.metadata.updated), - etag: file.metadata.etag, - metadata: file.metadata.metadata || {} + 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: metadata?.nextPageToken + nextPageToken: (apiResponse as { nextPageToken?: string })?.nextPageToken }; - } catch (error: any) { - logger.error('Failed to list files:', error); + } catch (error) { + const errorMessage = error instanceof Error ? error.message : String(error); + const errorObj = error instanceof Error ? error : new Error(String(error)); + logger.error('Failed to list files:', { error: errorObj }); return { success: false, - error: `Failed to list files: ${error.message}` + error: `Failed to list files: ${errorMessage}` }; } } @@ -204,23 +200,30 @@ export class ObjectStorage { const fileInfo: FileInfo = { name: file.name, - size: parseInt(metadata.size) || 0, + size: parseInt(metadata.size?.toString() || '0') || 0, contentType: metadata.contentType || 'application/octet-stream', - lastModified: new Date(metadata.updated), - etag: metadata.etag, - metadata: metadata.metadata || {} + 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: any) { - logger.error('Failed to get file info:', error); + } catch (error) { + const errorMessage = error instanceof Error ? error.message : String(error); + const errorObj = error instanceof Error ? error : new Error(String(error)); + logger.error('Failed to get file info:', { error: errorObj }); return { success: false, - error: `Failed to get file info: ${error.message}` + error: `Failed to get file info: ${errorMessage}` }; } } - } \ No newline at end of file +} diff --git a/lib/types/object-storage.ts b/lib/types/object-storage.ts index abec869..15caf70 100644 --- a/lib/types/object-storage.ts +++ b/lib/types/object-storage.ts @@ -1,52 +1,51 @@ -export interface UploadFileOptions { +export type UploadFileOptions = { contentType?: string; metadata?: Record; - public?: boolean; } -export interface UploadFileResponse { +export type UploadFileResponse = { success: boolean; fileName?: string; fileUrl?: string; error?: string; } -export interface DownloadFileResponse { +export type DownloadFileResponse = { success: boolean; content?: Buffer; contentType?: string; error?: string; } -export interface DeleteFileResponse { +export type DeleteFileResponse = { success: boolean; error?: string; } -export interface ListFilesOptions { +export type ListFilesOptions = { prefix?: string; maxResults?: number; pageToken?: string; } -export interface FileInfo { +export type FileInfo = { name: string; size: number; contentType: string; lastModified: Date; etag: string; - metadata?: Record; + metadata: Record; } -export interface ListFilesResponse { +export type ListFilesResponse = { success: boolean; - files?: FileInfo[]; + files?: Array; nextPageToken?: string; error?: string; } -export interface GetFileInfoResponse { +export type GetFileInfoResponse = { success: boolean; fileInfo?: FileInfo; error?: string; -} \ No newline at end of file +} diff --git a/tests/object-storage/object-storage.test.ts b/tests/object-storage/object-storage.test.ts index 18185d3..d3a6da4 100644 --- a/tests/object-storage/object-storage.test.ts +++ b/tests/object-storage/object-storage.test.ts @@ -11,10 +11,10 @@ describe('ObjectStorage', () => { it('should upload a file successfully', async () => { const fileName = 'test-file.txt'; const content = 'Hello, World!'; - + const result = await objectStorage.uploadFile(fileName, content, { contentType: 'text/plain', - metadata: { 'uploaded-by': 'test' } + metadata: { 'uploaded-by': 'test' }, }); expect(result.success).toBe(true); @@ -25,7 +25,7 @@ describe('ObjectStorage', () => { it('should handle upload failure gracefully', async () => { const fileName = ''; const content = 'Hello, World!'; - + const result = await objectStorage.uploadFile(fileName, content); expect(result.success).toBe(false); @@ -36,7 +36,7 @@ describe('ObjectStorage', () => { describe('downloadFile', () => { it('should download an existing file', async () => { const fileName = 'existing-file.txt'; - + const result = await objectStorage.downloadFile(fileName); expect(result.success).toBe(true); @@ -46,7 +46,7 @@ describe('ObjectStorage', () => { it('should handle file not found', async () => { const fileName = 'non-existent-file.txt'; - + const result = await objectStorage.downloadFile(fileName); expect(result.success).toBe(false); @@ -80,7 +80,7 @@ describe('ObjectStorage', () => { describe('deleteFile', () => { it('should delete an existing file', async () => { const fileName = 'file-to-delete.txt'; - + const result = await objectStorage.deleteFile(fileName); expect(result.success).toBe(true); @@ -88,7 +88,7 @@ describe('ObjectStorage', () => { it('should handle file not found during deletion', async () => { const fileName = 'non-existent-file.txt'; - + const result = await objectStorage.deleteFile(fileName); expect(result.success).toBe(false); @@ -99,7 +99,7 @@ describe('ObjectStorage', () => { describe('getFileInfo', () => { it('should return file information', async () => { const fileName = 'info-file.txt'; - + const result = await objectStorage.getFileInfo(fileName); expect(result.success).toBe(true); @@ -112,11 +112,11 @@ describe('ObjectStorage', () => { it('should handle file not found', async () => { const fileName = 'non-existent-file.txt'; - + const result = await objectStorage.getFileInfo(fileName); expect(result.success).toBe(false); expect(result.error).toBe('File not found'); }); }); -}); \ No newline at end of file +}); diff --git a/yarn.lock b/yarn.lock index 7341a8d..117ff86 100644 --- a/yarn.lock +++ b/yarn.lock @@ -509,7 +509,7 @@ resolved "https://registry.npmjs.org/@google-cloud/projectify/-/projectify-4.0.0.tgz" integrity sha512-MmaX6HeSvyPbWGwFq7mXdo0uQZLGBYCwziiLIGq5JVX+/bdI3SAq6bP98trV5eTWfLuvsMcIC1YJOF2vfteLFA== -"@google-cloud/promisify@^4.0.0": +"@google-cloud/promisify@<4.1.0", "@google-cloud/promisify@^4.0.0": version "4.0.0" resolved "https://registry.npmjs.org/@google-cloud/promisify/-/promisify-4.0.0.tgz" integrity sha512-Orxzlfb9c67A15cq2JQEyVc7wEsmFBmHjZWZYQMUyJ1qivXyMwdyNOs9odi79hze+2zqdTtu1E19IM/FtqZ10g== @@ -534,6 +534,27 @@ lodash.snakecase "^4.1.1" p-defer "^3.0.0" +"@google-cloud/storage@^7.7.0": + version "7.16.0" + resolved "https://registry.yarnpkg.com/@google-cloud/storage/-/storage-7.16.0.tgz#62c04ee4f80190992ef06cb033a90c054bcea575" + integrity sha512-7/5LRgykyOfQENcm6hDKP8SX/u9XxE5YOiWOkgkwcoO+cG8xT/cyOvp9wwN3IxfdYgpHs8CE7Nq2PKX2lNaEXw== + dependencies: + "@google-cloud/paginator" "^5.0.0" + "@google-cloud/projectify" "^4.0.0" + "@google-cloud/promisify" "<4.1.0" + abort-controller "^3.0.0" + async-retry "^1.3.3" + duplexify "^4.1.3" + fast-xml-parser "^4.4.1" + gaxios "^6.0.2" + google-auth-library "^9.6.3" + html-entities "^2.5.2" + mime "^3.0.0" + p-limit "^3.0.1" + retry-request "^7.0.0" + teeny-request "^9.0.0" + uuid "^8.0.0" + "@grpc/grpc-js@~1.10.3": version "1.10.8" resolved "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.10.8.tgz" @@ -1409,6 +1430,13 @@ ast-module-types@^5.0.0: resolved "https://registry.npmjs.org/ast-module-types/-/ast-module-types-5.0.0.tgz" integrity sha512-JvqziE0Wc0rXQfma0HZC/aY7URXHFuZV84fJRtP8u+lhp0JYCNd5wJzVXP45t0PH0Mej3ynlzvdyITYIu0G4LQ== +async-retry@^1.3.3: + version "1.3.3" + resolved "https://registry.yarnpkg.com/async-retry/-/async-retry-1.3.3.tgz#0e7f36c04d8478e7a58bdbed80cedf977785f280" + integrity sha512-wfr/jstw9xNi/0teMHrRW7dsz3Lt5ARhYNZ2ewpadnhaIp5mbALhOAP+EAdsC7t4Z6wqsDVv9+W6gm1Dk9mEyw== + dependencies: + retry "0.13.1" + asynckit@^0.4.0: version "0.4.0" resolved "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz" @@ -2084,7 +2112,7 @@ dot-prop@^5.1.0: dependencies: is-obj "^2.0.0" -duplexify@^4.0.0: +duplexify@^4.0.0, duplexify@^4.1.3: version "4.1.3" resolved "https://registry.npmjs.org/duplexify/-/duplexify-4.1.3.tgz" integrity sha512-M3BmBhwJRZsSx38lZyhE53Csddgzl5R7xGJNk7CVddZD6CcmwMCH8J+7AprIrQKH7TonKxaCjcv27Qmf+sQ+oA== @@ -2532,6 +2560,13 @@ fast-safe-stringify@^2.1.1: resolved "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz" integrity sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA== +fast-xml-parser@^4.4.1: + version "4.5.3" + resolved "https://registry.yarnpkg.com/fast-xml-parser/-/fast-xml-parser-4.5.3.tgz#c54d6b35aa0f23dc1ea60b6c884340c006dc6efb" + integrity sha512-RKihhV+SHsIUGXObeVy9AXiBbFwkVk7Syp8XgwN5U3JV416+Gwp/GO9i0JYKmikykgz/UHRrrV4ROuZEo/T0ig== + dependencies: + strnum "^1.1.1" + fastq@^1.6.0: version "1.15.0" resolved "https://registry.npmjs.org/fastq/-/fastq-1.15.0.tgz" @@ -2682,6 +2717,17 @@ gaxios@^6.0.0, gaxios@^6.1.1: node-fetch "^2.6.9" uuid "^9.0.1" +gaxios@^6.0.2: + version "6.7.1" + resolved "https://registry.yarnpkg.com/gaxios/-/gaxios-6.7.1.tgz#ebd9f7093ede3ba502685e73390248bb5b7f71fb" + integrity sha512-LDODD4TMYx7XXdpwxAVRAIAuB0bzv0s+ywFonY46k126qzQHT9ygyoa9tncmOiQmmDrik65UYsEkv3lbfqQ3yQ== + dependencies: + extend "^3.0.2" + https-proxy-agent "^7.0.1" + is-stream "^2.0.0" + node-fetch "^2.6.9" + uuid "^9.0.1" + gcp-metadata@^6.1.0: version "6.1.0" resolved "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-6.1.0.tgz" @@ -2871,6 +2917,18 @@ google-auth-library@^9.10.0, google-auth-library@^9.3.0: gtoken "^7.0.0" jws "^4.0.0" +google-auth-library@^9.6.3: + version "9.15.1" + resolved "https://registry.yarnpkg.com/google-auth-library/-/google-auth-library-9.15.1.tgz#0c5d84ed1890b2375f1cd74f03ac7b806b392928" + integrity sha512-Jb6Z0+nvECVz+2lzSMt9u98UsoakXxA2HGHMCxh+so3n90XgYWkq5dur19JAJV7ONiJY22yBTyJB1TSkvPq9Ng== + dependencies: + base64-js "^1.3.0" + ecdsa-sig-formatter "^1.0.11" + gaxios "^6.1.1" + gcp-metadata "^6.1.0" + gtoken "^7.0.0" + jws "^4.0.0" + google-gax@^4.3.1: version "4.3.3" resolved "https://registry.npmjs.org/google-gax/-/google-gax-4.3.3.tgz" @@ -3004,6 +3062,11 @@ html-encoding-sniffer@^2.0.1: dependencies: whatwg-encoding "^1.0.5" +html-entities@^2.5.2: + version "2.6.0" + resolved "https://registry.yarnpkg.com/html-entities/-/html-entities-2.6.0.tgz#7c64f1ea3b36818ccae3d3fb48b6974208e984f8" + integrity sha512-kig+rMn/QOVRvr7c86gQ8lWXq+Hkv6CbAH1hLu+RG338StTpE8Z0b44SDVaqVu7HGKf27frdmUYEs9hTUX/cLQ== + html-escaper@^2.0.0: version "2.0.2" resolved "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz" @@ -4217,6 +4280,11 @@ mime-types@^2.1.12: dependencies: mime-db "1.52.0" +mime@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/mime/-/mime-3.0.0.tgz#b374550dca3a0c18443b0c950a6a58f1931cf7a7" + integrity sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A== + mimic-fn@^2.1.0: version "2.1.0" resolved "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz" @@ -4472,9 +4540,9 @@ p-limit@^2.2.0: dependencies: p-try "^2.0.0" -p-limit@^3.0.2: +p-limit@^3.0.1, p-limit@^3.0.2: version "3.1.0" - resolved "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz" + resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-3.1.0.tgz#e1daccbe78d0d1388ca18c64fea38e3e57e3706b" integrity sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ== dependencies: yocto-queue "^0.1.0" @@ -4968,6 +5036,11 @@ retry-request@^7.0.0: extend "^3.0.2" teeny-request "^9.0.0" +retry@0.13.1: + version "0.13.1" + resolved "https://registry.yarnpkg.com/retry/-/retry-0.13.1.tgz#185b1587acf67919d63b357349e03537b2484658" + integrity sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg== + reusify@^1.0.4: version "1.0.4" resolved "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz" @@ -5286,6 +5359,11 @@ strip-json-comments@~2.0.1: resolved "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz" integrity sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ== +strnum@^1.1.1: + version "1.1.2" + resolved "https://registry.yarnpkg.com/strnum/-/strnum-1.1.2.tgz#57bca4fbaa6f271081715dbc9ed7cee5493e28e4" + integrity sha512-vrN+B7DBIoTTZjnPNewwhx6cBA/H+IS7rfW68n7XxC1y7uoiGQBxaKzqucGUgavX15dJgiGztLJ8vxuEzwqBdA== + stubs@^3.0.0: version "3.0.0" resolved "https://registry.npmjs.org/stubs/-/stubs-3.0.0.tgz" @@ -5654,6 +5732,11 @@ util-deprecate@^1.0.1: resolved "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz" integrity sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw== +uuid@^8.0.0: + version "8.3.2" + resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2" + integrity sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg== + uuid@^9.0.0, uuid@^9.0.1: version "9.0.1" resolved "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz" From 6c9811ca39ec13e85dcc19cda1f5ec6327a3fdd7 Mon Sep 17 00:00:00 2001 From: Daniel Vainshtein Date: Mon, 30 Jun 2025 14:44:25 +0200 Subject: [PATCH 03/17] test(sdk): fix tests --- tests/object-storage/object-storage.test.ts | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/tests/object-storage/object-storage.test.ts b/tests/object-storage/object-storage.test.ts index d3a6da4..c6abe2b 100644 --- a/tests/object-storage/object-storage.test.ts +++ b/tests/object-storage/object-storage.test.ts @@ -1,12 +1,24 @@ -import { ObjectStorage } from 'lib/object-storage'; +import { ObjectStorage } from '../../lib/object-storage'; describe('ObjectStorage', () => { let objectStorage: ObjectStorage; + const originalEnv = process.env; beforeEach(() => { + // Set up test environment + process.env = { + ...originalEnv, + OBJECT_STORAGE_BUCKET: 'test-bucket-object-storage', + }; + objectStorage = new ObjectStorage(); }); + afterEach(() => { + // Restore original environment + process.env = originalEnv; + }); + describe('uploadFile', () => { it('should upload a file successfully', async () => { const fileName = 'test-file.txt'; From 112837df7f8d6ac2ef02268eb7cfeb199a36437a Mon Sep 17 00:00:00 2001 From: Daniel Vainshtein Date: Mon, 30 Jun 2025 14:52:53 +0200 Subject: [PATCH 04/17] test(sdk): fix tests --- tests/object-storage/object-storage.test.ts | 85 +++++++++++++++++++++ 1 file changed, 85 insertions(+) diff --git a/tests/object-storage/object-storage.test.ts b/tests/object-storage/object-storage.test.ts index c6abe2b..2ec0e1a 100644 --- a/tests/object-storage/object-storage.test.ts +++ b/tests/object-storage/object-storage.test.ts @@ -1,5 +1,41 @@ import { ObjectStorage } from '../../lib/object-storage'; +// Mock Google Cloud Storage +const mockFile = { + save: jest.fn(), + exists: jest.fn(), + download: jest.fn(), + getMetadata: jest.fn(), + delete: jest.fn(), + name: 'test-file.txt', +}; + +const mockBucket = { + file: jest.fn((fileName: string) => ({ + ...mockFile, + name: fileName, + getMetadata: jest.fn().mockResolvedValue([ + { + name: fileName, + size: '100', + contentType: 'text/plain', + updated: '2023-01-01T00:00:00.000Z', + etag: 'test-etag', + metadata: { 'test-key': 'test-value' }, + }, + ]), + })), + getFiles: jest.fn(), +}; + +const mockStorage = { + bucket: jest.fn(() => mockBucket), +}; + +jest.mock('@google-cloud/storage', () => ({ + Storage: jest.fn(() => mockStorage), +})); + describe('ObjectStorage', () => { let objectStorage: ObjectStorage; const originalEnv = process.env; @@ -11,6 +47,43 @@ describe('ObjectStorage', () => { OBJECT_STORAGE_BUCKET: 'test-bucket-object-storage', }; + // Reset all mocks + jest.clearAllMocks(); + + // Set up default mock responses + mockFile.save.mockResolvedValue(undefined); + mockFile.exists.mockResolvedValue([true]); + mockFile.download.mockResolvedValue([Buffer.from('file content')]); + mockFile.getMetadata.mockResolvedValue([ + { + name: 'test-file.txt', + size: '100', + contentType: 'text/plain', + updated: '2023-01-01T00:00:00.000Z', + etag: 'test-etag', + metadata: { 'test-key': 'test-value' }, + }, + ]); + mockFile.delete.mockResolvedValue(undefined); + + mockBucket.getFiles.mockResolvedValue([ + [ + { + name: 'test-file.txt', + metadata: { + name: 'test-file.txt', + size: '100', + contentType: 'text/plain', + updated: '2023-01-01T00:00:00.000Z', + etag: 'test-etag', + metadata: { 'test-key': 'test-value' }, + }, + }, + ], + {}, + {}, + ]); + objectStorage = new ObjectStorage(); }); @@ -38,6 +111,9 @@ describe('ObjectStorage', () => { const fileName = ''; const content = 'Hello, World!'; + // Mock file save to throw an error + mockFile.save.mockRejectedValueOnce(new Error('Upload failed')); + const result = await objectStorage.uploadFile(fileName, content); expect(result.success).toBe(false); @@ -59,6 +135,9 @@ describe('ObjectStorage', () => { it('should handle file not found', async () => { const fileName = 'non-existent-file.txt'; + // Mock file doesn't exist + mockFile.exists.mockResolvedValueOnce([false]); + const result = await objectStorage.downloadFile(fileName); expect(result.success).toBe(false); @@ -101,6 +180,9 @@ describe('ObjectStorage', () => { it('should handle file not found during deletion', async () => { const fileName = 'non-existent-file.txt'; + // Mock file doesn't exist + mockFile.exists.mockResolvedValueOnce([false]); + const result = await objectStorage.deleteFile(fileName); expect(result.success).toBe(false); @@ -125,6 +207,9 @@ describe('ObjectStorage', () => { it('should handle file not found', async () => { const fileName = 'non-existent-file.txt'; + // Mock file doesn't exist + mockFile.exists.mockResolvedValueOnce([false]); + const result = await objectStorage.getFileInfo(fileName); expect(result.success).toBe(false); From a2ce539eba3b75edb530fc9f9a8b8a265afa4281 Mon Sep 17 00:00:00 2001 From: Daniel Vainshtein Date: Mon, 30 Jun 2025 14:55:08 +0200 Subject: [PATCH 05/17] test(sdk): new beta [beta] --- lib/minimal-package.ts | 2 +- package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/minimal-package.ts b/lib/minimal-package.ts index ef4bebe..ec64141 100644 --- a/lib/minimal-package.ts +++ b/lib/minimal-package.ts @@ -1 +1 @@ -export default { name: '@mondaycom/apps-sdk', version: '3.3.0-beta' }; +export default { name: '@mondaycom/apps-sdk', version: '3.3.0-beta.1' }; diff --git a/package.json b/package.json index 78fcaca..44ba1e8 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@mondaycom/apps-sdk", - "version": "3.3.0-beta", + "version": "3.3.0-beta.1", "description": "monday apps SDK for NodeJS", "main": "./dist/cjs/index.js", "module": "./dist/esm/index.js", From 377d9aa1f0c85170260fad3c341bbc7194252cb5 Mon Sep 17 00:00:00 2001 From: Daniel Vainshtein Date: Tue, 1 Jul 2025 11:24:47 +0200 Subject: [PATCH 06/17] test(sdk): export types [beta] --- lib/index.ts | 12 ++++++++++++ lib/minimal-package.ts | 2 +- lib/object-storage/index.ts | 14 +++++++++++++- package.json | 2 +- 4 files changed, 27 insertions(+), 3 deletions(-) diff --git a/lib/index.ts b/lib/index.ts index 38168d7..821ef5d 100644 --- a/lib/index.ts +++ b/lib/index.ts @@ -16,3 +16,15 @@ export { SecretsManager, Queue }; + +// Export ObjectStorage types +export type { + UploadFileOptions, + UploadFileResponse, + DownloadFileResponse, + DeleteFileResponse, + ListFilesOptions, + FileInfo, + ListFilesResponse, + GetFileInfoResponse +} from 'lib/object-storage'; diff --git a/lib/minimal-package.ts b/lib/minimal-package.ts index ec64141..512c079 100644 --- a/lib/minimal-package.ts +++ b/lib/minimal-package.ts @@ -1 +1 @@ -export default { name: '@mondaycom/apps-sdk', version: '3.3.0-beta.1' }; +export default { name: '@mondaycom/apps-sdk', version: '3.3.0-beta.2' }; diff --git a/lib/object-storage/index.ts b/lib/object-storage/index.ts index fb88f8c..cf36334 100644 --- a/lib/object-storage/index.ts +++ b/lib/object-storage/index.ts @@ -2,4 +2,16 @@ import { ObjectStorage } from './object-storage'; export { ObjectStorage -}; +}; + +// Export all ObjectStorage types +export type { + UploadFileOptions, + UploadFileResponse, + DownloadFileResponse, + DeleteFileResponse, + ListFilesOptions, + FileInfo, + ListFilesResponse, + GetFileInfoResponse +} from '../types/object-storage'; diff --git a/package.json b/package.json index 44ba1e8..a644222 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@mondaycom/apps-sdk", - "version": "3.3.0-beta.1", + "version": "3.3.0-beta.2", "description": "monday apps SDK for NodeJS", "main": "./dist/cjs/index.js", "module": "./dist/esm/index.js", From c1fe72fb71b096f2e6b3c611c53533af315ea933 Mon Sep 17 00:00:00 2001 From: Daniel Vainshtein Date: Tue, 8 Jul 2025 15:07:11 +0300 Subject: [PATCH 07/17] test(sdk): remove type exports --- lib/index.ts | 12 ------------ lib/minimal-package.ts | 2 +- lib/object-storage/index.ts | 14 +------------- lib/object-storage/object-storage.ts | 28 ++++++++++++++-------------- package.json | 2 +- 5 files changed, 17 insertions(+), 41 deletions(-) diff --git a/lib/index.ts b/lib/index.ts index 821ef5d..38168d7 100644 --- a/lib/index.ts +++ b/lib/index.ts @@ -16,15 +16,3 @@ export { SecretsManager, Queue }; - -// Export ObjectStorage types -export type { - UploadFileOptions, - UploadFileResponse, - DownloadFileResponse, - DeleteFileResponse, - ListFilesOptions, - FileInfo, - ListFilesResponse, - GetFileInfoResponse -} from 'lib/object-storage'; diff --git a/lib/minimal-package.ts b/lib/minimal-package.ts index 512c079..fa5d758 100644 --- a/lib/minimal-package.ts +++ b/lib/minimal-package.ts @@ -1 +1 @@ -export default { name: '@mondaycom/apps-sdk', version: '3.3.0-beta.2' }; +export default { name: '@mondaycom/apps-sdk', version: '3.3.0-beta.3' }; diff --git a/lib/object-storage/index.ts b/lib/object-storage/index.ts index cf36334..fb88f8c 100644 --- a/lib/object-storage/index.ts +++ b/lib/object-storage/index.ts @@ -2,16 +2,4 @@ import { ObjectStorage } from './object-storage'; export { ObjectStorage -}; - -// Export all ObjectStorage types -export type { - UploadFileOptions, - UploadFileResponse, - DownloadFileResponse, - DeleteFileResponse, - ListFilesOptions, - FileInfo, - ListFilesResponse, - GetFileInfoResponse -} from '../types/object-storage'; +}; diff --git a/lib/object-storage/object-storage.ts b/lib/object-storage/object-storage.ts index a0c50ca..fc0c8e4 100644 --- a/lib/object-storage/object-storage.ts +++ b/lib/object-storage/object-storage.ts @@ -1,4 +1,4 @@ -import { Storage } from '@google-cloud/storage'; +import { Bucket, File, Storage } from '@google-cloud/storage'; import { InternalServerError } from 'errors/apps-sdk-error'; import { @@ -34,8 +34,8 @@ export class ObjectStorage { */ async uploadFile(fileName: string, content: Buffer | string, options: UploadFileOptions = {}): Promise { try { - const bucket = this.storage.bucket(this.bucketName); - const file = bucket.file(fileName); + const bucket: Bucket = this.storage.bucket(this.bucketName); + const file: File = bucket.file(fileName); const uploadOptions = { metadata: { @@ -71,8 +71,8 @@ export class ObjectStorage { */ async downloadFile(fileName: string): Promise { try { - const bucket = this.storage.bucket(this.bucketName); - const file = bucket.file(fileName); + const bucket: Bucket = this.storage.bucket(this.bucketName); + const file: File = bucket.file(fileName); const [exists] = await file.exists(); if (!exists) { @@ -88,7 +88,7 @@ export class ObjectStorage { return { success: true, content, - contentType: metadata.contentType + contentType: metadata.contentType || 'application/octet-stream' }; } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); @@ -106,8 +106,8 @@ export class ObjectStorage { */ async deleteFile(fileName: string): Promise { try { - const bucket = this.storage.bucket(this.bucketName); - const file = bucket.file(fileName); + const bucket: Bucket = this.storage.bucket(this.bucketName); + const file: File = bucket.file(fileName); const [exists] = await file.exists(); if (!exists) { @@ -140,7 +140,7 @@ export class ObjectStorage { */ async listFiles(options: ListFilesOptions = {}): Promise { try { - const bucket = this.storage.bucket(this.bucketName); + const bucket: Bucket = this.storage.bucket(this.bucketName); const queryOptions = { maxResults: options.maxResults || 100, @@ -150,9 +150,9 @@ export class ObjectStorage { const [files, , apiResponse] = await bucket.getFiles(queryOptions); - const fileInfos: Array = files.map(file => ({ + const fileInfos: Array = files.map((file: File) => ({ name: file.name, - size: parseInt(file.metadata.size?.toString() || '0') || 0, + 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 || '', @@ -185,8 +185,8 @@ export class ObjectStorage { */ async getFileInfo(fileName: string): Promise { try { - const bucket = this.storage.bucket(this.bucketName); - const file = bucket.file(fileName); + const bucket: Bucket = this.storage.bucket(this.bucketName); + const file: File = bucket.file(fileName); const [exists] = await file.exists(); if (!exists) { @@ -200,7 +200,7 @@ export class ObjectStorage { const fileInfo: FileInfo = { name: file.name, - size: parseInt(metadata.size?.toString() || '0') || 0, + size: parseInt(String(metadata.size || '0'), 10) || 0, contentType: metadata.contentType || 'application/octet-stream', lastModified: new Date(metadata.updated || Date.now()), etag: metadata.etag || '', diff --git a/package.json b/package.json index a644222..31df6f9 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@mondaycom/apps-sdk", - "version": "3.3.0-beta.2", + "version": "3.3.0-beta.3", "description": "monday apps SDK for NodeJS", "main": "./dist/cjs/index.js", "module": "./dist/esm/index.js", From 34f89d16aff50a393c5cfebf3e2918ca8ddfdc43 Mon Sep 17 00:00:00 2001 From: Daniel Vainshtein Date: Tue, 8 Jul 2025 15:09:30 +0300 Subject: [PATCH 08/17] test(sdk): [beta] From 01d6628da6bf0b844611d823c63e732ebcf15dd5 Mon Sep 17 00:00:00 2001 From: Daniel Vainshtein Date: Mon, 21 Jul 2025 11:13:56 +0300 Subject: [PATCH 09/17] test(sdk): code review fixes [beta] --- lib/minimal-package.ts | 2 +- lib/object-storage/object-storage.ts | 60 +++++++----------- lib/types/object-storage.ts | 26 +++----- package.json | 2 +- tests/object-storage/object-storage.test.ts | 69 ++++++++++++++++++++- 5 files changed, 101 insertions(+), 58 deletions(-) diff --git a/lib/minimal-package.ts b/lib/minimal-package.ts index fa5d758..4ca00bc 100644 --- a/lib/minimal-package.ts +++ b/lib/minimal-package.ts @@ -1 +1 @@ -export default { name: '@mondaycom/apps-sdk', version: '3.3.0-beta.3' }; +export default { name: '@mondaycom/apps-sdk', version: '3.3.0-beta.4' }; diff --git a/lib/object-storage/object-storage.ts b/lib/object-storage/object-storage.ts index fc0c8e4..01cd7e3 100644 --- a/lib/object-storage/object-storage.ts +++ b/lib/object-storage/object-storage.ts @@ -29,12 +29,20 @@ export class ObjectStorage { logger.info(`ObjectStorage initialized with bucket: ${this.bucketName}`); } - /** - * Upload a file to the object storage bucket - */ + 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 }; + } + async uploadFile(fileName: string, content: Buffer | string, options: UploadFileOptions = {}): Promise { try { - const bucket: Bucket = this.storage.bucket(this.bucketName); + const bucket = this.getBucket(); const file: File = bucket.file(fileName); const uploadOptions = { @@ -56,9 +64,7 @@ export class ObjectStorage { fileUrl }; } catch (error) { - const errorMessage = error instanceof Error ? error.message : String(error); - const errorObj = error instanceof Error ? error : new Error(String(error)); - logger.error('Failed to upload file:', { error: errorObj }); + const { errorMessage } = this.handleError(error, 'upload file'); return { success: false, error: `Failed to upload file: ${errorMessage}` @@ -66,12 +72,9 @@ export class ObjectStorage { } } - /** - * Download a file from the object storage bucket - */ async downloadFile(fileName: string): Promise { try { - const bucket: Bucket = this.storage.bucket(this.bucketName); + const bucket = this.getBucket(); const file: File = bucket.file(fileName); const [exists] = await file.exists(); @@ -91,9 +94,7 @@ export class ObjectStorage { contentType: metadata.contentType || 'application/octet-stream' }; } catch (error) { - const errorMessage = error instanceof Error ? error.message : String(error); - const errorObj = error instanceof Error ? error : new Error(String(error)); - logger.error('Failed to download file:', { error: errorObj }); + const { errorMessage } = this.handleError(error, 'download file'); return { success: false, error: `Failed to download file: ${errorMessage}` @@ -101,12 +102,9 @@ export class ObjectStorage { } } - /** - * Delete a file from the object storage bucket - */ async deleteFile(fileName: string): Promise { try { - const bucket: Bucket = this.storage.bucket(this.bucketName); + const bucket = this.getBucket(); const file: File = bucket.file(fileName); const [exists] = await file.exists(); @@ -121,13 +119,9 @@ export class ObjectStorage { logger.info(`File deleted successfully: ${fileName}`); - return { - success: true - }; + return { success: true }; } catch (error) { - const errorMessage = error instanceof Error ? error.message : String(error); - const errorObj = error instanceof Error ? error : new Error(String(error)); - logger.error('Failed to delete file:', { error: errorObj }); + const { errorMessage } = this.handleError(error, 'delete file'); return { success: false, error: `Failed to delete file: ${errorMessage}` @@ -135,12 +129,9 @@ export class ObjectStorage { } } - /** - * List files in the object storage bucket - */ async listFiles(options: ListFilesOptions = {}): Promise { try { - const bucket: Bucket = this.storage.bucket(this.bucketName); + const bucket = this.getBucket(); const queryOptions = { maxResults: options.maxResults || 100, @@ -170,9 +161,7 @@ export class ObjectStorage { nextPageToken: (apiResponse as { nextPageToken?: string })?.nextPageToken }; } catch (error) { - const errorMessage = error instanceof Error ? error.message : String(error); - const errorObj = error instanceof Error ? error : new Error(String(error)); - logger.error('Failed to list files:', { error: errorObj }); + const { errorMessage } = this.handleError(error, 'list files'); return { success: false, error: `Failed to list files: ${errorMessage}` @@ -180,12 +169,9 @@ export class ObjectStorage { } } - /** - * Get information about a specific file - */ async getFileInfo(fileName: string): Promise { try { - const bucket: Bucket = this.storage.bucket(this.bucketName); + const bucket = this.getBucket(); const file: File = bucket.file(fileName); const [exists] = await file.exists(); @@ -217,9 +203,7 @@ export class ObjectStorage { fileInfo }; } catch (error) { - const errorMessage = error instanceof Error ? error.message : String(error); - const errorObj = error instanceof Error ? error : new Error(String(error)); - logger.error('Failed to get file info:', { error: errorObj }); + const { errorMessage } = this.handleError(error, 'get file info'); return { success: false, error: `Failed to get file info: ${errorMessage}` diff --git a/lib/types/object-storage.ts b/lib/types/object-storage.ts index 15caf70..c8bbd54 100644 --- a/lib/types/object-storage.ts +++ b/lib/types/object-storage.ts @@ -1,26 +1,24 @@ +export type BaseResponse = { + success: boolean; + error?: string; +} + export type UploadFileOptions = { contentType?: string; metadata?: Record; } -export type UploadFileResponse = { - success: boolean; +export type UploadFileResponse = BaseResponse & { fileName?: string; fileUrl?: string; - error?: string; } -export type DownloadFileResponse = { - success: boolean; +export type DownloadFileResponse = BaseResponse & { content?: Buffer; contentType?: string; - error?: string; } -export type DeleteFileResponse = { - success: boolean; - error?: string; -} +export type DeleteFileResponse = BaseResponse; export type ListFilesOptions = { prefix?: string; @@ -37,15 +35,11 @@ export type FileInfo = { metadata: Record; } -export type ListFilesResponse = { - success: boolean; +export type ListFilesResponse = BaseResponse & { files?: Array; nextPageToken?: string; - error?: string; } -export type GetFileInfoResponse = { - success: boolean; +export type GetFileInfoResponse = BaseResponse & { fileInfo?: FileInfo; - error?: string; } diff --git a/package.json b/package.json index 31df6f9..05e7f43 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@mondaycom/apps-sdk", - "version": "3.3.0-beta.3", + "version": "3.3.0-beta.4", "description": "monday apps SDK for NodeJS", "main": "./dist/cjs/index.js", "module": "./dist/esm/index.js", diff --git a/tests/object-storage/object-storage.test.ts b/tests/object-storage/object-storage.test.ts index 2ec0e1a..9a193af 100644 --- a/tests/object-storage/object-storage.test.ts +++ b/tests/object-storage/object-storage.test.ts @@ -151,20 +151,85 @@ describe('ObjectStorage', () => { expect(result.success).toBe(true); expect(Array.isArray(result.files)).toBe(true); + expect(mockBucket.getFiles).toHaveBeenCalledWith({ + maxResults: 100, // default value + }); }); it('should list files with prefix filter', async () => { - const result = await objectStorage.listFiles({ prefix: 'test-' }); + const prefix = 'test-'; + const result = await objectStorage.listFiles({ prefix }); expect(result.success).toBe(true); expect(Array.isArray(result.files)).toBe(true); + expect(mockBucket.getFiles).toHaveBeenCalledWith({ + maxResults: 100, + prefix: 'test-', + }); }); it('should list files with pagination', async () => { - const result = await objectStorage.listFiles({ maxResults: 10 }); + const maxResults = 10; + const result = await objectStorage.listFiles({ maxResults }); + + expect(result.success).toBe(true); + expect(Array.isArray(result.files)).toBe(true); + expect(mockBucket.getFiles).toHaveBeenCalledWith({ + maxResults: 10, + }); + }); + + it('should list files with all options', async () => { + const options = { + prefix: 'uploads/', + maxResults: 5, + pageToken: 'next-page-token', + }; + + const result = await objectStorage.listFiles(options); expect(result.success).toBe(true); expect(Array.isArray(result.files)).toBe(true); + expect(mockBucket.getFiles).toHaveBeenCalledWith({ + maxResults: 5, + prefix: 'uploads/', + pageToken: 'next-page-token', + }); + }); + + it('should handle next page token in response', async () => { + // Mock response with next page token + mockBucket.getFiles.mockResolvedValueOnce([ + [ + { + name: 'test-file.txt', + metadata: { + name: 'test-file.txt', + size: '100', + contentType: 'text/plain', + updated: '2023-01-01T00:00:00.000Z', + etag: 'test-etag', + metadata: { 'test-key': 'test-value' }, + }, + }, + ], + {}, + { nextPageToken: 'page-2-token' }, // API response with next page token + ]); + + const result = await objectStorage.listFiles({ maxResults: 1 }); + + expect(result.success).toBe(true); + expect(result.nextPageToken).toBe('page-2-token'); + }); + + it('should handle list files error', async () => { + mockBucket.getFiles.mockRejectedValueOnce(new Error('List failed')); + + const result = await objectStorage.listFiles(); + + expect(result.success).toBe(false); + expect(result.error).toContain('Failed to list files'); }); }); From 5f35043ff77a28ca1fa3e7e9ce1814136a6479ff Mon Sep 17 00:00:00 2001 From: Maor Barazani Date: Tue, 21 Oct 2025 23:36:23 +0300 Subject: [PATCH 10/17] add new method to get predefined url for upload --- lib/object-storage/object-storage.ts | 38 ++++++ lib/types/object-storage.ts | 10 ++ tests/object-storage/object-storage.test.ts | 136 ++++++++++++++++++++ 3 files changed, 184 insertions(+) diff --git a/lib/object-storage/object-storage.ts b/lib/object-storage/object-storage.ts index 01cd7e3..0fabde2 100644 --- a/lib/object-storage/object-storage.ts +++ b/lib/object-storage/object-storage.ts @@ -1,6 +1,7 @@ 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, @@ -8,6 +9,8 @@ import { GetFileInfoResponse, ListFilesOptions, ListFilesResponse, + PresignedUrlOptions, + PresignedUrlResponse, UploadFileOptions, UploadFileResponse } from 'types/object-storage'; @@ -210,4 +213,39 @@ export class ObjectStorage { }; } } + + async getPresignedUploadUrl(fileName: string, options: PresignedUrlOptions = {}): Promise { + 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}` + }; + } + } } diff --git a/lib/types/object-storage.ts b/lib/types/object-storage.ts index c8bbd54..f70d3f8 100644 --- a/lib/types/object-storage.ts +++ b/lib/types/object-storage.ts @@ -43,3 +43,13 @@ export type ListFilesResponse = BaseResponse & { export type GetFileInfoResponse = BaseResponse & { fileInfo?: FileInfo; } + +export type PresignedUrlOptions = { + expires?: Date; + contentType?: string; +} + +export type PresignedUrlResponse = BaseResponse & { + presignedUrl?: string; + fileName?: string; +} diff --git a/tests/object-storage/object-storage.test.ts b/tests/object-storage/object-storage.test.ts index 9a193af..b562bf7 100644 --- a/tests/object-storage/object-storage.test.ts +++ b/tests/object-storage/object-storage.test.ts @@ -7,6 +7,7 @@ const mockFile = { download: jest.fn(), getMetadata: jest.fn(), delete: jest.fn(), + getSignedUrl: jest.fn(), name: 'test-file.txt', }; @@ -65,6 +66,9 @@ describe('ObjectStorage', () => { }, ]); mockFile.delete.mockResolvedValue(undefined); + mockFile.getSignedUrl.mockResolvedValue([ + 'https://storage.googleapis.com/test-bucket/test-file.txt?signed-url-params', + ]); mockBucket.getFiles.mockResolvedValue([ [ @@ -281,4 +285,136 @@ describe('ObjectStorage', () => { expect(result.error).toBe('File not found'); }); }); + + describe('getPresignedUploadUrl', () => { + it('should generate a presigned upload URL successfully', async () => { + const fileName = 'upload-file.txt'; + const expectedUrl = 'https://storage.googleapis.com/test-bucket/upload-file.txt?signed-url-params'; + + mockFile.getSignedUrl.mockResolvedValueOnce([expectedUrl]); + + const result = await objectStorage.getPresignedUploadUrl(fileName); + + expect(result.success).toBe(true); + expect(result.presignedUrl).toBe(expectedUrl); + expect(result.fileName).toBe(fileName); + expect(mockFile.getSignedUrl).toHaveBeenCalledWith({ + version: 'v4', + action: 'write', + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + expires: expect.any(Date), + }); + }); + + it('should generate a presigned upload URL with custom expiration', async () => { + const fileName = 'upload-file.txt'; + const customExpires = new Date('2024-12-31T23:59:59Z'); + const expectedUrl = 'https://storage.googleapis.com/test-bucket/upload-file.txt?signed-url-params'; + + mockFile.getSignedUrl.mockResolvedValueOnce([expectedUrl]); + + const result = await objectStorage.getPresignedUploadUrl(fileName, { expires: customExpires }); + + expect(result.success).toBe(true); + expect(result.presignedUrl).toBe(expectedUrl); + expect(result.fileName).toBe(fileName); + expect(mockFile.getSignedUrl).toHaveBeenCalledWith({ + version: 'v4', + action: 'write', + expires: customExpires, + }); + }); + + it('should generate a presigned upload URL with content type restriction', async () => { + const fileName = 'upload-file.txt'; + const contentType = 'text/plain'; + const expectedUrl = 'https://storage.googleapis.com/test-bucket/upload-file.txt?signed-url-params'; + + mockFile.getSignedUrl.mockResolvedValueOnce([expectedUrl]); + + const result = await objectStorage.getPresignedUploadUrl(fileName, { contentType }); + + expect(result.success).toBe(true); + expect(result.presignedUrl).toBe(expectedUrl); + expect(result.fileName).toBe(fileName); + expect(mockFile.getSignedUrl).toHaveBeenCalledWith({ + version: 'v4', + action: 'write', + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + expires: expect.any(Date), + contentType: 'text/plain', + }); + }); + + it('should generate a presigned upload URL with all options', async () => { + const fileName = 'upload-file.txt'; + const customExpires = new Date('2024-12-31T23:59:59Z'); + const contentType = 'application/json'; + const expectedUrl = 'https://storage.googleapis.com/test-bucket/upload-file.txt?signed-url-params'; + + mockFile.getSignedUrl.mockResolvedValueOnce([expectedUrl]); + + const result = await objectStorage.getPresignedUploadUrl(fileName, { + expires: customExpires, + contentType, + }); + + expect(result.success).toBe(true); + expect(result.presignedUrl).toBe(expectedUrl); + expect(result.fileName).toBe(fileName); + expect(mockFile.getSignedUrl).toHaveBeenCalledWith({ + version: 'v4', + action: 'write', + expires: customExpires, + contentType: 'application/json', + }); + }); + + it('should use default expiration when no expires option is provided', async () => { + const fileName = 'upload-file.txt'; + const expectedUrl = 'https://storage.googleapis.com/test-bucket/upload-file.txt?signed-url-params'; + + mockFile.getSignedUrl.mockResolvedValueOnce([expectedUrl]); + + // Mock Date.now to have predictable test results + const mockNow = new Date('2023-01-01T12:00:00Z').getTime(); + const originalDateNow = Date.now; + Date.now = jest.fn(() => mockNow); + + const result = await objectStorage.getPresignedUploadUrl(fileName); + + expect(result.success).toBe(true); + expect(mockFile.getSignedUrl).toHaveBeenCalledWith({ + version: 'v4', + action: 'write', + expires: new Date(mockNow + 15 * 60 * 1000), // 15 minutes from mockNow + }); + + // Restore original Date.now + Date.now = originalDateNow; + }); + + it('should handle presigned URL generation failure', async () => { + const fileName = 'upload-file.txt'; + + mockFile.getSignedUrl.mockRejectedValueOnce(new Error('Signing failed')); + + const result = await objectStorage.getPresignedUploadUrl(fileName); + + expect(result.success).toBe(false); + expect(result.error).toContain('Failed to generate presigned upload URL'); + expect(result.presignedUrl).toBeUndefined(); + }); + + it('should handle empty file name gracefully', async () => { + const fileName = ''; + + mockFile.getSignedUrl.mockRejectedValueOnce(new Error('Invalid file name')); + + const result = await objectStorage.getPresignedUploadUrl(fileName); + + expect(result.success).toBe(false); + expect(result.error).toContain('Failed to generate presigned upload URL'); + }); + }); }); From a4fc1bddb5adf820ee5331836a8a6efca7884bcd Mon Sep 17 00:00:00 2001 From: Shahar Shaki Date: Tue, 16 Dec 2025 15:02:36 +0200 Subject: [PATCH 11/17] [beta] testing --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 05e7f43..24ec46d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@mondaycom/apps-sdk", - "version": "3.3.0-beta.4", + "version": "3.3.0-beta.5", "description": "monday apps SDK for NodeJS", "main": "./dist/cjs/index.js", "module": "./dist/esm/index.js", From 8b628fe955c7ce5ed32782c2d5c8f115c2632417 Mon Sep 17 00:00:00 2001 From: Shahar Shaki Date: Sun, 21 Dec 2025 14:36:09 +0200 Subject: [PATCH 12/17] feat: bump minor version --- lib/minimal-package.ts | 2 +- package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/minimal-package.ts b/lib/minimal-package.ts index 4ca00bc..dc7f895 100644 --- a/lib/minimal-package.ts +++ b/lib/minimal-package.ts @@ -1 +1 @@ -export default { name: '@mondaycom/apps-sdk', version: '3.3.0-beta.4' }; +export default { name: '@mondaycom/apps-sdk', version: '3.3.0' }; diff --git a/package.json b/package.json index 24ec46d..318bc41 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@mondaycom/apps-sdk", - "version": "3.3.0-beta.5", + "version": "3.3.0", "description": "monday apps SDK for NodeJS", "main": "./dist/cjs/index.js", "module": "./dist/esm/index.js", From d7cb9ae6f2ccf19de22f94068c49a7ba1351c658 Mon Sep 17 00:00:00 2001 From: Shahar Shaki Date: Sun, 21 Dec 2025 15:19:21 +0200 Subject: [PATCH 13/17] feat: add 50GB file size limit to object storage [beta] --- lib/minimal-package.ts | 2 +- lib/object-storage/object-storage.ts | 7 ++++- lib/types/object-storage.ts | 1 + package.json | 2 +- tests/object-storage/object-storage.test.ts | 34 +++++++++++++++++++++ 5 files changed, 43 insertions(+), 3 deletions(-) diff --git a/lib/minimal-package.ts b/lib/minimal-package.ts index dc7f895..905d9a4 100644 --- a/lib/minimal-package.ts +++ b/lib/minimal-package.ts @@ -1 +1 @@ -export default { name: '@mondaycom/apps-sdk', version: '3.3.0' }; +export default { name: '@mondaycom/apps-sdk', version: '3.3.0-beta.6' }; diff --git a/lib/object-storage/object-storage.ts b/lib/object-storage/object-storage.ts index 0fabde2..b24a056 100644 --- a/lib/object-storage/object-storage.ts +++ b/lib/object-storage/object-storage.ts @@ -222,13 +222,18 @@ export class ObjectStorage { const fifteenMinutesFromNow = new Date(Date.now() + TIME_IN_MILLISECOND.MINUTE * 15); const expires = options.expires || fifteenMinutesFromNow; + const maxFileSizeBytes = options.maxFileSizeBytes || (50 * 1024 * 1024 * 1024); + const signedUrlOptions = { version: 'v4' as const, action: 'write' as const, expires, ...(options.contentType && { contentType: options.contentType - }) + }), + extensionHeaders: { + 'x-goog-content-length-range': `0,${maxFileSizeBytes}` + } }; const [presignedUrl] = await file.getSignedUrl(signedUrlOptions); diff --git a/lib/types/object-storage.ts b/lib/types/object-storage.ts index f70d3f8..26cb2cd 100644 --- a/lib/types/object-storage.ts +++ b/lib/types/object-storage.ts @@ -47,6 +47,7 @@ export type GetFileInfoResponse = BaseResponse & { export type PresignedUrlOptions = { expires?: Date; contentType?: string; + maxFileSizeBytes?: number; } export type PresignedUrlResponse = BaseResponse & { diff --git a/package.json b/package.json index 318bc41..6b41209 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@mondaycom/apps-sdk", - "version": "3.3.0", + "version": "3.3.0-beta.6", "description": "monday apps SDK for NodeJS", "main": "./dist/cjs/index.js", "module": "./dist/esm/index.js", diff --git a/tests/object-storage/object-storage.test.ts b/tests/object-storage/object-storage.test.ts index b562bf7..9191b9d 100644 --- a/tests/object-storage/object-storage.test.ts +++ b/tests/object-storage/object-storage.test.ts @@ -303,6 +303,9 @@ describe('ObjectStorage', () => { action: 'write', // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment expires: expect.any(Date), + extensionHeaders: { + 'x-goog-content-length-range': '0,53687091200', // 50 GB default limit + }, }); }); @@ -322,6 +325,9 @@ describe('ObjectStorage', () => { version: 'v4', action: 'write', expires: customExpires, + extensionHeaders: { + 'x-goog-content-length-range': '0,53687091200', // 50 GB default limit + }, }); }); @@ -343,6 +349,9 @@ describe('ObjectStorage', () => { // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment expires: expect.any(Date), contentType: 'text/plain', + extensionHeaders: { + 'x-goog-content-length-range': '0,53687091200', // 50 GB default limit + }, }); }); @@ -367,6 +376,9 @@ describe('ObjectStorage', () => { action: 'write', expires: customExpires, contentType: 'application/json', + extensionHeaders: { + 'x-goog-content-length-range': '0,53687091200', // 50 GB default limit + }, }); }); @@ -388,6 +400,9 @@ describe('ObjectStorage', () => { version: 'v4', action: 'write', expires: new Date(mockNow + 15 * 60 * 1000), // 15 minutes from mockNow + extensionHeaders: { + 'x-goog-content-length-range': '0,53687091200', // 50 GB default limit + }, }); // Restore original Date.now @@ -416,5 +431,24 @@ describe('ObjectStorage', () => { expect(result.success).toBe(false); expect(result.error).toContain('Failed to generate presigned upload URL'); }); + + it('should enforce 50 GB max file size limit', async () => { + const fileName = 'large-file.bin'; + const expectedUrl = 'https://storage.googleapis.com/test-bucket/large-file.bin?signed-url-params'; + const fiftyGBInBytes = 50 * 1024 * 1024 * 1024; // 53,687,091,200 bytes + + mockFile.getSignedUrl.mockResolvedValueOnce([expectedUrl]); + + const result = await objectStorage.getPresignedUploadUrl(fileName); + + expect(result.success).toBe(true); + expect(mockFile.getSignedUrl).toHaveBeenCalledWith( + expect.objectContaining({ + extensionHeaders: { + 'x-goog-content-length-range': `0,${fiftyGBInBytes}`, + }, + }), + ); + }); }); }); From 6a0f647378ddb44c9fc1e3df2ed1bd5d35230ff4 Mon Sep 17 00:00:00 2001 From: Shahar Shaki Date: Sun, 21 Dec 2025 15:43:47 +0200 Subject: [PATCH 14/17] feat: limit to 50 gb uploading to bucket + bump minor --- lib/minimal-package.ts | 2 +- package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/minimal-package.ts b/lib/minimal-package.ts index 905d9a4..dc7f895 100644 --- a/lib/minimal-package.ts +++ b/lib/minimal-package.ts @@ -1 +1 @@ -export default { name: '@mondaycom/apps-sdk', version: '3.3.0-beta.6' }; +export default { name: '@mondaycom/apps-sdk', version: '3.3.0' }; diff --git a/package.json b/package.json index 6b41209..318bc41 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@mondaycom/apps-sdk", - "version": "3.3.0-beta.6", + "version": "3.3.0", "description": "monday apps SDK for NodeJS", "main": "./dist/cjs/index.js", "module": "./dist/esm/index.js", From e2c5beace854b3ee4ba343a0f3c18c6aad1e29ec Mon Sep 17 00:00:00 2001 From: Shahar Shaki Date: Mon, 22 Dec 2025 16:17:17 +0200 Subject: [PATCH 15/17] feat: limit upload of specific file to 50mb [beta] --- lib/minimal-package.ts | 2 +- lib/object-storage/object-storage.ts | 2 +- package.json | 2 +- tests/object-storage/object-storage.test.ts | 16 ++++++++-------- 4 files changed, 11 insertions(+), 11 deletions(-) diff --git a/lib/minimal-package.ts b/lib/minimal-package.ts index dc7f895..693d4b4 100644 --- a/lib/minimal-package.ts +++ b/lib/minimal-package.ts @@ -1 +1 @@ -export default { name: '@mondaycom/apps-sdk', version: '3.3.0' }; +export default { name: '@mondaycom/apps-sdk', version: '3.3.0-beta.7' }; diff --git a/lib/object-storage/object-storage.ts b/lib/object-storage/object-storage.ts index b24a056..e878a58 100644 --- a/lib/object-storage/object-storage.ts +++ b/lib/object-storage/object-storage.ts @@ -222,7 +222,7 @@ export class ObjectStorage { const fifteenMinutesFromNow = new Date(Date.now() + TIME_IN_MILLISECOND.MINUTE * 15); const expires = options.expires || fifteenMinutesFromNow; - const maxFileSizeBytes = options.maxFileSizeBytes || (50 * 1024 * 1024 * 1024); + const maxFileSizeBytes = options.maxFileSizeBytes || (50 * 1024 * 1024); const signedUrlOptions = { version: 'v4' as const, diff --git a/package.json b/package.json index 318bc41..f9de752 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@mondaycom/apps-sdk", - "version": "3.3.0", + "version": "3.3.0-beta.7", "description": "monday apps SDK for NodeJS", "main": "./dist/cjs/index.js", "module": "./dist/esm/index.js", diff --git a/tests/object-storage/object-storage.test.ts b/tests/object-storage/object-storage.test.ts index 9191b9d..b868871 100644 --- a/tests/object-storage/object-storage.test.ts +++ b/tests/object-storage/object-storage.test.ts @@ -304,7 +304,7 @@ describe('ObjectStorage', () => { // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment expires: expect.any(Date), extensionHeaders: { - 'x-goog-content-length-range': '0,53687091200', // 50 GB default limit + 'x-goog-content-length-range': '0,52428800', // 50 MB default limit }, }); }); @@ -326,7 +326,7 @@ describe('ObjectStorage', () => { action: 'write', expires: customExpires, extensionHeaders: { - 'x-goog-content-length-range': '0,53687091200', // 50 GB default limit + 'x-goog-content-length-range': '0,52428800', // 50 MB default limit }, }); }); @@ -350,7 +350,7 @@ describe('ObjectStorage', () => { expires: expect.any(Date), contentType: 'text/plain', extensionHeaders: { - 'x-goog-content-length-range': '0,53687091200', // 50 GB default limit + 'x-goog-content-length-range': '0,52428800', // 50 MB default limit }, }); }); @@ -377,7 +377,7 @@ describe('ObjectStorage', () => { expires: customExpires, contentType: 'application/json', extensionHeaders: { - 'x-goog-content-length-range': '0,53687091200', // 50 GB default limit + 'x-goog-content-length-range': '0,52428800', // 50 MB default limit }, }); }); @@ -401,7 +401,7 @@ describe('ObjectStorage', () => { action: 'write', expires: new Date(mockNow + 15 * 60 * 1000), // 15 minutes from mockNow extensionHeaders: { - 'x-goog-content-length-range': '0,53687091200', // 50 GB default limit + 'x-goog-content-length-range': '0,52428800', // 50 MB default limit }, }); @@ -432,10 +432,10 @@ describe('ObjectStorage', () => { expect(result.error).toContain('Failed to generate presigned upload URL'); }); - it('should enforce 50 GB max file size limit', async () => { + it('should enforce 50 MB max file size limit', async () => { const fileName = 'large-file.bin'; const expectedUrl = 'https://storage.googleapis.com/test-bucket/large-file.bin?signed-url-params'; - const fiftyGBInBytes = 50 * 1024 * 1024 * 1024; // 53,687,091,200 bytes + const fiftyMBInBytes = 50 * 1024 * 1024; // 52,428,800 bytes mockFile.getSignedUrl.mockResolvedValueOnce([expectedUrl]); @@ -445,7 +445,7 @@ describe('ObjectStorage', () => { expect(mockFile.getSignedUrl).toHaveBeenCalledWith( expect.objectContaining({ extensionHeaders: { - 'x-goog-content-length-range': `0,${fiftyGBInBytes}`, + 'x-goog-content-length-range': `0,${fiftyMBInBytes}`, }, }), ); From 089245d1adc717dc6b136184294a2ee51cfe3c2f Mon Sep 17 00:00:00 2001 From: Shahar Shaki Date: Thu, 5 Feb 2026 16:02:11 +0200 Subject: [PATCH 16/17] chore: bump minor version --- lib/minimal-package.ts | 2 +- package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/minimal-package.ts b/lib/minimal-package.ts index 693d4b4..94d89be 100644 --- a/lib/minimal-package.ts +++ b/lib/minimal-package.ts @@ -1 +1 @@ -export default { name: '@mondaycom/apps-sdk', version: '3.3.0-beta.7' }; +export default { name: '@mondaycom/apps-sdk', version: '3.3.1' }; diff --git a/package.json b/package.json index f9de752..ebe5281 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@mondaycom/apps-sdk", - "version": "3.3.0-beta.7", + "version": "3.3.1", "description": "monday apps SDK for NodeJS", "main": "./dist/cjs/index.js", "module": "./dist/esm/index.js", From 78278ed2217454f85cfeabd648d1f0744ced8830 Mon Sep 17 00:00:00 2001 From: Shahar Shaki Date: Sun, 8 Feb 2026 10:42:03 +0200 Subject: [PATCH 17/17] chore: bump minor version Co-authored-by: Cursor --- lib/minimal-package.ts | 2 +- package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/minimal-package.ts b/lib/minimal-package.ts index 94d89be..dc7f895 100644 --- a/lib/minimal-package.ts +++ b/lib/minimal-package.ts @@ -1 +1 @@ -export default { name: '@mondaycom/apps-sdk', version: '3.3.1' }; +export default { name: '@mondaycom/apps-sdk', version: '3.3.0' }; diff --git a/package.json b/package.json index ebe5281..318bc41 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@mondaycom/apps-sdk", - "version": "3.3.1", + "version": "3.3.0", "description": "monday apps SDK for NodeJS", "main": "./dist/cjs/index.js", "module": "./dist/esm/index.js",