Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 15 additions & 3 deletions src/drive/storage/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import {
FileVersion,
FolderAncestor,
FolderAncestorWorkspace,
GlobalSearchOptions,
FolderMeta,
FolderStatsResponse,
FolderTreeResponse,
Expand Down Expand Up @@ -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<SearchResultData>, 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<SearchResultData>, 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<SearchResultData>(
Expand Down
4 changes: 3 additions & 1 deletion src/drive/storage/types.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { components, paths } from '../../schema';
import { components, operations, paths } from '../../schema';
import { UserResumeData } from '../users/types';

export interface DriveFolderData {
Expand Down Expand Up @@ -363,6 +363,8 @@ export interface SearchResultData {
data: [SearchResult];
}

export type GlobalSearchOptions = NonNullable<operations['FuzzySearchController_fuzzySearch']['parameters']['query']>;

export interface FolderAncestor {
bucket: null | string;
createdAt: string;
Expand Down
30 changes: 26 additions & 4 deletions src/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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: {
Expand Down Expand Up @@ -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: {
Expand Down
69 changes: 69 additions & 0 deletions test/drive/storage/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
FolderAncestorWorkspace,
MoveFolderPayload,
MoveFolderResponse,
SearchResultData,
UpdateFilePayload,
} from '../../../src/drive/storage/types';
import { ApiSecurity, AppDetails } from '../../../src/shared';
Expand Down Expand Up @@ -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({
Expand Down
Loading