From 3cb0a371a671b3321df563f8a1e6243df282b5f3 Mon Sep 17 00:00:00 2001 From: Fabio Manganiello <fabio@manganiello.tech> Date: Sat, 22 Feb 2025 20:51:46 +0100 Subject: [PATCH] Support more filters and sorting options on the backend. --- src/helpers/logging.ts | 3 ++ src/main.ts | 6 ++- src/models/LocationRequest.ts | 96 ++++++++++++++++++++++++++++------ src/repo/LocationRepository.ts | 2 +- 4 files changed, 87 insertions(+), 20 deletions(-) create mode 100644 src/helpers/logging.ts diff --git a/src/helpers/logging.ts b/src/helpers/logging.ts new file mode 100644 index 0000000..12ea595 --- /dev/null +++ b/src/helpers/logging.ts @@ -0,0 +1,3 @@ +export function logRequest(req: any) { + console.log(`Request: ${req.method} ${req.url}`); +} diff --git a/src/main.ts b/src/main.ts index b7b42d7..6ae5af5 100644 --- a/src/main.ts +++ b/src/main.ts @@ -5,11 +5,13 @@ import dotenv from 'dotenv'; import { Db } from './db/Db'; import { LocationRequest } from './models/LocationRequest'; import { LocationRepository } from './repo/LocationRepository'; +import { logRequest } from './helpers/logging'; dotenv.config(); const app = express(); const db = Db.fromEnv(); +const locationRepo = new LocationRepository(db); const address = process.env.BACKEND_ADDRESS || '127.0.0.1'; const port = process.env.BACKEND_PORT || 3000; @@ -19,7 +21,7 @@ app.use(express.static('frontend/dist')); // API route app.get('/api/gpsdata', async (req, res) => { - console.log(`[${req.ip}] GET ${req.originalUrl}`); + logRequest(req); let query: any = {}; try { @@ -32,7 +34,7 @@ app.get('/api/gpsdata', async (req, res) => { } try { - const gpsData = await new LocationRepository(db).getHistory(query); + const gpsData = await locationRepo.getHistory(query); res.json(gpsData); } catch (error) { const e = `Error fetching data: ${error}`; diff --git a/src/models/LocationRequest.ts b/src/models/LocationRequest.ts index 97cff19..64d4f73 100644 --- a/src/models/LocationRequest.ts +++ b/src/models/LocationRequest.ts @@ -1,38 +1,100 @@ import { Nullable } from './Types'; +import { Db } from '../db/Db'; +import { Op } from 'sequelize'; class LocationRequest { - limit: Nullable<number> = 10; + limit: Nullable<number> = 250; offset: Nullable<number> = null; + startDate: Nullable<Date> = null; + endDate: Nullable<Date> = null; + minId: Nullable<number> = null; + maxId: Nullable<number> = null; + country: Nullable<string> = null; + locality: Nullable<string> = null; + postalCode: Nullable<string> = null; + orderBy: string = 'timestamp'; + order: string = 'DESC'; constructor(req: any) { - if (req.limit != null) { - this.limit = parseInt(req.limit); - if (isNaN(this.limit)) { - throw new TypeError('Invalid limit'); - } - } + this.initNumber('limit', req); + this.initNumber('offset', req); + this.initDate('startDate', req); + this.initDate('endDate', req); + this.initNumber('minId', req); + this.initNumber('maxId', req); + this.country = req.country; + this.locality = req.locality; + this.postalCode = req.postalCode; + this.orderBy = req.orderBy || 'timestamp'; + } - if (req.offset != null) { - this.offset = parseInt(req.offset); - if (isNaN(this.offset)) { - throw new TypeError('Invalid offset'); + private initNumber(key: string, req: any): void { + if (req[key] != null) { + const numValue = (this as any)[key] = parseInt(req[key]); + if (isNaN(numValue)) { + throw new TypeError(`Invalid value for ${key}: ${req[key]}`); } } } - public toMap(): any { - let map: any = {}; + private initDate(key: string, req: any): void { + if (req[key] != null) { + const numValue = (this as any)[key] = parseInt(req[key]); + const dateValue = (this as any)[key] = new Date(isNaN(numValue) ? req[key] : numValue); + if (isNaN(dateValue.getTime())) { + throw new TypeError(`Invalid value for ${key}: ${req[key]}`); + } + } + } + + public toMap(db: Db): any { + let queryMap: any = {}; + const where: any = {}; if (this.limit != null) { - map.limit = this.limit; + queryMap.limit = this.limit; } if (this.offset != null) { - map.offset = this.offset; + queryMap.offset = this.offset; } - map.order = [['created_at', 'DESC']]; - return map; + const colMapping: any = db.locationTableColumns + if (this.startDate != null || this.endDate != null) { + const start = this.startDate == null ? 0 : this.startDate.getTime(); + const end = this.endDate == null ? new Date().getTime() : this.endDate.getTime(); + const column = colMapping.timestamp || 'timestamp'; + const where_t: any = where[column] = {}; + where_t[Op.between] = [start, end]; + } + + if (this.minId != null || this.maxId != null) { + const column = colMapping.id || 'id'; + const where_id: any = where[column] = {}; + if (this.minId == null && this.maxId != null) { + where_id[Op.lte] = this.maxId; + } else if (this.minId != null && this.maxId == null) { + where_id[Op.gte] = this.minId; + } else { + where_id[Op.between] = [this.minId, this.maxId]; + } + } + + if (this.country != null) { + where[colMapping.country || 'country'] = this.country; + } + + if (this.locality != null) { + where[colMapping.locality || 'locality'] = this.locality; + } + + if (this.postalCode != null) { + where[colMapping.postal_code || 'postal_code'] = this.postalCode; + } + + queryMap.where = where; + queryMap.order = [[colMapping[this.orderBy], this.order]]; + return queryMap; } } diff --git a/src/repo/LocationRepository.ts b/src/repo/LocationRepository.ts index c344a52..ddb5308 100644 --- a/src/repo/LocationRepository.ts +++ b/src/repo/LocationRepository.ts @@ -13,7 +13,7 @@ export class LocationRepository { let apiResponse: any[] = []; try { - apiResponse = await this.db.GpsData().findAll(query.toMap()); + apiResponse = await this.db.GpsData().findAll(query.toMap(this.db)); } catch (error) { throw new Error(`Error fetching data: ${error}`); }