From 1c28342ae3cf5a40480bcb58f2c0d3aa953f7228 Mon Sep 17 00:00:00 2001 From: ronitjadhav Date: Wed, 22 May 2024 13:55:52 +0200 Subject: [PATCH 1/3] Use of ogc-client to generate ogc-api custom url --- .../record-api-form.component.spec.ts | 25 ++- .../record-api-form.component.ts | 169 ++++++++++-------- 2 files changed, 110 insertions(+), 84 deletions(-) diff --git a/libs/ui/elements/src/lib/record-api-form/record-api-form.component.spec.ts b/libs/ui/elements/src/lib/record-api-form/record-api-form.component.spec.ts index 65b193e11b..32b83da28a 100644 --- a/libs/ui/elements/src/lib/record-api-form/record-api-form.component.spec.ts +++ b/libs/ui/elements/src/lib/record-api-form/record-api-form.component.spec.ts @@ -9,6 +9,7 @@ const mockDatasetServiceDistribution: DatasetServiceDistribution = { url: new URL('https://api.example.com/data'), type: 'service', accessServiceProtocol: 'ogcFeatures', + name: 'mockFeatureType', } jest.mock('@camptocamp/ogc-client', () => ({ @@ -28,6 +29,16 @@ jest.mock('@camptocamp/ogc-client', () => ({ ], }) } + getCollectionItemsUrl(collectionName, options) { + const queryParams = new URLSearchParams() + if (options.limit !== undefined) queryParams.set('limit', options.limit) + if (options.offset !== undefined) + queryParams.set('offset', options.offset) + queryParams.set('f', options.outputFormat) + return `${ + this.url + }/collections/${collectionName}/items?${queryParams.toString()}` + } }, WfsEndpoint: class { constructor(private url) {} @@ -81,7 +92,9 @@ describe('RecordApFormComponent', () => { expect(component.limit$.getValue()).toBe('-1') expect(component.format$.getValue()).toBe('json') const url = await firstValueFrom(component.apiQueryUrl$) - expect(url).toBe('https://api.example.com/data?limit=-1&f=json') + expect(url).toBe( + 'https://api.example.com/data/collections/mockFeatureType/items?f=json' + ) }) }) describe('When URL params are changed', () => { @@ -94,11 +107,11 @@ describe('RecordApFormComponent', () => { component.setFormat(mockFormat) const url = await firstValueFrom(component.apiQueryUrl$) expect(url).toBe( - `https://api.example.com/data?offset=${mockOffset}&limit=${mockLimit}&f=${mockFormat}` + `https://api.example.com/data/collections/mockFeatureType/items?limit=${mockLimit}&offset=${mockOffset}&f=${mockFormat}` ) }) it('should remove the param in url if value is null', async () => { - const mockOffset = null + const mockOffset = '0' const mockLimit = '20' const mockFormat = 'json' component.setOffset(mockOffset) @@ -106,7 +119,7 @@ describe('RecordApFormComponent', () => { component.setFormat(mockFormat) const url = await firstValueFrom(component.apiQueryUrl$) expect(url).toBe( - `https://api.example.com/data?limit=${mockLimit}&f=${mockFormat}` + `https://api.example.com/data/collections/mockFeatureType/items?limit=${mockLimit}&offset=${mockOffset}&f=${mockFormat}` ) }) it('should remove the param in url if value is zero', async () => { @@ -118,7 +131,7 @@ describe('RecordApFormComponent', () => { component.setFormat(mockFormat) const url = await firstValueFrom(component.apiQueryUrl$) expect(url).toBe( - `https://api.example.com/data?offset=${mockOffset}&f=${mockFormat}` + `https://api.example.com/data/collections/mockFeatureType/items?limit=${mockLimit}&offset=${mockOffset}&f=${mockFormat}` ) }) }) @@ -164,7 +177,7 @@ describe('RecordApFormComponent', () => { expect(component.format$.getValue()).toBe('json') const url = await firstValueFrom(component.apiQueryUrl$) expect(url).toBe( - 'https://api.example.com/data?type=undefined&options={"outputFormat":"json","startIndex":0}' + `https://api.example.com/data?type=mockFeatureType&options={"outputFormat":"json"}` ) }) }) diff --git a/libs/ui/elements/src/lib/record-api-form/record-api-form.component.ts b/libs/ui/elements/src/lib/record-api-form/record-api-form.component.ts index 15178539a1..51e4d346cd 100644 --- a/libs/ui/elements/src/lib/record-api-form/record-api-form.component.ts +++ b/libs/ui/elements/src/lib/record-api-form/record-api-form.component.ts @@ -12,6 +12,12 @@ const DEFAULT_PARAMS = { LIMIT: '-1', FORMAT: 'json', } + +interface OutputFormats { + itemFormats?: any[] + outputFormats?: any[] +} + @Component({ selector: 'gn-ui-record-api-form', templateUrl: './record-api-form.component.html', @@ -25,56 +31,38 @@ export class RecordApiFormComponent { this.apiFeatureType = value ? value.name : undefined if (value) { this.apiBaseUrl = value.url.href - this.parseOutputFormats() + this.createEndpoint().then(() => this.parseOutputFormats()) } this.resetUrl() } - offset$ = new BehaviorSubject('') - limit$ = new BehaviorSubject('') - format$ = new BehaviorSubject('') + offset$ = new BehaviorSubject(DEFAULT_PARAMS.OFFSET) + limit$ = new BehaviorSubject(DEFAULT_PARAMS.LIMIT) + format$ = new BehaviorSubject(DEFAULT_PARAMS.FORMAT) + endpoint$ = new BehaviorSubject( + undefined + ) apiBaseUrl: string apiFeatureType: string supportOffset = true accessServiceProtocol: ServiceProtocol | undefined outputFormats = [{ value: 'json', label: 'JSON' }] + endpoint: WfsEndpoint | OgcApiEndpoint | undefined - apiQueryUrl$ = combineLatest([this.offset$, this.limit$, this.format$]).pipe( - switchMap(async ([offset, limit, format]) => { - let outputUrl - if (this.apiBaseUrl) { - const url = new URL(this.apiBaseUrl) - const params = { offset: offset, limit: limit, f: format } - for (const [key, value] of Object.entries(params)) { - if (value && value !== '0') { - url.searchParams.set(key, value) - } else { - url.searchParams.delete(key) - } - } - outputUrl = url.toString() - } - - if (this.accessServiceProtocol === 'wfs') { - const wfsEndpoint = new WfsEndpoint(this.apiBaseUrl) - if (await wfsEndpoint.isReady()) { - const options = { - outputFormat: format, - startIndex: Number(offset), - } - if (limit !== '-1') { - options['maxFeatures'] = Number(limit) - } - outputUrl = wfsEndpoint.getFeatureUrl(this.apiFeatureType, options) - } - } - return outputUrl - }) + apiQueryUrl$ = combineLatest([ + this.offset$, + this.limit$, + this.format$, + this.endpoint$, + ]).pipe( + switchMap(([offset, limit, format, endpoint]) => + this.generateApiQueryUrl(offset, limit, format) + ) ) + noLimitChecked$ = this.limit$.pipe( map((limit) => limit === '-1' || limit === '') ) - displayLimit$ = this.limit$.pipe( map((limit) => (limit !== '-1' ? limit : '')) ) @@ -84,8 +72,7 @@ export class RecordApiFormComponent { } setLimit(value: string) { - const newLimit = value === '' ? '-1' : value - this.limit$.next(newLimit) + this.limit$.next(value === '' ? '-1' : value) } setFormat(value: string | unknown) { @@ -98,55 +85,81 @@ export class RecordApiFormComponent { this.format$.next(DEFAULT_PARAMS.FORMAT) } - parseOutputFormats() { - const apiUrl = - this.apiBaseUrl.slice(-1) === '?' - ? this.apiBaseUrl.slice(0, -1) - : this.apiBaseUrl - - this.getOutputFormats(apiUrl, this.accessServiceProtocol).then( - (outputFormats) => { - let formatsList = [] - if ('itemFormats' in outputFormats) { - formatsList = this.mapFormats(outputFormats.itemFormats) - } else if ('outputFormats' in outputFormats) { - formatsList = this.mapFormats(outputFormats.outputFormats) - } - this.outputFormats = this.outputFormats.concat( - formatsList.filter(Boolean) - ) - this.outputFormats = this.outputFormats - .filter( - (format, index, self) => - index === self.findIndex((t) => t.value === format.value) - ) - .sort((a, b) => a.label.localeCompare(b.label)) - } - ) + async parseOutputFormats() { + if (!this.endpoint) return + const apiUrl = this.apiBaseUrl.endsWith('?') + ? this.apiBaseUrl.slice(0, -1) + : this.apiBaseUrl + const outputFormats = await this.getOutputFormats(apiUrl) + + const formatsList = outputFormats.itemFormats + ? this.mapFormats(outputFormats.itemFormats) + : this.mapFormats(outputFormats.outputFormats || []) + + this.outputFormats = this.outputFormats + .concat(formatsList.filter(Boolean)) + .filter( + (format, index, self) => + index === self.findIndex((t) => t.value === format.value) + ) + .sort((a, b) => a.label.localeCompare(b.label)) } mapFormats(formats: any[]) { return formats.map((format) => { const normalizedFormat = mimeTypeToFormat(format) - if (normalizedFormat) { - return { - label: normalizedFormat.toUpperCase(), - value: normalizedFormat, - } - } - return null + return normalizedFormat + ? { label: normalizedFormat.toUpperCase(), value: normalizedFormat } + : null }) } - async getOutputFormats(url: string, accessServiceProtocol: string) { - if (accessServiceProtocol === 'wfs') { - const endpoint = await new WfsEndpoint(url).isReady() - this.supportOffset = endpoint.supportsStartIndex() - return endpoint.getServiceInfo() + async getOutputFormats(url: string): Promise { + if (!this.endpoint) return {} + if (this.endpoint instanceof WfsEndpoint) { + this.supportOffset = this.endpoint.supportsStartIndex() + return this.endpoint.getServiceInfo() as OutputFormats + } else { + { + const firstCollection = (await this.endpoint.featureCollections)[0] + return (await this.endpoint.getCollectionInfo( + firstCollection + )) as OutputFormats + } + } + } + + async createEndpoint() { + if (!this.apiBaseUrl || !this.accessServiceProtocol) return + if (this.accessServiceProtocol === 'wfs') { + this.endpoint = new WfsEndpoint(this.apiBaseUrl) + await (this.endpoint as WfsEndpoint).isReady() + } else { + this.endpoint = new OgcApiEndpoint(this.apiBaseUrl) + } + this.endpoint$.next(this.endpoint) + } + + async generateApiQueryUrl( + offset: string, + limit: string, + format: string + ): Promise { + if (!this.apiBaseUrl || !this.endpoint || !this.apiFeatureType) return '' + + const options = { + outputFormat: format, + startIndex: offset ? Number(offset) : undefined, + maxFeatures: limit !== '-1' ? Number(limit) : undefined, + limit: limit !== '-1' ? Number(limit) : undefined, + offset: offset !== '' ? Number(offset) : undefined, + } + + if (this.endpoint instanceof WfsEndpoint) { + return this.endpoint.getFeatureUrl(this.apiFeatureType, options) } else { - const endpoint = await new OgcApiEndpoint(url) - const firstCollection = (await endpoint.featureCollections)[0] - return endpoint.getCollectionInfo(firstCollection) + const firstCollection = (await this.endpoint.featureCollections)[0] + return await this.endpoint.getCollectionItemsUrl(firstCollection, options) } } } From a3cd6cf8e73295bd92f478c6bad3b01907e77e43 Mon Sep 17 00:00:00 2001 From: ronitjadhav Date: Thu, 23 May 2024 12:59:56 +0200 Subject: [PATCH 2/3] Updated e2e test --- apps/datahub-e2e/src/e2e/datasetDetailPage.cy.ts | 4 ++-- .../record-api-form.component.spec.ts | 10 +++++----- .../record-api-form/record-api-form.component.ts | 16 ++++++++++------ 3 files changed, 17 insertions(+), 13 deletions(-) diff --git a/apps/datahub-e2e/src/e2e/datasetDetailPage.cy.ts b/apps/datahub-e2e/src/e2e/datasetDetailPage.cy.ts index 155e67a0c0..1b1d7da16f 100644 --- a/apps/datahub-e2e/src/e2e/datasetDetailPage.cy.ts +++ b/apps/datahub-e2e/src/e2e/datasetDetailPage.cy.ts @@ -684,7 +684,7 @@ describe('api form', () => { .find('gn-ui-copy-text-button') .find('input') .invoke('val') - .should('include', 'offset=87&limit=54&f=geojson') + .should('include', 'f=geojson&limit=54&offset=87') cy.get('@apiForm').children('div').first().find('button').first().click() @@ -699,7 +699,7 @@ describe('api form', () => { .find('gn-ui-copy-text-button') .find('input') .invoke('val') - .should('include', 'limit=-1&f=json') + .should('include', 'f=json&limit=-1') }) it('should close the panel on click', () => { cy.get('gn-ui-record-api-form').prev().find('button').click() diff --git a/libs/ui/elements/src/lib/record-api-form/record-api-form.component.spec.ts b/libs/ui/elements/src/lib/record-api-form/record-api-form.component.spec.ts index 32b83da28a..918df0cc5c 100644 --- a/libs/ui/elements/src/lib/record-api-form/record-api-form.component.spec.ts +++ b/libs/ui/elements/src/lib/record-api-form/record-api-form.component.spec.ts @@ -93,7 +93,7 @@ describe('RecordApFormComponent', () => { expect(component.format$.getValue()).toBe('json') const url = await firstValueFrom(component.apiQueryUrl$) expect(url).toBe( - 'https://api.example.com/data/collections/mockFeatureType/items?f=json' + 'https://api.example.com/data/collections/feature1/items?limit=-1&f=json' ) }) }) @@ -107,7 +107,7 @@ describe('RecordApFormComponent', () => { component.setFormat(mockFormat) const url = await firstValueFrom(component.apiQueryUrl$) expect(url).toBe( - `https://api.example.com/data/collections/mockFeatureType/items?limit=${mockLimit}&offset=${mockOffset}&f=${mockFormat}` + `https://api.example.com/data/collections/feature1/items?limit=${mockLimit}&offset=${mockOffset}&f=${mockFormat}` ) }) it('should remove the param in url if value is null', async () => { @@ -119,7 +119,7 @@ describe('RecordApFormComponent', () => { component.setFormat(mockFormat) const url = await firstValueFrom(component.apiQueryUrl$) expect(url).toBe( - `https://api.example.com/data/collections/mockFeatureType/items?limit=${mockLimit}&offset=${mockOffset}&f=${mockFormat}` + `https://api.example.com/data/collections/feature1/items?limit=${mockLimit}&offset=${mockOffset}&f=${mockFormat}` ) }) it('should remove the param in url if value is zero', async () => { @@ -131,7 +131,7 @@ describe('RecordApFormComponent', () => { component.setFormat(mockFormat) const url = await firstValueFrom(component.apiQueryUrl$) expect(url).toBe( - `https://api.example.com/data/collections/mockFeatureType/items?limit=${mockLimit}&offset=${mockOffset}&f=${mockFormat}` + `https://api.example.com/data/collections/feature1/items?limit=${mockLimit}&offset=${mockOffset}&f=${mockFormat}` ) }) }) @@ -177,7 +177,7 @@ describe('RecordApFormComponent', () => { expect(component.format$.getValue()).toBe('json') const url = await firstValueFrom(component.apiQueryUrl$) expect(url).toBe( - `https://api.example.com/data?type=mockFeatureType&options={"outputFormat":"json"}` + `https://api.example.com/data?type=mockFeatureType&options={"outputFormat":"json","limit":-1}` ) }) }) diff --git a/libs/ui/elements/src/lib/record-api-form/record-api-form.component.ts b/libs/ui/elements/src/lib/record-api-form/record-api-form.component.ts index 51e4d346cd..8a13579cdf 100644 --- a/libs/ui/elements/src/lib/record-api-form/record-api-form.component.ts +++ b/libs/ui/elements/src/lib/record-api-form/record-api-form.component.ts @@ -48,6 +48,7 @@ export class RecordApiFormComponent { accessServiceProtocol: ServiceProtocol | undefined outputFormats = [{ value: 'json', label: 'JSON' }] endpoint: WfsEndpoint | OgcApiEndpoint | undefined + firstCollection: string | undefined apiQueryUrl$ = combineLatest([ this.offset$, @@ -55,7 +56,7 @@ export class RecordApiFormComponent { this.format$, this.endpoint$, ]).pipe( - switchMap(([offset, limit, format, endpoint]) => + switchMap(([offset, limit, format]) => this.generateApiQueryUrl(offset, limit, format) ) ) @@ -121,9 +122,8 @@ export class RecordApiFormComponent { return this.endpoint.getServiceInfo() as OutputFormats } else { { - const firstCollection = (await this.endpoint.featureCollections)[0] return (await this.endpoint.getCollectionInfo( - firstCollection + this.firstCollection )) as OutputFormats } } @@ -136,6 +136,7 @@ export class RecordApiFormComponent { await (this.endpoint as WfsEndpoint).isReady() } else { this.endpoint = new OgcApiEndpoint(this.apiBaseUrl) + this.firstCollection = (await this.endpoint.featureCollections)[0] } this.endpoint$.next(this.endpoint) } @@ -151,15 +152,18 @@ export class RecordApiFormComponent { outputFormat: format, startIndex: offset ? Number(offset) : undefined, maxFeatures: limit !== '-1' ? Number(limit) : undefined, - limit: limit !== '-1' ? Number(limit) : undefined, + limit: limit !== '-1' ? Number(limit) : limit === '-1' ? -1 : undefined, offset: offset !== '' ? Number(offset) : undefined, } if (this.endpoint instanceof WfsEndpoint) { + options.maxFeatures = limit !== '-1' ? Number(limit) : undefined return this.endpoint.getFeatureUrl(this.apiFeatureType, options) } else { - const firstCollection = (await this.endpoint.featureCollections)[0] - return await this.endpoint.getCollectionItemsUrl(firstCollection, options) + return await this.endpoint.getCollectionItemsUrl( + this.firstCollection, + options + ) } } } From a568867032d0475e2d9c44572f75cdbe266b5196 Mon Sep 17 00:00:00 2001 From: ronitjadhav Date: Thu, 23 May 2024 14:20:23 +0200 Subject: [PATCH 3/3] Updated the ogc-client version in package/package.json --- package/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package/package.json b/package/package.json index d185bfc6cb..3b34357159 100644 --- a/package/package.json +++ b/package/package.json @@ -38,7 +38,7 @@ }, "dependencies": { "@biesbjerg/ngx-translate-extract-marker": "^1.0.0", - "@camptocamp/ogc-client": "^1.1.0-RC.3", + "@camptocamp/ogc-client": "1.1.1-dev.ddbb5b0", "@geospatial-sdk/geocoding": "^0.0.5-alpha.2", "@ltd/j-toml": "~1.35.2", "@messageformat/core": "^3.0.1",