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
|
# The name of the column that contains the primary key of each location point
|
||||||
DB_LOCATION__ID=id
|
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
|
# The name of the column that contains the timestamp of each location point
|
||||||
DB_LOCATION__TIMESTAMP=timestamp
|
DB_LOCATION__TIMESTAMP=timestamp
|
||||||
|
|
||||||
|
|
|
@ -86,6 +86,7 @@ class Db {
|
||||||
|
|
||||||
opts.locationTableColumns = [
|
opts.locationTableColumns = [
|
||||||
'id',
|
'id',
|
||||||
|
'deviceId',
|
||||||
'timestamp',
|
'timestamp',
|
||||||
'latitude',
|
'latitude',
|
||||||
'longitude',
|
'longitude',
|
||||||
|
@ -96,7 +97,7 @@ class Db {
|
||||||
'postalCode'
|
'postalCode'
|
||||||
].reduce((acc: any, name: string) => {
|
].reduce((acc: any, name: string) => {
|
||||||
acc[name] = process.env[this.prefixedEnv(name)];
|
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
|
// Default to the name of the required field
|
||||||
acc[name] = name;
|
acc[name] = name;
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,6 +9,13 @@ function GPSData(locationTableColumns: Record<string, string>): Record<string, a
|
||||||
autoIncrement: true
|
autoIncrement: true
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const deviceIdCol: string = locationTableColumns['deviceId'];
|
||||||
|
if (deviceIdCol?.length) {
|
||||||
|
typeDef[deviceIdCol] = {
|
||||||
|
type: DataTypes.UUID,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
typeDef[locationTableColumns['latitude']] = {
|
typeDef[locationTableColumns['latitude']] = {
|
||||||
type: DataTypes.FLOAT,
|
type: DataTypes.FLOAT,
|
||||||
allowNull: false
|
allowNull: false
|
||||||
|
|
|
@ -1,25 +1,27 @@
|
||||||
class GPSPoint {
|
class GPSPoint {
|
||||||
public id: number;
|
public id: number;
|
||||||
public latitude: number;
|
public deviceId: string;
|
||||||
public longitude: number;
|
public latitude: number;
|
||||||
public altitude: number | null;
|
public longitude: number;
|
||||||
public address: string | null;
|
public altitude: number | null;
|
||||||
public locality: string | null;
|
public address: string | null;
|
||||||
public country: string | null;
|
public locality: string | null;
|
||||||
public postalCode: string | null;
|
public country: string | null;
|
||||||
public timestamp: Date;
|
public postalCode: string | null;
|
||||||
|
public timestamp: Date;
|
||||||
|
|
||||||
constructor(record: any) {
|
constructor(record: any) {
|
||||||
this.id = record.id;
|
this.id = record.id;
|
||||||
this.latitude = record.latitude;
|
this.deviceId = record.deviceId;
|
||||||
this.longitude = record.longitude;
|
this.latitude = record.latitude;
|
||||||
this.altitude = record.altitude;
|
this.longitude = record.longitude;
|
||||||
this.address = record.address;
|
this.altitude = record.altitude;
|
||||||
this.locality = record.locality;
|
this.address = record.address;
|
||||||
this.country = record.country;
|
this.locality = record.locality;
|
||||||
this.postalCode = record.postalCode;
|
this.country = record.country;
|
||||||
this.timestamp = record.timestamp;
|
this.postalCode = record.postalCode;
|
||||||
}
|
this.timestamp = record.timestamp;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default GPSPoint;
|
export default GPSPoint;
|
||||||
|
|
|
@ -18,6 +18,7 @@ class Location {
|
||||||
|
|
||||||
return new GPSPoint({
|
return new GPSPoint({
|
||||||
id: data[mappings.id],
|
id: data[mappings.id],
|
||||||
|
deviceId: data[mappings.deviceId],
|
||||||
latitude: data[mappings.latitude],
|
latitude: data[mappings.latitude],
|
||||||
longitude: data[mappings.longitude],
|
longitude: data[mappings.longitude],
|
||||||
altitude: data[mappings.altitude],
|
altitude: data[mappings.altitude],
|
||||||
|
@ -32,6 +33,103 @@ class Location {
|
||||||
throw new Error(`Error parsing data: ${error}`);
|
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;
|
export default Location;
|
||||||
|
|
|
@ -17,6 +17,16 @@ class UserDevices {
|
||||||
return new UserDevice(dbDevice.dataValues);
|
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: {
|
public async create(name: string, args: {
|
||||||
userId: number,
|
userId: number,
|
||||||
}): Promise<UserDevice> {
|
}): Promise<UserDevice> {
|
||||||
|
|
|
@ -1,7 +1,10 @@
|
||||||
import { Request, Response } from 'express';
|
import { Request, Response } from 'express';
|
||||||
|
|
||||||
import { authenticate } from '../../../auth';
|
import { authenticate } from '../../../auth';
|
||||||
|
import { AuthInfo } from '../../../auth';
|
||||||
import { LocationRequest } from '../../../requests';
|
import { LocationRequest } from '../../../requests';
|
||||||
|
import { Optional } from '../../../types';
|
||||||
|
import { RoleName } from '../../../models';
|
||||||
import ApiV1Route from './Route';
|
import ApiV1Route from './Route';
|
||||||
|
|
||||||
class GPSData extends ApiV1Route {
|
class GPSData extends ApiV1Route {
|
||||||
|
@ -9,11 +12,22 @@ class GPSData extends ApiV1Route {
|
||||||
super('/gpsdata');
|
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()
|
@authenticate()
|
||||||
get = async (req: Request, res: Response) => {
|
get = async (req: Request, res: Response) => {
|
||||||
let query: LocationRequest
|
let query: LocationRequest
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
// TODO Limit to the points that the user has access to
|
||||||
query = new LocationRequest(req.query);
|
query = new LocationRequest(req.query);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const e = `Error parsing query: ${error}`;
|
const e = `Error parsing query: ${error}`;
|
||||||
|
@ -22,14 +36,26 @@ class GPSData extends ApiV1Route {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
const gpsData = await $repos.location.getHistory(query);
|
||||||
const gpsData = await $repos.location.getHistory(query);
|
res.json(gpsData);
|
||||||
res.json(gpsData);
|
}
|
||||||
} catch (error) {
|
|
||||||
const e = `Error fetching data: ${error}`;
|
@authenticate()
|
||||||
console.error(e);
|
post = async (req: Request, res: Response, auth: Optional<AuthInfo>) => {
|
||||||
res.status(500).send(e);
|
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