Sqlite upgrade and type refactorings

This commit is contained in:
baldo 2022-07-18 17:49:42 +02:00
parent 01691a0c20
commit 28c8429edd
20 changed files with 873 additions and 663 deletions

View file

@ -1,32 +1,17 @@
import {ArrayField, Field, RawJsonField} from "sparkson"
import {ClientConfig, to} from "./shared";
import {ClientConfig, JSONObject, Url} from "./shared";
// TODO: Replace string types by more specific types like URL, Password, etc.
export type Username = {
value: string;
readonly __tag: unique symbol
};
export type CleartextPassword = {
value: string;
readonly __tag: unique symbol
};
export type PasswordHash = {
value: string;
readonly __tag: unique symbol
};
export type Username = string & { readonly __tag: unique symbol };
export type CleartextPassword = string & { readonly __tag: unique symbol };
export type PasswordHash = string & { readonly __tag: unique symbol };
export class UsersConfig {
public username: Username;
public passwordHash: PasswordHash;
constructor(
@Field("user") username: string,
@Field("passwordHash") passwordHash: string,
) {
this.username = to(username);
this.passwordHash = to(passwordHash);
}
@Field("user") public username: Username,
@Field("passwordHash") public passwordHash: PasswordHash,
) {}
}
export class LoggingConfig {
@ -49,19 +34,19 @@ export class EmailConfig {
@Field("from") public from: string,
// For details see: https://nodemailer.com/2-0-0-beta/setup-smtp/
@RawJsonField("smtp") public smtp: any, // TODO: Better types!
@RawJsonField("smtp") public smtp: JSONObject,
) {}
}
export class ServerMapConfig {
constructor(
@ArrayField("nodesJsonUrl", String) public nodesJsonUrl: string[],
@ArrayField("nodesJsonUrl", String) public nodesJsonUrl: Url[],
) {}
}
export class ServerConfig {
constructor(
@Field("baseUrl") public baseUrl: string,
@Field("baseUrl") public baseUrl: Url,
@Field("port") public port: number,
@Field("databaseFile") public databaseFile: string,

View file

@ -1,4 +1,4 @@
import {Domain, MonitoringToken, OnlineState, Site} from "./shared";
import {Domain, EmailAddress, JSONObject, MonitoringToken, OnlineState, Site, toIsEnum} from "./shared";
export * from "./config";
export * from "./logger";
@ -15,15 +15,24 @@ export type NodeSecrets = {
monitoringToken?: MonitoringToken,
};
export type MailId = string;
export type MailData = any;
export type MailType = string;
export type MailId = number & { readonly __tag: unique symbol };
export type MailData = JSONObject;
export enum MailType {
MONITORING_OFFLINE_1 = "monitoring-offline-1",
MONITORING_OFFLINE_2 = "monitoring-offline-2",
MONITORING_OFFLINE_3 = "monitoring-offline-3",
MONITORING_ONLINE_AGAIN = "monitoring-online-again",
MONITORING_CONFIRMATION = "monitoring-confirmation",
}
export const isMailType = toIsEnum(MailType);
export interface Mail {
id: MailId,
email: MailType,
sender: string,
recipient: string,
sender: EmailAddress,
recipient: EmailAddress,
data: MailData,
failures: number,
}

View file

@ -1,8 +1,60 @@
import {ArrayField, Field, RawJsonField} from "sparkson";
import exp from "constants";
// Types shared with the client.
export type TypeGuard<T> = (arg: unknown) => arg is T;
export function parseJSON(str: string): JSONValue {
const json = JSON.parse(str);
if (!isJSONValue(json)) {
throw new Error("Invalid JSON returned. Should never happen.");
}
return json;
}
export type JSONValue =
| null
| string
| number
| boolean
| JSONObject
| JSONArray;
export function isJSONValue(arg: unknown): arg is JSONValue {
return (
arg === null ||
isString(arg) ||
isNumber(arg) ||
isBoolean(arg) ||
isJSONObject(arg) ||
isJSONArray(arg)
);
}
export interface JSONObject {
[x: string]: JSONValue;
}
export function isJSONObject(arg: unknown): arg is JSONObject {
if (!isObject(arg)) {
return false;
}
const obj = arg as object;
for (const [key, value] of Object.entries(obj)) {
if (!isString(key) || !isJSONValue(value)) {
return false;
}
}
return true;
}
export interface JSONArray extends Array<JSONValue> {
}
export const isJSONArray = toIsArray(isJSONValue);
export type EnumValue<E> = E[keyof E];
export type EnumTypeGuard<E> = TypeGuard<EnumValue<E>>;
@ -10,33 +62,10 @@ export function unhandledEnumField(field: never): never {
throw new Error(`Unhandled enum field: ${field}`);
}
export function to<Type extends { readonly __tag: symbol, value: any } = { readonly __tag: unique symbol, value: never }>(value: Type['value']): Type {
return value as any as Type;
}
export function lift2<Result, Type extends { readonly __tag: symbol, value: any }>(callback: (a: Type["value"], b: Type["value"]) => Result): (newtype1: Type, newtype2: Type) => Result {
return (a, b) => callback(a.value, b.value);
}
export function equal<Result, Type extends { readonly __tag: symbol, value: any }>(a: Type, b: Type): boolean {
return lift2((a, b) => a === b)(a, b);
}
export function isObject(arg: unknown): arg is object {
return arg !== null && typeof arg === "object";
}
export function toIsNewtype<Type extends { readonly __tag: symbol, value: Value } = { readonly __tag: unique symbol, value: never }, Value = any>(isValue: TypeGuard<Value>): TypeGuard<Type> {
// TODO: Add validation pattern.
return (arg: unknown): arg is Type => {
if (!isObject(arg)) {
return false;
}
const newtype = arg as Type;
return isValue(newtype.value);
}
}
export function isArray<T>(arg: unknown, isT: TypeGuard<T>): arg is Array<T> {
if (!Array.isArray(arg)) {
return false;
@ -77,11 +106,15 @@ export function isOptional<T>(arg: unknown, isT: TypeGuard<T>): arg is (T | unde
return arg === undefined || isT(arg);
}
export type Version = string;
export type Url = string & { readonly __tag: unique symbol };
export const isUrl = isString;
// Should be good enough for now.
export type Version = string & { readonly __tag: unique symbol };
export const isVersion = isString;
export type EmailAddress = string & { readonly __tag: unique symbol };
export const isEmailAddress = isString;
export type NodeStatistics = {
registered: number;
withVPN: number;
@ -119,10 +152,11 @@ export class CommunityConfig {
constructor(
@Field("name") public name: string,
@Field("domain") public domain: string,
@Field("contactEmail") public contactEmail: string,
@ArrayField("sites", String) public sites: string[],
@ArrayField("domains", String) public domains: string[],
) {}
@Field("contactEmail") public contactEmail: EmailAddress,
@ArrayField("sites", String) public sites: Site[],
@ArrayField("domains", String) public domains: Domain[],
) {
}
}
export function isCommunityConfig(arg: unknown): arg is CommunityConfig {
@ -133,17 +167,18 @@ export function isCommunityConfig(arg: unknown): arg is CommunityConfig {
return (
isString(cfg.name) &&
isString(cfg.domain) &&
isString(cfg.contactEmail) &&
isArray(cfg.sites, isString) &&
isArray(cfg.domains, isString)
isEmailAddress(cfg.contactEmail) &&
isArray(cfg.sites, isSite) &&
isArray(cfg.domains, isDomain)
);
}
export class LegalConfig {
constructor(
@Field("privacyUrl", true) public privacyUrl?: string,
@Field("imprintUrl", true) public imprintUrl?: string,
) {}
@Field("privacyUrl", true) public privacyUrl?: Url,
@Field("imprintUrl", true) public imprintUrl?: Url,
) {
}
}
export function isLegalConfig(arg: unknown): arg is LegalConfig {
@ -152,15 +187,16 @@ export function isLegalConfig(arg: unknown): arg is LegalConfig {
}
const cfg = arg as LegalConfig;
return (
isOptional(cfg.privacyUrl, isString) &&
isOptional(cfg.imprintUrl, isString)
isOptional(cfg.privacyUrl, isUrl) &&
isOptional(cfg.imprintUrl, isUrl)
);
}
export class ClientMapConfig {
constructor(
@Field("mapUrl") public mapUrl: string,
) {}
@Field("mapUrl") public mapUrl: Url,
) {
}
}
export function isClientMapConfig(arg: unknown): arg is ClientMapConfig {
@ -168,13 +204,14 @@ export function isClientMapConfig(arg: unknown): arg is ClientMapConfig {
return false;
}
const cfg = arg as ClientMapConfig;
return isString(cfg.mapUrl);
return isUrl(cfg.mapUrl);
}
export class MonitoringConfig {
constructor(
@Field("enabled") public enabled: boolean,
) {}
) {
}
}
export function isMonitoringConfig(arg: unknown): arg is MonitoringConfig {
@ -185,43 +222,45 @@ export function isMonitoringConfig(arg: unknown): arg is MonitoringConfig {
return isBoolean(cfg.enabled);
}
export class Coords {
export class CoordinatesConfig {
constructor(
@Field("lat") public lat: number,
@Field("lng") public lng: number,
) {}
) {
}
}
export function isCoords(arg: unknown): arg is Coords {
export function isCoordinatesConfig(arg: unknown): arg is CoordinatesConfig {
if (!isObject(arg)) {
return false;
}
const coords = arg as Coords;
const coords = arg as CoordinatesConfig;
return (
isNumber(coords.lat) &&
isNumber(coords.lng)
);
}
export class CoordsSelectorConfig {
export class CoordinatesSelectorConfig {
constructor(
@Field("lat") public lat: number,
@Field("lng") public lng: number,
@Field("defaultZoom") public defaultZoom: number,
@RawJsonField("layers") public layers: any, // TODO: Better types!
) {}
@RawJsonField("layers") public layers: JSONObject,
) {
}
}
export function isCoordsSelectorConfig(arg: unknown): arg is CoordsSelectorConfig {
export function isCoordinatesSelectorConfig(arg: unknown): arg is CoordinatesSelectorConfig {
if (!isObject(arg)) {
return false;
}
const cfg = arg as CoordsSelectorConfig;
const cfg = arg as CoordinatesSelectorConfig;
return (
isNumber(cfg.lat) &&
isNumber(cfg.lng) &&
isNumber(cfg.defaultZoom) &&
isObject(cfg.layers) // TODO: Better types!
isJSONObject(cfg.layers)
);
}
@ -229,8 +268,9 @@ export class OtherCommunityInfoConfig {
constructor(
@Field("showInfo") public showInfo: boolean,
@Field("showBorderForDebugging") public showBorderForDebugging: boolean,
@ArrayField("localCommunityPolygon", Coords) public localCommunityPolygon: Coords[],
) {}
@ArrayField("localCommunityPolygon", CoordinatesConfig) public localCommunityPolygon: CoordinatesConfig[],
) {
}
}
export function isOtherCommunityInfoConfig(arg: unknown): arg is OtherCommunityInfoConfig {
@ -241,7 +281,7 @@ export function isOtherCommunityInfoConfig(arg: unknown): arg is OtherCommunityI
return (
isBoolean(cfg.showInfo) &&
isBoolean(cfg.showBorderForDebugging) &&
isArray(cfg.localCommunityPolygon, isCoords)
isArray(cfg.localCommunityPolygon, isCoordinatesConfig)
);
}
@ -251,7 +291,7 @@ export class ClientConfig {
@Field("legal") public legal: LegalConfig,
@Field("map") public map: ClientMapConfig,
@Field("monitoring") public monitoring: MonitoringConfig,
@Field("coordsSelector") public coordsSelector: CoordsSelectorConfig,
@Field("coordsSelector") public coordsSelector: CoordinatesSelectorConfig,
@Field("otherCommunityInfo") public otherCommunityInfo: OtherCommunityInfoConfig,
@Field("rootPath", true, undefined, "/") public rootPath: string,
) {
@ -268,42 +308,33 @@ export function isClientConfig(arg: unknown): arg is ClientConfig {
isLegalConfig(cfg.legal) &&
isClientMapConfig(cfg.map) &&
isMonitoringConfig(cfg.monitoring) &&
isCoordsSelectorConfig(cfg.coordsSelector) &&
isCoordinatesSelectorConfig(cfg.coordsSelector) &&
isOtherCommunityInfoConfig(cfg.otherCommunityInfo) &&
isString(cfg.rootPath)
);
}
// TODO: Token type.
export type Token = {
value: string;
readonly __tag: unique symbol
};
export const isToken = toIsNewtype<Token>(isString);
export type Token = string & { readonly __tag: unique symbol };
export const isToken = isString;
export type FastdKey = {
value: string;
readonly __tag: unique symbol
};
export const isFastdKey = toIsNewtype<FastdKey>(isString);
export type FastdKey = string & { readonly __tag: unique symbol };
export const isFastdKey = isString;
export type MAC = {
value: string;
readonly __tag: unique symbol
};
export const isMAC = toIsNewtype<MAC>(isString);
export type MAC = string & { readonly __tag: unique symbol };
export const isMAC = isString;
export type UnixTimestampSeconds = number & { readonly __tag: unique symbol };
export const isUnixTimestampSeconds = isNumber;
export type UnixTimestampMilliseconds = number & { readonly __tag: unique symbol };
export const isUnixTimestampMilliseconds = isNumber;
export function toUnixTimestampSeconds(ms: UnixTimestampMilliseconds): UnixTimestampSeconds {
return Math.floor(ms) as UnixTimestampSeconds;
}
export type MonitoringToken = {
value: string;
readonly __tag: unique symbol
};
export type MonitoringToken = string & { readonly __tag: unique symbol };
export enum MonitoringState {
ACTIVE = "active",
@ -313,25 +344,31 @@ export enum MonitoringState {
export const isMonitoringState = toIsEnum(MonitoringState);
export type NodeId = {
value: string;
readonly __tag: unique symbol
};
export type NodeId = string & { readonly __tag: unique symbol };
export type Hostname = string & { readonly __tag: unique symbol };
export const isHostname = isString;
export type Nickname = string & { readonly __tag: unique symbol };
export const isNickname = isString;
export type Coordinates = string & { readonly __tag: unique symbol };
export const isCoordinates = isString;
// TODO: More Newtypes
export interface Node {
export type Node = {
token: Token;
nickname: string;
email: string;
hostname: string;
coords?: string; // TODO: Use object with longitude and latitude.
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 {
if (!isObject(arg)) {
@ -340,16 +377,16 @@ export function isNode(arg: unknown): arg is Node {
const node = arg as Node;
return (
isToken(node.token) &&
isString(node.nickname) &&
isString(node.email) &&
isString(node.hostname) &&
isOptional(node.coords, isString) &&
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) &&
isMonitoringState(node.monitoringState) &&
isNumber(node.modifiedAt)
isUnixTimestampSeconds(node.modifiedAt)
);
}
@ -360,17 +397,11 @@ export enum OnlineState {
export const isOnlineState = toIsEnum(OnlineState);
export type Site = {
value: string;
readonly __tag: unique symbol
};
export const isSite = toIsNewtype<Site>(isString);
export type Site = string & { readonly __tag: unique symbol };
export const isSite = isString;
export type Domain = {
value: string;
readonly __tag: unique symbol
};
export const isDomain = toIsNewtype<Domain>(isString);
export type Domain = string & { readonly __tag: unique symbol };
export const isDomain = isString;
export interface EnhancedNode extends Node {
site?: Site,