diff --git a/libs/api/repository/src/lib/gn4/elasticsearch/elasticsearch.service.spec.ts b/libs/api/repository/src/lib/gn4/elasticsearch/elasticsearch.service.spec.ts index d6069868c..acedf4d05 100644 --- a/libs/api/repository/src/lib/gn4/elasticsearch/elasticsearch.service.spec.ts +++ b/libs/api/repository/src/lib/gn4/elasticsearch/elasticsearch.service.spec.ts @@ -399,6 +399,37 @@ describe('ElasticsearchService', () => { }, }) }) + it('handle values expressed as reg exp', () => { + const query = service['buildPayloadQuery']( + { + Org: { + '/world.*/': true, + '/*country^[fr|en]/': false, + }, + }, + {}, + [] + ) + expect(query).toMatchObject({ + bool: { + filter: [ + { + terms: { + isTemplate: ['n'], + }, + }, + { + query_string: { + query: 'Org:(/world.*/ OR -/*country^[fr|en]/)', + }, + }, + { + ids: { values: [] }, + }, + ], + }, + }) + }) describe('any has special characters', () => { let query beforeEach(() => { @@ -857,14 +888,16 @@ describe('ElasticsearchService', () => { ).toStrictEqual({ myFilters: { filters: { - filter1: { - query_string: { query: 'field1:(100)' }, - }, - filter2: { - query_string: { query: 'field2:("value1" OR "value3")' }, - }, - filter3: { - query_string: { query: 'my own query' }, + filters: { + filter1: { + query_string: { query: 'field1:(100)' }, + }, + filter2: { + query_string: { query: 'field2:("value1" OR "value3")' }, + }, + filter3: { + query_string: { query: 'my own query' }, + }, }, }, }, diff --git a/libs/api/repository/src/lib/gn4/elasticsearch/elasticsearch.service.ts b/libs/api/repository/src/lib/gn4/elasticsearch/elasticsearch.service.ts index 8c33397cf..dacea5003 100644 --- a/libs/api/repository/src/lib/gn4/elasticsearch/elasticsearch.service.ts +++ b/libs/api/repository/src/lib/gn4/elasticsearch/elasticsearch.service.ts @@ -213,6 +213,7 @@ export class ElasticsearchService { } private filtersToQueryString(filters: FieldFilters): string { + const addQuote = (key: string) => (/^\/.+\/$/.test(key) ? key : `"${key}"`) const makeQuery = (filter: FieldFilter): string => { if (typeof filter === 'string') { return filter @@ -220,9 +221,9 @@ export class ElasticsearchService { return Object.keys(filter) .map((key) => { if (filter[key] === true) { - return `"${key}"` + return addQuote(key) } - return `-"${key}"` + return `-${addQuote(key)}` }) .join(' OR ') } @@ -463,20 +464,22 @@ export class ElasticsearchService { switch (aggregation.type) { case 'filters': return { - filters: Object.keys(aggregation.filters).reduce((prev, curr) => { - const filter = aggregation.filters[curr] - return { - ...prev, - [curr]: { - query_string: { - query: - typeof filter === 'string' - ? filter - : this.filtersToQueryString(filter), + filters: { + filters: Object.keys(aggregation.filters).reduce((prev, curr) => { + const filter = aggregation.filters[curr] + return { + ...prev, + [curr]: { + query_string: { + query: + typeof filter === 'string' + ? filter + : this.filtersToQueryString(filter), + }, }, - }, - } - }, {}), + } + }, {}), + }, } case 'terms': return { diff --git a/libs/feature/search/src/lib/utils/service/fields.service.ts b/libs/feature/search/src/lib/utils/service/fields.service.ts index 016b3162f..35fe8a442 100644 --- a/libs/feature/search/src/lib/utils/service/fields.service.ts +++ b/libs/feature/search/src/lib/utils/service/fields.service.ts @@ -1,6 +1,7 @@ import { Injectable, Injector } from '@angular/core' import { AbstractSearchField, + AvailableServicesField, FieldValue, FullTextSearchField, IsSpatialSearchField, @@ -87,6 +88,7 @@ export class FieldsService { 'key' ), user: new UserSearchField(this.injector), + availableServices: new AvailableServicesField(this.injector), } as Record get supportedFields() { diff --git a/libs/feature/search/src/lib/utils/service/fields.spec.ts b/libs/feature/search/src/lib/utils/service/fields.spec.ts index 39facadac..44548575a 100644 --- a/libs/feature/search/src/lib/utils/service/fields.spec.ts +++ b/libs/feature/search/src/lib/utils/service/fields.spec.ts @@ -1,13 +1,14 @@ import { lastValueFrom, of } from 'rxjs' import { AbstractSearchField, + AvailableServicesField, FullTextSearchField, IsSpatialSearchField, - TranslatedSearchField, LicenseSearchField, + MultilingualSearchField, OrganizationSearchField, SimpleSearchField, - MultilingualSearchField, + TranslatedSearchField, UserSearchField, } from './fields' import { TestBed } from '@angular/core/testing' @@ -29,7 +30,6 @@ class ElasticsearchServiceMock { class RecordsRepositoryMock { aggregate = jest.fn((aggregations) => { const aggName = Object.keys(aggregations)[0] - const sortType = aggregations[aggName].sort[1] if (aggName.startsWith('is')) return of({ [aggName]: { @@ -118,6 +118,21 @@ class RecordsRepositoryMock { ], }, }) + if (aggName === 'availableServices') + return of({ + availableServices: { + buckets: [ + { + term: 'view', + count: 10, + }, + { + term: 'download', + count: 5, + }, + ], + }, + }) const buckets = [ { term: 'First value', @@ -136,6 +151,7 @@ class RecordsRepositoryMock { count: 1, }, ] + const sortType = aggregations[aggName].sort?.[1] if (sortType === 'count') { buckets.sort((a, b) => b.count - a.count) } @@ -686,6 +702,7 @@ describe('search fields implementations', () => { }) }) }) + describe('UserSearchField', () => { beforeEach(() => { searchField = new UserSearchField(injector) @@ -723,4 +740,60 @@ describe('search fields implementations', () => { }) }) }) + + describe('AvailableServicesField', () => { + beforeEach(() => { + searchField = new AvailableServicesField(injector) + }) + describe('#getAvailableValues', () => { + let values + beforeEach(async () => { + values = await lastValueFrom(searchField.getAvailableValues()) + }) + it('returns the available values', () => { + expect(values).toEqual([ + { + label: 'search.filters.availableServices.view (10)', + value: 'view', + }, + { + label: 'search.filters.availableServices.download (5)', + value: 'download', + }, + ]) + }) + }) + describe('#getFiltersForValues', () => { + let filter + beforeEach(async () => { + filter = await lastValueFrom( + searchField.getFiltersForValues(['view', 'download']) + ) + }) + it('returns filter for both values', () => { + expect(filter).toEqual({ + linkProtocol: { + '/OGC:WFS.*/': true, + '/OGC:WMT?S.*/': true, + }, + }) + }) + }) + describe('#getValuesForFilters', () => { + let values + beforeEach(async () => { + values = await lastValueFrom( + searchField.getValuesForFilter({ + linkProtocol: { + '/OGC:WFS.*/': false, + '/OGC:WMT?S.*/': true, + }, + }) + ) + }) + it('returns value with an enabled filter', () => { + expect(values).toEqual(['view']) + }) + }) + }) }) diff --git a/libs/feature/search/src/lib/utils/service/fields.ts b/libs/feature/search/src/lib/utils/service/fields.ts index 4a377ec96..33f474ca4 100644 --- a/libs/feature/search/src/lib/utils/service/fields.ts +++ b/libs/feature/search/src/lib/utils/service/fields.ts @@ -9,6 +9,7 @@ import { PlatformServiceInterface } from '@geonetwork-ui/common/domain/platform. import { AggregationBuckets, AggregationsParams, + FieldFilter, FieldFilterByExpression, FieldFilters, TermBucket, @@ -372,3 +373,57 @@ export class UserSearchField extends SimpleSearchField { return undefined } } + +marker('search.filters.availableServices.view') +marker('search.filters.availableServices.download') + +export class AvailableServicesField extends SimpleSearchField { + private translateService = this.injector.get(TranslateService) + + constructor(injector: Injector) { + super('availableServices', injector, 'asc') + } + + linkProtocolViewFilter = '/OGC:WMT?S.*/' + linkProtocolDownloadFilter = '/OGC:WFS.*/' + + protected async getBucketLabel(bucket: TermBucket) { + return firstValueFrom( + this.translateService.get( + `search.filters.availableServices.${bucket.term}` + ) + ) + } + + protected getAggregations(): AggregationsParams { + return { + availableServices: { + type: 'filters', + filters: { + view: `+linkProtocol:${this.linkProtocolViewFilter}`, + download: `+linkProtocol:${this.linkProtocolDownloadFilter}`, + }, + }, + } + } + + getFiltersForValues(values: FieldValue[]): Observable { + const filters: FieldFilter = {} + if (values.includes('view')) filters[this.linkProtocolViewFilter] = true + if (values.includes('download')) + filters[this.linkProtocolDownloadFilter] = true + + return of({ + linkProtocol: filters, + }) + } + + getValuesForFilter(filters: FieldFilters): Observable { + const linkFilter = filters.linkProtocol + if (!linkFilter) return of([]) + const values = [] + if (linkFilter[this.linkProtocolViewFilter]) values.push('view') + if (linkFilter[this.linkProtocolDownloadFilter]) values.push('download') + return of(values) + } +}