diff --git a/api/src/modules/eudr-alerts/eudr.controller.ts b/api/src/modules/eudr-alerts/eudr.controller.ts index 01d8e7ec5..49636af99 100644 --- a/api/src/modules/eudr-alerts/eudr.controller.ts +++ b/api/src/modules/eudr-alerts/eudr.controller.ts @@ -12,8 +12,7 @@ import { ApiTags, ApiUnauthorizedResponse, } from '@nestjs/swagger'; -import { Response } from 'express'; -import { Writable } from 'stream'; + import { ApiOkTreeResponse } from 'decorators/api-tree-response.decorator'; import { Supplier } from 'modules/suppliers/supplier.entity'; import { SetScenarioIdsInterceptor } from 'modules/impact/set-scenario-ids.interceptor'; @@ -35,6 +34,8 @@ import { GetEUDRGeoRegions } from 'modules/geo-regions/dto/get-geo-region.dto'; import { EudrService } from 'modules/eudr-alerts/eudr.service'; import { GetEUDRAlertsDto } from 'modules/eudr-alerts/dto/get-alerts.dto'; import { EUDRAlertDates } from 'modules/eudr-alerts/eudr.repositoty.interface'; +import { GetEUDRFeaturesGeoJSONDto } from 'modules/geo-regions/dto/get-features-geojson.dto'; +import { Feature, FeatureCollection } from 'geojson'; @ApiTags('EUDR') @Controller('/api/v1/eudr') @@ -163,19 +164,13 @@ export class EudrController { return this.eudrAlertsService.getAlerts(dto); } - streamResponse(response: Response, stream: Writable): any { - stream.on('data', (data: any) => { - const json: string = JSON.stringify(data); - response.write(json + '\n'); - }); - - stream.on('end', () => { - response.end(); - }); - - stream.on('error', (error: any) => { - console.error('Stream error:', error); - response.status(500).send('Error processing stream'); - }); + @ApiOperation({ + description: 'Get a Feature List or Feature Collection by GeoRegion Ids', + }) + @Get('/geo-features') + async getGeoJson( + @Query(ValidationPipe) dto: GetEUDRFeaturesGeoJSONDto, + ): Promise { + return this.geoRegionsService.getGeoJson(dto); } } diff --git a/api/src/modules/geo-regions/dto/get-features-geojson.dto.ts b/api/src/modules/geo-regions/dto/get-features-geojson.dto.ts new file mode 100644 index 000000000..2ccbe754a --- /dev/null +++ b/api/src/modules/geo-regions/dto/get-features-geojson.dto.ts @@ -0,0 +1,30 @@ +import { ApiPropertyOptional } from '@nestjs/swagger'; +import { Type } from 'class-transformer'; +import { IsBoolean, IsOptional, IsUUID } from 'class-validator'; + +export class GetFeaturesGeoJsonDto { + @ApiPropertyOptional() + @IsOptional() + @IsUUID('4', { each: true }) + geoRegionIds!: string[]; + + @ApiPropertyOptional({ + description: + 'If true, it will return a FeatureCollection instead of a Feature List. Default is false.', + default: false, + }) + @IsOptional() + @IsBoolean() + @Type(() => Boolean) + collection: boolean = false; + + isEUDRRequested(): boolean { + return 'eudr' in this; + } +} + +export class GetEUDRFeaturesGeoJSONDto extends GetFeaturesGeoJsonDto { + @IsOptional() + @IsBoolean() + eudr: boolean = true; +} diff --git a/api/src/modules/geo-regions/geo-features.service.ts b/api/src/modules/geo-regions/geo-features.service.ts index c94e40a6c..231978169 100644 --- a/api/src/modules/geo-regions/geo-features.service.ts +++ b/api/src/modules/geo-regions/geo-features.service.ts @@ -1,4 +1,4 @@ -import { Injectable } from '@nestjs/common'; +import { Injectable, Logger, NotFoundException } from '@nestjs/common'; import { DataSource, Repository, SelectQueryBuilder } from 'typeorm'; import { GeoRegion } from 'modules/geo-regions/geo-region.entity'; import { FeatureCollection, Feature } from 'geojson'; @@ -13,35 +13,75 @@ import { @Injectable() export class GeoFeaturesService extends Repository { + logger: Logger = new Logger(GeoFeaturesService.name); + constructor(private dataSource: DataSource) { super(GeoRegion, dataSource.createEntityManager()); } - async getGeoFeatures(): Promise { - return null as any; - } - - async getGeoJson( - dto?: GetEUDRFeaturesGeoJSONDto | GetFeaturesGeoJsonDto, - ): Promise { + async getGeoFeatures( + dto: GetFeaturesGeoJsonDto | GetEUDRFeaturesGeoJSONDto, + ): Promise { const queryBuilder: SelectQueryBuilder = this.createQueryBuilder('gr'); - queryBuilder - .select( - 'json_build_object(type, FeatureCollection, features, json_agg(ST_AsGeoJSON(gr.theGeom)::json))', - 'geojson', - ) - .innerJoin(SourcingLocation, 'sl', 'sl.geoRegionId = gr.id'); - if (dto?.geoRegionIds) { - queryBuilder.where('gr.id IN (:...ids)', { ids: dto.geoRegionIds }); + queryBuilder.innerJoin(SourcingLocation, 'sl', 'sl.geoRegionId = gr.id'); + if (dto.isEUDRRequested()) { + queryBuilder.where('sl.locationType = :locationType', { + locationType: LOCATION_TYPES.EUDR, + }); } - - if (dto?.isEUDRRequested()) { - queryBuilder.andWhere('sl.locationType = :type', { - type: LOCATION_TYPES.EUDR, + if (dto.geoRegionIds) { + queryBuilder.andWhere('gr.id IN (:...geoRegionIds)', { + geoRegionIds: dto.geoRegionIds, }); } - const [qury, params] = queryBuilder.getQueryAndParameters(); - return queryBuilder.getRawMany(); + if (dto?.collection) { + return this.selectAsFeatureCollection(queryBuilder); + } + return this.selectAsFeatures(queryBuilder); + } + + private async selectAsFeatures( + queryBuilder: SelectQueryBuilder, + ): Promise { + queryBuilder.select( + ` + json_build_object( + 'type', 'Feature', + 'geometry', ST_AsGeoJSON(gr.theGeom)::json, + 'properties', json_build_object('id', gr.id) + )`, + 'geojson', + ); + const result: Feature[] | undefined = await queryBuilder.getRawMany(); + if (!result.length) { + throw new NotFoundException(`Could not retrieve geo features`); + } + return result; + } + + private async selectAsFeatureCollection( + queryBuilder: SelectQueryBuilder, + ): Promise { + queryBuilder.select( + ` + json_build_object( + 'type', 'FeatureCollection', + 'features', json_agg( + json_build_object( + 'type', 'Feature', + 'geometry', ST_AsGeoJSON(gr.theGeom)::json, + 'properties', json_build_object('id', gr.id) + ) + ) + )`, + 'geojson', + ); + const result: FeatureCollection | undefined = + await queryBuilder.getRawOne(); + if (!result) { + throw new NotFoundException(`Could not retrieve geo features`); + } + return result; } } diff --git a/api/src/modules/geo-regions/geo-region.entity.ts b/api/src/modules/geo-regions/geo-region.entity.ts index 7c05a4961..6c13c6df2 100644 --- a/api/src/modules/geo-regions/geo-region.entity.ts +++ b/api/src/modules/geo-regions/geo-region.entity.ts @@ -10,6 +10,7 @@ import { AdminRegion } from 'modules/admin-regions/admin-region.entity'; import { BaseServiceResource } from 'types/resource.interface'; import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; import { SourcingLocation } from 'modules/sourcing-locations/sourcing-location.entity'; +import { Geometry } from 'geojson'; export const geoRegionResource: BaseServiceResource = { className: 'GeoRegion', @@ -47,7 +48,7 @@ export class GeoRegion extends BaseEntity { nullable: true, }) @ApiPropertyOptional() - theGeom?: JSON; + theGeom?: Geometry; // TODO: It might be interesting to add a trigger to calculate the value in case it's not provided. We are considering that EUDR will alwaus provide the value // but not the regular ingestion diff --git a/api/src/modules/geo-regions/geo-region.repository.ts b/api/src/modules/geo-regions/geo-region.repository.ts index 365365038..5bd49584c 100644 --- a/api/src/modules/geo-regions/geo-region.repository.ts +++ b/api/src/modules/geo-regions/geo-region.repository.ts @@ -7,11 +7,9 @@ import { import { GeoRegion } from 'modules/geo-regions/geo-region.entity'; import { LocationGeoRegionDto } from 'modules/geo-regions/dto/location.geo-region.dto'; import { Injectable } from '@nestjs/common'; -import { GetAdminRegionTreeWithOptionsDto } from '../admin-regions/dto/get-admin-region-tree-with-options.dto'; -import { AdminRegion } from '../admin-regions/admin-region.entity'; -import { SourcingLocation } from '../sourcing-locations/sourcing-location.entity'; -import { BaseQueryBuilder } from '../../utils/base.query-builder'; -import { GetEUDRGeoRegions } from './dto/get-geo-region.dto'; +import { SourcingLocation } from 'modules/sourcing-locations/sourcing-location.entity'; +import { BaseQueryBuilder } from 'utils/base.query-builder'; +import { GetEUDRGeoRegions } from 'modules/geo-regions/dto/get-geo-region.dto'; @Injectable() export class GeoRegionRepository extends Repository { diff --git a/api/src/modules/geo-regions/geo-regions.controller.ts b/api/src/modules/geo-regions/geo-regions.controller.ts index 1ab8d4102..48937c8e6 100644 --- a/api/src/modules/geo-regions/geo-regions.controller.ts +++ b/api/src/modules/geo-regions/geo-regions.controller.ts @@ -6,7 +6,6 @@ import { Param, Patch, Post, - Query, UsePipes, ValidationPipe, } from '@nestjs/common'; @@ -36,7 +35,6 @@ import { import { CreateGeoRegionDto } from 'modules/geo-regions/dto/create.geo-region.dto'; import { UpdateGeoRegionDto } from 'modules/geo-regions/dto/update.geo-region.dto'; import { PaginationMeta } from 'utils/app-base.service'; -import { GetEUDRGeoRegions } from './dto/get-geo-region.dto'; @Controller(`/api/v1/geo-regions`) @ApiTags(geoRegionResource.className) diff --git a/api/src/modules/geo-regions/geo-regions.service.ts b/api/src/modules/geo-regions/geo-regions.service.ts index a0f575c0d..d4483dd36 100644 --- a/api/src/modules/geo-regions/geo-regions.service.ts +++ b/api/src/modules/geo-regions/geo-regions.service.ts @@ -1,4 +1,4 @@ -import { Injectable, Logger, NotFoundException } from '@nestjs/common'; +import { Injectable, NotFoundException } from '@nestjs/common'; import { AppBaseService, JSONAPISerializerConfig, @@ -122,6 +122,6 @@ export class GeoRegionsService extends AppBaseService< async getGeoJson( dto: GetFeaturesGeoJsonDto | GetEUDRFeaturesGeoJSONDto, ): Promise { - return this.geoFeatures.getGeoJson(dto); + return this.geoFeatures.getGeoFeatures(dto); } } diff --git a/api/src/modules/import-data/eudr/eudr.dto-processor.service.ts b/api/src/modules/import-data/eudr/eudr.dto-processor.service.ts index df36b2adc..f587ebcdc 100644 --- a/api/src/modules/import-data/eudr/eudr.dto-processor.service.ts +++ b/api/src/modules/import-data/eudr/eudr.dto-processor.service.ts @@ -20,6 +20,7 @@ import * as wellknown from 'wellknown'; import { DataSource, QueryRunner, Repository } from 'typeorm'; import { GeoCodingError } from 'modules/geo-coding/errors/geo-coding.error'; import { AdminRegion } from 'modules/admin-regions/admin-region.entity'; +import { Geometry } from 'geojson'; /** * @debt: Define a more accurate DTO / Interface / Class for API-DB trades @@ -87,7 +88,7 @@ export class EUDRDTOProcessor { const geoRegion: GeoRegion = new GeoRegion(); let savedGeoRegion: GeoRegion; geoRegion.totalArea = row.total_area_ha; - geoRegion.theGeom = wellknown.parse(row.geometry) as unknown as JSON; + geoRegion.theGeom = wellknown.parse(row.geometry) as Geometry; geoRegion.isCreatedByUser = true; geoRegion.name = row.plot_name; const foundGeoRegion: GeoRegion | null =