Added POST/DELETE /gpsdata endpoints.
Also, added deviceId to location_history table.
This commit is contained in:
parent
dced03fd5a
commit
0d7e199e37
7 changed files with 176 additions and 29 deletions
.env.example
src
|
@ -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
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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> {
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
Loading…
Add table
Reference in a new issue