import StatusCodes from 'http-status-codes' import { Request, Response, Router } from 'express' import { ApiError, paramMissingError } from '@shared/responseTypes' import { bikeTripsCollectionName, dbClient, dbName } from '@server' import BikePointDao from '@daos/BikePoint.ts/BikePointDao' const router = Router() const bikePointDao = new BikePointDao() const { OK } = StatusCodes interface QueryParams { from: string to: string } interface IBikePointActivity { rentals: number returns: number rentalsReturnsImbalance: number } interface IBikePointsActivity { [bikePointId: string]: IBikePointActivity } interface INumberRange { min: number max: number } interface IBikePointsActivityResponse { bikePointsActivity: IBikePointsActivity rentalsRange: INumberRange returnsRange: INumberRange rentalsReturnsImbalanceRange: INumberRange } /********************************************************************************************** * Get Bike Point Activity Statistics in time frame - "GET /api/bike-points-activity" **********************************************************************************************/ router.get('/', async (req: Request, res: Response) => { // read query params const from = Number(req.query.from) const to = Number(req.query.to) if (!from || !to) { return res.status(StatusCodes.BAD_REQUEST).json({error: paramMissingError}) } // read from database efficiently with custom aggregation query (consists of multiple 'stages') const [rentalsPerBikePoint, returnsPerBikePoint] = await Promise.all([ dbClient.db(dbName).collection(bikeTripsCollectionName).aggregate<{_id: string, count: number}>([ // only use data of correct time range {$match: {startDate: {$gte: from, $lt: to}}}, // count how often each station appears as startStationId in trips {$group: {_id: '$startStationId', count: {$sum: 1}}} ]).toArray(), dbClient.db(dbName).collection(bikeTripsCollectionName).aggregate<{_id: string, count: number}>([ // only use data of correct time range {$match: {startDate: {$gte: from, $lt: to}}}, // count how often each station appears as startStationId in trips {$group: {_id: '$endStationId', count: {$sum: 1}}} ]).toArray(), ]) // map data base result to a more useful object structure for frontend, adding in all possible bikePoint ids const bikePoints = await bikePointDao.getAll() const bikePointActivity: IBikePointsActivity = {} bikePoints.forEach(bikePoint => { const rentals = rentalsPerBikePoint.find(a => a._id === bikePoint.id)?.count ?? 0 const returns = returnsPerBikePoint.find(a => a._id === bikePoint.id)?.count ?? 0 bikePointActivity[bikePoint.id] = { rentals: rentals, returns: returns, rentalsReturnsImbalance: returns - rentals, } }) // serialize and send bike point activity data return res.status(OK).json({ bikePointsActivity: bikePointActivity, rentalsRange: getRange(Object.values(bikePointActivity).map(activity => activity.rentals)), returnsRange: getRange(Object.values(bikePointActivity).map(activity => activity.returns)), rentalsReturnsImbalanceRange: getRange(Object.values(bikePointActivity).map(activity => activity.rentalsReturnsImbalance)), }) }) const getRange = (list: number[]): INumberRange => ({ min: Math.min(...list), max: Math.max(...list), }) /****************************************************************************** * Export ******************************************************************************/ export default router