Vizzuality/landgriffon

View on GitHub
api/src/modules/admin-regions/admin-region.repository.ts

Summary

Maintainability
A
2 hrs
Test Coverage
F
50%
import { DataSource, SelectQueryBuilder } from 'typeorm';
import { AdminRegion } from 'modules/admin-regions/admin-region.entity';
import { ExtendedTreeRepository } from 'utils/tree.repository';
import { CreateAdminRegionDto } from 'modules/admin-regions/dto/create.admin-region.dto';
import { Injectable, Logger, NotFoundException } from '@nestjs/common';
import { SourcingLocation } from 'modules/sourcing-locations/sourcing-location.entity';
import { GetAdminRegionTreeWithOptionsDto } from 'modules/admin-regions/dto/get-admin-region-tree-with-options.dto';
import { GeoCodingError } from 'modules/geo-coding/errors/geo-coding.error';
import { BaseQueryBuilder } from 'utils/base.query-builder';

@Injectable()
export class AdminRegionRepository extends ExtendedTreeRepository<
  AdminRegion,
  CreateAdminRegionDto
> {
  logger: Logger = new Logger(AdminRegionRepository.name);

  constructor(private dataSource: DataSource) {
    super(AdminRegion, dataSource.createEntityManager());
  }

  async getAdminRegionAndGeoRegionIdByCoordinatesAndLevel(
    searchParams: {
      lng: number;
      lat: number;
      level: number;
    },
    sourcingLocation: SourcingLocation,
  ): Promise<{ adminRegionId: string; geoRegionId: string }> {
    const res: any = await this.query(
      `
        SELECT a.id AS "adminRegionId", g.id AS "geoRegionId"
        FROM admin_region a
               RIGHT JOIN geo_region g on a."geoRegionId" = g.id
        WHERE ST_Intersects(
          st_setsrid($1::geometry, 4326),
          st_setsrid(g."theGeom"::geometry, 4326)
          )
          AND a."level" = ${searchParams.level};
        ;
      `,
      [`POINT(${searchParams.lng} ${searchParams.lat})`],
    );

    if (!res.length) {
      this.logger.error(
        `Could not retrieve a Admin Region with LEVEL ${searchParams.level} and Coordinates: LAT: ${searchParams.lat} LONG: ${searchParams.lng}`,
      );
      throw new NotFoundException(
        `No Admin Region where Coordinates: LAT: ${searchParams.lat}, LONG: ${searchParams.lng} are could been found`,
      );
    }

    await this.validateAdminRegion(
      res[0].adminRegionId,
      sourcingLocation,
      searchParams,
    );

    return res[0];
  }

  /**
   * @description: Get the closest available geometry given some coordinates.
   * @note We don't know the adminRegion.level here so we just order the results by this field and get the most accurate one (with the higher level value)
   *       Limiting the result to 3 as we only have 3 levels of admin regions
   *       Then we take the most accurate geometry (the higher value of adminRegion.level)
   */

  //TODO: Couldn't figure out how to retrieve the most accurate value straight from the query. Ordering the values always retrieve
  //      level 1 or 2, and depending on coordinates, level 0.
  //      Check how to properly perform this

  async getClosestAdminRegionByCoordinates(
    coordinates: {
      lng: number;
      lat: number;
    },
    sourcingLocation: SourcingLocation,
  ): Promise<any> {
    const res: any = await this.query(
      `SELECT a.id AS "adminRegionId" , a."name", a."level" , g."name" , g.id AS "geoRegionId"
        FROM admin_region a
               RIGHT JOIN geo_region g on a."geoRegionId" = g.id
        WHERE ST_Intersects(
          st_setsrid($1::geometry, 4326),
          st_setsrid(g."theGeom"::geometry, 4326)
          )
        AND a.id IS NOT NULL
        ORDER BY a.level DESC
        LIMIT 3`,
      [`POINT(${coordinates.lng} ${coordinates.lat})`],
    );

    if (!res.length) {
      this.logger.error(
        `Could not find any Admin Region that intersects with Coordinates: LAT: ${coordinates.lat} LONG: ${coordinates.lng}`,
      );
      throw new GeoCodingError(
        `Coordinates ${coordinates.lat}, ${coordinates.lng} are not inside ${sourcingLocation.locationCountryInput}`,
      );
    }

    /**
     * In this case we can get an Intersection with the radius created by this sourcing location so we get the highest level
     * as doing this is more performant that getting the geometry and intersecting with it
     */
    const result: any = res.reduce(function (previous: any, current: any) {
      return previous.level > current.level ? previous : current;
    });

    await this.validateAdminRegion(
      result.adminRegionId,
      sourcingLocation,
      coordinates,
    );

    return result;
  }

  /**
   ** @description Retrieves Admin Region and its ancestor and checks if it's inside provided country
   */
  async validateAdminRegion(
    adminRegionId: string,
    sourcingLocation: SourcingLocation,
    coordinates: {
      lng: number;
      lat: number;
    },
  ): Promise<void> {
    const intersectingCountries: AdminRegion[] = await this.query(
      `
     SELECT a.id AS "adminRegionId" , a."name", a."level" , g.id AS "geoRegionId"
        FROM admin_region a
               RIGHT JOIN geo_region g on a."geoRegionId" = g.id
        WHERE ST_Intersects(
         ST_BUFFER(ST_SetSRID(ST_POINT($1 ,$2),4326)::geometry, 0.01),
          st_setsrid(g."theGeom"::geometry, 4326)
          )
        AND a.id IS NOT null
        and a."level" = 0
    `,
      [coordinates.lng, coordinates.lat],
    );
    if (
      !intersectingCountries.some(
        (intersectingCountry: AdminRegion) =>
          intersectingCountry.name === sourcingLocation.locationCountryInput,
      )
    )
      throw new GeoCodingError(
        sourcingLocation.locationAddressInput
          ? `Address ${sourcingLocation.locationAddressInput} is not inside ${sourcingLocation.locationCountryInput}`
          : `Coordinates ${coordinates.lat}, ${coordinates.lng} are not inside ${sourcingLocation.locationCountryInput}`,
      );
  }

  /**
   * @description Get all admin regions that are present in Sourcing Locations with given filters
   *              Additionally if withAncestry set to true (default) it will return the ancestry of each
   *              element up to the root
   */

  async getAdminRegionsFromSourcingLocations(
    adminRegionTreeOptions: GetAdminRegionTreeWithOptionsDto,
    withAncestry: boolean = true,
  ): Promise<AdminRegion[]> {
    // Join and filters over materials present in sourcing-locations. Resultant query returns IDs of elements meeting the filters
    const initialQueryBuilder: SelectQueryBuilder<AdminRegion> =
      this.createQueryBuilder('ar')
        .innerJoin(SourcingLocation, 'sl', 'sl.adminRegionId = ar.id')
        .distinct(true);
    const queryBuilder: SelectQueryBuilder<AdminRegion> =
      BaseQueryBuilder.addFilters(initialQueryBuilder, adminRegionTreeOptions);

    if (!withAncestry) {
      return queryBuilder.getMany();
    }
    queryBuilder.select('ar.id');

    return this.getEntityAncestryFlatArray<AdminRegion>(
      queryBuilder,
      AdminRegion.name,
    );
  }
}