Added POST/DELETE /gpsdata endpoints.

Also, added deviceId to location_history table.
This commit is contained in:
Fabio Manganiello 2025-03-10 01:33:33 +01:00
parent dced03fd5a
commit 0d7e199e37
Signed by: blacklight
GPG key ID: D90FBA7F76362774
7 changed files with 176 additions and 29 deletions

View file

@ -48,6 +48,9 @@ DB_LOCATION_TABLE=location_history
# The name of the column that contains the primary key of each location point
DB_LOCATION__ID=id
# The name of the column that contains the device ID of each location point
DB_LOCATION__DEVICE_ID=deviceId
# The name of the column that contains the timestamp of each location point
DB_LOCATION__TIMESTAMP=timestamp

View file

@ -86,6 +86,7 @@ class Db {
opts.locationTableColumns = [
'id',
'deviceId',
'timestamp',
'latitude',
'longitude',
@ -96,7 +97,7 @@ class Db {
'postalCode'
].reduce((acc: any, name: string) => {
acc[name] = process.env[this.prefixedEnv(name)];
if (!acc[name]?.length && requiredColumns[name]) {
if (!acc[name]?.length && (requiredColumns[name] || opts.locationUrl === opts.url)) {
// Default to the name of the required field
acc[name] = name;
}

View file

@ -9,6 +9,13 @@ function GPSData(locationTableColumns: Record<string, string>): Record<string, a
autoIncrement: true
};
const deviceIdCol: string = locationTableColumns['deviceId'];
if (deviceIdCol?.length) {
typeDef[deviceIdCol] = {
type: DataTypes.UUID,
};
}
typeDef[locationTableColumns['latitude']] = {
type: DataTypes.FLOAT,
allowNull: false

View file

@ -1,25 +1,27 @@
class GPSPoint {
public id: number;
public latitude: number;
public longitude: number;
public altitude: number | null;
public address: string | null;
public locality: string | null;
public country: string | null;
public postalCode: string | null;
public timestamp: Date;
public id: number;
public deviceId: string;
public latitude: number;
public longitude: number;
public altitude: number | null;
public address: string | null;
public locality: string | null;
public country: string | null;
public postalCode: string | null;
public timestamp: Date;
constructor(record: any) {
this.id = record.id;
this.latitude = record.latitude;
this.longitude = record.longitude;
this.altitude = record.altitude;
this.address = record.address;
this.locality = record.locality;
this.country = record.country;
this.postalCode = record.postalCode;
this.timestamp = record.timestamp;
}
constructor(record: any) {
this.id = record.id;
this.deviceId = record.deviceId;
this.latitude = record.latitude;
this.longitude = record.longitude;
this.altitude = record.altitude;
this.address = record.address;
this.locality = record.locality;
this.country = record.country;
this.postalCode = record.postalCode;
this.timestamp = record.timestamp;
}
}
export default GPSPoint;

View file

@ -18,6 +18,7 @@ class Location {
return new GPSPoint({
id: data[mappings.id],
deviceId: data[mappings.deviceId],
latitude: data[mappings.latitude],
longitude: data[mappings.longitude],
altitude: data[mappings.altitude],
@ -32,6 +33,103 @@ class Location {
throw new Error(`Error parsing data: ${error}`);
}
}
public async getByIds(ids: number[]): Promise<GPSPoint[]> {
let apiResponse: any[] = [];
try {
apiResponse = await $db.GPSData().findAll({
where: {
id: ids
}
});
} catch (error) {
throw new Error(`Error fetching data: ${error}`);
}
try {
return apiResponse.map((p) => {
const data = p.dataValues;
const mappings: any = $db.locationTableColumns;
return new GPSPoint({
id: data[mappings.id],
deviceId: data[mappings.deviceId],
latitude: data[mappings.latitude],
longitude: data[mappings.longitude],
altitude: data[mappings.altitude],
address: data[mappings.address],
locality: data[mappings.locality],
country: data[mappings.country],
postalCode: data[mappings.postalCode],
timestamp: data[mappings.timestamp],
});
});
} catch (error) {
throw new Error(`Error parsing data: ${error}`);
}
}
public async createPoints(points: GPSPoint[]): Promise<GPSPoint[]> {
const mappings: any = $db.locationTableColumns;
// Lowercase the keys of the mappings object -
// some databases are case-insensitive and this will help with consistency
const normalizedPoints = points.map((p) =>
Object.entries(p).reduce((acc, [key, value]) => {
acc[key.toLowerCase()] = value;
return acc;
} , {} as Record<string, any>)
);
try {
return (
await $db.GPSData().bulkCreate(
normalizedPoints.map((p) => {
return {
[mappings.deviceId]: p.deviceid,
[mappings.latitude]: p.latitude,
[mappings.longitude]: p.longitude,
[mappings.altitude]: p.altitude,
[mappings.address]: p.address,
[mappings.locality]: p.locality,
[mappings.country]: p.country,
[mappings.postalCode]: p.postalcode,
[mappings.timestamp]: p.timestamp
}
},
{ returning: true }
))
).map((p) => {
const data = p.dataValues;
return new GPSPoint({
id: data[mappings.id],
deviceId: data[mappings.deviceId],
latitude: data[mappings.latitude],
longitude: data[mappings.longitude],
altitude: data[mappings.altitude],
address: data[mappings.address],
locality: data[mappings.locality],
country: data[mappings.country],
postalCode: data[mappings.postalCode],
timestamp: data[mappings.timestamp],
});
});
} catch (error) {
throw new Error(`Error saving data: ${error}`);
}
}
public async deletePoints(points: number[]): Promise<void> {
try {
await $db.GPSData().destroy({
where: {
id: points
}
});
} catch (error) {
throw new Error(`Error deleting data: ${error}`);
}
}
}
export default Location;

View file

@ -17,6 +17,16 @@ class UserDevices {
return new UserDevice(dbDevice.dataValues);
}
public async getAll(deviceIds: string[]): Promise<UserDevice[]> {
const dbDevices = await $db.UserDevice().findAll({
where: {
name: deviceIds,
}
});
return dbDevices.map((d) => new UserDevice(d.dataValues));
}
public async create(name: string, args: {
userId: number,
}): Promise<UserDevice> {

View file

@ -1,7 +1,10 @@
import { Request, Response } from 'express';
import { authenticate } from '../../../auth';
import { AuthInfo } from '../../../auth';
import { LocationRequest } from '../../../requests';
import { Optional } from '../../../types';
import { RoleName } from '../../../models';
import ApiV1Route from './Route';
class GPSData extends ApiV1Route {
@ -9,11 +12,22 @@ class GPSData extends ApiV1Route {
super('/gpsdata');
}
private validateOwnership = async (deviceIds: string[], auth: AuthInfo) => {
const user = auth.user;
const notOwnedDevices = (await $repos.userDevices.getAll(deviceIds))
.filter((d) => d.userId !== user.id);
if (notOwnedDevices.length > 0) {
authenticate([RoleName.Admin]);
}
};
@authenticate()
get = async (req: Request, res: Response) => {
let query: LocationRequest
try {
// TODO Limit to the points that the user has access to
query = new LocationRequest(req.query);
} catch (error) {
const e = `Error parsing query: ${error}`;
@ -22,14 +36,26 @@ class GPSData extends ApiV1Route {
return;
}
try {
const gpsData = await $repos.location.getHistory(query);
res.json(gpsData);
} catch (error) {
const e = `Error fetching data: ${error}`;
console.error(e);
res.status(500).send(e);
}
const gpsData = await $repos.location.getHistory(query);
res.json(gpsData);
}
@authenticate()
post = async (req: Request, res: Response, auth: Optional<AuthInfo>) => {
const deviceIds = req.body.map((p: any) => p.deviceId).filter((d: any) => !!d);
this.validateOwnership(deviceIds, auth!);
await $repos.location.createPoints(req.body);
res.status(201).send();
}
@authenticate()
delete = async (req: Request, res: Response, auth: Optional<AuthInfo>) => {
const pointIds = req.body as number[];
const points = await $repos.location.getByIds(pointIds);
const deviceIds = points.map((p) => p.deviceId);
this.validateOwnership(deviceIds, auth!);
await $repos.location.deletePoints(pointIds);
res.status(204).send();
}
}