From 279879cb1a185b8e9d63b7ffbc7a0047d8a98644 Mon Sep 17 00:00:00 2001 From: Guillaume DE OLIVEIRA Date: Fri, 21 Feb 2025 12:20:47 +0100 Subject: [PATCH 01/14] GSIGNGPF-39-load attribute descriptions embedded ( feature catalog) --- .../src/lib/gn4/gn4-repository.spec.ts | 73 ++++++++++++++++++- .../repository/src/lib/gn4/gn4-repository.ts | 27 ++++++- .../src/lib/model/record/metadata.model.ts | 3 + .../records-repository.interface.ts | 6 +- .../record/src/lib/state/mdview.actions.ts | 16 ++++ .../record/src/lib/state/mdview.effects.ts | 30 +++++++- .../record/src/lib/state/mdview.facade.ts | 18 +++++ .../record/src/lib/state/mdview.reducer.ts | 29 ++++++++ .../src/lib/state/mdview.selectors.spec.ts | 3 + .../record/src/lib/state/mdview.selectors.ts | 8 ++ 10 files changed, 209 insertions(+), 4 deletions(-) diff --git a/libs/api/repository/src/lib/gn4/gn4-repository.spec.ts b/libs/api/repository/src/lib/gn4/gn4-repository.spec.ts index cfb0b56d1b..f395e8220f 100644 --- a/libs/api/repository/src/lib/gn4/gn4-repository.spec.ts +++ b/libs/api/repository/src/lib/gn4/gn4-repository.spec.ts @@ -20,7 +20,10 @@ import { simpleDatasetRecordFixture, duplicateDatasetRecordAsXmlFixture, } from '@geonetwork-ui/common/fixtures' -import { CatalogRecord } from '@geonetwork-ui/common/domain/model/record' +import { + CatalogRecord, + DatasetFeatureCatalog, +} from '@geonetwork-ui/common/domain/model/record' import { map } from 'rxjs/operators' import { HttpErrorResponse } from '@angular/common/http' import { @@ -92,6 +95,7 @@ class RecordsApiServiceMock { ) deleteRecord = jest.fn(() => of({})) create = jest.fn(() => of('1234-5678')) + getFeatureCatalog = jest.fn(() => of({})) } class PlatformServiceInterfaceMock { @@ -253,6 +257,73 @@ describe('Gn4Repository', () => { }) }) }) + + describe('getFeatureCatalog', () => { + let catalog: DatasetFeatureCatalog + const mockFeatureCatalogResponse = { + decodeMap: { + feature1: ['memberName', 'definition'], + feature2: ['name2', 'title2'], + feature3: ['name3', 'title3'], + }, + } + + describe('when feature catalog exists', () => { + beforeEach(async () => { + ;(gn4RecordsApi.getFeatureCatalog as jest.Mock).mockReturnValue( + of(mockFeatureCatalogResponse) + ) + catalog = await lastValueFrom(repository.getFeatureCatalog('1234-5678')) + }) + + it('calls the API with correct parameters', () => { + expect(gn4RecordsApi.getFeatureCatalog).toHaveBeenCalledWith( + '1234-5678', + undefined + ) + }) + + it('returns the feature catalog with mapped features', () => { + expect(catalog).toEqual({ + features: [ + { name: 'memberName', title: 'definition' }, + { name: 'name2', title: 'title2' }, + { name: 'name3', title: 'title3' }, + ], + }) + }) + }) + + describe('when feature catalog does not exist', () => { + beforeEach(async () => { + ;(gn4RecordsApi.getFeatureCatalog as jest.Mock).mockReturnValue(of({})) + catalog = await lastValueFrom(repository.getFeatureCatalog('1234-5678')) + }) + + it('returns null when no decode map is present', () => { + expect(catalog).toBe(null) + }) + }) + + describe('with approved version parameter', () => { + beforeEach(async () => { + ;(gn4RecordsApi.getFeatureCatalog as jest.Mock).mockReturnValue( + of(mockFeatureCatalogResponse) + ) + catalog = await lastValueFrom( + repository.getFeatureCatalog('1234-5678', true) + ) + }) + + it('calls the API with approved parameter', () => { + expect(gn4RecordsApi.getFeatureCatalog).toHaveBeenCalledWith( + '1234-5678', + true + ) + }) + }) + }) + describe('getSimilarRecords', () => { let results: CatalogRecord[] beforeEach(async () => { diff --git a/libs/api/repository/src/lib/gn4/gn4-repository.ts b/libs/api/repository/src/lib/gn4/gn4-repository.ts index 15f27fe0d4..9d26a4c067 100644 --- a/libs/api/repository/src/lib/gn4/gn4-repository.ts +++ b/libs/api/repository/src/lib/gn4/gn4-repository.ts @@ -12,7 +12,10 @@ import { Iso19139Converter, } from '@geonetwork-ui/api/metadata-converter' import { PublicationVersionError } from '@geonetwork-ui/common/domain/model/error' -import { CatalogRecord } from '@geonetwork-ui/common/domain/model/record' +import { + CatalogRecord, + DatasetFeatureCatalog, +} from '@geonetwork-ui/common/domain/model/record' import { Aggregations, AggregationsParams, @@ -27,6 +30,7 @@ import { RecordsRepositoryInterface } from '@geonetwork-ui/common/domain/reposit import { RecordsApiService, SearchApiService, + FeatureResponseApiModel, } from '@geonetwork-ui/data-access/gn4' import { combineLatest, @@ -137,6 +141,27 @@ export class Gn4Repository implements RecordsRepositoryInterface { ) } + getFeatureCatalog( + metadataUuid: string, + approvedVersion?: boolean + ): Observable { + return this.gn4RecordsApi + .getFeatureCatalog(metadataUuid, approvedVersion) + .pipe( + map((results: FeatureResponseApiModel) => { + if (results.decodeMap) { + const features = Object.keys(results.decodeMap).map((key) => { + const feature = results.decodeMap[key] + return { name: feature[0], title: feature[1] } + }) + return { features } as DatasetFeatureCatalog + } + return null + }), + switchMap((record) => (record ? of(record) : of(null))) + ) + } + getSimilarRecords(similarTo: CatalogRecord): Observable { return this.gn4SearchApi .search( diff --git a/libs/common/domain/src/lib/model/record/metadata.model.ts b/libs/common/domain/src/lib/model/record/metadata.model.ts index 94b9c01b5b..09b4afadaf 100644 --- a/libs/common/domain/src/lib/model/record/metadata.model.ts +++ b/libs/common/domain/src/lib/model/record/metadata.model.ts @@ -226,6 +226,9 @@ export interface DatasetRecord extends BaseRecord { temporalExtents: Array spatialRepresentation?: SpatialRepresentationType } +export type DatasetFeatureCatalog = { + features: Array<{ name: string; title: string }> +} export interface ServiceEndpoint { endpointUrl: URL diff --git a/libs/common/domain/src/lib/repository/records-repository.interface.ts b/libs/common/domain/src/lib/repository/records-repository.interface.ts index bfc85c5513..dfb25ea27d 100644 --- a/libs/common/domain/src/lib/repository/records-repository.interface.ts +++ b/libs/common/domain/src/lib/repository/records-repository.interface.ts @@ -6,12 +6,16 @@ import { SearchParams, SearchResults, } from '../model/search' -import { CatalogRecord } from '../model/record' +import { CatalogRecord, DatasetFeatureCatalog } from '../model/record' export abstract class RecordsRepositoryInterface { abstract search(params: SearchParams): Observable abstract getMatchesCount(filters: FieldFilters): Observable abstract getRecord(uniqueIdentifier: string): Observable + abstract getFeatureCatalog( + metadataUuid: string, + approvedVersion?: boolean + ): Observable abstract aggregate(params: AggregationsParams): Observable abstract getSimilarRecords( similarTo: CatalogRecord diff --git a/libs/feature/record/src/lib/state/mdview.actions.ts b/libs/feature/record/src/lib/state/mdview.actions.ts index 9cc84be989..7b1b1354af 100644 --- a/libs/feature/record/src/lib/state/mdview.actions.ts +++ b/libs/feature/record/src/lib/state/mdview.actions.ts @@ -2,6 +2,7 @@ import { DatavizConfigurationModel } from '@geonetwork-ui/common/domain/model/da import { createAction, props } from '@ngrx/store' import { CatalogRecord, + DatasetFeatureCatalog, UserFeedback, } from '@geonetwork-ui/common/domain/model/record' @@ -28,6 +29,21 @@ export const loadFullMetadataFailure = createAction( props<{ otherError?: string; notFound?: boolean }>() ) +export const loadFeatureCatalogAttributes = createAction( + '[Metadata view] Load catalog attributes of the metadata', + props<{ metadataUuid: string; approvedVersion?: boolean }>() +) + +export const loadFeatureCatalogAttributesSuccess = createAction( + '[Metadata view] Load full metadata success', + props<{ datasetCatalog: DatasetFeatureCatalog }>() +) + +export const loadFeatureCatalogAttributesFailure = createAction( + '[Metadata view] Load full metadata failure', + props<{ otherError?: string; notFound?: boolean }>() +) + export const closeMetadata = createAction('[Metadata view] close') /* diff --git a/libs/feature/record/src/lib/state/mdview.effects.ts b/libs/feature/record/src/lib/state/mdview.effects.ts index cc97d7918c..6247ecad86 100644 --- a/libs/feature/record/src/lib/state/mdview.effects.ts +++ b/libs/feature/record/src/lib/state/mdview.effects.ts @@ -5,7 +5,6 @@ import { catchError, map, switchMap } from 'rxjs/operators' import * as MdViewActions from './mdview.actions' import { RecordsRepositoryInterface } from '@geonetwork-ui/common/domain/repository/records-repository.interface' import { PlatformServiceInterface } from '@geonetwork-ui/common/domain/platform.service.interface' - @Injectable() export class MdViewEffects { constructor( @@ -32,6 +31,35 @@ export class MdViewEffects { ) ) ) + /* + Feature Catalog effects + */ + loadCatalogAttributes = createEffect(() => + this.actions$.pipe( + ofType(MdViewActions.loadFeatureCatalogAttributes), + switchMap(({ metadataUuid, approvedVersion }) => + this.recordsRepository.getFeatureCatalog(metadataUuid, approvedVersion) + ), + map((record) => { + if (record === null) { + return MdViewActions.loadFeatureCatalogAttributesFailure({ + notFound: true, + }) + } + + return MdViewActions.loadFeatureCatalogAttributesSuccess({ + datasetCatalog: record, + }) + }), + catchError((error) => + of( + MdViewActions.loadFeatureCatalogAttributesFailure({ + otherError: error.message, + }) + ) + ) + ) + ) /* Related effects diff --git a/libs/feature/record/src/lib/state/mdview.facade.ts b/libs/feature/record/src/lib/state/mdview.facade.ts index bba7c3ea6e..cbae8033d0 100644 --- a/libs/feature/record/src/lib/state/mdview.facade.ts +++ b/libs/feature/record/src/lib/state/mdview.facade.ts @@ -52,6 +52,11 @@ export class MdViewFacade { filter((md) => !!md) ) + featureCatalog$ = this.store.pipe( + select(MdViewSelectors.getFeatureCatalog), + filter((fc) => !!fc) + ) + isIncomplete$ = this.store.pipe( select(MdViewSelectors.getMetadataIsIncomplete), filter((incomplete) => incomplete !== null) @@ -204,4 +209,17 @@ export class MdViewFacade { loadUserFeedbacks(datasetUuid: string) { this.store.dispatch(MdViewActions.loadUserFeedbacks({ datasetUuid })) } + + /** + * FeatureCatalog + */ + + loadFeatureCatalog(metadataUuid: string, approvedVersion = false) { + this.store.dispatch( + MdViewActions.loadFeatureCatalogAttributes({ + metadataUuid, + approvedVersion, + }) + ) + } } diff --git a/libs/feature/record/src/lib/state/mdview.reducer.ts b/libs/feature/record/src/lib/state/mdview.reducer.ts index e09622e089..236f78a441 100644 --- a/libs/feature/record/src/lib/state/mdview.reducer.ts +++ b/libs/feature/record/src/lib/state/mdview.reducer.ts @@ -3,6 +3,7 @@ import * as MetadataViewActions from './mdview.actions' import { DatavizConfigurationModel } from '@geonetwork-ui/common/domain/model/dataviz/dataviz-configuration.model' import { CatalogRecord, + DatasetFeatureCatalog, UserFeedback, } from '@geonetwork-ui/common/domain/model/record' @@ -17,6 +18,8 @@ export interface MetadataViewState { allUserFeedbacksLoading: boolean addUserFeedbackLoading: boolean chartConfig?: DatavizConfigurationModel + featureCatalog?: DatasetFeatureCatalog + featureCatalogLoading: boolean } export const initialMetadataViewState: MetadataViewState = { @@ -24,6 +27,7 @@ export const initialMetadataViewState: MetadataViewState = { loadingFull: false, allUserFeedbacksLoading: false, addUserFeedbackLoading: false, + featureCatalogLoading: false, } const metadataViewReducer = createReducer( @@ -105,6 +109,31 @@ const metadataViewReducer = createReducer( addUserFeedbackLoading: false, allUserFeedbacksLoading: false, }) + ), + + /** + * FeatureCatalog reducers + */ + + on(MetadataViewActions.loadFeatureCatalogAttributes, (state) => ({ + ...state, + featureCatalogLoading: true, + })), + on( + MetadataViewActions.loadFeatureCatalogAttributesSuccess, + (state, { datasetCatalog }) => ({ + ...state, + featureCatalog: datasetCatalog, + featureCatalogLoading: false, + }) + ), + on( + MetadataViewActions.loadFeatureCatalogAttributesFailure, + (state, { otherError, notFound }) => ({ + ...state, + error: { otherError, notFound }, + featureCatalogLoading: false, + }) ) ) diff --git a/libs/feature/record/src/lib/state/mdview.selectors.spec.ts b/libs/feature/record/src/lib/state/mdview.selectors.spec.ts index 23524200d6..50a76e17cc 100644 --- a/libs/feature/record/src/lib/state/mdview.selectors.spec.ts +++ b/libs/feature/record/src/lib/state/mdview.selectors.spec.ts @@ -41,6 +41,7 @@ describe('MdView Selectors', () => { error: null, allUserFeedbacksLoading: false, addUserFeedbackLoading: false, + featureCatalogLoading: false, }) expect(results).toBe(null) }) @@ -73,6 +74,7 @@ describe('MdView Selectors', () => { error: null, allUserFeedbacksLoading: false, addUserFeedbackLoading: false, + featureCatalogLoading: false, }) expect(results).toBe(null) }) @@ -108,6 +110,7 @@ describe('MdView Selectors', () => { error: null, allUserFeedbacksLoading: false, addUserFeedbackLoading: false, + featureCatalogLoading: false, }) expect(results).toBe(null) }) diff --git a/libs/feature/record/src/lib/state/mdview.selectors.ts b/libs/feature/record/src/lib/state/mdview.selectors.ts index bb035e12a7..06abf90db2 100644 --- a/libs/feature/record/src/lib/state/mdview.selectors.ts +++ b/libs/feature/record/src/lib/state/mdview.selectors.ts @@ -64,3 +64,11 @@ export const getAddUserFeedbacksLoading = createSelector( getMdViewState, (state: MetadataViewState) => state.addUserFeedbackLoading ) + +/* + Feature Catalog Selectors +*/ +export const getFeatureCatalog = createSelector( + getMdViewState, + (state: MetadataViewState) => state.featureCatalog +) From cf19431ae10f122ff858ff1275a8322a75eb7757 Mon Sep 17 00:00:00 2001 From: Guillaume DE OLIVEIRA Date: Wed, 26 Feb 2025 15:40:15 +0100 Subject: [PATCH 02/14] mapping des featureTypes dans extras --- .../metadata-converter/src/lib/gn4/gn4.field.mapper.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/libs/api/metadata-converter/src/lib/gn4/gn4.field.mapper.ts b/libs/api/metadata-converter/src/lib/gn4/gn4.field.mapper.ts index c858149215..9d7dda123e 100644 --- a/libs/api/metadata-converter/src/lib/gn4/gn4.field.mapper.ts +++ b/libs/api/metadata-converter/src/lib/gn4/gn4.field.mapper.ts @@ -277,6 +277,14 @@ export class Gn4FieldMapper { }, output ), + featureTypes: (output, source) => + this.addExtra( + { + featureTypes: selectField(source, 'featureTypes'), + }, + output + ), + isPublishedToAll: (output, source) => this.addExtra( { From 8d0ae280373c286332230c9e06ed24bab77fca22 Mon Sep 17 00:00:00 2001 From: Guillaume DE OLIVEIRA Date: Wed, 26 Feb 2025 15:55:36 +0100 Subject: [PATCH 03/14] UT adaptation --- libs/api/metadata-converter/src/lib/gn4/gn4.converter.spec.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/libs/api/metadata-converter/src/lib/gn4/gn4.converter.spec.ts b/libs/api/metadata-converter/src/lib/gn4/gn4.converter.spec.ts index 20d6b52f15..072df11066 100644 --- a/libs/api/metadata-converter/src/lib/gn4/gn4.converter.spec.ts +++ b/libs/api/metadata-converter/src/lib/gn4/gn4.converter.spec.ts @@ -2363,6 +2363,7 @@ describe('Gn4Converter', () => { isPublishedToAll: true, id: '53583', favoriteCount: 0, + featureTypes: [], catalogUuid: 'metawal.wallonie.be', edit: true, }, @@ -2642,6 +2643,7 @@ describe('Gn4Converter', () => { extras: { catalogUuid: 'metawal.wallonie.be', favoriteCount: 0, + featureTypes: [], id: '1215', isOpenData: false, isPublishedToAll: true, From dcbadc452470ea12194de652945b94b52d0b67bc Mon Sep 17 00:00:00 2001 From: Guillaume DE OLIVEIRA Date: Thu, 27 Feb 2025 10:46:55 +0100 Subject: [PATCH 04/14] =?UTF-8?q?Commit=20interm=C3=A9diaire?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../repository/src/lib/gn4/gn4-repository.ts | 42 ++++++++++++------- .../records-repository.interface.ts | 2 +- .../record/src/lib/state/mdview.actions.ts | 2 +- .../record/src/lib/state/mdview.effects.ts | 4 +- .../src/lib/default/state/router.effects.ts | 3 ++ 5 files changed, 33 insertions(+), 20 deletions(-) diff --git a/libs/api/repository/src/lib/gn4/gn4-repository.ts b/libs/api/repository/src/lib/gn4/gn4-repository.ts index 9d26a4c067..b6f80cc8f3 100644 --- a/libs/api/repository/src/lib/gn4/gn4-repository.ts +++ b/libs/api/repository/src/lib/gn4/gn4-repository.ts @@ -142,24 +142,34 @@ export class Gn4Repository implements RecordsRepositoryInterface { } getFeatureCatalog( - metadataUuid: string, + metadata: CatalogRecord, approvedVersion?: boolean ): Observable { - return this.gn4RecordsApi - .getFeatureCatalog(metadataUuid, approvedVersion) - .pipe( - map((results: FeatureResponseApiModel) => { - if (results.decodeMap) { - const features = Object.keys(results.decodeMap).map((key) => { - const feature = results.decodeMap[key] - return { name: feature[0], title: feature[1] } - }) - return { features } as DatasetFeatureCatalog - } - return null - }), - switchMap((record) => (record ? of(record) : of(null))) - ) + const featureTypes = metadata.extras['featureTypes'] as any[] + if (!featureTypes || featureTypes.length === 0) { + return this.gn4RecordsApi + .getFeatureCatalog(metadata.uniqueIdentifier, approvedVersion) + .pipe( + map((results: FeatureResponseApiModel) => { + if (results.decodeMap) { + const features = Object.keys(results.decodeMap).map((key) => { + const feature = results.decodeMap[key] + return { name: feature[0], title: feature[1] } + }) + return { features } as DatasetFeatureCatalog + } + return null + }), + switchMap((record) => (record ? of(record) : of(null))) + ) + } else { + return of({ + features: featureTypes[0]?.attributeTable?.map((attr) => ({ + name: attr.typeName, + title: attr.definition, + })), + } as DatasetFeatureCatalog) + } } getSimilarRecords(similarTo: CatalogRecord): Observable { diff --git a/libs/common/domain/src/lib/repository/records-repository.interface.ts b/libs/common/domain/src/lib/repository/records-repository.interface.ts index dfb25ea27d..9072c9ca0e 100644 --- a/libs/common/domain/src/lib/repository/records-repository.interface.ts +++ b/libs/common/domain/src/lib/repository/records-repository.interface.ts @@ -13,7 +13,7 @@ export abstract class RecordsRepositoryInterface { abstract getMatchesCount(filters: FieldFilters): Observable abstract getRecord(uniqueIdentifier: string): Observable abstract getFeatureCatalog( - metadataUuid: string, + metadata: CatalogRecord, approvedVersion?: boolean ): Observable abstract aggregate(params: AggregationsParams): Observable diff --git a/libs/feature/record/src/lib/state/mdview.actions.ts b/libs/feature/record/src/lib/state/mdview.actions.ts index 7b1b1354af..cad7130046 100644 --- a/libs/feature/record/src/lib/state/mdview.actions.ts +++ b/libs/feature/record/src/lib/state/mdview.actions.ts @@ -31,7 +31,7 @@ export const loadFullMetadataFailure = createAction( export const loadFeatureCatalogAttributes = createAction( '[Metadata view] Load catalog attributes of the metadata', - props<{ metadataUuid: string; approvedVersion?: boolean }>() + props<{ metadata: CatalogRecord; approvedVersion?: boolean }>() ) export const loadFeatureCatalogAttributesSuccess = createAction( diff --git a/libs/feature/record/src/lib/state/mdview.effects.ts b/libs/feature/record/src/lib/state/mdview.effects.ts index 6247ecad86..2a7ab8a332 100644 --- a/libs/feature/record/src/lib/state/mdview.effects.ts +++ b/libs/feature/record/src/lib/state/mdview.effects.ts @@ -37,8 +37,8 @@ export class MdViewEffects { loadCatalogAttributes = createEffect(() => this.actions$.pipe( ofType(MdViewActions.loadFeatureCatalogAttributes), - switchMap(({ metadataUuid, approvedVersion }) => - this.recordsRepository.getFeatureCatalog(metadataUuid, approvedVersion) + switchMap(({ metadata, approvedVersion }) => + this.recordsRepository.getFeatureCatalog(metadata, approvedVersion) ), map((record) => { if (record === null) { diff --git a/libs/feature/router/src/lib/default/state/router.effects.ts b/libs/feature/router/src/lib/default/state/router.effects.ts index 462b0d7f70..c74a1120be 100644 --- a/libs/feature/router/src/lib/default/state/router.effects.ts +++ b/libs/feature/router/src/lib/default/state/router.effects.ts @@ -121,6 +121,9 @@ export class RouterEffects { title: '', }, }), + MdViewActions.loadFeatureCatalogAttributes({ + metadataUuid: activatedRouteSnapshot.params.metadataUuid, + }), MdViewActions.loadFullMetadata({ uuid: activatedRouteSnapshot.params.metadataUuid, }) From 31b919d203cf3a17984c490500652df2bf37ab7e Mon Sep 17 00:00:00 2001 From: AlitaBernachot Date: Wed, 26 Feb 2025 18:00:12 +0100 Subject: [PATCH 05/14] feat: feature catalog --- conf/default.toml | 2 +- .../src/lib/gn4/types/metadata.model.ts | 5 ++ .../src/lib/gn4/gn4-repository.spec.ts | 4 +- .../repository/src/lib/gn4/gn4-repository.ts | 34 +++++------- .../src/lib/model/record/metadata.model.ts | 1 - .../records-repository.interface.ts | 3 +- .../record/src/lib/state/mdview.actions.ts | 14 ++--- .../record/src/lib/state/mdview.effects.ts | 28 +++++----- .../record/src/lib/state/mdview.facade.ts | 8 ++- .../record/src/lib/state/mdview.reducer.ts | 11 ++-- .../src/lib/state/mdview.selectors.spec.ts | 52 +++++++++++++++++++ .../record/src/lib/state/mdview.selectors.ts | 4 ++ .../src/lib/default/state/router.effects.ts | 3 -- 13 files changed, 106 insertions(+), 63 deletions(-) diff --git a/conf/default.toml b/conf/default.toml index 1325a05793..df6e051e8e 100644 --- a/conf/default.toml +++ b/conf/default.toml @@ -48,7 +48,7 @@ proxy_path = "" [theme] primary_color = "#c82850" secondary_color = "#001638" -main_color = "#555" # All-purpose text color +main_color = "#555" # All-purpose text color background_color = "#fdfbff" # These optional parameters indicate which background should be used for the main header and the text color used diff --git a/libs/api/metadata-converter/src/lib/gn4/types/metadata.model.ts b/libs/api/metadata-converter/src/lib/gn4/types/metadata.model.ts index 3cbfce4b74..03c2d5d2b7 100644 --- a/libs/api/metadata-converter/src/lib/gn4/types/metadata.model.ts +++ b/libs/api/metadata-converter/src/lib/gn4/types/metadata.model.ts @@ -202,4 +202,9 @@ export interface Gn4Record { featured?: boolean guestdownload?: boolean selected?: boolean + related?: Gn4RecordRelated +} + +export interface Gn4RecordRelated { + fcats?: Gn4Record[] } diff --git a/libs/api/repository/src/lib/gn4/gn4-repository.spec.ts b/libs/api/repository/src/lib/gn4/gn4-repository.spec.ts index f395e8220f..4826ef5ccd 100644 --- a/libs/api/repository/src/lib/gn4/gn4-repository.spec.ts +++ b/libs/api/repository/src/lib/gn4/gn4-repository.spec.ts @@ -310,9 +310,7 @@ describe('Gn4Repository', () => { ;(gn4RecordsApi.getFeatureCatalog as jest.Mock).mockReturnValue( of(mockFeatureCatalogResponse) ) - catalog = await lastValueFrom( - repository.getFeatureCatalog('1234-5678', true) - ) + catalog = await lastValueFrom(repository.getFeatureCatalog('1234-5678')) }) it('calls the API with approved parameter', () => { diff --git a/libs/api/repository/src/lib/gn4/gn4-repository.ts b/libs/api/repository/src/lib/gn4/gn4-repository.ts index b6f80cc8f3..aed03d2460 100644 --- a/libs/api/repository/src/lib/gn4/gn4-repository.ts +++ b/libs/api/repository/src/lib/gn4/gn4-repository.ts @@ -128,7 +128,7 @@ export class Gn4Repository implements RecordsRepositoryInterface { return this.gn4SearchApi .search( 'bucket', - null, + ['fcats'], JSON.stringify( this.gn4SearchHelper.getMetadataByIdPayload(uniqueIdentifier) ) @@ -142,27 +142,10 @@ export class Gn4Repository implements RecordsRepositoryInterface { } getFeatureCatalog( - metadata: CatalogRecord, - approvedVersion?: boolean + record: CatalogRecord ): Observable { - const featureTypes = metadata.extras['featureTypes'] as any[] - if (!featureTypes || featureTypes.length === 0) { - return this.gn4RecordsApi - .getFeatureCatalog(metadata.uniqueIdentifier, approvedVersion) - .pipe( - map((results: FeatureResponseApiModel) => { - if (results.decodeMap) { - const features = Object.keys(results.decodeMap).map((key) => { - const feature = results.decodeMap[key] - return { name: feature[0], title: feature[1] } - }) - return { features } as DatasetFeatureCatalog - } - return null - }), - switchMap((record) => (record ? of(record) : of(null))) - ) - } else { + const featureTypes = record.extras['featureTypes'] + if (featureTypes[0]) { return of({ features: featureTypes[0]?.attributeTable?.map((attr) => ({ name: attr.typeName, @@ -170,6 +153,15 @@ export class Gn4Repository implements RecordsRepositoryInterface { })), } as DatasetFeatureCatalog) } + + if (/*record.linkedFeatureCatalog*/ false) { + // TODO: + return this.getRecord('uniqueIdentifierTODO').pipe( + switchMap((record) => this.getFeatureCatalog(record)) + ) + } + + return of(null) } getSimilarRecords(similarTo: CatalogRecord): Observable { diff --git a/libs/common/domain/src/lib/model/record/metadata.model.ts b/libs/common/domain/src/lib/model/record/metadata.model.ts index 09b4afadaf..040de905b6 100644 --- a/libs/common/domain/src/lib/model/record/metadata.model.ts +++ b/libs/common/domain/src/lib/model/record/metadata.model.ts @@ -229,7 +229,6 @@ export interface DatasetRecord extends BaseRecord { export type DatasetFeatureCatalog = { features: Array<{ name: string; title: string }> } - export interface ServiceEndpoint { endpointUrl: URL protocol: string diff --git a/libs/common/domain/src/lib/repository/records-repository.interface.ts b/libs/common/domain/src/lib/repository/records-repository.interface.ts index 9072c9ca0e..39c4d33b9c 100644 --- a/libs/common/domain/src/lib/repository/records-repository.interface.ts +++ b/libs/common/domain/src/lib/repository/records-repository.interface.ts @@ -13,8 +13,7 @@ export abstract class RecordsRepositoryInterface { abstract getMatchesCount(filters: FieldFilters): Observable abstract getRecord(uniqueIdentifier: string): Observable abstract getFeatureCatalog( - metadata: CatalogRecord, - approvedVersion?: boolean + record: CatalogRecord ): Observable abstract aggregate(params: AggregationsParams): Observable abstract getSimilarRecords( diff --git a/libs/feature/record/src/lib/state/mdview.actions.ts b/libs/feature/record/src/lib/state/mdview.actions.ts index cad7130046..8a8d5d19ab 100644 --- a/libs/feature/record/src/lib/state/mdview.actions.ts +++ b/libs/feature/record/src/lib/state/mdview.actions.ts @@ -29,18 +29,18 @@ export const loadFullMetadataFailure = createAction( props<{ otherError?: string; notFound?: boolean }>() ) -export const loadFeatureCatalogAttributes = createAction( - '[Metadata view] Load catalog attributes of the metadata', - props<{ metadata: CatalogRecord; approvedVersion?: boolean }>() +export const loadFeatureCatalog = createAction( + "[Metadata view] Load metadata's feature catalog", + props<{ metadata: CatalogRecord }>() ) -export const loadFeatureCatalogAttributesSuccess = createAction( - '[Metadata view] Load full metadata success', +export const loadFeatureCatalogSuccess = createAction( + '[Metadata view] Load metadata feature catalog success', props<{ datasetCatalog: DatasetFeatureCatalog }>() ) -export const loadFeatureCatalogAttributesFailure = createAction( - '[Metadata view] Load full metadata failure', +export const loadFeatureCatalogFailure = createAction( + '[Metadata view] Load metadata feature catalog failure', props<{ otherError?: string; notFound?: boolean }>() ) diff --git a/libs/feature/record/src/lib/state/mdview.effects.ts b/libs/feature/record/src/lib/state/mdview.effects.ts index 2a7ab8a332..44b12538ff 100644 --- a/libs/feature/record/src/lib/state/mdview.effects.ts +++ b/libs/feature/record/src/lib/state/mdview.effects.ts @@ -1,7 +1,7 @@ import { Injectable } from '@angular/core' import { Actions, createEffect, ofType } from '@ngrx/effects' import { exhaustMap, mergeMap, of } from 'rxjs' -import { catchError, map, switchMap } from 'rxjs/operators' +import { catchError, filter, map, switchMap, take } from 'rxjs/operators' import * as MdViewActions from './mdview.actions' import { RecordsRepositoryInterface } from '@geonetwork-ui/common/domain/repository/records-repository.interface' import { PlatformServiceInterface } from '@geonetwork-ui/common/domain/platform.service.interface' @@ -31,29 +31,25 @@ export class MdViewEffects { ) ) ) - /* - Feature Catalog effects - */ - loadCatalogAttributes = createEffect(() => + + loadFeatureCatalog$ = createEffect(() => this.actions$.pipe( - ofType(MdViewActions.loadFeatureCatalogAttributes), - switchMap(({ metadata, approvedVersion }) => - this.recordsRepository.getFeatureCatalog(metadata, approvedVersion) - ), - map((record) => { - if (record === null) { - return MdViewActions.loadFeatureCatalogAttributesFailure({ + ofType(MdViewActions.loadFullMetadataSuccess), + filter(({ full }) => full !== undefined), + switchMap(({ full }) => this.recordsRepository.getFeatureCatalog(full)), + map((featureCatalog) => { + if (featureCatalog === null) { + return MdViewActions.loadFeatureCatalogFailure({ notFound: true, }) } - - return MdViewActions.loadFeatureCatalogAttributesSuccess({ - datasetCatalog: record, + return MdViewActions.loadFeatureCatalogSuccess({ + datasetCatalog: featureCatalog, }) }), catchError((error) => of( - MdViewActions.loadFeatureCatalogAttributesFailure({ + MdViewActions.loadFeatureCatalogFailure({ otherError: error.message, }) ) diff --git a/libs/feature/record/src/lib/state/mdview.facade.ts b/libs/feature/record/src/lib/state/mdview.facade.ts index cbae8033d0..0e64e0fd41 100644 --- a/libs/feature/record/src/lib/state/mdview.facade.ts +++ b/libs/feature/record/src/lib/state/mdview.facade.ts @@ -213,12 +213,10 @@ export class MdViewFacade { /** * FeatureCatalog */ - - loadFeatureCatalog(metadataUuid: string, approvedVersion = false) { + loadFeatureCatalog(metadata: CatalogRecord) { this.store.dispatch( - MdViewActions.loadFeatureCatalogAttributes({ - metadataUuid, - approvedVersion, + MdViewActions.loadFeatureCatalog({ + metadata, }) ) } diff --git a/libs/feature/record/src/lib/state/mdview.reducer.ts b/libs/feature/record/src/lib/state/mdview.reducer.ts index 236f78a441..9646226c71 100644 --- a/libs/feature/record/src/lib/state/mdview.reducer.ts +++ b/libs/feature/record/src/lib/state/mdview.reducer.ts @@ -20,6 +20,7 @@ export interface MetadataViewState { chartConfig?: DatavizConfigurationModel featureCatalog?: DatasetFeatureCatalog featureCatalogLoading: boolean + featureCatalogError: { notFound?: boolean; otherError?: string } | null } export const initialMetadataViewState: MetadataViewState = { @@ -28,6 +29,7 @@ export const initialMetadataViewState: MetadataViewState = { allUserFeedbacksLoading: false, addUserFeedbackLoading: false, featureCatalogLoading: false, + featureCatalogError: null, } const metadataViewReducer = createReducer( @@ -115,12 +117,13 @@ const metadataViewReducer = createReducer( * FeatureCatalog reducers */ - on(MetadataViewActions.loadFeatureCatalogAttributes, (state) => ({ + on(MetadataViewActions.loadFeatureCatalog, (state) => ({ ...state, + featureCatalogError: null, featureCatalogLoading: true, })), on( - MetadataViewActions.loadFeatureCatalogAttributesSuccess, + MetadataViewActions.loadFeatureCatalogSuccess, (state, { datasetCatalog }) => ({ ...state, featureCatalog: datasetCatalog, @@ -128,10 +131,10 @@ const metadataViewReducer = createReducer( }) ), on( - MetadataViewActions.loadFeatureCatalogAttributesFailure, + MetadataViewActions.loadFeatureCatalogFailure, (state, { otherError, notFound }) => ({ ...state, - error: { otherError, notFound }, + featureCatalogError: { otherError, notFound }, featureCatalogLoading: false, }) ) diff --git a/libs/feature/record/src/lib/state/mdview.selectors.spec.ts b/libs/feature/record/src/lib/state/mdview.selectors.spec.ts index 50a76e17cc..3085ded92d 100644 --- a/libs/feature/record/src/lib/state/mdview.selectors.spec.ts +++ b/libs/feature/record/src/lib/state/mdview.selectors.spec.ts @@ -25,6 +25,20 @@ describe('MdView Selectors', () => { }, loadingFull: false, error: null, + featureCatalog: { + features: [ + { + name: 'feature_1', + title: 'Feature 1', + }, + { + name: 'feature_2', + title: 'Feature 2', + }, + ], + }, + featureCatalogLoading: false, + featureCatalogError: null, } }) @@ -42,6 +56,7 @@ describe('MdView Selectors', () => { allUserFeedbacksLoading: false, addUserFeedbackLoading: false, featureCatalogLoading: false, + featureCatalogError: null, }) expect(results).toBe(null) }) @@ -75,6 +90,7 @@ describe('MdView Selectors', () => { allUserFeedbacksLoading: false, addUserFeedbackLoading: false, featureCatalogLoading: false, + featureCatalogError: null, }) expect(results).toBe(null) }) @@ -111,6 +127,7 @@ describe('MdView Selectors', () => { allUserFeedbacksLoading: false, addUserFeedbackLoading: false, featureCatalogLoading: false, + featureCatalogError: null, }) expect(results).toBe(null) }) @@ -135,5 +152,40 @@ describe('MdView Selectors', () => { expect(results).toEqual([chartConfigMock]) }) }) + + describe('getFeatureCatalog', () => { + it('returns the feature catalog', () => { + const expectedFeatures = { + features: [ + { + name: 'feature_1', + title: 'Feature 1', + }, + { + name: 'feature_2', + title: 'Feature 2', + }, + ], + } + const results = MdViewSelectors.getFeatureCatalog.projector(state) + expect(results).toStrictEqual(expectedFeatures) + }) + }) + + describe('getFeatureCatalogIsLoading', () => { + it('returns false if not loading', () => { + const results = + MdViewSelectors.getFeatureCatalogIsLoading.projector(state) + expect(results).toBe(false) + }) + + it('returns true if loading', () => { + const results = MdViewSelectors.getFeatureCatalogIsLoading.projector({ + ...state, + featureCatalogLoading: true, + }) + expect(results).toBe(true) + }) + }) }) }) diff --git a/libs/feature/record/src/lib/state/mdview.selectors.ts b/libs/feature/record/src/lib/state/mdview.selectors.ts index 06abf90db2..9b30a8582c 100644 --- a/libs/feature/record/src/lib/state/mdview.selectors.ts +++ b/libs/feature/record/src/lib/state/mdview.selectors.ts @@ -72,3 +72,7 @@ export const getFeatureCatalog = createSelector( getMdViewState, (state: MetadataViewState) => state.featureCatalog ) +export const getFeatureCatalogIsLoading = createSelector( + getMdViewState, + (state: MetadataViewState) => state.featureCatalogLoading +) diff --git a/libs/feature/router/src/lib/default/state/router.effects.ts b/libs/feature/router/src/lib/default/state/router.effects.ts index c74a1120be..462b0d7f70 100644 --- a/libs/feature/router/src/lib/default/state/router.effects.ts +++ b/libs/feature/router/src/lib/default/state/router.effects.ts @@ -121,9 +121,6 @@ export class RouterEffects { title: '', }, }), - MdViewActions.loadFeatureCatalogAttributes({ - metadataUuid: activatedRouteSnapshot.params.metadataUuid, - }), MdViewActions.loadFullMetadata({ uuid: activatedRouteSnapshot.params.metadataUuid, }) From 57cf729050fbfe9b49b02c30ee96fabbbf88dc92 Mon Sep 17 00:00:00 2001 From: AlitaBernachot Date: Thu, 27 Feb 2025 18:34:26 +0100 Subject: [PATCH 06/14] feat: feature catalog from related in repo and mapper --- .../src/lib/gn4/gn4.field.mapper.ts | 13 +++++++++++-- libs/api/repository/src/lib/gn4/gn4-repository.ts | 12 ++++++------ 2 files changed, 17 insertions(+), 8 deletions(-) diff --git a/libs/api/metadata-converter/src/lib/gn4/gn4.field.mapper.ts b/libs/api/metadata-converter/src/lib/gn4/gn4.field.mapper.ts index 9d7dda123e..4547e2fa33 100644 --- a/libs/api/metadata-converter/src/lib/gn4/gn4.field.mapper.ts +++ b/libs/api/metadata-converter/src/lib/gn4/gn4.field.mapper.ts @@ -43,7 +43,7 @@ export class Gn4FieldMapper { constructor( private metadataUrlService: MetadataUrlService, private langService: LangService - ) {} + ) { } private lang3 = this.langService.gnLang @@ -190,7 +190,7 @@ export class Gn4FieldMapper { ...output, contactsForResource: [ ...('contactsForResource' in output && - Array.isArray(output.contactsForResource) + Array.isArray(output.contactsForResource) ? output.contactsForResource : []), ...getAsArray(selectField(source, 'contactForResource')).map( @@ -284,6 +284,15 @@ export class Gn4FieldMapper { }, output ), + related: (output, source) => { + // TODO: + return this.addExtra( + { + featureCatalogIdentifier: selectField(source, 'related'), + }, + output + ) + }, isPublishedToAll: (output, source) => this.addExtra( diff --git a/libs/api/repository/src/lib/gn4/gn4-repository.ts b/libs/api/repository/src/lib/gn4/gn4-repository.ts index aed03d2460..efb7cca3fd 100644 --- a/libs/api/repository/src/lib/gn4/gn4-repository.ts +++ b/libs/api/repository/src/lib/gn4/gn4-repository.ts @@ -65,7 +65,7 @@ export class Gn4Repository implements RecordsRepositoryInterface { private gn4Mapper: Gn4Converter, private gn4RecordsApi: RecordsApiService, private platformService: PlatformServiceInterface - ) {} + ) { } search({ filters, @@ -154,9 +154,9 @@ export class Gn4Repository implements RecordsRepositoryInterface { } as DatasetFeatureCatalog) } - if (/*record.linkedFeatureCatalog*/ false) { - // TODO: - return this.getRecord('uniqueIdentifierTODO').pipe( + const featureCatalogIdentifier = record.extras['featureCatalogIdentifier'] + if (featureCatalogIdentifier) { + return this.getRecord(featureCatalogIdentifier).pipe( switchMap((record) => this.getFeatureCatalog(record)) ) } @@ -227,8 +227,8 @@ export class Gn4Repository implements RecordsRepositoryInterface { getRecordPublicationStatus(uniqueIdentifier: string): Observable { return uniqueIdentifier ? this.getRecord(uniqueIdentifier).pipe( - map((record) => record.extras['isPublishedToAll'] as boolean) - ) + map((record) => record.extras['isPublishedToAll'] as boolean) + ) : of(true) } From 50037e666b9f7dc92e9aae0646bfe59b1ec3c6e8 Mon Sep 17 00:00:00 2001 From: Guillaume DE OLIVEIRA Date: Thu, 27 Feb 2025 16:08:37 +0100 Subject: [PATCH 07/14] init xtra test + code review feedbacks --- .../src/lib/gn4/gn4-repository.spec.ts | 48 +++++++++++++++---- .../repository/src/lib/gn4/gn4-repository.ts | 33 ++++++++++--- .../src/lib/model/record/metadata.model.ts | 2 +- 3 files changed, 67 insertions(+), 16 deletions(-) diff --git a/libs/api/repository/src/lib/gn4/gn4-repository.spec.ts b/libs/api/repository/src/lib/gn4/gn4-repository.spec.ts index 4826ef5ccd..617f0bb66f 100644 --- a/libs/api/repository/src/lib/gn4/gn4-repository.spec.ts +++ b/libs/api/repository/src/lib/gn4/gn4-repository.spec.ts @@ -260,6 +260,7 @@ describe('Gn4Repository', () => { describe('getFeatureCatalog', () => { let catalog: DatasetFeatureCatalog + let metadata: CatalogRecord const mockFeatureCatalogResponse = { decodeMap: { feature1: ['memberName', 'definition'], @@ -267,25 +268,54 @@ describe('Gn4Repository', () => { feature3: ['name3', 'title3'], }, } + //TODO rewrite - describe('when feature catalog exists', () => { + describe('when extras feature catalog is defined ', () => { beforeEach(async () => { ;(gn4RecordsApi.getFeatureCatalog as jest.Mock).mockReturnValue( of(mockFeatureCatalogResponse) ) - catalog = await lastValueFrom(repository.getFeatureCatalog('1234-5678')) + metadata = await lastValueFrom(repository.getRecord('1234-5678')) + catalog = await lastValueFrom(repository.getFeatureCatalog(metadata)) }) it('calls the API with correct parameters', () => { expect(gn4RecordsApi.getFeatureCatalog).toHaveBeenCalledWith( - '1234-5678', + 'my-dataset-001', //uniqueid of the record + undefined + ) + }) + + it('returns the feature catalog with mapped features', () => { + expect(catalog).toEqual({ + attributes: [ + { name: 'memberName', title: 'definition' }, + { name: 'name2', title: 'title2' }, + { name: 'name3', title: 'title3' }, + ], + }) + }) + }) + //End todo rewrite + describe('when feature catalog exists, no extras defined', () => { + beforeEach(async () => { + ;(gn4RecordsApi.getFeatureCatalog as jest.Mock).mockReturnValue( + of(mockFeatureCatalogResponse) + ) + metadata = await lastValueFrom(repository.getRecord('1234-5678')) + catalog = await lastValueFrom(repository.getFeatureCatalog(metadata)) + }) + + it('calls the API with correct parameters', () => { + expect(gn4RecordsApi.getFeatureCatalog).toHaveBeenCalledWith( + 'my-dataset-001', //uniqueid of the record undefined ) }) it('returns the feature catalog with mapped features', () => { expect(catalog).toEqual({ - features: [ + attributes: [ { name: 'memberName', title: 'definition' }, { name: 'name2', title: 'title2' }, { name: 'name3', title: 'title3' }, @@ -294,10 +324,10 @@ describe('Gn4Repository', () => { }) }) - describe('when feature catalog does not exist', () => { + describe('when feature catalog does not exist, nor in extras', () => { beforeEach(async () => { ;(gn4RecordsApi.getFeatureCatalog as jest.Mock).mockReturnValue(of({})) - catalog = await lastValueFrom(repository.getFeatureCatalog('1234-5678')) + catalog = await lastValueFrom(repository.getFeatureCatalog(metadata)) }) it('returns null when no decode map is present', () => { @@ -310,12 +340,14 @@ describe('Gn4Repository', () => { ;(gn4RecordsApi.getFeatureCatalog as jest.Mock).mockReturnValue( of(mockFeatureCatalogResponse) ) - catalog = await lastValueFrom(repository.getFeatureCatalog('1234-5678')) + catalog = await lastValueFrom( + repository.getFeatureCatalog(metadata, true) + ) }) it('calls the API with approved parameter', () => { expect(gn4RecordsApi.getFeatureCatalog).toHaveBeenCalledWith( - '1234-5678', + 'my-dataset-001', //uniqueid of the record true ) }) diff --git a/libs/api/repository/src/lib/gn4/gn4-repository.ts b/libs/api/repository/src/lib/gn4/gn4-repository.ts index efb7cca3fd..6ac695c922 100644 --- a/libs/api/repository/src/lib/gn4/gn4-repository.ts +++ b/libs/api/repository/src/lib/gn4/gn4-repository.ts @@ -65,7 +65,7 @@ export class Gn4Repository implements RecordsRepositoryInterface { private gn4Mapper: Gn4Converter, private gn4RecordsApi: RecordsApiService, private platformService: PlatformServiceInterface - ) { } + ) {} search({ filters, @@ -144,14 +144,33 @@ export class Gn4Repository implements RecordsRepositoryInterface { getFeatureCatalog( record: CatalogRecord ): Observable { - const featureTypes = record.extras['featureTypes'] - if (featureTypes[0]) { - return of({ - features: featureTypes[0]?.attributeTable?.map((attr) => ({ + const featureTypes = metadata.extras['featureTypes'] as any[] + if (!featureTypes || featureTypes.length === 0) { + const temp = this.gn4RecordsApi + .getFeatureCatalog(metadata.uniqueIdentifier, approvedVersion) + .pipe( + map((results: FeatureResponseApiModel) => { + if (!results.decodeMap) { + return null + } + const attributes = Object.keys(results.decodeMap).map((key) => { + const attribute = results.decodeMap[key] + return { name: attribute[0], title: attribute[1] } + }) + return { attributes } as DatasetFeatureCatalog + }) + ) + console.log(temp) + return temp + } else { + const temp = of({ + attributes: featureTypes[0]?.attributeTable?.map((attr) => ({ name: attr.typeName, title: attr.definition, })), } as DatasetFeatureCatalog) + console.log(temp) + return temp } const featureCatalogIdentifier = record.extras['featureCatalogIdentifier'] @@ -227,8 +246,8 @@ export class Gn4Repository implements RecordsRepositoryInterface { getRecordPublicationStatus(uniqueIdentifier: string): Observable { return uniqueIdentifier ? this.getRecord(uniqueIdentifier).pipe( - map((record) => record.extras['isPublishedToAll'] as boolean) - ) + map((record) => record.extras['isPublishedToAll'] as boolean) + ) : of(true) } diff --git a/libs/common/domain/src/lib/model/record/metadata.model.ts b/libs/common/domain/src/lib/model/record/metadata.model.ts index 040de905b6..0da5d2577a 100644 --- a/libs/common/domain/src/lib/model/record/metadata.model.ts +++ b/libs/common/domain/src/lib/model/record/metadata.model.ts @@ -227,7 +227,7 @@ export interface DatasetRecord extends BaseRecord { spatialRepresentation?: SpatialRepresentationType } export type DatasetFeatureCatalog = { - features: Array<{ name: string; title: string }> + attributes: Array<{ name: string; title: string }> } export interface ServiceEndpoint { endpointUrl: URL From b32c400ee140497b56a64b2a8431f6b87953f6f3 Mon Sep 17 00:00:00 2001 From: Guillaume DE OLIVEIRA Date: Fri, 28 Feb 2025 16:36:28 +0100 Subject: [PATCH 08/14] fixtures test and adjustments --- .../src/lib/gn4/gn4-repository.spec.ts | 86 +++++++------------ .../repository/src/lib/gn4/gn4-repository.ts | 27 +++--- .../fixtures/src/lib/records.fixtures.ts | 32 +++++++ 3 files changed, 81 insertions(+), 64 deletions(-) diff --git a/libs/api/repository/src/lib/gn4/gn4-repository.spec.ts b/libs/api/repository/src/lib/gn4/gn4-repository.spec.ts index 617f0bb66f..25672209d5 100644 --- a/libs/api/repository/src/lib/gn4/gn4-repository.spec.ts +++ b/libs/api/repository/src/lib/gn4/gn4-repository.spec.ts @@ -79,10 +79,10 @@ class SearchApiServiceMock { class RecordsApiServiceMock { getRecordAs = jest.fn((uuid) => of(` - - ${uuid} - -`).pipe(map((xml) => ({ body: xml }))) + + ${uuid} + + `).pipe(map((xml) => ({ body: xml }))) ) insert = jest.fn(() => of({ @@ -95,7 +95,15 @@ class RecordsApiServiceMock { ) deleteRecord = jest.fn(() => of({})) create = jest.fn(() => of('1234-5678')) - getFeatureCatalog = jest.fn(() => of({})) + getFeatureCatalog = jest.fn((uuid) => + of({ + decodeMap: { + feature1: ['memberName', 'definition'], + feature2: ['name2', 'title2'], + feature3: ['name3', 'title3'], + }, + }) + ) } class PlatformServiceInterfaceMock { @@ -261,48 +269,33 @@ describe('Gn4Repository', () => { describe('getFeatureCatalog', () => { let catalog: DatasetFeatureCatalog let metadata: CatalogRecord - const mockFeatureCatalogResponse = { - decodeMap: { - feature1: ['memberName', 'definition'], - feature2: ['name2', 'title2'], - feature3: ['name3', 'title3'], - }, - } - //TODO rewrite - describe('when extras feature catalog is defined ', () => { beforeEach(async () => { - ;(gn4RecordsApi.getFeatureCatalog as jest.Mock).mockReturnValue( - of(mockFeatureCatalogResponse) - ) - metadata = await lastValueFrom(repository.getRecord('1234-5678')) + metadata = datasetRecordsFixture()[0] catalog = await lastValueFrom(repository.getFeatureCatalog(metadata)) }) - it('calls the API with correct parameters', () => { - expect(gn4RecordsApi.getFeatureCatalog).toHaveBeenCalledWith( - 'my-dataset-001', //uniqueid of the record - undefined - ) + it('should not call the feature catalog API (extras contain the answer) ', () => { + expect(gn4RecordsApi.getFeatureCatalog).not.toHaveBeenCalled() }) it('returns the feature catalog with mapped features', () => { expect(catalog).toEqual({ attributes: [ - { name: 'memberName', title: 'definition' }, - { name: 'name2', title: 'title2' }, - { name: 'name3', title: 'title3' }, + { name: 'OBJECTID', title: 'Object identifier' }, + { name: 'Nom', title: 'Nom de la rue' }, + { name: 'Rue', title: '' }, ], }) }) }) - //End todo rewrite + describe('when feature catalog exists, no extras defined', () => { beforeEach(async () => { - ;(gn4RecordsApi.getFeatureCatalog as jest.Mock).mockReturnValue( - of(mockFeatureCatalogResponse) - ) - metadata = await lastValueFrom(repository.getRecord('1234-5678')) + metadata = { + ...simpleDatasetRecordFixture(), + extras: {}, + } catalog = await lastValueFrom(repository.getFeatureCatalog(metadata)) }) @@ -310,7 +303,7 @@ describe('Gn4Repository', () => { expect(gn4RecordsApi.getFeatureCatalog).toHaveBeenCalledWith( 'my-dataset-001', //uniqueid of the record undefined - ) + ) // TODO a debug code de prod peut etre pas bon }) it('returns the feature catalog with mapped features', () => { @@ -326,30 +319,17 @@ describe('Gn4Repository', () => { describe('when feature catalog does not exist, nor in extras', () => { beforeEach(async () => { - ;(gn4RecordsApi.getFeatureCatalog as jest.Mock).mockReturnValue(of({})) - catalog = await lastValueFrom(repository.getFeatureCatalog(metadata)) - }) - - it('returns null when no decode map is present', () => { - expect(catalog).toBe(null) - }) - }) - - describe('with approved version parameter', () => { - beforeEach(async () => { + metadata = { + ...simpleDatasetRecordFixture(), + extras: {}, + } ;(gn4RecordsApi.getFeatureCatalog as jest.Mock).mockReturnValue( - of(mockFeatureCatalogResponse) - ) - catalog = await lastValueFrom( - repository.getFeatureCatalog(metadata, true) + of({ decodeMap: {} }) ) + catalog = await lastValueFrom(repository.getFeatureCatalog(metadata)) }) - - it('calls the API with approved parameter', () => { - expect(gn4RecordsApi.getFeatureCatalog).toHaveBeenCalledWith( - 'my-dataset-001', //uniqueid of the record - true - ) + it('returns null when no decode map is present', () => { + expect(catalog).toEqual({ attributes: [] }) }) }) }) diff --git a/libs/api/repository/src/lib/gn4/gn4-repository.ts b/libs/api/repository/src/lib/gn4/gn4-repository.ts index 6ac695c922..601c31254e 100644 --- a/libs/api/repository/src/lib/gn4/gn4-repository.ts +++ b/libs/api/repository/src/lib/gn4/gn4-repository.ts @@ -144,8 +144,22 @@ export class Gn4Repository implements RecordsRepositoryInterface { getFeatureCatalog( record: CatalogRecord ): Observable { - const featureTypes = metadata.extras['featureTypes'] as any[] - if (!featureTypes || featureTypes.length === 0) { + if ( + metadata.extras['featureTypes'] && + metadata.extras['featureTypes'][0]?.attributeTable && + Array.isArray(metadata.extras['featureTypes'][0].attributeTable) + ) { + const temp = of({ + attributes: metadata.extras['featureTypes'][0]?.attributeTable?.map( + (attr) => ({ + name: attr.name, + title: attr.definition, + }) + ), + } as DatasetFeatureCatalog) + console.log(temp) + return temp + } else { const temp = this.gn4RecordsApi .getFeatureCatalog(metadata.uniqueIdentifier, approvedVersion) .pipe( @@ -162,15 +176,6 @@ export class Gn4Repository implements RecordsRepositoryInterface { ) console.log(temp) return temp - } else { - const temp = of({ - attributes: featureTypes[0]?.attributeTable?.map((attr) => ({ - name: attr.typeName, - title: attr.definition, - })), - } as DatasetFeatureCatalog) - console.log(temp) - return temp } const featureCatalogIdentifier = record.extras['featureCatalogIdentifier'] diff --git a/libs/common/fixtures/src/lib/records.fixtures.ts b/libs/common/fixtures/src/lib/records.fixtures.ts index ec77d7453b..5210cb9999 100644 --- a/libs/common/fixtures/src/lib/records.fixtures.ts +++ b/libs/common/fixtures/src/lib/records.fixtures.ts @@ -160,6 +160,38 @@ As such, **it is not very interesting at all.**`, extras: { isPublishedToAll: true, edit: true, + featureTypes: [ + { + attributeTable: [ + { + code: 'OBJECTID', + name: 'OBJECTID', + link: '', + definition: 'Object identifier', + type: 'OID', + }, + { + code: 'NOM', + name: 'Nom', + link: '', + definition: 'Nom de la rue', + type: 'String (48)', + }, + { + code: 'RUE', + name: 'Rue', + link: '', + definition: '', + type: 'String (50)', + }, + ], + code: '', + aliases: '', + typeName: "Catalogue d'attributs", + definition: '', + isAbstract: 'false', + }, + ], }, }, { From a504ca05c2a80b95c0639df91641bd2e500d893d Mon Sep 17 00:00:00 2001 From: Guillaume DE OLIVEIRA Date: Mon, 3 Mar 2025 14:22:22 +0100 Subject: [PATCH 09/14] fix tests and function --- .../src/lib/gn4/gn4.converter.spec.ts | 9 ++++ .../src/lib/gn4/gn4-repository.spec.ts | 9 ++-- .../repository/src/lib/gn4/gn4-repository.ts | 42 +++++-------------- 3 files changed, 25 insertions(+), 35 deletions(-) diff --git a/libs/api/metadata-converter/src/lib/gn4/gn4.converter.spec.ts b/libs/api/metadata-converter/src/lib/gn4/gn4.converter.spec.ts index 072df11066..d44d1a2ad2 100644 --- a/libs/api/metadata-converter/src/lib/gn4/gn4.converter.spec.ts +++ b/libs/api/metadata-converter/src/lib/gn4/gn4.converter.spec.ts @@ -3357,6 +3357,15 @@ describe('Gn4Converter', () => { isPublishedToAll: true, id: '8705', favoriteCount: 0, + featureCatalogIdentifier: { + associated: [], + brothersAndSisters: [], + children: [], + hasfeaturecats: [], + hassources: [], + parent: [], + services: [], + }, catalogUuid: 'ce008f24-8e0d-45a8-97f8-9f10399f0190', edit: true, }, diff --git a/libs/api/repository/src/lib/gn4/gn4-repository.spec.ts b/libs/api/repository/src/lib/gn4/gn4-repository.spec.ts index 25672209d5..f134c57977 100644 --- a/libs/api/repository/src/lib/gn4/gn4-repository.spec.ts +++ b/libs/api/repository/src/lib/gn4/gn4-repository.spec.ts @@ -289,7 +289,8 @@ describe('Gn4Repository', () => { }) }) }) - + /* TODO cas a refaire avec la fonction refaite par Alita + describe('when feature catalog exists, no extras defined', () => { beforeEach(async () => { metadata = { @@ -303,7 +304,7 @@ describe('Gn4Repository', () => { expect(gn4RecordsApi.getFeatureCatalog).toHaveBeenCalledWith( 'my-dataset-001', //uniqueid of the record undefined - ) // TODO a debug code de prod peut etre pas bon + ) }) it('returns the feature catalog with mapped features', () => { @@ -315,7 +316,7 @@ describe('Gn4Repository', () => { ], }) }) - }) + })*/ describe('when feature catalog does not exist, nor in extras', () => { beforeEach(async () => { @@ -329,7 +330,7 @@ describe('Gn4Repository', () => { catalog = await lastValueFrom(repository.getFeatureCatalog(metadata)) }) it('returns null when no decode map is present', () => { - expect(catalog).toEqual({ attributes: [] }) + expect(catalog).toEqual(null) }) }) }) diff --git a/libs/api/repository/src/lib/gn4/gn4-repository.ts b/libs/api/repository/src/lib/gn4/gn4-repository.ts index 601c31254e..0f02fdbe85 100644 --- a/libs/api/repository/src/lib/gn4/gn4-repository.ts +++ b/libs/api/repository/src/lib/gn4/gn4-repository.ts @@ -145,47 +145,27 @@ export class Gn4Repository implements RecordsRepositoryInterface { record: CatalogRecord ): Observable { if ( - metadata.extras['featureTypes'] && - metadata.extras['featureTypes'][0]?.attributeTable && - Array.isArray(metadata.extras['featureTypes'][0].attributeTable) + record.extras['featureTypes'] && + record.extras['featureTypes'][0]?.attributeTable && + Array.isArray(record.extras['featureTypes'][0].attributeTable) ) { - const temp = of({ - attributes: metadata.extras['featureTypes'][0]?.attributeTable?.map( + return of({ + attributes: record.extras['featureTypes'][0]?.attributeTable?.map( (attr) => ({ name: attr.name, title: attr.definition, }) ), } as DatasetFeatureCatalog) - console.log(temp) - return temp } else { - const temp = this.gn4RecordsApi - .getFeatureCatalog(metadata.uniqueIdentifier, approvedVersion) - .pipe( - map((results: FeatureResponseApiModel) => { - if (!results.decodeMap) { - return null - } - const attributes = Object.keys(results.decodeMap).map((key) => { - const attribute = results.decodeMap[key] - return { name: attribute[0], title: attribute[1] } - }) - return { attributes } as DatasetFeatureCatalog - }) + const featureCatalogIdentifier = record.extras['featureCatalogIdentifier'] + if (featureCatalogIdentifier) { + return this.getRecord(featureCatalogIdentifier).pipe( + switchMap((record) => this.getFeatureCatalog(record)) ) - console.log(temp) - return temp + } + return of(null) } - - const featureCatalogIdentifier = record.extras['featureCatalogIdentifier'] - if (featureCatalogIdentifier) { - return this.getRecord(featureCatalogIdentifier).pipe( - switchMap((record) => this.getFeatureCatalog(record)) - ) - } - - return of(null) } getSimilarRecords(similarTo: CatalogRecord): Observable { From 61be185f9a22080924805838a0b95149207c8d83 Mon Sep 17 00:00:00 2001 From: Guillaume DE OLIVEIRA Date: Mon, 3 Mar 2025 14:45:14 +0100 Subject: [PATCH 10/14] suppression related --- .../src/lib/gn4/gn4.converter.spec.ts | 9 --------- .../src/lib/gn4/gn4.field.mapper.ts | 13 ++----------- 2 files changed, 2 insertions(+), 20 deletions(-) diff --git a/libs/api/metadata-converter/src/lib/gn4/gn4.converter.spec.ts b/libs/api/metadata-converter/src/lib/gn4/gn4.converter.spec.ts index d44d1a2ad2..072df11066 100644 --- a/libs/api/metadata-converter/src/lib/gn4/gn4.converter.spec.ts +++ b/libs/api/metadata-converter/src/lib/gn4/gn4.converter.spec.ts @@ -3357,15 +3357,6 @@ describe('Gn4Converter', () => { isPublishedToAll: true, id: '8705', favoriteCount: 0, - featureCatalogIdentifier: { - associated: [], - brothersAndSisters: [], - children: [], - hasfeaturecats: [], - hassources: [], - parent: [], - services: [], - }, catalogUuid: 'ce008f24-8e0d-45a8-97f8-9f10399f0190', edit: true, }, diff --git a/libs/api/metadata-converter/src/lib/gn4/gn4.field.mapper.ts b/libs/api/metadata-converter/src/lib/gn4/gn4.field.mapper.ts index 4547e2fa33..9d7dda123e 100644 --- a/libs/api/metadata-converter/src/lib/gn4/gn4.field.mapper.ts +++ b/libs/api/metadata-converter/src/lib/gn4/gn4.field.mapper.ts @@ -43,7 +43,7 @@ export class Gn4FieldMapper { constructor( private metadataUrlService: MetadataUrlService, private langService: LangService - ) { } + ) {} private lang3 = this.langService.gnLang @@ -190,7 +190,7 @@ export class Gn4FieldMapper { ...output, contactsForResource: [ ...('contactsForResource' in output && - Array.isArray(output.contactsForResource) + Array.isArray(output.contactsForResource) ? output.contactsForResource : []), ...getAsArray(selectField(source, 'contactForResource')).map( @@ -284,15 +284,6 @@ export class Gn4FieldMapper { }, output ), - related: (output, source) => { - // TODO: - return this.addExtra( - { - featureCatalogIdentifier: selectField(source, 'related'), - }, - output - ) - }, isPublishedToAll: (output, source) => this.addExtra( From a483769ba5232aa592b698ab37eba6ce7db1a345 Mon Sep 17 00:00:00 2001 From: Guillaume DE OLIVEIRA Date: Mon, 3 Mar 2025 16:52:35 +0100 Subject: [PATCH 11/14] reducer facade and effects tests --- .../src/lib/state/mdview.effects.spec.ts | 57 ++++++++++++++ .../src/lib/state/mdview.facade.spec.ts | 11 +++ .../src/lib/state/mdview.reducer.spec.ts | 76 +++++++++++++++++++ 3 files changed, 144 insertions(+) diff --git a/libs/feature/record/src/lib/state/mdview.effects.spec.ts b/libs/feature/record/src/lib/state/mdview.effects.spec.ts index cfd7f71d04..7432b9feb2 100644 --- a/libs/feature/record/src/lib/state/mdview.effects.spec.ts +++ b/libs/feature/record/src/lib/state/mdview.effects.spec.ts @@ -262,4 +262,61 @@ describe('MdViewEffects', () => { }) }) }) + + describe('loadFeatureCatalog$', () => { + const featureCatalog = { + attributes: [{ name: 'test', title: 'Test' }], + } + + describe('when api success and feature catalog found', () => { + beforeEach(() => { + repository.getFeatureCatalog = jest.fn(() => of(featureCatalog)) + }) + it('should dispatch loadFeatureCatalogSuccess', () => { + actions = hot('-a-|', { + a: MdViewActions.loadFullMetadataSuccess({ full }), + }) + const expected = hot('-a-|', { + a: MdViewActions.loadFeatureCatalogSuccess({ + datasetCatalog: featureCatalog, + }), + }) + expect(effects.loadFeatureCatalog$).toBeObservable(expected) + }) + }) + + describe('when api success but no feature catalog found', () => { + beforeEach(() => { + repository.getFeatureCatalog = jest.fn(() => of(null)) + }) + it('should dispatch loadFeatureCatalogFailure with notFound', () => { + actions = hot('-a-|', { + a: MdViewActions.loadFullMetadataSuccess({ full }), + }) + const expected = hot('-a-|', { + a: MdViewActions.loadFeatureCatalogFailure({ notFound: true }), + }) + expect(effects.loadFeatureCatalog$).toBeObservable(expected) + }) + }) + + describe('when api fails', () => { + beforeEach(() => { + repository.getFeatureCatalog = jest.fn(() => + throwError(() => new Error('api error')) + ) + }) + it('should dispatch loadFeatureCatalogFailure with error', () => { + actions = hot('-a-|', { + a: MdViewActions.loadFullMetadataSuccess({ full }), + }) + const expected = hot('-(a|)', { + a: MdViewActions.loadFeatureCatalogFailure({ + otherError: 'api error', + }), + }) + expect(effects.loadFeatureCatalog$).toBeObservable(expected) + }) + }) + }) }) diff --git a/libs/feature/record/src/lib/state/mdview.facade.spec.ts b/libs/feature/record/src/lib/state/mdview.facade.spec.ts index a5df490a5a..bd434a69a2 100644 --- a/libs/feature/record/src/lib/state/mdview.facade.spec.ts +++ b/libs/feature/record/src/lib/state/mdview.facade.spec.ts @@ -380,5 +380,16 @@ describe('MdViewFacade', () => { expect(result).toEqual(values.a) })) }) + describe('loadFeatureCatalog', () => { + it('dispatches a loadFeatureCatalog action', () => { + facade.loadFeatureCatalog(datasetRecordsFixture()[0]) + const expected = hot('a', { + a: MdViewActions.loadFeatureCatalog({ + metadata: datasetRecordsFixture()[0], + }), + }) + expect(store.scannedActions$).toBeObservable(expected) + }) + }) }) }) diff --git a/libs/feature/record/src/lib/state/mdview.reducer.spec.ts b/libs/feature/record/src/lib/state/mdview.reducer.spec.ts index fee04f182f..5b24c1f535 100644 --- a/libs/feature/record/src/lib/state/mdview.reducer.spec.ts +++ b/libs/feature/record/src/lib/state/mdview.reducer.spec.ts @@ -255,4 +255,80 @@ describe('metadataViewReducer', () => { }) }) }) + + describe('loadFeatureCatalog', () => { + let action + + beforeEach(() => { + action = MdViewActions.loadFeatureCatalog({ + metadata: datasetRecordsFixture()[0], + }) + }) + + it('should set loading state and clear errors', () => { + const state = reducer(initialMetadataViewState, action) + expect(state).toEqual({ + ...initialMetadataViewState, + featureCatalogError: null, + featureCatalogLoading: true, + }) + }) + }) + + describe('loadFeatureCatalogSuccess', () => { + let action + const mockDatasetCatalog = { + attributes: [ + { name: 'feature1', title: 'Feature 1' }, + { name: 'feature2', title: 'Feature 2' }, + ], + } + + beforeEach(() => { + action = MdViewActions.loadFeatureCatalogSuccess({ + datasetCatalog: mockDatasetCatalog, + }) + }) + + it('should store the feature catalog and set loading to false', () => { + const state = reducer( + { + ...initialMetadataViewState, + featureCatalogLoading: true, + }, + action + ) + expect(state).toEqual({ + ...initialMetadataViewState, + featureCatalog: mockDatasetCatalog, + featureCatalogLoading: false, + }) + }) + }) + + describe('loadFeatureCatalogFailure', () => { + let action + + beforeEach(() => { + action = MdViewActions.loadFeatureCatalogFailure({ + otherError: 'Some error', + notFound: true, + }) + }) + + it('should set error and set loading to false', () => { + const state = reducer( + { + ...initialMetadataViewState, + featureCatalogLoading: true, + }, + action + ) + expect(state).toEqual({ + ...initialMetadataViewState, + featureCatalogError: { otherError: 'Some error', notFound: true }, + featureCatalogLoading: false, + }) + }) + }) }) From 7291367f6b8d497d7603a9557a96e4fbcab75280 Mon Sep 17 00:00:00 2001 From: AlitaBernachot Date: Mon, 3 Mar 2025 16:29:19 +0100 Subject: [PATCH 12/14] feat: feature catalog from linked record --- .../src/lib/gn4/gn4.converter.spec.ts | 38 ++++++++++ .../src/lib/gn4/gn4.field.mapper.ts | 31 +++++++- .../src/lib/gn4/types/metadata.model.ts | 2 +- .../src/lib/gn4/gn4-repository.spec.ts | 75 +++++++++++-------- .../repository/src/lib/gn4/gn4-repository.ts | 22 ++++-- .../src/lib/model/record/metadata.model.ts | 1 + .../fixtures/src/lib/records.fixtures.ts | 41 ++++++++++ 7 files changed, 169 insertions(+), 41 deletions(-) diff --git a/libs/api/metadata-converter/src/lib/gn4/gn4.converter.spec.ts b/libs/api/metadata-converter/src/lib/gn4/gn4.converter.spec.ts index 072df11066..1c8d682d64 100644 --- a/libs/api/metadata-converter/src/lib/gn4/gn4.converter.spec.ts +++ b/libs/api/metadata-converter/src/lib/gn4/gn4.converter.spec.ts @@ -1006,6 +1006,44 @@ describe('Gn4Converter', () => { }) }) + describe('feature catalog (fcats)', () => { + it('sets the featureCatalogIdentifier from the link uuid', async () => { + const record = await service.readRecord({ + ...hit, + _source: { + ...hit._source, + recordLink_fcats_uuid: 'linked-metadata-with-fcats', + }, + }) + + expect(record.featureCatalogIdentifier).toEqual( + 'linked-metadata-with-fcats' + ) + }) + + it('sets the featureCatalogIdentifier from the fcats', async () => { + const record = await service.readRecord({ + ...hit, + _source: { + ...hit._source, + related: { + fcats: [ + { + _source: { + uuid: 'related-metadata-with-fcats', + }, + }, + ], + }, + }, + }) + + expect(record.featureCatalogIdentifier).toEqual( + 'related-metadata-with-fcats' + ) + }) + }) + describe('full record', () => { it('builds a complete record object', async () => { const record = await service.readRecord( diff --git a/libs/api/metadata-converter/src/lib/gn4/gn4.field.mapper.ts b/libs/api/metadata-converter/src/lib/gn4/gn4.field.mapper.ts index 9d7dda123e..3b63a07d87 100644 --- a/libs/api/metadata-converter/src/lib/gn4/gn4.field.mapper.ts +++ b/libs/api/metadata-converter/src/lib/gn4/gn4.field.mapper.ts @@ -284,7 +284,36 @@ export class Gn4FieldMapper { }, output ), - + recordLink_fcats_uuid: (output, source) => { + const featureCatalogIdentifier = selectField( + source, + 'recordLink_fcats_uuid' + ) + return { + ...output, + ...(featureCatalogIdentifier && { featureCatalogIdentifier }), + } as CatalogRecord + }, + related: (output, source) => { + const fcatSource = selectField( + getFirstValue( + selectField( + selectField(source, 'related'), + 'fcats' + ) + ), + '_source' + ) + const featureCatalogIdentifier = selectField( + fcatSource, + 'uuid' + ) + return { + ...output, + ...(!output.featureCatalogIdentifier && // If not already found by recordLink_fcats_uuid + featureCatalogIdentifier && { featureCatalogIdentifier }), + } as CatalogRecord + }, isPublishedToAll: (output, source) => this.addExtra( { diff --git a/libs/api/metadata-converter/src/lib/gn4/types/metadata.model.ts b/libs/api/metadata-converter/src/lib/gn4/types/metadata.model.ts index 03c2d5d2b7..4a13a886b4 100644 --- a/libs/api/metadata-converter/src/lib/gn4/types/metadata.model.ts +++ b/libs/api/metadata-converter/src/lib/gn4/types/metadata.model.ts @@ -158,6 +158,7 @@ export type MetadataObject = Partial<{ record: string recordGroup: string recordOwner: string + recordLink_fcats_uuid?: string resolutionDistance: string[] resolutionScaleDenominator: string[] resourceAbstractObject: MultilingualField @@ -202,7 +203,6 @@ export interface Gn4Record { featured?: boolean guestdownload?: boolean selected?: boolean - related?: Gn4RecordRelated } export interface Gn4RecordRelated { diff --git a/libs/api/repository/src/lib/gn4/gn4-repository.spec.ts b/libs/api/repository/src/lib/gn4/gn4-repository.spec.ts index f134c57977..f724f13bb3 100644 --- a/libs/api/repository/src/lib/gn4/gn4-repository.spec.ts +++ b/libs/api/repository/src/lib/gn4/gn4-repository.spec.ts @@ -18,6 +18,7 @@ import { datasetRecordsFixture, simpleDatasetRecordAsXmlFixture, simpleDatasetRecordFixture, + simpleDatasetRecordWithFcatsFixture, duplicateDatasetRecordAsXmlFixture, } from '@geonetwork-ui/common/fixtures' import { @@ -56,7 +57,7 @@ class ElasticsearchServiceMock { } class SearchApiServiceMock { - search = jest.fn((bucket, relatedType, payload) => { + search(bucket, relatedType, payload) { const body = JSON.parse(payload) const count = body.size || 1234 const result: EsSearchResponse = { @@ -73,7 +74,7 @@ class SearchApiServiceMock { } } return of(result) - }) + } } class RecordsApiServiceMock { @@ -95,15 +96,6 @@ class RecordsApiServiceMock { ) deleteRecord = jest.fn(() => of({})) create = jest.fn(() => of('1234-5678')) - getFeatureCatalog = jest.fn((uuid) => - of({ - decodeMap: { - feature1: ['memberName', 'definition'], - feature2: ['name2', 'title2'], - feature3: ['name3', 'title3'], - }, - }) - ) } class PlatformServiceInterfaceMock { @@ -269,14 +261,17 @@ describe('Gn4Repository', () => { describe('getFeatureCatalog', () => { let catalog: DatasetFeatureCatalog let metadata: CatalogRecord + const spySearch = jest.spyOn(SearchApiServiceMock.prototype, 'search') + describe('when extras feature catalog is defined ', () => { beforeEach(async () => { + jest.clearAllMocks() metadata = datasetRecordsFixture()[0] catalog = await lastValueFrom(repository.getFeatureCatalog(metadata)) }) it('should not call the feature catalog API (extras contain the answer) ', () => { - expect(gn4RecordsApi.getFeatureCatalog).not.toHaveBeenCalled() + expect(spySearch).not.toHaveBeenCalled() }) it('returns the feature catalog with mapped features', () => { @@ -289,34 +284,55 @@ describe('Gn4Repository', () => { }) }) }) - /* TODO cas a refaire avec la fonction refaite par Alita - describe('when feature catalog exists, no extras defined', () => { beforeEach(async () => { - metadata = { - ...simpleDatasetRecordFixture(), - extras: {}, - } + metadata = simpleDatasetRecordWithFcatsFixture() + spySearch.mockReturnValue( + of({ hits: { hits: datasetRecordsFixture(), total: { value: 0 } } }) + ) catalog = await lastValueFrom(repository.getFeatureCatalog(metadata)) }) + afterEach(() => { + spySearch.mockRestore() + }) + it('calls the API with correct parameters', () => { - expect(gn4RecordsApi.getFeatureCatalog).toHaveBeenCalledWith( - 'my-dataset-001', //uniqueid of the record - undefined - ) + expect(spySearch).toHaveBeenCalledWith( + 'bucket', + ['fcats'], + '{"uuids":["feature-catalog-identifier"]}' + ) }) - it('returns the feature catalog with mapped features', () => { + it('returns the attributes coming from the linked feature catalog', () => { expect(catalog).toEqual({ attributes: [ - { name: 'memberName', title: 'definition' }, - { name: 'name2', title: 'title2' }, - { name: 'name3', title: 'title3' }, + { name: 'OBJECTID', title: 'Object identifier' }, + { name: 'Nom', title: 'Nom de la rue' }, + { name: 'Rue', title: '' }, ], }) }) - })*/ + + it('prevent looping', async () => { + metadata = { + ...simpleDatasetRecordWithFcatsFixture(), + ...{ featureCatalogIdentifier: 'my-dataset-with-fcats' }, + } + spySearch.mockReturnValue( + of({ + hits: { + hits: metadata, + total: { value: 0 }, + }, + }) + ) + repository.getRecord = jest.fn().mockReturnValue(of(metadata)) + catalog = await lastValueFrom(repository.getFeatureCatalog(metadata)) + expect(catalog).toEqual(null) + }) + }) describe('when feature catalog does not exist, nor in extras', () => { beforeEach(async () => { @@ -324,12 +340,9 @@ describe('Gn4Repository', () => { ...simpleDatasetRecordFixture(), extras: {}, } - ;(gn4RecordsApi.getFeatureCatalog as jest.Mock).mockReturnValue( - of({ decodeMap: {} }) - ) catalog = await lastValueFrom(repository.getFeatureCatalog(metadata)) }) - it('returns null when no decode map is present', () => { + it('returns null', () => { expect(catalog).toEqual(null) }) }) diff --git a/libs/api/repository/src/lib/gn4/gn4-repository.ts b/libs/api/repository/src/lib/gn4/gn4-repository.ts index 0f02fdbe85..8f5e7ba968 100644 --- a/libs/api/repository/src/lib/gn4/gn4-repository.ts +++ b/libs/api/repository/src/lib/gn4/gn4-repository.ts @@ -142,9 +142,11 @@ export class Gn4Repository implements RecordsRepositoryInterface { } getFeatureCatalog( - record: CatalogRecord + record: CatalogRecord, + visited: Set = new Set() // prevent looping ): Observable { if ( + record.extras && record.extras['featureTypes'] && record.extras['featureTypes'][0]?.attributeTable && Array.isArray(record.extras['featureTypes'][0].attributeTable) @@ -157,15 +159,19 @@ export class Gn4Repository implements RecordsRepositoryInterface { }) ), } as DatasetFeatureCatalog) - } else { - const featureCatalogIdentifier = record.extras['featureCatalogIdentifier'] - if (featureCatalogIdentifier) { - return this.getRecord(featureCatalogIdentifier).pipe( - switchMap((record) => this.getFeatureCatalog(record)) + } + + const featureCatalogIdentifier = record.featureCatalogIdentifier + if (featureCatalogIdentifier && !visited.has(featureCatalogIdentifier)) { + visited.add(featureCatalogIdentifier) + return this.getRecord(featureCatalogIdentifier).pipe( + switchMap((record) => + record ? this.getFeatureCatalog(record, visited) : of(null) ) - } - return of(null) + ) } + + return of(null) } getSimilarRecords(similarTo: CatalogRecord): Observable { diff --git a/libs/common/domain/src/lib/model/record/metadata.model.ts b/libs/common/domain/src/lib/model/record/metadata.model.ts index 0da5d2577a..e9299cdf17 100644 --- a/libs/common/domain/src/lib/model/record/metadata.model.ts +++ b/libs/common/domain/src/lib/model/record/metadata.model.ts @@ -124,6 +124,7 @@ export interface BaseRecord { extras?: Record landingPage?: URL updateFrequency?: UpdateFrequency + featureCatalogIdentifier?: string // information related to the resource (dataset, service) resourceIdentifier?: string diff --git a/libs/common/fixtures/src/lib/records.fixtures.ts b/libs/common/fixtures/src/lib/records.fixtures.ts index 5210cb9999..ab165926ea 100644 --- a/libs/common/fixtures/src/lib/records.fixtures.ts +++ b/libs/common/fixtures/src/lib/records.fixtures.ts @@ -344,6 +344,47 @@ export const simpleDatasetRecordFixture = (): DatasetRecord => ({ translations: {}, }) +export const simpleDatasetRecordWithFcatsFixture = (): DatasetRecord => ({ + uniqueIdentifier: 'my-dataset-with-fcats', + featureCatalogIdentifier: 'feature-catalog-identifier', + kind: 'dataset', + otherLanguages: [], + defaultLanguage: 'en', + recordUpdated: new Date('2022-02-01T14:12:00.000Z'), + resourceCreated: new Date('2022-09-01T12:18:19.000Z'), + resourceUpdated: new Date('2022-12-04T14:12:00.000Z'), + status: 'ongoing', + title: 'A very interesting dataset with a related feature catalog', + abstract: `This dataset has been established for testing purposes.`, + ownerOrganization: { name: 'MyOrganization', translations: {} }, + contacts: [ + { + email: 'bob@org.net', + position: 'developer', + organization: { name: 'MyOrganization', translations: {} }, + role: 'point_of_contact', + firstName: 'Bob', + lastName: 'TheGreat', + }, + ], + contactsForResource: [], + keywords: [], + topics: ['testData'], + licenses: [], + legalConstraints: [], + securityConstraints: [], + otherConstraints: [], + lineage: + 'This record was edited manually to test the feature catalog parsing', + spatialRepresentation: 'grid', + overviews: [], + spatialExtents: [], + temporalExtents: [], + onlineResources: [], + updateFrequency: { per: 'month', updatedTimes: 3 }, + translations: {}, +}) + export const simpleDatasetRecordAsXmlFixture = (): string => ` From 35ce1f8d80fd758ad39b3e778dd823c9be0365b3 Mon Sep 17 00:00:00 2001 From: AlitaBernachot Date: Tue, 4 Mar 2025 09:29:37 +0100 Subject: [PATCH 13/14] fix: eclude fcats identifier from keys --- libs/common/domain/src/lib/model/record/metadata.model.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/libs/common/domain/src/lib/model/record/metadata.model.ts b/libs/common/domain/src/lib/model/record/metadata.model.ts index e9299cdf17..d077ce29a3 100644 --- a/libs/common/domain/src/lib/model/record/metadata.model.ts +++ b/libs/common/domain/src/lib/model/record/metadata.model.ts @@ -263,7 +263,7 @@ export type OnlineResource = DatasetOnlineResource | ServiceOnlineResource export type CatalogRecord = DatasetRecord | ReuseRecord | ServiceRecord -export type CatalogRecordKeys = - | keyof DatasetRecord - | keyof ReuseRecord - | keyof ServiceRecord +export type CatalogRecordKeys = Exclude< + keyof DatasetRecord | keyof ReuseRecord | keyof ServiceRecord, + 'featureCatalogIdentifier' +> From f98e22f264dab37d6068bd2fa8fe8b54525f8e62 Mon Sep 17 00:00:00 2001 From: AlitaBernachot Date: Tue, 4 Mar 2025 16:10:08 +0100 Subject: [PATCH 14/14] fix: remove use of recordLink_fcats_uuid --- .../src/lib/gn4/gn4.converter.spec.ts | 14 -------------- .../src/lib/gn4/gn4.field.mapper.ts | 13 +------------ .../src/lib/gn4/types/metadata.model.ts | 1 - 3 files changed, 1 insertion(+), 27 deletions(-) diff --git a/libs/api/metadata-converter/src/lib/gn4/gn4.converter.spec.ts b/libs/api/metadata-converter/src/lib/gn4/gn4.converter.spec.ts index 1c8d682d64..c6c47a90ba 100644 --- a/libs/api/metadata-converter/src/lib/gn4/gn4.converter.spec.ts +++ b/libs/api/metadata-converter/src/lib/gn4/gn4.converter.spec.ts @@ -1007,20 +1007,6 @@ describe('Gn4Converter', () => { }) describe('feature catalog (fcats)', () => { - it('sets the featureCatalogIdentifier from the link uuid', async () => { - const record = await service.readRecord({ - ...hit, - _source: { - ...hit._source, - recordLink_fcats_uuid: 'linked-metadata-with-fcats', - }, - }) - - expect(record.featureCatalogIdentifier).toEqual( - 'linked-metadata-with-fcats' - ) - }) - it('sets the featureCatalogIdentifier from the fcats', async () => { const record = await service.readRecord({ ...hit, diff --git a/libs/api/metadata-converter/src/lib/gn4/gn4.field.mapper.ts b/libs/api/metadata-converter/src/lib/gn4/gn4.field.mapper.ts index 3b63a07d87..94a2682b27 100644 --- a/libs/api/metadata-converter/src/lib/gn4/gn4.field.mapper.ts +++ b/libs/api/metadata-converter/src/lib/gn4/gn4.field.mapper.ts @@ -284,16 +284,6 @@ export class Gn4FieldMapper { }, output ), - recordLink_fcats_uuid: (output, source) => { - const featureCatalogIdentifier = selectField( - source, - 'recordLink_fcats_uuid' - ) - return { - ...output, - ...(featureCatalogIdentifier && { featureCatalogIdentifier }), - } as CatalogRecord - }, related: (output, source) => { const fcatSource = selectField( getFirstValue( @@ -310,8 +300,7 @@ export class Gn4FieldMapper { ) return { ...output, - ...(!output.featureCatalogIdentifier && // If not already found by recordLink_fcats_uuid - featureCatalogIdentifier && { featureCatalogIdentifier }), + ...(featureCatalogIdentifier && { featureCatalogIdentifier }), } as CatalogRecord }, isPublishedToAll: (output, source) => diff --git a/libs/api/metadata-converter/src/lib/gn4/types/metadata.model.ts b/libs/api/metadata-converter/src/lib/gn4/types/metadata.model.ts index 4a13a886b4..67daf4d0eb 100644 --- a/libs/api/metadata-converter/src/lib/gn4/types/metadata.model.ts +++ b/libs/api/metadata-converter/src/lib/gn4/types/metadata.model.ts @@ -158,7 +158,6 @@ export type MetadataObject = Partial<{ record: string recordGroup: string recordOwner: string - recordLink_fcats_uuid?: string resolutionDistance: string[] resolutionScaleDenominator: string[] resourceAbstractObject: MultilingualField