diff --git a/package-lock.json b/package-lock.json index 838d391..fcf7d0f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -54,7 +54,7 @@ "supertest": "^6.3.3", "ts-jest": "^29.1.0", "ts-loader": "^9.4.3", - "ts-node": "^10.9.1", + "ts-node": "^10.9.2", "tsconfig-paths": "^4.2.0", "typescript": "^5.1.3" } diff --git a/package.json b/package.json index 4656df1..568eeae 100644 --- a/package.json +++ b/package.json @@ -66,7 +66,7 @@ "supertest": "^6.3.3", "ts-jest": "^29.1.0", "ts-loader": "^9.4.3", - "ts-node": "^10.9.1", + "ts-node": "^10.9.2", "tsconfig-paths": "^4.2.0", "typescript": "^5.1.3" }, diff --git a/prisma/migrations/20241203015556_add_place/migration.sql b/prisma/migrations/20241203015556_add_place/migration.sql new file mode 100644 index 0000000..165b005 --- /dev/null +++ b/prisma/migrations/20241203015556_add_place/migration.sql @@ -0,0 +1,10 @@ +-- CreateTable +CREATE TABLE "Place" ( + "id" SERIAL NOT NULL, + "name" TEXT NOT NULL, + "type" TEXT NOT NULL, + "tag" TEXT NOT NULL, + "location" geometry(Point, 4326) NOT NULL, + + CONSTRAINT "Place_pkey" PRIMARY KEY ("id") +); diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 7e2e3ed..df535c8 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -74,3 +74,11 @@ model Village { state_id Int? state State? @relation(fields: [state_id], references: [state_code]) } + +model Place { + id Int @id @default(autoincrement()) + name String + type String + tag String + location Unsupported("geometry(Point, 4326)") +} diff --git a/src/app.module.ts b/src/app.module.ts index 9534cd0..7bb73b7 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -7,12 +7,14 @@ import { ConfigModule } from '@nestjs/config'; import { config } from './config/config'; import { AppController } from './app.controller'; import { AppService } from './app.service'; +import { PlaceModule } from './modules/place/place.module'; @Module({ imports: [ CityModule, GeorevModule, LocationModule, + PlaceModule, PrismaModule, ConfigModule.forRoot({ envFilePath: `.env`, diff --git a/src/main.ts b/src/main.ts index d36f4ea..b807fa0 100644 --- a/src/main.ts +++ b/src/main.ts @@ -24,7 +24,6 @@ async function bootstrap() { const configService = app.get(ConfigService); const port = configService.get('port'); const host = configService.get('host'); - const method = configService.get('method'); // Register plugins and middleware await app.register(multipart); @@ -46,7 +45,7 @@ async function bootstrap() { // Start the server await app.listen(port, host, (err, address) => { - logger.log(`Server running on ${method}://${host}:${port}`); + logger.log(`Server running on ${host}:${port}`); }); // Log additional information as needed diff --git a/src/modules/place/dto/place.dto.ts b/src/modules/place/dto/place.dto.ts new file mode 100644 index 0000000..83c445f --- /dev/null +++ b/src/modules/place/dto/place.dto.ts @@ -0,0 +1,36 @@ +import { IsString, IsNumber, IsLatitude, IsLongitude, ValidateIf, IsNotEmpty, IsArray, IsOptional } from 'class-validator'; + +export class CreatePlaceDto { + @IsString() + name: string; + + @IsString() + type: string; + + @IsString() + tag: string; + + @IsLatitude() + lat: number; + + @IsLongitude() + lon: number; +} + +export class SearchPlaceDto { + @IsArray() + @IsNotEmpty() + geofenceBoundary: number[][]; // Array of [lon, lat] pairs defining the geofence (polygon) + + @IsOptional() + @IsString() + name?: string; + + @IsOptional() + @IsString() + tag?: string; + + @IsOptional() + @IsString() + type?: string; + } \ No newline at end of file diff --git a/src/modules/place/place.controller.ts b/src/modules/place/place.controller.ts new file mode 100644 index 0000000..fb3edcf --- /dev/null +++ b/src/modules/place/place.controller.ts @@ -0,0 +1,20 @@ +import { Body, Controller, Get, Post } from "@nestjs/common"; +import { ApiTags } from "@nestjs/swagger"; +import { PlaceService } from "./place.service"; +import { CreatePlaceDto, SearchPlaceDto } from "./dto/place.dto"; + +@ApiTags('/place') +@Controller('place') +export class PlaceController { + constructor(private readonly placeService: PlaceService) {} + + @Post('search') + async searchPlace(@Body() searchPlaceDto: SearchPlaceDto) { + return this.placeService.searchPlaces(searchPlaceDto); + } + + @Post() + async addPlace(@Body() createPlaceDto: CreatePlaceDto) { + return this.placeService.createPlace(createPlaceDto); + } +} \ No newline at end of file diff --git a/src/modules/place/place.module.ts b/src/modules/place/place.module.ts new file mode 100644 index 0000000..b02e395 --- /dev/null +++ b/src/modules/place/place.module.ts @@ -0,0 +1,9 @@ +import { Module } from '@nestjs/common'; +import { PlaceController } from './place.controller'; +import { PlaceService } from './place.service'; + +@Module({ + controllers: [PlaceController], + providers: [PlaceService], +}) +export class PlaceModule {} diff --git a/src/modules/place/place.service.ts b/src/modules/place/place.service.ts new file mode 100644 index 0000000..229c728 --- /dev/null +++ b/src/modules/place/place.service.ts @@ -0,0 +1,52 @@ +import { Injectable, Logger } from "@nestjs/common"; +import { CreatePlaceDto, SearchPlaceDto } from "./dto/place.dto"; +import { PrismaService } from "../prisma/prisma.service"; + +@Injectable() +export class PlaceService { + private readonly logger = new Logger(PlaceService.name); + + constructor(private readonly prisma: PrismaService) { } + + async createPlace(createPlaceDto: CreatePlaceDto): Promise { + const { name, type, tag, lat, lon } = createPlaceDto; + const point = `ST_SetSRID(ST_MakePoint(${lon}, ${lat}), 4326)`; + this.logger.debug(`Adding place ${createPlaceDto}`) + return this.prisma.$executeRawUnsafe( + `INSERT INTO "Place" (name, type, tag, location) VALUES ($1, $2, $3, ${point})`, + name, + type, + tag, + ); + } + + async searchPlaces(searchPlaceDto: SearchPlaceDto): Promise { + const { geofenceBoundary, name, tag, type } = searchPlaceDto; + + // Create a polygon for the geofence + const polygon = `ST_MakePolygon(ST_GeomFromText('LINESTRING(${geofenceBoundary + .map((point) => point.join(' ')) + .join(', ')}, ${geofenceBoundary[0].join(' ')})', 4326))`; + + // Base query for filtering places + let query = ` + SELECT id, name, type, tag, ST_AsText(location::geometry) as location + FROM "Place" + WHERE ST_Within(location, ${polygon}) + `; + + // Add optional filters + if (name) { + query += ` AND name ILIKE '%${name}%'`; + } + else if (tag) { + query += ` AND tag ILIKE '%${tag}%'`; + } + else if (type) { + query += ` AND type ILIKE '%${type}%'`; + } + + // Execute the query + return this.prisma.$queryRawUnsafe(query); + } +}