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/minimal-package.ts b/lib/minimal-package.ts index cf4f033..693d4b4 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.7' }; diff --git a/lib/object-storage/index.ts b/lib/object-storage/index.ts new file mode 100644 index 0000000..fb88f8c --- /dev/null +++ b/lib/object-storage/index.ts @@ -0,0 +1,5 @@ +import { ObjectStorage } from './object-storage'; + +export { + ObjectStorage +}; diff --git a/lib/object-storage/object-storage.ts b/lib/object-storage/object-storage.ts new file mode 100644 index 0000000..e878a58 --- /dev/null +++ b/lib/object-storage/object-storage.ts @@ -0,0 +1,256 @@ +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 }; + } + + async uploadFile(fileName: string, content: Buffer | string, options: UploadFileOptions = {}): Promise { + 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 { + 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 { + 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 { + 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 = 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 { + 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 { + 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 maxFileSizeBytes = options.maxFileSizeBytes || (50 * 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); + + 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 new file mode 100644 index 0000000..26cb2cd --- /dev/null +++ b/lib/types/object-storage.ts @@ -0,0 +1,56 @@ +export type BaseResponse = { + success: boolean; + error?: string; +} + +export type UploadFileOptions = { + contentType?: string; + metadata?: Record; +} + +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; +} + +export type ListFilesResponse = BaseResponse & { + files?: Array; + nextPageToken?: string; +} + +export type GetFileInfoResponse = BaseResponse & { + fileInfo?: FileInfo; +} + +export type PresignedUrlOptions = { + expires?: Date; + contentType?: string; + maxFileSizeBytes?: number; +} + +export type PresignedUrlResponse = BaseResponse & { + presignedUrl?: string; + fileName?: string; +} diff --git a/package.json b/package.json index 73b5975..f9de752 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@mondaycom/apps-sdk", - "version": "3.2.1", + "version": "3.3.0-beta.7", "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..b868871 --- /dev/null +++ b/tests/object-storage/object-storage.test.ts @@ -0,0 +1,454 @@ +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(), + getSignedUrl: 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; + + beforeEach(() => { + // Set up test environment + process.env = { + ...originalEnv, + 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); + mockFile.getSignedUrl.mockResolvedValue([ + 'https://storage.googleapis.com/test-bucket/test-file.txt?signed-url-params', + ]); + + 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(); + }); + + afterEach(() => { + // Restore original environment + process.env = originalEnv; + }); + + 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!'; + + // 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); + 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'; + + // Mock file doesn't exist + mockFile.exists.mockResolvedValueOnce([false]); + + 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); + expect(mockBucket.getFiles).toHaveBeenCalledWith({ + maxResults: 100, // default value + }); + }); + + it('should list files with prefix filter', async () => { + 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 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'); + }); + }); + + 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'; + + // Mock file doesn't exist + mockFile.exists.mockResolvedValueOnce([false]); + + 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'; + + // Mock file doesn't exist + mockFile.exists.mockResolvedValueOnce([false]); + + const result = await objectStorage.getFileInfo(fileName); + + expect(result.success).toBe(false); + 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), + extensionHeaders: { + 'x-goog-content-length-range': '0,52428800', // 50 MB default limit + }, + }); + }); + + 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, + extensionHeaders: { + 'x-goog-content-length-range': '0,52428800', // 50 MB default limit + }, + }); + }); + + 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', + extensionHeaders: { + 'x-goog-content-length-range': '0,52428800', // 50 MB default limit + }, + }); + }); + + 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', + extensionHeaders: { + 'x-goog-content-length-range': '0,52428800', // 50 MB default limit + }, + }); + }); + + 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 + extensionHeaders: { + 'x-goog-content-length-range': '0,52428800', // 50 MB default limit + }, + }); + + // 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'); + }); + + 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 fiftyMBInBytes = 50 * 1024 * 1024; // 52,428,800 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,${fiftyMBInBytes}`, + }, + }), + ); + }); + }); +}); 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"