Merge branch 'main' into new-admin
This commit is contained in:
commit
7cb11259dc
37 changed files with 2960 additions and 1311 deletions
|
@ -20,9 +20,9 @@ import {
|
|||
isSortDirection,
|
||||
isString,
|
||||
type JSONValue,
|
||||
parseJSON,
|
||||
type TypeGuard,
|
||||
} from "@/types";
|
||||
import { parseJSON } from "@/shared/utils/json";
|
||||
|
||||
export interface Route {
|
||||
name: RouteName;
|
||||
|
|
|
@ -1,10 +1,13 @@
|
|||
module.exports = {
|
||||
preset: 'ts-jest',
|
||||
testEnvironment: 'node',
|
||||
roots: ['<rootDir>/server'],
|
||||
globals: {
|
||||
'ts-jest': {
|
||||
tsconfig: '<rootDir>/server/tsconfig.json'
|
||||
}
|
||||
}
|
||||
preset: "ts-jest",
|
||||
testEnvironment: "node",
|
||||
roots: ["<rootDir>/server"],
|
||||
transform: {
|
||||
"^.+\\.tsx?$": [
|
||||
"ts-jest",
|
||||
{
|
||||
tsconfig: "<rootDir>/server/tsconfig.json",
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
|
20
package.json
20
package.json
|
@ -56,7 +56,7 @@
|
|||
"serve-static": "^1.14.1",
|
||||
"sparkson": "^1.3.6",
|
||||
"sqlite": "^4.1.2",
|
||||
"sqlite3": "^5.0.11"
|
||||
"sqlite3": "^5.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@rushstack/eslint-patch": "^1.1.4",
|
||||
|
@ -66,15 +66,15 @@
|
|||
"@types/command-line-usage": "^5.0.2",
|
||||
"@types/compression": "^1.7.2",
|
||||
"@types/deep-extend": "^0.4.32",
|
||||
"@types/express": "^4.17.13",
|
||||
"@types/express": "^4.17.14",
|
||||
"@types/glob": "^8.0.0",
|
||||
"@types/graceful-fs": "^4.1.5",
|
||||
"@types/html-to-text": "^8.1.1",
|
||||
"@types/jest": "^29.0.0",
|
||||
"@types/lodash": "^4.14.184",
|
||||
"@types/node": "^18.7.15",
|
||||
"@types/node-cron": "^3.0.3",
|
||||
"@types/nodemailer": "^6.4.5",
|
||||
"@types/jest": "^29.0.2",
|
||||
"@types/lodash": "^4.14.185",
|
||||
"@types/node": "^18.7.18",
|
||||
"@types/node-cron": "^3.0.4",
|
||||
"@types/nodemailer": "^6.4.6",
|
||||
"@types/request": "^2.48.8",
|
||||
"@vue/eslint-config-prettier": "^7.0.0",
|
||||
"@vue/eslint-config-typescript": "^11.0.1",
|
||||
|
@ -102,13 +102,13 @@
|
|||
"grunt-rev": "^0.1.0",
|
||||
"grunt-usemin": "^3.1.1",
|
||||
"grunt-wiredep": "^3.0.1",
|
||||
"jest": "^29.0.2",
|
||||
"jest": "^29.0.3",
|
||||
"jshint-stylish": "^2.2.1",
|
||||
"load-grunt-tasks": "^5.1.0",
|
||||
"prettier": "^2.7.1",
|
||||
"time-grunt": "^2.0.0",
|
||||
"ts-jest": "^28.0.8",
|
||||
"typescript": "^4.8.2",
|
||||
"ts-jest": "^29.0.1",
|
||||
"typescript": "^4.8.3",
|
||||
"yarn-audit-fix": "^9.3.5"
|
||||
},
|
||||
"resolutions": {
|
||||
|
|
|
@ -9,11 +9,7 @@ import FixNodeFilenamesJob from "./FixNodeFilenamesJob";
|
|||
import NodeInformationRetrievalJob from "./NodeInformationRetrievalJob";
|
||||
import MonitoringMailsSendingJob from "./MonitoringMailsSendingJob";
|
||||
import OfflineNodesDeletionJob from "./OfflineNodesDeletionJob";
|
||||
|
||||
export enum JobResultState {
|
||||
OKAY = "okay",
|
||||
WARNING = "warning",
|
||||
}
|
||||
import { DurationSeconds, JobResultState, TaskState } from "../shared/types";
|
||||
|
||||
export type JobResult = {
|
||||
state: JobResultState;
|
||||
|
@ -41,12 +37,6 @@ export interface Job {
|
|||
run(): Promise<JobResult>;
|
||||
}
|
||||
|
||||
export enum TaskState {
|
||||
IDLE = "idle",
|
||||
RUNNING = "running",
|
||||
FAILED = "failed",
|
||||
}
|
||||
|
||||
export class Task {
|
||||
constructor(
|
||||
public id: number,
|
||||
|
@ -56,7 +46,7 @@ export class Task {
|
|||
public job: Job,
|
||||
public runningSince: moment.Moment | null,
|
||||
public lastRunStarted: moment.Moment | null,
|
||||
public lastRunDuration: number | null,
|
||||
public lastRunDuration: DurationSeconds | null,
|
||||
public state: TaskState,
|
||||
public result: JobResult | null,
|
||||
public enabled: boolean
|
||||
|
@ -74,7 +64,9 @@ export class Task {
|
|||
|
||||
const done = (state: TaskState, result: JobResult | null): void => {
|
||||
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);
|
||||
|
||||
this.runningSince = null;
|
||||
|
|
|
@ -10,7 +10,6 @@ import { Request, Response } from "express";
|
|||
import {
|
||||
CreateOrUpdateNode,
|
||||
DomainSpecificNodeResponse,
|
||||
filterUndefinedFromJSON,
|
||||
isCreateOrUpdateNode,
|
||||
isNodeSortField,
|
||||
isString,
|
||||
|
@ -27,6 +26,7 @@ import {
|
|||
toNodeResponse,
|
||||
toNodeTokenResponse,
|
||||
} from "../types";
|
||||
import { filterUndefinedFromJSON } from "../shared/utils/json";
|
||||
|
||||
const nodeFields = [
|
||||
"hostname",
|
||||
|
|
|
@ -2,36 +2,33 @@ import CONSTRAINTS from "../shared/validation/constraints";
|
|||
import ErrorTypes from "../utils/errorTypes";
|
||||
import * as Resources 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 { forConstraint } from "../shared/validation/validator";
|
||||
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);
|
||||
|
||||
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 {
|
||||
return {
|
||||
id: task.id,
|
||||
name: task.name,
|
||||
description: task.description,
|
||||
schedule: task.schedule,
|
||||
runningSince: task.runningSince && task.runningSince.unix(),
|
||||
lastRunStarted: task.lastRunStarted && task.lastRunStarted.unix(),
|
||||
runningSince:
|
||||
task.runningSince &&
|
||||
(task.runningSince.unix() as UnixTimestampSeconds),
|
||||
lastRunStarted:
|
||||
task.lastRunStarted &&
|
||||
(task.lastRunStarted.unix() as UnixTimestampSeconds),
|
||||
lastRunDuration: task.lastRunDuration || null,
|
||||
state: task.state,
|
||||
result:
|
||||
|
@ -89,7 +86,7 @@ async function doGetAll(
|
|||
): Promise<{ total: number; pageTasks: Task[] }> {
|
||||
const restParams = await Resources.getValidRestParams("list", null, req);
|
||||
|
||||
const tasks = Resources.sort(
|
||||
const tasks = Resources.sort<Task, TaskSortField>(
|
||||
Object.values(getTasks()),
|
||||
isTaskSortField,
|
||||
restParams
|
||||
|
|
|
@ -14,12 +14,13 @@ import {
|
|||
MailData,
|
||||
MailId,
|
||||
MailSortField,
|
||||
MailSortFieldEnum,
|
||||
MailType,
|
||||
parseJSON,
|
||||
UnixTimestampSeconds,
|
||||
} from "../types";
|
||||
import ErrorTypes from "../utils/errorTypes";
|
||||
import { send } from "../mail";
|
||||
import { parseJSON } from "../shared/utils/json";
|
||||
|
||||
type EmaiQueueRow = {
|
||||
id: MailId;
|
||||
|
@ -81,6 +82,8 @@ async function findPendingMailsBefore(
|
|||
recipient: row.recipient,
|
||||
data,
|
||||
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 filter = Resources.filterClause(
|
||||
const filter = Resources.filterClause<MailSortField>(
|
||||
restParams,
|
||||
MailSortField.ID,
|
||||
MailSortFieldEnum.ID,
|
||||
isMailSortField,
|
||||
["id", "failures", "sender", "recipient", "email"]
|
||||
);
|
||||
|
||||
const mails = await db.all(
|
||||
const mails = await db.all<Mail>(
|
||||
"SELECT * FROM email_queue WHERE " + filter.query,
|
||||
filter.params
|
||||
);
|
||||
|
|
|
@ -18,7 +18,6 @@ import { forConstraint } from "../shared/validation/validator";
|
|||
import {
|
||||
Domain,
|
||||
DurationSeconds,
|
||||
filterUndefinedFromJSON,
|
||||
Hostname,
|
||||
isBoolean,
|
||||
isDomain,
|
||||
|
@ -33,15 +32,14 @@ import {
|
|||
JSONValue,
|
||||
MAC,
|
||||
MailType,
|
||||
mapIdFromMAC,
|
||||
MonitoringSortField,
|
||||
MonitoringSortFieldEnum,
|
||||
MonitoringState,
|
||||
MonitoringToken,
|
||||
NodeMonitoringStateResponse,
|
||||
NodeStateData,
|
||||
NodeStateId,
|
||||
OnlineState,
|
||||
parseJSON,
|
||||
Site,
|
||||
StoredNode,
|
||||
toCreateOrUpdateNode,
|
||||
|
@ -56,6 +54,8 @@ import {
|
|||
subtract,
|
||||
weeks,
|
||||
} from "../utils/time";
|
||||
import { filterUndefinedFromJSON, parseJSON } from "../shared/utils/json";
|
||||
import { mapIdFromMAC } from "../shared/utils/node";
|
||||
|
||||
type NodeStateRow = {
|
||||
id: NodeStateId;
|
||||
|
@ -743,9 +743,9 @@ export async function getAll(
|
|||
|
||||
const total = row?.total || 0;
|
||||
|
||||
const filter = Resources.filterClause(
|
||||
const filter = Resources.filterClause<MonitoringSortField>(
|
||||
restParams,
|
||||
MonitoringSortField.ID,
|
||||
MonitoringSortFieldEnum.ID,
|
||||
isMonitoringSortField,
|
||||
filterFields
|
||||
);
|
||||
|
|
|
@ -19,7 +19,6 @@ import {
|
|||
CreateOrUpdateNode,
|
||||
EmailAddress,
|
||||
FastdKey,
|
||||
filterUndefinedFromJSON,
|
||||
Hostname,
|
||||
isFastdKey,
|
||||
isHostname,
|
||||
|
@ -36,13 +35,14 @@ import {
|
|||
NodeStatistics,
|
||||
StoredNode,
|
||||
Token,
|
||||
toUnixTimestampSeconds,
|
||||
TypeGuard,
|
||||
unhandledEnumField,
|
||||
UnixTimestampMilliseconds,
|
||||
UnixTimestampSeconds,
|
||||
} from "../types";
|
||||
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);
|
||||
|
||||
|
|
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 type { 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);
|
||||
}
|
378
server/shared/types/config.ts
Normal file
378
server/shared/types/config.ts
Normal file
|
@ -0,0 +1,378 @@
|
|||
/**
|
||||
* 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 {
|
||||
type Domain,
|
||||
isDomain,
|
||||
isSite,
|
||||
isUrl,
|
||||
type Site,
|
||||
type Url,
|
||||
} from "./newtypes";
|
||||
import { type 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 type { JSONObject } from "./json";
|
||||
import { toIsEnum } from "./enums";
|
||||
import { type SortFieldFor, toIsSortField } from "./sortfields";
|
||||
import type { 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 type { 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,788 +1,25 @@
|
|||
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.
|
||||
* E.g.: <code>"53.565278 10.001389"</code>
|
||||
* This module and all submodules provide types that are being shared between client and server.
|
||||
*/
|
||||
export type Coordinates = string & { readonly __tag: unique symbol };
|
||||
export const isCoordinates = toIsNewtype(isString, "" as Coordinates);
|
||||
import { isString } from "./primitives";
|
||||
|
||||
/**
|
||||
* Basic node data.
|
||||
*/
|
||||
export type BaseNode = {
|
||||
nickname: Nickname;
|
||||
email: EmailAddress;
|
||||
hostname: Hostname;
|
||||
coords: Coordinates | undefined;
|
||||
key: FastdKey | undefined;
|
||||
mac: MAC;
|
||||
};
|
||||
|
||||
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 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
|
||||
export 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 * from "./arrays";
|
||||
export * from "./config";
|
||||
export * from "./email";
|
||||
export * from "./enums";
|
||||
export * from "./helpers";
|
||||
export * from "./json";
|
||||
export * from "./maps";
|
||||
export * from "./monitoring";
|
||||
export * from "./newtypes";
|
||||
export * from "./node";
|
||||
export * from "./objects";
|
||||
export * from "./primitives";
|
||||
export * from "./regexps";
|
||||
export * from "./statistics";
|
||||
export * from "./sortfields";
|
||||
export * from "./task";
|
||||
export * from "./time";
|
||||
|
||||
export type SearchTerm = string & { readonly __tag: unique symbol };
|
||||
export const isSearchTerm = isString;
|
||||
|
||||
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;
|
||||
}
|
343
server/shared/types/monitoring.ts
Normal file
343
server/shared/types/monitoring.ts
Normal file
|
@ -0,0 +1,343 @@
|
|||
/**
|
||||
* Contains types and type guards for monitoring data.
|
||||
*/
|
||||
import {
|
||||
type Domain,
|
||||
isDomain,
|
||||
isMAC,
|
||||
isSite,
|
||||
type MAC,
|
||||
type Site,
|
||||
toIsNewtype,
|
||||
} from "./newtypes";
|
||||
import { isBoolean, isNumber, isString } from "./primitives";
|
||||
import { toIsEnum } from "./enums";
|
||||
import { type Hostname, isHostname, isMapId, type MapId } from "./node";
|
||||
import {
|
||||
type EmailAddress,
|
||||
isEmailAddress,
|
||||
isMailType,
|
||||
MailType,
|
||||
} from "./email";
|
||||
import { isUnixTimestampSeconds, type UnixTimestampSeconds } from "./time";
|
||||
import { isOptional } from "./helpers";
|
||||
import { type 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 type { 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);
|
485
server/shared/types/node.ts
Normal file
485
server/shared/types/node.ts
Normal file
|
@ -0,0 +1,485 @@
|
|||
/**
|
||||
* Contains types and type guards for representing Freifunk nodes in various states.
|
||||
*/
|
||||
import { isObject } from "./objects";
|
||||
import { isOptional } from "./helpers";
|
||||
import type {
|
||||
Coordinates,
|
||||
Domain,
|
||||
FastdKey,
|
||||
MAC,
|
||||
Nickname,
|
||||
Site,
|
||||
} from "./newtypes";
|
||||
import {
|
||||
isCoordinates,
|
||||
isDomain,
|
||||
isFastdKey,
|
||||
isMAC,
|
||||
isNickname,
|
||||
isSite,
|
||||
toIsNewtype,
|
||||
} from "./newtypes";
|
||||
import { isBoolean, isString } from "./primitives";
|
||||
import { type SortFieldFor, toIsSortField } from "./sortfields";
|
||||
import { type EmailAddress, isEmailAddress } from "./email";
|
||||
import { isUnixTimestampSeconds, type 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 { type Enum, toIsEnum } from "./enums";
|
||||
import { isString } from "./primitives";
|
||||
import type { 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 { type SortFieldFor, toIsSortField } from "./sortfields";
|
||||
import {
|
||||
type DurationSeconds,
|
||||
isDurationSeconds,
|
||||
isUnixTimestampSeconds,
|
||||
type 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, type JSONObject, type 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 type { 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, type MAC } from "../types";
|
||||
/**
|
||||
* Utility functions all around strings.
|
||||
*/
|
||||
import { isInteger, type 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 {
|
||||
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 {
|
||||
// parts only contains values at odd indexes
|
||||
const parts = mac
|
||||
|
@ -20,9 +40,26 @@ export function normalizeMac(mac: MAC): 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 {
|
||||
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;
|
||||
} else {
|
||||
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 type { 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;
|
||||
}
|
|
@ -1,95 +1,5 @@
|
|||
import {
|
||||
CreateOrUpdateNode,
|
||||
Domain,
|
||||
DomainSpecificNodeResponse,
|
||||
MonitoringResponse,
|
||||
MonitoringState,
|
||||
MonitoringToken,
|
||||
NodeResponse,
|
||||
NodeTokenResponse,
|
||||
OnlineState,
|
||||
Site,
|
||||
StoredNode,
|
||||
} from "../shared/types";
|
||||
|
||||
export * from "./config";
|
||||
export * from "./database";
|
||||
export * from "./logger";
|
||||
export * from "./node";
|
||||
export * from "../shared/types";
|
||||
|
||||
export type NodeStateData = {
|
||||
site?: Site;
|
||||
domain?: Domain;
|
||||
state: OnlineState;
|
||||
};
|
||||
|
||||
export function toCreateOrUpdateNode(node: StoredNode): CreateOrUpdateNode {
|
||||
return {
|
||||
nickname: node.nickname,
|
||||
email: node.email,
|
||||
hostname: node.hostname,
|
||||
coords: node.coords,
|
||||
key: node.key,
|
||||
mac: node.mac,
|
||||
monitoring: node.monitoringState !== MonitoringState.DISABLED,
|
||||
};
|
||||
}
|
||||
|
||||
export function toNodeResponse(node: StoredNode): NodeResponse {
|
||||
return {
|
||||
token: node.token,
|
||||
nickname: node.nickname,
|
||||
email: node.email,
|
||||
hostname: node.hostname,
|
||||
coords: node.coords,
|
||||
key: node.key,
|
||||
mac: node.mac,
|
||||
monitoring: node.monitoringState !== MonitoringState.DISABLED,
|
||||
monitoringConfirmed: node.monitoringState === MonitoringState.ACTIVE,
|
||||
monitoringState: node.monitoringState,
|
||||
modifiedAt: node.modifiedAt,
|
||||
};
|
||||
}
|
||||
|
||||
export function toNodeTokenResponse(node: StoredNode): NodeTokenResponse {
|
||||
return {
|
||||
token: node.token,
|
||||
node: toNodeResponse(node),
|
||||
};
|
||||
}
|
||||
|
||||
export function toDomainSpecificNodeResponse(
|
||||
node: StoredNode,
|
||||
nodeStateData: NodeStateData
|
||||
): DomainSpecificNodeResponse {
|
||||
return {
|
||||
token: node.token,
|
||||
nickname: node.nickname,
|
||||
email: node.email,
|
||||
hostname: node.hostname,
|
||||
coords: node.coords,
|
||||
key: node.key,
|
||||
mac: node.mac,
|
||||
monitoring: node.monitoringState !== MonitoringState.DISABLED,
|
||||
monitoringConfirmed: node.monitoringState === MonitoringState.ACTIVE,
|
||||
monitoringState: node.monitoringState,
|
||||
modifiedAt: node.modifiedAt,
|
||||
site: nodeStateData.site,
|
||||
domain: nodeStateData.domain,
|
||||
onlineState: nodeStateData.state,
|
||||
};
|
||||
}
|
||||
|
||||
export function toMonitoringResponse(node: StoredNode): MonitoringResponse {
|
||||
return {
|
||||
hostname: node.hostname,
|
||||
mac: node.mac,
|
||||
email: node.email,
|
||||
monitoring: node.monitoringState !== MonitoringState.DISABLED,
|
||||
monitoringConfirmed: node.monitoringState === MonitoringState.ACTIVE,
|
||||
};
|
||||
}
|
||||
|
||||
export type NodeSecrets = {
|
||||
monitoringToken?: MonitoringToken;
|
||||
};
|
||||
|
|
90
server/types/node.ts
Normal file
90
server/types/node.ts
Normal file
|
@ -0,0 +1,90 @@
|
|||
import {
|
||||
CreateOrUpdateNode,
|
||||
Domain,
|
||||
DomainSpecificNodeResponse,
|
||||
MonitoringResponse,
|
||||
MonitoringState,
|
||||
MonitoringToken,
|
||||
NodeResponse,
|
||||
NodeTokenResponse,
|
||||
OnlineState,
|
||||
Site,
|
||||
StoredNode,
|
||||
} from "../shared/types";
|
||||
|
||||
export type NodeStateData = {
|
||||
site?: Site;
|
||||
domain?: Domain;
|
||||
state: OnlineState;
|
||||
};
|
||||
|
||||
export function toCreateOrUpdateNode(node: StoredNode): CreateOrUpdateNode {
|
||||
return {
|
||||
nickname: node.nickname,
|
||||
email: node.email,
|
||||
hostname: node.hostname,
|
||||
coords: node.coords,
|
||||
key: node.key,
|
||||
mac: node.mac,
|
||||
monitoring: node.monitoringState !== MonitoringState.DISABLED,
|
||||
};
|
||||
}
|
||||
|
||||
export function toNodeResponse(node: StoredNode): NodeResponse {
|
||||
return {
|
||||
token: node.token,
|
||||
nickname: node.nickname,
|
||||
email: node.email,
|
||||
hostname: node.hostname,
|
||||
coords: node.coords,
|
||||
key: node.key,
|
||||
mac: node.mac,
|
||||
monitoring: node.monitoringState !== MonitoringState.DISABLED,
|
||||
monitoringConfirmed: node.monitoringState === MonitoringState.ACTIVE,
|
||||
monitoringState: node.monitoringState,
|
||||
modifiedAt: node.modifiedAt,
|
||||
};
|
||||
}
|
||||
|
||||
export function toNodeTokenResponse(node: StoredNode): NodeTokenResponse {
|
||||
return {
|
||||
token: node.token,
|
||||
node: toNodeResponse(node),
|
||||
};
|
||||
}
|
||||
|
||||
export function toDomainSpecificNodeResponse(
|
||||
node: StoredNode,
|
||||
nodeStateData: NodeStateData
|
||||
): DomainSpecificNodeResponse {
|
||||
return {
|
||||
token: node.token,
|
||||
nickname: node.nickname,
|
||||
email: node.email,
|
||||
hostname: node.hostname,
|
||||
coords: node.coords,
|
||||
key: node.key,
|
||||
mac: node.mac,
|
||||
monitoring: node.monitoringState !== MonitoringState.DISABLED,
|
||||
monitoringConfirmed: node.monitoringState === MonitoringState.ACTIVE,
|
||||
monitoringState: node.monitoringState,
|
||||
modifiedAt: node.modifiedAt,
|
||||
site: nodeStateData.site,
|
||||
domain: nodeStateData.domain,
|
||||
onlineState: nodeStateData.state,
|
||||
};
|
||||
}
|
||||
|
||||
export function toMonitoringResponse(node: StoredNode): MonitoringResponse {
|
||||
return {
|
||||
hostname: node.hostname,
|
||||
mac: node.mac,
|
||||
email: node.email,
|
||||
monitoring: node.monitoringState !== MonitoringState.DISABLED,
|
||||
monitoringConfirmed: node.monitoringState === MonitoringState.ACTIVE,
|
||||
};
|
||||
}
|
||||
|
||||
export type NodeSecrets = {
|
||||
monitoringToken?: MonitoringToken;
|
||||
};
|
|
@ -11,10 +11,7 @@ import {
|
|||
} from "../shared/validation/validator";
|
||||
import { Request, Response } from "express";
|
||||
import {
|
||||
EnumTypeGuard,
|
||||
ValueOf,
|
||||
type GenericSortField,
|
||||
getFieldIfExists,
|
||||
isJSONObject,
|
||||
isNumber,
|
||||
isString,
|
||||
|
@ -24,6 +21,7 @@ import {
|
|||
SortDirection,
|
||||
TypeGuard,
|
||||
} from "../types";
|
||||
import { getFieldIfExists } from "../shared/utils/objects";
|
||||
|
||||
export type RequestData = JSONObject;
|
||||
export type RequestHandler = (request: Request, response: Response) => void;
|
||||
|
@ -77,12 +75,12 @@ function respond(
|
|||
}
|
||||
}
|
||||
|
||||
function orderByClause<S>(
|
||||
function orderByClause<SortField>(
|
||||
restParams: RestParams,
|
||||
defaultSortField: ValueOf<S>,
|
||||
isSortField: EnumTypeGuard<S>
|
||||
defaultSortField: SortField,
|
||||
isSortField: TypeGuard<SortField>
|
||||
): OrderByClause {
|
||||
let sortField: ValueOf<S> | undefined = isSortField(restParams._sortField)
|
||||
let sortField: SortField | undefined = isSortField(restParams._sortField)
|
||||
? restParams._sortField
|
||||
: undefined;
|
||||
if (!sortField) {
|
||||
|
@ -345,13 +343,17 @@ export function getPageEntities<Entity>(
|
|||
|
||||
export { filterCondition as whereCondition };
|
||||
|
||||
export function filterClause<S>(
|
||||
export function filterClause<SortField>(
|
||||
restParams: RestParams,
|
||||
defaultSortField: ValueOf<S>,
|
||||
isSortField: EnumTypeGuard<S>,
|
||||
defaultSortField: SortField,
|
||||
isSortField: TypeGuard<SortField>,
|
||||
filterFields: string[]
|
||||
): FilterClause {
|
||||
const orderBy = orderByClause<S>(restParams, defaultSortField, isSortField);
|
||||
const orderBy = orderByClause<SortField>(
|
||||
restParams,
|
||||
defaultSortField,
|
||||
isSortField
|
||||
);
|
||||
const limitOffset = limitOffsetClause(restParams);
|
||||
|
||||
const filter = filterCondition(restParams, filterFields);
|
||||
|
|
Loading…
Reference in a new issue