Bernd-L/exDateMan

View on GitHub
backend/src/app/client-events.ts

Summary

Maintainability
B
5 hrs
Test Coverage
import { Router, Request, Response, NextFunction } from "express";
import { log, error } from "console";
import db from "./db";
import { ServerEvents } from "./server-events";
import { ExdatemanApplication } from "./application";
import { crudType } from "./authentication";
 
export class ClientEvents {
private static singletonFlag = false;
 
/**
* The ClientEvents routes
*/
public routes: Router;
 
/**
* The event-logs (every inventory has its own)
*
* Works like a dict
*/
private static eventLogs: { [uuid: string]: InventoryEvent[] } = {};
 
/**
* Public accessor for the event log
*/
get events(): { [uuid: string]: InventoryEvent[] } {
return ClientEvents.eventLogs;
}
 
constructor() {
// Enforce singleton
if (ClientEvents.singletonFlag) {
throw new Error("A ClientEvents instance already exists.");
} else ClientEvents.singletonFlag = true;
 
// Fetch all event logs available on the db and load them into the dict
this.fetchAllInventoryEvents();
 
// Instantiate the router
this.routes = Router();
 
// Get the events of one inventory
Similar blocks of code found in 2 locations. Consider refactoring.
this.routes.get(
"/:inventoryUuid",
this.checkForManagementEventLogs,
(req: Request, res: Response) =>
this.handleGetInventoryEventsRequest(req, res),
);
 
// Add an event to an inventory
Similar blocks of code found in 2 locations. Consider refactoring.
this.routes.put(
"/",
this.checkForManagementEventLogs,
(req: Request, res: Response) =>
this.handleAppendInventoryEventRequest(req, res),
);
}
 
/**
* Fetches all events of every inventory from the db and parses them
*
* This method is called in the ClientEvents constructor.
*/
private async fetchAllInventoryEvents() {
/**
* A list of all uuids of inventories
*/
const inventoryUuids = await this.getAllInventoryUuids();
 
// Iterate over all inventory uuids
for (const inventoryUuid of inventoryUuids) {
// Get the events of that inventory
ClientEvents.eventLogs[
inventoryUuid.inventoryUuid
] = await this.getInventoryEvents(inventoryUuid.inventoryUuid);
 
// Apply the events inside of the Authorization instance
ExdatemanApplication.ao.applyInventory(
ClientEvents.eventLogs[inventoryUuid.inventoryUuid],
);
}
}
 
/**
* Check for the management event stream uuid, and deny access to it
*/
private checkForManagementEventLogs(
req: Request,
res: Response,
next: NextFunction,
) {
if (req.body.inventoryUuid == ServerEvents.userEventLogUuid) {
res.status(403).json({
message: "You may not access that",
oof: true,
});
return;
} else {
next();
}
}
 
private async handleGetInventoryEventsRequest(req: Request, res: Response) {
try {
// Check for read access
if (
!ExdatemanApplication.ao.checkReadAccess(
req.params.inventoryUuid,
ExdatemanApplication.ae.verifyJWT(req.cookies.JWT).sub,
)
) {
// Unauthorized
res.sendStatus(401);
return;
}
 
// Get the events from the db
const result = ClientEvents.eventLogs[req.params.inventoryUuid];
 
// Send the events back
res.json(result);
} catch (err) {
// error(err);
res.status(400).json({
message: "That didn't work",
oof: true,
});
return;
}
}
 
private async getInventoryEvents(
inventoryUuid: string,
): Promise<InventoryEvent[]> {
return (
await (await db()).query(
`
SELECT "inventoryUuid", date, data
FROM ${process.env.EDM_DB_SCHEMA}.events
WHERE "inventoryUuid" = $1
ORDER BY date ASC;
`,
[inventoryUuid],
)
).rows;
}
 
/**
* Gets all inventory uuids (with event logs) from the db
*/
public async getAllInventoryUuids(): Promise<{ inventoryUuid: string }[]> {
return (
await (await db()).query(
`
SELECT "inventoryUuid"
FROM ${process.env.EDM_DB_SCHEMA}.events
WHERE "inventoryUuid" != $1
GROUP BY "inventoryUuid";
`,
[ServerEvents.userEventLogUuid],
)
).rows;
}
 
/**
* Handles API requests to append an event to an inventory event log
*/
private async handleAppendInventoryEventRequest(req: Request, res: Response) {
try {
// Check for authorization
if (
!ExdatemanApplication.ao.checkEventLegitimacy(
req.body,
ExdatemanApplication.ae.verifyJWT(req.cookies.JWT).sub,
)
) {
// Unauthorized
res.sendStatus(401);
return;
}
 
// Append the event
const result = await this.appendInventoryEvent(req.body);
 
// If the item is about an inventory...
if ((req.body as InventoryEvent).data.itemType == itemType.INVENTORY)
// ...update the inventory projection
ExdatemanApplication.ao.updateInventoriesProjection(req.body);
 
// Send the events back
res.json(result);
} catch (err) {
// error(err);
res.status(400).json({
message: "That didn't work",
oof: true,
});
return;
}
}
 
/**
* Appends an event both to the db and the local event log.
* The projection is not affected.
* If the local dict is missing the event log required, it will get
* initialized.
*
* @param event The event to be appended
*/
private async appendInventoryEvent(event: InventoryEvent) {
// Write the event to the db
Similar blocks of code found in 2 locations. Consider refactoring.
const result = await (await db()).query(
`
INSERT INTO ${process.env.EDM_DB_SCHEMA}.events
("inventoryUuid", date, data)
VALUES ($1, $2, $3)
`,
[event.inventoryUuid, event.date, event.data],
);
 
// Check, if the dict entry needs to be initialized
if (ClientEvents.eventLogs[event.inventoryUuid] == null)
ClientEvents.eventLogs[event.inventoryUuid] = [];
 
// Write the event to the local cache at the correct position
ClientEvents.sortedInsert(
ClientEvents.eventLogs[event.inventoryUuid],
event,
"date",
);
 
return result.rows;
}
 
/**
* Inserts an item into a sorted array keeping the array sorted
*
* This assumes that the array is already sorted.
* This method can handle items which should be placed after the last element
* in the array.
*
* @param array The array to insert into
* @param item The item to be inserted
* @param keyName The name of the key of the item
*/
static sortedInsert<T>(array: T[], item: T, keyName: string): void {
for (let i = 0, length = array.length; i < length; i++) {
if (item[keyName] < array[i][keyName]) {
// Insert the item before the one at i in the array
array.splice(i, 0, item);
 
// Stop this method to avoid duplicates
return;
}
}
 
// If the last item in the array is still smaller
// than the one to be inserted, append the item.
// This will also ensure that an empty array is handled properly.
array.push(item);
}
}
 
/**
* The data structure of an event
*/
export interface InventoryEvent {
/**,
* The date of the event
*/
date: Date;
 
/**
* The uuid of the inventory-event-stream this event belongs to
*/
inventoryUuid: string;
 
/**
* The data of the event
*/
data: {
/**
* The uuid of the item this event is about
* This information is redundant (but still required) on inventory events
*/
uuid: string;
 
/**
* The uuid of the user who issued this event
*/
userUuid: string;
 
/**
* Defines what type of item is this event about
*/
itemType: itemType;
 
/**
* Defines what type of operation was performed
*/
crudType: crudType;
 
/**
* The inventory-specific data (if this event is about an inventory)
*/
Identical blocks of code found in 2 locations. Consider refactoring.
inventoryData?: {
/**
* The name of this inventory
*/
name?: string;
 
/**
* The uuid of the owner of this inventory
*/
ownerUuid?: string;
 
/**
* An array of users who have the admin privilege for this inventory
*/
adminsUuids?: string[];
 
/**
* An array of user uuids who have the write privilege for this inventory
*/
WritablesUuids?: string[];
 
/**
* An array of user uuids who have the read privilege for this inventory
*/
readablesUuids?: string[];
 
/**
* The date of the creation of this inventory
*
* This field may only be set in an inventory-created event
*/
createdOn?: Date;
};
/**
* The category-specific data (if this event is about a category)
*/
Identical blocks of code found in 2 locations. Consider refactoring.
categoryData?: {
/**
* The name of the category
*/
name?: string;
 
/**
* The parent-category of this category
*
* Top-level categories are their own parent
*/
parentUuid?: string;
 
/**
* The date of the creation of this category
*
* This field may only be set in an category-created event
*/
createdOn?: Date;
};
 
/**
* The thing-specific data (if this event is about a thing)
*/
Similar blocks of code found in 2 locations. Consider refactoring.
thingData?: {
/**
* The name of the thing
*/
name: string;
 
/**
* The date of the creation of this category
*
* This field may only be set in an category-created event
*/
createdOn?: Date;
 
/**
* The UUIDs of the categories this thing has
*/
categoryUuids?: string[];
};
 
/**
* The stock-specific data (if this event is about a stock)
*/
stockData?: {
/**
* The expiration date of the stock
*/
exDate?: Date;
 
/**
* How many days after the opening of this stock is it still usable?
*/
useUpIn?: number;
 
/**
* Text description of the quantity of the stock
*/
quantity?: string;
 
/**
* When was this stock opened?
*/
openedOn: Date;
 
/**
* The date of the creation of this category
*
* This field may only be set in an category-created event
*/
createdOn?: Date;
};
};
}
 
/**
* Used to describe on which type of item an operation was performed on
*/
export enum itemType {
INVENTORY = "inventory",
CATEGORY = "category",
THING = "thing",
STOCK = "stock",
}