diff --git a/package.json b/package.json index 34b43f3898..862792c964 100644 --- a/package.json +++ b/package.json @@ -58,7 +58,7 @@ "build:i18n": "node scripts/buildTranslations.js", "build:npm": "npm-run-all clean build:i18n build:prod:npm build:prod:es", "build:prod:analyze": "BUNDLE_ANALYSIS=true npm-run-all setup build:prod:npm", - "build:prod:dist": "NODE_ENV=production webpack --config scripts/webpack.config.js --mode production", + "build:prod:dist": "NODE_ENV=production webpack --config scripts/webpack.config.js --mode production --progress", "build:prod:es": "NODE_ENV=production BABEL_ENV=npm yarn build:es --source-maps --ignore \"**/*.d.ts,**/__tests__/**,**/__mocks__/**\"", "build:prod:npm": "BABEL_ENV=production OUTPUT=dist LANGUAGE=en-US REACT=true yarn build:prod:dist", "build:prod:storybook": "NODE_ENV=production BABEL_ENV=production BROWSERSLIST_ENV=production LANGUAGE=en-US REACT=true storybook build -o storybook", @@ -107,6 +107,9 @@ "commit-msg": "commitlint -e" } }, + "dependencies": { + "box-typescript-sdk-gen": "^1.17.1" + }, "devDependencies": { "@babel/cli": "^7.24.7", "@babel/core": "^7.24.7", @@ -136,7 +139,7 @@ "@box/languages": "^1.0.0", "@box/metadata-editor": "^0.122.12", "@box/metadata-filter": "^1.19.2", - "@box/metadata-view": "^0.48.1", + "@box/metadata-view": "^0.41.2", "@box/react-virtualized": "^9.22.3-rc-box.10", "@box/types": "^0.2.1", "@cfaester/enzyme-adapter-react-18": "^0.8.0", @@ -303,7 +306,7 @@ "@box/item-icon": "^0.17.15", "@box/metadata-editor": "^0.122.12", "@box/metadata-filter": "^1.19.2", - "@box/metadata-view": "^0.48.1", + "@box/metadata-view": "^0.41.2", "@box/react-virtualized": "^9.22.3-rc-box.10", "@box/types": "^0.2.1", "@hapi/address": "^2.1.4", diff --git a/scripts/jest/jest-setup.ts b/scripts/jest/jest-setup.ts index a6f4614737..70977348a5 100644 --- a/scripts/jest/jest-setup.ts +++ b/scripts/jest/jest-setup.ts @@ -24,6 +24,10 @@ Object.defineProperty(global, 'TextEncoder', { value: util.TextEncoder, }); +Object.defineProperty(global, 'TextDecoder', { + value: util.TextDecoder, +}); + global.ResizeObserver = jest.fn().mockImplementation(() => ({ observe: jest.fn(), unobserve: jest.fn(), diff --git a/scripts/jest/jest.config.js b/scripts/jest/jest.config.js index c558824b13..a210d90e21 100644 --- a/scripts/jest/jest.config.js +++ b/scripts/jest/jest.config.js @@ -26,6 +26,6 @@ module.exports = { testMatch: ['**/__tests__/**/*.test.+(js|jsx|ts|tsx)'], testPathIgnorePatterns: ['stories.test.js$', 'stories.test.tsx$', 'stories.test.d.ts'], transformIgnorePatterns: [ - 'node_modules/(?!(@box/react-virtualized/dist/es|@box/cldr-data|@box/blueprint-web|@box/blueprint-web-assets|@box/metadata-editor|@box/box-ai-content-answers|@box/box-ai-agent-selector|@box/item-icon|@box/combobox-with-api|@box/tree|@box/metadata-filter|@box/metadata-view|@box/types|@box/box-item-type-selector)/)', + 'node_modules/(?!(@box/react-virtualized/dist/es|@box/cldr-data|@box/blueprint-web|@box/blueprint-web-assets|@box/metadata-editor|@box/box-ai-content-answers|@box/box-ai-agent-selector|@box/item-icon|@box/combobox-with-api|@box/tree|@box/metadata-filter|@box/metadata-view|@box/types|@box/box-item-type-selector|jose)/)', ], }; diff --git a/src/api/APIFactory.js b/src/api/APIFactory.js index e2a3897494..f6d0b175e7 100644 --- a/src/api/APIFactory.js +++ b/src/api/APIFactory.js @@ -36,6 +36,8 @@ import OpenWithAPI from './OpenWith'; import MetadataQueryAPI from './MetadataQuery'; import BoxEditAPI from './box-edit'; import IntelligenceAPI from './Intelligence'; +// $FlowFixMe +import ZipDownloadAPI from './ZipDownload'; import { DEFAULT_HOSTNAME_API, DEFAULT_HOSTNAME_UPLOAD, TYPE_FOLDER, TYPE_FILE, TYPE_WEBLINK } from '../constants'; import type { ItemType } from '../common/types/core'; import type { APIOptions } from '../common/types/api'; @@ -204,6 +206,8 @@ class APIFactory { */ intelligenceAPI: IntelligenceAPI; + zipDownloadAPI: ZipDownloadAPI; + /** * [constructor] * @@ -857,6 +861,11 @@ class APIFactory { this.intelligenceAPI = new IntelligenceAPI(this.options); return this.intelligenceAPI; } + + getZipDownloadAPI(): ZipDownloadAPI { + this.zipDownloadAPI = new ZipDownloadAPI(this.options); + return this.zipDownloadAPI; + } } export default APIFactory; diff --git a/src/api/Metadata.js b/src/api/Metadata.js index ddc567aaa4..f26fc9c4c7 100644 --- a/src/api/Metadata.js +++ b/src/api/Metadata.js @@ -16,7 +16,7 @@ import partition from 'lodash/partition'; import uniq from 'lodash/uniq'; import uniqueId from 'lodash/uniqueId'; import { getBadItemError, getBadPermissionsError, isUserCorrectableError } from '../utils/error'; -import { getTypedFileId, getTypedFolderId } from '../utils/file'; +import { getTypedFileId } from '../utils/file'; import { handleOnAbort, formatMetadataFieldValue } from './utils'; import File from './File'; import { @@ -90,16 +90,6 @@ class Metadata extends File { return `${this.getMetadataCacheKey(id)}_classification`; } - /** - * Creates a key for the metadata template schema cache - * - * @param {string} templateKey - template key - * @return {string} key - */ - getMetadataTemplateSchemaCacheKey(templateKey: string): string { - return `${CACHE_PREFIX_METADATA}template_schema_${templateKey}`; - } - /** * API URL for metadata * @@ -115,21 +105,6 @@ class Metadata extends File { return baseUrl; } - /** - * API URL for metadata - * - * @param {string} id - a Box folder id - * @param {string} field - metadata field - * @return {string} base url for files - */ - getMetadataUrlForFolder(id: string, scope?: string, template?: string): string { - const baseUrl = `${this.getBaseApiUrl()}/folders/${id}/metadata`; - if (scope && template) { - return `${baseUrl}/${scope}/${template}`; - } - return baseUrl; - } - /** * API URL for metadata templates for a scope * @@ -362,23 +337,9 @@ class Metadata extends File { * @param {string} templateKey - template key * @return {Promise} Promise object of metadata template */ - async getSchemaByTemplateKey(templateKey: string): Promise { - const cache: APICache = this.getCache(); - const key = this.getMetadataTemplateSchemaCacheKey(templateKey); - - // Return cached value if it exists - if (cache.has(key)) { - return cache.get(key); - } - - // Fetch from API if not cached + getSchemaByTemplateKey(templateKey: string): Promise { const url = this.getMetadataTemplateSchemaUrl(templateKey); - const response = await this.xhr.get({ url }); - - // Cache the response - cache.set(key, response); - - return response; + return this.xhr.get({ url }); } /** @@ -825,33 +786,27 @@ class Metadata extends File { } /** - * API for patching metadata on item (file/folder) + * API for patching metadata on file * - * @param {BoxItem} item - File/Folder object for which we are changing the description + * @param {BoxItem} file - File object for which we are changing the description * @param {Object} template - Metadata template * @param {Array} operations - Array of JSON patch operations * @param {Function} successCallback - Success callback * @param {Function} errorCallback - Error callback - * @param {boolean} suppressCallbacks - Boolean to decide whether suppress callbacks or not * @return {Promise} */ async updateMetadata( - item: BoxItem, + file: BoxItem, template: MetadataTemplate, operations: JSONPatchOperations, successCallback: Function, errorCallback: ElementsErrorCallback, - suppressCallbacks?: boolean, ): Promise { this.errorCode = ERROR_CODE_UPDATE_METADATA; - if (!suppressCallbacks) { - // Only set callbacks when we intend to invoke them for this call - // so that callers performing bulk operations can suppress per-item callbacks - this.successCallback = successCallback; - this.errorCallback = errorCallback; - } + this.successCallback = successCallback; + this.errorCallback = errorCallback; - const { id, permissions, type } = item; + const { id, permissions } = file; if (!id || !permissions) { this.errorHandler(getBadItemError()); return; @@ -866,14 +821,11 @@ class Metadata extends File { try { const metadata = await this.xhr.put({ - url: - type === 'file' - ? this.getMetadataUrl(id, template.scope, template.templateKey) - : this.getMetadataUrlForFolder(id, template.scope, template.templateKey), + url: this.getMetadataUrl(id, template.scope, template.templateKey), headers: { [HEADER_CONTENT_TYPE]: 'application/json-patch+json', }, - id: type === 'file' ? getTypedFileId(id) : getTypedFolderId(id), + id: getTypedFileId(id), data: operations, }); if (!this.isDestroyed()) { @@ -888,63 +840,13 @@ class Metadata extends File { editor, ); } - if (!suppressCallbacks) { - this.successHandler(editor); - } + this.successHandler(editor); } } catch (e) { - if (suppressCallbacks) { - // Let the caller decide how to handle errors (e.g., aggregate for bulk operations) - throw e; - } this.errorHandler(e); } } - /** - * API for bulk patching metadata on items (file/folder) - * - * @param {BoxItem[]} items - File/Folder object for which we are changing the description - * @param {Object} template - Metadata template - * @param {Array} operations - Array of JSON patch operations for each item - * @param {Function} successCallback - Success callback - * @param {Function} errorCallback - Error callback - * @return {Promise} - */ - async bulkUpdateMetadata( - items: BoxItem[], - template: MetadataTemplate, - operations: JSONPatchOperations[], - successCallback: Function, - errorCallback: ElementsErrorCallback, - ): Promise { - this.errorCode = ERROR_CODE_UPDATE_METADATA; - this.successCallback = successCallback; - this.errorCallback = errorCallback; - - try { - const updatePromises = items.map(async (item, index) => { - try { - // Suppress per-item callbacks; aggregate outcome at the bulk level only - await this.updateMetadata(item, template, operations[index], successCallback, errorCallback, true); - } catch (e) { - // Re-throw to be caught by Promise.all and handled once below - throw new Error(`Failed to update metadata: ${e.message || e}`); - } - }); - - await Promise.all(updatePromises); - - if (!this.isDestroyed()) { - this.successHandler(); - } - } catch (e) { - if (!this.isDestroyed()) { - this.errorHandler(e); - } - } - } - /** * API for patching metadata on file * diff --git a/src/api/ZipDownload.ts b/src/api/ZipDownload.ts new file mode 100644 index 0000000000..c79e6c23a1 --- /dev/null +++ b/src/api/ZipDownload.ts @@ -0,0 +1,104 @@ +/** + * @file ZipDownload class for creating and downloading ZIP archives using Box TypeScript SDK + * @author Box + */ + +import { BoxClient, BoxDeveloperTokenAuth } from 'box-typescript-sdk-gen'; +import { ZipDownloadRequest } from 'box-typescript-sdk-gen/lib/schemas/zipDownloadRequest.generated.d.ts.js'; + +export interface ZipDownloadItem { + id: string; + type: 'file' | 'folder'; +} + +export interface ZipDownloadOptions { + token: string; + downloadFileName?: string; +} + +export interface ZipDownloadResponse { + downloadUrl?: string; + statusUrl?: string; + expiresAt?: string; + state?: 'in_progress' | 'failed' | 'succeeded'; + totalCount?: number; + downloadedCount?: number; + skippedCount?: number; +} + +/** + * ZipDownload class for creating and downloading ZIP archives from Box items + * Uses the box-typescript-sdk-gen for modern TypeScript support + */ +export default class ZipDownloadAPI { + private client: BoxClient; + + private options: ZipDownloadOptions; + + /** + * Constructor + * @param options - Configuration options including auth token + */ + constructor(options: ZipDownloadOptions) { + this.options = options; + + // Initialize Box client with developer token authentication + const auth = new BoxDeveloperTokenAuth({ token: options.token }); + this.client = new BoxClient({ + auth, + }); + } + + /** + * Create a ZIP download request and initiate the download + * @param items - Array of file and folder items to include in ZIP + * @returns Promise resolving to the ZIP download response + */ + async createZipDownload(items: ZipDownloadItem[]): Promise { + if (!items || items.length === 0) { + throw new Error('Items array cannot be empty'); + } + + // Create the ZIP download request + const zipRequest: ZipDownloadRequest = { + items, + downloadFileName: this.options.downloadFileName, + }; + + try { + // Create the ZIP download using the Box SDK + const zipDownload = await this.client.zipDownloads.createZipDownload(zipRequest); + + // Only download if we have a download URL + if (zipDownload.downloadUrl) { + this.downloadZipFile(zipDownload.downloadUrl); + } + + return zipDownload as unknown as ZipDownloadResponse; + } catch (error: unknown) { + const errorMessage = error instanceof Error ? error.message : String(error); + throw new Error(`Failed to create ZIP download: ${errorMessage}`); + } + } + + /** + * Download the ZIP file bytestream to the user's device using window.open + * @param url - The URL of the ZIP file to download + */ + private downloadZipFile(url: string): void { + try { + // Open in new tab - user can save from there + window.open(url, '_blank', 'noopener,noreferrer'); + + window.focus(); + + // Clean up after a delay to allow the download to start + setTimeout(() => { + URL.revokeObjectURL(url); + }, 1000); + } catch (error: unknown) { + const errorMessage = error instanceof Error ? error.message : String(error); + throw new Error(`Failed to download ZIP file: ${errorMessage}`); + } + } +} diff --git a/src/api/__tests__/Metadata.test.js b/src/api/__tests__/Metadata.test.js index 00c17e5c58..fd90a02099 100644 --- a/src/api/__tests__/Metadata.test.js +++ b/src/api/__tests__/Metadata.test.js @@ -1700,7 +1700,6 @@ describe('api/Metadata', () => { permissions: { can_upload: true, }, - type: 'file', }; const ops = [{ op: 'add' }, { op: 'test' }]; const cache = new Cache(); @@ -1768,7 +1767,6 @@ describe('api/Metadata', () => { permissions: { can_upload: true, }, - type: 'file', }; const ops = [{ op: 'add' }, { op: 'test' }]; const cache = new Cache(); @@ -1835,7 +1833,6 @@ describe('api/Metadata', () => { permissions: { can_upload: true, }, - type: 'file', }; const ops = [{ op: 'add' }, { op: 'test' }]; const cache = new Cache(); @@ -1897,123 +1894,6 @@ describe('api/Metadata', () => { }); }); - describe('bulkUpdateMetadata()', () => { - test('should call updateMetadata for each item and call successHandler when all succeed', async () => { - const success = jest.fn(); - const error = jest.fn(); - const items = [ - { id: '1', name: 'file1', permissions: { can_upload: true }, type: 'file' }, - { id: '2', name: 'file2', permissions: { can_upload: true }, type: 'file' }, - ]; - const template = { scope: 'scope', templateKey: 'templateKey' }; - const ops = [[{ op: 'replace', path: '/foo', value: 'a' }], [{ op: 'replace', path: '/foo', value: 'b' }]]; - - metadata.updateMetadata = jest.fn().mockResolvedValue(undefined); - metadata.isDestroyed = jest.fn().mockReturnValue(false); - metadata.successHandler = jest.fn(); - metadata.errorHandler = jest.fn(); - - await metadata.bulkUpdateMetadata(items, template, ops, success, error); - - expect(metadata.errorCode).toBe(ERROR_CODE_UPDATE_METADATA); - expect(metadata.successCallback).toBe(success); - expect(metadata.errorCallback).toBe(error); - expect(metadata.updateMetadata).toHaveBeenCalledTimes(2); - expect(metadata.updateMetadata).toHaveBeenNthCalledWith( - 1, - items[0], - template, - ops[0], - success, - error, - true, - ); - expect(metadata.updateMetadata).toHaveBeenNthCalledWith( - 2, - items[1], - template, - ops[1], - success, - error, - true, - ); - expect(metadata.isDestroyed).toHaveBeenCalledTimes(1); - expect(metadata.successHandler).toHaveBeenCalledTimes(1); - expect(metadata.errorHandler).not.toHaveBeenCalled(); - }); - - test('should call errorHandler with aggregated error when any update fails', async () => { - const success = jest.fn(); - const error = jest.fn(); - const items = [ - { id: '1', name: 'file1', permissions: { can_upload: true }, type: 'file' }, - { id: '2', name: 'file2', permissions: { can_upload: true }, type: 'file' }, - ]; - const template = { scope: 'scope', templateKey: 'templateKey' }; - const ops = [[], []]; - - metadata.updateMetadata = jest - .fn() - .mockResolvedValueOnce(undefined) - .mockRejectedValueOnce(new Error('mock error')); - metadata.isDestroyed = jest.fn().mockReturnValue(false); - metadata.successHandler = jest.fn(); - metadata.errorHandler = jest.fn(); - - await metadata.bulkUpdateMetadata(items, template, ops, success, error); - - expect(metadata.updateMetadata).toHaveBeenCalledTimes(2); - expect(metadata.errorHandler).toHaveBeenCalledTimes(1); - const errArg = metadata.errorHandler.mock.calls[0][0]; - expect(errArg).toBeInstanceOf(Error); - expect(errArg.message).toContain('Failed to update metadata'); - expect(errArg.message).toContain('mock error'); - expect(metadata.successHandler).not.toHaveBeenCalled(); - }); - - test('should not call successHandler when destroyed after successful updates', async () => { - const success = jest.fn(); - const error = jest.fn(); - const items = [ - { id: '1', name: 'file1', permissions: { can_upload: true }, type: 'file' }, - { id: '2', name: 'file2', permissions: { can_upload: true }, type: 'file' }, - ]; - const template = { scope: 'scope', templateKey: 'templateKey' }; - const ops = [[], []]; - - metadata.updateMetadata = jest.fn().mockResolvedValue(undefined); - metadata.isDestroyed = jest.fn().mockReturnValue(true); - metadata.successHandler = jest.fn(); - metadata.errorHandler = jest.fn(); - - await metadata.bulkUpdateMetadata(items, template, ops, success, error); - - expect(metadata.successHandler).not.toHaveBeenCalled(); - expect(metadata.errorHandler).not.toHaveBeenCalled(); - }); - - test('should not call errorHandler when destroyed after failure', async () => { - const success = jest.fn(); - const error = jest.fn(); - const items = [ - { id: '1', name: 'file1', permissions: { can_upload: true }, type: 'file' }, - { id: '2', name: 'file2', permissions: { can_upload: true }, type: 'file' }, - ]; - const template = { scope: 'scope', templateKey: 'templateKey' }; - const ops = [[], []]; - - metadata.updateMetadata = jest.fn().mockRejectedValue(new Error('mock error')); - metadata.isDestroyed = jest.fn().mockReturnValue(true); - metadata.successHandler = jest.fn(); - metadata.errorHandler = jest.fn(); - - await metadata.bulkUpdateMetadata(items, template, ops, success, error); - - expect(metadata.successHandler).not.toHaveBeenCalled(); - expect(metadata.errorHandler).not.toHaveBeenCalled(); - }); - }); - describe('updateMetadataRedesign()', () => { test('should call error callback with a bad item error when no id', () => { jest.spyOn(ErrorUtil, 'getBadItemError').mockReturnValueOnce('error'); diff --git a/src/api/__tests__/ZipDownload.test.ts b/src/api/__tests__/ZipDownload.test.ts new file mode 100644 index 0000000000..6ba81be638 --- /dev/null +++ b/src/api/__tests__/ZipDownload.test.ts @@ -0,0 +1,285 @@ +import { BoxClient, BoxDeveloperTokenAuth } from 'box-typescript-sdk-gen'; +import ZipDownloadAPI, { ZipDownloadItem, ZipDownloadOptions, ZipDownloadResponse } from '../ZipDownload'; + +// Mock the box-typescript-sdk-gen +jest.mock('box-typescript-sdk-gen', () => ({ + BoxClient: jest.fn(), + BoxDeveloperTokenAuth: jest.fn(), +})); + +// Mock window.open and URL.revokeObjectURL +const mockWindowOpen = jest.fn(); +const mockUrlRevokeObjectURL = jest.fn(); + +Object.defineProperty(window, 'open', { + value: mockWindowOpen, + writable: true, +}); + +Object.defineProperty(window, 'focus', { + value: jest.fn(), + writable: true, +}); + +Object.defineProperty(global, 'URL', { + value: { + revokeObjectURL: mockUrlRevokeObjectURL, + }, + writable: true, +}); + +describe('ZipDownloadAPI', () => { + let zipDownloadAPI: ZipDownloadAPI; + let mockClient: jest.Mocked; + let mockAuth: jest.Mocked; + let mockCreateZipDownload: jest.Mock; + + const mockOptions: ZipDownloadOptions = { + token: 'test-token', + downloadFileName: 'test-download.zip', + }; + + const mockItems: ZipDownloadItem[] = [ + { id: '123', type: 'file' }, + { id: '456', type: 'folder' }, + ]; + + const mockZipDownloadResponse: ZipDownloadResponse = { + downloadUrl: 'https://api.box.com/2.0/zip_downloads/test-download-url', + statusUrl: 'https://api.box.com/2.0/zip_downloads/test-status-url', + expiresAt: '2024-01-01T00:00:00Z', + state: 'succeeded', + totalCount: 2, + downloadedCount: 2, + skippedCount: 0, + }; + + beforeEach(() => { + jest.clearAllMocks(); + + // Setup mocks + mockAuth = { + token: 'test-token', + } as jest.Mocked; + + mockCreateZipDownload = jest.fn(); + mockClient = { + zipDownloads: { + createZipDownload: mockCreateZipDownload, + }, + } as unknown as jest.Mocked; + + (BoxDeveloperTokenAuth as jest.Mock).mockImplementation(() => mockAuth); + (BoxClient as jest.Mock).mockImplementation(() => mockClient); + + zipDownloadAPI = new ZipDownloadAPI(mockOptions); + }); + + describe('constructor', () => { + test('should initialize with correct options', () => { + expect(BoxDeveloperTokenAuth).toHaveBeenCalledWith({ token: 'test-token' }); + expect(BoxClient).toHaveBeenCalledWith({ auth: mockAuth }); + }); + + test('should initialize without downloadFileName', () => { + const optionsWithoutFileName: ZipDownloadOptions = { + token: 'test-token', + }; + + const apiWithoutFileName = new ZipDownloadAPI(optionsWithoutFileName); + + expect(BoxDeveloperTokenAuth).toHaveBeenCalledWith({ token: 'test-token' }); + expect(BoxClient).toHaveBeenCalledWith({ auth: mockAuth }); + expect(apiWithoutFileName).toBeInstanceOf(ZipDownloadAPI); + }); + }); + + describe('createZipDownload', () => { + test('should successfully create ZIP download', async () => { + mockCreateZipDownload.mockResolvedValue(mockZipDownloadResponse); + + const result = await zipDownloadAPI.createZipDownload(mockItems); + + expect(mockCreateZipDownload).toHaveBeenCalledWith({ + items: mockItems, + downloadFileName: 'test-download.zip', + }); + expect(result).toEqual(mockZipDownloadResponse); + expect(mockWindowOpen).toHaveBeenCalledWith( + 'https://api.box.com/2.0/zip_downloads/test-download-url', + '_blank', + 'noopener,noreferrer', + ); + }); + + test('should create ZIP download without downloadFileName', async () => { + const optionsWithoutFileName: ZipDownloadOptions = { + token: 'test-token', + }; + const apiWithoutFileName = new ZipDownloadAPI(optionsWithoutFileName); + + mockCreateZipDownload.mockResolvedValue(mockZipDownloadResponse); + + await apiWithoutFileName.createZipDownload(mockItems); + + expect(mockCreateZipDownload).toHaveBeenCalledWith({ + items: mockItems, + downloadFileName: undefined, + }); + }); + + test('should throw error when items array is empty', async () => { + await expect(zipDownloadAPI.createZipDownload([])).rejects.toThrow('Items array cannot be empty'); + }); + + test('should throw error when items array is null', async () => { + await expect(zipDownloadAPI.createZipDownload(null as unknown as ZipDownloadItem[])).rejects.toThrow( + 'Items array cannot be empty', + ); + }); + + test('should throw error when items array is undefined', async () => { + await expect(zipDownloadAPI.createZipDownload(undefined as unknown as ZipDownloadItem[])).rejects.toThrow( + 'Items array cannot be empty', + ); + }); + + test('should handle API errors and throw with descriptive message', async () => { + const apiError = new Error('API Error'); + mockCreateZipDownload.mockRejectedValue(apiError); + + await expect(zipDownloadAPI.createZipDownload(mockItems)).rejects.toThrow( + 'Failed to create ZIP download: API Error', + ); + }); + + test('should handle non-Error objects and convert to string', async () => { + const apiError = 'String error'; + mockCreateZipDownload.mockRejectedValue(apiError); + + await expect(zipDownloadAPI.createZipDownload(mockItems)).rejects.toThrow( + 'Failed to create ZIP download: String error', + ); + }); + + test('should handle different item types', async () => { + const mixedItems: ZipDownloadItem[] = [ + { id: '123', type: 'file' }, + { id: '456', type: 'folder' }, + { id: '789', type: 'file' }, + ]; + + mockCreateZipDownload.mockResolvedValue(mockZipDownloadResponse); + + await zipDownloadAPI.createZipDownload(mixedItems); + + expect(mockCreateZipDownload).toHaveBeenCalledWith({ + items: mixedItems, + downloadFileName: 'test-download.zip', + }); + }); + }); + + describe('downloadZipFile (private method)', () => { + test('should open download URL in new window', async () => { + mockCreateZipDownload.mockResolvedValue(mockZipDownloadResponse); + + await zipDownloadAPI.createZipDownload(mockItems); + + expect(mockWindowOpen).toHaveBeenCalledWith( + 'https://api.box.com/2.0/zip_downloads/test-download-url', + '_blank', + 'noopener,noreferrer', + ); + expect(window.focus).toHaveBeenCalled(); + }); + + test('should revoke object URL after timeout', async () => { + jest.useFakeTimers(); + + mockCreateZipDownload.mockResolvedValue(mockZipDownloadResponse); + + await zipDownloadAPI.createZipDownload(mockItems); + + // Fast-forward timers to trigger the setTimeout + jest.advanceTimersByTime(1000); + + expect(mockUrlRevokeObjectURL).toHaveBeenCalledWith( + 'https://api.box.com/2.0/zip_downloads/test-download-url', + ); + + jest.useRealTimers(); + }); + + test('should handle window.open errors', async () => { + const windowError = new Error('Window open error'); + mockWindowOpen.mockImplementation(() => { + throw windowError; + }); + + mockCreateZipDownload.mockResolvedValue(mockZipDownloadResponse); + + await expect(zipDownloadAPI.createZipDownload(mockItems)).rejects.toThrow( + 'Failed to download ZIP file: Window open error', + ); + }); + + test('should handle non-Error window.open errors', async () => { + mockWindowOpen.mockImplementation(() => { + throw new Error('String window error'); + }); + + mockCreateZipDownload.mockResolvedValue(mockZipDownloadResponse); + + await expect(zipDownloadAPI.createZipDownload(mockItems)).rejects.toThrow( + 'Failed to download ZIP file: String window error', + ); + }); + }); + + describe('integration scenarios', () => { + test('should handle successful download with in_progress state', async () => { + const inProgressResponse: ZipDownloadResponse = { + ...mockZipDownloadResponse, + state: 'in_progress', + downloadUrl: undefined, + }; + + mockCreateZipDownload.mockResolvedValue(inProgressResponse); + + const result = await zipDownloadAPI.createZipDownload(mockItems); + + expect(result.state).toBe('in_progress'); + expect(mockWindowOpen).not.toHaveBeenCalled(); + }); + + test('should handle failed download state', async () => { + const failedResponse: ZipDownloadResponse = { + ...mockZipDownloadResponse, + state: 'failed', + downloadUrl: undefined, + }; + + mockCreateZipDownload.mockResolvedValue(failedResponse); + + const result = await zipDownloadAPI.createZipDownload(mockItems); + + expect(result.state).toBe('failed'); + expect(mockWindowOpen).not.toHaveBeenCalled(); + }); + + test('should handle response without downloadUrl', async () => { + const responseWithoutUrl: ZipDownloadResponse = { + ...mockZipDownloadResponse, + downloadUrl: undefined, + }; + + mockCreateZipDownload.mockResolvedValue(responseWithoutUrl); + + const result = await zipDownloadAPI.createZipDownload(mockItems); + + expect(result.downloadUrl).toBeUndefined(); + expect(mockWindowOpen).not.toHaveBeenCalled(); + }); + }); +}); diff --git a/yarn.lock b/yarn.lock index befd7d8610..b40e56a1ed 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1522,10 +1522,10 @@ resolved "https://registry.yarnpkg.com/@box/metadata-filter/-/metadata-filter-1.19.3.tgz#87364bea4cbb1417866e65639f3b1e137a6d9b6a" integrity sha512-5cSY8yLW7S1zsiqBHAuKkHjcyHFBuBUBHGTnYigV0eKyLH4Dm9ozjon23P3Z9HXVB5IMHwTM3I9TRDFAZuP7vw== -"@box/metadata-view@^0.48.1": - version "0.48.1" - resolved "https://registry.yarnpkg.com/@box/metadata-view/-/metadata-view-0.48.1.tgz#58cab5153cf343726aa9718debccdca88e8fb10f" - integrity sha512-+OeSLT5AEqgSDz1p61mV/VVPb/qGAmlC/+GOyqSZ/aSu2D8owUUS0BO/rPVIF1DA56NrPoeGfQA8GgxdKzNisA== +"@box/metadata-view@^0.41.2": + version "0.41.3" + resolved "https://registry.yarnpkg.com/@box/metadata-view/-/metadata-view-0.41.3.tgz#95a4d8322d02c13172fb0be681e74e17f8fe90dc" + integrity sha512-7ZqUrx4YmfmwXeDoPhSpLnL8xxVBkZ3Hlw4gpfpCw8IPHdT/nYTFml1GW7DO5d43jICf3foD08wwksW9IeB7/A== "@box/react-virtualized@^9.22.3-rc-box.10": version "9.22.3-rc-box.10" @@ -5299,6 +5299,11 @@ resolved "https://registry.yarnpkg.com/@tootallnate/once/-/once-2.0.0.tgz#f544a148d3ab35801c1f633a7441fd87c2e484bf" integrity sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A== +"@tootallnate/quickjs-emscripten@^0.23.0": + version "0.23.0" + resolved "https://registry.yarnpkg.com/@tootallnate/quickjs-emscripten/-/quickjs-emscripten-0.23.0.tgz#db4ecfd499a9765ab24002c3b696d02e6d32a12c" + integrity sha512-C5Mc6rdnsaJDjO3UpGW/CQTHtCKaYlScZTly4JIu97Jxo/odCiH0ITnDXSJPTOrEKk/ycSZ0AOgTmkDtkOsvIA== + "@trysound/sax@0.2.0": version "0.2.0" resolved "https://registry.yarnpkg.com/@trysound/sax/-/sax-0.2.0.tgz#cccaab758af56761eb7bf37af6f03f326dd798ad" @@ -6777,6 +6782,13 @@ ast-types-flow@^0.0.8: resolved "https://registry.yarnpkg.com/ast-types-flow/-/ast-types-flow-0.0.8.tgz#0a85e1c92695769ac13a428bb653e7538bea27d6" integrity sha512-OH/2E5Fg20h2aPrbe+QL8JZQFko0YZaF+j4mnQ7BGhfavO7OpSLa8a0y9sBwomHdSbkhTS8TQNayBfnW5DwbvQ== +ast-types@^0.13.4: + version "0.13.4" + resolved "https://registry.yarnpkg.com/ast-types/-/ast-types-0.13.4.tgz#ee0d77b343263965ecc3fb62da16e7222b2b6782" + integrity sha512-x1FCFnFifvYDDzTaLII71vG5uvDwgtmDTEVWAxrgeiR8VjMONcCXJx7E+USjDtHlwFmt9MysbqgF9b9Vjr6w+w== + dependencies: + tslib "^2.0.1" + ast-types@^0.16.1: version "0.16.1" resolved "https://registry.yarnpkg.com/ast-types/-/ast-types-0.16.1.tgz#7a9da1617c9081bc121faafe91711b4c8bb81da2" @@ -7094,6 +7106,11 @@ base64-js@^1.3.1: resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a" integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA== +basic-ftp@^5.0.2: + version "5.0.5" + resolved "https://registry.yarnpkg.com/basic-ftp/-/basic-ftp-5.0.5.tgz#14a474f5fffecca1f4f406f1c26b18f800225ac0" + integrity sha512-4Bcg1P8xhUuqcii/S0Z9wiHIrQVPMermM1any+MX5GeGD7faD3/msQUDGLol9wOcz4/jbg/WJnGqoJF6LiBdtg== + batch@0.6.1: version "0.6.1" resolved "https://registry.yarnpkg.com/batch/-/batch-0.6.1.tgz#dc34314f4e679318093fc760272525f94bf25c16" @@ -7205,6 +7222,20 @@ bottleneck@^2.15.3: resolved "https://registry.yarnpkg.com/bottleneck/-/bottleneck-2.19.5.tgz#5df0b90f59fd47656ebe63c78a98419205cadd91" integrity sha512-VHiNCbI1lKdl44tGrhNfU3lup0Tj/ZBMJB5/2ZbNXRCPuRCO7ed2mgcK4r17y+KB2EfuYuRaVlwNbAeaWGSpbw== +box-typescript-sdk-gen@^1.17.1: + version "1.17.1" + resolved "https://registry.yarnpkg.com/box-typescript-sdk-gen/-/box-typescript-sdk-gen-1.17.1.tgz#b76a62a8b1ea5761bee6177620896c9028d6ce8c" + integrity sha512-YPzzdUj25SnruKVAsez7L1kuFi+oXZdwt7JH2CNS8TO5wRxMyHJqNKYDeXLFWfh+B0q9G1njMhdOKFcSEgbsGg== + dependencies: + buffer "^6.0.3" + form-data "^4.0.0" + hash-wasm "^4.12.0" + jose "^5.2.2" + node-fetch "^2.6.3" + proxy-agent "^6.4.0" + tslib "^2.6.2" + uuid "^9.0.0" + brace-expansion@^1.1.7: version "1.1.11" resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd" @@ -8798,6 +8829,11 @@ dashdash@^1.12.0: dependencies: assert-plus "^1.0.0" +data-uri-to-buffer@^6.0.2: + version "6.0.2" + resolved "https://registry.yarnpkg.com/data-uri-to-buffer/-/data-uri-to-buffer-6.0.2.tgz#8a58bb67384b261a38ef18bea1810cb01badd28b" + integrity sha512-7hvf7/GW8e86rW0ptuwS3OcBGDjIi6SZva7hCyWC0yYry2cOPmLIjXAUHI6DK2HsnwJd9ifmt57i8eV2n4YNpw== + data-urls@^3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/data-urls/-/data-urls-3.0.2.tgz#9cf24a477ae22bcef5cd5f6f0bfbc1d2d3be9143" @@ -9009,6 +9045,15 @@ define-properties@^1.1.3, define-properties@^1.2.0, define-properties@^1.2.1: has-property-descriptors "^1.0.0" object-keys "^1.1.1" +degenerator@^5.0.0: + version "5.0.1" + resolved "https://registry.yarnpkg.com/degenerator/-/degenerator-5.0.1.tgz#9403bf297c6dad9a1ece409b37db27954f91f2f5" + integrity sha512-TllpMR/t0M5sqCXfj85i4XaAzxmS5tVA16dqvdkMwGmzI+dXLXnw3J+3Vdv7VKw+ThlTMboK6i9rnZ6Nntj5CQ== + dependencies: + ast-types "^0.13.4" + escodegen "^2.1.0" + esprima "^4.0.1" + del@^5.0.0: version "5.1.0" resolved "https://registry.yarnpkg.com/del/-/del-5.1.0.tgz#d9487c94e367410e6eff2925ee58c0c84a75b3a7" @@ -9762,7 +9807,7 @@ escape-string-regexp@^4.0.0: resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz#14ba83a5d373e3d311e5afca29cf5bfad965bf34" integrity sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA== -escodegen@^2.0.0: +escodegen@^2.0.0, escodegen@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/escodegen/-/escodegen-2.1.0.tgz#ba93bbb7a43986d29d6041f99f5262da773e2e17" integrity sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w== @@ -11040,6 +11085,15 @@ get-symbol-description@^1.0.2: es-errors "^1.3.0" get-intrinsic "^1.2.4" +get-uri@^6.0.1: + version "6.0.5" + resolved "https://registry.yarnpkg.com/get-uri/-/get-uri-6.0.5.tgz#714892aa4a871db671abc5395e5e9447bc306a16" + integrity sha512-b1O07XYq8eRuVzBNgJLstU6FYc1tS6wnMtF1I1D9lE8LxZSOGZ7LhxN54yPP6mGw5f2CkXY2BQUL9Fx41qvcIg== + dependencies: + basic-ftp "^5.0.2" + data-uri-to-buffer "^6.0.2" + debug "^4.3.4" + getos@^3.2.1: version "3.2.1" resolved "https://registry.yarnpkg.com/getos/-/getos-3.2.1.tgz#0134d1f4e00eb46144c5a9c0ac4dc087cbb27dc5" @@ -11362,6 +11416,11 @@ hash-base@~3.0, hash-base@~3.0.4: inherits "^2.0.4" safe-buffer "^5.2.1" +hash-wasm@^4.12.0: + version "4.12.0" + resolved "https://registry.yarnpkg.com/hash-wasm/-/hash-wasm-4.12.0.tgz#f9f1a9f9121e027a9acbf6db5d59452ace1ef9bb" + integrity sha512-+/2B2rYLb48I/evdOIhP+K/DD2ca2fgBjp6O+GBEnCDk2e4rpeXIK8GvIyRPjTezgmWn9gmKwkQjjx6BtqDHVQ== + hash.js@^1.0.0, hash.js@^1.0.3: version "1.1.7" resolved "https://registry.yarnpkg.com/hash.js/-/hash.js-1.1.7.tgz#0babca538e8d4ee4a0f8988d68866537a003cf42" @@ -11598,7 +11657,7 @@ http-proxy-agent@^5.0.0: agent-base "6" debug "4" -http-proxy-agent@^7.0.0: +http-proxy-agent@^7.0.0, http-proxy-agent@^7.0.1: version "7.0.2" resolved "https://registry.yarnpkg.com/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz#9a8b1f246866c028509486585f62b8f2c18c270e" integrity sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig== @@ -11656,7 +11715,7 @@ https-proxy-agent@^5.0.1: agent-base "6" debug "4" -https-proxy-agent@^7.0.0, https-proxy-agent@^7.0.1: +https-proxy-agent@^7.0.0, https-proxy-agent@^7.0.1, https-proxy-agent@^7.0.6: version "7.0.6" resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz#da8dfeac7da130b05c2ba4b59c9b6cd66611a6b9" integrity sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw== @@ -12970,6 +13029,11 @@ joi@^17.13.3: "@sideway/formula" "^3.0.1" "@sideway/pinpoint" "^2.0.0" +jose@^5.2.2: + version "5.10.0" + resolved "https://registry.yarnpkg.com/jose/-/jose-5.10.0.tgz#c37346a099d6467c401351a9a0c2161e0f52c4be" + integrity sha512-s+3Al/p9g32Iq+oqXxkW//7jk2Vig6FF1CFqzVXoTUXt2qz89YWbL+OwS17NFYEvxC35n0FKeGO2LGYSxeM2Gg== + js-sha1@0.6.0: version "0.6.0" resolved "https://registry.yarnpkg.com/js-sha1/-/js-sha1-0.6.0.tgz#adbee10f0e8e18aa07cdea807cf08e9183dbc7f9" @@ -13761,6 +13825,11 @@ lru-cache@^5.1.1: dependencies: yallist "^3.0.2" +lru-cache@^7.14.1: + version "7.18.3" + resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-7.18.3.tgz#f793896e0fd0e954a59dfdd82f0773808df6aa89" + integrity sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA== + lz-string@^1.5.0: version "1.5.0" resolved "https://registry.yarnpkg.com/lz-string/-/lz-string-1.5.0.tgz#c1ab50f77887b712621201ba9fd4e3a6ed099941" @@ -14343,6 +14412,11 @@ nerf-dart@^1.0.0: resolved "https://registry.yarnpkg.com/nerf-dart/-/nerf-dart-1.0.0.tgz#e6dab7febf5ad816ea81cf5c629c5a0ebde72c1a" integrity sha512-EZSPZB70jiVsivaBLYDCyntd5eH8NTSMOn3rB+HxwdmKThGELLdYv8qVIMWvZEFy9w8ZZpW9h9OB32l1rGtj7g== +netmask@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/netmask/-/netmask-2.0.2.tgz#8b01a07644065d536383835823bc52004ebac5e7" + integrity sha512-dBpDMdxv9Irdq66304OLfEmQ9tbNRFnFTuZiLo+bD+r332bBmMJ8GBLXklIXXgxd3+v9+KUnZaUR5PJMa75Gsg== + next-tick@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/next-tick/-/next-tick-1.1.0.tgz#1836ee30ad56d67ef281b22bd199f709449b35eb" @@ -14376,7 +14450,7 @@ node-emoji@^2.2.0: emojilib "^2.4.0" skin-tone "^2.0.0" -node-fetch@^2.6.12, node-fetch@^2.6.7: +node-fetch@^2.6.12, node-fetch@^2.6.3, node-fetch@^2.6.7: version "2.7.0" resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.7.0.tgz#d0f0fa6e3e2dc1d27efcd8ad99d550bda94d187d" integrity sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A== @@ -15101,6 +15175,28 @@ p-try@^2.0.0: resolved "https://registry.yarnpkg.com/p-try/-/p-try-2.2.0.tgz#cb2868540e313d61de58fafbe35ce9004d5540e6" integrity sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ== +pac-proxy-agent@^7.1.0: + version "7.2.0" + resolved "https://registry.yarnpkg.com/pac-proxy-agent/-/pac-proxy-agent-7.2.0.tgz#9cfaf33ff25da36f6147a20844230ec92c06e5df" + integrity sha512-TEB8ESquiLMc0lV8vcd5Ql/JAKAoyzHFXaStwjkzpOpC5Yv+pIzLfHvjTSdf3vpa2bMiUQrg9i6276yn8666aA== + dependencies: + "@tootallnate/quickjs-emscripten" "^0.23.0" + agent-base "^7.1.2" + debug "^4.3.4" + get-uri "^6.0.1" + http-proxy-agent "^7.0.0" + https-proxy-agent "^7.0.6" + pac-resolver "^7.0.1" + socks-proxy-agent "^8.0.5" + +pac-resolver@^7.0.1: + version "7.0.1" + resolved "https://registry.yarnpkg.com/pac-resolver/-/pac-resolver-7.0.1.tgz#54675558ea368b64d210fd9c92a640b5f3b8abb6" + integrity sha512-5NPgf87AT2STgwa2ntRMr45jTKrYBGkVU36yT0ig/n/GMAa3oPqhZfIQ2kMEimReg0+t9kZViDVZ83qfVUlckg== + dependencies: + degenerator "^5.0.0" + netmask "^2.0.2" + package-json-from-dist@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/package-json-from-dist/-/package-json-from-dist-1.0.0.tgz#e501cd3094b278495eb4258d4c9f6d5ac3019f00" @@ -16256,6 +16352,20 @@ proxy-addr@~2.0.7: forwarded "0.2.0" ipaddr.js "1.9.1" +proxy-agent@^6.4.0: + version "6.5.0" + resolved "https://registry.yarnpkg.com/proxy-agent/-/proxy-agent-6.5.0.tgz#9e49acba8e4ee234aacb539f89ed9c23d02f232d" + integrity sha512-TmatMXdr2KlRiA2CyDu8GqR8EjahTG3aY3nXjdzFyoZbmB8hrBsTyMezhULIXKnC0jpfjlmiZ3+EaCzoInSu/A== + dependencies: + agent-base "^7.1.2" + debug "^4.3.4" + http-proxy-agent "^7.0.1" + https-proxy-agent "^7.0.6" + lru-cache "^7.14.1" + pac-proxy-agent "^7.1.0" + proxy-from-env "^1.1.0" + socks-proxy-agent "^8.0.5" + proxy-from-env@1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/proxy-from-env/-/proxy-from-env-1.0.0.tgz#33c50398f70ea7eb96d21f7b817630a55791c7ee" @@ -17937,7 +18047,7 @@ sockjs@^0.3.24: uuid "^8.3.2" websocket-driver "^0.7.4" -socks-proxy-agent@^8.0.3: +socks-proxy-agent@^8.0.3, socks-proxy-agent@^8.0.5: version "8.0.5" resolved "https://registry.yarnpkg.com/socks-proxy-agent/-/socks-proxy-agent-8.0.5.tgz#b9cdb4e7e998509d7659d689ce7697ac21645bee" integrity sha512-HehCEsotFqbPW9sJ8WVYB6UbmIMv7kUUORIF2Nncq4VQvBfNBLibW9YZR5dlYCSUhwcD628pRllm7n+E+YTzJw== @@ -19078,7 +19188,7 @@ tslib@^1.9.0: resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00" integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg== -tslib@^2.0.0, tslib@^2.0.1, tslib@^2.0.3, tslib@^2.1.0, tslib@^2.3.0, tslib@^2.4.0, tslib@^2.8.0: +tslib@^2.0.0, tslib@^2.0.1, tslib@^2.0.3, tslib@^2.1.0, tslib@^2.3.0, tslib@^2.4.0, tslib@^2.6.2, tslib@^2.8.0: version "2.8.1" resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.8.1.tgz#612efe4ed235d567e8aba5f2a5fab70280ade83f" integrity sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w== @@ -19520,6 +19630,11 @@ uuid@^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: + version "9.0.1" + resolved "https://registry.yarnpkg.com/uuid/-/uuid-9.0.1.tgz#e188d4c8853cc722220392c424cd637f32293f30" + integrity sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA== + v8-to-istanbul@^9.0.1: version "9.3.0" resolved "https://registry.yarnpkg.com/v8-to-istanbul/-/v8-to-istanbul-9.3.0.tgz#b9572abfa62bd556c16d75fdebc1a411d5ff3175"