diff --git a/src/drive/storage/index.ts b/src/drive/storage/index.ts index 6c5819e8..c3868f57 100644 --- a/src/drive/storage/index.ts +++ b/src/drive/storage/index.ts @@ -31,6 +31,7 @@ import { FileVersion, FolderAncestor, FolderAncestorWorkspace, + GlobalSearchOptions, FolderMeta, FolderStatsResponse, FolderTreeResponse, @@ -660,16 +661,27 @@ export class Storage { * * @param {string} search - The name of the item. * @param {string} workspaceId - The ID of the workspace (optional). - * @param {number} offset - The position of the first item to return (optional). + * @param {number | GlobalSearchOptions} offsetOrOptions - The position of the first item to return, or an options + * object (optional) with the offset and the search filters: `type` (file categories, combined with OR), + * `minSize`/`maxSize` (bytes, files only) and `modifiedAfter`/`modifiedBefore` (ISO 8601 dates). Filters are + * combined with AND. * @returns {[Promise, RequestCanceler]} An array containing a promise to get the API response and a function to cancel the request. */ public getGlobalSearchItems( search: string, workspaceId?: string, - offset?: number, + offsetOrOptions?: number | GlobalSearchOptions, ): [Promise, RequestCanceler] { + const options: GlobalSearchOptions = + typeof offsetOrOptions === 'number' ? { offset: offsetOrOptions } : (offsetOrOptions ?? {}); + const query = new URLSearchParams(); - if (offset !== undefined) query.set('offset', String(offset)); + if (options.offset !== undefined) query.set('offset', String(options.offset)); + options.type?.forEach((category) => query.append('type', category)); + if (options.minSize !== undefined) query.set('minSize', String(options.minSize)); + if (options.maxSize !== undefined) query.set('maxSize', String(options.maxSize)); + if (options.modifiedAfter !== undefined) query.set('modifiedAfter', options.modifiedAfter); + if (options.modifiedBefore !== undefined) query.set('modifiedBefore', options.modifiedBefore); const { promise, requestCanceler } = workspaceId ? this.client.getCancellable( diff --git a/src/drive/storage/types.ts b/src/drive/storage/types.ts index 7ba3c959..4fab7c96 100644 --- a/src/drive/storage/types.ts +++ b/src/drive/storage/types.ts @@ -1,4 +1,4 @@ -import { components, paths } from '../../schema'; +import { components, operations, paths } from '../../schema'; import { UserResumeData } from '../users/types'; export interface DriveFolderData { @@ -363,6 +363,8 @@ export interface SearchResultData { data: [SearchResult]; } +export type GlobalSearchOptions = NonNullable; + export interface FolderAncestor { bucket: null | string; createdAt: string; diff --git a/src/schema.ts b/src/schema.ts index e74e3563..3dae3cd5 100644 --- a/src/schema.ts +++ b/src/schema.ts @@ -8946,8 +8946,19 @@ export interface operations { }; WorkspacesController_searchWorkspace: { parameters: { - query: { - offset: number; + query?: { + /** @description Offset for pagination */ + offset?: number; + /** @description File extensions to filter by, or the reserved value "folder" to include folders (single value or repeated param) */ + type?: string[]; + /** @description Minimum file size in bytes (folders are excluded) */ + minSize?: number; + /** @description Maximum file size in bytes (folders are excluded) */ + maxSize?: number; + /** @description Filter items modified after this date */ + modifiedAfter?: string; + /** @description Filter items modified before this date */ + modifiedBefore?: string; }; header?: never; path: { @@ -9032,8 +9043,19 @@ export interface operations { }; FuzzySearchController_fuzzySearch: { parameters: { - query: { - offset: number; + query?: { + /** @description Offset for pagination */ + offset?: number; + /** @description File extensions to filter by, or the reserved value "folder" to include folders (single value or repeated param) */ + type?: string[]; + /** @description Minimum file size in bytes (folders are excluded) */ + minSize?: number; + /** @description Maximum file size in bytes (folders are excluded) */ + maxSize?: number; + /** @description Filter items modified after this date */ + modifiedAfter?: string; + /** @description Filter items modified before this date */ + modifiedBefore?: string; }; header?: never; path: { diff --git a/test/drive/storage/index.test.ts b/test/drive/storage/index.test.ts index 0e45138b..ca505f60 100644 --- a/test/drive/storage/index.test.ts +++ b/test/drive/storage/index.test.ts @@ -10,6 +10,7 @@ import { FolderAncestorWorkspace, MoveFolderPayload, MoveFolderResponse, + SearchResultData, UpdateFilePayload, } from '../../../src/drive/storage/types'; import { ApiSecurity, AppDetails } from '../../../src/shared'; @@ -1027,6 +1028,74 @@ describe('# storage service tests', () => { }); }); }); + + describe('-> global search', () => { + const emptySearchResponse: SearchResultData = { data: [] as unknown as SearchResultData['data'] }; + + it('Should call the personal fuzzy endpoint without query params, when no options are passed', async () => { + const { client, headers } = clientAndHeaders({}); + const getCancellableStub = vi.spyOn(HttpClient.prototype, 'getCancellable').mockReturnValue({ + promise: Promise.resolve(emptySearchResponse), + requestCanceler: { cancel: () => null }, + }); + + const [promise] = client.getGlobalSearchItems('report'); + await promise; + + expect(getCancellableStub).toHaveBeenCalledWith('fuzzy/report?', headers); + }); + + it('Should call the workspace fuzzy endpoint, when a workspaceId is passed', async () => { + const workspaceId = 'workspace-id'; + const { client, headers } = clientAndHeaders({}); + const getCancellableStub = vi.spyOn(HttpClient.prototype, 'getCancellable').mockReturnValue({ + promise: Promise.resolve(emptySearchResponse), + requestCanceler: { cancel: () => null }, + }); + + const [promise] = client.getGlobalSearchItems('report', workspaceId); + await promise; + + expect(getCancellableStub).toHaveBeenCalledWith(`workspaces/${workspaceId}/fuzzy/report?`, headers); + }); + + it('Should keep supporting a numeric offset as third argument', async () => { + const { client, headers } = clientAndHeaders({}); + const getCancellableStub = vi.spyOn(HttpClient.prototype, 'getCancellable').mockReturnValue({ + promise: Promise.resolve(emptySearchResponse), + requestCanceler: { cancel: () => null }, + }); + + const [promise] = client.getGlobalSearchItems('report', undefined, 10); + await promise; + + expect(getCancellableStub).toHaveBeenCalledWith('fuzzy/report?offset=10', headers); + }); + + it('Should serialize all search filters as query params, when options are passed', async () => { + const { client, headers } = clientAndHeaders({}); + const getCancellableStub = vi.spyOn(HttpClient.prototype, 'getCancellable').mockReturnValue({ + promise: Promise.resolve(emptySearchResponse), + requestCanceler: { cancel: () => null }, + }); + + const [promise] = client.getGlobalSearchItems('report', undefined, { + offset: 0, + type: ['pdf', 'image'], + minSize: 1024, + maxSize: 5242880, + modifiedAfter: '2026-01-01T00:00:00.000Z', + modifiedBefore: '2026-06-30T23:59:59.999Z', + }); + await promise; + + expect(getCancellableStub).toHaveBeenCalledWith( + 'fuzzy/report?offset=0&type=pdf&type=image&minSize=1024&maxSize=5242880' + + '&modifiedAfter=2026-01-01T00%3A00%3A00.000Z&modifiedBefore=2026-06-30T23%3A59%3A59.999Z', + headers, + ); + }); + }); }); function clientAndHeaders({