diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..e3e615b --- /dev/null +++ b/.env.example @@ -0,0 +1,2 @@ +MAPBOX_TOKEN= +BDC_KEY= \ No newline at end of file diff --git a/src/app.module.ts b/src/app.module.ts index 55c03be..e344490 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -17,6 +17,7 @@ import { LoggerMiddleware } from './logger/logger.middleware'; import { PrayerTimesModule } from '@/prayer-times/prayer-times.module'; import { ResponseInterceptor } from '@/response/response.interceptor'; import { HealthModule } from './health/health.module'; +import { ReverseGeocodeModule } from './reverse-geocode/reverse-geocode.module'; @Module({ imports: [ @@ -60,6 +61,7 @@ import { HealthModule } from './health/health.module'; }), PrayerTimesModule, HealthModule, + ReverseGeocodeModule, ], controllers: [ AppController, diff --git a/src/reverse-geocode/interfaces/reverse-geocode-response.interface.ts b/src/reverse-geocode/interfaces/reverse-geocode-response.interface.ts new file mode 100644 index 0000000..7ef3ee7 --- /dev/null +++ b/src/reverse-geocode/interfaces/reverse-geocode-response.interface.ts @@ -0,0 +1,52 @@ +export interface BDCReverseGeocodeTimezoneResponse { + latitude: number; + longitude: number; + localityLanguageRequested: string; + continent: string; + continentCode: string; + countryName: string; + countryCode: string; + principalSubdivision: string; + principalSubdivisionCode: string; + city: string; + locality: string; + postcode: string; + plusCode: string; + localityInfo: { + administrative: Array<{ + name: string; + description: string; + isoName?: string; + order: number; + adminLevel: number; + isoCode?: string; + wikidataId?: string; + geonameId?: number; + }>; + informative: Array<{ + name: string; + description: string; + isoName?: string; + order: number; + isoCode?: string; + wikidataId?: string; + geonameId?: number; + }>; + }; + timeZone: { + ianaTimeId: string; + displayName: string; + effectiveTimeZoneFull: string; + effectiveTimeZoneShort: string; + utcOffsetSeconds: number; + utcOffset: string; + isDaylightSavingTime: boolean; + localTime: string; + }; +} + +export interface ReverseGeocodeResponse { + countryName: string; + city: string; + timezone: string; +} diff --git a/src/reverse-geocode/reverse-geocode.controller.ts b/src/reverse-geocode/reverse-geocode.controller.ts new file mode 100644 index 0000000..a137b69 --- /dev/null +++ b/src/reverse-geocode/reverse-geocode.controller.ts @@ -0,0 +1,17 @@ +import { Controller, Get, Query, Version } from '@nestjs/common'; +import { ReverseGeocodeService } from '@/reverse-geocode/reverse-geocode.service'; +import { ReverseGeocodeDto } from '@/reverse-geocode/reverse-geocode.dto'; +import { Throttle } from '@nestjs/throttler'; + +@Controller('reverse-geocode') +export class ReverseGeocodeController { + constructor(private readonly geocodingService: ReverseGeocodeService) {} + + // Allow only three request every 15m + @Throttle({ default: { limit: 3, ttl: 1000 * 60 * 15 } }) + @Get() + @Version('2') + getTimezone(@Query() query: ReverseGeocodeDto) { + return this.geocodingService.reverseGeocodeWithTimezone(query); + } +} diff --git a/src/reverse-geocode/reverse-geocode.dto.ts b/src/reverse-geocode/reverse-geocode.dto.ts new file mode 100644 index 0000000..ea83686 --- /dev/null +++ b/src/reverse-geocode/reverse-geocode.dto.ts @@ -0,0 +1,18 @@ +import { Type } from 'class-transformer'; +import { IsLatitude, IsLocale, IsLongitude, IsNotEmpty } from 'class-validator'; + +export class ReverseGeocodeDto { + @IsNotEmpty() + @IsLatitude() + @Type(() => Number) + latitude: number; + + @IsNotEmpty() + @IsLongitude() + @Type(() => Number) + longitude: number; + + @IsNotEmpty() + @IsLocale() + locale: string; +} diff --git a/src/reverse-geocode/reverse-geocode.module.ts b/src/reverse-geocode/reverse-geocode.module.ts new file mode 100644 index 0000000..b463863 --- /dev/null +++ b/src/reverse-geocode/reverse-geocode.module.ts @@ -0,0 +1,11 @@ +import { Module } from '@nestjs/common'; +import { ReverseGeocodeController } from '@/reverse-geocode/reverse-geocode.controller'; +import { ReverseGeocodeService } from '@/reverse-geocode/reverse-geocode.service'; +import { HttpModule } from '@nestjs/axios'; + +@Module({ + imports: [HttpModule], + controllers: [ReverseGeocodeController], + providers: [ReverseGeocodeService], +}) +export class ReverseGeocodeModule {} diff --git a/src/reverse-geocode/reverse-geocode.service.ts b/src/reverse-geocode/reverse-geocode.service.ts new file mode 100644 index 0000000..82b0657 --- /dev/null +++ b/src/reverse-geocode/reverse-geocode.service.ts @@ -0,0 +1,46 @@ +import { Injectable } from '@nestjs/common'; +import { ReverseGeocodeDto } from '@/reverse-geocode/reverse-geocode.dto'; +import { HttpService } from '@nestjs/axios'; +import { firstValueFrom } from 'rxjs'; + +// interfaces +import { + BDCReverseGeocodeTimezoneResponse, + ReverseGeocodeResponse, +} from '@/reverse-geocode/interfaces/reverse-geocode-response.interface'; + +@Injectable() +export class ReverseGeocodeService { + private url = 'https://api-bdc.net'; + private apiKey = process.env.BDC_KEY; + + constructor(private http: HttpService) {} + async reverseGeocodeWithTimezone( + query: ReverseGeocodeDto, + ): Promise { + const latitude = query.latitude; + const longitude = query.longitude; + const locale = query.locale; + + const endpoint = `/data/reverse-geocode-with-timezone`; + + const response = await firstValueFrom( + this.http.get(`${this.url}${endpoint}`, { + params: { + latitude, + longitude, + localityLanguage: locale, + key: this.apiKey, + }, + }), + ); + + const data = response.data as BDCReverseGeocodeTimezoneResponse; + + return { + countryName: data.countryName, + city: data.city, + timezone: data.timeZone.ianaTimeId, + }; + } +}