From 38ae77f09c926247058b5d11e6d0aa85a9e27904 Mon Sep 17 00:00:00 2001 From: Victor Fernandez Robles Date: Fri, 24 Jul 2026 10:37:56 +0200 Subject: [PATCH 1/3] feat: add advanced search filters to global search --- src/drive/storage/index.ts | 18 +++++++-- src/drive/storage/types.ts | 25 ++++++++++++ test/drive/storage/index.test.ts | 69 ++++++++++++++++++++++++++++++++ 3 files changed, 109 insertions(+), 3 deletions(-) diff --git a/src/drive/storage/index.ts b/src/drive/storage/index.ts index c2f0df13..54152060 100644 --- a/src/drive/storage/index.ts +++ b/src/drive/storage/index.ts @@ -27,6 +27,7 @@ import { FileVersion, FolderAncestor, FolderAncestorWorkspace, + GlobalSearchOptions, FolderMeta, FolderStatsResponse, FolderTreeResponse, @@ -655,16 +656,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 e86af125..85f73383 100644 --- a/src/drive/storage/types.ts +++ b/src/drive/storage/types.ts @@ -360,6 +360,31 @@ export interface SearchResultData { data: [SearchResult]; } +export type SearchFileCategory = + | 'folder' + | 'audio' + | 'code' + | 'csv' + | 'figma' + | 'image' + | 'pdf' + | 'ppt' + | 'txt' + | 'video' + | 'word' + | 'xls' + | 'xml' + | 'zip'; + +export interface GlobalSearchOptions { + offset?: number; + type?: SearchFileCategory[]; + minSize?: number; + maxSize?: number; + modifiedAfter?: string; + modifiedBefore?: string; +} + export interface FolderAncestor { bucket: null | string; createdAt: string; diff --git a/test/drive/storage/index.test.ts b/test/drive/storage/index.test.ts index f0f343e8..e0f00c61 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'; @@ -917,6 +918,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({ From b14521614c1ab6edd5d39535e246109e8041f5f7 Mon Sep 17 00:00:00 2001 From: Victor Fernandez Robles Date: Tue, 28 Jul 2026 15:49:30 +0200 Subject: [PATCH 2/3] refactor: regenerate schema with fuzzy search filters and derive global search types from it --- src/drive/storage/types.ts | 29 +++--------------- src/schema.ts | 60 +++++++++++++++++++++++++++++++++++--- 2 files changed, 60 insertions(+), 29 deletions(-) diff --git a/src/drive/storage/types.ts b/src/drive/storage/types.ts index 85f73383..8a73eae0 100644 --- a/src/drive/storage/types.ts +++ b/src/drive/storage/types.ts @@ -1,4 +1,4 @@ -import { paths } from '../../schema'; +import { operations, paths } from '../../schema'; import { UserResumeData } from '../users/types'; export interface DriveFolderData { @@ -360,30 +360,9 @@ export interface SearchResultData { data: [SearchResult]; } -export type SearchFileCategory = - | 'folder' - | 'audio' - | 'code' - | 'csv' - | 'figma' - | 'image' - | 'pdf' - | 'ppt' - | 'txt' - | 'video' - | 'word' - | 'xls' - | 'xml' - | 'zip'; - -export interface GlobalSearchOptions { - offset?: number; - type?: SearchFileCategory[]; - minSize?: number; - maxSize?: number; - modifiedAfter?: string; - modifiedBefore?: string; -} +export type GlobalSearchOptions = NonNullable; + +export type SearchFileCategory = NonNullable[number]; export interface FolderAncestor { bucket: null | string; diff --git a/src/schema.ts b/src/schema.ts index 6402a9d6..bcdde631 100644 --- a/src/schema.ts +++ b/src/schema.ts @@ -8850,8 +8850,34 @@ export interface operations { }; WorkspacesController_searchWorkspace: { parameters: { - query: { - offset: number; + query?: { + /** @description Offset for pagination */ + offset?: number; + /** @description File categories to filter by (single value or repeated param) */ + type?: ( + | 'folder' + | 'audio' + | 'code' + | 'csv' + | 'figma' + | 'image' + | 'pdf' + | 'ppt' + | 'txt' + | 'video' + | 'word' + | 'xls' + | 'xml' + | 'zip' + )[]; + /** @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: { @@ -8936,8 +8962,34 @@ export interface operations { }; FuzzySearchController_fuzzySearch: { parameters: { - query: { - offset: number; + query?: { + /** @description Offset for pagination */ + offset?: number; + /** @description File categories to filter by (single value or repeated param) */ + type?: ( + | 'folder' + | 'audio' + | 'code' + | 'csv' + | 'figma' + | 'image' + | 'pdf' + | 'ppt' + | 'txt' + | 'video' + | 'word' + | 'xls' + | 'xml' + | 'zip' + )[]; + /** @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: { From bc8c38e79b97f4b4288be37771157ac7104b48fd Mon Sep 17 00:00:00 2001 From: Victor Fernandez Robles Date: Wed, 29 Jul 2026 13:11:03 +0200 Subject: [PATCH 3/3] refactor: replace search category enum with plain extensions in schema --- src/drive/storage/types.ts | 2 -- src/schema.ts | 38 ++++---------------------------------- 2 files changed, 4 insertions(+), 36 deletions(-) diff --git a/src/drive/storage/types.ts b/src/drive/storage/types.ts index 662f1356..4fab7c96 100644 --- a/src/drive/storage/types.ts +++ b/src/drive/storage/types.ts @@ -365,8 +365,6 @@ export interface SearchResultData { export type GlobalSearchOptions = NonNullable; -export type SearchFileCategory = NonNullable[number]; - export interface FolderAncestor { bucket: null | string; createdAt: string; diff --git a/src/schema.ts b/src/schema.ts index bcc38941..3dae3cd5 100644 --- a/src/schema.ts +++ b/src/schema.ts @@ -8949,23 +8949,8 @@ export interface operations { query?: { /** @description Offset for pagination */ offset?: number; - /** @description File categories to filter by (single value or repeated param) */ - type?: ( - | 'folder' - | 'audio' - | 'code' - | 'csv' - | 'figma' - | 'image' - | 'pdf' - | 'ppt' - | 'txt' - | 'video' - | 'word' - | 'xls' - | 'xml' - | 'zip' - )[]; + /** @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) */ @@ -9061,23 +9046,8 @@ export interface operations { query?: { /** @description Offset for pagination */ offset?: number; - /** @description File categories to filter by (single value or repeated param) */ - type?: ( - | 'folder' - | 'audio' - | 'code' - | 'csv' - | 'figma' - | 'image' - | 'pdf' - | 'ppt' - | 'txt' - | 'video' - | 'word' - | 'xls' - | 'xml' - | 'zip' - )[]; + /** @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) */