Added /stats endpoint

This commit is contained in:
Fabio Manganiello 2025-04-03 01:21:10 +02:00
parent 02d7d885ba
commit f050397509
Signed by: blacklight
GPG key ID: D90FBA7F76362774
8 changed files with 136 additions and 0 deletions

43
src/repos/Stats.ts Normal file
View file

@ -0,0 +1,43 @@
import {LocationStats} from '../responses';
import {Sequelize} from 'sequelize';
import {StatsRequest} from '../requests';
class Stats {
public async get(req: StatsRequest): Promise<LocationStats[]> {
const dbColumnsToProps = req.groupBy.reduce((acc, g) => {
acc[$db.locationTableColumns[g]] = g;
return acc;
}, {} as Record<string, string>);
const groupBy = Object.keys(dbColumnsToProps);
return (
await $db.GPSData().findAll({
attributes: [
...groupBy,
[Sequelize.fn('COUNT', Sequelize.col($db.locationTableColumns.id)), 'count'],
[Sequelize.fn('MIN', Sequelize.col($db.locationTableColumns.timestamp)), 'startDate'],
[Sequelize.fn('MAX', Sequelize.col($db.locationTableColumns.timestamp)), 'endDate'],
],
where: {
deviceId: (await $db.UserDevice() .findAll({where: {userId: req.userId}}))
.map((d) => d.dataValues.id)
},
group: groupBy,
order: [[Sequelize.fn('COUNT', Sequelize.col($db.locationTableColumns.id)), req.order]],
})
).map(({dataValues: data}: any) =>
new LocationStats({
key: Object.keys(data).reduce((acc, k) => {
acc[dbColumnsToProps[k] || k] = data[k];
return acc;
}, {} as Record<string, any>),
count: data.count,
startDate: data.startDate,
endDate: data.endDate,
})
);
}
}
export default Stats;

View file

@ -1,4 +1,5 @@
import Location from './Location';
import Stats from './Stats';
import Users from './Users';
import UserDevices from './UserDevices';
import UserRoles from './UserRoles';
@ -6,6 +7,7 @@ import UserSessions from './UserSessions';
class Repositories {
public location: Location;
public stats: Stats;
public users: Users;
public userDevices: UserDevices;
public userRoles: UserRoles;
@ -13,6 +15,7 @@ class Repositories {
constructor() {
this.location = new Location();
this.stats = new Stats();
this.users = new Users();
this.userDevices = new UserDevices();
this.userRoles = new UserRoles();

View file

@ -0,0 +1,25 @@
type Order = 'ASC' | 'DESC';
type GroupBy = 'device' | 'country' | 'locality' | 'postalCode' | 'description';
class StatsRequest {
userId: number;
groupBy: GroupBy[];
order: Order = 'DESC';
constructor(req: {
userId: number;
groupBy: string[] | string;
order?: string;
}) {
this.userId = req.userId;
this.groupBy = (
typeof req.groupBy === 'string' ?
req.groupBy.split(/\s*,\s*/) :
req.groupBy
) as GroupBy[];
this.order = (req.order || this.order).toUpperCase() as Order;
}
}
export default StatsRequest;

View file

@ -1,5 +1,7 @@
import LocationRequest from "./LocationRequest";
import StatsRequest from "./StatsRequest";
export {
LocationRequest,
StatsRequest,
}

View file

@ -0,0 +1,22 @@
import {Optional} from "~/types";
class LocationStats {
public key: Record<string, any>;
public count: number;
public startDate: Optional<Date>;
public endDate: Optional<Date>;
constructor(data: {
key: Record<string, any>;
count: number;
startDate: Optional<Date>;
endDate: Optional<Date>;
}) {
this.key = data.key;
this.count = data.count;
this.startDate = data.startDate;
this.endDate = data.endDate;
}
}
export default LocationStats;

5
src/responses/index.ts Normal file
View file

@ -0,0 +1,5 @@
import LocationStats from "./LocationStats"
export {
LocationStats,
}

View file

@ -0,0 +1,34 @@
import { Request, Response } from 'express';
import { authenticate } from '../../../auth';
import { AuthInfo } from '../../../auth';
import { Optional } from '../../../types';
import { StatsRequest } from '../../../requests';
import ApiV1Route from './Route';
class Stats extends ApiV1Route {
constructor() {
super('/stats');
}
@authenticate()
get = async (req: Request, res: Response, auth: Optional<AuthInfo>) => {
let query: StatsRequest;
try {
query = new StatsRequest({
...req.query as any,
userId: auth!.user.id,
});
} catch (error) {
const e = `Error parsing query: ${error}`;
console.warn(e);
res.status(400).send(e);
return;
}
res.json(await $repos.stats.get(query));
}
}
export default Stats;

View file

@ -3,6 +3,7 @@ import Devices from "./Devices";
import DevicesById from "./DevicesById";
import GPSData from "./GPSData";
import Routes from "../../Routes";
import Stats from "./Stats";
import Tokens from "./Tokens";
import TokensById from "./TokensById";
import UserSelf from "./UserSelf";
@ -13,6 +14,7 @@ class ApiV1Routes extends Routes {
new Devices(),
new DevicesById(),
new GPSData(),
new Stats(),
new Tokens(),
new TokensById(),
new UserSelf(),