diff --git a/libraries/api-client/src/cells/cellsApi.test.ts b/libraries/api-client/src/cells/cellsApi.test.ts index c6966dd50cf..25a9455a2b7 100644 --- a/libraries/api-client/src/cells/cellsApi.test.ts +++ b/libraries/api-client/src/cells/cellsApi.test.ts @@ -1914,9 +1914,9 @@ describe('CellsAPI', () => { ); }); - it('filters by tags when provided', async () => { + it('creates a Should metadata filter for a single tag', async () => { const searchPhrase = 'test'; - const tags = ['tag1', 'tag2']; + const tags = ['tag1']; const mockResponse: RestNodeCollection = { Nodes: [ { @@ -1938,7 +1938,7 @@ describe('CellsAPI', () => { Status: { Deleted: 'Not', }, - Metadata: [{Namespace: 'usermeta-tags', Term: JSON.stringify(tags.join(','))}], + Metadata: [{Namespace: 'usermeta-tags', Term: 'tag1', Operation: 'Should'}], }, Flags: ['WithPreSignedURLs'], Limit: '10', @@ -1947,6 +1947,34 @@ describe('CellsAPI', () => { expect(result).toEqual(mockResponse); }); + it('creates one Should metadata filter per selected tag', async () => { + const searchPhrase = 'test'; + const tags = ['tag1', 'tag2']; + const mockResponse: RestNodeCollection = {Nodes: []} as RestNodeCollection; + + mockNodeServiceApi.lookup.mockResolvedValueOnce(createMockResponse(mockResponse)); + + await cellsAPI.searchNodes({phrase: searchPhrase, tags}); + + expect(mockNodeServiceApi.lookup).toHaveBeenCalledWith({ + Scope: {Root: {Path: ''}, Recursive: true}, + Filters: { + Text: {SearchIn: 'BaseName', Term: searchPhrase}, + Type: 'UNKNOWN', + Status: { + Deleted: 'Not', + }, + Metadata: [ + {Namespace: 'usermeta-tags', Term: 'tag1', Operation: 'Should'}, + {Namespace: 'usermeta-tags', Term: 'tag2', Operation: 'Should'}, + ], + }, + Flags: ['WithPreSignedURLs'], + Limit: '10', + Offset: '0', + }); + }); + it('handles empty tags array', async () => { const searchPhrase = 'test'; const tags: string[] = []; @@ -2249,7 +2277,7 @@ describe('CellsAPI', () => { Type: 'UNKNOWN', Status: {Deleted: 'Not', HasPublicLink: true}, Metadata: [ - {Namespace: 'usermeta-tags', Term: JSON.stringify(tags.join(','))}, + {Namespace: 'usermeta-tags', Term: 'important', Operation: 'Should'}, {Namespace: 'mime', Term: 'image/*', Operation: 'Must'}, {Namespace: 'usermeta-owner-uuid', Term: JSON.stringify(creatorId), Operation: 'Must'}, ], diff --git a/libraries/api-client/src/cells/cellsApi.ts b/libraries/api-client/src/cells/cellsApi.ts index c1c3268cd41..648df68d802 100644 --- a/libraries/api-client/src/cells/cellsApi.ts +++ b/libraries/api-client/src/cells/cellsApi.ts @@ -20,6 +20,7 @@ import {AxiosHeaders} from 'axios'; import { NodeServiceApi, + LookupFilterMetaFilter, RestLookupRequest, RestCreateCheckResponse, RestDeleteVersionResponse, @@ -55,6 +56,17 @@ const USER_META_TAGS_NAMESPACE = 'usermeta-tags'; const USER_META_OWNER_UUID_NAMESPACE = 'usermeta-owner-uuid'; const MIME_NAMESPACE = 'mime'; +// Each selected tag is sent as its own metadata filter with the `Should` operation so the +// backend applies OR semantics across tags (a node matching any selected tag is returned). +// Matches the iOS client shape in WireMessaging RestAPI.swift. +function createTagMetadataFilters(tags: string[]): LookupFilterMetaFilter[] { + return tags.map(tag => ({ + Namespace: USER_META_TAGS_NAMESPACE, + Term: tag, + Operation: 'Should', + })); +} + // TODO: remove the apiKey (from pydio and s3) once the Pydio backend has fully support for the auth with the Wire's access token // If it's passed we use it to authenticate, instead of the access token interface CellsConfig { @@ -450,6 +462,7 @@ export class CellsAPI { throw new Error(CONFIGURATION_ERROR); } + const tagMetadataFilters = createTagMetadataFilters(tags ?? []); const mimeOp: 'Should' | 'Must' = mimeTypes !== undefined && mimeTypes.length > 1 ? 'Should' : 'Must'; const creatorOp: 'Should' | 'Must' = creatorIds !== undefined && creatorIds.length > 1 ? 'Should' : 'Must'; @@ -468,9 +481,7 @@ export class CellsAPI { ...(hasPublicLink !== undefined ? {HasPublicLink: hasPublicLink} : {}), }, Metadata: [ - ...(tags !== undefined && tags.length > 0 - ? [{Namespace: USER_META_TAGS_NAMESPACE, Term: this.transformTagsToJson(tags)}] - : []), + ...tagMetadataFilters, ...(mimeTypes?.map(term => ({Namespace: MIME_NAMESPACE, Term: term, Operation: mimeOp})) ?? []), ...(creatorIds?.map(term => ({ Namespace: USER_META_OWNER_UUID_NAMESPACE,