- Add versioning to API endpoints. - Refactored $db and $repos as global variables. - Extracted routes into separate components with deferred registration. - Support for a different db URL for location data than the one used by the application. - Added sqlite and passport dependencies (passport will soon be used to handle authentication).
This commit is contained in:
parent
f349b2c5be
commit
533ebe960f
26 changed files with 1853 additions and 146 deletions
.env.example
frontend
package-lock.jsonpackage.jsonsrc
tsconfig.json
11
.env.example
11
.env.example
|
@ -8,12 +8,19 @@ BACKEND_ADDRESS=127.0.0.1
|
||||||
# Listen port for the backend (default: 3000)
|
# Listen port for the backend (default: 3000)
|
||||||
BACKEND_PORT=3000
|
BACKEND_PORT=3000
|
||||||
|
|
||||||
# Database URL (required)
|
# Application database URL (required)
|
||||||
DB_URL=postgres://user:password@host:port/dbname
|
DB_URL=postgres://user:password@host:port/dbname
|
||||||
|
|
||||||
|
# If the location data is stored on another db than the one used by the backend,
|
||||||
|
# you can specify a different database URL here.
|
||||||
|
# DB_LOCATION_URL=postgres://user:password@host:port/dbname
|
||||||
|
|
||||||
# Database dialect (default: inferred from the URL)
|
# Database dialect (default: inferred from the URL)
|
||||||
# DB_DIALECT=postgres
|
# DB_DIALECT=postgres
|
||||||
|
|
||||||
|
# Location database dialect (default: inferred from the URL)
|
||||||
|
# DB_LOCATION_DIALECT=postgres
|
||||||
|
|
||||||
# Name of the table that contains the location points (required)
|
# Name of the table that contains the location points (required)
|
||||||
DB_LOCATION_TABLE=gpsdata
|
DB_LOCATION_TABLE=gpsdata
|
||||||
|
|
||||||
|
@ -55,4 +62,4 @@ DB_LOCATION__POSTAL_CODE=postal_code
|
||||||
###
|
###
|
||||||
|
|
||||||
VITE_API_BASE_URL=http://localhost:${BACKEND_PORT}
|
VITE_API_BASE_URL=http://localhost:${BACKEND_PORT}
|
||||||
VITE_API_PATH=/api
|
VITE_API_PATH=/api/v1
|
||||||
|
|
|
@ -11,7 +11,7 @@ export default defineConfig((env) => {
|
||||||
const serverURL = new URL(
|
const serverURL = new URL(
|
||||||
envars.VITE_API_SERVER_URL ?? 'http://localhost:3000'
|
envars.VITE_API_SERVER_URL ?? 'http://localhost:3000'
|
||||||
);
|
);
|
||||||
const serverAPIPath = envars.VITE_API_PATH ?? '/api';
|
const serverAPIPath = envars.VITE_API_PATH ?? '/api/v1';
|
||||||
|
|
||||||
return {
|
return {
|
||||||
envDir: envDir,
|
envDir: envDir,
|
||||||
|
|
1481
package-lock.json
generated
1481
package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
@ -27,8 +27,10 @@
|
||||||
"cors": "^2.8.5",
|
"cors": "^2.8.5",
|
||||||
"dotenv": "^16.4.7",
|
"dotenv": "^16.4.7",
|
||||||
"express": "^4.21.2",
|
"express": "^4.21.2",
|
||||||
|
"passport": "^0.7.0",
|
||||||
"pg": "^8.13.3",
|
"pg": "^8.13.3",
|
||||||
"sequelize": "^6.37.5"
|
"sequelize": "^6.37.5",
|
||||||
|
"sqlite3": "^5.1.7"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/cors": "^2.8.17",
|
"@types/cors": "^2.8.17",
|
||||||
|
|
112
src/db/Db.ts
112
src/db/Db.ts
|
@ -1,38 +1,58 @@
|
||||||
import { Sequelize, DataTypes, Dialect } from 'sequelize';
|
import { Sequelize, Dialect } from 'sequelize';
|
||||||
|
|
||||||
export class Db {
|
import GPSData from './types/GPSData';
|
||||||
|
|
||||||
|
class Db {
|
||||||
private readonly url: string;
|
private readonly url: string;
|
||||||
|
private readonly locationUrl: string;
|
||||||
private readonly locationTable: string;
|
private readonly locationTable: string;
|
||||||
public readonly locationTableColumns: object;
|
public readonly locationTableColumns: Record<string, string>;
|
||||||
private readonly dialect: Dialect;
|
private readonly dialect: Dialect;
|
||||||
private readonly sequelize: Sequelize;
|
private readonly locationDialect: Dialect;
|
||||||
|
private readonly appDb: Sequelize;
|
||||||
|
private readonly locationDb: Sequelize;
|
||||||
|
|
||||||
private static readonly envColumnPrefix = 'DB_LOCATION__';
|
private static readonly envColumnPrefix = 'DB_LOCATION__';
|
||||||
|
|
||||||
private constructor(
|
private constructor(
|
||||||
opts: {
|
opts: {
|
||||||
url: string,
|
url: string,
|
||||||
|
locationUrl: string,
|
||||||
locationTable: string,
|
locationTable: string,
|
||||||
locationTableColumns: object,
|
locationTableColumns: Record<string, string>,
|
||||||
dialect: Dialect,
|
dialect: Dialect,
|
||||||
|
locationDialect: Dialect | null,
|
||||||
}
|
}
|
||||||
) {
|
) {
|
||||||
this.url = opts.url;
|
this.url = opts.url;
|
||||||
|
this.locationUrl = opts.locationUrl;
|
||||||
this.locationTable = opts.locationTable;
|
this.locationTable = opts.locationTable;
|
||||||
this.locationTableColumns = opts.locationTableColumns
|
this.locationTableColumns = opts.locationTableColumns
|
||||||
this.dialect = opts.dialect as Dialect;
|
this.dialect = opts.dialect as Dialect;
|
||||||
|
this.locationDialect = (opts.locationDialect || this.dialect) as Dialect;
|
||||||
|
|
||||||
this.sequelize = new Sequelize(this.url, {
|
this.appDb = new Sequelize(this.url, {
|
||||||
dialect: this.dialect,
|
dialect: this.dialect,
|
||||||
logging: process.env.DEBUG === 'true' ? console.log : false
|
logging: process.env.DEBUG === 'true' ? console.log : false
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (this.locationUrl === this.url) {
|
||||||
|
this.locationDb = this.appDb;
|
||||||
|
} else {
|
||||||
|
this.locationDb = new Sequelize(this.locationUrl, {
|
||||||
|
dialect: this.locationDialect,
|
||||||
|
logging: process.env.DEBUG === 'true' ? console.log : false
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static fromEnv(): Db {
|
public static fromEnv(): Db {
|
||||||
const opts: any = {}
|
const opts: any = {}
|
||||||
opts.url = process.env.DB_URL;
|
opts.url = process.env.DB_URL;
|
||||||
|
opts.locationUrl = process.env.DB_LOCATION_URL || opts.url;
|
||||||
opts.locationTable = process.env.DB_LOCATION_TABLE;
|
opts.locationTable = process.env.DB_LOCATION_TABLE;
|
||||||
opts.dialect = process.env.DB_DIALECT || opts.url.split(':')[0];
|
opts.dialect = process.env.DB_DIALECT || opts.url.split(':')[0];
|
||||||
|
opts.locationDialect = process.env.DB_LOCATION_DIALECT || opts.locationUrl.split(':')[0];
|
||||||
|
|
||||||
if (!opts.url?.length) {
|
if (!opts.url?.length) {
|
||||||
console.error('No DB_URL provided');
|
console.error('No DB_URL provided');
|
||||||
|
@ -77,82 +97,16 @@ export class Db {
|
||||||
return `${Db.envColumnPrefix}${name.toUpperCase()}`;
|
return `${Db.envColumnPrefix}${name.toUpperCase()}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
public GpsData() {
|
/**
|
||||||
const typeDef: any = {};
|
* Tables
|
||||||
|
*/
|
||||||
|
|
||||||
// @ts-expect-error
|
public GPSData() {
|
||||||
typeDef[this.locationTableColumns['id']] = {
|
return this.locationDb.define('GPSData', GPSData(this.locationTableColumns), {
|
||||||
type: DataTypes.INTEGER,
|
|
||||||
primaryKey: true,
|
|
||||||
autoIncrement: true
|
|
||||||
};
|
|
||||||
|
|
||||||
// @ts-expect-error
|
|
||||||
typeDef[this.locationTableColumns['latitude']] = {
|
|
||||||
type: DataTypes.FLOAT,
|
|
||||||
allowNull: false
|
|
||||||
};
|
|
||||||
|
|
||||||
// @ts-expect-error
|
|
||||||
typeDef[this.locationTableColumns['longitude']] = {
|
|
||||||
type: DataTypes.FLOAT,
|
|
||||||
allowNull: false
|
|
||||||
};
|
|
||||||
|
|
||||||
// @ts-expect-error
|
|
||||||
const altitudeCol: string = this.locationTableColumns['altitude'];
|
|
||||||
if (altitudeCol?.length) {
|
|
||||||
typeDef[altitudeCol] = {
|
|
||||||
type: DataTypes.FLOAT,
|
|
||||||
allowNull: true
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// @ts-expect-error
|
|
||||||
const addressCol: string = this.locationTableColumns['address'];
|
|
||||||
if (addressCol?.length) {
|
|
||||||
typeDef[addressCol] = {
|
|
||||||
type: DataTypes.STRING,
|
|
||||||
allowNull: true
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// @ts-expect-error
|
|
||||||
const localityCol: string = this.locationTableColumns['locality'];
|
|
||||||
if (localityCol?.length) {
|
|
||||||
typeDef[localityCol] = {
|
|
||||||
type: DataTypes.STRING,
|
|
||||||
allowNull: true
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// @ts-expect-error
|
|
||||||
const countryCol: string = this.locationTableColumns['country'];
|
|
||||||
if (countryCol?.length) {
|
|
||||||
typeDef[countryCol] = {
|
|
||||||
type: DataTypes.STRING,
|
|
||||||
allowNull: true
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// @ts-expect-error
|
|
||||||
const postalCodeCol: string = this.locationTableColumns['postal_code'];
|
|
||||||
if (postalCodeCol?.length) {
|
|
||||||
typeDef[postalCodeCol] = {
|
|
||||||
type: DataTypes.STRING,
|
|
||||||
allowNull: true
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// @ts-expect-error
|
|
||||||
typeDef[this.locationTableColumns['timestamp']] = {
|
|
||||||
type: DataTypes.DATE,
|
|
||||||
defaultValue: DataTypes.NOW
|
|
||||||
};
|
|
||||||
|
|
||||||
return this.sequelize.define('GpsData', typeDef, {
|
|
||||||
tableName: this.locationTable,
|
tableName: this.locationTable,
|
||||||
timestamps: false
|
timestamps: false
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export default Db;
|
||||||
|
|
3
src/db/index.ts
Normal file
3
src/db/index.ts
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
import Db from "./Db";
|
||||||
|
|
||||||
|
export { Db };
|
70
src/db/types/GPSData.ts
Normal file
70
src/db/types/GPSData.ts
Normal file
|
@ -0,0 +1,70 @@
|
||||||
|
import { DataTypes } from 'sequelize';
|
||||||
|
|
||||||
|
function GPSData(locationTableColumns: Record<string, string>): Record<string, any> {
|
||||||
|
const typeDef: Record<string, any> = {};
|
||||||
|
|
||||||
|
typeDef[locationTableColumns['id']] = {
|
||||||
|
type: DataTypes.INTEGER,
|
||||||
|
primaryKey: true,
|
||||||
|
autoIncrement: true
|
||||||
|
};
|
||||||
|
|
||||||
|
typeDef[locationTableColumns['latitude']] = {
|
||||||
|
type: DataTypes.FLOAT,
|
||||||
|
allowNull: false
|
||||||
|
};
|
||||||
|
|
||||||
|
typeDef[locationTableColumns['longitude']] = {
|
||||||
|
type: DataTypes.FLOAT,
|
||||||
|
allowNull: false
|
||||||
|
};
|
||||||
|
|
||||||
|
const altitudeCol: string = locationTableColumns['altitude'];
|
||||||
|
if (altitudeCol?.length) {
|
||||||
|
typeDef[altitudeCol] = {
|
||||||
|
type: DataTypes.FLOAT,
|
||||||
|
allowNull: true
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const addressCol: string = locationTableColumns['address'];
|
||||||
|
if (addressCol?.length) {
|
||||||
|
typeDef[addressCol] = {
|
||||||
|
type: DataTypes.STRING,
|
||||||
|
allowNull: true
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const localityCol: string = locationTableColumns['locality'];
|
||||||
|
if (localityCol?.length) {
|
||||||
|
typeDef[localityCol] = {
|
||||||
|
type: DataTypes.STRING,
|
||||||
|
allowNull: true
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const countryCol: string = locationTableColumns['country'];
|
||||||
|
if (countryCol?.length) {
|
||||||
|
typeDef[countryCol] = {
|
||||||
|
type: DataTypes.STRING,
|
||||||
|
allowNull: true
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const postalCodeCol: string = locationTableColumns['postal_code'];
|
||||||
|
if (postalCodeCol?.length) {
|
||||||
|
typeDef[postalCodeCol] = {
|
||||||
|
type: DataTypes.STRING,
|
||||||
|
allowNull: true
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
typeDef[locationTableColumns['timestamp']] = {
|
||||||
|
type: DataTypes.DATE,
|
||||||
|
defaultValue: DataTypes.NOW
|
||||||
|
};
|
||||||
|
|
||||||
|
return typeDef;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default GPSData;
|
16
src/globals.ts
Normal file
16
src/globals.ts
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
import dotenv from 'dotenv';
|
||||||
|
|
||||||
|
import { Db } from './db';
|
||||||
|
import Repositories from './repos';
|
||||||
|
|
||||||
|
dotenv.config();
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
var $db: Db;
|
||||||
|
var $repos: Repositories;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useGlobals() {
|
||||||
|
globalThis.$db = Db.fromEnv();
|
||||||
|
globalThis.$repos = new Repositories(globalThis.$db);
|
||||||
|
}
|
41
src/main.ts
41
src/main.ts
|
@ -1,49 +1,20 @@
|
||||||
import cors from 'cors';
|
import cors from 'cors';
|
||||||
import express from 'express';
|
import express from 'express';
|
||||||
import dotenv from 'dotenv';
|
|
||||||
|
|
||||||
import { Db } from './db/Db';
|
import { useGlobals } from './globals';
|
||||||
import { LocationRequest } from './models/LocationRequest';
|
useGlobals();
|
||||||
import { LocationRepository } from './repo/LocationRepository';
|
|
||||||
import { logRequest } from './helpers/logging';
|
|
||||||
|
|
||||||
dotenv.config();
|
import Routes from './routes';
|
||||||
|
|
||||||
const app = express();
|
const app = express();
|
||||||
const db = Db.fromEnv();
|
|
||||||
const locationRepo = new LocationRepository(db);
|
|
||||||
const address = process.env.BACKEND_ADDRESS || '127.0.0.1';
|
const address = process.env.BACKEND_ADDRESS || '127.0.0.1';
|
||||||
const port = process.env.BACKEND_PORT || 3000;
|
const port = process.env.BACKEND_PORT || 3000;
|
||||||
|
const routes = new Routes();
|
||||||
|
|
||||||
// Middleware
|
|
||||||
app.use(cors());
|
app.use(cors());
|
||||||
app.use(express.static('frontend/dist'));
|
app.use(express.static('frontend/dist'));
|
||||||
|
routes.register(app)
|
||||||
|
|
||||||
// API route
|
app.listen(new Number(port).valueOf(), address, () => {
|
||||||
app.get('/api/gpsdata', async (req, res) => {
|
|
||||||
logRequest(req);
|
|
||||||
let query: any = {};
|
|
||||||
|
|
||||||
try {
|
|
||||||
query = new LocationRequest(req.query);
|
|
||||||
} catch (error) {
|
|
||||||
const e = `Error parsing query: ${error}`;
|
|
||||||
console.warn(e);
|
|
||||||
res.status(400).send(e);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const gpsData = await locationRepo.getHistory(query);
|
|
||||||
res.json(gpsData);
|
|
||||||
} catch (error) {
|
|
||||||
const e = `Error fetching data: ${error}`;
|
|
||||||
console.error(e);
|
|
||||||
res.status(500).send(e);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// @ts-ignore
|
|
||||||
app.listen(port, address, () => {
|
|
||||||
console.log(`Server is running on port ${address}:${port}`);
|
console.log(`Server is running on port ${address}:${port}`);
|
||||||
});
|
});
|
||||||
|
|
|
@ -22,4 +22,4 @@ class GPSPoint {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export { GPSPoint };
|
export default GPSPoint;
|
||||||
|
|
|
@ -1,3 +0,0 @@
|
||||||
type Nullable<T> = T | null;
|
|
||||||
|
|
||||||
export { Nullable };
|
|
5
src/models/index.ts
Normal file
5
src/models/index.ts
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
import GPSPoint from "./GPSPoint";
|
||||||
|
|
||||||
|
export {
|
||||||
|
GPSPoint,
|
||||||
|
};
|
|
@ -1,8 +1,8 @@
|
||||||
import { Db } from '../db/Db';
|
import { Db } from '~/db';
|
||||||
import { GPSPoint } from '../models/GPSPoint';
|
import { GPSPoint } from '../models';
|
||||||
import { LocationRequest } from 'src/models/LocationRequest';
|
import { LocationRequest } from '../requests';
|
||||||
|
|
||||||
export class LocationRepository {
|
class LocationRepository {
|
||||||
private db: Db;
|
private db: Db;
|
||||||
|
|
||||||
constructor(db: Db) {
|
constructor(db: Db) {
|
||||||
|
@ -13,7 +13,7 @@ export class LocationRepository {
|
||||||
let apiResponse: any[] = [];
|
let apiResponse: any[] = [];
|
||||||
|
|
||||||
try {
|
try {
|
||||||
apiResponse = await this.db.GpsData().findAll(query.toMap(this.db));
|
apiResponse = await this.db.GPSData().findAll(query.toMap(this.db));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw new Error(`Error fetching data: ${error}`);
|
throw new Error(`Error fetching data: ${error}`);
|
||||||
}
|
}
|
||||||
|
@ -40,3 +40,5 @@ export class LocationRepository {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export default LocationRepository;
|
12
src/repos/index.ts
Normal file
12
src/repos/index.ts
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
import { Db } from 'src/db';
|
||||||
|
import LocationRepository from './LocationRepository';
|
||||||
|
|
||||||
|
class Repositories {
|
||||||
|
public location: LocationRepository;
|
||||||
|
|
||||||
|
constructor(db: Db) {
|
||||||
|
this.location = new LocationRepository(db);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Repositories;
|
|
@ -1,17 +1,18 @@
|
||||||
import { Nullable } from './Types';
|
|
||||||
import { Db } from '../db/Db';
|
|
||||||
import { Op } from 'sequelize';
|
import { Op } from 'sequelize';
|
||||||
|
|
||||||
|
import { Optional } from 'src/types';
|
||||||
|
import { Db } from 'src/db';
|
||||||
|
|
||||||
class LocationRequest {
|
class LocationRequest {
|
||||||
limit: Nullable<number> = 250;
|
limit: Optional<number> = 250;
|
||||||
offset: Nullable<number> = null;
|
offset: Optional<number> = null;
|
||||||
startDate: Nullable<Date> = null;
|
startDate: Optional<Date> = null;
|
||||||
endDate: Nullable<Date> = null;
|
endDate: Optional<Date> = null;
|
||||||
minId: Nullable<number> = null;
|
minId: Optional<number> = null;
|
||||||
maxId: Nullable<number> = null;
|
maxId: Optional<number> = null;
|
||||||
country: Nullable<string> = null;
|
country: Optional<string> = null;
|
||||||
locality: Nullable<string> = null;
|
locality: Optional<string> = null;
|
||||||
postalCode: Nullable<string> = null;
|
postalCode: Optional<string> = null;
|
||||||
orderBy: string = 'timestamp';
|
orderBy: string = 'timestamp';
|
||||||
order: string = 'DESC';
|
order: string = 'DESC';
|
||||||
|
|
||||||
|
@ -99,4 +100,4 @@ class LocationRequest {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export { LocationRequest };
|
export default LocationRequest;
|
5
src/requests/index.ts
Normal file
5
src/requests/index.ts
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
import LocationRequest from "./LocationRequest";
|
||||||
|
|
||||||
|
export {
|
||||||
|
LocationRequest,
|
||||||
|
}
|
75
src/routes/Route.ts
Normal file
75
src/routes/Route.ts
Normal file
|
@ -0,0 +1,75 @@
|
||||||
|
import { Express, Request, Response } from 'express';
|
||||||
|
|
||||||
|
import { logRequest } from '../helpers/logging';
|
||||||
|
|
||||||
|
abstract class Route {
|
||||||
|
protected readonly path: string;
|
||||||
|
|
||||||
|
constructor(path: string) {
|
||||||
|
this.path = path;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected static NotAllowed(res: Response) {
|
||||||
|
res.status(405).send('Method Not Allowed');
|
||||||
|
}
|
||||||
|
|
||||||
|
private getRoute: (req: Request, res: Response) => Promise<void> = async (req, res) => {
|
||||||
|
logRequest(req);
|
||||||
|
return await this.get(req, res);
|
||||||
|
}
|
||||||
|
|
||||||
|
private postRoute: (req: Request, res: Response) => Promise<void> = async (req, res) => {
|
||||||
|
logRequest(req);
|
||||||
|
return await this.post(req, res);
|
||||||
|
}
|
||||||
|
|
||||||
|
private putRoute: (req: Request, res: Response) => Promise<void> = async (req, res) => {
|
||||||
|
logRequest(req);
|
||||||
|
return await this.put(req, res);
|
||||||
|
}
|
||||||
|
|
||||||
|
private deleteRoute: (req: Request, res: Response) => Promise<void> = async (req, res) => {
|
||||||
|
logRequest(req);
|
||||||
|
return await this.delete(req, res);
|
||||||
|
}
|
||||||
|
|
||||||
|
private patchRoute: (req: Request, res: Response) => Promise<void> = async (req, res) => {
|
||||||
|
logRequest(req);
|
||||||
|
return await this.patch(req, res);
|
||||||
|
}
|
||||||
|
|
||||||
|
public get: (req: Request, res: Response) => Promise<void> = async (_, res) => {
|
||||||
|
Route.NotAllowed(res);
|
||||||
|
return Promise.resolve();
|
||||||
|
}
|
||||||
|
|
||||||
|
public post: (req: Request, res: Response) => Promise<void> = async (_, res) => {
|
||||||
|
Route.NotAllowed(res);
|
||||||
|
return Promise.resolve();
|
||||||
|
}
|
||||||
|
|
||||||
|
public put: (req: Request, res: Response) => Promise<void> = async (_, res) => {
|
||||||
|
Route.NotAllowed(res);
|
||||||
|
return Promise.resolve();
|
||||||
|
}
|
||||||
|
|
||||||
|
public delete: (req: Request, res: Response) => Promise<void> = async (_, res) => {
|
||||||
|
Route.NotAllowed(res);
|
||||||
|
return Promise.resolve();
|
||||||
|
}
|
||||||
|
|
||||||
|
public patch: (req: Request, res: Response) => Promise<void> = async (_, res) => {
|
||||||
|
Route.NotAllowed(res);
|
||||||
|
return Promise.resolve();
|
||||||
|
}
|
||||||
|
|
||||||
|
public register(app: Express) {
|
||||||
|
app.get(this.path, this.getRoute);
|
||||||
|
app.post(this.path, this.postRoute);
|
||||||
|
app.put(this.path, this.putRoute);
|
||||||
|
app.delete(this.path, this.deleteRoute);
|
||||||
|
app.patch(this.path, this.patchRoute);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Route;
|
15
src/routes/Routes.ts
Normal file
15
src/routes/Routes.ts
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
import { Express } from 'express';
|
||||||
|
|
||||||
|
import Route from './Route';
|
||||||
|
|
||||||
|
abstract class Routes {
|
||||||
|
public abstract routes: Route[];
|
||||||
|
|
||||||
|
public register(app: Express) {
|
||||||
|
this.routes.forEach((route) => {
|
||||||
|
route.register(app);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Routes;
|
19
src/routes/api/Route.ts
Normal file
19
src/routes/api/Route.ts
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
import Route from '../Route';
|
||||||
|
|
||||||
|
abstract class ApiRoute extends Route {
|
||||||
|
protected version: string;
|
||||||
|
|
||||||
|
constructor(path: string, version: string) {
|
||||||
|
super(ApiRoute.toApiPath(path, version));
|
||||||
|
}
|
||||||
|
|
||||||
|
protected static toApiPath(path: string, version: string): string {
|
||||||
|
if (!path.startsWith('/')) {
|
||||||
|
path = `/${path}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return `/api/${version}${path}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ApiRoute;
|
9
src/routes/api/index.ts
Normal file
9
src/routes/api/index.ts
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
import ApiV1Routes from "./v1";
|
||||||
|
import Routes from "../Routes";
|
||||||
|
|
||||||
|
class ApiRoutes extends Routes {
|
||||||
|
private v1: ApiV1Routes = new ApiV1Routes();
|
||||||
|
public routes = [...this.v1.routes];
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ApiRoutes;
|
37
src/routes/api/v1/GPSData.ts
Normal file
37
src/routes/api/v1/GPSData.ts
Normal file
|
@ -0,0 +1,37 @@
|
||||||
|
import { Request, Response } from 'express';
|
||||||
|
|
||||||
|
import { LocationRequest } from '../../../requests';
|
||||||
|
import LocationRepository from '~/repos/LocationRepository';
|
||||||
|
import ApiV1Route from './Route';
|
||||||
|
|
||||||
|
const $location: LocationRepository = globalThis.$repos.location;
|
||||||
|
|
||||||
|
class GPSData extends ApiV1Route {
|
||||||
|
constructor() {
|
||||||
|
super('/gpsdata');
|
||||||
|
}
|
||||||
|
|
||||||
|
get = async (req: Request, res: Response) => {
|
||||||
|
let query: LocationRequest
|
||||||
|
|
||||||
|
try {
|
||||||
|
query = new LocationRequest(req.query);
|
||||||
|
} catch (error) {
|
||||||
|
const e = `Error parsing query: ${error}`;
|
||||||
|
console.warn(e);
|
||||||
|
res.status(400).send(e);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const gpsData = await $location.getHistory(query);
|
||||||
|
res.json(gpsData);
|
||||||
|
} catch (error) {
|
||||||
|
const e = `Error fetching data: ${error}`;
|
||||||
|
console.error(e);
|
||||||
|
res.status(500).send(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default GPSData;
|
9
src/routes/api/v1/Route.ts
Normal file
9
src/routes/api/v1/Route.ts
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
import ApiRoute from '../Route';
|
||||||
|
|
||||||
|
abstract class ApiV1Route extends ApiRoute {
|
||||||
|
constructor(path: string) {
|
||||||
|
super(path, 'v1');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ApiV1Route;
|
8
src/routes/api/v1/index.ts
Normal file
8
src/routes/api/v1/index.ts
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
import GPSData from "./GPSData";
|
||||||
|
import Routes from "../../Routes";
|
||||||
|
|
||||||
|
class ApiV1Routes extends Routes {
|
||||||
|
public routes = [new GPSData()];
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ApiV1Routes;
|
9
src/routes/index.ts
Normal file
9
src/routes/index.ts
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
import ApiRoutes from "./api";
|
||||||
|
import Routes from "./Routes";
|
||||||
|
|
||||||
|
class AllRoutes extends Routes {
|
||||||
|
private api: ApiRoutes = new ApiRoutes();
|
||||||
|
public routes = [...this.api.routes];
|
||||||
|
}
|
||||||
|
|
||||||
|
export default AllRoutes;
|
3
src/types.ts
Normal file
3
src/types.ts
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
type Optional<T> = T | null | undefined;
|
||||||
|
|
||||||
|
export { Optional };
|
|
@ -21,8 +21,15 @@
|
||||||
"emitDecoratorMetadata": true,
|
"emitDecoratorMetadata": true,
|
||||||
"experimentalDecorators": true,
|
"experimentalDecorators": true,
|
||||||
"resolveJsonModule": true,
|
"resolveJsonModule": true,
|
||||||
"baseUrl": "."
|
"baseUrl": ".",
|
||||||
|
"rootDir": "./src",
|
||||||
|
"paths": {
|
||||||
|
"~/*": ["./src/*"]
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"exclude": ["node_modules"],
|
"exclude": ["node_modules"],
|
||||||
"include": ["./src/**/*.ts"]
|
"include": [
|
||||||
|
"./src/**/*.ts",
|
||||||
|
"./src/**/*.d.ts"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Reference in a new issue