Major refactoring and fixes.

* Split Node into multiple types and make sure fields are actually set
  when type says so.
* Refactor request handling.
* Start getting rid of moment as a dependency by using
  UnixTimestampSeconds instead.
This commit is contained in:
baldo 2022-07-21 18:39:33 +02:00
parent cfa784dfe2
commit 250353edbf
16 changed files with 676 additions and 455 deletions
server/types

View file

@ -1,4 +1,19 @@
import {Domain, EmailAddress, JSONObject, MonitoringToken, OnlineState, Site, toIsEnum} from "./shared";
import {
CreateOrUpdateNode,
Domain,
DomainSpecificNodeResponse,
EmailAddress,
JSONObject,
MonitoringResponse,
MonitoringState,
MonitoringToken,
NodeResponse,
NodeTokenResponse,
OnlineState,
Site,
StoredNode,
toIsEnum,
} from "./shared";
export * from "./config";
export * from "./database";
@ -11,6 +26,60 @@ export type NodeStateData = {
state: OnlineState,
}
export function toCreateOrUpdateNode(node: StoredNode): CreateOrUpdateNode {
return {
nickname: node.nickname,
email: node.email,
hostname: node.hostname,
coords: node.coords,
key: node.key,
mac: node.mac,
monitoring: node.monitoringState !== MonitoringState.DISABLED,
}
}
export function toNodeResponse(node: StoredNode): NodeResponse {
return {
token: node.token,
nickname: node.nickname,
email: node.email,
hostname: node.hostname,
coords: node.coords,
key: node.key,
mac: node.mac,
monitoring: node.monitoringState !== MonitoringState.DISABLED,
monitoringConfirmed: node.monitoringState === MonitoringState.ACTIVE,
monitoringState: node.monitoringState,
modifiedAt: node.modifiedAt,
}
}
export function toNodeTokenResponse(node: StoredNode): NodeTokenResponse {
return {
token: node.token,
node: toNodeResponse(node),
}
}
export function toDomainSpecificNodeResponse(node: StoredNode, nodeStateData: NodeStateData): DomainSpecificNodeResponse {
return {
...toNodeResponse(node),
site: nodeStateData.site,
domain: nodeStateData.domain,
onlineState: nodeStateData.state,
}
}
export function toMonitoringResponse(node: StoredNode): MonitoringResponse {
return {
hostname: node.hostname,
mac: node.mac,
email: node.email,
monitoring: node.monitoringState !== MonitoringState.DISABLED,
monitoringConfirmed: node.monitoringState === MonitoringState.ACTIVE,
};
}
// TODO: Complete interface / class declaration.
export type NodeSecrets = {
monitoringToken?: MonitoringToken,

View file

@ -1,5 +1,4 @@
import {ArrayField, Field, RawJsonField} from "sparkson";
import exp from "constants";
// Types shared with the client.
export type TypeGuard<T> = (arg: unknown) => arg is T;
@ -140,7 +139,7 @@ export function isNodeStatistics(arg: unknown): arg is NodeStatistics {
);
}
export interface Statistics {
export type Statistics = {
nodes: NodeStatistics;
}
@ -324,6 +323,9 @@ export const isFastdKey = isString;
export type MAC = string & { readonly __tag: unique symbol };
export const isMAC = isString;
export type DurationSeconds = number & { readonly __tag: unique symbol };
export const isDurationSeconds = isNumber;
export type UnixTimestampSeconds = number & { readonly __tag: unique symbol };
export const isUnixTimestampSeconds = isNumber;
@ -355,41 +357,105 @@ export const isNickname = isString;
export type Coordinates = string & { readonly __tag: unique symbol };
export const isCoordinates = isString;
// TODO: More Newtypes
export type Node = {
token: Token;
/**
* Basic node data.
*/
export type BaseNode = {
nickname: Nickname;
email: EmailAddress;
hostname: Hostname;
coords?: Coordinates;
key?: FastdKey;
mac: MAC;
monitoring: boolean;
monitoringConfirmed: boolean;
monitoringState: MonitoringState;
modifiedAt: UnixTimestampSeconds;
};
}
export function isNode(arg: unknown): arg is Node {
export function isBaseNode(arg: unknown): arg is BaseNode {
if (!isObject(arg)) {
return false;
}
const node = arg as Node;
const node = arg as BaseNode;
return (
isToken(node.token) &&
isNickname(node.nickname) &&
isEmailAddress(node.email) &&
isHostname(node.hostname) &&
isOptional(node.coords, isCoordinates) &&
isOptional(node.key, isFastdKey) &&
isMAC(node.mac) &&
isBoolean(node.monitoring) &&
isBoolean(node.monitoringConfirmed) &&
isMAC(node.mac)
);
}
/**
* Node data used for creating or updating a node.
*/
export type CreateOrUpdateNode = BaseNode & {
monitoring: boolean;
}
export function isCreateOrUpdateNode(arg: unknown): arg is CreateOrUpdateNode {
if (!isBaseNode(arg)) {
return false;
}
const node = arg as CreateOrUpdateNode;
return (
isBoolean(node.monitoring)
);
}
/**
* Representation of a stored node.
*/
export type StoredNode = BaseNode & {
token: Token;
monitoringState: MonitoringState;
modifiedAt: UnixTimestampSeconds;
}
export function isStoredNode(arg: unknown): arg is StoredNode {
if (!isObject(arg)) {
return false;
}
const node = arg as StoredNode;
return (
isBaseNode(node) &&
isToken(node.token) &&
isMonitoringState(node.monitoringState) &&
isUnixTimestampSeconds(node.modifiedAt)
);
}
export type NodeResponse = StoredNode & {
monitoring: boolean;
monitoringConfirmed: boolean;
}
export function isNodeResponse(arg: unknown): arg is NodeResponse {
if (!isStoredNode(arg)) {
return false;
}
const node = arg as NodeResponse;
return (
isBoolean(node.monitoring) &&
isBoolean(node.monitoringConfirmed)
);
}
export type NodeTokenResponse = {
token: Token;
node: NodeResponse;
}
export function isNodeTokenResponse(arg: unknown): arg is NodeTokenResponse {
if (!isObject(arg)) {
return false;
}
const response = arg as NodeTokenResponse;
return (
isToken(response.token) &&
isNodeResponse(response.node) &&
response.token === response.node.token
);
}
export enum OnlineState {
ONLINE = "ONLINE",
OFFLINE = "OFFLINE",
@ -403,17 +469,20 @@ export const isSite = isString;
export type Domain = string & { readonly __tag: unique symbol };
export const isDomain = isString;
export interface EnhancedNode extends Node {
/**
* Represents a node in the context of a Freifunk site and domain.
*/
export type DomainSpecificNodeResponse = NodeResponse & {
site?: Site,
domain?: Domain,
onlineState?: OnlineState,
}
export function isEnhancedNode(arg: unknown): arg is EnhancedNode {
if (!isNode(arg)) {
export function isDomainSpecificNodeResponse(arg: unknown): arg is DomainSpecificNodeResponse {
if (!isNodeResponse(arg)) {
return false;
}
const node = arg as EnhancedNode;
const node = arg as DomainSpecificNodeResponse;
return (
isOptional(node.site, isSite) &&
isOptional(node.domain, isDomain) &&
@ -421,6 +490,28 @@ export function isEnhancedNode(arg: unknown): arg is EnhancedNode {
);
}
export type MonitoringResponse = {
hostname: Hostname,
mac: MAC,
email: EmailAddress,
monitoring: boolean,
monitoringConfirmed: boolean,
}
export function isMonitoringResponse(arg: unknown): arg is MonitoringResponse {
if (!Object(arg)) {
return false;
}
const response = arg as MonitoringResponse;
return (
isHostname(response.hostname) &&
isMAC(response.mac) &&
isEmailAddress(response.email) &&
isBoolean(response.monitoring) &&
isBoolean(response.monitoringConfirmed)
);
}
export enum NodeSortField {
HOSTNAME = 'hostname',
NICKNAME = 'nickname',
@ -437,7 +528,7 @@ export enum NodeSortField {
export const isNodeSortField = toIsEnum(NodeSortField);
export interface NodesFilter {
export type NodesFilter = {
hasKey?: boolean;
hasCoords?: boolean;
monitoringState?: MonitoringState;