Skip to content

Commit

Permalink
refactor map service
Browse files Browse the repository at this point in the history
  • Loading branch information
alexeh committed Oct 29, 2024
1 parent 9e63738 commit d33eb29
Show file tree
Hide file tree
Showing 10 changed files with 100 additions and 45 deletions.
9 changes: 9 additions & 0 deletions api/src/modules/countries/map/map-service.interface.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { MapRepository } from '@api/modules/countries/map/map.repository';

export interface IMapService {
mapRepository: MapRepository;

getMap(): Promise<any>;

getGeoPropertiesQuery(): string;
}
12 changes: 9 additions & 3 deletions api/src/modules/countries/map/map.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,15 @@ export class MapController {
@TsRestHandler(mapContract.getGeoFeatures)
async getGeoFeatures(): ControllerResponse {
return tsRestHandler(mapContract.getGeoFeatures, async ({ query }) => {
const geoFeatures = await this.mapRepository.getGeoFeatures(
query.countryCode,
);
const propertiesSubQuery = `'country', country.name`;
const queryBuilder =
this.mapRepository.getGeoFeaturesQueryBuilder(propertiesSubQuery);
if (query.countryCode) {
queryBuilder.where('country.code = :countryCode', {
countryCode: query.countryCode,
});
}
const geoFeatures = await queryBuilder.getRawOne();
return { body: geoFeatures, status: HttpStatus.OK };
});
}
Expand Down
51 changes: 12 additions & 39 deletions api/src/modules/countries/map/map.repository.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,14 @@
import { Injectable, Logger, NotFoundException } from '@nestjs/common';
import { Repository } from 'typeorm';
import { Injectable, Logger } from '@nestjs/common';
import { Repository, SelectQueryBuilder } from 'typeorm';
import { InjectRepository } from '@nestjs/typeorm';
import { Country } from '@shared/entities/country.entity';
import { BaseData } from '@shared/entities/base-data.entity';

import { FeatureCollection, Geometry } from 'geojson';

import { ProjectGeoProperties } from '@shared/schemas/geometries/projects';
import { FeatureCollection } from 'geojson';

/**
* @description: The aim for this repository is to work with geospatial data, for now "geometry" column in countries
* table. The country repository will be used to work with the metadata of the countries and avoid loading geometries when only metadata is needed,
* which can consume a lot of resources.
* @description: The aim for this repository is to work with geospatial data. Since we need to join with different entities and apply different filters
* depending on the joint entity, this repository returns a queryBuilder that can be used to retrieve all geometries by default, and/or to build
* a custom query with joins and filters.
*/

@Injectable()
Expand All @@ -24,48 +21,24 @@ export class MapRepository extends Repository<Country> {
super(repository.target, repository.manager, repository.queryRunner);
}

async getGeoFeatures(
countryCode: Country['code'],
): Promise<FeatureCollection<Geometry, ProjectGeoProperties>> {
getGeoFeaturesQueryBuilder(
propertiesSubQuery: string,
): SelectQueryBuilder<FeatureCollection> {
const queryBuilder = this.createQueryBuilder('country');
queryBuilder.innerJoin(BaseData, 'bd', 'bd.country_code = country.code');
queryBuilder.select(
`
json_build_object(
'type', 'FeatureCollection',
'features', json_agg(
json_build_object(
'type', 'Feature',
'geometry', ST_AsGeoJSON(ST_Simplify(country.geometry, 0.01))::jsonb,
'properties', json_build_object(${this.getPropertiesQuery()})
'geometry', ST_AsGeoJSON(country.geometry)::jsonb,
'properties', json_build_object(${propertiesSubQuery})
)
)
)`,
'geojson',
);
if (countryCode) {
queryBuilder.where('country.code = :countryCode', {
countryCode,
});
}

const result:
| {
geojson: FeatureCollection<Geometry, ProjectGeoProperties>;
}
| undefined = await queryBuilder.getRawOne<{
geojson: FeatureCollection<Geometry, ProjectGeoProperties>;
}>();
this.logger.log(`Retrieved geo features`);
if (!result) {
throw new NotFoundException(`Could not retrieve geo features`);
}
return result.geojson;
}

private getPropertiesQuery(): string {
return `'country', country.name,
'abatementPotential', 10000,
'cost', 20000`;
return queryBuilder as unknown as SelectQueryBuilder<FeatureCollection>;
}
}
41 changes: 41 additions & 0 deletions api/src/modules/projects/projects-map/projects-map.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { Injectable, NotFoundException } from '@nestjs/common';
import { MapRepository } from '@api/modules/countries/map/map.repository';
import { ProjectMap } from '@shared/dtos/projects/projects-map.dto';
import { IMapService } from '@api/modules/countries/map/map-service.interface';
import { Project } from '@shared/entities/projects.entity';

@Injectable()
export class ProjectsMapService implements IMapService {
mapRepository: MapRepository;
constructor(mapRepo: MapRepository) {
this.mapRepository = mapRepo;
}

async getMap(): Promise<ProjectMap> {
const mapQueryBuilder = this.mapRepository.getGeoFeaturesQueryBuilder(
this.getGeoPropertiesQuery(),
);
mapQueryBuilder.leftJoin(
Project,
'project',
'project.countryCode = country.code',
);
const result:
| {
geojson: ProjectMap;
}
| undefined = await mapQueryBuilder.getRawOne<{
geojson: ProjectMap;
}>();
if (!result) {
throw new NotFoundException(`Could not retrieve geo features`);
}
return result.geojson;
}

getGeoPropertiesQuery(): string {
return `'country', country.name,
'abatementPotential', project.abatementPotential,
'cost', project.totalCost`;
}
}
8 changes: 8 additions & 0 deletions api/src/modules/projects/projects.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,14 @@ export class ProjectsController {
});
}

// @TsRestHandler(projectsContract.getProjectsMap)
// async getProjectsMap(): ControllerResponse {
// return tsRestHandler(projectsContract.getProjectsMap, async () => {
// const data = await this.projectsService.getById(id, query);
// return { body: { data }, status: HttpStatus.OK };
// });
// }

@TsRestHandler(projectsContract.getProject)
async getProject(): ControllerResponse {
return tsRestHandler(
Expand Down
3 changes: 2 additions & 1 deletion api/src/modules/projects/projects.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,11 @@ import { Project } from '@shared/entities/projects.entity';
import { ProjectsController } from './projects.controller';
import { ProjectsService } from './projects.service';
import { CountriesModule } from '@api/modules/countries/countries.module';
import { ProjectsMapService } from '@api/modules/projects/projects-map/projects-map.service';

@Module({
imports: [TypeOrmModule.forFeature([Project]), CountriesModule],
controllers: [ProjectsController],
providers: [ProjectsService],
providers: [ProjectsService, ProjectsMapService],
})
export class ProjectsModule {}
3 changes: 1 addition & 2 deletions shared/contracts/map.contract.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,14 @@
import { initContract } from "@ts-rest/core";
import { z } from "zod";
import { FeatureCollection, Geometry } from "geojson";
import { ProjectGeoProperties } from "@shared/schemas/geometries/projects";

const contract = initContract();
export const mapContract = contract.router({
getGeoFeatures: {
method: "GET",
path: "/map/geo-features",
responses: {
200: contract.type<FeatureCollection<Geometry, ProjectGeoProperties>>(),
200: contract.type<FeatureCollection<Geometry, any>>(),
},
query: z.object({ countryCode: z.string().length(3).optional() }),
},
Expand Down
9 changes: 9 additions & 0 deletions shared/contracts/projects.contract.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import {
import { Project } from "@shared/entities/projects.entity";
import { FetchSpecification } from "nestjs-base-service";
import { CountryWithNoGeometry } from "@shared/entities/country.entity";
import { FeatureCollection, Geometry } from "geojson";
import { ProjectGeoProperties } from "@shared/schemas/geometries/projects";

const contract = initContract();
export const projectsContract = contract.router({
Expand Down Expand Up @@ -36,4 +38,11 @@ export const projectsContract = contract.router({
200: contract.type<ApiResponse<CountryWithNoGeometry[]>>(),
},
},
getProjectsMap: {
method: "GET",
path: "/projects/map",
responses: {
200: contract.type<FeatureCollection<Geometry, ProjectGeoProperties>>(),
},
},
});
7 changes: 7 additions & 0 deletions shared/dtos/projects/projects-map.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { z } from "zod";
import { FeatureCollection, Geometry } from "geojson";
import { ProjectGeoPropertiesSchema } from "@shared/schemas/geometries/projects";

export type ProjectGeoProperties = z.infer<typeof ProjectGeoPropertiesSchema>;

export type ProjectMap = FeatureCollection<Geometry, ProjectGeoProperties>;
2 changes: 2 additions & 0 deletions shared/schemas/geometries/projects.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { z } from "zod";
import { FeatureCollection, Geometry } from "geojson";

export const ProjectGeoPropertiesSchema = z.object({
abatementPotential: z.number(),
Expand All @@ -8,3 +9,4 @@ export const ProjectGeoPropertiesSchema = z.object({

export type ProjectGeoProperties = z.infer<typeof ProjectGeoPropertiesSchema>;

export type ProjectMap = FeatureCollection<Geometry, ProjectGeoProperties>;

0 comments on commit d33eb29

Please sign in to comment.