BikePointsActivity.ts 3.8 KB
Newer Older
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
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<any, any, any, QueryParams>, res: Response<IBikePointsActivityResponse | ApiError>) => {
    // 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