diff --git a/.gitignore b/.gitignore index ee0dc93..0ae4a00 100644 --- a/.gitignore +++ b/.gitignore @@ -49,3 +49,6 @@ src/.DS_Store /src/geojson-data/ /src/geoquery.in.data/ db.mmdb + + +.env diff --git a/package-lock.json b/package-lock.json index 8733f7e..838d391 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,6 +16,7 @@ "@nestjs/platform-express": "^10.0.0", "@nestjs/platform-fastify": "^10.3.0", "@nestjs/swagger": "^7.3.1", + "@prisma/client": "^5.17.0", "@samagra-x/stencil": "^0.0.6", "@turf/turf": "^6.5.0", "@types/multer": "^1.4.11", @@ -48,6 +49,7 @@ "husky": "8.0.3", "jest": "^29.5.0", "prettier": "^3.0.0", + "prisma": "^5.17.0", "source-map-support": "^0.5.21", "supertest": "^6.3.3", "ts-jest": "^29.1.0", @@ -2150,6 +2152,68 @@ "url": "https://opencollective.com/unts" } }, + "node_modules/@prisma/client": { + "version": "5.17.0", + "resolved": "https://registry.npmjs.org/@prisma/client/-/client-5.17.0.tgz", + "integrity": "sha512-N2tnyKayT0Zf7mHjwEyE8iG7FwTmXDHFZ1GnNhQp0pJUObsuel4ZZ1XwfuAYkq5mRIiC/Kot0kt0tGCfLJ70Jw==", + "hasInstallScript": true, + "engines": { + "node": ">=16.13" + }, + "peerDependencies": { + "prisma": "*" + }, + "peerDependenciesMeta": { + "prisma": { + "optional": true + } + } + }, + "node_modules/@prisma/debug": { + "version": "5.17.0", + "resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-5.17.0.tgz", + "integrity": "sha512-l7+AteR3P8FXiYyo496zkuoiJ5r9jLQEdUuxIxNCN1ud8rdbH3GTxm+f+dCyaSv9l9WY+29L9czaVRXz9mULfg==", + "devOptional": true + }, + "node_modules/@prisma/engines": { + "version": "5.17.0", + "resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-5.17.0.tgz", + "integrity": "sha512-+r+Nf+JP210Jur+/X8SIPLtz+uW9YA4QO5IXA+KcSOBe/shT47bCcRMTYCbOESw3FFYFTwe7vU6KTWHKPiwvtg==", + "devOptional": true, + "hasInstallScript": true, + "dependencies": { + "@prisma/debug": "5.17.0", + "@prisma/engines-version": "5.17.0-31.393aa359c9ad4a4bb28630fb5613f9c281cde053", + "@prisma/fetch-engine": "5.17.0", + "@prisma/get-platform": "5.17.0" + } + }, + "node_modules/@prisma/engines-version": { + "version": "5.17.0-31.393aa359c9ad4a4bb28630fb5613f9c281cde053", + "resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-5.17.0-31.393aa359c9ad4a4bb28630fb5613f9c281cde053.tgz", + "integrity": "sha512-tUuxZZysZDcrk5oaNOdrBnnkoTtmNQPkzINFDjz7eG6vcs9AVDmA/F6K5Plsb2aQc/l5M2EnFqn3htng9FA4hg==", + "devOptional": true + }, + "node_modules/@prisma/fetch-engine": { + "version": "5.17.0", + "resolved": "https://registry.npmjs.org/@prisma/fetch-engine/-/fetch-engine-5.17.0.tgz", + "integrity": "sha512-ESxiOaHuC488ilLPnrv/tM2KrPhQB5TRris/IeIV4ZvUuKeaicCl4Xj/JCQeG9IlxqOgf1cCg5h5vAzlewN91Q==", + "devOptional": true, + "dependencies": { + "@prisma/debug": "5.17.0", + "@prisma/engines-version": "5.17.0-31.393aa359c9ad4a4bb28630fb5613f9c281cde053", + "@prisma/get-platform": "5.17.0" + } + }, + "node_modules/@prisma/get-platform": { + "version": "5.17.0", + "resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-5.17.0.tgz", + "integrity": "sha512-UlDgbRozCP1rfJ5Tlkf3Cnftb6srGrEQ4Nm3og+1Se2gWmCZ0hmPIi+tQikGDUVLlvOWx3Gyi9LzgRP+HTXV9w==", + "devOptional": true, + "dependencies": { + "@prisma/debug": "5.17.0" + } + }, "node_modules/@protobufjs/aspromise": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", @@ -10211,6 +10275,22 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/prisma": { + "version": "5.17.0", + "resolved": "https://registry.npmjs.org/prisma/-/prisma-5.17.0.tgz", + "integrity": "sha512-m4UWkN5lBE6yevqeOxEvmepnL5cNPEjzMw2IqDB59AcEV6w7D8vGljDLd1gPFH+W6gUxw9x7/RmN5dCS/WTPxA==", + "devOptional": true, + "hasInstallScript": true, + "dependencies": { + "@prisma/engines": "5.17.0" + }, + "bin": { + "prisma": "build/index.js" + }, + "engines": { + "node": ">=16.13" + } + }, "node_modules/process": { "version": "0.11.10", "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", @@ -14061,6 +14141,56 @@ "integrity": "sha512-cq8o4cWH0ibXh9VGi5P20Tu9XF/0fFXl9EUinr9QfTM7a7p0oTA4iJRCQWppXR1Pg8dSM0UCItCkPwsk9qWWYA==", "dev": true }, + "@prisma/client": { + "version": "5.17.0", + "resolved": "https://registry.npmjs.org/@prisma/client/-/client-5.17.0.tgz", + "integrity": "sha512-N2tnyKayT0Zf7mHjwEyE8iG7FwTmXDHFZ1GnNhQp0pJUObsuel4ZZ1XwfuAYkq5mRIiC/Kot0kt0tGCfLJ70Jw==", + "requires": {} + }, + "@prisma/debug": { + "version": "5.17.0", + "resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-5.17.0.tgz", + "integrity": "sha512-l7+AteR3P8FXiYyo496zkuoiJ5r9jLQEdUuxIxNCN1ud8rdbH3GTxm+f+dCyaSv9l9WY+29L9czaVRXz9mULfg==", + "devOptional": true + }, + "@prisma/engines": { + "version": "5.17.0", + "resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-5.17.0.tgz", + "integrity": "sha512-+r+Nf+JP210Jur+/X8SIPLtz+uW9YA4QO5IXA+KcSOBe/shT47bCcRMTYCbOESw3FFYFTwe7vU6KTWHKPiwvtg==", + "devOptional": true, + "requires": { + "@prisma/debug": "5.17.0", + "@prisma/engines-version": "5.17.0-31.393aa359c9ad4a4bb28630fb5613f9c281cde053", + "@prisma/fetch-engine": "5.17.0", + "@prisma/get-platform": "5.17.0" + } + }, + "@prisma/engines-version": { + "version": "5.17.0-31.393aa359c9ad4a4bb28630fb5613f9c281cde053", + "resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-5.17.0-31.393aa359c9ad4a4bb28630fb5613f9c281cde053.tgz", + "integrity": "sha512-tUuxZZysZDcrk5oaNOdrBnnkoTtmNQPkzINFDjz7eG6vcs9AVDmA/F6K5Plsb2aQc/l5M2EnFqn3htng9FA4hg==", + "devOptional": true + }, + "@prisma/fetch-engine": { + "version": "5.17.0", + "resolved": "https://registry.npmjs.org/@prisma/fetch-engine/-/fetch-engine-5.17.0.tgz", + "integrity": "sha512-ESxiOaHuC488ilLPnrv/tM2KrPhQB5TRris/IeIV4ZvUuKeaicCl4Xj/JCQeG9IlxqOgf1cCg5h5vAzlewN91Q==", + "devOptional": true, + "requires": { + "@prisma/debug": "5.17.0", + "@prisma/engines-version": "5.17.0-31.393aa359c9ad4a4bb28630fb5613f9c281cde053", + "@prisma/get-platform": "5.17.0" + } + }, + "@prisma/get-platform": { + "version": "5.17.0", + "resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-5.17.0.tgz", + "integrity": "sha512-UlDgbRozCP1rfJ5Tlkf3Cnftb6srGrEQ4Nm3og+1Se2gWmCZ0hmPIi+tQikGDUVLlvOWx3Gyi9LzgRP+HTXV9w==", + "devOptional": true, + "requires": { + "@prisma/debug": "5.17.0" + } + }, "@protobufjs/aspromise": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", @@ -20176,6 +20306,15 @@ } } }, + "prisma": { + "version": "5.17.0", + "resolved": "https://registry.npmjs.org/prisma/-/prisma-5.17.0.tgz", + "integrity": "sha512-m4UWkN5lBE6yevqeOxEvmepnL5cNPEjzMw2IqDB59AcEV6w7D8vGljDLd1gPFH+W6gUxw9x7/RmN5dCS/WTPxA==", + "devOptional": true, + "requires": { + "@prisma/engines": "5.17.0" + } + }, "process": { "version": "0.11.10", "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", diff --git a/package.json b/package.json index ddf4142..4656df1 100644 --- a/package.json +++ b/package.json @@ -28,6 +28,7 @@ "@nestjs/platform-express": "^10.0.0", "@nestjs/platform-fastify": "^10.3.0", "@nestjs/swagger": "^7.3.1", + "@prisma/client": "^5.17.0", "@samagra-x/stencil": "^0.0.6", "@turf/turf": "^6.5.0", "@types/multer": "^1.4.11", @@ -60,6 +61,7 @@ "husky": "8.0.3", "jest": "^29.5.0", "prettier": "^3.0.0", + "prisma": "^5.17.0", "source-map-support": "^0.5.21", "supertest": "^6.3.3", "ts-jest": "^29.1.0", diff --git a/prisma/docker-compose.yaml b/prisma/docker-compose.yaml new file mode 100644 index 0000000..9bcac96 --- /dev/null +++ b/prisma/docker-compose.yaml @@ -0,0 +1,12 @@ +version: "3.9" + +services: + postgis: + container_name: geopostgis + image: postgis/postgis:16-3.4-alpine + ports: + - "5432:5432" + environment: + - POSTGRES_PASSWORD=password + - POSTGRES_USER=admin + - POSTGRES_DB=gis diff --git a/prisma/migrations/20240906152553_init/migration.sql b/prisma/migrations/20240906152553_init/migration.sql new file mode 100644 index 0000000..afd9cd0 --- /dev/null +++ b/prisma/migrations/20240906152553_init/migration.sql @@ -0,0 +1,85 @@ +-- CreateExtension +CREATE EXTENSION IF NOT EXISTS "fuzzystrmatch"; + +-- CreateExtension +CREATE EXTENSION IF NOT EXISTS "postgis"; + +-- CreateTable +CREATE TABLE "State" ( + "id" SERIAL NOT NULL, + "state_code" INTEGER NOT NULL, + "state_name" TEXT NOT NULL, + "metadata" JSONB, + "geometry" geometry NOT NULL, + + CONSTRAINT "State_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "District" ( + "id" SERIAL NOT NULL, + "district_code" INTEGER NOT NULL, + "district_name" TEXT NOT NULL, + "geometry" geometry NOT NULL, + "metadata" JSONB, + "state_id" INTEGER, + + CONSTRAINT "District_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "SubDistrict" ( + "id" SERIAL NOT NULL, + "subdistrict_code" INTEGER NOT NULL, + "subdistrict_name" TEXT NOT NULL, + "geometry" geometry NOT NULL, + "metadata" JSONB, + "district_id" INTEGER, + "state_id" INTEGER, + + CONSTRAINT "SubDistrict_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "Village" ( + "id" SERIAL NOT NULL, + "village_code" SERIAL NOT NULL, + "geometry" geometry NOT NULL, + "village_name" TEXT NOT NULL, + "metadata" JSONB, + "subdistrict_id" INTEGER, + "district_id" INTEGER, + "state_id" INTEGER, + + CONSTRAINT "Village_pkey" PRIMARY KEY ("id") +); + +-- CreateIndex +CREATE UNIQUE INDEX "State_state_code_key" ON "State"("state_code"); + +-- CreateIndex +CREATE UNIQUE INDEX "State_state_name_key" ON "State"("state_name"); + +-- CreateIndex +CREATE UNIQUE INDEX "District_district_code_key" ON "District"("district_code"); + +-- CreateIndex +CREATE UNIQUE INDEX "SubDistrict_subdistrict_code_key" ON "SubDistrict"("subdistrict_code"); + +-- AddForeignKey +ALTER TABLE "District" ADD CONSTRAINT "District_state_id_fkey" FOREIGN KEY ("state_id") REFERENCES "State"("state_code") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "SubDistrict" ADD CONSTRAINT "SubDistrict_district_id_fkey" FOREIGN KEY ("district_id") REFERENCES "District"("district_code") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "SubDistrict" ADD CONSTRAINT "SubDistrict_state_id_fkey" FOREIGN KEY ("state_id") REFERENCES "State"("state_code") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Village" ADD CONSTRAINT "Village_subdistrict_id_fkey" FOREIGN KEY ("subdistrict_id") REFERENCES "SubDistrict"("subdistrict_code") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Village" ADD CONSTRAINT "Village_district_id_fkey" FOREIGN KEY ("district_id") REFERENCES "District"("district_code") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Village" ADD CONSTRAINT "Village_state_id_fkey" FOREIGN KEY ("state_id") REFERENCES "State"("state_code") ON DELETE SET NULL ON UPDATE CASCADE; diff --git a/prisma/migrations/migration_lock.toml b/prisma/migrations/migration_lock.toml new file mode 100644 index 0000000..fbffa92 --- /dev/null +++ b/prisma/migrations/migration_lock.toml @@ -0,0 +1,3 @@ +# Please do not edit this file manually +# It should be added in your version-control system (i.e. Git) +provider = "postgresql" \ No newline at end of file diff --git a/prisma/schema.prisma b/prisma/schema.prisma new file mode 100644 index 0000000..7e2e3ed --- /dev/null +++ b/prisma/schema.prisma @@ -0,0 +1,76 @@ +// This is your Prisma schema file, +// learn more about it in the docs: https://pris.ly/d/prisma-schema + +// Looking for ways to speed up your queries, or scale easily with your serverless or edge functions? +// Try Prisma Accelerate: https://pris.ly/cli/accelerate-init + +// Command for migration: npx prisma migrate dev --name init + +generator client { + provider = "prisma-client-js" + previewFeatures = ["postgresqlExtensions"] +} + +datasource db { + provider = "postgresql" + url = env("DATABASE_URL") + + extensions = [postgis(), fuzzystrmatch()] +} + +model State { + id Int @id @default(autoincrement()) + state_code Int @unique + state_name String @unique + metadata Json? + geometry Unsupported("geometry") + district District[] + subdistrict SubDistrict[] + village Village[] +} + +model District { + id Int @id @default(autoincrement()) + district_code Int @unique + district_name String + geometry Unsupported("geometry") + metadata Json? + + state_id Int? + state State? @relation(fields: [state_id], references: [state_code]) + subdistrict SubDistrict[] + village Village[] +} + +model SubDistrict { + id Int @id @default(autoincrement()) + subdistrict_code Int @unique + subdistrict_name String + geometry Unsupported("geometry") + metadata Json? + + district_id Int? + district District? @relation(fields: [district_id], references: [district_code]) + + state_id Int? + state State? @relation(fields: [state_id], references: [state_code]) + village Village[] +} + +model Village { + id Int @id @default(autoincrement()) + village_code Int @default(autoincrement()) + + geometry Unsupported("geometry") + village_name String + metadata Json? + + subdistrict_id Int? + subdistrict SubDistrict? @relation(fields: [subdistrict_id], references: [subdistrict_code]) + + district_id Int? + district District? @relation(fields: [district_id], references: [district_code]) + + state_id Int? + state State? @relation(fields: [state_id], references: [state_code]) +} diff --git a/src/app.module.ts b/src/app.module.ts index 395fed7..9534cd0 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -2,6 +2,7 @@ import { Module } from '@nestjs/common'; import { CityModule } from './modules/city/city.module'; import { GeorevModule } from './modules/georev/georev.module'; import { LocationModule } from './modules/location/location.module'; +import { PrismaModule } from './modules/prisma/prisma.module'; import { ConfigModule } from '@nestjs/config'; import { config } from './config/config'; import { AppController } from './app.controller'; @@ -12,6 +13,7 @@ import { AppService } from './app.service'; CityModule, GeorevModule, LocationModule, + PrismaModule, ConfigModule.forRoot({ envFilePath: `.env`, load: [config], diff --git a/src/config/config.ts b/src/config/config.ts index 18690ff..469ff8e 100644 --- a/src/config/config.ts +++ b/src/config/config.ts @@ -1,36 +1,23 @@ export const config = () => ({ NODE_ENV: process.env.NODE_ENV || 'default', port: parseInt(process.env.PORT) || 3000, - host: parseInt(process.env.HOST) || '0.0.0.0', - method: parseInt(process.env.METHOD) || 'http', - requiredGeoLocationLevels: ['SUBDISTRICT', 'DISTRICT', 'STATE'], - geoLocationLevels: { - VILLAGE: 'VILLAGE', - SUBDISTRICT: 'SUBDISTRICT', - DISTRICT: 'DISTRICT', - STATE: 'STATE', - }, - levelsMapping: { - STATE: { - name: 'state', - path: 'state', - depth: 0, + tableLevels: [], + tableMeta: { + "STATE": { + tname: "State", + fname: "state_name" }, - DISTRICT: { - name: 'district', - path: 'state->district', - depth: 1, + "DISTRICT": { + tname: "District", + fname: "district_name", }, - SUBDISTRICT: { - name: 'subDistrict', - path: 'state->district->subDistrict', - depth: 2, + "SUBDISTRICT": { + tname: "SubDistrict", + fname: "subdistrict_name", }, - VILLAGE: { - name: 'village', - path: 'state->district->subDistrict->village', - depth: 3, - }, - }, - country: 'INDIA', + "VILLAGE": { + tname: "Village", + fname: "village_name", + } + } }); diff --git a/src/modules/georev/georev.controller.spec.ts b/src/modules/georev/georev.controller.spec.ts index ef32cf0..ef8bf2e 100644 --- a/src/modules/georev/georev.controller.spec.ts +++ b/src/modules/georev/georev.controller.spec.ts @@ -3,8 +3,9 @@ import { ConfigService } from '@nestjs/config'; import { GeorevController } from './georev.controller'; import { GeorevService } from './georev.service'; import { GeoqueryService } from '../../services/geoquery/geoquery.service'; -import { GeojsonService } from '../../services/geojson/geojson.service'; import { HttpException, HttpStatus } from '@nestjs/common'; +import { PrismaService } from '../prisma/prisma.service'; +// import { PrismaService } from '../../services/prisma/prisma.service'; // Add PrismaService import describe('GeorevController', () => { let controller: GeorevController; @@ -16,7 +17,19 @@ describe('GeorevController', () => { providers: [ GeorevService, GeoqueryService, - GeojsonService, + { + provide: PrismaService, // Mock PrismaService here + useValue: { + // Mocked methods of PrismaService, if needed + $queryRawUnsafe: jest.fn((query: string) => { + return [{ + state_name: 'DELHI', + district_name: 'North West', + subdistrict_name: 'Saraswati Vihar', + }] + }), + }, + }, { provide: ConfigService, useValue: { @@ -63,13 +76,17 @@ describe('GeorevController', () => { it('should handle missing lat lon query parameters', async () => { const lat = ''; const lon = ''; - + try{ const result = await controller.getGeoRev(lat, lon); - - expect(result).toEqual({ + } catch (error) { + expect(error).toBeInstanceOf(HttpException); + expect(error.getStatus()).toBe(HttpStatus.INTERNAL_SERVER_ERROR); + expect(error.getResponse()).toEqual({ status: 'fail', error: 'lat lon query missing', }); + } + }); it('should handle error when processing lat lon', async () => { @@ -78,12 +95,13 @@ describe('GeorevController', () => { try { await controller.getGeoRev(lat, lon); - } catch (error) { + } + catch (error) { expect(error).toBeInstanceOf(HttpException); expect(error.getStatus()).toBe(HttpStatus.INTERNAL_SERVER_ERROR); expect(error.getResponse()).toEqual({ status: 'fail', - error: 'coordinates must contain numbers', + error: 'Invalid latitude or longitude', }); } }); @@ -92,6 +110,16 @@ describe('GeorevController', () => { const lat = '1.2345'; // valid latitude const lon = '2.3456'; // valid longitude + jest.spyOn(service, 'getGeoRev').mockRejectedValue( + new HttpException( + { + status: 'fail', + error: `No GeoLocation found for lat: ${lat}, lon: ${lon}`, + }, + HttpStatus.INTERNAL_SERVER_ERROR, + ), + ); + try { await controller.getGeoRev(lat, lon); } catch (error) { @@ -99,7 +127,7 @@ describe('GeorevController', () => { expect(error.getStatus()).toBe(HttpStatus.INTERNAL_SERVER_ERROR); expect(error.getResponse()).toEqual({ status: 'fail', - error: 'No GeoLocation found for lat: 1.2345, lon: 2.3456', + error: `No GeoLocation found for lat: ${lat}, lon: ${lon}`, }); } }); diff --git a/src/modules/georev/georev.controller.ts b/src/modules/georev/georev.controller.ts index 6910318..076ff08 100644 --- a/src/modules/georev/georev.controller.ts +++ b/src/modules/georev/georev.controller.ts @@ -8,24 +8,38 @@ import { } from '@nestjs/common'; import { ApiTags } from '@nestjs/swagger'; import { GeorevService } from './georev.service'; -import { formatGeorevSuccessResponse } from '../..//utils/serializer/success'; +import { formatGeorevSuccessResponse } from '../../utils/serializer/success'; @ApiTags('/georev') @Controller('georev') export class GeorevController { private readonly logger = new Logger(GeorevController.name); - constructor(private readonly geoRevService: GeorevService) {} + constructor(private readonly geoRevService: GeorevService) { + } @Get() async getGeoRev(@Query('lat') lat: string, @Query('lon') lon: string) { try { if (!lat || !lon) { this.logger.error(`lat lon query missing`); - return { status: 'fail', error: `lat lon query missing` }; + throw new HttpException( + { status: 'fail', error: `lat lon query missing` }, + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + + if (!this.geoRevService.isValidLatitudeLongitude(lat, lon)) { + this.logger.error('Invalid latitude or longitude'); + throw new HttpException( + { 'status': 'fail', 'error': 'Invalid latitude or longitude' }, + HttpStatus.INTERNAL_SERVER_ERROR, + ); } - const resp = this.geoRevService.getGeoRev(lat, lon); + let resp = await this.geoRevService.getGeoRev(lat, lon); + resp = resp[0]; + if (!resp) { this.logger.error(`No GeoLocation found for lat: ${lat}, lon: ${lon}`); diff --git a/src/modules/georev/georev.module.ts b/src/modules/georev/georev.module.ts index 0cfac64..adf6695 100644 --- a/src/modules/georev/georev.module.ts +++ b/src/modules/georev/georev.module.ts @@ -1,11 +1,10 @@ import { Module } from '@nestjs/common'; import { GeorevController } from './georev.controller'; import { GeorevService } from './georev.service'; -import { GeojsonService } from '../../services/geojson/geojson.service'; import { GeoqueryService } from '../../services/geoquery/geoquery.service'; @Module({ controllers: [GeorevController], - providers: [GeorevService, GeojsonService, GeoqueryService], + providers: [GeorevService, GeoqueryService], }) export class GeorevModule {} diff --git a/src/modules/georev/georev.service.spec.ts b/src/modules/georev/georev.service.spec.ts index 26d4dc2..ab97a28 100644 --- a/src/modules/georev/georev.service.spec.ts +++ b/src/modules/georev/georev.service.spec.ts @@ -2,24 +2,14 @@ import { Test, TestingModule } from '@nestjs/testing'; import { GeorevService } from './georev.service'; import { ConfigService } from '@nestjs/config'; import { GeoqueryService } from '../../services/geoquery/geoquery.service'; -import { GeojsonService } from '../../services/geojson/geojson.service'; +import { PrismaService } from '../prisma/prisma.service'; const constants = { getGeoRev: { success: { - levelLocationName: 'Saraswati Vihar', - OBJECTID: 430, - stcode11: '07', - dtcode11: '090', - sdtcode11: '00431', - Shape_Length: 107706.63225163253, - Shape_Area: 199520680.70346165, - stname: 'DELHI', - dtname: 'North West', - sdtname: 'Saraswati Vihar', - Subdt_LGD: 431, - Dist_LGD: 82, - State_LGD: 7, + state_name: 'DELHI', + district_name: 'North West', + subdistrict_name: 'Saraswati Vihar', }, }, }; @@ -31,7 +21,19 @@ describe('GeorevService', () => { const module: TestingModule = await Test.createTestingModule({ providers: [ GeorevService, - GeojsonService, + { + provide: PrismaService, // Mock PrismaService here + useValue: { + // Mocked methods of PrismaService, if needed + $queryRawUnsafe: jest.fn((query: string) => { + return [{ + state_name: 'DELHI', + district_name: 'North West', + subdistrict_name: 'Saraswati Vihar', + }] + }), + }, + }, GeoqueryService, { provide: ConfigService, @@ -62,24 +64,16 @@ describe('GeorevService', () => { expect(service).toBeDefined(); }); - it('should call individualQuery method with correct parameters', () => { + it('should call individualQuery method with correct parameters', async () => { const lat = '10.12345'; const lon = '20.67890'; - jest - .spyOn(service, 'getGeoRev') - .mockReturnValue(constants.getGeoRev.success); - - const result = service.getGeoRev(lat, lon); + // jest + // .spyOn(service, 'getGeoRev') + // .mockReturnValue(constants.getGeoRev.success); - expect(result).toEqual(constants.getGeoRev.success); - }); + const result = await service.getGeoRev(lat, lon); - it('should handle a missing coordinate', () => { - jest - .spyOn(service, 'getGeoRev') - .mockReturnValue(Error('coordinates must contain numbers')); - const result = service.getGeoRev(null, '20.67890'); - expect(result).toEqual(Error('coordinates must contain numbers')); + expect(result).toEqual([constants.getGeoRev.success]); }); }); diff --git a/src/modules/georev/georev.service.ts b/src/modules/georev/georev.service.ts index f7aed16..08b4c07 100644 --- a/src/modules/georev/georev.service.ts +++ b/src/modules/georev/georev.service.ts @@ -1,6 +1,5 @@ import { Injectable } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; -import { GeojsonService } from '../../services/geojson/geojson.service'; import { GeoqueryService } from '../../services/geoquery/geoquery.service'; @Injectable() @@ -9,24 +8,26 @@ export class GeorevService { constructor( private readonly geoQueryService: GeoqueryService, - private readonly geoJsonService: GeojsonService, private readonly configService: ConfigService, ) { - this.geoJsonFiles = geoJsonService.getGeoJsonFiles(); } - getGeoRev(lat: string, lon: string) { + async getGeoRev(lat: string, lon: string) { try { - // Searching for SUBDISTRICT GeoLocation Level - const response = this.geoQueryService.individualQuery( - this.configService.get('country'), - this.configService.get('geoLocationLevels.SUBDISTRICT'), - [parseFloat(lon), parseFloat(lat)], - this.geoJsonFiles, - ); - return response; + return await this.geoQueryService.querySubDistrictContains(parseFloat(lat), parseFloat(lon)); } catch (error) { throw error; } } + + + isValidLatitudeLongitude(lat: string, lon: string) { + const parsedLat = parseFloat(lat); + const parsedLon = parseFloat(lon); + + const isValidLat = !isNaN(parsedLat) && parsedLat >= -90 && parsedLat <= 90; + const isValidLon = !isNaN(parsedLon) && parsedLon >= -180 && parsedLon <= 180; + + return isValidLat && isValidLon; + } } diff --git a/src/modules/location/location.controller.spec.ts b/src/modules/location/location.controller.spec.ts index 1f7a76a..8df1b75 100644 --- a/src/modules/location/location.controller.spec.ts +++ b/src/modules/location/location.controller.spec.ts @@ -3,133 +3,115 @@ import { LocationController } from './location.controller'; import { ConfigService } from '@nestjs/config'; import { LocationSearchService } from './location.search-service'; import { LocationService } from './location.service'; -import { HttpException, HttpStatus, Logger } from '@nestjs/common'; +import { HttpException, HttpStatus } from '@nestjs/common'; describe('LocationController', () => { - let controller: LocationController; - let configService: ConfigService; - let locationSearchService: LocationSearchService; + let locationController: LocationController; let locationService: LocationService; + let locationSearchService: LocationSearchService; beforeEach(async () => { const module: TestingModule = await Test.createTestingModule({ controllers: [LocationController], providers: [ { - provide: ConfigService, + provide: LocationService, useValue: { - get: jest.fn((key: string) => { - if (key === 'geoLocationLevels') { - return { - VILLAGE: 'VILLAGE', - SUBDISTRICT: 'SUBDISTRICT', - DISTRICT: 'DISTRICT', - STATE: 'STATE', - }; - } else if (key === 'levelsMapping') { - return { - STATE: { - name: 'state', - path: 'state', - depth: 0, - }, - DISTRICT: { - name: 'district', - path: 'state->district', - depth: 1, - }, - SUBDISTRICT: { - name: 'subDistrict', - path: 'state->district->subDistrict', - depth: 2, - }, - VILLAGE: { - name: 'village', - path: 'state->district->subDistrict->village', - depth: 3, - }, - }; - } - }), + getCentroid: jest.fn(), // Mock the method for LocationService }, }, { provide: LocationSearchService, useValue: { - fuzzySearch: jest.fn(() => ({ result: 'mocked result' })), + fuzzySearch: jest.fn(( + locationLevel, + query, + filter + ) => { + return [{"name": "Location 1"}] + }), // Mock the method for LocationSearchService }, }, { - provide: LocationService, + provide: ConfigService, useValue: { - getCentroid: jest.fn(() => ({ - properties: { - levelLocationName: 'Lucknow', - dtname: 'Lucknow', - stname: 'UTTAR PRADESH', - stcode11: '09', - dtcode11: '157', - year_stat: '2011_c', - SHAPE_Length: 424086.646831452, - SHAPE_Area: 3190740670.6066375, - OBJECTID: 229, - test: null, - Dist_LGD: 162, - State_LGD: 9, - }, - latitude: 26.830190863213858, - longitude: 80.89119983155268, - })), + get: jest.fn((key: string) => { + if (key === 'tableMeta') { + return { LEVEL1: 'Level 1', LEVEL2: 'Level 2' }; + } else if (key === 'levelsMapping') { + return { LEVEL1: 'Mapping 1', LEVEL2: 'Mapping 2' }; + } + return null; + }), }, }, ], }).compile(); - controller = module.get(LocationController); - configService = module.get(ConfigService); + locationController = module.get(LocationController); + locationService = module.get(LocationService); locationSearchService = module.get( LocationSearchService, ); - locationService = module.get(LocationService); }); it('should be defined', () => { - expect(controller).toBeDefined(); + expect(locationController).toBeDefined(); }); - it('should handle missing query parameter in getCentroid', () => { - expect(() => controller.getCentroid('state', null)).toThrow(HttpException); + describe('health', () => { + it('should return "up" message', () => { + expect(locationController.health()).toEqual({ message: 'up' }); + }); }); - it('should call getCentroid method with correct parameters', () => { - const result = controller.getCentroid('DISTRICT', 'lucknow'); - expect(result).toEqual({ - status: 'success', - state: 'UTTAR PRADESH', - district: 'Lucknow', - subDistrict: '', - city: '', - block: '', - village: '', - lat: 26.830190863213858, - lon: 80.89119983155268, + describe('getCentroid', () => { + it('should throw BAD_REQUEST if query is missing', async () => { + await expect( + locationController.getCentroid('LEVEL1', ''), + ).rejects.toThrow(HttpException); + await expect( + locationController.getCentroid('LEVEL1', ''), + ).rejects.toThrow(`No LEVEL1 query found`); }); - }); - it('should handle missing query parameter in fuzzySearch', () => { - expect(() => controller.fuzzySearch('state', { query: null })).toThrow( - HttpException, - ); - }); + it('should return centroid data when query is valid', async () => { + const mockCentroidResponse = { + properties: { name: 'Location' }, + latitude: 12.9716, + longitude: 77.5946, + }; - it('should handle unsupported GeoLocation Level in fuzzySearch', () => { - expect(() => - controller.fuzzySearch('country', { query: 'testQuery' }), - ).toThrow(HttpException); - }); + jest + .spyOn(locationService, 'getCentroid') + .mockResolvedValueOnce(mockCentroidResponse); + + const result = await locationController.getCentroid('LEVEL1', 'query'); + expect(result).toEqual({ + "block": "", + "city": "", + "district": "", + "lat": 12.9716, + "lon": 77.5946, + "state": "", + "status": "success", + "subDistrict": "", + "village": "", + }); + }); - it('should call fuzzySearch method with correct parameters', () => { - const result = controller.fuzzySearch('state', { query: 'testQuery' }); - expect(result).toEqual({ result: 'mocked result' }); + it('should throw NOT_FOUND if location service throws an error', async () => { + jest + .spyOn(locationService, 'getCentroid') + .mockRejectedValueOnce(new Error('NotFoundError')); + + await expect( + locationController.getCentroid('LEVEL1', 'query'), + ).rejects.toThrow(HttpException); + await expect( + locationController.getCentroid('LEVEL1', 'query'), + ).rejects.toThrow('TypeError'); + }); }); + }); diff --git a/src/modules/location/location.controller.ts b/src/modules/location/location.controller.ts index ec133c2..c178274 100644 --- a/src/modules/location/location.controller.ts +++ b/src/modules/location/location.controller.ts @@ -1,19 +1,10 @@ -import { - Controller, - Get, - Post, - Body, - Param, - Query, - HttpException, - HttpStatus, - Logger, -} from '@nestjs/common'; +import { Body, Controller, Get, HttpException, HttpStatus, Logger, Param, Post, Query } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; import { ApiTags } from '@nestjs/swagger'; import { formatCentroidResponse } from '../../utils/serializer/success'; import { LocationSearchService } from './location.search-service'; import { LocationService } from './location.service'; + @ApiTags('/location') @Controller('location') export class LocationController { @@ -27,15 +18,20 @@ export class LocationController { private readonly locationService: LocationService, ) { this.geoLocationLevels = this.configService.get<{ [key: string]: any }>( - 'geoLocationLevels', + 'tableMeta', ); this.levelsMapping = this.configService.get<{ [key: string]: any }>( 'levelsMapping', ); } + @Get() + health() { + return {"message": "up"} + } + @Get(':locationlevel/centroid') - getCentroid( + async getCentroid( @Param('locationlevel') locationLevel: string, @Query('query') query: string, ) { @@ -47,7 +43,8 @@ export class LocationController { ); } try { - const response = this.locationService.getCentroid(locationLevel, query); + const response = await this.locationService.getCentroid(locationLevel, query); + this.logger.log(response); return formatCentroidResponse( response.properties, response.latitude, @@ -62,9 +59,9 @@ export class LocationController { } @Post(':locationlevel/fuzzysearch') - fuzzySearch( + async fuzzySearch( @Param('locationlevel') locationLevel: string, - @Body() body: any, + @Body() body: any, // ) { try { if ( @@ -87,50 +84,14 @@ export class LocationController { } const filter = body.filter || {}; - const filterArray = []; - for (const filterKey of Object.keys(filter)) { - if ( - !Object.keys(this.geoLocationLevels).includes(filterKey.toUpperCase()) - ) { - throw new HttpException( - `Unsupported GeoLocation Level Filter: ${filterKey}`, - HttpStatus.BAD_REQUEST, - ); - } - filterArray.push({ - level: this.levelsMapping[filterKey.toUpperCase()], - query: filter[filterKey], - }); - } - - let searchLevel; - switch (locationLevel.toUpperCase()) { - case 'STATE': - searchLevel = this.levelsMapping.STATE; - break; - case 'DISTRICT': - searchLevel = this.levelsMapping.DISTRICT; - break; - case 'SUBDISTRICT': - searchLevel = this.levelsMapping.SUBDISTRICT; - break; - case 'VILLAGE': - searchLevel = this.levelsMapping.VILLAGE; - break; - default: - throw new HttpException( - 'Invalid location level', - HttpStatus.INTERNAL_SERVER_ERROR, - ); - } - const queryResponse = this.locationSearchService.fuzzySearch( - searchLevel, + return this.locationSearchService.fuzzySearch( + locationLevel, query, - filterArray, + filter, ); - return queryResponse; } catch (error) { + this.logger.error(error) throw new HttpException(error.name, HttpStatus.INTERNAL_SERVER_ERROR); } } diff --git a/src/modules/location/location.module.ts b/src/modules/location/location.module.ts index a1c2e1c..49b605d 100644 --- a/src/modules/location/location.module.ts +++ b/src/modules/location/location.module.ts @@ -2,23 +2,14 @@ import { Module } from '@nestjs/common'; import { LocationController } from './location.controller'; import { LocationService } from './location.service'; import { LocationSearchService } from './location.search-service'; -import { GeojsonService } from '../../services/geojson/geojson.service'; import * as path from 'path'; +import { GeoqueryService } from '../../services/geoquery/geoquery.service'; @Module({ controllers: [LocationController], providers: [ - { - useFactory: () => { - const filePath = path.join( - process.cwd(), - './src/geojson-data/PARSED_MASTER_LOCATION_NAMES.json', - ); - return new LocationSearchService(filePath); - }, - provide: LocationSearchService, - }, - GeojsonService, + LocationSearchService, + GeoqueryService, LocationService, ], }) diff --git a/src/modules/location/location.search-service.spec.ts b/src/modules/location/location.search-service.spec.ts deleted file mode 100644 index 7820610..0000000 --- a/src/modules/location/location.search-service.spec.ts +++ /dev/null @@ -1,124 +0,0 @@ -import * as fs from 'fs'; -import { Level, LocationSearchService } from './location.search-service'; - -jest.mock('fs'); - -describe('LocationSearchService', () => { - let locationSearchService: LocationSearchService; - - const mockData = JSON.stringify([ - { - state: 'State1', - districts: [ - { - district: 'District1', - subDistricts: [ - { - subDistrict: 'SubDistrict1', - villages: ['Village1', 'Village2'], - }, - ], - }, - ], - }, - { - state: 'State2', - districts: [ - { - district: 'District2', - subDistricts: [ - { - subDistrict: 'SubDistrict2', - villages: ['Village3', 'Village4'], - }, - ], - }, - ], - }, - ]); - - beforeEach(() => { - (fs.readFileSync as jest.Mock).mockReturnValue(mockData); - locationSearchService = new LocationSearchService('mockFilePath'); - }); - - it('should be defined', () => { - expect(locationSearchService).toBeDefined(); - }); - - it('should preprocess data correctly', () => { - expect(locationSearchService['villagePreprocessedData']).toHaveLength(4); - expect(locationSearchService['subDistrictPreprocessedData']).toHaveLength( - 2, - ); - expect(locationSearchService['districtPreprocessedData']).toHaveLength(2); - expect(locationSearchService['statePreProcessedData']).toHaveLength(2); - }); - - it('should return correct results for state level search', () => { - const result = locationSearchService.search(Level.STATE, 'State1', null); - expect(result).toEqual([{ state: 'State1' }]); - }); - - it('should return correct results for district level search', () => { - const result = locationSearchService.search( - Level.DISTRICT, - 'District1', - null, - ); - expect(result).toEqual([{ state: 'State1', district: 'District1' }]); - }); - - it('should return correct results for sub-district level search', () => { - const result = locationSearchService.search( - Level.SUBDISTRICT, - 'SubDistrict1', - null, - ); - expect(result).toEqual([ - { state: 'State1', district: 'District1', subDistrict: 'SubDistrict1' }, - ]); - }); - - it('should return correct results for village level search', () => { - const result = locationSearchService.search( - Level.VILLAGE, - 'Village1', - null, - ); - expect(result).toEqual([ - { - state: 'State1', - district: 'District1', - subDistrict: 'SubDistrict1', - village: 'Village1', - }, - ]); - }); - - it('should apply filters correctly', () => { - const filters = [{ level: Level.STATE, query: 'State1' }]; - const result = locationSearchService.search( - Level.VILLAGE, - 'Village1', - filters, - ); - expect(result).toEqual([ - { - state: 'State1', - district: 'District1', - subDistrict: 'SubDistrict1', - village: 'Village1', - }, - ]); - }); - - it('should return no results if no matches are found', () => { - const result = locationSearchService.search( - Level.VILLAGE, - 'NonExistentVillage', - null, - ); - expect(result).toEqual([]); - }); -}); diff --git a/src/modules/location/location.search-service.ts b/src/modules/location/location.search-service.ts index 1d61206..213125f 100644 --- a/src/modules/location/location.search-service.ts +++ b/src/modules/location/location.search-service.ts @@ -1,150 +1,56 @@ import { Injectable, Logger } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; +import { GeoqueryService } from '../../services/geoquery/geoquery.service'; -const Fuse = require('fuse.js'); -import * as fs from 'fs'; - -const logger = new Logger('ls-st'); - -type LevelType = { - name: string; - path: string; - depth: number; -}; - -export const Level = Object.freeze({ - STATE: { - name: 'state', - path: 'state', - depth: 0, - } as LevelType, - DISTRICT: { - name: 'district', - path: 'state->district', - depth: 1, - } as LevelType, - SUBDISTRICT: { - name: 'subDistrict', - path: 'state->district->subDistrict', - depth: 2, - } as LevelType, - VILLAGE: { - name: 'village', - path: 'state->district->subDistrict->village', - depth: 3, - } as LevelType, -}); - -type LevelKeys = keyof typeof Level; -type Level = (typeof Level)[LevelKeys]; @Injectable() export class LocationSearchService { - private villagePreprocessedData: any[]; - private subDistrictPreprocessedData: any[]; - private districtPreprocessedData: any[]; - private statePreProcessedData: any[]; + logger = new Logger(LocationSearchService.name); - constructor(filePath: string) { - this.villagePreprocessedData = []; - this.subDistrictPreprocessedData = []; - this.districtPreprocessedData = []; - this.statePreProcessedData = []; + constructor(private readonly config: ConfigService, private readonly geoquery: GeoqueryService) { - const jsonData = JSON.parse(fs.readFileSync(filePath, 'utf-8')); - - jsonData.forEach((stateData: any) => { - stateData.districts.forEach((districtData: any) => { - districtData.subDistricts.forEach((subDistrictData: any) => { - subDistrictData.villages.forEach((village: any) => { - if (village !== null) { - this.villagePreprocessedData.push({ - state: stateData.state, - district: districtData.district, - subDistrict: subDistrictData.subDistrict, - village, - }); - } - }); - this.subDistrictPreprocessedData.push({ - state: stateData.state, - district: districtData.district, - subDistrict: subDistrictData.subDistrict, - }); - }); - this.districtPreprocessedData.push({ - state: stateData.state, - district: districtData.district, - }); - }); - this.statePreProcessedData.push({ - state: stateData.state, - }); - }); } - fuzzySearch(level: Level, query: string, filters: any[] | null): any[] { + fuzzySearch(level: string, query: string, filters) { return this.querySearch(level, query, 0.1, 0, filters); } - search(level: Level, query: string, filters: any[] | null): any[] { + search(level: string, query: string, filters) { return this.querySearch(level, query, 0.0, 0, filters); } - private querySearch( - searchLevel: Level | any, + private async querySearch( + searchLevel: string, query: string, threshold: number, distance: number = 0, - filters: any[] | null, - ): any[] { - const options = { - keys: [searchLevel.name], - threshold, - distance, - isCaseSensitive: false, - }; - let processedData: any[]; + filters, + ) { + const { state_name, district_name, subdistrict_name } = filters; - switch (searchLevel.name) { - case Level.STATE.name: - processedData = this.statePreProcessedData; - break; - case Level.DISTRICT.name: - processedData = this.districtPreprocessedData; + let result; + + switch (searchLevel.toLowerCase()) { + case 'state': break; - case Level.SUBDISTRICT.name: - processedData = this.subDistrictPreprocessedData; + case 'district': + result = await this.geoquery.fuzzyDistrictSearch(query, { state_name }); break; - case Level.VILLAGE.name: - processedData = this.villagePreprocessedData; + case 'subdistrict': + result = await this.geoquery.fuzzySubDistrictSearch(query, { state_name, district_name }); break; - default: - processedData = []; + case 'village': + result = await this.geoquery.fuzzyVillageSearch(query, { state_name, district_name, subdistrict_name }); break; } - - if (filters !== null) { - for (let nodeDepth = 0; nodeDepth < searchLevel.depth; nodeDepth++) { - for (const filter of filters) { - if (filter.level.depth !== nodeDepth) continue; - const filteredData = []; - for (let index = 0; index < processedData.length; index++) { - if ( - processedData[index][`${filter.level.name}`] - .toLowerCase() - .includes(filter.query.toLowerCase()) - ) { - filteredData.push(processedData[index]); - } - } - processedData = filteredData; - } - } - } - - const fuse = new Fuse(processedData, options); - const result = fuse.search(query); - - return result.map((entry) => ({ ...entry.item })); + this.logger.log(result); + return { + matches: result.map((item: any) => ({ + state: item.state_name || state_name || null, + district: item.district_name || district_name || null, + subDistrict: item.subdistrict_name || subdistrict_name || null, + village: item.village_name || null, + })), + }; } } diff --git a/src/modules/location/location.service.spec.ts b/src/modules/location/location.service.spec.ts index c9592c4..d6754ce 100644 --- a/src/modules/location/location.service.spec.ts +++ b/src/modules/location/location.service.spec.ts @@ -1,17 +1,13 @@ import { Test, TestingModule } from '@nestjs/testing'; import { ConfigService } from '@nestjs/config'; -import { Logger } from '@nestjs/common'; import { LocationService } from './location.service'; -import { GeojsonService } from '../../services/geojson/geojson.service'; -import * as turf from '@turf/turf'; - -jest.mock('@nestjs/common/services/logger.service'); -jest.mock('@turf/turf'); +import { GeoqueryService } from '../../services/geoquery/geoquery.service'; +import { Logger } from '@nestjs/common'; describe('LocationService', () => { - let service: LocationService; + let locationService: LocationService; let configService: ConfigService; - let geojsonService: GeojsonService; + let geoQueryService: GeoqueryService; beforeEach(async () => { const module: TestingModule = await Test.createTestingModule({ @@ -20,75 +16,78 @@ describe('LocationService', () => { { provide: ConfigService, useValue: { - get: jest.fn().mockReturnValue('testCountry'), + get: jest.fn(), // Mock method }, }, { - provide: GeojsonService, + provide: GeoqueryService, useValue: { - getGeoJsonFiles: jest.fn().mockReturnValue({ - testCountry_state: { - features: [ - { - properties: { levelLocationName: 'testState' }, - geometry: { - type: 'Polygon', - coordinates: [ - [ - [0, 0], - [1, 0], - [1, 1], - [0, 1], - [0, 0], - ], - ], - }, - }, - ], - }, - }), + geometryCentroid: jest.fn(), // Mock method }, }, + Logger, // NestJS logger service ], }).compile(); - service = module.get(LocationService); + locationService = module.get(LocationService); configService = module.get(ConfigService); - geojsonService = module.get(GeojsonService); + geoQueryService = module.get(GeoqueryService); }); it('should be defined', () => { - expect(service).toBeDefined(); + expect(locationService).toBeDefined(); }); - it('should return the correct centroid for a given location', () => { - const centroidMock = { geometry: { coordinates: [0.5, 0.5] } }; - (turf.centroid as jest.Mock).mockReturnValue(centroidMock); + describe('getCentroid', () => { + it('should return centroid data for valid locationLevel and query', async () => { + const locationLevel = 'LEVEL1'; + const query = 'some query'; + + // Mock ConfigService.get to return a value for the tableMeta + jest.spyOn(configService, 'get').mockReturnValue('mockTableMeta'); + + // Mock GeoqueryService.geometryCentroid to return valid centroid data + const mockResponse = [ + { + coordinate: JSON.stringify({ + coordinates: [77.5946, 12.9716], // Longitude, Latitude + }), + name: 'Sample Location', + }, + ]; + jest + .spyOn(geoQueryService, 'geometryCentroid') + .mockResolvedValueOnce(mockResponse); - const result = service.getCentroid('state', 'testState'); + const result = await locationService.getCentroid(locationLevel, query); - expect(result).toEqual({ - properties: { levelLocationName: 'testState' }, - latitude: 0.5, - longitude: 0.5, + expect(result).toEqual({ + properties: mockResponse[0], + latitude: 12.9716, + longitude: 77.5946, + }); + expect(configService.get).toHaveBeenCalledWith(`tableMeta.${locationLevel}`); + expect(geoQueryService.geometryCentroid).toHaveBeenCalledWith('mockTableMeta', query); }); - expect(turf.centroid).toHaveBeenCalledWith( - turf.polygon([ - [ - [0, 0], - [1, 0], - [1, 1], - [0, 1], - [0, 0], - ], - ]), - ); - }); + it('should throw an error when geoQueryService fails', async () => { + const locationLevel = 'LEVEL1'; + const query = 'some query'; + + // Mock ConfigService.get to return a value for the tableMeta + jest.spyOn(configService, 'get').mockReturnValue('mockTableMeta'); - it('should throw an error if the location is not found', () => { - expect(() => service.getCentroid('state', 'invalidState')).toThrowError( - 'No state found with name: invalidState', - ); + // Mock GeoqueryService.geometryCentroid to throw an error + jest + .spyOn(geoQueryService, 'geometryCentroid') + .mockRejectedValueOnce(new Error('Service Error')); + + await expect( + locationService.getCentroid(locationLevel, query), + ).rejects.toThrow('Service Error'); + + expect(configService.get).toHaveBeenCalledWith(`tableMeta.${locationLevel}`); + expect(geoQueryService.geometryCentroid).toHaveBeenCalledWith('mockTableMeta', query); + }); }); }); diff --git a/src/modules/location/location.service.ts b/src/modules/location/location.service.ts index 38d578b..e034ee0 100644 --- a/src/modules/location/location.service.ts +++ b/src/modules/location/location.service.ts @@ -1,56 +1,28 @@ import { Injectable, Logger } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; -import * as turf from '@turf/turf'; -import { GeojsonService } from '../../services/geojson/geojson.service'; +import { GeoqueryService } from '../../services/geoquery/geoquery.service'; @Injectable() export class LocationService { private readonly logger = new Logger(LocationService.name); - private readonly geoJsonFiles: { [key: string]: any }; private readonly country: string; constructor( private readonly configService: ConfigService, - private readonly geoJsonService: GeojsonService, + private readonly geoQueryService: GeoqueryService, ) { - this.geoJsonFiles = this.geoJsonService.getGeoJsonFiles(); - this.country = this.configService.get('country'); } - getCentroid(locationLevel: string, query: string) { + async getCentroid(locationLevel: string, query: string) { try { - let queryFeature; - for (const feature of this.geoJsonFiles[ - `${this.country}_${locationLevel}` - ].features) { - if ( - feature.properties.levelLocationName.toLowerCase() === - query.toLowerCase() - ) { - queryFeature = feature; - break; - } - } - - if (!queryFeature) { - throw new Error(`No ${locationLevel} found with name: ${query}`); - } - - let polygonFeature; - if (queryFeature.geometry.type === 'Polygon') { - polygonFeature = turf.polygon(queryFeature.geometry.coordinates); - } else { - polygonFeature = turf.multiPolygon(queryFeature.geometry.coordinates); - } - - const centroid = turf.centroid(polygonFeature); - const longitude = centroid.geometry.coordinates[0]; - const latitude = centroid.geometry.coordinates[1]; - + const tableMeta = this.configService.get(`tableMeta.${locationLevel}`); + let resp: any = await this.geoQueryService.geometryCentroid(tableMeta, query); + resp = resp[0]; + const { coordinates } = JSON.parse(resp.coordinate); this.logger.log( - `Centroid Success Response: ${JSON.stringify(queryFeature.properties)}`, + `Centroid Success Response: ${resp}`, ); - return { properties: queryFeature.properties, latitude, longitude }; + return { properties: resp, latitude: coordinates[1], longitude: coordinates[0] }; } catch (error) { throw error; } diff --git a/src/modules/prisma/prisma.module.ts b/src/modules/prisma/prisma.module.ts new file mode 100644 index 0000000..d80c9f3 --- /dev/null +++ b/src/modules/prisma/prisma.module.ts @@ -0,0 +1,9 @@ +import { Global, Module } from '@nestjs/common'; +import { PrismaService } from './prisma.service'; + +@Global() +@Module({ + providers: [PrismaService], + exports: [PrismaService] +}) +export class PrismaModule {} diff --git a/src/modules/prisma/prisma.service.ts b/src/modules/prisma/prisma.service.ts new file mode 100644 index 0000000..359f950 --- /dev/null +++ b/src/modules/prisma/prisma.service.ts @@ -0,0 +1,9 @@ +import { Injectable, OnModuleInit } from '@nestjs/common'; +import { PrismaClient } from '@prisma/client'; + +@Injectable() +export class PrismaService extends PrismaClient implements OnModuleInit { + async onModuleInit() { + await this.$connect(); + } +} diff --git a/src/scripts/ingestors/district.geojson.ts b/src/scripts/ingestors/district.geojson.ts new file mode 100644 index 0000000..584c7a5 --- /dev/null +++ b/src/scripts/ingestors/district.geojson.ts @@ -0,0 +1,70 @@ +import { PrismaClient } from '@prisma/client'; +import * as fs from 'fs'; +import { executeDistrictCreateQuery, executeStateCreateQuery, findState } from './service.geojson'; + +const prisma = new PrismaClient(); +const districtGeoJSONLocation = `${__dirname}/../../geojson-data/INDIA_DISTRICT.geojson`; + +const insertDistrictData = async () => { + const rawData = fs.readFileSync(districtGeoJSONLocation); + const geojson = JSON.parse(rawData.toString()); + + for (const feature of geojson.features) { + const properties = feature.properties; + const geoJsonData = JSON.stringify(feature.geometry); + let state; + + // Find the stateId based on the fuzzy state name (stname) + state = await findState(properties.stname); + state = state[0]; + if (!state) { + console.error(`State not found for district: ${properties.dtname}`); + + const newState = { + STCODE11: properties.stcode11, + STNAME: properties.stname, + levelLocationName: properties.stname, + STNAME_SH: properties.stname, + Shape_Length: 0, + Shape_Area: 0, + State_LGD: properties.State_LGD, + MaxSimpTol: 0, + MinSimpTol: 0, + metadata: JSON.stringify({ createdBy: 'insertDistrictData script' }), + }; + + await executeStateCreateQuery(newState, `{ + "type": "GeometryCollection", + "geometries": [] + }`, + ); + state = await findState(properties.stname); + state = state[0]; + console.log(`Created new state: ${properties.stname}`); + } + + try { + console.log(`INGESTING: ${properties.dtname}`, state); + await executeDistrictCreateQuery(properties, geoJsonData, state); + console.log(`Ingested: ${properties.dtname}`); + } catch (error) { + if (error.meta && error.meta.code === '23505') { + console.log(error); + console.log(`District already exists: ${properties.dtname}. Skipping...`); + } else { + console.log(state); + console.error(`Error inserting District data for ${properties.dtname}:`, error); + } + } + } + console.log('District data ingestion completed!'); +}; + +insertDistrictData() + .then(async () => { + await prisma.$disconnect(); + }) + .catch(async (e) => { + console.error('Error during district data ingestion:', e); + await prisma.$disconnect(); + }); diff --git a/src/scripts/ingestors/service.geojson.ts b/src/scripts/ingestors/service.geojson.ts new file mode 100644 index 0000000..4092f47 --- /dev/null +++ b/src/scripts/ingestors/service.geojson.ts @@ -0,0 +1,132 @@ +import { PrismaClient } from '@prisma/client'; + +const prisma = new PrismaClient(); + +export async function executeStateCreateQuery(properties, geoJsonData) { + const query = ` + INSERT INTO "State" ("state_code", + "state_name", + "metadata", + "geometry") + VALUES ('${properties.STCODE11}', + '${properties.STNAME}', + jsonb_build_object( + 'levelLocationName', '${properties.levelLocationName}', + 'stname_sh', '${properties.STNAME_SH}', + 'shape_length', ${properties.Shape_Length}, + 'shape_area', ${properties.Shape_Area}, + 'state_lgd', ${properties.State_LGD}, + 'max_simp_tol', ${properties.MaxSimpTol}, + 'min_simp_tol', ${properties.MinSimpTol} + ), + ST_SetSRID(ST_GeomFromGeoJSON('${geoJsonData}'), 4326)); + `; + + await prisma.$executeRawUnsafe(query); +} + + +export async function findState(state_name) { + return prisma.$queryRawUnsafe(` + WITH input AS (SELECT '${state_name}'::VARCHAR AS input_name) + SELECT st.state_name, st.state_code, levenshtein(i.input_name, st.state_name) as levenshtein + FROM "State" st, + input i + WHERE levenshtein(i.input_name, st.state_name) <= 5 + ORDER BY levenshtein LIMIT 1; + `); +} + + +export async function executeDistrictCreateQuery(properties, geoJsonData, state) { + const query = ` + INSERT INTO "District" ("district_code", + "district_name", + "metadata", + "geometry", + "state_id") + VALUES ('${properties.dtcode11}', + '${properties.dtname}', + jsonb_build_object( + 'levelLocationName', '${properties.levelLocationName}', + 'year_stat', '${properties.year_stat}', + 'shape_length', ${properties.SHAPE_Length}, + 'shape_area', ${properties.SHAPE_Area}, + 'dist_lgd', ${properties.Dist_LGD} + ), + ST_SetSRID(ST_GeomFromGeoJSON('${geoJsonData}'), 4326), + ${state.state_code}); + `; + await prisma.$executeRawUnsafe(query); +} + + +export async function findDistrict(district_name) { + return prisma.$queryRawUnsafe(` + WITH input AS (SELECT '${district_name}'::VARCHAR AS input_name) + SELECT dt.district_name, dt.district_code, levenshtein(i.input_name, dt.district_name) as levenshtein + FROM "District" dt, + input i + WHERE levenshtein(i.input_name, dt.district_name) <= 5 + OR i.input_name ILIKE '%' || dt.district_name || '%' + OR dt.district_name ILIKE '%' || i.input_name || '%' + ORDER BY levenshtein LIMIT 1; + `); +} + + +export async function executeSubDistrictCreateQuery(properties, geoJsonData, state, district) { + const query = ` + INSERT INTO "SubDistrict" ("subdistrict_code", + "subdistrict_name", + "metadata", + "geometry", + "state_id", + "district_id") + VALUES ('${properties.sdtcode11}', + '${properties.sdtname}', + jsonb_build_object( + 'levelLocationName', '${properties.levelLocationName}', + 'Shape_Length', ${properties.Shape_Length}, + 'Shape_Area', ${properties.Shape_Area}, + 'Subdt_LGD', ${properties.Subdt_LGD} + ), + ST_SetSRID(ST_GeomFromGeoJSON('${geoJsonData}'), 4326), + ${state.state_code}, + ${district.district_code}); + `; + await prisma.$executeRawUnsafe(query); +} + + +export async function findSubDistrict(subdistrict_name) { + return prisma.$queryRawUnsafe(` + WITH input AS (SELECT '${subdistrict_name}'::VARCHAR AS input_name) + SELECT dt.subdistrict_name, dt.subdistrict_code, levenshtein(i.input_name, dt.subdistrict_name) as levenshtein + FROM "SubDistrict" dt, + input i + WHERE levenshtein(i.input_name, dt.subdistrict_name) <= 5 + ORDER BY levenshtein LIMIT 1; + `); +} + + +export async function executeVillageCreateQuery(properties, geoJsonData, state, district, subDistrict) { + const query = ` + INSERT INTO "Village" ( + "village_name", + "metadata", + "geometry", + "state_id", + "district_id", + "subdistrict_id") + VALUES ( + '${properties.NAME}', + jsonb_build_object(), + ST_SetSRID(ST_GeomFromGeoJSON('${geoJsonData}'), 4326), + ${state.state_code}, + ${district.district_code}, + ${subDistrict.subdistrict_code}); + `; + await prisma.$executeRawUnsafe(query); +} diff --git a/src/scripts/ingestors/state.geojson.ts b/src/scripts/ingestors/state.geojson.ts new file mode 100644 index 0000000..af9dc8c --- /dev/null +++ b/src/scripts/ingestors/state.geojson.ts @@ -0,0 +1,41 @@ +import { PrismaClient } from '@prisma/client'; +import * as fs from 'fs'; +import { executeStateCreateQuery } from './service.geojson'; + +const prisma = new PrismaClient(); +const subDistrictGeoJSONLocation = `${__dirname}/../../geojson-data/INDIA_STATE.geojson`; + + +const insertStateData = async () => { + const rawData = fs.readFileSync(subDistrictGeoJSONLocation); + const geojson = JSON.parse(rawData.toString()); + + for (const feature of geojson.features) { + const properties = feature.properties; + const geoJsonData = JSON.stringify(feature.geometry); + console.log(`Ingesting: ${properties.stname}`); + try { + await executeStateCreateQuery(properties, geoJsonData); + console.log(`Ingested: ${properties.stname} !`); + } catch (error) { + if (error.meta && error.meta.code === '23505') { + console.log(`State already exists: ${properties.stname}. Skipping...`); + } else { + console.error(`Error inserting state data for ${properties.stname}:`, error); + } + } + console.log(`Ingeted: ${properties.stname} !`); + } + + console.log('State data added successfully!'); +}; + +insertStateData() + .then(async () => { + + await prisma.$disconnect(); + }) + .catch(async (e) => { + console.error('Error inserting state data:', e); + await prisma.$disconnect(); + }); diff --git a/src/scripts/ingestors/subdistrict.geojson.ts b/src/scripts/ingestors/subdistrict.geojson.ts new file mode 100644 index 0000000..5512f4d --- /dev/null +++ b/src/scripts/ingestors/subdistrict.geojson.ts @@ -0,0 +1,118 @@ +import { PrismaClient } from '@prisma/client'; +import * as fs from 'fs'; +import { + executeDistrictCreateQuery, + executeStateCreateQuery, + findState, + findDistrict, + executeSubDistrictCreateQuery, +} from './service.geojson'; + +const prisma = new PrismaClient(); +const subDistrictGeoJSONLocation = `${__dirname}/../../geojson-data/INDIA_SUBDISTRICT.geojson`; + + + +const insertSubDistrictData = async () => { + const rawData = fs.readFileSync(subDistrictGeoJSONLocation); + const geojson = JSON.parse(rawData.toString()); + + for (const feature of geojson.features) { + const properties = feature.properties; + const geoJsonData = JSON.stringify(feature.geometry); + console.log(`Ingesting: ${properties.sdtname}`, properties.stname, properties.dtname); + + // Find or create the stateId based on the state code (stcode11) and state name (stname) + let state = await findState(properties.stname); + state = state[0]; + + if (!state) { + // Create the state if it does not exist + const newState = { + STCODE11: properties.stcode11, + STNAME: properties.stname, + levelLocationName: properties.stname, + STNAME_SH: properties.stname, + Shape_Length: 0, + Shape_Area: 0, + State_LGD: 0, + MaxSimpTol: 0, + MinSimpTol: 0, + metadata: JSON.stringify({ createdBy: 'insertSubDistrictData script' }), + }; + + try { + await executeStateCreateQuery(newState, `{ + "type": "GeometryCollection", + "geometries": [] + }`); + } catch (e) { + continue; + } + state = await findState(properties.stname); + state = state[0]; + console.log(`Created new state: ${properties.stname}`); + } + + // Find or create the districtId based on the district code (dtcode11) and district name (dtname) + let district = await findDistrict(properties.dtname); + district = district[0]; + + if (!district) { + console.log(properties.dtname); + // Create the district if it does not exist + // @ts-ignore + // @ts-ignore + const newDistrict = { + dtcode11: properties.dtcode11, + dtname: properties.dtname, + levelLocationName: properties.dtname, + SHAPE_Length: 0, + SHAPE_Area: 0, + Dist_LGD: 0, + metadata: JSON.stringify({ createdBy: 'insertSubDistrictData script' }), + + // @ts-ignore + stateId: state.stcode11, + }; + + try { + await executeDistrictCreateQuery(newDistrict, `{ + "type": "GeometryCollection", + "geometries": [] + }`, state); + } catch (e) { + continue; + } + district = await findDistrict(properties.dtname); + district = district[0]; + console.log(`Created new district: ${properties.dtname}`); + } + + + + // console.log(query); + + try { + await executeSubDistrictCreateQuery(properties, geoJsonData, state, district); + console.log(`Ingested: ${properties.sdtname}`); + } catch (error) { + if (error.meta && error.meta.code === '23505') { + console.log(`SubDistrict already exists: ${properties.dtname}. Skipping...`); + } else { + console.error(`Error inserting SubDistrict data for ${properties.dtname}:`, error); + } + } + } + + console.log('Subdistrict data ingestion completed!'); +}; + +insertSubDistrictData() + .then(async () => { + await prisma.$disconnect(); + }) + .catch(async (e) => { + console.error('Error during subdistrict data ingestion:', e); + await prisma.$disconnect(); + }); diff --git a/src/scripts/ingestors/village.geojson.ts b/src/scripts/ingestors/village.geojson.ts new file mode 100644 index 0000000..6b4b10e --- /dev/null +++ b/src/scripts/ingestors/village.geojson.ts @@ -0,0 +1,155 @@ +import { PrismaClient } from '@prisma/client'; +import * as fs from 'fs'; +import { + executeDistrictCreateQuery, + executeStateCreateQuery, + findState, + findDistrict, + executeVillageCreateQuery, + findSubDistrict, executeSubDistrictCreateQuery, +} from './service.geojson'; +import * as path from 'path'; + +const prisma = new PrismaClient(); +const villageMasterLocation = `${__dirname}/../../geojson-data/indian_village_boundaries`; + +const processGeojsonFile = async (filePath) => { + const rawData = fs.readFileSync(filePath); + const geojson = JSON.parse(rawData.toString()); + + for (const feature of geojson.features) { + const properties = feature.properties; + const geoJsonData = JSON.stringify(feature.geometry); + console.log(`Ingesting: ${properties.NAME}`, properties.STATE, properties.DISTRICT, properties.SUB_DIST); + + // Find or create the stateId based on the state code (stcode11) and state name (STATE) + let state = await findState(properties.STATE); + state = state[0]; + + if (!state) { + const newState = { + STNAME: properties.STATE, + levelLocationName: properties.STATE, + STNAME_SH: properties.STATE, + Shape_Length: 0, + Shape_Area: 0, + State_LGD: 0, + MaxSimpTol: 0, + MinSimpTol: 0, + metadata: JSON.stringify({ createdBy: 'insertVillageData script' }), + }; + + try { + await executeStateCreateQuery(newState, `{ + "type": "GeometryCollection", + "geometries": [] + }`); + } catch (e) { + continue; + } + state = await findState(properties.STATE); + state = state[0]; + console.log(`Created new state: ${properties.STATE}`); + } + + // Find or create the districtId based on the district code (dtcode11) and district name (DISTRICT) + let district = await findDistrict(properties.DISTRICT); + district = district[0]; + + if (!district) { + const newDistrict = { + DISTRICT: properties.DISTRICT, + levelLocationName: properties.DISTRICT, + SHAPE_Length: 0, + SHAPE_Area: 0, + Dist_LGD: 0, + metadata: JSON.stringify({ createdBy: 'insertVillageData script' }), + // @ts-ignore + stateId: state.stcode11, + }; + + try { + await executeDistrictCreateQuery(newDistrict, `{ + "type": "GeometryCollection", + "geometries": [] + }`, state); + } catch (e) { + continue; + } + district = await findDistrict(properties.DISTRICT); + district = district[0]; + console.log(`Created new district: ${properties.DISTRICT}`); + } + + // Find or create the subDistrictId based on the subdistrict code (sdtcode11) and subdistrict name (SUB_DIST) + let subDistrict = await findSubDistrict(properties.SUB_DIST); + subDistrict = subDistrict[0]; + + if (!subDistrict) { + const newSubDistrict = { + sdtname: properties.SUB_DIST, + levelLocationName: properties.SUB_DIST, + Shape_Length: 0, + Shape_Area: 0, + Subdt_LGD: 0, + + // @ts-ignore + stateId: state.stcode11, + // @ts-ignore + districtId: district.dtcode11, + }; + + try { + await executeSubDistrictCreateQuery(newSubDistrict, `{ + "type": "GeometryCollection", + "geometries": [] + }`, state, district); + } catch (e) { + continue; + } + subDistrict = await findSubDistrict(properties.SUB_DIST); + subDistrict = subDistrict[0]; + console.log(`Created new subdistrict: ${properties.SUB_DIST}`); + } + + try { + await executeVillageCreateQuery(properties, geoJsonData, state, district, subDistrict); + console.log(`Ingested: ${properties.NAME}`); + } catch (error) { + if (error.meta && error.meta.code === '23505') { + console.log(`Village already exists: ${properties.NAME}. Skipping...`); + } else { + console.error(`Error inserting Village data for ${properties.NAME}:`, error); + } + } + } + + console.log('Village data ingestion completed!'); +}; + +const insertVillageData = async (directoryPath) => { + const walk = async (dir) => { + const files = fs.readdirSync(dir); + + for (const file of files) { + const fullPath = path.join(dir, file); + + if (fs.statSync(fullPath).isDirectory()) { + await walk(fullPath); + } else if (path.extname(fullPath) === '.geojson') { + await processGeojsonFile(fullPath); + } + } + }; + + await walk(directoryPath); +}; + +insertVillageData(villageMasterLocation) + .then(async () => { + await prisma.$disconnect(); + }) + .catch(async (e) => { + console.error('Error during village data ingestion:', e); + await prisma.$disconnect(); + }); diff --git a/src/services/geojson/geojson.service.spec.ts b/src/services/geojson/geojson.service.spec.ts deleted file mode 100644 index 29b78b5..0000000 --- a/src/services/geojson/geojson.service.spec.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { Test, TestingModule } from '@nestjs/testing'; -import { GeojsonService } from './geojson.service'; -import { ConfigService } from '@nestjs/config'; - -describe('GeojsonService', () => { - let service: GeojsonService; - - beforeEach(async () => { - const module: TestingModule = await Test.createTestingModule({ - providers: [ - GeojsonService, - { - provide: ConfigService, - useValue: { - get: jest.fn().mockImplementation((key: string) => { - switch (key) { - case 'requiredGeoLocationLevels': - return ['SUBDISTRICT', 'DISTRICT', 'STATE']; - case 'country': - return 'INDIA'; - default: - return null; - } - }), - }, - }, - ], - }).compile(); - - service = module.get(GeojsonService); - }); - - it('should be defined', () => { - expect(service).toBeDefined(); - }); -}); diff --git a/src/services/geojson/geojson.service.ts b/src/services/geojson/geojson.service.ts deleted file mode 100644 index 8ac1ea4..0000000 --- a/src/services/geojson/geojson.service.ts +++ /dev/null @@ -1,52 +0,0 @@ -import { Injectable, Logger } from '@nestjs/common'; -import { ConfigService } from '@nestjs/config'; -import * as fs from 'fs'; -import * as path from 'path'; - -@Injectable() -export class GeojsonService { - private readonly logger = new Logger(GeojsonService.name); - private readonly geoJsonFilesPath: string; - private readonly requiredGeoLocationLevels: Array; - private readonly country: string; - private geoJsonFiles: { [key: string]: any } = {}; - - constructor(private configService: ConfigService) { - this.geoJsonFilesPath = path.join(process.cwd(), './src/geojson-data'); // Adjust the path as needed - this.requiredGeoLocationLevels = this.configService.get>( - 'requiredGeoLocationLevels', - ); - this.country = this.configService.get('country'); - this.loadGeoJsonFiles(); - } - - private loadGeoJsonFiles(): void { - try { - const files = fs.readdirSync(this.geoJsonFilesPath); - for (const locationLevel of this.requiredGeoLocationLevels) { - const geoJsonFileName = `${this.country}_${locationLevel}.geojson`; - const geoJsonKeyName = `${this.country}_${locationLevel}`; - if (!files.includes(geoJsonFileName)) { - this.logger.error( - `Required GeoJson file: ${geoJsonFileName} not present`, - ); - process.exit(); - } else { - this.geoJsonFiles[geoJsonKeyName] = JSON.parse( - fs.readFileSync( - `${this.geoJsonFilesPath}/${geoJsonFileName}`, - 'utf8', - ), - ); - this.logger.log(`Loaded GeoJson file: ${geoJsonFileName}`); - } - } - } catch (err) { - this.logger.error(`Error loading GeoJson files: ${err}`); - } - } - - getGeoJsonFiles(): { [key: string]: any } { - return this.geoJsonFiles; - } -} diff --git a/src/services/geoquery/geoquery.service.spec.ts b/src/services/geoquery/geoquery.service.spec.ts deleted file mode 100644 index 8efef09..0000000 --- a/src/services/geoquery/geoquery.service.spec.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { Test, TestingModule } from '@nestjs/testing'; -import { GeoqueryService } from './geoquery.service'; - -describe('GeoqueryService', () => { - let service: GeoqueryService; - - beforeEach(async () => { - const module: TestingModule = await Test.createTestingModule({ - providers: [GeoqueryService], - }).compile(); - - service = module.get(GeoqueryService); - }); - - it('should be defined', () => { - expect(service).toBeDefined(); - }); -}); diff --git a/src/services/geoquery/geoquery.service.ts b/src/services/geoquery/geoquery.service.ts index 083b5b8..88017c9 100644 --- a/src/services/geoquery/geoquery.service.ts +++ b/src/services/geoquery/geoquery.service.ts @@ -1,44 +1,176 @@ + + import { Injectable, Logger } from '@nestjs/common'; -import * as turf from '@turf/turf'; +import { PrismaService } from '../../modules/prisma/prisma.service'; @Injectable() export class GeoqueryService { private readonly logger = new Logger(GeoqueryService.name); - isPointInMultiPolygon(multiPolygon, point) { - this.logger.log(`Checking if point is in MultiPolygon`); - return multiPolygon.geometry.coordinates.some((polygonCoordinates) => { - const poly = turf.polygon(polygonCoordinates); - return turf.booleanContains(poly, point); - }); - } - - individualQuery( - country: string, - geoLocationLevel: string, - coordinates, - geoJsonFiles, - ) { - const pointToSearch = turf.point(coordinates); - for (const feature of geoJsonFiles[`${country}_${geoLocationLevel}`] - .features) { - if (feature.geometry.type === 'Polygon') { - this.logger.log(`Checking if point is in Polygon`); - const poly = turf.polygon( - feature.geometry.coordinates, - feature.properties, - ); - if (turf.booleanContains(poly, pointToSearch)) { - this.logger.log(`Point is in Polygon`); - return poly.properties; - } - } else if (feature.geometry.type === 'MultiPolygon') { - this.logger.log(`Checking if point is in MultiPolygon`); - if (this.isPointInMultiPolygon(feature, pointToSearch)) { - this.logger.log(`Point is in MultiPolygon`); - return feature.properties; - } - } + constructor(private readonly prisma: PrismaService) {} + + async queryStateContains(lat: number, lon: number) { + this.logger.log(`Querying state with lat: ${lat}, lon: ${lon}`); + + return this.prisma.$queryRawUnsafe(` + SELECT s.state_code AS state_code, + s.state_name AS state_name + FROM "State" s + WHERE ST_Contains(s.geometry, ST_SetSRID(ST_MakePoint(${lon}, ${lat}), 4326)); + `); + } + + async queryDistrictContains(lat: number, lon: number) { + this.logger.log(`Querying district with lat: ${lat}, lon: ${lon}`); + + return this.prisma.$queryRawUnsafe(` + SELECT s.state_code AS state_code, + s.state_name AS state_name, + d.district_code AS district_code, + d.district_name AS district_name + FROM "District" d + JOIN "State" s ON d.state_id = s.state_code + WHERE ST_Contains(d.geometry, ST_SetSRID(ST_MakePoint(${lon}, ${lat}), 4326)); + `); + } + + async querySubDistrictContains(lat: number, lon: number) { + this.logger.log(`Querying sub-district with lat: ${lat}, lon: ${lon}`); + + return this.prisma.$queryRawUnsafe(` + SELECT s.state_code AS state_code, + s.state_name AS state_name, + d.district_code AS district_code, + d.district_name AS district_name, + sd.subdistrict_code AS subdistrict_code, + sd.subdistrict_name AS subdistrict_name + FROM "SubDistrict" sd + JOIN "District" d ON sd.district_id = d.district_code + JOIN "State" s ON d.state_id = s.state_code + WHERE ST_Contains(sd.geometry, ST_SetSRID(ST_MakePoint(${lon}, ${lat}), 4326)); + `); + } + + async queryVillageContains(lat: number, lon: number) { + this.logger.log(`Querying village with lat: ${lat}, lon: ${lon}`); + + return this.prisma.$queryRawUnsafe(` + SELECT s.state_code AS state_code, + s.state_name AS state_name, + d.district_code AS district_code, + d.district_name AS district_name, + sd.subdistrict_code AS subdistrict_code, + sd.subdistrict_name AS subdistrict_name, + v.village_code AS village_code, + v.village_name AS village_name + FROM "Village" v + JOIN "SubDistrict" sd ON v.subdistrict_id = sd.subdistrict_code + JOIN "District" d ON sd.district_id = d.district_code + JOIN "State" s ON d.state_id = s.state_code + WHERE ST_Contains(v.geometry, ST_SetSRID(ST_MakePoint(${lon}, ${lat}), 4326)); + `); + } + + async geometryCentroid(tableMeta: { tname: string; fname: string }, fieldName: string) { + this.logger.log(`SELECT *, ST_AsGeoJson(ST_Centroid(geometry)) as coordinate + FROM "${tableMeta.tname}" + WHERE ${tableMeta.fname} ILIKE '${fieldName}';`); + return this.prisma.$queryRawUnsafe(` + SELECT ${tableMeta.fname}, ST_AsGeoJson(ST_Centroid(geometry)) as coordinate + FROM "${tableMeta.tname}" + WHERE ${tableMeta.fname} ILIKE '${fieldName}' LIMIT 1; + `); + } + + async fuzzyStateSearch(state_name: string) { + return this.prisma.$queryRawUnsafe(` + WITH input AS (SELECT '${state_name }'::VARCHAR AS input_name) + SELECT st.name AS state_name, st.code AS state_code, levenshtein(i.input_name, st.name) as levenshtein + FROM "State" st, + input i + WHERE levenshtein(i.input_name, st.name) <= 5 + ORDER BY levenshtein LIMIT 1; + `); + } + + async fuzzyDistrictSearch(district_name: string, filter: { state_name?: string }) { + const { state_name } = filter; + + let whereClause = ` + WHERE (levenshtein(i.input_name, dt.name) <= 5 + OR i.input_name ILIKE '%' || dt.name || '%' + OR dt.name ILIKE '%' || i.input_name || '%')`; + + if (state_name) { + whereClause += ` AND st.name ILIKE '${state_name}'`; + } + + return this.prisma.$queryRawUnsafe(` + WITH input AS (SELECT '${district_name}'::VARCHAR AS input_name) + SELECT dt.name AS district_name, dt.code AS district_code, levenshtein(i.input_name, dt.name) as levenshtein, st.name AS state_name + FROM "District" dt + JOIN "State" st ON dt.state_id = st.state_code, + input i ${whereClause} + ORDER BY levenshtein LIMIT 1; + `); + } + + async fuzzySubDistrictSearch(subdistrict_name: string, filter: { state_name?: string, district_name?: string }) { + const { state_name, district_name } = filter; + + let whereClause = ` + WHERE (levenshtein(i.input_name, sdt.name) <= 5 + OR i.input_name ILIKE '%' || sdt.name || '%' + OR sdt.name ILIKE '%' || i.input_name || '%')`; + + if (state_name) { + whereClause += ` AND st.name ILIKE '${state_name}'`; + } + + if (district_name) { + whereClause += ` AND dt.name ILIKE '${district_name}'`; + } + + return this.prisma.$queryRawUnsafe(` + WITH input AS (SELECT '${subdistrict_name}'::VARCHAR AS input_name) + SELECT sdt.name AS subdistrict_name, sdt.code AS subdistrict_code, levenshtein(i.input_name, sdt.name) as levenshtein, st.name AS state_name, dt.name AS district_name + FROM "SubDistrict" sdt + JOIN "District" dt ON sdt.district_id = dt.district_code + JOIN "State" st ON dt.state_id = st.state_code, + input i ${whereClause} + ORDER BY levenshtein LIMIT 1; + `); + } + + async fuzzyVillageSearch(village_name: string, filter: { state_name?: string, district_name?: string, subdistrict_name?: string }) { + const { state_name, district_name, subdistrict_name } = filter; + + let whereClause = ` + WHERE (levenshtein(i.input_name, v.village_name) <= 5 + OR i.input_name ILIKE '%' || v.village_name || '%' + OR v.village_name ILIKE '%' || i.input_name || '%')`; + + if (state_name) { + whereClause += ` AND st.name ILIKE '${state_name}'`; } + + if (district_name) { + whereClause += ` AND dt.name ILIKE '${district_name}'`; + } + + if (subdistrict_name) { + whereClause += ` AND sdt.name ILIKE '${subdistrict_name}'`; + } + + return this.prisma.$queryRawUnsafe(` + WITH input AS (SELECT '${village_name}'::VARCHAR AS input_name) + SELECT v.village_name AS village_name, v.village_code AS village_code, levenshtein(i.input_name, v.village_name) as levenshtein, st.name AS state_name, dt.name AS district_name, sdt.name AS subdistrict_name + FROM "Village" v + JOIN "SubDistrict" sdt ON v.subdistrict_id = sdt.subdistrict_code + JOIN "District" dt ON sdt.district_id = dt.district_code + JOIN "State" st ON dt.state_id = st.state_code, + input i ${whereClause} + ORDER BY levenshtein LIMIT 1; + `); } } diff --git a/src/utils/serializer/success.ts b/src/utils/serializer/success.ts index 3f52f86..6be7bf6 100644 --- a/src/utils/serializer/success.ts +++ b/src/utils/serializer/success.ts @@ -28,9 +28,9 @@ export const formatGeorevSuccessResponse = (data: any) => { logger.log(`GeoRev Success Response: ${JSON.stringify(data)}`); return { status: 'success', - state: data.stname ?? '', - district: data.dtname ?? '', - subDistrict: data.sdtname ?? '', + state: data.state_name ?? '', + district: data.district_name ?? '', + subDistrict: data.subdistrict_name ?? '', }; }; @@ -42,12 +42,12 @@ export const formatCentroidResponse = ( logger.log(`Centroid Success Response: ${JSON.stringify(data)}`); return { status: 'success', - state: data.stname ?? '', - district: data.dtname ?? '', - subDistrict: data.sdtname ?? '', + state: data.state_name ?? '', + district: data.district_name ?? '', + subDistrict: data.subdistrict_name ?? '', city: '', block: '', - village: '', + village: data.village_name ?? '', lat: latitude, lon: longitude, }; diff --git a/test/app.e2e-spec.ts b/test/app.e2e-spec.ts index e8a95b7..2b6520a 100644 --- a/test/app.e2e-spec.ts +++ b/test/app.e2e-spec.ts @@ -158,7 +158,7 @@ describe('AppController (e2e)', () => { expect(response.status).toBe(500); expect(response.body).toEqual({ status: 'fail', - error: 'coordinates must contain numbers', + error: 'Invalid latitude or longitude', }); }); @@ -169,14 +169,14 @@ describe('AppController (e2e)', () => { expect(response.status).toBe(200); expect(response.body).toEqual({ status: 'success', - state: 'UTTAR PRADESH', + state: '', district: 'Lucknow', subDistrict: '', city: '', block: '', village: '', - lat: 26.830190863213858, - lon: 80.89119983155268, + lat: 26.841984034, + lon: 80.905485485, }); }); @@ -186,4 +186,6 @@ describe('AppController (e2e)', () => { ); expect(response.status).toBe(404); }); + + }); diff --git a/test/jest-e2e.json b/test/jest-e2e.json index e9d912f..68080d3 100644 --- a/test/jest-e2e.json +++ b/test/jest-e2e.json @@ -5,5 +5,6 @@ "testRegex": ".e2e-spec.ts$", "transform": { "^.+\\.(t|j)s$": "ts-jest" - } + }, + "testTimeout": 10000 }