Refactoring: Split shared types into seperate modules and document alot.
This commit is contained in:
parent
e08ae944c4
commit
843cd37243
|
@ -9,11 +9,7 @@ import FixNodeFilenamesJob from "./FixNodeFilenamesJob";
|
||||||
import NodeInformationRetrievalJob from "./NodeInformationRetrievalJob";
|
import NodeInformationRetrievalJob from "./NodeInformationRetrievalJob";
|
||||||
import MonitoringMailsSendingJob from "./MonitoringMailsSendingJob";
|
import MonitoringMailsSendingJob from "./MonitoringMailsSendingJob";
|
||||||
import OfflineNodesDeletionJob from "./OfflineNodesDeletionJob";
|
import OfflineNodesDeletionJob from "./OfflineNodesDeletionJob";
|
||||||
|
import { DurationSeconds, JobResultState, TaskState } from "../shared/types";
|
||||||
export enum JobResultState {
|
|
||||||
OKAY = "okay",
|
|
||||||
WARNING = "warning",
|
|
||||||
}
|
|
||||||
|
|
||||||
export type JobResult = {
|
export type JobResult = {
|
||||||
state: JobResultState;
|
state: JobResultState;
|
||||||
|
@ -41,12 +37,6 @@ export interface Job {
|
||||||
run(): Promise<JobResult>;
|
run(): Promise<JobResult>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum TaskState {
|
|
||||||
IDLE = "idle",
|
|
||||||
RUNNING = "running",
|
|
||||||
FAILED = "failed",
|
|
||||||
}
|
|
||||||
|
|
||||||
export class Task {
|
export class Task {
|
||||||
constructor(
|
constructor(
|
||||||
public id: number,
|
public id: number,
|
||||||
|
@ -56,7 +46,7 @@ export class Task {
|
||||||
public job: Job,
|
public job: Job,
|
||||||
public runningSince: moment.Moment | null,
|
public runningSince: moment.Moment | null,
|
||||||
public lastRunStarted: moment.Moment | null,
|
public lastRunStarted: moment.Moment | null,
|
||||||
public lastRunDuration: number | null,
|
public lastRunDuration: DurationSeconds | null,
|
||||||
public state: TaskState,
|
public state: TaskState,
|
||||||
public result: JobResult | null,
|
public result: JobResult | null,
|
||||||
public enabled: boolean
|
public enabled: boolean
|
||||||
|
@ -74,7 +64,9 @@ export class Task {
|
||||||
|
|
||||||
const done = (state: TaskState, result: JobResult | null): void => {
|
const done = (state: TaskState, result: JobResult | null): void => {
|
||||||
const now = moment();
|
const now = moment();
|
||||||
const duration = now.diff(this.runningSince || now);
|
const duration = now.diff(
|
||||||
|
this.runningSince || now
|
||||||
|
) as DurationSeconds;
|
||||||
Logger.tag("jobs").profile("[%sms]\t%s", duration, this.name);
|
Logger.tag("jobs").profile("[%sms]\t%s", duration, this.name);
|
||||||
|
|
||||||
this.runningSince = null;
|
this.runningSince = null;
|
||||||
|
|
|
@ -10,7 +10,6 @@ import { Request, Response } from "express";
|
||||||
import {
|
import {
|
||||||
CreateOrUpdateNode,
|
CreateOrUpdateNode,
|
||||||
DomainSpecificNodeResponse,
|
DomainSpecificNodeResponse,
|
||||||
filterUndefinedFromJSON,
|
|
||||||
isCreateOrUpdateNode,
|
isCreateOrUpdateNode,
|
||||||
isNodeSortField,
|
isNodeSortField,
|
||||||
isString,
|
isString,
|
||||||
|
@ -27,6 +26,7 @@ import {
|
||||||
toNodeResponse,
|
toNodeResponse,
|
||||||
toNodeTokenResponse,
|
toNodeTokenResponse,
|
||||||
} from "../types";
|
} from "../types";
|
||||||
|
import { filterUndefinedFromJSON } from "../shared/utils/json";
|
||||||
|
|
||||||
const nodeFields = [
|
const nodeFields = [
|
||||||
"hostname",
|
"hostname",
|
||||||
|
|
|
@ -2,36 +2,33 @@ import CONSTRAINTS from "../shared/validation/constraints";
|
||||||
import ErrorTypes from "../utils/errorTypes";
|
import ErrorTypes from "../utils/errorTypes";
|
||||||
import * as Resources from "../utils/resources";
|
import * as Resources from "../utils/resources";
|
||||||
import { handleJSONWithData, RequestData } from "../utils/resources";
|
import { handleJSONWithData, RequestData } from "../utils/resources";
|
||||||
import { getTasks, Task, TaskState } from "../jobs/scheduler";
|
import { getTasks, Task } from "../jobs/scheduler";
|
||||||
import { normalizeString } from "../shared/utils/strings";
|
import { normalizeString } from "../shared/utils/strings";
|
||||||
import { forConstraint } from "../shared/validation/validator";
|
import { forConstraint } from "../shared/validation/validator";
|
||||||
import { Request, Response } from "express";
|
import { Request, Response } from "express";
|
||||||
import { isString, isTaskSortField } from "../types";
|
import {
|
||||||
|
isString,
|
||||||
|
isTaskSortField,
|
||||||
|
TaskResponse,
|
||||||
|
TaskSortField,
|
||||||
|
TaskState,
|
||||||
|
UnixTimestampSeconds,
|
||||||
|
} from "../types";
|
||||||
|
|
||||||
const isValidId = forConstraint(CONSTRAINTS.id, false);
|
const isValidId = forConstraint(CONSTRAINTS.id, false);
|
||||||
|
|
||||||
type TaskResponse = {
|
|
||||||
id: number;
|
|
||||||
name: string;
|
|
||||||
description: string;
|
|
||||||
schedule: string;
|
|
||||||
runningSince: number | null;
|
|
||||||
lastRunStarted: number | null;
|
|
||||||
lastRunDuration: number | null;
|
|
||||||
state: string;
|
|
||||||
result: string | null;
|
|
||||||
message: string | null;
|
|
||||||
enabled: boolean;
|
|
||||||
};
|
|
||||||
|
|
||||||
function toTaskResponse(task: Task): TaskResponse {
|
function toTaskResponse(task: Task): TaskResponse {
|
||||||
return {
|
return {
|
||||||
id: task.id,
|
id: task.id,
|
||||||
name: task.name,
|
name: task.name,
|
||||||
description: task.description,
|
description: task.description,
|
||||||
schedule: task.schedule,
|
schedule: task.schedule,
|
||||||
runningSince: task.runningSince && task.runningSince.unix(),
|
runningSince:
|
||||||
lastRunStarted: task.lastRunStarted && task.lastRunStarted.unix(),
|
task.runningSince &&
|
||||||
|
(task.runningSince.unix() as UnixTimestampSeconds),
|
||||||
|
lastRunStarted:
|
||||||
|
task.lastRunStarted &&
|
||||||
|
(task.lastRunStarted.unix() as UnixTimestampSeconds),
|
||||||
lastRunDuration: task.lastRunDuration || null,
|
lastRunDuration: task.lastRunDuration || null,
|
||||||
state: task.state,
|
state: task.state,
|
||||||
result:
|
result:
|
||||||
|
@ -89,7 +86,7 @@ async function doGetAll(
|
||||||
): Promise<{ total: number; pageTasks: Task[] }> {
|
): Promise<{ total: number; pageTasks: Task[] }> {
|
||||||
const restParams = await Resources.getValidRestParams("list", null, req);
|
const restParams = await Resources.getValidRestParams("list", null, req);
|
||||||
|
|
||||||
const tasks = Resources.sort(
|
const tasks = Resources.sort<Task, TaskSortField>(
|
||||||
Object.values(getTasks()),
|
Object.values(getTasks()),
|
||||||
isTaskSortField,
|
isTaskSortField,
|
||||||
restParams
|
restParams
|
||||||
|
|
|
@ -14,12 +14,13 @@ import {
|
||||||
MailData,
|
MailData,
|
||||||
MailId,
|
MailId,
|
||||||
MailSortField,
|
MailSortField,
|
||||||
|
MailSortFieldEnum,
|
||||||
MailType,
|
MailType,
|
||||||
parseJSON,
|
|
||||||
UnixTimestampSeconds,
|
UnixTimestampSeconds,
|
||||||
} from "../types";
|
} from "../types";
|
||||||
import ErrorTypes from "../utils/errorTypes";
|
import ErrorTypes from "../utils/errorTypes";
|
||||||
import { send } from "../mail";
|
import { send } from "../mail";
|
||||||
|
import { parseJSON } from "../shared/utils/json";
|
||||||
|
|
||||||
type EmaiQueueRow = {
|
type EmaiQueueRow = {
|
||||||
id: MailId;
|
id: MailId;
|
||||||
|
@ -81,6 +82,8 @@ async function findPendingMailsBefore(
|
||||||
recipient: row.recipient,
|
recipient: row.recipient,
|
||||||
data,
|
data,
|
||||||
failures: row.failures,
|
failures: row.failures,
|
||||||
|
created_at: row.created_at,
|
||||||
|
modified_at: row.modified_at,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -156,14 +159,14 @@ export async function getPendingMails(
|
||||||
|
|
||||||
const total = row?.total || 0;
|
const total = row?.total || 0;
|
||||||
|
|
||||||
const filter = Resources.filterClause(
|
const filter = Resources.filterClause<MailSortField>(
|
||||||
restParams,
|
restParams,
|
||||||
MailSortField.ID,
|
MailSortFieldEnum.ID,
|
||||||
isMailSortField,
|
isMailSortField,
|
||||||
["id", "failures", "sender", "recipient", "email"]
|
["id", "failures", "sender", "recipient", "email"]
|
||||||
);
|
);
|
||||||
|
|
||||||
const mails = await db.all(
|
const mails = await db.all<Mail>(
|
||||||
"SELECT * FROM email_queue WHERE " + filter.query,
|
"SELECT * FROM email_queue WHERE " + filter.query,
|
||||||
filter.params
|
filter.params
|
||||||
);
|
);
|
||||||
|
|
|
@ -18,7 +18,6 @@ import { forConstraint } from "../shared/validation/validator";
|
||||||
import {
|
import {
|
||||||
Domain,
|
Domain,
|
||||||
DurationSeconds,
|
DurationSeconds,
|
||||||
filterUndefinedFromJSON,
|
|
||||||
Hostname,
|
Hostname,
|
||||||
isBoolean,
|
isBoolean,
|
||||||
isDomain,
|
isDomain,
|
||||||
|
@ -33,15 +32,14 @@ import {
|
||||||
JSONValue,
|
JSONValue,
|
||||||
MAC,
|
MAC,
|
||||||
MailType,
|
MailType,
|
||||||
mapIdFromMAC,
|
|
||||||
MonitoringSortField,
|
MonitoringSortField,
|
||||||
|
MonitoringSortFieldEnum,
|
||||||
MonitoringState,
|
MonitoringState,
|
||||||
MonitoringToken,
|
MonitoringToken,
|
||||||
NodeMonitoringStateResponse,
|
NodeMonitoringStateResponse,
|
||||||
NodeStateData,
|
NodeStateData,
|
||||||
NodeStateId,
|
NodeStateId,
|
||||||
OnlineState,
|
OnlineState,
|
||||||
parseJSON,
|
|
||||||
Site,
|
Site,
|
||||||
StoredNode,
|
StoredNode,
|
||||||
toCreateOrUpdateNode,
|
toCreateOrUpdateNode,
|
||||||
|
@ -56,6 +54,8 @@ import {
|
||||||
subtract,
|
subtract,
|
||||||
weeks,
|
weeks,
|
||||||
} from "../utils/time";
|
} from "../utils/time";
|
||||||
|
import { filterUndefinedFromJSON, parseJSON } from "../shared/utils/json";
|
||||||
|
import { mapIdFromMAC } from "../shared/utils/node";
|
||||||
|
|
||||||
type NodeStateRow = {
|
type NodeStateRow = {
|
||||||
id: NodeStateId;
|
id: NodeStateId;
|
||||||
|
@ -743,9 +743,9 @@ export async function getAll(
|
||||||
|
|
||||||
const total = row?.total || 0;
|
const total = row?.total || 0;
|
||||||
|
|
||||||
const filter = Resources.filterClause(
|
const filter = Resources.filterClause<MonitoringSortField>(
|
||||||
restParams,
|
restParams,
|
||||||
MonitoringSortField.ID,
|
MonitoringSortFieldEnum.ID,
|
||||||
isMonitoringSortField,
|
isMonitoringSortField,
|
||||||
filterFields
|
filterFields
|
||||||
);
|
);
|
||||||
|
|
|
@ -19,7 +19,6 @@ import {
|
||||||
CreateOrUpdateNode,
|
CreateOrUpdateNode,
|
||||||
EmailAddress,
|
EmailAddress,
|
||||||
FastdKey,
|
FastdKey,
|
||||||
filterUndefinedFromJSON,
|
|
||||||
Hostname,
|
Hostname,
|
||||||
isFastdKey,
|
isFastdKey,
|
||||||
isHostname,
|
isHostname,
|
||||||
|
@ -36,13 +35,14 @@ import {
|
||||||
NodeStatistics,
|
NodeStatistics,
|
||||||
StoredNode,
|
StoredNode,
|
||||||
Token,
|
Token,
|
||||||
toUnixTimestampSeconds,
|
|
||||||
TypeGuard,
|
TypeGuard,
|
||||||
unhandledEnumField,
|
|
||||||
UnixTimestampMilliseconds,
|
UnixTimestampMilliseconds,
|
||||||
UnixTimestampSeconds,
|
UnixTimestampSeconds,
|
||||||
} from "../types";
|
} from "../types";
|
||||||
import util from "util";
|
import util from "util";
|
||||||
|
import { filterUndefinedFromJSON } from "../shared/utils/json";
|
||||||
|
import { unhandledEnumField } from "../shared/utils/enums";
|
||||||
|
import { toUnixTimestampSeconds } from "../shared/utils/time";
|
||||||
|
|
||||||
const pglob = util.promisify(glob);
|
const pglob = util.promisify(glob);
|
||||||
|
|
||||||
|
|
39
server/shared/types/arrays.ts
Normal file
39
server/shared/types/arrays.ts
Normal file
|
@ -0,0 +1,39 @@
|
||||||
|
/**
|
||||||
|
* Contains type guards for arrays.
|
||||||
|
*
|
||||||
|
* @module arrays
|
||||||
|
*/
|
||||||
|
import { TypeGuard } from "./helpers";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Type guard for an array with elements of type `<Element>`.
|
||||||
|
*
|
||||||
|
* @param arg - Value to check
|
||||||
|
* @param isElement - Type guard to check elements for type `<Element>`
|
||||||
|
*/
|
||||||
|
export function isArray<Element>(
|
||||||
|
arg: unknown,
|
||||||
|
isElement: TypeGuard<Element>
|
||||||
|
): arg is Array<Element> {
|
||||||
|
if (!Array.isArray(arg)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
for (const element of arg) {
|
||||||
|
if (!isElement(element)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper function to construct array type guards.
|
||||||
|
*
|
||||||
|
* @param isElement - Type guard to check elements for type `<Element>`
|
||||||
|
* @returns A type guard for arrays with elements of type `<Element>`.
|
||||||
|
*/
|
||||||
|
export function toIsArray<Element>(
|
||||||
|
isElement: TypeGuard<Element>
|
||||||
|
): TypeGuard<Element[]> {
|
||||||
|
return (arg): arg is Element[] => isArray(arg, isElement);
|
||||||
|
}
|
371
server/shared/types/config.ts
Normal file
371
server/shared/types/config.ts
Normal file
|
@ -0,0 +1,371 @@
|
||||||
|
/**
|
||||||
|
* Contains types and corresponding type guards for the client side configuration of ffffng.
|
||||||
|
*
|
||||||
|
* @module config
|
||||||
|
*/
|
||||||
|
import { ArrayField, Field, RawJsonField } from "sparkson";
|
||||||
|
import { isObject, isPlainObject } from "./objects";
|
||||||
|
import { isBoolean, isNumber, isString } from "./primitives";
|
||||||
|
import { isArray } from "./arrays";
|
||||||
|
import { isOptional } from "./helpers";
|
||||||
|
import { isJSONObject } from "./json";
|
||||||
|
import { Domain, isDomain, isSite, isUrl, Site, Url } from "./newtypes";
|
||||||
|
import { EmailAddress, isEmailAddress } from "./email";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Configuration for a single coordinate.
|
||||||
|
*
|
||||||
|
* See {@link CommunityConfig.constructor}.
|
||||||
|
*/
|
||||||
|
export class CoordinatesConfig {
|
||||||
|
/**
|
||||||
|
* @param lat - Latitude of the coordinate.
|
||||||
|
* @param lng - Longitude of the coordinate.
|
||||||
|
*/
|
||||||
|
constructor(
|
||||||
|
@Field("lat") public lat: number,
|
||||||
|
@Field("lng") public lng: number
|
||||||
|
) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Type guard for {@link CoordinatesConfig}.
|
||||||
|
*
|
||||||
|
* @param arg - Value to check.
|
||||||
|
*/
|
||||||
|
export function isCoordinatesConfig(arg: unknown): arg is CoordinatesConfig {
|
||||||
|
if (!isObject(arg)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const coords = arg as CoordinatesConfig;
|
||||||
|
return isNumber(coords.lat) && isNumber(coords.lng);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Configuration for checking if a node is inside the community boundaries.
|
||||||
|
*
|
||||||
|
* See {@link OtherCommunityInfoConfig.constructor}.
|
||||||
|
*/
|
||||||
|
export class OtherCommunityInfoConfig {
|
||||||
|
/**
|
||||||
|
* @param showInfo - Specifies if for nodes outside the community boundaries a confirmation screen should be shown.
|
||||||
|
* @param showBorderForDebugging - If set to `true` the outline of the community is rendered on the map.
|
||||||
|
* @param localCommunityPolygon - Boundaries of the community.
|
||||||
|
*/
|
||||||
|
constructor(
|
||||||
|
@Field("showInfo") public showInfo: boolean,
|
||||||
|
@Field("showBorderForDebugging") public showBorderForDebugging: boolean,
|
||||||
|
@ArrayField("localCommunityPolygon", CoordinatesConfig)
|
||||||
|
public localCommunityPolygon: CoordinatesConfig[]
|
||||||
|
) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Type guard for {@link OtherCommunityInfoConfig}.
|
||||||
|
*
|
||||||
|
* @param arg - Value to check.
|
||||||
|
*/
|
||||||
|
export function isOtherCommunityInfoConfig(
|
||||||
|
arg: unknown
|
||||||
|
): arg is OtherCommunityInfoConfig {
|
||||||
|
if (!isObject(arg)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const cfg = arg as OtherCommunityInfoConfig;
|
||||||
|
return (
|
||||||
|
isBoolean(cfg.showInfo) &&
|
||||||
|
isBoolean(cfg.showBorderForDebugging) &&
|
||||||
|
isArray(cfg.localCommunityPolygon, isCoordinatesConfig)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Options of a map layer.
|
||||||
|
*/
|
||||||
|
export type LayerOptions = {
|
||||||
|
/**
|
||||||
|
* Attribution shown for the map layer (HTML).
|
||||||
|
*/
|
||||||
|
attribution: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Subdomains used to load tiles for the map layer, e.g.: `"1234"` or `"abcd"`.
|
||||||
|
*/
|
||||||
|
subdomains?: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Maximum zoom level for the map layer.
|
||||||
|
*/
|
||||||
|
maxZoom: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Type guard for {@link LayerOptions}.
|
||||||
|
*
|
||||||
|
* @param arg - Value to check.
|
||||||
|
*/
|
||||||
|
export function isLayerOptions(arg: unknown): arg is LayerOptions {
|
||||||
|
if (!isPlainObject(arg)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const obj = arg as LayerOptions;
|
||||||
|
return (
|
||||||
|
isString(obj.attribution) &&
|
||||||
|
isOptional(obj.subdomains, isString) &&
|
||||||
|
isNumber(obj.maxZoom)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Configuration of a map layer.
|
||||||
|
*/
|
||||||
|
export type LayerConfig = {
|
||||||
|
/**
|
||||||
|
* Display name of the map layer.
|
||||||
|
*/
|
||||||
|
name: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tiles URL of the layer.
|
||||||
|
*/
|
||||||
|
url: Url;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Type of the map (e.g. `"xyz"`). Unused in new frontend.
|
||||||
|
*/
|
||||||
|
type: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* See {@link LayerOptions}.
|
||||||
|
*/
|
||||||
|
layerOptions: LayerOptions;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Type guard for {@link LayerConfig}.
|
||||||
|
*
|
||||||
|
* @param arg - Value to check.
|
||||||
|
*/
|
||||||
|
export function isLayerConfig(arg: unknown): arg is LayerConfig {
|
||||||
|
if (!isPlainObject(arg)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const obj = arg as LayerConfig;
|
||||||
|
return (
|
||||||
|
isString(obj.name) &&
|
||||||
|
isUrl(obj.url) &&
|
||||||
|
isString(obj.type) &&
|
||||||
|
isLayerOptions(obj.layerOptions)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Configuration of the map for picking node coordinates.
|
||||||
|
*
|
||||||
|
* See {@link CoordinatesSelectorConfig.constructor}
|
||||||
|
*/
|
||||||
|
export class CoordinatesSelectorConfig {
|
||||||
|
/**
|
||||||
|
* @param lat - Latitude to center the map on
|
||||||
|
* @param lng - Longitude to center the map on
|
||||||
|
* @param defaultZoom - Default zoom level of the map
|
||||||
|
* @param layers - Mapping of layer ids to layer configurations for the map
|
||||||
|
*/
|
||||||
|
constructor(
|
||||||
|
@Field("lat") public lat: number,
|
||||||
|
@Field("lng") public lng: number,
|
||||||
|
@Field("defaultZoom") public defaultZoom: number,
|
||||||
|
@RawJsonField("layers") public layers: Record<string, LayerConfig>
|
||||||
|
) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Type guard for {@link CoordinatesSelectorConfig}.
|
||||||
|
*
|
||||||
|
* @param arg - Value to check.
|
||||||
|
*/
|
||||||
|
export function isCoordinatesSelectorConfig(
|
||||||
|
arg: unknown
|
||||||
|
): arg is CoordinatesSelectorConfig {
|
||||||
|
if (!isObject(arg)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const cfg = arg as CoordinatesSelectorConfig;
|
||||||
|
return (
|
||||||
|
isNumber(cfg.lat) &&
|
||||||
|
isNumber(cfg.lng) &&
|
||||||
|
isNumber(cfg.defaultZoom) &&
|
||||||
|
isJSONObject(cfg.layers)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Configuration of monitoring options.
|
||||||
|
*
|
||||||
|
* See {@link MonitoringConfig.constructor}.
|
||||||
|
*/
|
||||||
|
export class MonitoringConfig {
|
||||||
|
/**
|
||||||
|
* @param enabled - Specifies if node owners may activate monitoring for their devices
|
||||||
|
*/
|
||||||
|
constructor(@Field("enabled") public enabled: boolean) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Type guard for {@link MonitoringConfig}.
|
||||||
|
*
|
||||||
|
* @param arg - Value to check.
|
||||||
|
*/
|
||||||
|
export function isMonitoringConfig(arg: unknown): arg is MonitoringConfig {
|
||||||
|
if (!isObject(arg)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const cfg = arg as MonitoringConfig;
|
||||||
|
return isBoolean(cfg.enabled);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Configuration of the community map instance.
|
||||||
|
*
|
||||||
|
* See {@link CommunityMapConfig.constructor}.
|
||||||
|
*/
|
||||||
|
export class CommunityMapConfig {
|
||||||
|
/**
|
||||||
|
* @param mapUrl - Base URL of the Freifunk community's node map
|
||||||
|
*/
|
||||||
|
constructor(@Field("mapUrl") public mapUrl: Url) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Type guard for {@link CommunityMapConfig}.
|
||||||
|
*
|
||||||
|
* @param arg - Value to check.
|
||||||
|
*/
|
||||||
|
export function isCommunityMapConfig(arg: unknown): arg is CommunityMapConfig {
|
||||||
|
if (!isObject(arg)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const cfg = arg as CommunityMapConfig;
|
||||||
|
return isUrl(cfg.mapUrl);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Configuration of URLs for legal information.
|
||||||
|
*
|
||||||
|
* See {@link LegalConfig.constructor}
|
||||||
|
*/
|
||||||
|
export class LegalConfig {
|
||||||
|
/**
|
||||||
|
* @param privacyUrl - Optional: URL to the privacy conditions
|
||||||
|
* @param imprintUrl - Optional: URL to the imprint
|
||||||
|
*/
|
||||||
|
constructor(
|
||||||
|
@Field("privacyUrl", true) public privacyUrl?: Url,
|
||||||
|
@Field("imprintUrl", true) public imprintUrl?: Url
|
||||||
|
) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Type guard for {@link LegalConfig}.
|
||||||
|
*
|
||||||
|
* @param arg - Value to check.
|
||||||
|
*/
|
||||||
|
export function isLegalConfig(arg: unknown): arg is LegalConfig {
|
||||||
|
if (!isObject(arg)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const cfg = arg as LegalConfig;
|
||||||
|
return (
|
||||||
|
isOptional(cfg.privacyUrl, isUrl) && isOptional(cfg.imprintUrl, isUrl)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Configuration for community settings.
|
||||||
|
*
|
||||||
|
* See: {@link CommunityConfig.constructor}
|
||||||
|
*/
|
||||||
|
export class CommunityConfig {
|
||||||
|
/**
|
||||||
|
* @param name - Name of the Freifunk community, e.g. `"Freifunk Musterstadt"`
|
||||||
|
* @param domain - Domain of the Freifunk community, e.g. `"musterstadt.freifunk.net"`
|
||||||
|
* @param contactEmail - Contact email address of the Freifunk community
|
||||||
|
* @param sites - Array of the valid site codes found in the `nodes.json`, e.g.: `["ffms-site1", "ffms-site2"]`
|
||||||
|
* @param domains - Array of the valid domain codes found in the `nodes.json`, e.g.: `["ffms-domain1", "ffms-domain2"]`
|
||||||
|
*/
|
||||||
|
constructor(
|
||||||
|
@Field("name") public name: string,
|
||||||
|
@Field("domain") public domain: string,
|
||||||
|
@Field("contactEmail") public contactEmail: EmailAddress,
|
||||||
|
@ArrayField("sites", String) public sites: Site[],
|
||||||
|
@ArrayField("domains", String) public domains: Domain[]
|
||||||
|
) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Type guard for {@link CommunityConfig}.
|
||||||
|
*
|
||||||
|
* @param arg - Value to check.
|
||||||
|
*/
|
||||||
|
export function isCommunityConfig(arg: unknown): arg is CommunityConfig {
|
||||||
|
if (!isObject(arg)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const cfg = arg as CommunityConfig;
|
||||||
|
return (
|
||||||
|
isString(cfg.name) &&
|
||||||
|
isString(cfg.domain) &&
|
||||||
|
isEmailAddress(cfg.contactEmail) &&
|
||||||
|
isArray(cfg.sites, isSite) &&
|
||||||
|
isArray(cfg.domains, isDomain)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Configuration shared with the client.
|
||||||
|
*
|
||||||
|
* See: {@link ClientConfig.constructor}
|
||||||
|
*/
|
||||||
|
export class ClientConfig {
|
||||||
|
/**
|
||||||
|
* @param community - See {@link CommunityConfig}
|
||||||
|
* @param legal - See {@link LegalConfig}
|
||||||
|
* @param map - See {@link CommunityMapConfig}
|
||||||
|
* @param monitoring - See {@link MonitoringConfig}
|
||||||
|
* @param coordsSelector - See {@link CoordinatesSelectorConfig}
|
||||||
|
* @param otherCommunityInfo - See {@link OtherCommunityInfoConfig}
|
||||||
|
* @param rootPath - Path under which ffffng is served.
|
||||||
|
*/
|
||||||
|
constructor(
|
||||||
|
@Field("community") public community: CommunityConfig,
|
||||||
|
@Field("legal") public legal: LegalConfig,
|
||||||
|
@Field("map") public map: CommunityMapConfig,
|
||||||
|
@Field("monitoring") public monitoring: MonitoringConfig,
|
||||||
|
@Field("coordsSelector")
|
||||||
|
public coordsSelector: CoordinatesSelectorConfig,
|
||||||
|
@Field("otherCommunityInfo")
|
||||||
|
public otherCommunityInfo: OtherCommunityInfoConfig,
|
||||||
|
@Field("rootPath", true, undefined, "/") public rootPath: string
|
||||||
|
) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Type guard for {@link ClientConfig}.
|
||||||
|
*
|
||||||
|
* @param arg - Value to check.
|
||||||
|
*/
|
||||||
|
export function isClientConfig(arg: unknown): arg is ClientConfig {
|
||||||
|
if (!isObject(arg)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const cfg = arg as ClientConfig;
|
||||||
|
return (
|
||||||
|
isCommunityConfig(cfg.community) &&
|
||||||
|
isLegalConfig(cfg.legal) &&
|
||||||
|
isCommunityMapConfig(cfg.map) &&
|
||||||
|
isMonitoringConfig(cfg.monitoring) &&
|
||||||
|
isCoordinatesSelectorConfig(cfg.coordsSelector) &&
|
||||||
|
isOtherCommunityInfoConfig(cfg.otherCommunityInfo) &&
|
||||||
|
isString(cfg.rootPath)
|
||||||
|
);
|
||||||
|
}
|
183
server/shared/types/email.ts
Normal file
183
server/shared/types/email.ts
Normal file
|
@ -0,0 +1,183 @@
|
||||||
|
/**
|
||||||
|
* Contains types and type guards around emails.
|
||||||
|
*/
|
||||||
|
import { toIsNewtype } from "./newtypes";
|
||||||
|
import { isNumber, isString } from "./primitives";
|
||||||
|
import { JSONObject } from "./json";
|
||||||
|
import { toIsEnum } from "./enums";
|
||||||
|
import { SortFieldFor, toIsSortField } from "./sortfields";
|
||||||
|
import { UnixTimestampSeconds } from "./time";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An email address.
|
||||||
|
*/
|
||||||
|
export type EmailAddress = string & { readonly __tag: unique symbol };
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Type guard for {@link EmailAddress}.
|
||||||
|
*
|
||||||
|
* @param arg - Value to check.
|
||||||
|
*/
|
||||||
|
export const isEmailAddress = toIsNewtype(isString, "" as EmailAddress);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ID of an email in the mail queue waiting to be sent.
|
||||||
|
*/
|
||||||
|
export type MailId = number & { readonly __tag: unique symbol };
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Type guard for {@link MailId}.
|
||||||
|
*
|
||||||
|
* @param arg - Value to check.
|
||||||
|
*/
|
||||||
|
export const isMailId = toIsNewtype(isNumber, NaN as MailId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Data of an email in the mail queue waiting to be sent.
|
||||||
|
*/
|
||||||
|
export type Mail = {
|
||||||
|
/**
|
||||||
|
* ID of the email in the queue.
|
||||||
|
*/
|
||||||
|
id: MailId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Type of the to be sent email.
|
||||||
|
*
|
||||||
|
* See {@link MailType}.
|
||||||
|
*/
|
||||||
|
email: MailType;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sender address of the email.
|
||||||
|
*/
|
||||||
|
sender: EmailAddress;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Recipient address of the email.
|
||||||
|
*/
|
||||||
|
recipient: EmailAddress;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Data to be rendered into the email template. This is specific to the email's {@link MailType}.
|
||||||
|
*/
|
||||||
|
data: MailData;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Number of times trying to send the queued email has failed.
|
||||||
|
*/
|
||||||
|
failures: number;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Time the email has been queued first.
|
||||||
|
*/
|
||||||
|
created_at: UnixTimestampSeconds;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Last time the email has been modified inside the queue.
|
||||||
|
*/
|
||||||
|
modified_at: UnixTimestampSeconds;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Type of the email being sent. This determines which email template is being used and in which format the
|
||||||
|
* {@link MailData} is being expected.
|
||||||
|
*/
|
||||||
|
export enum MailType {
|
||||||
|
/**
|
||||||
|
* First monitoring email sent when a Freifunk node is offline.
|
||||||
|
*/
|
||||||
|
MONITORING_OFFLINE_1 = "monitoring-offline-1",
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Second monitoring (first reminder) email sent when a Freifunk node is offline.
|
||||||
|
*/
|
||||||
|
MONITORING_OFFLINE_2 = "monitoring-offline-2",
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Third monitoring (second and last reminder) email sent when a Freifunk node is offline.
|
||||||
|
*/
|
||||||
|
MONITORING_OFFLINE_3 = "monitoring-offline-3",
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Email notifying the owner that their Freifunk node is back online.
|
||||||
|
*/
|
||||||
|
MONITORING_ONLINE_AGAIN = "monitoring-online-again",
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Email holding a confirmation link to enable monitoring for a Freifunk node (double opt-in).
|
||||||
|
*/
|
||||||
|
MONITORING_CONFIRMATION = "monitoring-confirmation",
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Type guard for {@link MailType}.
|
||||||
|
*
|
||||||
|
* @param arg - Value to check.
|
||||||
|
*/
|
||||||
|
export const isMailType = toIsEnum(MailType);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Type of data being rendered into an email template. This is specific to the email's {@link MailType}.
|
||||||
|
*/
|
||||||
|
export type MailData = JSONObject;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enum specifying the allowed sort fields when retrieving the list of emails in the mail queue via
|
||||||
|
* the REST API.
|
||||||
|
*/
|
||||||
|
export enum MailSortFieldEnum {
|
||||||
|
// noinspection JSUnusedGlobalSymbols
|
||||||
|
|
||||||
|
/**
|
||||||
|
* See {@link Mail.id}.
|
||||||
|
*/
|
||||||
|
ID = "id",
|
||||||
|
|
||||||
|
/**
|
||||||
|
* See {@link Mail.failures}.
|
||||||
|
*/
|
||||||
|
FAILURES = "failures",
|
||||||
|
|
||||||
|
/**
|
||||||
|
* See {@link Mail.sender}.
|
||||||
|
*/
|
||||||
|
SENDER = "sender",
|
||||||
|
|
||||||
|
/**
|
||||||
|
* See {@link Mail.recipient}.
|
||||||
|
*/
|
||||||
|
RECIPIENT = "recipient",
|
||||||
|
|
||||||
|
/**
|
||||||
|
* See {@link Mail.email}.
|
||||||
|
*/
|
||||||
|
EMAIL = "email",
|
||||||
|
|
||||||
|
/**
|
||||||
|
* See {@link Mail.created_at}.
|
||||||
|
*/
|
||||||
|
CREATED_AT = "created_at",
|
||||||
|
|
||||||
|
/**
|
||||||
|
See {@link Mail.modified_at}.
|
||||||
|
*/
|
||||||
|
MODIFIED_AT = "modified_at",
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Allowed sort fields when retrieving the list of emails in the mail queue via the REST API.
|
||||||
|
*/
|
||||||
|
export type MailSortField = SortFieldFor<Mail, MailSortFieldEnum>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Type guard for {@link MailSortField}.
|
||||||
|
*
|
||||||
|
* @param arg - Value to check.
|
||||||
|
*/
|
||||||
|
export const isMailSortField = toIsSortField<
|
||||||
|
Mail,
|
||||||
|
MailSortFieldEnum,
|
||||||
|
typeof MailSortFieldEnum,
|
||||||
|
MailSortField
|
||||||
|
>(MailSortFieldEnum);
|
25
server/shared/types/enums.ts
Normal file
25
server/shared/types/enums.ts
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
/**
|
||||||
|
* Contains type guards and helpers for enums.
|
||||||
|
*/
|
||||||
|
import { TypeGuard, ValueOf } from "./helpers";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Shorthand type alias for enum {@link TypeGuard}s.
|
||||||
|
*/
|
||||||
|
export type EnumTypeGuard<E> = TypeGuard<ValueOf<E>>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Shorthand type for descrbing enum objects.
|
||||||
|
*/
|
||||||
|
export type Enum<E> = Record<keyof E, ValueOf<E>>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper function to construct enum type guards.
|
||||||
|
*
|
||||||
|
* @param enumDef - The enum object to check against.
|
||||||
|
* @returns A type guard for values of the enum `<Enum>`
|
||||||
|
*/
|
||||||
|
export function toIsEnum<E extends Enum<E>>(enumDef: E): EnumTypeGuard<E> {
|
||||||
|
return (arg): arg is ValueOf<E> =>
|
||||||
|
Object.values(enumDef).includes(arg as [keyof E]);
|
||||||
|
}
|
45
server/shared/types/helpers.ts
Normal file
45
server/shared/types/helpers.ts
Normal file
|
@ -0,0 +1,45 @@
|
||||||
|
/**
|
||||||
|
* Contains helper types and type guards.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { isNull, isUndefined } from "./primitives";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Shorthand type alias for type guards checking for values of type `<ValueType>`.
|
||||||
|
*/
|
||||||
|
export type TypeGuard<ValueType> = (arg: unknown) => arg is ValueType;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Shorthand type alias for referencing values hold by an object of type `<Type>`.
|
||||||
|
*
|
||||||
|
* See it as an addition to typescript's `keyof`.
|
||||||
|
*/
|
||||||
|
export type ValueOf<Type> = Type[keyof Type];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generic type guard to check for optional values of type `<Type>`.
|
||||||
|
* Optional means the value must either be `undefined` or a valid value of type `<Type>`.
|
||||||
|
*
|
||||||
|
* @param arg - Value to check
|
||||||
|
* @param isType - Type guard for checking for values of type `<Type>`
|
||||||
|
*/
|
||||||
|
export function isOptional<Type>(
|
||||||
|
arg: unknown,
|
||||||
|
isType: TypeGuard<Type>
|
||||||
|
): arg is Type | undefined {
|
||||||
|
return isUndefined(arg) || isType(arg);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generic type guard to check for nullable values of type `<Type>`.
|
||||||
|
* The value must either be `null` or a valid value of type `<Type>`.
|
||||||
|
*
|
||||||
|
* @param arg - Value to check
|
||||||
|
* @param isType - Type guard for checking for values of type `<Type>`
|
||||||
|
*/
|
||||||
|
export function isNullable<Type>(
|
||||||
|
arg: unknown,
|
||||||
|
isType: TypeGuard<Type>
|
||||||
|
): arg is Type | null {
|
||||||
|
return isNull(arg) || isType(arg);
|
||||||
|
}
|
|
@ -1,785 +1,20 @@
|
||||||
import { ArrayField, Field, RawJsonField } from "sparkson";
|
|
||||||
|
|
||||||
// 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 function filterUndefinedFromJSON(obj: {
|
|
||||||
[key: string]: JSONValue | undefined;
|
|
||||||
}): JSONObject {
|
|
||||||
const result: JSONObject = {};
|
|
||||||
for (const [key, value] of Object.entries(obj)) {
|
|
||||||
if (value !== undefined) {
|
|
||||||
result[key] = value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
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 type JSONArray = Array<JSONValue>;
|
|
||||||
|
|
||||||
export const isJSONArray = toIsArray(isJSONValue);
|
|
||||||
|
|
||||||
export type ValueOf<T> = T[keyof T];
|
|
||||||
export type EnumTypeGuard<E> = TypeGuard<ValueOf<E>>;
|
|
||||||
|
|
||||||
export function unhandledEnumField(field: never): never {
|
|
||||||
throw new Error(`Unhandled enum field: ${field}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function isObject(arg: unknown): arg is object {
|
|
||||||
return arg !== null && typeof arg === "object";
|
|
||||||
}
|
|
||||||
|
|
||||||
export function isPlainObject(arg: unknown): arg is { [key: string]: unknown } {
|
|
||||||
return isObject(arg) && !Array.isArray(arg);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function hasOwnProperty<Key extends PropertyKey>(
|
|
||||||
arg: unknown,
|
|
||||||
key: Key
|
|
||||||
): arg is Record<Key, unknown> {
|
|
||||||
return isObject(arg) && key in arg;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getFieldIfExists(
|
|
||||||
arg: unknown,
|
|
||||||
key: PropertyKey
|
|
||||||
): unknown | undefined {
|
|
||||||
return hasOwnProperty(arg, key) ? arg[key] : undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function isArray<T>(arg: unknown, isT: TypeGuard<T>): arg is Array<T> {
|
|
||||||
if (!Array.isArray(arg)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
for (const element of arg) {
|
|
||||||
if (!isT(element)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function isMap(arg: unknown): arg is Map<unknown, unknown> {
|
|
||||||
return arg instanceof Map;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function isString(arg: unknown): arg is string {
|
|
||||||
return typeof arg === "string";
|
|
||||||
}
|
|
||||||
|
|
||||||
// noinspection JSUnusedLocalSymbols
|
|
||||||
export function toIsNewtype<
|
|
||||||
Type extends Value & { readonly __tag: symbol },
|
|
||||||
Value
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
||||||
>(isValue: TypeGuard<Value>, example: Type): TypeGuard<Type> {
|
|
||||||
return (arg: unknown): arg is Type => isValue(arg);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function isNumber(arg: unknown): arg is number {
|
|
||||||
return typeof arg === "number";
|
|
||||||
}
|
|
||||||
|
|
||||||
export function isBoolean(arg: unknown): arg is boolean {
|
|
||||||
return typeof arg === "boolean";
|
|
||||||
}
|
|
||||||
|
|
||||||
export function isUndefined(arg: unknown): arg is undefined {
|
|
||||||
return typeof arg === "undefined";
|
|
||||||
}
|
|
||||||
|
|
||||||
export function isNull(arg: unknown): arg is null {
|
|
||||||
return arg === null;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function toIsArray<T>(isT: TypeGuard<T>): TypeGuard<T[]> {
|
|
||||||
return (arg): arg is T[] => isArray(arg, isT);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function toIsEnum<E extends Record<keyof E, ValueOf<E>>>(
|
|
||||||
enumDef: E
|
|
||||||
): EnumTypeGuard<E> {
|
|
||||||
return (arg): arg is ValueOf<E> =>
|
|
||||||
Object.values(enumDef).includes(arg as [keyof E]);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function isRegExp(arg: unknown): arg is RegExp {
|
|
||||||
return isObject(arg) && arg instanceof RegExp;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function isOptional<T>(
|
|
||||||
arg: unknown,
|
|
||||||
isT: TypeGuard<T>
|
|
||||||
): arg is T | undefined {
|
|
||||||
return arg === undefined || isT(arg);
|
|
||||||
}
|
|
||||||
|
|
||||||
export type Url = string & { readonly __tag: unique symbol };
|
|
||||||
export const isUrl = toIsNewtype(isString, "" as Url);
|
|
||||||
|
|
||||||
export type Version = string & { readonly __tag: unique symbol };
|
|
||||||
export const isVersion = toIsNewtype(isString, "" as Version);
|
|
||||||
|
|
||||||
export type EmailAddress = string & { readonly __tag: unique symbol };
|
|
||||||
export const isEmailAddress = toIsNewtype(isString, "" as EmailAddress);
|
|
||||||
|
|
||||||
export type NodeStatistics = {
|
|
||||||
registered: number;
|
|
||||||
withVPN: number;
|
|
||||||
withCoords: number;
|
|
||||||
monitoring: {
|
|
||||||
active: number;
|
|
||||||
pending: number;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export function isNodeStatistics(arg: unknown): arg is NodeStatistics {
|
|
||||||
if (!isObject(arg)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
const stats = arg as NodeStatistics;
|
|
||||||
return (
|
|
||||||
isNumber(stats.registered) &&
|
|
||||||
isNumber(stats.withVPN) &&
|
|
||||||
isNumber(stats.withCoords) &&
|
|
||||||
isObject(stats.monitoring) &&
|
|
||||||
isNumber(stats.monitoring.active) &&
|
|
||||||
isNumber(stats.monitoring.pending)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export type Statistics = {
|
|
||||||
nodes: NodeStatistics;
|
|
||||||
};
|
|
||||||
|
|
||||||
export function isStatistics(arg: unknown): arg is Statistics {
|
|
||||||
return isObject(arg) && isNodeStatistics((arg as Statistics).nodes);
|
|
||||||
}
|
|
||||||
|
|
||||||
export class CommunityConfig {
|
|
||||||
constructor(
|
|
||||||
@Field("name") public name: string,
|
|
||||||
@Field("domain") public domain: 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 {
|
|
||||||
if (!isObject(arg)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
const cfg = arg as CommunityConfig;
|
|
||||||
return (
|
|
||||||
isString(cfg.name) &&
|
|
||||||
isString(cfg.domain) &&
|
|
||||||
isEmailAddress(cfg.contactEmail) &&
|
|
||||||
isArray(cfg.sites, isSite) &&
|
|
||||||
isArray(cfg.domains, isDomain)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export class LegalConfig {
|
|
||||||
constructor(
|
|
||||||
@Field("privacyUrl", true) public privacyUrl?: Url,
|
|
||||||
@Field("imprintUrl", true) public imprintUrl?: Url
|
|
||||||
) {}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function isLegalConfig(arg: unknown): arg is LegalConfig {
|
|
||||||
if (!isObject(arg)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
const cfg = arg as LegalConfig;
|
|
||||||
return (
|
|
||||||
isOptional(cfg.privacyUrl, isUrl) && isOptional(cfg.imprintUrl, isUrl)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export class ClientMapConfig {
|
|
||||||
constructor(@Field("mapUrl") public mapUrl: Url) {}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function isClientMapConfig(arg: unknown): arg is ClientMapConfig {
|
|
||||||
if (!isObject(arg)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
const cfg = arg as ClientMapConfig;
|
|
||||||
return isUrl(cfg.mapUrl);
|
|
||||||
}
|
|
||||||
|
|
||||||
export class MonitoringConfig {
|
|
||||||
constructor(@Field("enabled") public enabled: boolean) {}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function isMonitoringConfig(arg: unknown): arg is MonitoringConfig {
|
|
||||||
if (!isObject(arg)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
const cfg = arg as MonitoringConfig;
|
|
||||||
return isBoolean(cfg.enabled);
|
|
||||||
}
|
|
||||||
|
|
||||||
export class CoordinatesConfig {
|
|
||||||
constructor(
|
|
||||||
@Field("lat") public lat: number,
|
|
||||||
@Field("lng") public lng: number
|
|
||||||
) {}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function isCoordinatesConfig(arg: unknown): arg is CoordinatesConfig {
|
|
||||||
if (!isObject(arg)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
const coords = arg as CoordinatesConfig;
|
|
||||||
return isNumber(coords.lat) && isNumber(coords.lng);
|
|
||||||
}
|
|
||||||
|
|
||||||
export type LayerOptions = {
|
|
||||||
attribution: string;
|
|
||||||
subdomains?: string;
|
|
||||||
maxZoom: number;
|
|
||||||
};
|
|
||||||
|
|
||||||
export function isLayerOptions(arg: unknown): arg is LayerOptions {
|
|
||||||
if (!isPlainObject(arg)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
const obj = arg as LayerOptions;
|
|
||||||
return (
|
|
||||||
isString(obj.attribution) &&
|
|
||||||
isOptional(obj.subdomains, isString) &&
|
|
||||||
isNumber(obj.maxZoom)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export type LayerConfig = {
|
|
||||||
name: string;
|
|
||||||
url: Url;
|
|
||||||
type: string;
|
|
||||||
layerOptions: LayerOptions;
|
|
||||||
};
|
|
||||||
|
|
||||||
export function isLayerConfig(arg: unknown): arg is LayerConfig {
|
|
||||||
if (!isPlainObject(arg)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
const obj = arg as LayerConfig;
|
|
||||||
return (
|
|
||||||
isString(obj.name) &&
|
|
||||||
isUrl(obj.url) &&
|
|
||||||
isString(obj.type) &&
|
|
||||||
isLayerOptions(obj.layerOptions)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export class CoordinatesSelectorConfig {
|
|
||||||
constructor(
|
|
||||||
@Field("lat") public lat: number,
|
|
||||||
@Field("lng") public lng: number,
|
|
||||||
@Field("defaultZoom") public defaultZoom: number,
|
|
||||||
@RawJsonField("layers") public layers: Record<string, LayerConfig>
|
|
||||||
) {}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function isCoordinatesSelectorConfig(
|
|
||||||
arg: unknown
|
|
||||||
): arg is CoordinatesSelectorConfig {
|
|
||||||
if (!isObject(arg)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
const cfg = arg as CoordinatesSelectorConfig;
|
|
||||||
return (
|
|
||||||
isNumber(cfg.lat) &&
|
|
||||||
isNumber(cfg.lng) &&
|
|
||||||
isNumber(cfg.defaultZoom) &&
|
|
||||||
isJSONObject(cfg.layers)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export class OtherCommunityInfoConfig {
|
|
||||||
constructor(
|
|
||||||
@Field("showInfo") public showInfo: boolean,
|
|
||||||
@Field("showBorderForDebugging") public showBorderForDebugging: boolean,
|
|
||||||
@ArrayField("localCommunityPolygon", CoordinatesConfig)
|
|
||||||
public localCommunityPolygon: CoordinatesConfig[]
|
|
||||||
) {}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function isOtherCommunityInfoConfig(
|
|
||||||
arg: unknown
|
|
||||||
): arg is OtherCommunityInfoConfig {
|
|
||||||
if (!isObject(arg)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
const cfg = arg as OtherCommunityInfoConfig;
|
|
||||||
return (
|
|
||||||
isBoolean(cfg.showInfo) &&
|
|
||||||
isBoolean(cfg.showBorderForDebugging) &&
|
|
||||||
isArray(cfg.localCommunityPolygon, isCoordinatesConfig)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export class ClientConfig {
|
|
||||||
constructor(
|
|
||||||
@Field("community") public community: CommunityConfig,
|
|
||||||
@Field("legal") public legal: LegalConfig,
|
|
||||||
@Field("map") public map: ClientMapConfig,
|
|
||||||
@Field("monitoring") public monitoring: MonitoringConfig,
|
|
||||||
@Field("coordsSelector")
|
|
||||||
public coordsSelector: CoordinatesSelectorConfig,
|
|
||||||
@Field("otherCommunityInfo")
|
|
||||||
public otherCommunityInfo: OtherCommunityInfoConfig,
|
|
||||||
@Field("rootPath", true, undefined, "/") public rootPath: string
|
|
||||||
) {}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function isClientConfig(arg: unknown): arg is ClientConfig {
|
|
||||||
if (!isObject(arg)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
const cfg = arg as ClientConfig;
|
|
||||||
return (
|
|
||||||
isCommunityConfig(cfg.community) &&
|
|
||||||
isLegalConfig(cfg.legal) &&
|
|
||||||
isClientMapConfig(cfg.map) &&
|
|
||||||
isMonitoringConfig(cfg.monitoring) &&
|
|
||||||
isCoordinatesSelectorConfig(cfg.coordsSelector) &&
|
|
||||||
isOtherCommunityInfoConfig(cfg.otherCommunityInfo) &&
|
|
||||||
isString(cfg.rootPath)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export type Token = string & { readonly __tag: unique symbol };
|
|
||||||
export const isToken = toIsNewtype(isString, "" as Token);
|
|
||||||
|
|
||||||
export type FastdKey = string & { readonly __tag: unique symbol };
|
|
||||||
export const isFastdKey = toIsNewtype(isString, "" as FastdKey);
|
|
||||||
|
|
||||||
export type MAC = string & { readonly __tag: unique symbol };
|
|
||||||
export const isMAC = toIsNewtype(isString, "" as MAC);
|
|
||||||
|
|
||||||
export type MapId = string & { readonly __tag: unique symbol };
|
|
||||||
export const isMapId = toIsNewtype(isString, "" as MapId);
|
|
||||||
export function mapIdFromMAC(mac: MAC): MapId {
|
|
||||||
return mac.toLowerCase().replace(/:/g, "") as MapId;
|
|
||||||
}
|
|
||||||
|
|
||||||
export type DurationSeconds = number & { readonly __tag: unique symbol };
|
|
||||||
export const isDurationSeconds = toIsNewtype(isNumber, NaN as DurationSeconds);
|
|
||||||
|
|
||||||
export type DurationMilliseconds = number & { readonly __tag: unique symbol };
|
|
||||||
export const isDurationMilliseconds = toIsNewtype(
|
|
||||||
isNumber,
|
|
||||||
NaN as DurationMilliseconds
|
|
||||||
);
|
|
||||||
|
|
||||||
export type UnixTimestampSeconds = number & { readonly __tag: unique symbol };
|
|
||||||
export const isUnixTimestampSeconds = toIsNewtype(
|
|
||||||
isNumber,
|
|
||||||
NaN as UnixTimestampSeconds
|
|
||||||
);
|
|
||||||
|
|
||||||
export type UnixTimestampMilliseconds = number & {
|
|
||||||
readonly __tag: unique symbol;
|
|
||||||
};
|
|
||||||
export const isUnixTimestampMilliseconds = toIsNewtype(
|
|
||||||
isNumber,
|
|
||||||
NaN as UnixTimestampMilliseconds
|
|
||||||
);
|
|
||||||
|
|
||||||
export function toUnixTimestampSeconds(
|
|
||||||
ms: UnixTimestampMilliseconds
|
|
||||||
): UnixTimestampSeconds {
|
|
||||||
return Math.floor(ms) as UnixTimestampSeconds;
|
|
||||||
}
|
|
||||||
|
|
||||||
export type MonitoringToken = string & { readonly __tag: unique symbol };
|
|
||||||
export const isMonitoringToken = toIsNewtype(isString, "" as MonitoringToken);
|
|
||||||
|
|
||||||
export enum MonitoringState {
|
|
||||||
ACTIVE = "active",
|
|
||||||
PENDING = "pending",
|
|
||||||
DISABLED = "disabled",
|
|
||||||
}
|
|
||||||
|
|
||||||
export const isMonitoringState = toIsEnum(MonitoringState);
|
|
||||||
|
|
||||||
export type NodeId = string & { readonly __tag: unique symbol };
|
|
||||||
export const isNodeId = toIsNewtype(isString, "" as NodeId);
|
|
||||||
|
|
||||||
export type Hostname = string & { readonly __tag: unique symbol };
|
|
||||||
export const isHostname = toIsNewtype(isString, "" as Hostname);
|
|
||||||
|
|
||||||
export type Nickname = string & { readonly __tag: unique symbol };
|
|
||||||
export const isNickname = toIsNewtype(isString, "" as Nickname);
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* String representing geo coordinates. Latitude and longitude are delimited by one whitespace.
|
* This module and all submodules provide types that are being shared between client and server.
|
||||||
* E.g.: <code>"53.565278 10.001389"</code>
|
|
||||||
*/
|
*/
|
||||||
export type Coordinates = string & { readonly __tag: unique symbol };
|
export * from "./arrays";
|
||||||
export const isCoordinates = toIsNewtype(isString, "" as Coordinates);
|
export * from "./config";
|
||||||
|
export * from "./email";
|
||||||
/**
|
export * from "./enums";
|
||||||
* Basic node data.
|
export * from "./helpers";
|
||||||
*/
|
export * from "./json";
|
||||||
export type BaseNode = {
|
export * from "./maps";
|
||||||
nickname: Nickname;
|
export * from "./monitoring";
|
||||||
email: EmailAddress;
|
export * from "./newtypes";
|
||||||
hostname: Hostname;
|
export * from "./node";
|
||||||
coords: Coordinates | undefined;
|
export * from "./objects";
|
||||||
key: FastdKey | undefined;
|
export * from "./primitives";
|
||||||
mac: MAC;
|
export * from "./regexps";
|
||||||
};
|
export * from "./statistics";
|
||||||
|
export * from "./sortfields";
|
||||||
export function isBaseNode(arg: unknown): arg is BaseNode {
|
export * from "./task";
|
||||||
if (!isObject(arg)) {
|
export * from "./time";
|
||||||
return false;
|
|
||||||
}
|
|
||||||
const node = arg as BaseNode;
|
|
||||||
return (
|
|
||||||
isNickname(node.nickname) &&
|
|
||||||
isEmailAddress(node.email) &&
|
|
||||||
isHostname(node.hostname) &&
|
|
||||||
isOptional(node.coords, isCoordinates) &&
|
|
||||||
isOptional(node.key, isFastdKey) &&
|
|
||||||
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",
|
|
||||||
}
|
|
||||||
|
|
||||||
export const isOnlineState = toIsEnum(OnlineState);
|
|
||||||
|
|
||||||
export type Site = string & { readonly __tag: unique symbol };
|
|
||||||
export const isSite = toIsNewtype(isString, "" as Site);
|
|
||||||
|
|
||||||
export type Domain = string & { readonly __tag: unique symbol };
|
|
||||||
export const isDomain = toIsNewtype(isString, "" as Domain);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Represents a node in the context of a Freifunk site and domain.
|
|
||||||
*/
|
|
||||||
export type DomainSpecificNodeResponse = NodeResponse & {
|
|
||||||
site: Site | undefined;
|
|
||||||
domain: Domain | undefined;
|
|
||||||
onlineState: OnlineState | undefined;
|
|
||||||
};
|
|
||||||
|
|
||||||
export function isDomainSpecificNodeResponse(
|
|
||||||
arg: unknown
|
|
||||||
): arg is DomainSpecificNodeResponse {
|
|
||||||
if (!isNodeResponse(arg)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
const node = arg as DomainSpecificNodeResponse;
|
|
||||||
return (
|
|
||||||
isOptional(node.site, isSite) &&
|
|
||||||
isOptional(node.domain, isDomain) &&
|
|
||||||
isOptional(node.onlineState, isOnlineState)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
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 type NodeStateId = number & { readonly __tag: unique symbol };
|
|
||||||
export const isNodeStateId = toIsNewtype(isNumber, 0 as NodeStateId);
|
|
||||||
|
|
||||||
export type NodeMonitoringStateResponse = {
|
|
||||||
id: NodeStateId;
|
|
||||||
created_at: UnixTimestampSeconds;
|
|
||||||
domain?: Domain;
|
|
||||||
hostname?: Hostname;
|
|
||||||
import_timestamp: UnixTimestampSeconds;
|
|
||||||
last_seen: UnixTimestampSeconds;
|
|
||||||
last_status_mail_sent?: UnixTimestampSeconds;
|
|
||||||
last_status_mail_type?: MailType;
|
|
||||||
mac: MAC;
|
|
||||||
modified_at: UnixTimestampSeconds;
|
|
||||||
monitoring_state?: MonitoringState;
|
|
||||||
site?: Site;
|
|
||||||
state: OnlineState;
|
|
||||||
mapId: MapId;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type MailId = number & { readonly __tag: unique symbol };
|
|
||||||
export const isMailId = toIsNewtype(isNumber, NaN as MailId);
|
|
||||||
|
|
||||||
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 type Mail = {
|
|
||||||
id: MailId;
|
|
||||||
email: MailType;
|
|
||||||
sender: EmailAddress;
|
|
||||||
recipient: EmailAddress;
|
|
||||||
data: MailData;
|
|
||||||
failures: number;
|
|
||||||
};
|
|
||||||
|
|
||||||
// noinspection JSUnusedGlobalSymbols
|
|
||||||
enum NodeSortFieldEnum {
|
|
||||||
HOSTNAME = "hostname",
|
|
||||||
NICKNAME = "nickname",
|
|
||||||
EMAIL = "email",
|
|
||||||
TOKEN = "token",
|
|
||||||
MAC = "mac",
|
|
||||||
KEY = "key",
|
|
||||||
SITE = "site",
|
|
||||||
DOMAIN = "domain",
|
|
||||||
COORDS = "coords",
|
|
||||||
ONLINE_STATE = "onlineState",
|
|
||||||
MONITORING_STATE = "monitoringState",
|
|
||||||
}
|
|
||||||
|
|
||||||
export type NodeSortField = keyof Pick<
|
|
||||||
DomainSpecificNodeResponse,
|
|
||||||
NodeSortFieldEnum
|
|
||||||
>;
|
|
||||||
|
|
||||||
export function isNodeSortField(arg: unknown): arg is NodeSortField {
|
|
||||||
if (!isString(arg)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return Object.values(NodeSortFieldEnum).includes(arg as NodeSortField);
|
|
||||||
}
|
|
||||||
|
|
||||||
export type NodesFilter = {
|
|
||||||
hasKey?: boolean;
|
|
||||||
hasCoords?: boolean;
|
|
||||||
monitoringState?: MonitoringState;
|
|
||||||
site?: Site;
|
|
||||||
domain?: Domain;
|
|
||||||
onlineState?: OnlineState;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const NODES_FILTER_FIELDS: Record<
|
|
||||||
keyof NodesFilter,
|
|
||||||
| BooleanConstructor
|
|
||||||
| StringConstructor
|
|
||||||
| typeof MonitoringState
|
|
||||||
| typeof OnlineState
|
|
||||||
> = {
|
|
||||||
hasKey: Boolean,
|
|
||||||
hasCoords: Boolean,
|
|
||||||
monitoringState: MonitoringState,
|
|
||||||
site: String,
|
|
||||||
domain: String,
|
|
||||||
onlineState: OnlineState,
|
|
||||||
};
|
|
||||||
|
|
||||||
export function isNodesFilter(arg: unknown): arg is NodesFilter {
|
|
||||||
if (!isObject(arg)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
const filter = arg as NodesFilter;
|
|
||||||
return (
|
|
||||||
isOptional(filter.hasKey, isBoolean) &&
|
|
||||||
isOptional(filter.hasCoords, isBoolean) &&
|
|
||||||
isOptional(filter.monitoringState, isMonitoringState) &&
|
|
||||||
isOptional(filter.site, isSite) &&
|
|
||||||
isOptional(filter.domain, isDomain) &&
|
|
||||||
isOptional(filter.onlineState, isOnlineState)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export enum MonitoringSortField {
|
|
||||||
ID = "id",
|
|
||||||
HOSTNAME = "hostname",
|
|
||||||
MAC = "mac",
|
|
||||||
SITE = "site",
|
|
||||||
DOMAIN = "domain",
|
|
||||||
MONITORING_STATE = "monitoring_state",
|
|
||||||
STATE = "state",
|
|
||||||
LAST_SEEN = "last_seen",
|
|
||||||
IMPORT_TIMESTAMP = "import_timestamp",
|
|
||||||
LAST_STATUS_MAIL_TYPE = "last_status_mail_type",
|
|
||||||
LAST_STATUS_MAIL_SENT = "last_status_mail_sent",
|
|
||||||
CREATED_AT = "created_at",
|
|
||||||
MODIFIED_AT = "modified_at",
|
|
||||||
}
|
|
||||||
|
|
||||||
export const isMonitoringSortField = toIsEnum(MonitoringSortField);
|
|
||||||
|
|
||||||
export enum TaskSortField {
|
|
||||||
ID = "id",
|
|
||||||
NAME = "name",
|
|
||||||
SCHEDULE = "schedule",
|
|
||||||
STATE = "state",
|
|
||||||
RUNNING_SINCE = "runningSince",
|
|
||||||
LAST_RUN_STARTED = "lastRunStarted",
|
|
||||||
}
|
|
||||||
|
|
||||||
export const isTaskSortField = toIsEnum(TaskSortField);
|
|
||||||
|
|
||||||
export enum MailSortField {
|
|
||||||
ID = "id",
|
|
||||||
FAILURES = "failures",
|
|
||||||
SENDER = "sender",
|
|
||||||
RECIPIENT = "recipient",
|
|
||||||
EMAIL = "email",
|
|
||||||
CREATED_AT = "created_at",
|
|
||||||
MODIFIED_AT = "modified_at",
|
|
||||||
}
|
|
||||||
|
|
||||||
export const isMailSortField = toIsEnum(MailSortField);
|
|
||||||
|
|
||||||
export type GenericSortField = {
|
|
||||||
value: string;
|
|
||||||
readonly __tag: unique symbol;
|
|
||||||
};
|
|
||||||
|
|
||||||
export enum SortDirection {
|
|
||||||
ASCENDING = "ASC",
|
|
||||||
DESCENDING = "DESC",
|
|
||||||
}
|
|
||||||
|
|
||||||
export const isSortDirection = toIsEnum(SortDirection);
|
|
||||||
|
|
72
server/shared/types/json.ts
Normal file
72
server/shared/types/json.ts
Normal file
|
@ -0,0 +1,72 @@
|
||||||
|
/**
|
||||||
|
* Contains types and type guard for representing JSON values.
|
||||||
|
*/
|
||||||
|
import { isBoolean, isNull, isNumber, isString } from "./primitives";
|
||||||
|
import { toIsArray } from "./arrays";
|
||||||
|
import { isPlainObject } from "./objects";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Shorthand type alias representing a JSON value.
|
||||||
|
*/
|
||||||
|
export type JSONValue =
|
||||||
|
| null
|
||||||
|
| string
|
||||||
|
| number
|
||||||
|
| boolean
|
||||||
|
| JSONObject
|
||||||
|
| JSONArray;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Type guard checking the given value is a valid {@link JSONValue}.
|
||||||
|
*
|
||||||
|
* @param arg - Value to check.
|
||||||
|
*/
|
||||||
|
export function isJSONValue(arg: unknown): arg is JSONValue {
|
||||||
|
return (
|
||||||
|
isNull(arg) ||
|
||||||
|
isString(arg) ||
|
||||||
|
isNumber(arg) ||
|
||||||
|
isBoolean(arg) ||
|
||||||
|
isJSONObject(arg) ||
|
||||||
|
isJSONArray(arg)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Type representing a JSON object of `string` keys and values of type {@link JSONValue}.
|
||||||
|
*/
|
||||||
|
export interface JSONObject {
|
||||||
|
[x: string]: JSONValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Type guard checking the given value is a valid {@link JSONObject}.
|
||||||
|
*
|
||||||
|
* @param arg - Value to check.
|
||||||
|
*/
|
||||||
|
export function isJSONObject(arg: unknown): arg is JSONObject {
|
||||||
|
if (!isPlainObject(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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Shorthand type alias representing a JSON array with elements of type {@link JSONValue}.
|
||||||
|
*/
|
||||||
|
export type JSONArray = Array<JSONValue>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Type guard checking the given value is a valid {@link JSONArray}.
|
||||||
|
*
|
||||||
|
* @param arg - Value to check.
|
||||||
|
*/
|
||||||
|
export const isJSONArray = toIsArray(isJSONValue);
|
12
server/shared/types/maps.ts
Normal file
12
server/shared/types/maps.ts
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
/**
|
||||||
|
* Contains type guards for regular expressions.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Type guard for {@link Map}s.
|
||||||
|
*
|
||||||
|
* @param arg - Value to check.
|
||||||
|
*/
|
||||||
|
export function isMap(arg: unknown): arg is Map<unknown, unknown> {
|
||||||
|
return arg instanceof Map;
|
||||||
|
}
|
338
server/shared/types/monitoring.ts
Normal file
338
server/shared/types/monitoring.ts
Normal file
|
@ -0,0 +1,338 @@
|
||||||
|
/**
|
||||||
|
* Contains types and type guards for monitoring data.
|
||||||
|
*/
|
||||||
|
import {
|
||||||
|
Domain,
|
||||||
|
isDomain,
|
||||||
|
isMAC,
|
||||||
|
isSite,
|
||||||
|
MAC,
|
||||||
|
Site,
|
||||||
|
toIsNewtype,
|
||||||
|
} from "./newtypes";
|
||||||
|
import { isBoolean, isNumber, isString } from "./primitives";
|
||||||
|
import { toIsEnum } from "./enums";
|
||||||
|
import { Hostname, isHostname, isMapId, MapId } from "./node";
|
||||||
|
import { EmailAddress, isEmailAddress, isMailType, MailType } from "./email";
|
||||||
|
import { isUnixTimestampSeconds, UnixTimestampSeconds } from "./time";
|
||||||
|
import { isOptional } from "./helpers";
|
||||||
|
import { SortFieldFor, toIsSortField } from "./sortfields";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Token for activating monitoring of a Freifunk node. This is being sent to verify the email address to use.
|
||||||
|
*/
|
||||||
|
export type MonitoringToken = string & { readonly __tag: unique symbol };
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Type guard for {@link MonitoringToken}.
|
||||||
|
*
|
||||||
|
* @param arg - Value to check.
|
||||||
|
*/
|
||||||
|
export const isMonitoringToken = toIsNewtype(isString, "" as MonitoringToken);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The different states of monitoring of a Freifunk node.
|
||||||
|
*/
|
||||||
|
export enum MonitoringState {
|
||||||
|
/**
|
||||||
|
* The node's online state is being actively monitored. If the node goes offline for a certain period of time
|
||||||
|
* a notification email will be sent.
|
||||||
|
*/
|
||||||
|
ACTIVE = "active",
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Monitoring has been activated by the user, but the email address used is not yet verified.
|
||||||
|
*/
|
||||||
|
PENDING = "pending",
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Monitoring is disabled.
|
||||||
|
*/
|
||||||
|
DISABLED = "disabled",
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Type guard for {@link MonitoringState}.
|
||||||
|
*
|
||||||
|
* @param arg - Value to check.
|
||||||
|
*/
|
||||||
|
export const isMonitoringState = toIsEnum(MonitoringState);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Online state of a Freifunk node.
|
||||||
|
*/
|
||||||
|
export enum OnlineState {
|
||||||
|
/**
|
||||||
|
* The node is currently online.
|
||||||
|
*/
|
||||||
|
ONLINE = "ONLINE",
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The node is currently offline.
|
||||||
|
*/
|
||||||
|
OFFLINE = "OFFLINE",
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Type guard for {@link OnlineState}.
|
||||||
|
*
|
||||||
|
* @param arg - Value to check.
|
||||||
|
*/
|
||||||
|
export const isOnlineState = toIsEnum(OnlineState);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Data of a Freifunk node as it is provided by the server's API when changing the nodes monitoring state.
|
||||||
|
*/
|
||||||
|
export type MonitoringResponse = {
|
||||||
|
/**
|
||||||
|
* Hostname of the node.
|
||||||
|
*/
|
||||||
|
hostname: Hostname;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* MAC address of the node.
|
||||||
|
*/
|
||||||
|
mac: MAC;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Email address that is being used for monitoring.
|
||||||
|
*/
|
||||||
|
email: EmailAddress;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether monitoring is enabled.
|
||||||
|
*/
|
||||||
|
monitoring: boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether the email address has been confirmed for use in monitoring the node.
|
||||||
|
*/
|
||||||
|
monitoringConfirmed: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Type guard for {@link MonitoringResponse}.
|
||||||
|
*
|
||||||
|
* @param arg - Value to check.
|
||||||
|
*/
|
||||||
|
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)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ID of the monitoring data of a Freifunk node stored in the database.
|
||||||
|
*/
|
||||||
|
export type NodeStateId = number & { readonly __tag: unique symbol };
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Type guard for {@link NodeStateId}.
|
||||||
|
*
|
||||||
|
* @param arg - Value to check.
|
||||||
|
*/
|
||||||
|
export const isNodeStateId = toIsNewtype(isNumber, NaN as NodeStateId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Monitoring related data of a Freifunk node as it is provided by the server's API to the admin frontend.
|
||||||
|
*/
|
||||||
|
export type NodeMonitoringStateResponse = {
|
||||||
|
/**
|
||||||
|
* ID of the monitoring data stored in the database.
|
||||||
|
*/
|
||||||
|
id: NodeStateId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Time the monitoring data has first been stored.
|
||||||
|
*/
|
||||||
|
created_at: UnixTimestampSeconds;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Time the monitoring data has last been updated.
|
||||||
|
*/
|
||||||
|
modified_at: UnixTimestampSeconds;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hostname of the Freifunk node.
|
||||||
|
*/
|
||||||
|
hostname?: Hostname;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* MAC address of the Freifunk node.
|
||||||
|
*/
|
||||||
|
mac: MAC;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ID to identify a Freifunk node on the communities map.
|
||||||
|
*/
|
||||||
|
mapId: MapId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Freifunk site as specified in the community map's `nodes.json`.
|
||||||
|
*/
|
||||||
|
site?: Site;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Freifunk domain as specified in the community map's `nodes.json`.
|
||||||
|
*/
|
||||||
|
domain?: Domain;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Time data for the node has last been imported from the community map's `nodes.json`.
|
||||||
|
*/
|
||||||
|
import_timestamp: UnixTimestampSeconds;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Time the node has last been seen online.
|
||||||
|
*/
|
||||||
|
last_seen: UnixTimestampSeconds;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Time the last monitoring notification email has been sent, if any.
|
||||||
|
*/
|
||||||
|
last_status_mail_sent?: UnixTimestampSeconds;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Type of the last monitoring notification email sent, if any.
|
||||||
|
*/
|
||||||
|
last_status_mail_type?: MailType;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Monitoring state of the node.
|
||||||
|
*/
|
||||||
|
monitoring_state?: MonitoringState;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Online state of the node.
|
||||||
|
*/
|
||||||
|
state: OnlineState;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Type guard for {@link NodeMonitoringStateResponse}.
|
||||||
|
*
|
||||||
|
* @param arg - Value to check.
|
||||||
|
*/
|
||||||
|
export function isNodeMonitoringStateResponse(
|
||||||
|
arg: unknown
|
||||||
|
): arg is NodeMonitoringStateResponse {
|
||||||
|
if (!Object(arg)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const response = arg as NodeMonitoringStateResponse;
|
||||||
|
return (
|
||||||
|
isNodeStateId(response.id) &&
|
||||||
|
isUnixTimestampSeconds(response.created_at) &&
|
||||||
|
isOptional(response.domain, isDomain) &&
|
||||||
|
isOptional(response.hostname, isHostname) &&
|
||||||
|
isUnixTimestampSeconds(response.import_timestamp) &&
|
||||||
|
isUnixTimestampSeconds(response.last_seen) &&
|
||||||
|
isOptional(response.last_status_mail_sent, isUnixTimestampSeconds) &&
|
||||||
|
isOptional(response.last_status_mail_type, isMailType) &&
|
||||||
|
isMAC(response.mac) &&
|
||||||
|
isUnixTimestampSeconds(response.modified_at) &&
|
||||||
|
isOptional(response.monitoring_state, isMonitoringState) &&
|
||||||
|
isOptional(response.site, isSite) &&
|
||||||
|
isOnlineState(response.state) &&
|
||||||
|
isMapId(response.mapId)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enum specifying the allowed sort fields when retrieving the list of monitoring data via the REST API.
|
||||||
|
*/
|
||||||
|
export enum MonitoringSortFieldEnum {
|
||||||
|
// noinspection JSUnusedGlobalSymbols
|
||||||
|
/**
|
||||||
|
* See {@link NodeMonitoringStateResponse.id}.
|
||||||
|
*/
|
||||||
|
ID = "id",
|
||||||
|
|
||||||
|
/**
|
||||||
|
* See {@link NodeMonitoringStateResponse.hostname}.
|
||||||
|
*/
|
||||||
|
HOSTNAME = "hostname",
|
||||||
|
|
||||||
|
/**
|
||||||
|
* See {@link NodeMonitoringStateResponse.mac}.
|
||||||
|
*/
|
||||||
|
MAC = "mac",
|
||||||
|
|
||||||
|
/**
|
||||||
|
* See {@link NodeMonitoringStateResponse.site}.
|
||||||
|
*/
|
||||||
|
SITE = "site",
|
||||||
|
|
||||||
|
/**
|
||||||
|
* See {@link NodeMonitoringStateResponse.domain}.
|
||||||
|
*/
|
||||||
|
DOMAIN = "domain",
|
||||||
|
|
||||||
|
/**
|
||||||
|
* See {@link NodeMonitoringStateResponse.monitoring_state}.
|
||||||
|
*/
|
||||||
|
MONITORING_STATE = "monitoring_state",
|
||||||
|
|
||||||
|
/**
|
||||||
|
* See {@link NodeMonitoringStateResponse.state}.
|
||||||
|
*/
|
||||||
|
STATE = "state",
|
||||||
|
|
||||||
|
/**
|
||||||
|
* See {@link NodeMonitoringStateResponse.last_seen}.
|
||||||
|
*/
|
||||||
|
LAST_SEEN = "last_seen",
|
||||||
|
|
||||||
|
/**
|
||||||
|
* See {@link NodeMonitoringStateResponse.import_timestamp}.
|
||||||
|
*/
|
||||||
|
IMPORT_TIMESTAMP = "import_timestamp",
|
||||||
|
|
||||||
|
/**
|
||||||
|
* See {@link NodeMonitoringStateResponse.last_status_mail_type}.
|
||||||
|
*/
|
||||||
|
LAST_STATUS_MAIL_TYPE = "last_status_mail_type",
|
||||||
|
|
||||||
|
/**
|
||||||
|
* See {@link NodeMonitoringStateResponse.last_status_mail_sent}.
|
||||||
|
*/
|
||||||
|
LAST_STATUS_MAIL_SENT = "last_status_mail_sent",
|
||||||
|
|
||||||
|
/**
|
||||||
|
* See {@link NodeMonitoringStateResponse.created_at}.
|
||||||
|
*/
|
||||||
|
CREATED_AT = "created_at",
|
||||||
|
|
||||||
|
/**
|
||||||
|
* See {@link NodeMonitoringStateResponse.modified_at}.
|
||||||
|
*/
|
||||||
|
MODIFIED_AT = "modified_at",
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Allowed sort fields when retrieving the list of monitoring data via the REST API.
|
||||||
|
*/
|
||||||
|
export type MonitoringSortField = SortFieldFor<
|
||||||
|
NodeMonitoringStateResponse,
|
||||||
|
MonitoringSortFieldEnum
|
||||||
|
>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Type guard for {@link MonitoringSortField}.
|
||||||
|
*
|
||||||
|
* @param arg - Value to check.
|
||||||
|
*/
|
||||||
|
export const isMonitoringSortField = toIsSortField<
|
||||||
|
NodeMonitoringStateResponse,
|
||||||
|
MonitoringSortFieldEnum,
|
||||||
|
typeof MonitoringSortFieldEnum,
|
||||||
|
MonitoringSortField
|
||||||
|
>(MonitoringSortFieldEnum);
|
142
server/shared/types/newtypes.ts
Normal file
142
server/shared/types/newtypes.ts
Normal file
|
@ -0,0 +1,142 @@
|
||||||
|
/**
|
||||||
|
* Contains type guards for newtypes. Newtypes are a way to strongly type strings, numbers, ...
|
||||||
|
*
|
||||||
|
* This is inspired by the tagged intersection types in
|
||||||
|
* {@link https://kubyshkin.name/posts/newtype-in-typescript/}.
|
||||||
|
*
|
||||||
|
* Also holds newtype definitions that don't fit elsewhere.
|
||||||
|
*/
|
||||||
|
import { TypeGuard } from "./helpers";
|
||||||
|
import { isString } from "./primitives";
|
||||||
|
|
||||||
|
// =====================================================================================================================
|
||||||
|
// General newtype helpers.
|
||||||
|
// =====================================================================================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper function to generate type guards for newtypes of type `<Newtype>`.
|
||||||
|
*
|
||||||
|
* Newtypes can be defined as follows:
|
||||||
|
*
|
||||||
|
* @param isValue - Typeguard to check for the value-type (`<ValueType>`) of the newtype.
|
||||||
|
* @param example - An example value of type `<Newtype>`.
|
||||||
|
* @returns A type guard for `<Newtype>`.
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* type StringNewtype = string & { readonly __tag: unique symbol };
|
||||||
|
* const isStringNewtype = toIsNewtype(isString, "" as StringNewtype);
|
||||||
|
*
|
||||||
|
* type NumberNewtype = number & { readonly __tag: unique symbol };
|
||||||
|
* const isNumberNewtype = toIsNewtype(isNumber, NaN as NumberNewtype);
|
||||||
|
*/
|
||||||
|
// noinspection JSUnusedLocalSymbols
|
||||||
|
export function toIsNewtype<
|
||||||
|
Newtype extends ValueType & { readonly __tag: symbol },
|
||||||
|
ValueType
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
|
>(isValue: TypeGuard<ValueType>, example: Newtype): TypeGuard<Newtype> {
|
||||||
|
return (arg: unknown): arg is Newtype => isValue(arg);
|
||||||
|
}
|
||||||
|
|
||||||
|
// =====================================================================================================================
|
||||||
|
// Newtype definitions.
|
||||||
|
// =====================================================================================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Version of ffffng.
|
||||||
|
*/
|
||||||
|
export type Version = string & { readonly __tag: unique symbol };
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Type guard for {@link Version}.
|
||||||
|
*
|
||||||
|
* @param arg - Value to check.
|
||||||
|
*/
|
||||||
|
export const isVersion = toIsNewtype(isString, "" as Version);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Typesafe string representation of URLs.
|
||||||
|
*
|
||||||
|
* Note: Not to be confused with Javascript's own {@link URL} type.
|
||||||
|
*/
|
||||||
|
export type Url = string & { readonly __tag: unique symbol };
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Type guard for {@link Url}.
|
||||||
|
*
|
||||||
|
* @param arg - Value to check.
|
||||||
|
*/
|
||||||
|
export const isUrl = toIsNewtype(isString, "" as Url);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fastd VPN key of a Freifunk node. This is the key used by the node to open a VPN tunnel to Freifunk gateways.
|
||||||
|
*/
|
||||||
|
export type FastdKey = string & { readonly __tag: unique symbol };
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Type guard for {@link FastdKey}.
|
||||||
|
*
|
||||||
|
* @param arg - Value to check.
|
||||||
|
*/
|
||||||
|
export const isFastdKey = toIsNewtype(isString, "" as FastdKey);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A MAC address.
|
||||||
|
*/
|
||||||
|
export type MAC = string & { readonly __tag: unique symbol };
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Type guard for {@link MAC}.
|
||||||
|
*
|
||||||
|
* @param arg - Value to check.
|
||||||
|
*/
|
||||||
|
export const isMAC = toIsNewtype(isString, "" as MAC);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* String representing geo coordinates. Latitude and longitude are delimited by exactly one whitespace.
|
||||||
|
* E.g.: <code>"53.565278 10.001389"</code>
|
||||||
|
*/
|
||||||
|
export type Coordinates = string & { readonly __tag: unique symbol };
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Type guard for {@link Coordinates}.
|
||||||
|
*
|
||||||
|
* @param arg - Value to check.
|
||||||
|
*/
|
||||||
|
export const isCoordinates = toIsNewtype(isString, "" as Coordinates);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* String representation of contact's name / nickname.
|
||||||
|
*/
|
||||||
|
export type Nickname = string & { readonly __tag: unique symbol };
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Type guard for {@link Nickname}.
|
||||||
|
*
|
||||||
|
* @param arg - Value to check.
|
||||||
|
*/
|
||||||
|
export const isNickname = toIsNewtype(isString, "" as Nickname);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Freifunk site as specified in the community map's `nodes.json`.
|
||||||
|
*/
|
||||||
|
export type Site = string & { readonly __tag: unique symbol };
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Type guard for {@link Site}.
|
||||||
|
*
|
||||||
|
* @param arg - Value to check.
|
||||||
|
*/
|
||||||
|
export const isSite = toIsNewtype(isString, "" as Site);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Freifunk domain as specified in the community map's `nodes.json`.
|
||||||
|
*/
|
||||||
|
export type Domain = string & { readonly __tag: unique symbol };
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Type guard for {@link Domain}.
|
||||||
|
*
|
||||||
|
* @param arg - Value to check.
|
||||||
|
*/
|
||||||
|
export const isDomain = toIsNewtype(isString, "" as Domain);
|
483
server/shared/types/node.ts
Normal file
483
server/shared/types/node.ts
Normal file
|
@ -0,0 +1,483 @@
|
||||||
|
/**
|
||||||
|
* Contains types and type guards for representing Freifunk nodes in various states.
|
||||||
|
*/
|
||||||
|
import { isObject } from "./objects";
|
||||||
|
import { isOptional } from "./helpers";
|
||||||
|
import {
|
||||||
|
Coordinates,
|
||||||
|
Domain,
|
||||||
|
FastdKey,
|
||||||
|
isCoordinates,
|
||||||
|
isDomain,
|
||||||
|
isFastdKey,
|
||||||
|
isMAC,
|
||||||
|
isNickname,
|
||||||
|
isSite,
|
||||||
|
MAC,
|
||||||
|
Nickname,
|
||||||
|
Site,
|
||||||
|
toIsNewtype,
|
||||||
|
} from "./newtypes";
|
||||||
|
import { isBoolean, isString } from "./primitives";
|
||||||
|
import { SortFieldFor, toIsSortField } from "./sortfields";
|
||||||
|
import { EmailAddress, isEmailAddress } from "./email";
|
||||||
|
import { isUnixTimestampSeconds, UnixTimestampSeconds } from "./time";
|
||||||
|
import {
|
||||||
|
isMonitoringState,
|
||||||
|
isOnlineState,
|
||||||
|
MonitoringState,
|
||||||
|
OnlineState,
|
||||||
|
} from "./monitoring";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ID of a node in the context of the `nodes.json` of the Freifunk community's node map.
|
||||||
|
*
|
||||||
|
* This is typically the nodes lowercase MAC address without any delimiters.
|
||||||
|
*/
|
||||||
|
export type NodeId = string & { readonly __tag: unique symbol };
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Type guard for {@link NodeId}.
|
||||||
|
*
|
||||||
|
* @param arg - Value to check.
|
||||||
|
*/
|
||||||
|
export const isNodeId = toIsNewtype(isString, "" as NodeId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Token of a Freifunk node registered with ffffng. This is being used to authorize a user to delete or modify the
|
||||||
|
* data being stored for a node.
|
||||||
|
*
|
||||||
|
* This token should be kept secret by the owner of the node.
|
||||||
|
*/
|
||||||
|
export type Token = string & { readonly __tag: unique symbol };
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Type guard for {@link Token}.
|
||||||
|
*
|
||||||
|
* @param arg - Value to check.
|
||||||
|
*/
|
||||||
|
export const isToken = toIsNewtype(isString, "" as Token);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Representation of a Freifunk node's hostname.
|
||||||
|
*/
|
||||||
|
export type Hostname = string & { readonly __tag: unique symbol };
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Type guard for {@link Hostname}.
|
||||||
|
*
|
||||||
|
* @param arg - Value to check.
|
||||||
|
*/
|
||||||
|
export const isHostname = toIsNewtype(isString, "" as Hostname);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ID to identify a Freifunk node on the communities map.
|
||||||
|
*/
|
||||||
|
export type MapId = string & { readonly __tag: unique symbol };
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Type guard for {@link MapId}.
|
||||||
|
*
|
||||||
|
* @param arg - Value to check.
|
||||||
|
*/
|
||||||
|
export const isMapId = toIsNewtype(isString, "" as MapId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Most basic information of a Freifunk Node.
|
||||||
|
*/
|
||||||
|
export type BaseNode = {
|
||||||
|
/**
|
||||||
|
* Name / nickname that should be used to contact the node owner.
|
||||||
|
*/
|
||||||
|
nickname: Nickname;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Email address that should be used to contact the node owner.
|
||||||
|
*/
|
||||||
|
email: EmailAddress;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hostname of the node that will be displayed on the community's node map.
|
||||||
|
*/
|
||||||
|
hostname: Hostname;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Optional coordinates of the node to position it on the community's node map.
|
||||||
|
*/
|
||||||
|
coords: Coordinates | undefined;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Optional fastd key of the node. This is the key used by the node to open a VPN tunnel to Freifunk gateways.
|
||||||
|
*/
|
||||||
|
key: FastdKey | undefined;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* MAC address of the node. This MAC address is used to identify the node at various places, e.g. when retrieving
|
||||||
|
* information of the node from the `nodes.json` of the communities node map.
|
||||||
|
*/
|
||||||
|
mac: MAC;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Type guard for {@link BaseNode}.
|
||||||
|
*
|
||||||
|
* @param arg - Value to check.
|
||||||
|
*/
|
||||||
|
export function isBaseNode(arg: unknown): arg is BaseNode {
|
||||||
|
if (!isObject(arg)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const node = arg as BaseNode;
|
||||||
|
return (
|
||||||
|
isNickname(node.nickname) &&
|
||||||
|
isEmailAddress(node.email) &&
|
||||||
|
isHostname(node.hostname) &&
|
||||||
|
isOptional(node.coords, isCoordinates) &&
|
||||||
|
isOptional(node.key, isFastdKey) &&
|
||||||
|
isMAC(node.mac)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Node data used when creating or updating a node.
|
||||||
|
*/
|
||||||
|
export type CreateOrUpdateNode = BaseNode & {
|
||||||
|
/**
|
||||||
|
* Whether to monitor the nodes online state and notify its owner when it's offline for a longer period of time.
|
||||||
|
*/
|
||||||
|
monitoring: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Type guard for {@link CreateOrUpdateNode}.
|
||||||
|
*
|
||||||
|
* @param arg - Value to check.
|
||||||
|
*/
|
||||||
|
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 Freifunk node as it is stored on the server.
|
||||||
|
*/
|
||||||
|
export type StoredNode = BaseNode & {
|
||||||
|
/**
|
||||||
|
* Token used to authorize a user to delete or modify the data being stored for a node.
|
||||||
|
*
|
||||||
|
* This token should be kept secret by the owner of the node.
|
||||||
|
*/
|
||||||
|
token: Token;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* State of the online monitoring for this node.
|
||||||
|
*
|
||||||
|
* See {@link MonitoringState}.
|
||||||
|
*/
|
||||||
|
monitoringState: MonitoringState;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Last time the node data has been updated on the server.
|
||||||
|
*/
|
||||||
|
modifiedAt: UnixTimestampSeconds;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Type guard for {@link StoredNode}.
|
||||||
|
*
|
||||||
|
* @param arg - Value to check.
|
||||||
|
*/
|
||||||
|
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)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Data of a Freifunk node as it is provided by the server's API.
|
||||||
|
*/
|
||||||
|
export type NodeResponse = StoredNode & {
|
||||||
|
/**
|
||||||
|
* Whether the node's online state should be monitored.
|
||||||
|
*/
|
||||||
|
monitoring: boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Specifies if the node owner has clicked the email confirmation link to enable monitoring of the online state.
|
||||||
|
*/
|
||||||
|
monitoringConfirmed: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Type guard for {@link NodeResponse}.
|
||||||
|
*
|
||||||
|
* @param arg - Value to check.
|
||||||
|
*/
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Data of a Freifunk node as it is provided by the server's API also providing the token to authorize node owners.
|
||||||
|
*/
|
||||||
|
export type NodeTokenResponse = {
|
||||||
|
/**
|
||||||
|
* Token used to authorize a user to delete or modify the data being stored for a node.
|
||||||
|
*
|
||||||
|
* This token should be kept secret by the owner of the node.
|
||||||
|
*/
|
||||||
|
token: Token;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Data of the Freifunk node. See {@link NodeResponse}.
|
||||||
|
*/
|
||||||
|
node: NodeResponse;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Type guard for {@link NodeTokenResponse}.
|
||||||
|
*
|
||||||
|
* @param arg - Value to check.
|
||||||
|
*/
|
||||||
|
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
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents a node in the context of a Freifunk site and domain.
|
||||||
|
*/
|
||||||
|
export type DomainSpecificNodeResponse = NodeResponse & {
|
||||||
|
/**
|
||||||
|
* Freifunk site the node resides in or `undefined` if unknown.
|
||||||
|
*/
|
||||||
|
site: Site | undefined;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Freifunk domain the node resides in or `undefined` if unknown.
|
||||||
|
*/
|
||||||
|
domain: Domain | undefined;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Online state of the Freifunk node or `undefined` if unknown.
|
||||||
|
*/
|
||||||
|
onlineState: OnlineState | undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Type guard for {@link DomainSpecificNodeResponse}.
|
||||||
|
*
|
||||||
|
* @param arg - Value to check.
|
||||||
|
*/
|
||||||
|
export function isDomainSpecificNodeResponse(
|
||||||
|
arg: unknown
|
||||||
|
): arg is DomainSpecificNodeResponse {
|
||||||
|
if (!isNodeResponse(arg)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const node = arg as DomainSpecificNodeResponse;
|
||||||
|
return (
|
||||||
|
isOptional(node.site, isSite) &&
|
||||||
|
isOptional(node.domain, isDomain) &&
|
||||||
|
isOptional(node.onlineState, isOnlineState)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enum specifying the allowed sort fields when retrieving the list of nodes via the REST API.
|
||||||
|
*/
|
||||||
|
export enum NodeSortFieldEnum {
|
||||||
|
// noinspection JSUnusedGlobalSymbols
|
||||||
|
|
||||||
|
/**
|
||||||
|
* See {@link BaseNode.hostname}.
|
||||||
|
*/
|
||||||
|
HOSTNAME = "hostname",
|
||||||
|
|
||||||
|
/**
|
||||||
|
* See {@link BaseNode.nickname}.
|
||||||
|
*/
|
||||||
|
NICKNAME = "nickname",
|
||||||
|
|
||||||
|
/**
|
||||||
|
* See {@link BaseNode.email}.
|
||||||
|
*/
|
||||||
|
EMAIL = "email",
|
||||||
|
|
||||||
|
/**
|
||||||
|
* See {@link StoredNode.token}.
|
||||||
|
*/
|
||||||
|
TOKEN = "token",
|
||||||
|
|
||||||
|
/**
|
||||||
|
* See {@link BaseNode.mac}.
|
||||||
|
*/
|
||||||
|
MAC = "mac",
|
||||||
|
|
||||||
|
/**
|
||||||
|
* See {@link BaseNode.key}.
|
||||||
|
*/
|
||||||
|
KEY = "key",
|
||||||
|
|
||||||
|
/**
|
||||||
|
* See {@link DomainSpecificNodeResponse.site}.
|
||||||
|
*/
|
||||||
|
SITE = "site",
|
||||||
|
|
||||||
|
/**
|
||||||
|
* See {@link DomainSpecificNodeResponse.domain}.
|
||||||
|
*/
|
||||||
|
DOMAIN = "domain",
|
||||||
|
|
||||||
|
/**
|
||||||
|
* See {@link BaseNode.coords}.
|
||||||
|
*/
|
||||||
|
COORDS = "coords",
|
||||||
|
|
||||||
|
/**
|
||||||
|
* See {@link DomainSpecificNodeResponse.onlineState}.
|
||||||
|
*/
|
||||||
|
ONLINE_STATE = "onlineState",
|
||||||
|
|
||||||
|
/**
|
||||||
|
* See {@link StoredNode.monitoringState}.
|
||||||
|
*/
|
||||||
|
MONITORING_STATE = "monitoringState",
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Allowed sort fields when retrieving the list of nodes via the REST API.
|
||||||
|
*/
|
||||||
|
export type NodeSortField = SortFieldFor<
|
||||||
|
DomainSpecificNodeResponse,
|
||||||
|
NodeSortFieldEnum
|
||||||
|
>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Type guard for {@link NodeSortField}.
|
||||||
|
*
|
||||||
|
* @param arg - Value to check.
|
||||||
|
*/
|
||||||
|
export const isNodeSortField = toIsSortField<
|
||||||
|
DomainSpecificNodeResponse,
|
||||||
|
NodeSortFieldEnum,
|
||||||
|
typeof NodeSortFieldEnum,
|
||||||
|
NodeSortField
|
||||||
|
>(NodeSortFieldEnum);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Allowed filters when retrieving the list of nodes via the REST API.
|
||||||
|
*/
|
||||||
|
export type NodesFilter = {
|
||||||
|
/**
|
||||||
|
* If set only nodes with / without a Fastd key will be returned.
|
||||||
|
*/
|
||||||
|
hasKey?: boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If set only nodes with / without geo coordinates will be returned.
|
||||||
|
*/
|
||||||
|
hasCoords?: boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If set only nodes having the given monitoring state will be returned.
|
||||||
|
*/
|
||||||
|
monitoringState?: MonitoringState;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If set only nodes belonging to the given Freifunk site will be returned.
|
||||||
|
*/
|
||||||
|
site?: Site;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If set only nodes belonging to the given Freifunk domain will be returned.
|
||||||
|
*/
|
||||||
|
domain?: Domain;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If set only nodes having the given online state will be returned.
|
||||||
|
*/
|
||||||
|
onlineState?: OnlineState;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Allowed filter fields when retrieving the list of nodes via the REST API.
|
||||||
|
*/
|
||||||
|
export const NODES_FILTER_FIELDS: Record<
|
||||||
|
keyof NodesFilter,
|
||||||
|
| BooleanConstructor
|
||||||
|
| StringConstructor
|
||||||
|
| typeof MonitoringState
|
||||||
|
| typeof OnlineState
|
||||||
|
> = {
|
||||||
|
/**
|
||||||
|
* See {@link NodesFilter.hasKey}.
|
||||||
|
*/
|
||||||
|
hasKey: Boolean,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* See {@link NodesFilter.hasCoords}.
|
||||||
|
*/
|
||||||
|
hasCoords: Boolean,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* See {@link NodesFilter.monitoringState}.
|
||||||
|
*/
|
||||||
|
monitoringState: MonitoringState,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* See {@link NodesFilter.site}.
|
||||||
|
*/
|
||||||
|
site: String,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* See {@link NodesFilter.domain}.
|
||||||
|
*/
|
||||||
|
domain: String,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* See {@link NodesFilter.onlineState}.
|
||||||
|
*/
|
||||||
|
onlineState: OnlineState,
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Type guard for {@link NodesFilter}.
|
||||||
|
*
|
||||||
|
* @param arg - Value to check.
|
||||||
|
*/
|
||||||
|
export function isNodesFilter(arg: unknown): arg is NodesFilter {
|
||||||
|
if (!isObject(arg)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const filter = arg as NodesFilter;
|
||||||
|
return (
|
||||||
|
isOptional(filter.hasKey, isBoolean) &&
|
||||||
|
isOptional(filter.hasCoords, isBoolean) &&
|
||||||
|
isOptional(filter.monitoringState, isMonitoringState) &&
|
||||||
|
isOptional(filter.site, isSite) &&
|
||||||
|
isOptional(filter.domain, isDomain) &&
|
||||||
|
isOptional(filter.onlineState, isOnlineState)
|
||||||
|
);
|
||||||
|
}
|
36
server/shared/types/objects.ts
Normal file
36
server/shared/types/objects.ts
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
/**
|
||||||
|
* Contains type guards for objects.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Type guard checking the given value is a non-null `object`.
|
||||||
|
*
|
||||||
|
* Warning: This is also true for e.g. arrays, so don't rely to heavily on this.
|
||||||
|
*
|
||||||
|
* @param arg - Value to check.
|
||||||
|
*/
|
||||||
|
export function isObject(arg: unknown): arg is object {
|
||||||
|
return arg !== null && typeof arg === "object";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Type guard checking the given value is a plain object (not an `array`).
|
||||||
|
*
|
||||||
|
* @param arg - Value to check.
|
||||||
|
*/
|
||||||
|
export function isPlainObject(arg: unknown): arg is { [key: string]: unknown } {
|
||||||
|
return isObject(arg) && !Array.isArray(arg);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Type guard checking if the given value is an object having the property specified by `key`.
|
||||||
|
*
|
||||||
|
* @param arg - Value to check.
|
||||||
|
* @param key - Key to index the property on the object.
|
||||||
|
*/
|
||||||
|
export function hasOwnProperty<Key extends PropertyKey>(
|
||||||
|
arg: unknown,
|
||||||
|
key: Key
|
||||||
|
): arg is Record<Key, unknown> {
|
||||||
|
return isObject(arg) && key in arg;
|
||||||
|
}
|
73
server/shared/types/primitives.ts
Normal file
73
server/shared/types/primitives.ts
Normal file
|
@ -0,0 +1,73 @@
|
||||||
|
/**
|
||||||
|
* Contains type guards for primitive types and values.
|
||||||
|
*/
|
||||||
|
|
||||||
|
// =====================================================================================================================
|
||||||
|
// Numbers
|
||||||
|
// =====================================================================================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Type guard for numbers.
|
||||||
|
*
|
||||||
|
* @param arg - Value to check.
|
||||||
|
*/
|
||||||
|
export function isNumber(arg: unknown): arg is number {
|
||||||
|
return typeof arg === "number";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Type guard checking the given value is an integer `number`.
|
||||||
|
*
|
||||||
|
* @param arg - Value to check.
|
||||||
|
*/
|
||||||
|
export function isInteger(arg: unknown): arg is number {
|
||||||
|
return isNumber(arg) && Number.isInteger(arg);
|
||||||
|
}
|
||||||
|
|
||||||
|
// =====================================================================================================================
|
||||||
|
// Strings
|
||||||
|
// =====================================================================================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Type guard for strings.
|
||||||
|
*
|
||||||
|
* @param arg - Value to check.
|
||||||
|
*/
|
||||||
|
export function isString(arg: unknown): arg is string {
|
||||||
|
return typeof arg === "string";
|
||||||
|
}
|
||||||
|
|
||||||
|
// =====================================================================================================================
|
||||||
|
// Booleans
|
||||||
|
// =====================================================================================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Type guard for booleans.
|
||||||
|
*
|
||||||
|
* @param arg - Value to check.
|
||||||
|
*/
|
||||||
|
export function isBoolean(arg: unknown): arg is boolean {
|
||||||
|
return typeof arg === "boolean";
|
||||||
|
}
|
||||||
|
|
||||||
|
// =====================================================================================================================
|
||||||
|
// Primitive values
|
||||||
|
// =====================================================================================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Type guard for null values.
|
||||||
|
*
|
||||||
|
* @param arg - Value to check.
|
||||||
|
*/
|
||||||
|
export function isNull(arg: unknown): arg is null {
|
||||||
|
return arg === null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Type guard for undefined values.
|
||||||
|
*
|
||||||
|
* @param arg - Value to check.
|
||||||
|
*/
|
||||||
|
export function isUndefined(arg: unknown): arg is undefined {
|
||||||
|
return typeof arg === "undefined";
|
||||||
|
}
|
13
server/shared/types/regexps.ts
Normal file
13
server/shared/types/regexps.ts
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
/**
|
||||||
|
* Contains type guards for regular expressions.
|
||||||
|
*/
|
||||||
|
import { isObject } from "./objects";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Type guard for {@link RegExp}s.
|
||||||
|
*
|
||||||
|
* @param arg - Value to check.
|
||||||
|
*/
|
||||||
|
export function isRegExp(arg: unknown): arg is RegExp {
|
||||||
|
return isObject(arg) && arg instanceof RegExp;
|
||||||
|
}
|
76
server/shared/types/sortfields.ts
Normal file
76
server/shared/types/sortfields.ts
Normal file
|
@ -0,0 +1,76 @@
|
||||||
|
/**
|
||||||
|
* Contains helper types and type guards for sort fields.
|
||||||
|
*/
|
||||||
|
import { Enum, toIsEnum } from "./enums";
|
||||||
|
import { isString } from "./primitives";
|
||||||
|
import { TypeGuard } from "./helpers";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generic untyped sort field.
|
||||||
|
*/
|
||||||
|
export type GenericSortField = {
|
||||||
|
value: string;
|
||||||
|
readonly __tag: unique symbol;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper to define sort field types that match an enum `<SortFieldEnum>` listing allowed values to a type
|
||||||
|
* `<SortableType>` being indexed by those values.
|
||||||
|
*
|
||||||
|
* In short: If the enum values don't match the sortable type keys the compiler will complain.
|
||||||
|
*/
|
||||||
|
export type SortFieldFor<
|
||||||
|
SortableType,
|
||||||
|
SortFieldEnum extends keyof SortableType
|
||||||
|
> = keyof Pick<SortableType, SortFieldEnum>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper function to construct a type guard for sort fields.
|
||||||
|
*
|
||||||
|
* Generic type parameters:
|
||||||
|
*
|
||||||
|
* * `<SortableType>` - The type to sort.
|
||||||
|
* * `<SortFieldEnum>` - The enum used to specify the field of `<SortableType>` to sort by.
|
||||||
|
* * `<SortFieldEnumType>` - The `typeof` the `<SortFieldEnum>`.
|
||||||
|
* * `<SortField>` - The type of the sort field.
|
||||||
|
*
|
||||||
|
* Warning: I could not get the compiler to ensure `<SortFieldEnum>` and `<SortFieldEnumType>` refer to the same enum!
|
||||||
|
*
|
||||||
|
* @param sortFieldEnumDef - Enum representing the allowed sort fields.
|
||||||
|
* @returns A type guard for sort fields of type `<SortField>`.
|
||||||
|
*/
|
||||||
|
export function toIsSortField<
|
||||||
|
SortableType,
|
||||||
|
SortFieldEnum extends keyof SortableType,
|
||||||
|
SortFieldEnumType extends Enum<SortFieldEnumType>,
|
||||||
|
SortField extends SortFieldFor<SortableType, SortFieldEnum>
|
||||||
|
>(sortFieldEnumDef: SortFieldEnumType): TypeGuard<SortField> {
|
||||||
|
return (arg: unknown): arg is SortField => {
|
||||||
|
if (!isString(arg)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return Object.values(sortFieldEnumDef).includes(arg as SortField);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Direction in which to sort.
|
||||||
|
*/
|
||||||
|
export enum SortDirection {
|
||||||
|
/**
|
||||||
|
* Sort in ascending order.
|
||||||
|
*/
|
||||||
|
ASCENDING = "ASC",
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sort in descending order.
|
||||||
|
*/
|
||||||
|
DESCENDING = "DESC",
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Type guard for {@link SortDirection}.
|
||||||
|
*
|
||||||
|
* @param arg - Value to check.
|
||||||
|
*/
|
||||||
|
export const isSortDirection = toIsEnum(SortDirection);
|
76
server/shared/types/statistics.ts
Normal file
76
server/shared/types/statistics.ts
Normal file
|
@ -0,0 +1,76 @@
|
||||||
|
/**
|
||||||
|
* Contains types and type guards for representing statistics information.
|
||||||
|
*/
|
||||||
|
import { isPlainObject } from "./objects";
|
||||||
|
import { isNumber } from "./primitives";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Some basic statistics of the known Freifunk nodes in the community.
|
||||||
|
*/
|
||||||
|
export type NodeStatistics = {
|
||||||
|
/**
|
||||||
|
* Number of nodes registered via ffffng.
|
||||||
|
*/
|
||||||
|
registered: number;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Number of nodes with {@link FastdKey}
|
||||||
|
*/
|
||||||
|
withVPN: number;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Number of nodes with geo-coordinates.
|
||||||
|
*/
|
||||||
|
withCoords: number;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Monitoring statistics.
|
||||||
|
*/
|
||||||
|
monitoring: {
|
||||||
|
/**
|
||||||
|
* Number of registered nodes with active monitoring.
|
||||||
|
*/
|
||||||
|
active: number;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Number of registered nodes with activated monitoring but pending email confirmation.
|
||||||
|
*/
|
||||||
|
pending: number;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Type guard for {@link NodeStatistics}.
|
||||||
|
*
|
||||||
|
* @param arg - Value to check.
|
||||||
|
*/
|
||||||
|
export function isNodeStatistics(arg: unknown): arg is NodeStatistics {
|
||||||
|
if (!isPlainObject(arg)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const stats = arg as NodeStatistics;
|
||||||
|
return (
|
||||||
|
isNumber(stats.registered) &&
|
||||||
|
isNumber(stats.withVPN) &&
|
||||||
|
isNumber(stats.withCoords) &&
|
||||||
|
isPlainObject(stats.monitoring) &&
|
||||||
|
isNumber(stats.monitoring.active) &&
|
||||||
|
isNumber(stats.monitoring.pending)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Statistics object wrapping {@link NodeStatistics} to be used a REST API response.
|
||||||
|
*/
|
||||||
|
export type Statistics = {
|
||||||
|
nodes: NodeStatistics;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Type guard for {@link Statistics}.
|
||||||
|
*
|
||||||
|
* @param arg - Value to check.
|
||||||
|
*/
|
||||||
|
export function isStatistics(arg: unknown): arg is Statistics {
|
||||||
|
return isPlainObject(arg) && isNodeStatistics((arg as Statistics).nodes);
|
||||||
|
}
|
207
server/shared/types/task.ts
Normal file
207
server/shared/types/task.ts
Normal file
|
@ -0,0 +1,207 @@
|
||||||
|
/**
|
||||||
|
* Contains types and type guards all around tasks.
|
||||||
|
*/
|
||||||
|
import { toIsEnum } from "./enums";
|
||||||
|
import { isNullable } from "./helpers";
|
||||||
|
import { isPlainObject } from "./objects";
|
||||||
|
import { isBoolean, isNumber, isString } from "./primitives";
|
||||||
|
import { SortFieldFor, toIsSortField } from "./sortfields";
|
||||||
|
import {
|
||||||
|
DurationSeconds,
|
||||||
|
isDurationSeconds,
|
||||||
|
isUnixTimestampSeconds,
|
||||||
|
UnixTimestampSeconds,
|
||||||
|
} from "./time";
|
||||||
|
|
||||||
|
// FIXME: Naming Task vs. Job
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The state a task can be in.
|
||||||
|
*/
|
||||||
|
export enum TaskState {
|
||||||
|
/**
|
||||||
|
* The task is not currently running.
|
||||||
|
*/
|
||||||
|
IDLE = "idle",
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The task is running.
|
||||||
|
*/
|
||||||
|
RUNNING = "running",
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The task is idle but has had a failure on its last run.
|
||||||
|
*/
|
||||||
|
FAILED = "failed",
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Type guard for {@link TaskState}.
|
||||||
|
*
|
||||||
|
* @param arg - Value to check.
|
||||||
|
*/
|
||||||
|
export const isTaskState = toIsEnum(TaskState);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* State the last run of the task resulted in.
|
||||||
|
*/
|
||||||
|
export enum JobResultState {
|
||||||
|
/**
|
||||||
|
* The run did finish as expected.
|
||||||
|
*/
|
||||||
|
OKAY = "okay",
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The run resulted in one or more warnings.
|
||||||
|
*/
|
||||||
|
WARNING = "warning",
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Type guard for {@link JobResultState}.
|
||||||
|
*
|
||||||
|
* @param arg - Value to check.
|
||||||
|
*/
|
||||||
|
export const isJobResultState = toIsEnum(JobResultState);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Task as returned by the REST API.
|
||||||
|
*/
|
||||||
|
// TODO: Introduce newtypes.
|
||||||
|
export type TaskResponse = {
|
||||||
|
/**
|
||||||
|
* ID of the task.
|
||||||
|
*/
|
||||||
|
id: number;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Task name as displayed in the admin panel.
|
||||||
|
*/
|
||||||
|
name: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A short description of what the task does.
|
||||||
|
*/
|
||||||
|
description: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The schedule of the task in classical cronjob notation.
|
||||||
|
*/
|
||||||
|
schedule: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Time the current run of this task started. `null` if the task is not running.
|
||||||
|
*/
|
||||||
|
runningSince: UnixTimestampSeconds | null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Time the last run of this task started. `null` if the task has not run before.
|
||||||
|
*/
|
||||||
|
lastRunStarted: UnixTimestampSeconds | null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Duration of the last run in seconds. `null` if the task has not run before.
|
||||||
|
*/
|
||||||
|
lastRunDuration: DurationSeconds | null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The state the task is in.
|
||||||
|
*/
|
||||||
|
state: TaskState;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* State the last run of the task resulted in.
|
||||||
|
*/
|
||||||
|
result: JobResultState | null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Message of the last run, e.g. a warning.
|
||||||
|
*/
|
||||||
|
message: string | null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether the task is enabled and therefor may run.
|
||||||
|
*
|
||||||
|
* Note: A task may be running even if it is disabled if the run started befor disabling it.
|
||||||
|
*/
|
||||||
|
enabled: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Type guard for {@link TaskResponse}.
|
||||||
|
*
|
||||||
|
* @param arg - Value to check.
|
||||||
|
*/
|
||||||
|
export function isTaskResponse(arg: unknown): arg is TaskResponse {
|
||||||
|
if (!isPlainObject(arg)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const task = arg as TaskResponse;
|
||||||
|
return (
|
||||||
|
isNumber(task.id) &&
|
||||||
|
isString(task.name) &&
|
||||||
|
isString(task.description) &&
|
||||||
|
isString(task.schedule) &&
|
||||||
|
isNullable(task.runningSince, isUnixTimestampSeconds) &&
|
||||||
|
isNullable(task.lastRunStarted, isUnixTimestampSeconds) &&
|
||||||
|
isNullable(task.lastRunDuration, isDurationSeconds) &&
|
||||||
|
isTaskState(task.state) &&
|
||||||
|
isNullable(task.result, isJobResultState) &&
|
||||||
|
isNullable(task.message, isString) &&
|
||||||
|
isBoolean(task.enabled)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enum specifying the allowed sort fields when retrieving the list of tasks via the REST API.
|
||||||
|
*/
|
||||||
|
export enum TaskSortFieldEnum {
|
||||||
|
// noinspection JSUnusedGlobalSymbols
|
||||||
|
/**
|
||||||
|
* See {@link TaskResponse.id}.
|
||||||
|
*/
|
||||||
|
ID = "id",
|
||||||
|
|
||||||
|
/**
|
||||||
|
* See {@link TaskResponse.name}.
|
||||||
|
*/
|
||||||
|
NAME = "name",
|
||||||
|
|
||||||
|
/**
|
||||||
|
* See {@link TaskResponse.schedule}.
|
||||||
|
*/
|
||||||
|
SCHEDULE = "schedule",
|
||||||
|
|
||||||
|
/**
|
||||||
|
* See {@link TaskResponse.state}.
|
||||||
|
*/
|
||||||
|
STATE = "state",
|
||||||
|
|
||||||
|
/**
|
||||||
|
* See {@link TaskResponse.runningSince}.
|
||||||
|
*/
|
||||||
|
RUNNING_SINCE = "runningSince",
|
||||||
|
|
||||||
|
/**
|
||||||
|
* See {@link TaskResponse.lastRunStarted}.
|
||||||
|
*/
|
||||||
|
LAST_RUN_STARTED = "lastRunStarted",
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Allowed sort fields when retrieving the list of tasks via the REST API.
|
||||||
|
*/
|
||||||
|
export type TaskSortField = SortFieldFor<TaskResponse, TaskSortFieldEnum>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Type guard for {@link TaskSortField}.
|
||||||
|
*
|
||||||
|
* @param arg - Value to check.
|
||||||
|
*/
|
||||||
|
export const isTaskSortField = toIsSortField<
|
||||||
|
TaskResponse,
|
||||||
|
TaskSortFieldEnum,
|
||||||
|
typeof TaskSortFieldEnum,
|
||||||
|
TaskSortField
|
||||||
|
>(TaskSortFieldEnum);
|
66
server/shared/types/time.ts
Normal file
66
server/shared/types/time.ts
Normal file
|
@ -0,0 +1,66 @@
|
||||||
|
/**
|
||||||
|
* Contains types and type guards for "wibbly wobbly timey wimey" stuff.
|
||||||
|
*/
|
||||||
|
import { toIsNewtype } from "./newtypes";
|
||||||
|
import { isNumber } from "./primitives";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Duration of a period of time in seconds.
|
||||||
|
*/
|
||||||
|
export type DurationSeconds = number & { readonly __tag: unique symbol };
|
||||||
|
|
||||||
|
/**
|
||||||
|
* UnixTimestampMilliseconds}.
|
||||||
|
*
|
||||||
|
* @param arg - Value to check.
|
||||||
|
*/
|
||||||
|
export const isDurationSeconds = toIsNewtype(isNumber, NaN as DurationSeconds);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Duration of a period of time in milliseconds.
|
||||||
|
*/
|
||||||
|
export type DurationMilliseconds = number & { readonly __tag: unique symbol };
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Type guard for {@UnixTimestampMilliseconds}.
|
||||||
|
*
|
||||||
|
* @param arg - Value to check.
|
||||||
|
*/
|
||||||
|
export const isDurationMilliseconds = toIsNewtype(
|
||||||
|
isNumber,
|
||||||
|
NaN as DurationMilliseconds
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Timestamp representing a point in time specified by the number of seconds passed
|
||||||
|
* since the 1970-01-01 at 0:00:00 UTC.
|
||||||
|
*/
|
||||||
|
export type UnixTimestampSeconds = number & { readonly __tag: unique symbol };
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Type guard for {@UnixTimestampMilliseconds}.
|
||||||
|
*
|
||||||
|
* @param arg - Value to check.
|
||||||
|
*/
|
||||||
|
export const isUnixTimestampSeconds = toIsNewtype(
|
||||||
|
isNumber,
|
||||||
|
NaN as UnixTimestampSeconds
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Timestamp representing a point in time specified by the number of milliseconds passed
|
||||||
|
* since the 1970-01-01 at 0:00:00 UTC.
|
||||||
|
*/
|
||||||
|
export type UnixTimestampMilliseconds = number & {
|
||||||
|
readonly __tag: unique symbol;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Type guard for {@link UnixTimestampMilliseconds}.
|
||||||
|
*
|
||||||
|
* @param arg - Value to check.
|
||||||
|
*/
|
||||||
|
export const isUnixTimestampMilliseconds = toIsNewtype(
|
||||||
|
isNumber,
|
||||||
|
NaN as UnixTimestampMilliseconds
|
||||||
|
);
|
30
server/shared/utils/enums.ts
Normal file
30
server/shared/utils/enums.ts
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
/**
|
||||||
|
* Utility functions for enums.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper function to detect unhandled enum fields in `switch` statements at compile time. In case this function
|
||||||
|
* is called at runtime anyway (which should not happen) it throws a runtime error.
|
||||||
|
*
|
||||||
|
* In the example below the compiler will complain if not for all fields of `Enum` a corresponding `case` statement
|
||||||
|
* exists.
|
||||||
|
*
|
||||||
|
* @param field - Unhandled field, the value being switched over.
|
||||||
|
* @throws {@link Error} - If the function is called at runtime.
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* switch (enumValue) {
|
||||||
|
* case Enum.FIELD1:
|
||||||
|
* return;
|
||||||
|
* case Enum.FIELD2:
|
||||||
|
* return;
|
||||||
|
*
|
||||||
|
* ...
|
||||||
|
*
|
||||||
|
* default:
|
||||||
|
* return unhandledEnumField(enumValue);
|
||||||
|
* }
|
||||||
|
*/
|
||||||
|
export function unhandledEnumField(field: never): never {
|
||||||
|
throw new Error(`Unhandled enum field: ${field}`);
|
||||||
|
}
|
42
server/shared/utils/json.ts
Normal file
42
server/shared/utils/json.ts
Normal file
|
@ -0,0 +1,42 @@
|
||||||
|
/**
|
||||||
|
* Utility functions for JSON.
|
||||||
|
*/
|
||||||
|
import { isJSONValue, JSONObject, JSONValue } from "../types";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parses the given `string` and converts it into a {@link JSONValue}.
|
||||||
|
*
|
||||||
|
* For the string to be considered valid JSON it has to satisfy the requirements for {@link JSON.parse}.
|
||||||
|
*
|
||||||
|
* @param str - `string` to parse.
|
||||||
|
* @returns The parsed integer JSON value.
|
||||||
|
* @throws {@link SyntaxError} - If the given `string` does not represent a valid JSON value.
|
||||||
|
*/
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes `undefined` fields from the given JSON'ish object to make it a valid {@link JSONObject}.
|
||||||
|
*
|
||||||
|
* Note: This only happens for fields directly belonging to the given object. No recursive cleanup is performed.
|
||||||
|
*
|
||||||
|
* @param obj - Object to remove `undefined` fields from.
|
||||||
|
* @returns Cleaned up JSON object.
|
||||||
|
*/
|
||||||
|
export function filterUndefinedFromJSON(obj: {
|
||||||
|
[key: string]: JSONValue | undefined;
|
||||||
|
}): JSONObject {
|
||||||
|
const result: JSONObject = {};
|
||||||
|
for (const [key, value] of Object.entries(obj)) {
|
||||||
|
if (value !== undefined) {
|
||||||
|
result[key] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
14
server/shared/utils/node.ts
Normal file
14
server/shared/utils/node.ts
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
/**
|
||||||
|
* Utility functions for node related data.
|
||||||
|
*/
|
||||||
|
import { MAC, MapId } from "../types";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts the MAC address of a Freifunk node to an id representing it on the community's node map.
|
||||||
|
*
|
||||||
|
* @param mac - MAC address of the node
|
||||||
|
* @returns ID of the node on the map
|
||||||
|
*/
|
||||||
|
export function mapIdFromMAC(mac: MAC): MapId {
|
||||||
|
return mac.toLowerCase().replace(/:/g, "") as MapId;
|
||||||
|
}
|
19
server/shared/utils/objects.ts
Normal file
19
server/shared/utils/objects.ts
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
/**
|
||||||
|
* Helper functions for objects.
|
||||||
|
*/
|
||||||
|
import { hasOwnProperty } from "../types";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If the given value is an object this function returns the property specified by `key` if it exists.
|
||||||
|
*
|
||||||
|
* @param arg - Value to treat as an object to look up the property.
|
||||||
|
* @param key - Key indexing the property.
|
||||||
|
* @returns The property of the given object indexed by `key` or `undefined` if `arg` is not an object
|
||||||
|
* or has no property `key`.
|
||||||
|
*/
|
||||||
|
export function getFieldIfExists(
|
||||||
|
arg: unknown,
|
||||||
|
key: PropertyKey
|
||||||
|
): unknown | undefined {
|
||||||
|
return hasOwnProperty(arg, key) ? arg[key] : undefined;
|
||||||
|
}
|
|
@ -1,9 +1,29 @@
|
||||||
import { isString, MAC } from "../types";
|
/**
|
||||||
|
* Utility functions all around strings.
|
||||||
|
*/
|
||||||
|
import { isInteger, MAC } from "../types";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Trims the given `string` and replaces multiple whitespaces by one space each.
|
||||||
|
*
|
||||||
|
* Can be used to make sure user input has a canonical form.
|
||||||
|
*
|
||||||
|
* @param str - `string` to normalize.
|
||||||
|
* @returns The normalized `string`.
|
||||||
|
*/
|
||||||
export function normalizeString(str: string): string {
|
export function normalizeString(str: string): string {
|
||||||
return isString(str) ? str.trim().replace(/\s+/g, " ") : str;
|
return str.trim().replace(/\s+/g, " ");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Normalizes a {@link MAC} address so that it has a canonical format:
|
||||||
|
*
|
||||||
|
* The `MAC` address will be converted so that it is all uppercase with colon as the delimiter, e.g.:
|
||||||
|
* `12:34:56:78:9A:BC`.
|
||||||
|
*
|
||||||
|
* @param mac - `MAC` address to normalize.
|
||||||
|
* @returns The normalized `MAC` address.
|
||||||
|
*/
|
||||||
export function normalizeMac(mac: MAC): MAC {
|
export function normalizeMac(mac: MAC): MAC {
|
||||||
// parts only contains values at odd indexes
|
// parts only contains values at odd indexes
|
||||||
const parts = mac
|
const parts = mac
|
||||||
|
@ -20,9 +40,26 @@ export function normalizeMac(mac: MAC): MAC {
|
||||||
return macParts.join(":") as MAC;
|
return macParts.join(":") as MAC;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parses the given `string` and converts it into an integer.
|
||||||
|
*
|
||||||
|
* For a `string` to be considered a valid representation of an integer `number` it has to satisfy the
|
||||||
|
* following criteria:
|
||||||
|
*
|
||||||
|
* * The integer is base `10`.
|
||||||
|
* * The `string` starts with an optional `+` or `-` sign followed by one or more digits.
|
||||||
|
* * The first digit must not be `0`.
|
||||||
|
* * The `string` does not contain any other characters.
|
||||||
|
*
|
||||||
|
* @param str - `string` to parse.
|
||||||
|
* @returns The parsed integer `number`.
|
||||||
|
* @throws {@link SyntaxError} - If the given `string` does not represent a valid integer.
|
||||||
|
*/
|
||||||
export function parseInteger(str: string): number {
|
export function parseInteger(str: string): number {
|
||||||
const parsed = parseInt(str, 10);
|
const parsed = parseInt(str, 10);
|
||||||
if (parsed.toString() === str) {
|
const original = str.startsWith("+") ? str.slice(1) : str;
|
||||||
|
|
||||||
|
if (isInteger(parsed) && parsed.toString() === original) {
|
||||||
return parsed;
|
return parsed;
|
||||||
} else {
|
} else {
|
||||||
throw new SyntaxError(
|
throw new SyntaxError(
|
||||||
|
|
28
server/shared/utils/time.ts
Normal file
28
server/shared/utils/time.ts
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
/**
|
||||||
|
* Utility functions for "wibbly wobbly timey wimey" stuff.
|
||||||
|
*/
|
||||||
|
import { UnixTimestampMilliseconds, UnixTimestampSeconds } from "../types";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts an {@link UnixTimestampMilliseconds} to an {@link UnixTimestampSeconds} rounding down.
|
||||||
|
*
|
||||||
|
* @param ms - The timestamp in milliseconds.
|
||||||
|
* @returns - The timestamp in seconds.
|
||||||
|
*/
|
||||||
|
export function toUnixTimestampSeconds(
|
||||||
|
ms: UnixTimestampMilliseconds
|
||||||
|
): UnixTimestampSeconds {
|
||||||
|
return Math.floor(ms / 1000) as UnixTimestampSeconds;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts an {@link UnixTimestampSeconds} to an {@link UnixTimestampMilliseconds}.
|
||||||
|
*
|
||||||
|
* @param s - The timestamp in seconds.
|
||||||
|
* @returns - The timestamp in milliseconds.
|
||||||
|
*/
|
||||||
|
export function toUnixTimestampMilliseconds(
|
||||||
|
s: UnixTimestampSeconds
|
||||||
|
): UnixTimestampMilliseconds {
|
||||||
|
return (s * 1000) as UnixTimestampMilliseconds;
|
||||||
|
}
|
|
@ -11,10 +11,7 @@ import {
|
||||||
} from "../shared/validation/validator";
|
} from "../shared/validation/validator";
|
||||||
import { Request, Response } from "express";
|
import { Request, Response } from "express";
|
||||||
import {
|
import {
|
||||||
EnumTypeGuard,
|
|
||||||
ValueOf,
|
|
||||||
type GenericSortField,
|
type GenericSortField,
|
||||||
getFieldIfExists,
|
|
||||||
isJSONObject,
|
isJSONObject,
|
||||||
isNumber,
|
isNumber,
|
||||||
isString,
|
isString,
|
||||||
|
@ -24,6 +21,7 @@ import {
|
||||||
SortDirection,
|
SortDirection,
|
||||||
TypeGuard,
|
TypeGuard,
|
||||||
} from "../types";
|
} from "../types";
|
||||||
|
import { getFieldIfExists } from "../shared/utils/objects";
|
||||||
|
|
||||||
export type RequestData = JSONObject;
|
export type RequestData = JSONObject;
|
||||||
export type RequestHandler = (request: Request, response: Response) => void;
|
export type RequestHandler = (request: Request, response: Response) => void;
|
||||||
|
@ -77,12 +75,12 @@ function respond(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function orderByClause<S>(
|
function orderByClause<SortField>(
|
||||||
restParams: RestParams,
|
restParams: RestParams,
|
||||||
defaultSortField: ValueOf<S>,
|
defaultSortField: SortField,
|
||||||
isSortField: EnumTypeGuard<S>
|
isSortField: TypeGuard<SortField>
|
||||||
): OrderByClause {
|
): OrderByClause {
|
||||||
let sortField: ValueOf<S> | undefined = isSortField(restParams._sortField)
|
let sortField: SortField | undefined = isSortField(restParams._sortField)
|
||||||
? restParams._sortField
|
? restParams._sortField
|
||||||
: undefined;
|
: undefined;
|
||||||
if (!sortField) {
|
if (!sortField) {
|
||||||
|
@ -345,13 +343,17 @@ export function getPageEntities<Entity>(
|
||||||
|
|
||||||
export { filterCondition as whereCondition };
|
export { filterCondition as whereCondition };
|
||||||
|
|
||||||
export function filterClause<S>(
|
export function filterClause<SortField>(
|
||||||
restParams: RestParams,
|
restParams: RestParams,
|
||||||
defaultSortField: ValueOf<S>,
|
defaultSortField: SortField,
|
||||||
isSortField: EnumTypeGuard<S>,
|
isSortField: TypeGuard<SortField>,
|
||||||
filterFields: string[]
|
filterFields: string[]
|
||||||
): FilterClause {
|
): FilterClause {
|
||||||
const orderBy = orderByClause<S>(restParams, defaultSortField, isSortField);
|
const orderBy = orderByClause<SortField>(
|
||||||
|
restParams,
|
||||||
|
defaultSortField,
|
||||||
|
isSortField
|
||||||
|
);
|
||||||
const limitOffset = limitOffsetClause(restParams);
|
const limitOffset = limitOffsetClause(restParams);
|
||||||
|
|
||||||
const filter = filterCondition(restParams, filterFields);
|
const filter = filterCondition(restParams, filterFields);
|
||||||
|
|
Loading…
Reference in a new issue