From e08ae944c48c0006811115e1595bf2b51f8bae13 Mon Sep 17 00:00:00 2001 From: baldo Date: Tue, 6 Sep 2022 17:43:00 +0200 Subject: [PATCH 1/3] Move node types into own module to keep index.ts clean. --- server/types/index.ts | 92 +------------------------------------------ server/types/node.ts | 90 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 91 insertions(+), 91 deletions(-) create mode 100644 server/types/node.ts diff --git a/server/types/index.ts b/server/types/index.ts index cba4a55..9b4ba79 100644 --- a/server/types/index.ts +++ b/server/types/index.ts @@ -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; -}; diff --git a/server/types/node.ts b/server/types/node.ts new file mode 100644 index 0000000..d0dbdc8 --- /dev/null +++ b/server/types/node.ts @@ -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; +}; From 843cd37243c77b9a2902d89da2df05836c3dc888 Mon Sep 17 00:00:00 2001 From: baldo Date: Tue, 6 Sep 2022 19:09:25 +0200 Subject: [PATCH 2/3] Refactoring: Split shared types into seperate modules and document alot. --- server/jobs/scheduler.ts | 18 +- server/resources/nodeResource.ts | 2 +- server/resources/taskResource.ts | 35 +- server/services/mailService.ts | 11 +- server/services/monitoringService.ts | 10 +- server/services/nodeService.ts | 6 +- server/shared/types/arrays.ts | 39 ++ server/shared/types/config.ts | 371 +++++++++++++ server/shared/types/email.ts | 183 ++++++ server/shared/types/enums.ts | 25 + server/shared/types/helpers.ts | 45 ++ server/shared/types/index.ts | 801 +-------------------------- server/shared/types/json.ts | 72 +++ server/shared/types/maps.ts | 12 + server/shared/types/monitoring.ts | 338 +++++++++++ server/shared/types/newtypes.ts | 142 +++++ server/shared/types/node.ts | 483 ++++++++++++++++ server/shared/types/objects.ts | 36 ++ server/shared/types/primitives.ts | 73 +++ server/shared/types/regexps.ts | 13 + server/shared/types/sortfields.ts | 76 +++ server/shared/types/statistics.ts | 76 +++ server/shared/types/task.ts | 207 +++++++ server/shared/types/time.ts | 66 +++ server/shared/utils/enums.ts | 30 + server/shared/utils/json.ts | 42 ++ server/shared/utils/node.ts | 14 + server/shared/utils/objects.ts | 19 + server/shared/utils/strings.ts | 43 +- server/shared/utils/time.ts | 28 + server/utils/resources.ts | 24 +- 31 files changed, 2498 insertions(+), 842 deletions(-) create mode 100644 server/shared/types/arrays.ts create mode 100644 server/shared/types/config.ts create mode 100644 server/shared/types/email.ts create mode 100644 server/shared/types/enums.ts create mode 100644 server/shared/types/helpers.ts create mode 100644 server/shared/types/json.ts create mode 100644 server/shared/types/maps.ts create mode 100644 server/shared/types/monitoring.ts create mode 100644 server/shared/types/newtypes.ts create mode 100644 server/shared/types/node.ts create mode 100644 server/shared/types/objects.ts create mode 100644 server/shared/types/primitives.ts create mode 100644 server/shared/types/regexps.ts create mode 100644 server/shared/types/sortfields.ts create mode 100644 server/shared/types/statistics.ts create mode 100644 server/shared/types/task.ts create mode 100644 server/shared/types/time.ts create mode 100644 server/shared/utils/enums.ts create mode 100644 server/shared/utils/json.ts create mode 100644 server/shared/utils/node.ts create mode 100644 server/shared/utils/objects.ts create mode 100644 server/shared/utils/time.ts diff --git a/server/jobs/scheduler.ts b/server/jobs/scheduler.ts index 76a3672..871aac9 100644 --- a/server/jobs/scheduler.ts +++ b/server/jobs/scheduler.ts @@ -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; } -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; diff --git a/server/resources/nodeResource.ts b/server/resources/nodeResource.ts index 45964c6..2f1808f 100644 --- a/server/resources/nodeResource.ts +++ b/server/resources/nodeResource.ts @@ -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", diff --git a/server/resources/taskResource.ts b/server/resources/taskResource.ts index 035d7bd..dfadc96 100644 --- a/server/resources/taskResource.ts +++ b/server/resources/taskResource.ts @@ -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( Object.values(getTasks()), isTaskSortField, restParams diff --git a/server/services/mailService.ts b/server/services/mailService.ts index f45f535..351f791 100644 --- a/server/services/mailService.ts +++ b/server/services/mailService.ts @@ -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( restParams, - MailSortField.ID, + MailSortFieldEnum.ID, isMailSortField, ["id", "failures", "sender", "recipient", "email"] ); - const mails = await db.all( + const mails = await db.all( "SELECT * FROM email_queue WHERE " + filter.query, filter.params ); diff --git a/server/services/monitoringService.ts b/server/services/monitoringService.ts index 897c8da..a8dd83e 100644 --- a/server/services/monitoringService.ts +++ b/server/services/monitoringService.ts @@ -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( restParams, - MonitoringSortField.ID, + MonitoringSortFieldEnum.ID, isMonitoringSortField, filterFields ); diff --git a/server/services/nodeService.ts b/server/services/nodeService.ts index 3e9c06e..6b92e05 100644 --- a/server/services/nodeService.ts +++ b/server/services/nodeService.ts @@ -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); diff --git a/server/shared/types/arrays.ts b/server/shared/types/arrays.ts new file mode 100644 index 0000000..49c0dec --- /dev/null +++ b/server/shared/types/arrays.ts @@ -0,0 +1,39 @@ +/** + * Contains type guards for arrays. + * + * @module arrays + */ +import { TypeGuard } from "./helpers"; + +/** + * Type guard for an array with elements of type ``. + * + * @param arg - Value to check + * @param isElement - Type guard to check elements for type `` + */ +export function isArray( + arg: unknown, + isElement: TypeGuard +): arg is Array { + 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 `` + * @returns A type guard for arrays with elements of type ``. + */ +export function toIsArray( + isElement: TypeGuard +): TypeGuard { + return (arg): arg is Element[] => isArray(arg, isElement); +} diff --git a/server/shared/types/config.ts b/server/shared/types/config.ts new file mode 100644 index 0000000..96ec9e3 --- /dev/null +++ b/server/shared/types/config.ts @@ -0,0 +1,371 @@ +/** + * Contains types and corresponding type guards for the client side configuration of ffffng. + * + * @module config + */ +import { ArrayField, Field, RawJsonField } from "sparkson"; +import { isObject, isPlainObject } from "./objects"; +import { isBoolean, isNumber, isString } from "./primitives"; +import { isArray } from "./arrays"; +import { isOptional } from "./helpers"; +import { isJSONObject } from "./json"; +import { Domain, isDomain, isSite, isUrl, Site, Url } from "./newtypes"; +import { EmailAddress, isEmailAddress } from "./email"; + +/** + * Configuration for a single coordinate. + * + * See {@link CommunityConfig.constructor}. + */ +export class CoordinatesConfig { + /** + * @param lat - Latitude of the coordinate. + * @param lng - Longitude of the coordinate. + */ + constructor( + @Field("lat") public lat: number, + @Field("lng") public lng: number + ) {} +} + +/** + * Type guard for {@link CoordinatesConfig}. + * + * @param arg - Value to check. + */ +export function isCoordinatesConfig(arg: unknown): arg is CoordinatesConfig { + if (!isObject(arg)) { + return false; + } + const coords = arg as CoordinatesConfig; + return isNumber(coords.lat) && isNumber(coords.lng); +} + +/** + * Configuration for checking if a node is inside the community boundaries. + * + * See {@link OtherCommunityInfoConfig.constructor}. + */ +export class OtherCommunityInfoConfig { + /** + * @param showInfo - Specifies if for nodes outside the community boundaries a confirmation screen should be shown. + * @param showBorderForDebugging - If set to `true` the outline of the community is rendered on the map. + * @param localCommunityPolygon - Boundaries of the community. + */ + constructor( + @Field("showInfo") public showInfo: boolean, + @Field("showBorderForDebugging") public showBorderForDebugging: boolean, + @ArrayField("localCommunityPolygon", CoordinatesConfig) + public localCommunityPolygon: CoordinatesConfig[] + ) {} +} + +/** + * Type guard for {@link OtherCommunityInfoConfig}. + * + * @param arg - Value to check. + */ +export function isOtherCommunityInfoConfig( + arg: unknown +): arg is OtherCommunityInfoConfig { + if (!isObject(arg)) { + return false; + } + const cfg = arg as OtherCommunityInfoConfig; + return ( + isBoolean(cfg.showInfo) && + isBoolean(cfg.showBorderForDebugging) && + isArray(cfg.localCommunityPolygon, isCoordinatesConfig) + ); +} + +/** + * Options of a map layer. + */ +export type LayerOptions = { + /** + * Attribution shown for the map layer (HTML). + */ + attribution: string; + + /** + * Subdomains used to load tiles for the map layer, e.g.: `"1234"` or `"abcd"`. + */ + subdomains?: string; + + /** + * Maximum zoom level for the map layer. + */ + maxZoom: number; +}; + +/** + * Type guard for {@link LayerOptions}. + * + * @param arg - Value to check. + */ +export function isLayerOptions(arg: unknown): arg is LayerOptions { + if (!isPlainObject(arg)) { + return false; + } + const obj = arg as LayerOptions; + return ( + isString(obj.attribution) && + isOptional(obj.subdomains, isString) && + isNumber(obj.maxZoom) + ); +} + +/** + * Configuration of a map layer. + */ +export type LayerConfig = { + /** + * Display name of the map layer. + */ + name: string; + + /** + * Tiles URL of the layer. + */ + url: Url; + + /** + * Type of the map (e.g. `"xyz"`). Unused in new frontend. + */ + type: string; + + /** + * See {@link LayerOptions}. + */ + layerOptions: LayerOptions; +}; + +/** + * Type guard for {@link LayerConfig}. + * + * @param arg - Value to check. + */ +export function isLayerConfig(arg: unknown): arg is LayerConfig { + if (!isPlainObject(arg)) { + return false; + } + const obj = arg as LayerConfig; + return ( + isString(obj.name) && + isUrl(obj.url) && + isString(obj.type) && + isLayerOptions(obj.layerOptions) + ); +} + +/** + * Configuration of the map for picking node coordinates. + * + * See {@link CoordinatesSelectorConfig.constructor} + */ +export class CoordinatesSelectorConfig { + /** + * @param lat - Latitude to center the map on + * @param lng - Longitude to center the map on + * @param defaultZoom - Default zoom level of the map + * @param layers - Mapping of layer ids to layer configurations for the map + */ + constructor( + @Field("lat") public lat: number, + @Field("lng") public lng: number, + @Field("defaultZoom") public defaultZoom: number, + @RawJsonField("layers") public layers: Record + ) {} +} + +/** + * 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) + ); +} diff --git a/server/shared/types/email.ts b/server/shared/types/email.ts new file mode 100644 index 0000000..842cda6 --- /dev/null +++ b/server/shared/types/email.ts @@ -0,0 +1,183 @@ +/** + * Contains types and type guards around emails. + */ +import { toIsNewtype } from "./newtypes"; +import { isNumber, isString } from "./primitives"; +import { JSONObject } from "./json"; +import { toIsEnum } from "./enums"; +import { SortFieldFor, toIsSortField } from "./sortfields"; +import { UnixTimestampSeconds } from "./time"; + +/** + * An email address. + */ +export type EmailAddress = string & { readonly __tag: unique symbol }; + +/** + * Type guard for {@link EmailAddress}. + * + * @param arg - Value to check. + */ +export const isEmailAddress = toIsNewtype(isString, "" as EmailAddress); + +/** + * ID of an email in the mail queue waiting to be sent. + */ +export type MailId = number & { readonly __tag: unique symbol }; + +/** + * Type guard for {@link MailId}. + * + * @param arg - Value to check. + */ +export const isMailId = toIsNewtype(isNumber, NaN as MailId); + +/** + * Data of an email in the mail queue waiting to be sent. + */ +export type Mail = { + /** + * ID of the email in the queue. + */ + id: MailId; + + /** + * Type of the to be sent email. + * + * See {@link MailType}. + */ + email: MailType; + + /** + * Sender address of the email. + */ + sender: EmailAddress; + + /** + * Recipient address of the email. + */ + recipient: EmailAddress; + + /** + * Data to be rendered into the email template. This is specific to the email's {@link MailType}. + */ + data: MailData; + + /** + * Number of times trying to send the queued email has failed. + */ + failures: number; + + /** + * Time the email has been queued first. + */ + created_at: UnixTimestampSeconds; + + /** + * Last time the email has been modified inside the queue. + */ + modified_at: UnixTimestampSeconds; +}; + +/** + * Type of the email being sent. This determines which email template is being used and in which format the + * {@link MailData} is being expected. + */ +export enum MailType { + /** + * First monitoring email sent when a Freifunk node is offline. + */ + MONITORING_OFFLINE_1 = "monitoring-offline-1", + + /** + * Second monitoring (first reminder) email sent when a Freifunk node is offline. + */ + MONITORING_OFFLINE_2 = "monitoring-offline-2", + + /** + * Third monitoring (second and last reminder) email sent when a Freifunk node is offline. + */ + MONITORING_OFFLINE_3 = "monitoring-offline-3", + + /** + * Email notifying the owner that their Freifunk node is back online. + */ + MONITORING_ONLINE_AGAIN = "monitoring-online-again", + + /** + * Email holding a confirmation link to enable monitoring for a Freifunk node (double opt-in). + */ + MONITORING_CONFIRMATION = "monitoring-confirmation", +} + +/** + * Type guard for {@link MailType}. + * + * @param arg - Value to check. + */ +export const isMailType = toIsEnum(MailType); + +/** + * Type of data being rendered into an email template. This is specific to the email's {@link MailType}. + */ +export type MailData = JSONObject; + +/** + * Enum specifying the allowed sort fields when retrieving the list of emails in the mail queue via + * the REST API. + */ +export enum MailSortFieldEnum { + // noinspection JSUnusedGlobalSymbols + + /** + * See {@link Mail.id}. + */ + ID = "id", + + /** + * See {@link Mail.failures}. + */ + FAILURES = "failures", + + /** + * See {@link Mail.sender}. + */ + SENDER = "sender", + + /** + * See {@link Mail.recipient}. + */ + RECIPIENT = "recipient", + + /** + * See {@link Mail.email}. + */ + EMAIL = "email", + + /** + * See {@link Mail.created_at}. + */ + CREATED_AT = "created_at", + + /** + See {@link Mail.modified_at}. + */ + MODIFIED_AT = "modified_at", +} + +/** + * Allowed sort fields when retrieving the list of emails in the mail queue via the REST API. + */ +export type MailSortField = SortFieldFor; + +/** + * Type guard for {@link MailSortField}. + * + * @param arg - Value to check. + */ +export const isMailSortField = toIsSortField< + Mail, + MailSortFieldEnum, + typeof MailSortFieldEnum, + MailSortField +>(MailSortFieldEnum); diff --git a/server/shared/types/enums.ts b/server/shared/types/enums.ts new file mode 100644 index 0000000..7a5a1fd --- /dev/null +++ b/server/shared/types/enums.ts @@ -0,0 +1,25 @@ +/** + * Contains type guards and helpers for enums. + */ +import { TypeGuard, ValueOf } from "./helpers"; + +/** + * Shorthand type alias for enum {@link TypeGuard}s. + */ +export type EnumTypeGuard = TypeGuard>; + +/** + * Shorthand type for descrbing enum objects. + */ +export type Enum = Record>; + +/** + * Helper function to construct enum type guards. + * + * @param enumDef - The enum object to check against. + * @returns A type guard for values of the enum `` + */ +export function toIsEnum>(enumDef: E): EnumTypeGuard { + return (arg): arg is ValueOf => + Object.values(enumDef).includes(arg as [keyof E]); +} diff --git a/server/shared/types/helpers.ts b/server/shared/types/helpers.ts new file mode 100644 index 0000000..981705c --- /dev/null +++ b/server/shared/types/helpers.ts @@ -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 ``. + */ +export type TypeGuard = (arg: unknown) => arg is ValueType; + +/** + * Shorthand type alias for referencing values hold by an object of type ``. + * + * See it as an addition to typescript's `keyof`. + */ +export type ValueOf = Type[keyof Type]; + +/** + * Generic type guard to check for optional values of type ``. + * Optional means the value must either be `undefined` or a valid value of type ``. + * + * @param arg - Value to check + * @param isType - Type guard for checking for values of type `` + */ +export function isOptional( + arg: unknown, + isType: TypeGuard +): arg is Type | undefined { + return isUndefined(arg) || isType(arg); +} + +/** + * Generic type guard to check for nullable values of type ``. + * The value must either be `null` or a valid value of type ``. + * + * @param arg - Value to check + * @param isType - Type guard for checking for values of type `` + */ +export function isNullable( + arg: unknown, + isType: TypeGuard +): arg is Type | null { + return isNull(arg) || isType(arg); +} diff --git a/server/shared/types/index.ts b/server/shared/types/index.ts index 80e17b1..3150ef5 100644 --- a/server/shared/types/index.ts +++ b/server/shared/types/index.ts @@ -1,785 +1,20 @@ -import { ArrayField, Field, RawJsonField } from "sparkson"; - -// Types shared with the client. -export type TypeGuard = (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; - -export const isJSONArray = toIsArray(isJSONValue); - -export type ValueOf = T[keyof T]; -export type EnumTypeGuard = TypeGuard>; - -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( - arg: unknown, - key: Key -): arg is Record { - 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(arg: unknown, isT: TypeGuard): arg is Array { - 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 { - 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, example: Type): TypeGuard { - 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(isT: TypeGuard): TypeGuard { - return (arg): arg is T[] => isArray(arg, isT); -} - -export function toIsEnum>>( - enumDef: E -): EnumTypeGuard { - return (arg): arg is ValueOf => - 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( - arg: unknown, - isT: TypeGuard -): 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 - ) {} -} - -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.: "53.565278 10.001389" + * 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); - -/** - * 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 -enum NodeSortFieldEnum { - HOSTNAME = "hostname", - NICKNAME = "nickname", - EMAIL = "email", - TOKEN = "token", - MAC = "mac", - KEY = "key", - SITE = "site", - DOMAIN = "domain", - COORDS = "coords", - ONLINE_STATE = "onlineState", - MONITORING_STATE = "monitoringState", -} - -export type NodeSortField = keyof Pick< - DomainSpecificNodeResponse, - NodeSortFieldEnum ->; - -export function isNodeSortField(arg: unknown): arg is NodeSortField { - if (!isString(arg)) { - return false; - } - return Object.values(NodeSortFieldEnum).includes(arg as NodeSortField); -} - -export type NodesFilter = { - hasKey?: boolean; - hasCoords?: boolean; - monitoringState?: MonitoringState; - site?: Site; - domain?: Domain; - onlineState?: OnlineState; -}; - -export const NODES_FILTER_FIELDS: Record< - keyof NodesFilter, - | BooleanConstructor - | StringConstructor - | typeof MonitoringState - | typeof OnlineState -> = { - hasKey: Boolean, - hasCoords: Boolean, - monitoringState: MonitoringState, - site: String, - domain: String, - onlineState: OnlineState, -}; - -export function isNodesFilter(arg: unknown): arg is NodesFilter { - if (!isObject(arg)) { - return false; - } - const filter = arg as NodesFilter; - return ( - isOptional(filter.hasKey, isBoolean) && - isOptional(filter.hasCoords, isBoolean) && - isOptional(filter.monitoringState, isMonitoringState) && - isOptional(filter.site, isSite) && - isOptional(filter.domain, isDomain) && - isOptional(filter.onlineState, isOnlineState) - ); -} - -export enum MonitoringSortField { - ID = "id", - HOSTNAME = "hostname", - MAC = "mac", - SITE = "site", - DOMAIN = "domain", - MONITORING_STATE = "monitoring_state", - STATE = "state", - LAST_SEEN = "last_seen", - IMPORT_TIMESTAMP = "import_timestamp", - LAST_STATUS_MAIL_TYPE = "last_status_mail_type", - LAST_STATUS_MAIL_SENT = "last_status_mail_sent", - CREATED_AT = "created_at", - MODIFIED_AT = "modified_at", -} - -export const isMonitoringSortField = toIsEnum(MonitoringSortField); - -export enum TaskSortField { - ID = "id", - NAME = "name", - SCHEDULE = "schedule", - STATE = "state", - RUNNING_SINCE = "runningSince", - LAST_RUN_STARTED = "lastRunStarted", -} - -export const isTaskSortField = toIsEnum(TaskSortField); - -export enum MailSortField { - ID = "id", - FAILURES = "failures", - SENDER = "sender", - RECIPIENT = "recipient", - EMAIL = "email", - CREATED_AT = "created_at", - MODIFIED_AT = "modified_at", -} - -export const isMailSortField = toIsEnum(MailSortField); - -export type GenericSortField = { - value: string; - readonly __tag: unique symbol; -}; - -export enum SortDirection { - ASCENDING = "ASC", - DESCENDING = "DESC", -} - -export const isSortDirection = toIsEnum(SortDirection); +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"; diff --git a/server/shared/types/json.ts b/server/shared/types/json.ts new file mode 100644 index 0000000..77c805d --- /dev/null +++ b/server/shared/types/json.ts @@ -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; + +/** + * Type guard checking the given value is a valid {@link JSONArray}. + * + * @param arg - Value to check. + */ +export const isJSONArray = toIsArray(isJSONValue); diff --git a/server/shared/types/maps.ts b/server/shared/types/maps.ts new file mode 100644 index 0000000..7dbd050 --- /dev/null +++ b/server/shared/types/maps.ts @@ -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 { + return arg instanceof Map; +} diff --git a/server/shared/types/monitoring.ts b/server/shared/types/monitoring.ts new file mode 100644 index 0000000..baf496f --- /dev/null +++ b/server/shared/types/monitoring.ts @@ -0,0 +1,338 @@ +/** + * Contains types and type guards for monitoring data. + */ +import { + Domain, + isDomain, + isMAC, + isSite, + MAC, + Site, + toIsNewtype, +} from "./newtypes"; +import { isBoolean, isNumber, isString } from "./primitives"; +import { toIsEnum } from "./enums"; +import { Hostname, isHostname, isMapId, MapId } from "./node"; +import { EmailAddress, isEmailAddress, isMailType, MailType } from "./email"; +import { isUnixTimestampSeconds, UnixTimestampSeconds } from "./time"; +import { isOptional } from "./helpers"; +import { SortFieldFor, toIsSortField } from "./sortfields"; + +/** + * Token for activating monitoring of a Freifunk node. This is being sent to verify the email address to use. + */ +export type MonitoringToken = string & { readonly __tag: unique symbol }; + +/** + * Type guard for {@link MonitoringToken}. + * + * @param arg - Value to check. + */ +export const isMonitoringToken = toIsNewtype(isString, "" as MonitoringToken); + +/** + * The different states of monitoring of a Freifunk node. + */ +export enum MonitoringState { + /** + * The node's online state is being actively monitored. If the node goes offline for a certain period of time + * a notification email will be sent. + */ + ACTIVE = "active", + + /** + * Monitoring has been activated by the user, but the email address used is not yet verified. + */ + PENDING = "pending", + + /** + * Monitoring is disabled. + */ + DISABLED = "disabled", +} + +/** + * Type guard for {@link MonitoringState}. + * + * @param arg - Value to check. + */ +export const isMonitoringState = toIsEnum(MonitoringState); + +/** + * Online state of a Freifunk node. + */ +export enum OnlineState { + /** + * The node is currently online. + */ + ONLINE = "ONLINE", + + /** + * The node is currently offline. + */ + OFFLINE = "OFFLINE", +} + +/** + * Type guard for {@link OnlineState}. + * + * @param arg - Value to check. + */ +export const isOnlineState = toIsEnum(OnlineState); + +/** + * Data of a Freifunk node as it is provided by the server's API when changing the nodes monitoring state. + */ +export type MonitoringResponse = { + /** + * Hostname of the node. + */ + hostname: Hostname; + + /** + * MAC address of the node. + */ + mac: MAC; + + /** + * Email address that is being used for monitoring. + */ + email: EmailAddress; + + /** + * Whether monitoring is enabled. + */ + monitoring: boolean; + + /** + * Whether the email address has been confirmed for use in monitoring the node. + */ + monitoringConfirmed: boolean; +}; + +/** + * Type guard for {@link MonitoringResponse}. + * + * @param arg - Value to check. + */ +export function isMonitoringResponse(arg: unknown): arg is MonitoringResponse { + if (!Object(arg)) { + return false; + } + const response = arg as MonitoringResponse; + return ( + isHostname(response.hostname) && + isMAC(response.mac) && + isEmailAddress(response.email) && + isBoolean(response.monitoring) && + isBoolean(response.monitoringConfirmed) + ); +} + +/** + * ID of the monitoring data of a Freifunk node stored in the database. + */ +export type NodeStateId = number & { readonly __tag: unique symbol }; + +/** + * Type guard for {@link NodeStateId}. + * + * @param arg - Value to check. + */ +export const isNodeStateId = toIsNewtype(isNumber, NaN as NodeStateId); + +/** + * Monitoring related data of a Freifunk node as it is provided by the server's API to the admin frontend. + */ +export type NodeMonitoringStateResponse = { + /** + * ID of the monitoring data stored in the database. + */ + id: NodeStateId; + + /** + * Time the monitoring data has first been stored. + */ + created_at: UnixTimestampSeconds; + + /** + * Time the monitoring data has last been updated. + */ + modified_at: UnixTimestampSeconds; + + /** + * Hostname of the Freifunk node. + */ + hostname?: Hostname; + + /** + * MAC address of the Freifunk node. + */ + mac: MAC; + + /** + * ID to identify a Freifunk node on the communities map. + */ + mapId: MapId; + + /** + * Freifunk site as specified in the community map's `nodes.json`. + */ + site?: Site; + + /** + * Freifunk domain as specified in the community map's `nodes.json`. + */ + domain?: Domain; + + /** + * Time data for the node has last been imported from the community map's `nodes.json`. + */ + import_timestamp: UnixTimestampSeconds; + + /** + * Time the node has last been seen online. + */ + last_seen: UnixTimestampSeconds; + + /** + * Time the last monitoring notification email has been sent, if any. + */ + last_status_mail_sent?: UnixTimestampSeconds; + + /** + * Type of the last monitoring notification email sent, if any. + */ + last_status_mail_type?: MailType; + + /** + * Monitoring state of the node. + */ + monitoring_state?: MonitoringState; + + /** + * Online state of the node. + */ + state: OnlineState; +}; + +/** + * Type guard for {@link NodeMonitoringStateResponse}. + * + * @param arg - Value to check. + */ +export function isNodeMonitoringStateResponse( + arg: unknown +): arg is NodeMonitoringStateResponse { + if (!Object(arg)) { + return false; + } + const response = arg as NodeMonitoringStateResponse; + return ( + isNodeStateId(response.id) && + isUnixTimestampSeconds(response.created_at) && + isOptional(response.domain, isDomain) && + isOptional(response.hostname, isHostname) && + isUnixTimestampSeconds(response.import_timestamp) && + isUnixTimestampSeconds(response.last_seen) && + isOptional(response.last_status_mail_sent, isUnixTimestampSeconds) && + isOptional(response.last_status_mail_type, isMailType) && + isMAC(response.mac) && + isUnixTimestampSeconds(response.modified_at) && + isOptional(response.monitoring_state, isMonitoringState) && + isOptional(response.site, isSite) && + isOnlineState(response.state) && + isMapId(response.mapId) + ); +} + +/** + * Enum specifying the allowed sort fields when retrieving the list of monitoring data via the REST API. + */ +export enum MonitoringSortFieldEnum { + // noinspection JSUnusedGlobalSymbols + /** + * See {@link NodeMonitoringStateResponse.id}. + */ + ID = "id", + + /** + * See {@link NodeMonitoringStateResponse.hostname}. + */ + HOSTNAME = "hostname", + + /** + * See {@link NodeMonitoringStateResponse.mac}. + */ + MAC = "mac", + + /** + * See {@link NodeMonitoringStateResponse.site}. + */ + SITE = "site", + + /** + * See {@link NodeMonitoringStateResponse.domain}. + */ + DOMAIN = "domain", + + /** + * See {@link NodeMonitoringStateResponse.monitoring_state}. + */ + MONITORING_STATE = "monitoring_state", + + /** + * See {@link NodeMonitoringStateResponse.state}. + */ + STATE = "state", + + /** + * See {@link NodeMonitoringStateResponse.last_seen}. + */ + LAST_SEEN = "last_seen", + + /** + * See {@link NodeMonitoringStateResponse.import_timestamp}. + */ + IMPORT_TIMESTAMP = "import_timestamp", + + /** + * See {@link NodeMonitoringStateResponse.last_status_mail_type}. + */ + LAST_STATUS_MAIL_TYPE = "last_status_mail_type", + + /** + * See {@link NodeMonitoringStateResponse.last_status_mail_sent}. + */ + LAST_STATUS_MAIL_SENT = "last_status_mail_sent", + + /** + * See {@link NodeMonitoringStateResponse.created_at}. + */ + CREATED_AT = "created_at", + + /** + * See {@link NodeMonitoringStateResponse.modified_at}. + */ + MODIFIED_AT = "modified_at", +} + +/** + * Allowed sort fields when retrieving the list of monitoring data via the REST API. + */ +export type MonitoringSortField = SortFieldFor< + NodeMonitoringStateResponse, + MonitoringSortFieldEnum +>; + +/** + * Type guard for {@link MonitoringSortField}. + * + * @param arg - Value to check. + */ +export const isMonitoringSortField = toIsSortField< + NodeMonitoringStateResponse, + MonitoringSortFieldEnum, + typeof MonitoringSortFieldEnum, + MonitoringSortField +>(MonitoringSortFieldEnum); diff --git a/server/shared/types/newtypes.ts b/server/shared/types/newtypes.ts new file mode 100644 index 0000000..9a0a609 --- /dev/null +++ b/server/shared/types/newtypes.ts @@ -0,0 +1,142 @@ +/** + * Contains type guards for newtypes. Newtypes are a way to strongly type strings, numbers, ... + * + * This is inspired by the tagged intersection types in + * {@link https://kubyshkin.name/posts/newtype-in-typescript/}. + * + * Also holds newtype definitions that don't fit elsewhere. + */ +import { TypeGuard } from "./helpers"; +import { isString } from "./primitives"; + +// ===================================================================================================================== +// General newtype helpers. +// ===================================================================================================================== + +/** + * Helper function to generate type guards for newtypes of type ``. + * + * Newtypes can be defined as follows: + * + * @param isValue - Typeguard to check for the value-type (``) of the newtype. + * @param example - An example value of type ``. + * @returns A type guard for ``. + * + * @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, example: Newtype): TypeGuard { + 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.: "53.565278 10.001389" + */ +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); diff --git a/server/shared/types/node.ts b/server/shared/types/node.ts new file mode 100644 index 0000000..ac204f4 --- /dev/null +++ b/server/shared/types/node.ts @@ -0,0 +1,483 @@ +/** + * Contains types and type guards for representing Freifunk nodes in various states. + */ +import { isObject } from "./objects"; +import { isOptional } from "./helpers"; +import { + Coordinates, + Domain, + FastdKey, + isCoordinates, + isDomain, + isFastdKey, + isMAC, + isNickname, + isSite, + MAC, + Nickname, + Site, + toIsNewtype, +} from "./newtypes"; +import { isBoolean, isString } from "./primitives"; +import { SortFieldFor, toIsSortField } from "./sortfields"; +import { EmailAddress, isEmailAddress } from "./email"; +import { isUnixTimestampSeconds, UnixTimestampSeconds } from "./time"; +import { + isMonitoringState, + isOnlineState, + MonitoringState, + OnlineState, +} from "./monitoring"; + +/** + * ID of a node in the context of the `nodes.json` of the Freifunk community's node map. + * + * This is typically the nodes lowercase MAC address without any delimiters. + */ +export type NodeId = string & { readonly __tag: unique symbol }; + +/** + * Type guard for {@link NodeId}. + * + * @param arg - Value to check. + */ +export const isNodeId = toIsNewtype(isString, "" as NodeId); + +/** + * Token of a Freifunk node registered with ffffng. This is being used to authorize a user to delete or modify the + * data being stored for a node. + * + * This token should be kept secret by the owner of the node. + */ +export type Token = string & { readonly __tag: unique symbol }; + +/** + * Type guard for {@link Token}. + * + * @param arg - Value to check. + */ +export const isToken = toIsNewtype(isString, "" as Token); + +/** + * Representation of a Freifunk node's hostname. + */ +export type Hostname = string & { readonly __tag: unique symbol }; + +/** + * Type guard for {@link Hostname}. + * + * @param arg - Value to check. + */ +export const isHostname = toIsNewtype(isString, "" as Hostname); + +/** + * ID to identify a Freifunk node on the communities map. + */ +export type MapId = string & { readonly __tag: unique symbol }; + +/** + * Type guard for {@link MapId}. + * + * @param arg - Value to check. + */ +export const isMapId = toIsNewtype(isString, "" as MapId); + +/** + * Most basic information of a Freifunk Node. + */ +export type BaseNode = { + /** + * Name / nickname that should be used to contact the node owner. + */ + nickname: Nickname; + + /** + * Email address that should be used to contact the node owner. + */ + email: EmailAddress; + + /** + * Hostname of the node that will be displayed on the community's node map. + */ + hostname: Hostname; + + /** + * Optional coordinates of the node to position it on the community's node map. + */ + coords: Coordinates | undefined; + + /** + * Optional fastd key of the node. This is the key used by the node to open a VPN tunnel to Freifunk gateways. + */ + key: FastdKey | undefined; + + /** + * MAC address of the node. This MAC address is used to identify the node at various places, e.g. when retrieving + * information of the node from the `nodes.json` of the communities node map. + */ + mac: MAC; +}; + +/** + * Type guard for {@link BaseNode}. + * + * @param arg - Value to check. + */ +export function isBaseNode(arg: unknown): arg is BaseNode { + if (!isObject(arg)) { + return false; + } + const node = arg as BaseNode; + return ( + isNickname(node.nickname) && + isEmailAddress(node.email) && + isHostname(node.hostname) && + isOptional(node.coords, isCoordinates) && + isOptional(node.key, isFastdKey) && + isMAC(node.mac) + ); +} + +/** + * Node data used when creating or updating a node. + */ +export type CreateOrUpdateNode = BaseNode & { + /** + * Whether to monitor the nodes online state and notify its owner when it's offline for a longer period of time. + */ + monitoring: boolean; +}; + +/** + * Type guard for {@link CreateOrUpdateNode}. + * + * @param arg - Value to check. + */ +export function isCreateOrUpdateNode(arg: unknown): arg is CreateOrUpdateNode { + if (!isBaseNode(arg)) { + return false; + } + const node = arg as CreateOrUpdateNode; + return isBoolean(node.monitoring); +} + +/** + * Representation of a Freifunk node as it is stored on the server. + */ +export type StoredNode = BaseNode & { + /** + * Token used to authorize a user to delete or modify the data being stored for a node. + * + * This token should be kept secret by the owner of the node. + */ + token: Token; + + /** + * State of the online monitoring for this node. + * + * See {@link MonitoringState}. + */ + monitoringState: MonitoringState; + + /** + * Last time the node data has been updated on the server. + */ + modifiedAt: UnixTimestampSeconds; +}; + +/** + * Type guard for {@link StoredNode}. + * + * @param arg - Value to check. + */ +export function isStoredNode(arg: unknown): arg is StoredNode { + if (!isObject(arg)) { + return false; + } + const node = arg as StoredNode; + return ( + isBaseNode(node) && + isToken(node.token) && + isMonitoringState(node.monitoringState) && + isUnixTimestampSeconds(node.modifiedAt) + ); +} + +/** + * Data of a Freifunk node as it is provided by the server's API. + */ +export type NodeResponse = StoredNode & { + /** + * Whether the node's online state should be monitored. + */ + monitoring: boolean; + + /** + * Specifies if the node owner has clicked the email confirmation link to enable monitoring of the online state. + */ + monitoringConfirmed: boolean; +}; + +/** + * Type guard for {@link NodeResponse}. + * + * @param arg - Value to check. + */ +export function isNodeResponse(arg: unknown): arg is NodeResponse { + if (!isStoredNode(arg)) { + return false; + } + const node = arg as NodeResponse; + return isBoolean(node.monitoring) && isBoolean(node.monitoringConfirmed); +} + +/** + * Data of a Freifunk node as it is provided by the server's API also providing the token to authorize node owners. + */ +export type NodeTokenResponse = { + /** + * Token used to authorize a user to delete or modify the data being stored for a node. + * + * This token should be kept secret by the owner of the node. + */ + token: Token; + + /** + * Data of the Freifunk node. See {@link NodeResponse}. + */ + node: NodeResponse; +}; + +/** + * Type guard for {@link NodeTokenResponse}. + * + * @param arg - Value to check. + */ +export function isNodeTokenResponse(arg: unknown): arg is NodeTokenResponse { + if (!isObject(arg)) { + return false; + } + const response = arg as NodeTokenResponse; + return ( + isToken(response.token) && + isNodeResponse(response.node) && + response.token === response.node.token + ); +} + +/** + * Represents a node in the context of a Freifunk site and domain. + */ +export type DomainSpecificNodeResponse = NodeResponse & { + /** + * Freifunk site the node resides in or `undefined` if unknown. + */ + site: Site | undefined; + + /** + * Freifunk domain the node resides in or `undefined` if unknown. + */ + domain: Domain | undefined; + + /** + * Online state of the Freifunk node or `undefined` if unknown. + */ + onlineState: OnlineState | undefined; +}; + +/** + * Type guard for {@link DomainSpecificNodeResponse}. + * + * @param arg - Value to check. + */ +export function isDomainSpecificNodeResponse( + arg: unknown +): arg is DomainSpecificNodeResponse { + if (!isNodeResponse(arg)) { + return false; + } + const node = arg as DomainSpecificNodeResponse; + return ( + isOptional(node.site, isSite) && + isOptional(node.domain, isDomain) && + isOptional(node.onlineState, isOnlineState) + ); +} + +/** + * Enum specifying the allowed sort fields when retrieving the list of nodes via the REST API. + */ +export enum NodeSortFieldEnum { + // noinspection JSUnusedGlobalSymbols + + /** + * See {@link BaseNode.hostname}. + */ + HOSTNAME = "hostname", + + /** + * See {@link BaseNode.nickname}. + */ + NICKNAME = "nickname", + + /** + * See {@link BaseNode.email}. + */ + EMAIL = "email", + + /** + * See {@link StoredNode.token}. + */ + TOKEN = "token", + + /** + * See {@link BaseNode.mac}. + */ + MAC = "mac", + + /** + * See {@link BaseNode.key}. + */ + KEY = "key", + + /** + * See {@link DomainSpecificNodeResponse.site}. + */ + SITE = "site", + + /** + * See {@link DomainSpecificNodeResponse.domain}. + */ + DOMAIN = "domain", + + /** + * See {@link BaseNode.coords}. + */ + COORDS = "coords", + + /** + * See {@link DomainSpecificNodeResponse.onlineState}. + */ + ONLINE_STATE = "onlineState", + + /** + * See {@link StoredNode.monitoringState}. + */ + MONITORING_STATE = "monitoringState", +} + +/** + * Allowed sort fields when retrieving the list of nodes via the REST API. + */ +export type NodeSortField = SortFieldFor< + DomainSpecificNodeResponse, + NodeSortFieldEnum +>; + +/** + * Type guard for {@link NodeSortField}. + * + * @param arg - Value to check. + */ +export const isNodeSortField = toIsSortField< + DomainSpecificNodeResponse, + NodeSortFieldEnum, + typeof NodeSortFieldEnum, + NodeSortField +>(NodeSortFieldEnum); + +/** + * Allowed filters when retrieving the list of nodes via the REST API. + */ +export type NodesFilter = { + /** + * If set only nodes with / without a Fastd key will be returned. + */ + hasKey?: boolean; + + /** + * If set only nodes with / without geo coordinates will be returned. + */ + hasCoords?: boolean; + + /** + * If set only nodes having the given monitoring state will be returned. + */ + monitoringState?: MonitoringState; + + /** + * If set only nodes belonging to the given Freifunk site will be returned. + */ + site?: Site; + + /** + * If set only nodes belonging to the given Freifunk domain will be returned. + */ + domain?: Domain; + + /** + * If set only nodes having the given online state will be returned. + */ + onlineState?: OnlineState; +}; + +/** + * Allowed filter fields when retrieving the list of nodes via the REST API. + */ +export const NODES_FILTER_FIELDS: Record< + keyof NodesFilter, + | BooleanConstructor + | StringConstructor + | typeof MonitoringState + | typeof OnlineState +> = { + /** + * See {@link NodesFilter.hasKey}. + */ + hasKey: Boolean, + + /** + * See {@link NodesFilter.hasCoords}. + */ + hasCoords: Boolean, + + /** + * See {@link NodesFilter.monitoringState}. + */ + monitoringState: MonitoringState, + + /** + * See {@link NodesFilter.site}. + */ + site: String, + + /** + * See {@link NodesFilter.domain}. + */ + domain: String, + + /** + * See {@link NodesFilter.onlineState}. + */ + onlineState: OnlineState, +}; + +/** + * Type guard for {@link NodesFilter}. + * + * @param arg - Value to check. + */ +export function isNodesFilter(arg: unknown): arg is NodesFilter { + if (!isObject(arg)) { + return false; + } + const filter = arg as NodesFilter; + return ( + isOptional(filter.hasKey, isBoolean) && + isOptional(filter.hasCoords, isBoolean) && + isOptional(filter.monitoringState, isMonitoringState) && + isOptional(filter.site, isSite) && + isOptional(filter.domain, isDomain) && + isOptional(filter.onlineState, isOnlineState) + ); +} diff --git a/server/shared/types/objects.ts b/server/shared/types/objects.ts new file mode 100644 index 0000000..8e27c92 --- /dev/null +++ b/server/shared/types/objects.ts @@ -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( + arg: unknown, + key: Key +): arg is Record { + return isObject(arg) && key in arg; +} diff --git a/server/shared/types/primitives.ts b/server/shared/types/primitives.ts new file mode 100644 index 0000000..d9a6acf --- /dev/null +++ b/server/shared/types/primitives.ts @@ -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"; +} diff --git a/server/shared/types/regexps.ts b/server/shared/types/regexps.ts new file mode 100644 index 0000000..2395871 --- /dev/null +++ b/server/shared/types/regexps.ts @@ -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; +} diff --git a/server/shared/types/sortfields.ts b/server/shared/types/sortfields.ts new file mode 100644 index 0000000..46c8601 --- /dev/null +++ b/server/shared/types/sortfields.ts @@ -0,0 +1,76 @@ +/** + * Contains helper types and type guards for sort fields. + */ +import { Enum, toIsEnum } from "./enums"; +import { isString } from "./primitives"; +import { TypeGuard } from "./helpers"; + +/** + * Generic untyped sort field. + */ +export type GenericSortField = { + value: string; + readonly __tag: unique symbol; +}; + +/** + * Helper to define sort field types that match an enum `` listing allowed values to a type + * `` 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; + +/** + * Helper function to construct a type guard for sort fields. + * + * Generic type parameters: + * + * * `` - The type to sort. + * * `` - The enum used to specify the field of `` to sort by. + * * `` - The `typeof` the ``. + * * `` - The type of the sort field. + * + * Warning: I could not get the compiler to ensure `` and `` refer to the same enum! + * + * @param sortFieldEnumDef - Enum representing the allowed sort fields. + * @returns A type guard for sort fields of type ``. + */ +export function toIsSortField< + SortableType, + SortFieldEnum extends keyof SortableType, + SortFieldEnumType extends Enum, + SortField extends SortFieldFor +>(sortFieldEnumDef: SortFieldEnumType): TypeGuard { + 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); diff --git a/server/shared/types/statistics.ts b/server/shared/types/statistics.ts new file mode 100644 index 0000000..17aa233 --- /dev/null +++ b/server/shared/types/statistics.ts @@ -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); +} diff --git a/server/shared/types/task.ts b/server/shared/types/task.ts new file mode 100644 index 0000000..c482cf0 --- /dev/null +++ b/server/shared/types/task.ts @@ -0,0 +1,207 @@ +/** + * Contains types and type guards all around tasks. + */ +import { toIsEnum } from "./enums"; +import { isNullable } from "./helpers"; +import { isPlainObject } from "./objects"; +import { isBoolean, isNumber, isString } from "./primitives"; +import { SortFieldFor, toIsSortField } from "./sortfields"; +import { + DurationSeconds, + isDurationSeconds, + isUnixTimestampSeconds, + UnixTimestampSeconds, +} from "./time"; + +// FIXME: Naming Task vs. Job + +/** + * The state a task can be in. + */ +export enum TaskState { + /** + * The task is not currently running. + */ + IDLE = "idle", + + /** + * The task is running. + */ + RUNNING = "running", + + /** + * The task is idle but has had a failure on its last run. + */ + FAILED = "failed", +} + +/** + * Type guard for {@link TaskState}. + * + * @param arg - Value to check. + */ +export const isTaskState = toIsEnum(TaskState); + +/** + * State the last run of the task resulted in. + */ +export enum JobResultState { + /** + * The run did finish as expected. + */ + OKAY = "okay", + + /** + * The run resulted in one or more warnings. + */ + WARNING = "warning", +} + +/** + * Type guard for {@link JobResultState}. + * + * @param arg - Value to check. + */ +export const isJobResultState = toIsEnum(JobResultState); + +/** + * Task as returned by the REST API. + */ +// TODO: Introduce newtypes. +export type TaskResponse = { + /** + * ID of the task. + */ + id: number; + + /** + * Task name as displayed in the admin panel. + */ + name: string; + + /** + * A short description of what the task does. + */ + description: string; + + /** + * The schedule of the task in classical cronjob notation. + */ + schedule: string; + + /** + * Time the current run of this task started. `null` if the task is not running. + */ + runningSince: UnixTimestampSeconds | null; + + /** + * Time the last run of this task started. `null` if the task has not run before. + */ + lastRunStarted: UnixTimestampSeconds | null; + + /** + * Duration of the last run in seconds. `null` if the task has not run before. + */ + lastRunDuration: DurationSeconds | null; + + /** + * The state the task is in. + */ + state: TaskState; + + /** + * State the last run of the task resulted in. + */ + result: JobResultState | null; + + /** + * Message of the last run, e.g. a warning. + */ + message: string | null; + + /** + * Whether the task is enabled and therefor may run. + * + * Note: A task may be running even if it is disabled if the run started befor disabling it. + */ + enabled: boolean; +}; + +/** + * Type guard for {@link TaskResponse}. + * + * @param arg - Value to check. + */ +export function isTaskResponse(arg: unknown): arg is TaskResponse { + if (!isPlainObject(arg)) { + return false; + } + + const task = arg as TaskResponse; + return ( + isNumber(task.id) && + isString(task.name) && + isString(task.description) && + isString(task.schedule) && + isNullable(task.runningSince, isUnixTimestampSeconds) && + isNullable(task.lastRunStarted, isUnixTimestampSeconds) && + isNullable(task.lastRunDuration, isDurationSeconds) && + isTaskState(task.state) && + isNullable(task.result, isJobResultState) && + isNullable(task.message, isString) && + isBoolean(task.enabled) + ); +} + +/** + * Enum specifying the allowed sort fields when retrieving the list of tasks via the REST API. + */ +export enum TaskSortFieldEnum { + // noinspection JSUnusedGlobalSymbols + /** + * See {@link TaskResponse.id}. + */ + ID = "id", + + /** + * See {@link TaskResponse.name}. + */ + NAME = "name", + + /** + * See {@link TaskResponse.schedule}. + */ + SCHEDULE = "schedule", + + /** + * See {@link TaskResponse.state}. + */ + STATE = "state", + + /** + * See {@link TaskResponse.runningSince}. + */ + RUNNING_SINCE = "runningSince", + + /** + * See {@link TaskResponse.lastRunStarted}. + */ + LAST_RUN_STARTED = "lastRunStarted", +} + +/** + * Allowed sort fields when retrieving the list of tasks via the REST API. + */ +export type TaskSortField = SortFieldFor; + +/** + * Type guard for {@link TaskSortField}. + * + * @param arg - Value to check. + */ +export const isTaskSortField = toIsSortField< + TaskResponse, + TaskSortFieldEnum, + typeof TaskSortFieldEnum, + TaskSortField +>(TaskSortFieldEnum); diff --git a/server/shared/types/time.ts b/server/shared/types/time.ts new file mode 100644 index 0000000..5c2fa20 --- /dev/null +++ b/server/shared/types/time.ts @@ -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 +); diff --git a/server/shared/utils/enums.ts b/server/shared/utils/enums.ts new file mode 100644 index 0000000..17fb5cf --- /dev/null +++ b/server/shared/utils/enums.ts @@ -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}`); +} diff --git a/server/shared/utils/json.ts b/server/shared/utils/json.ts new file mode 100644 index 0000000..93d2791 --- /dev/null +++ b/server/shared/utils/json.ts @@ -0,0 +1,42 @@ +/** + * Utility functions for JSON. + */ +import { isJSONValue, JSONObject, JSONValue } from "../types"; + +/** + * Parses the given `string` and converts it into a {@link JSONValue}. + * + * For the string to be considered valid JSON it has to satisfy the requirements for {@link JSON.parse}. + * + * @param str - `string` to parse. + * @returns The parsed integer JSON value. + * @throws {@link SyntaxError} - If the given `string` does not represent a valid JSON value. + */ +export function parseJSON(str: string): JSONValue { + const json = JSON.parse(str); + if (!isJSONValue(json)) { + throw new Error("Invalid JSON returned. Should never happen."); + } + return json; +} + +/** + * Removes `undefined` fields from the given JSON'ish object to make it a valid {@link JSONObject}. + * + * Note: This only happens for fields directly belonging to the given object. No recursive cleanup is performed. + * + * @param obj - Object to remove `undefined` fields from. + * @returns Cleaned up JSON object. + */ +export function filterUndefinedFromJSON(obj: { + [key: string]: JSONValue | undefined; +}): JSONObject { + const result: JSONObject = {}; + for (const [key, value] of Object.entries(obj)) { + if (value !== undefined) { + result[key] = value; + } + } + + return result; +} diff --git a/server/shared/utils/node.ts b/server/shared/utils/node.ts new file mode 100644 index 0000000..d3fa480 --- /dev/null +++ b/server/shared/utils/node.ts @@ -0,0 +1,14 @@ +/** + * Utility functions for node related data. + */ +import { MAC, MapId } from "../types"; + +/** + * Converts the MAC address of a Freifunk node to an id representing it on the community's node map. + * + * @param mac - MAC address of the node + * @returns ID of the node on the map + */ +export function mapIdFromMAC(mac: MAC): MapId { + return mac.toLowerCase().replace(/:/g, "") as MapId; +} diff --git a/server/shared/utils/objects.ts b/server/shared/utils/objects.ts new file mode 100644 index 0000000..ae21b70 --- /dev/null +++ b/server/shared/utils/objects.ts @@ -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; +} diff --git a/server/shared/utils/strings.ts b/server/shared/utils/strings.ts index ff92f2d..5413888 100644 --- a/server/shared/utils/strings.ts +++ b/server/shared/utils/strings.ts @@ -1,9 +1,29 @@ -import { isString, MAC } from "../types"; +/** + * Utility functions all around strings. + */ +import { isInteger, MAC } from "../types"; +/** + * Trims the given `string` and replaces multiple whitespaces by one space each. + * + * Can be used to make sure user input has a canonical form. + * + * @param str - `string` to normalize. + * @returns The normalized `string`. + */ export function normalizeString(str: string): string { - 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( diff --git a/server/shared/utils/time.ts b/server/shared/utils/time.ts new file mode 100644 index 0000000..e530397 --- /dev/null +++ b/server/shared/utils/time.ts @@ -0,0 +1,28 @@ +/** + * Utility functions for "wibbly wobbly timey wimey" stuff. + */ +import { UnixTimestampMilliseconds, UnixTimestampSeconds } from "../types"; + +/** + * Converts an {@link UnixTimestampMilliseconds} to an {@link UnixTimestampSeconds} rounding down. + * + * @param ms - The timestamp in milliseconds. + * @returns - The timestamp in seconds. + */ +export function toUnixTimestampSeconds( + ms: UnixTimestampMilliseconds +): UnixTimestampSeconds { + return Math.floor(ms / 1000) as UnixTimestampSeconds; +} + +/** + * Converts an {@link UnixTimestampSeconds} to an {@link UnixTimestampMilliseconds}. + * + * @param s - The timestamp in seconds. + * @returns - The timestamp in milliseconds. + */ +export function toUnixTimestampMilliseconds( + s: UnixTimestampSeconds +): UnixTimestampMilliseconds { + return (s * 1000) as UnixTimestampMilliseconds; +} diff --git a/server/utils/resources.ts b/server/utils/resources.ts index e218f1a..4ebcbbf 100644 --- a/server/utils/resources.ts +++ b/server/utils/resources.ts @@ -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( +function orderByClause( restParams: RestParams, - defaultSortField: ValueOf, - isSortField: EnumTypeGuard + defaultSortField: SortField, + isSortField: TypeGuard ): OrderByClause { - let sortField: ValueOf | undefined = isSortField(restParams._sortField) + let sortField: SortField | undefined = isSortField(restParams._sortField) ? restParams._sortField : undefined; if (!sortField) { @@ -345,13 +343,17 @@ export function getPageEntities( export { filterCondition as whereCondition }; -export function filterClause( +export function filterClause( restParams: RestParams, - defaultSortField: ValueOf, - isSortField: EnumTypeGuard, + defaultSortField: SortField, + isSortField: TypeGuard, filterFields: string[] ): FilterClause { - const orderBy = orderByClause(restParams, defaultSortField, isSortField); + const orderBy = orderByClause( + restParams, + defaultSortField, + isSortField + ); const limitOffset = limitOffsetClause(restParams); const filter = filterCondition(restParams, filterFields); From 894ee97fdfc32e4d2c3b3cbd26f8c1f017ee7b1e Mon Sep 17 00:00:00 2001 From: baldo Date: Wed, 14 Sep 2022 15:56:16 +0200 Subject: [PATCH 3/3] Update server dependencies. --- jest.server.config.js | 19 +- package.json | 20 +- yarn.lock | 694 ++++++++++++++++++++---------------------- 3 files changed, 355 insertions(+), 378 deletions(-) diff --git a/jest.server.config.js b/jest.server.config.js index a5505d0..3fd4b98 100644 --- a/jest.server.config.js +++ b/jest.server.config.js @@ -1,10 +1,13 @@ module.exports = { - preset: 'ts-jest', - testEnvironment: 'node', - roots: ['/server'], - globals: { - 'ts-jest': { - tsconfig: '/server/tsconfig.json' - } - } + preset: "ts-jest", + testEnvironment: "node", + roots: ["/server"], + transform: { + "^.+\\.tsx?$": [ + "ts-jest", + { + tsconfig: "/server/tsconfig.json", + }, + ], + }, }; diff --git a/package.json b/package.json index cd05816..c50a676 100644 --- a/package.json +++ b/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": { diff --git a/yarn.lock b/yarn.lock index c17cfe3..2bcd138 100644 --- a/yarn.lock +++ b/yarn.lock @@ -435,28 +435,28 @@ resolved "https://registry.yarnpkg.com/@istanbuljs/schema/-/schema-0.1.3.tgz#e45e384e4b8ec16bce2fd903af78450f6bf7ec98" integrity sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA== -"@jest/console@^29.0.2": - version "29.0.2" - resolved "https://registry.yarnpkg.com/@jest/console/-/console-29.0.2.tgz#3a02dccad4dd37c25fd30013df67ec50998402ce" - integrity sha512-Fv02ijyhF4D/Wb3DvZO3iBJQz5DnzpJEIDBDbvje8Em099N889tNMUnBw7SalmSuOI+NflNG40RA1iK71kImPw== +"@jest/console@^29.0.3": + version "29.0.3" + resolved "https://registry.yarnpkg.com/@jest/console/-/console-29.0.3.tgz#a222ab87e399317a89db88a58eaec289519e807a" + integrity sha512-cGg0r+klVHSYnfE977S9wmpuQ9L+iYuYgL+5bPXiUlUynLLYunRxswEmhBzvrSKGof5AKiHuTTmUKAqRcDY9dg== dependencies: - "@jest/types" "^29.0.2" + "@jest/types" "^29.0.3" "@types/node" "*" chalk "^4.0.0" - jest-message-util "^29.0.2" - jest-util "^29.0.2" + jest-message-util "^29.0.3" + jest-util "^29.0.3" slash "^3.0.0" -"@jest/core@^29.0.2": - version "29.0.2" - resolved "https://registry.yarnpkg.com/@jest/core/-/core-29.0.2.tgz#7bf47ff6cd882678c47fbdea562bdf1ff03b6d33" - integrity sha512-imP5M6cdpHEOkmcuFYZuM5cTG1DAF7ZlVNCq1+F7kbqme2Jcl+Kh4M78hihM76DJHNkurbv4UVOnejGxBKEmww== +"@jest/core@^29.0.3": + version "29.0.3" + resolved "https://registry.yarnpkg.com/@jest/core/-/core-29.0.3.tgz#ba22a9cbd0c7ba36e04292e2093c547bf53ec1fd" + integrity sha512-1d0hLbOrM1qQE3eP3DtakeMbKTcXiXP3afWxqz103xPyddS2NhnNghS7MaXx1dcDt4/6p4nlhmeILo2ofgi8cQ== dependencies: - "@jest/console" "^29.0.2" - "@jest/reporters" "^29.0.2" - "@jest/test-result" "^29.0.2" - "@jest/transform" "^29.0.2" - "@jest/types" "^29.0.2" + "@jest/console" "^29.0.3" + "@jest/reporters" "^29.0.3" + "@jest/test-result" "^29.0.3" + "@jest/transform" "^29.0.3" + "@jest/types" "^29.0.3" "@types/node" "*" ansi-escapes "^4.2.1" chalk "^4.0.0" @@ -464,32 +464,32 @@ exit "^0.1.2" graceful-fs "^4.2.9" jest-changed-files "^29.0.0" - jest-config "^29.0.2" - jest-haste-map "^29.0.2" - jest-message-util "^29.0.2" + jest-config "^29.0.3" + jest-haste-map "^29.0.3" + jest-message-util "^29.0.3" jest-regex-util "^29.0.0" - jest-resolve "^29.0.2" - jest-resolve-dependencies "^29.0.2" - jest-runner "^29.0.2" - jest-runtime "^29.0.2" - jest-snapshot "^29.0.2" - jest-util "^29.0.2" - jest-validate "^29.0.2" - jest-watcher "^29.0.2" + jest-resolve "^29.0.3" + jest-resolve-dependencies "^29.0.3" + jest-runner "^29.0.3" + jest-runtime "^29.0.3" + jest-snapshot "^29.0.3" + jest-util "^29.0.3" + jest-validate "^29.0.3" + jest-watcher "^29.0.3" micromatch "^4.0.4" - pretty-format "^29.0.2" + pretty-format "^29.0.3" slash "^3.0.0" strip-ansi "^6.0.0" -"@jest/environment@^29.0.2": - version "29.0.2" - resolved "https://registry.yarnpkg.com/@jest/environment/-/environment-29.0.2.tgz#9e4b6d4c9bce5bfced6f63945d8c8e571394f572" - integrity sha512-Yf+EYaLOrVCgts/aTS5nGznU4prZUPa5k9S63Yct8YSOKj2jkdS17hHSUKhk5jxDFMyCy1PXknypDw7vfgc/mA== +"@jest/environment@^29.0.3": + version "29.0.3" + resolved "https://registry.yarnpkg.com/@jest/environment/-/environment-29.0.3.tgz#7745ec30a954e828e8cc6df6a13280d3b51d8f35" + integrity sha512-iKl272NKxYNQNqXMQandAIwjhQaGw5uJfGXduu8dS9llHi8jV2ChWrtOAVPnMbaaoDhnI3wgUGNDvZgHeEJQCA== dependencies: - "@jest/fake-timers" "^29.0.2" - "@jest/types" "^29.0.2" + "@jest/fake-timers" "^29.0.3" + "@jest/types" "^29.0.3" "@types/node" "*" - jest-mock "^29.0.2" + jest-mock "^29.0.3" "@jest/expect-utils@^29.0.1": version "29.0.1" @@ -498,53 +498,53 @@ dependencies: jest-get-type "^29.0.0" -"@jest/expect-utils@^29.0.2": - version "29.0.2" - resolved "https://registry.yarnpkg.com/@jest/expect-utils/-/expect-utils-29.0.2.tgz#00dfcb9e6fe99160c326ba39f7734b984543dea8" - integrity sha512-+wcQF9khXKvAEi8VwROnCWWmHfsJYCZAs5dmuMlJBKk57S6ZN2/FQMIlo01F29fJyT8kV/xblE7g3vkIdTLOjw== +"@jest/expect-utils@^29.0.3": + version "29.0.3" + resolved "https://registry.yarnpkg.com/@jest/expect-utils/-/expect-utils-29.0.3.tgz#f5bb86f5565bf2dacfca31ccbd887684936045b2" + integrity sha512-i1xUkau7K/63MpdwiRqaxgZOjxYs4f0WMTGJnYwUKubsNRZSeQbLorS7+I4uXVF9KQ5r61BUPAUMZ7Lf66l64Q== dependencies: jest-get-type "^29.0.0" -"@jest/expect@^29.0.2": - version "29.0.2" - resolved "https://registry.yarnpkg.com/@jest/expect/-/expect-29.0.2.tgz#641d151e1062ceb976c5ad1c23eba3bb1e188896" - integrity sha512-y/3geZ92p2/zovBm/F+ZjXUJ3thvT9IRzD6igqaWskFE2aR0idD+N/p5Lj/ZautEox/9RwEc6nqergebeh72uQ== +"@jest/expect@^29.0.3": + version "29.0.3" + resolved "https://registry.yarnpkg.com/@jest/expect/-/expect-29.0.3.tgz#9dc7c46354eeb7a348d73881fba6402f5fdb2c30" + integrity sha512-6W7K+fsI23FQ01H/BWccPyDZFrnU9QlzDcKOjrNVU5L8yUORFAJJIpmyxWPW70+X624KUNqzZwPThPMX28aXEQ== dependencies: - expect "^29.0.2" - jest-snapshot "^29.0.2" + expect "^29.0.3" + jest-snapshot "^29.0.3" -"@jest/fake-timers@^29.0.2": - version "29.0.2" - resolved "https://registry.yarnpkg.com/@jest/fake-timers/-/fake-timers-29.0.2.tgz#6f15f4d8eb1089d445e3f73473ddc434faa2f798" - integrity sha512-2JhQeWU28fvmM5r33lxg6BxxkTKaVXs6KMaJ6eXSM8ml/MaWkt2BvbIO8G9KWAJFMdBXWbn+2h9OK1/s5urKZA== +"@jest/fake-timers@^29.0.3": + version "29.0.3" + resolved "https://registry.yarnpkg.com/@jest/fake-timers/-/fake-timers-29.0.3.tgz#ad5432639b715d45a86a75c47fd75019bc36b22c" + integrity sha512-tmbUIo03x0TdtcZCESQ0oQSakPCpo7+s6+9mU19dd71MptkP4zCwoeZqna23//pgbhtT1Wq02VmA9Z9cNtvtCQ== dependencies: - "@jest/types" "^29.0.2" + "@jest/types" "^29.0.3" "@sinonjs/fake-timers" "^9.1.2" "@types/node" "*" - jest-message-util "^29.0.2" - jest-mock "^29.0.2" - jest-util "^29.0.2" + jest-message-util "^29.0.3" + jest-mock "^29.0.3" + jest-util "^29.0.3" -"@jest/globals@^29.0.2": - version "29.0.2" - resolved "https://registry.yarnpkg.com/@jest/globals/-/globals-29.0.2.tgz#605d3389ad0c6bfe17ad3e1359b5bc39aefd8b65" - integrity sha512-4hcooSNJCVXuTu07/VJwCWW6HTnjLtQdqlcGisK6JST7z2ixa8emw4SkYsOk7j36WRc2ZUEydlUePnOIOTCNXg== +"@jest/globals@^29.0.3": + version "29.0.3" + resolved "https://registry.yarnpkg.com/@jest/globals/-/globals-29.0.3.tgz#681950c430fdc13ff9aa89b2d8d572ac0e4a1bf5" + integrity sha512-YqGHT65rFY2siPIHHFjuCGUsbzRjdqkwbat+Of6DmYRg5shIXXrLdZoVE/+TJ9O1dsKsFmYhU58JvIbZRU1Z9w== dependencies: - "@jest/environment" "^29.0.2" - "@jest/expect" "^29.0.2" - "@jest/types" "^29.0.2" - jest-mock "^29.0.2" + "@jest/environment" "^29.0.3" + "@jest/expect" "^29.0.3" + "@jest/types" "^29.0.3" + jest-mock "^29.0.3" -"@jest/reporters@^29.0.2": - version "29.0.2" - resolved "https://registry.yarnpkg.com/@jest/reporters/-/reporters-29.0.2.tgz#5f927646b6f01029525c05ac108324eac7d7ad5c" - integrity sha512-Kr41qejRQHHkCgWHC9YwSe7D5xivqP4XML+PvgwsnRFaykKdNflDUb4+xLXySOU+O/bPkVdFpGzUpVNSJChCrw== +"@jest/reporters@^29.0.3": + version "29.0.3" + resolved "https://registry.yarnpkg.com/@jest/reporters/-/reporters-29.0.3.tgz#735f110e08b44b38729d8dbbb74063bdf5aba8a5" + integrity sha512-3+QU3d4aiyOWfmk1obDerie4XNCaD5Xo1IlKNde2yGEi02WQD+ZQD0i5Hgqm1e73sMV7kw6pMlCnprtEwEVwxw== dependencies: "@bcoe/v8-coverage" "^0.2.3" - "@jest/console" "^29.0.2" - "@jest/test-result" "^29.0.2" - "@jest/transform" "^29.0.2" - "@jest/types" "^29.0.2" + "@jest/console" "^29.0.3" + "@jest/test-result" "^29.0.3" + "@jest/transform" "^29.0.3" + "@jest/types" "^29.0.3" "@jridgewell/trace-mapping" "^0.3.15" "@types/node" "*" chalk "^4.0.0" @@ -557,22 +557,15 @@ istanbul-lib-report "^3.0.0" istanbul-lib-source-maps "^4.0.0" istanbul-reports "^3.1.3" - jest-message-util "^29.0.2" - jest-util "^29.0.2" - jest-worker "^29.0.2" + jest-message-util "^29.0.3" + jest-util "^29.0.3" + jest-worker "^29.0.3" slash "^3.0.0" string-length "^4.0.1" strip-ansi "^6.0.0" terminal-link "^2.0.0" v8-to-istanbul "^9.0.1" -"@jest/schemas@^28.0.2": - version "28.0.2" - resolved "https://registry.yarnpkg.com/@jest/schemas/-/schemas-28.0.2.tgz#08c30df6a8d07eafea0aef9fb222c5e26d72e613" - integrity sha512-YVDJZjd4izeTDkij00vHHAymNXQ6WWsdChFRK86qck6Jpr3DCL5W3Is3vslviRlP+bLuMYRLbdp98amMvqudhA== - dependencies: - "@sinclair/typebox" "^0.23.3" - "@jest/schemas@^29.0.0": version "29.0.0" resolved "https://registry.yarnpkg.com/@jest/schemas/-/schemas-29.0.0.tgz#5f47f5994dd4ef067fb7b4188ceac45f77fe952a" @@ -589,59 +582,47 @@ callsites "^3.0.0" graceful-fs "^4.2.9" -"@jest/test-result@^29.0.2": - version "29.0.2" - resolved "https://registry.yarnpkg.com/@jest/test-result/-/test-result-29.0.2.tgz#dde4922e6234dd311c85ddf1ec2b7f600a90295d" - integrity sha512-b5rDc0lLL6Kx73LyCx6370k9uZ8o5UKdCpMS6Za3ke7H9y8PtAU305y6TeghpBmf2In8p/qqi3GpftgzijSsNw== +"@jest/test-result@^29.0.3": + version "29.0.3" + resolved "https://registry.yarnpkg.com/@jest/test-result/-/test-result-29.0.3.tgz#b03d8ef4c58be84cd5d5d3b24d4b4c8cabbf2746" + integrity sha512-vViVnQjCgTmbhDKEonKJPtcFe9G/CJO4/Np4XwYJah+lF2oI7KKeRp8t1dFvv44wN2NdbDb/qC6pi++Vpp0Dlg== dependencies: - "@jest/console" "^29.0.2" - "@jest/types" "^29.0.2" + "@jest/console" "^29.0.3" + "@jest/types" "^29.0.3" "@types/istanbul-lib-coverage" "^2.0.0" collect-v8-coverage "^1.0.0" -"@jest/test-sequencer@^29.0.2": - version "29.0.2" - resolved "https://registry.yarnpkg.com/@jest/test-sequencer/-/test-sequencer-29.0.2.tgz#ae9b2d2c1694c7aa1a407713100e14dbfa79293e" - integrity sha512-fsyZqHBlXNMv5ZqjQwCuYa2pskXCO0DVxh5aaVCuAtwzHuYEGrhordyEncBLQNuCGQSYgElrEEmS+7wwFnnMKw== +"@jest/test-sequencer@^29.0.3": + version "29.0.3" + resolved "https://registry.yarnpkg.com/@jest/test-sequencer/-/test-sequencer-29.0.3.tgz#0681061ad21fb8e293b49c4fdf7e631ca79240ba" + integrity sha512-Hf4+xYSWZdxTNnhDykr8JBs0yBN/nxOXyUQWfotBUqqy0LF9vzcFB0jm/EDNZCx587znLWTIgxcokW7WeZMobQ== dependencies: - "@jest/test-result" "^29.0.2" + "@jest/test-result" "^29.0.3" graceful-fs "^4.2.9" - jest-haste-map "^29.0.2" + jest-haste-map "^29.0.3" slash "^3.0.0" -"@jest/transform@^29.0.2": - version "29.0.2" - resolved "https://registry.yarnpkg.com/@jest/transform/-/transform-29.0.2.tgz#eef90ebd939b68bf2c2508d9e914377871869146" - integrity sha512-lajVQx2AnsR+Pa17q2zR7eikz2PkPs1+g/qPbZkqQATeS/s6eT55H+yHcsLfuI/0YQ/4VSBepSu3bOX+44q0aA== +"@jest/transform@^29.0.3": + version "29.0.3" + resolved "https://registry.yarnpkg.com/@jest/transform/-/transform-29.0.3.tgz#9eb1fed2072a0354f190569807d1250572fb0970" + integrity sha512-C5ihFTRYaGDbi/xbRQRdbo5ddGtI4VSpmL6AIcZxdhwLbXMa7PcXxxqyI91vGOFHnn5aVM3WYnYKCHEqmLVGzg== dependencies: "@babel/core" "^7.11.6" - "@jest/types" "^29.0.2" + "@jest/types" "^29.0.3" "@jridgewell/trace-mapping" "^0.3.15" babel-plugin-istanbul "^6.1.1" chalk "^4.0.0" convert-source-map "^1.4.0" fast-json-stable-stringify "^2.1.0" graceful-fs "^4.2.9" - jest-haste-map "^29.0.2" + jest-haste-map "^29.0.3" jest-regex-util "^29.0.0" - jest-util "^29.0.2" + jest-util "^29.0.3" micromatch "^4.0.4" pirates "^4.0.4" slash "^3.0.0" write-file-atomic "^4.0.1" -"@jest/types@^28.1.0": - version "28.1.0" - resolved "https://registry.yarnpkg.com/@jest/types/-/types-28.1.0.tgz#508327a89976cbf9bd3e1cc74641a29fd7dfd519" - integrity sha512-xmEggMPr317MIOjjDoZ4ejCSr9Lpbt/u34+dvc99t7DS8YirW5rwZEhzKPC2BMUFkUhI48qs6qLUSGw5FuL0GA== - dependencies: - "@jest/schemas" "^28.0.2" - "@types/istanbul-lib-coverage" "^2.0.0" - "@types/istanbul-reports" "^3.0.0" - "@types/node" "*" - "@types/yargs" "^17.0.8" - chalk "^4.0.0" - "@jest/types@^29.0.1": version "29.0.1" resolved "https://registry.yarnpkg.com/@jest/types/-/types-29.0.1.tgz#1985650acf137bdb81710ff39a4689ec071dd86a" @@ -654,10 +635,10 @@ "@types/yargs" "^17.0.8" chalk "^4.0.0" -"@jest/types@^29.0.2": - version "29.0.2" - resolved "https://registry.yarnpkg.com/@jest/types/-/types-29.0.2.tgz#5a5391fa7f7f41bf4b201d6d2da30e874f95b6c1" - integrity sha512-5WNMesBLmlkt1+fVkoCjHa0X3i3q8zc4QLTDkdHgCa2gyPZc7rdlZBWgVLqwS1860ZW5xJuCDwAzqbGaXIr/ew== +"@jest/types@^29.0.3": + version "29.0.3" + resolved "https://registry.yarnpkg.com/@jest/types/-/types-29.0.3.tgz#0be78fdddb1a35aeb2041074e55b860561c8ef63" + integrity sha512-coBJmOQvurXjN1Hh5PzF7cmsod0zLIOXpP8KD161mqNlroMhLcwpODiEzi7ZsRl5Z/AIuxpeNm8DCl43F4kz8A== dependencies: "@jest/schemas" "^29.0.0" "@types/istanbul-lib-coverage" "^2.0.0" @@ -787,11 +768,6 @@ domhandler "^4.2.0" selderee "^0.6.0" -"@sinclair/typebox@^0.23.3": - version "0.23.5" - resolved "https://registry.yarnpkg.com/@sinclair/typebox/-/typebox-0.23.5.tgz#93f7b9f4e3285a7a9ade7557d9a8d36809cbc47d" - integrity sha512-AFBVi/iT4g20DHoujvMH1aEDn8fGJh4xsRGCP6d8RpLPMqsNPvW01Jcn0QysXTsg++/xj25NmJsGyH9xug/wKg== - "@sinclair/typebox@^0.24.1": version "0.24.19" resolved "https://registry.yarnpkg.com/@sinclair/typebox/-/typebox-0.24.19.tgz#5297278e0d8a1aea084685a3216074910ac6c113" @@ -912,7 +888,7 @@ "@types/qs" "*" "@types/range-parser" "*" -"@types/express@*", "@types/express@^4.17.13": +"@types/express@*": version "4.17.13" resolved "https://registry.yarnpkg.com/@types/express/-/express-4.17.13.tgz#a76e2995728999bab51a33fabce1d705a3709034" integrity sha512-6bSZTPaTIACxn48l50SR+axgrqm6qXFIxrdAKaG6PaJk3+zuUr35hBlgT7vOmJcum+OEaIBLtHV/qloEAFITeA== @@ -922,6 +898,16 @@ "@types/qs" "*" "@types/serve-static" "*" +"@types/express@^4.17.14": + version "4.17.14" + resolved "https://registry.yarnpkg.com/@types/express/-/express-4.17.14.tgz#143ea0557249bc1b3b54f15db4c81c3d4eb3569c" + integrity sha512-TEbt+vaPFQ+xpxFLFssxUDXj5cWCxZJjIcB7Yg0k0GMHGtgtQgpvx/MUQUeAkNbA9AAGrwkAsoeItdTgS7FMyg== + dependencies: + "@types/body-parser" "*" + "@types/express-serve-static-core" "^4.17.18" + "@types/qs" "*" + "@types/serve-static" "*" + "@types/find-cache-dir@^3.2.1": version "3.2.1" resolved "https://registry.yarnpkg.com/@types/find-cache-dir/-/find-cache-dir-3.2.1.tgz#7b959a4b9643a1e6a1a5fe49032693cc36773501" @@ -973,10 +959,10 @@ dependencies: "@types/istanbul-lib-report" "*" -"@types/jest@^29.0.0": - version "29.0.0" - resolved "https://registry.yarnpkg.com/@types/jest/-/jest-29.0.0.tgz#bc66835bf6b09d6a47e22c21d7f5b82692e60e72" - integrity sha512-X6Zjz3WO4cT39Gkl0lZ2baFRaEMqJl5NC1OjElkwtNzAlbkr2K/WJXkBkH5VP0zx4Hgsd2TZYdOEfvp2Dxia+Q== +"@types/jest@^29.0.2": + version "29.0.2" + resolved "https://registry.yarnpkg.com/@types/jest/-/jest-29.0.2.tgz#05dcb2d78d2fcc444be89f95b7389f2c3601d336" + integrity sha512-TaklkwSEtvwJpleiKBHgEBySIQlcZ08gYP/s5wdtdLnjz9uxjnDd7U+Y0JWACebkqBc+jtbol2PEtEW0wQV2zQ== dependencies: expect "^29.0.0" pretty-format "^29.0.0" @@ -998,10 +984,10 @@ resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.182.tgz#05301a4d5e62963227eaafe0ce04dd77c54ea5c2" integrity sha512-/THyiqyQAP9AfARo4pF+aCGcyiQ94tX/Is2I7HofNRqoYLgN1PBoOWu2/zTA5zMxzP5EFutMtWtGAFRKUe961Q== -"@types/lodash@^4.14.184": - version "4.14.184" - resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.184.tgz#23f96cd2a21a28e106dc24d825d4aa966de7a9fe" - integrity sha512-RoZphVtHbxPZizt4IcILciSWiC6dcn+eZ8oX9IWEYfDMcocdd42f7NPI6fQj+6zI8y4E0L7gu2pcZKLGTRaV9Q== +"@types/lodash@^4.14.185": + version "4.14.185" + resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.185.tgz#c9843f5a40703a8f5edfd53358a58ae729816908" + integrity sha512-evMDG1bC4rgQg4ku9tKpuMh5iBNEwNa3tf9zRHdP1qlv+1WUg44xat4IxCE14gIpZRGUUWAx2VhItCZc25NfMA== "@types/mime@^1": version "1.3.2" @@ -1013,25 +999,25 @@ resolved "https://registry.yarnpkg.com/@types/minimatch/-/minimatch-3.0.5.tgz#1001cc5e6a3704b83c236027e77f2f58ea010f40" integrity sha512-Klz949h02Gz2uZCMGwDUSDS1YBlTdDDgbWHi+81l29tQALUtvz4rAYi5uoVhE5Lagoq6DeqAUlbrHvW/mXDgdQ== -"@types/node-cron@^3.0.3": - version "3.0.3" - resolved "https://registry.yarnpkg.com/@types/node-cron/-/node-cron-3.0.3.tgz#6d7a7bebecb20d83c850421d4d3178a7555be538" - integrity sha512-FPzux/llEiCe5mPn3TvLEORcF2pRXvH5cugtJCJf+UrkwQ7pYfb4wn9J/sxJ8QkT/sw9BjWSi9uur5Vh1OuAZQ== +"@types/node-cron@^3.0.4": + version "3.0.4" + resolved "https://registry.yarnpkg.com/@types/node-cron/-/node-cron-3.0.4.tgz#ade755125a5b9e409ba9598e04c7132a05c108db" + integrity sha512-A2H+uz5ry4hohYjRe5mQSE/8Dx/HGw4WZ728JxhKUZ7z8CMvRuG2tpbzGHRGQCuQzz5aCNB1iXzPZYHd4BPHvw== "@types/node@*": version "17.0.34" resolved "https://registry.yarnpkg.com/@types/node/-/node-17.0.34.tgz#3b0b6a50ff797280b8d000c6281d229f9c538cef" integrity sha512-XImEz7XwTvDBtzlTnm8YvMqGW/ErMWBsKZ+hMTvnDIjGCKxwK5Xpc+c/oQjOauwq8M4OS11hEkpjX8rrI/eEgA== -"@types/node@^18.7.15": - version "18.7.15" - resolved "https://registry.yarnpkg.com/@types/node/-/node-18.7.15.tgz#20ae1ec80c57ee844b469f968a1cd511d4088b29" - integrity sha512-XnjpaI8Bgc3eBag2Aw4t2Uj/49lLBSStHWfqKvIuXD7FIrZyMLWp8KuAFHAqxMZYTF9l08N1ctUn9YNybZJVmQ== +"@types/node@^18.7.18": + version "18.7.18" + resolved "https://registry.yarnpkg.com/@types/node/-/node-18.7.18.tgz#633184f55c322e4fb08612307c274ee6d5ed3154" + integrity sha512-m+6nTEOadJZuTPkKR/SYK3A2d7FZrgElol9UP1Kae90VVU4a6mxnPuLiIW1m4Cq4gZ/nWb9GrdVXJCoCazDAbg== -"@types/nodemailer@^6.4.5": - version "6.4.5" - resolved "https://registry.yarnpkg.com/@types/nodemailer/-/nodemailer-6.4.5.tgz#09011ac73259245475d1688e4ba101860567dc39" - integrity sha512-zuP3nBRQHI6M2PkXnGGy1Ww4VB+MyYHGgnfV2T+JR9KLkeWqPJuyVUgLpKXuFnA/b7pZaIDFh2sV4759B7jK1g== +"@types/nodemailer@^6.4.6": + version "6.4.6" + resolved "https://registry.yarnpkg.com/@types/nodemailer/-/nodemailer-6.4.6.tgz#ce21b4b474a08f672f182e15982b7945dde1f288" + integrity sha512-pD6fL5GQtUKvD2WnPmg5bC2e8kWCAPDwMPmHe/ohQbW+Dy0EcHgZ2oCSuPlWNqk74LS5BVMig1SymQbFMPPK3w== dependencies: "@types/node" "*" @@ -1573,12 +1559,12 @@ aws4@^1.8.0: resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.11.0.tgz#d61f46d83b2519250e2784daf5b09479a8b41c59" integrity sha512-xh1Rl34h6Fi1DC2WWKfxUTVqRsNnr6LsKz2+hfwDxQJWmrx8+c7ylaqBMcHfl1U1r2dsifOvKX3LQuLNZ+XSvA== -babel-jest@^29.0.2: - version "29.0.2" - resolved "https://registry.yarnpkg.com/babel-jest/-/babel-jest-29.0.2.tgz#7efde496c07607949e9be499bf277aa1543ded95" - integrity sha512-yTu4/WSi/HzarjQtrJSwV+/0maoNt+iP0DmpvFJdv9yY+5BuNle8TbheHzzcSWj5gIHfuhpbLYHWRDYhWKyeKQ== +babel-jest@^29.0.3: + version "29.0.3" + resolved "https://registry.yarnpkg.com/babel-jest/-/babel-jest-29.0.3.tgz#64e156a47a77588db6a669a88dedff27ed6e260f" + integrity sha512-ApPyHSOhS/sVzwUOQIWJmdvDhBsMG01HX9z7ogtkp1TToHGGUWFlnXJUIzCgKPSfiYLn3ibipCYzsKSURHEwLg== dependencies: - "@jest/transform" "^29.0.2" + "@jest/transform" "^29.0.3" "@types/babel__core" "^7.1.14" babel-plugin-istanbul "^6.1.1" babel-preset-jest "^29.0.2" @@ -2893,16 +2879,16 @@ expect@^29.0.0: jest-message-util "^29.0.1" jest-util "^29.0.1" -expect@^29.0.2: - version "29.0.2" - resolved "https://registry.yarnpkg.com/expect/-/expect-29.0.2.tgz#22c7132400f60444b427211f1d6bb604a9ab2420" - integrity sha512-JeJlAiLKn4aApT4pzUXBVxl3NaZidWIOdg//smaIlP9ZMBDkHZGFd9ubphUZP9pUyDEo7bC6M0IIZR51o75qQw== +expect@^29.0.3: + version "29.0.3" + resolved "https://registry.yarnpkg.com/expect/-/expect-29.0.3.tgz#6be65ddb945202f143c4e07c083f4f39f3bd326f" + integrity sha512-t8l5DTws3212VbmPL+tBFXhjRHLmctHB0oQbL8eUc6S7NzZtYUhycrFO9mkxA0ZUC6FAWdNi7JchJSkODtcu1Q== dependencies: - "@jest/expect-utils" "^29.0.2" + "@jest/expect-utils" "^29.0.3" jest-get-type "^29.0.0" - jest-matcher-utils "^29.0.2" - jest-message-util "^29.0.2" - jest-util "^29.0.2" + jest-matcher-utils "^29.0.3" + jest-message-util "^29.0.3" + jest-util "^29.0.3" express@^4.17.2: version "4.18.1" @@ -4375,74 +4361,74 @@ jest-changed-files@^29.0.0: execa "^5.0.0" p-limit "^3.1.0" -jest-circus@^29.0.2: - version "29.0.2" - resolved "https://registry.yarnpkg.com/jest-circus/-/jest-circus-29.0.2.tgz#7dda94888a8d47edb58e85a8e5f688f9da6657a3" - integrity sha512-YTPEsoE1P1X0bcyDQi3QIkpt2Wl9om9k2DQRuLFdS5x8VvAKSdYAVJufgvudhnKgM8WHvvAzhBE+1DRQB8x1CQ== +jest-circus@^29.0.3: + version "29.0.3" + resolved "https://registry.yarnpkg.com/jest-circus/-/jest-circus-29.0.3.tgz#90faebc90295291cfc636b27dbd82e3bfb9e7a48" + integrity sha512-QeGzagC6Hw5pP+df1+aoF8+FBSgkPmraC1UdkeunWh0jmrp7wC0Hr6umdUAOELBQmxtKAOMNC3KAdjmCds92Zg== dependencies: - "@jest/environment" "^29.0.2" - "@jest/expect" "^29.0.2" - "@jest/test-result" "^29.0.2" - "@jest/types" "^29.0.2" + "@jest/environment" "^29.0.3" + "@jest/expect" "^29.0.3" + "@jest/test-result" "^29.0.3" + "@jest/types" "^29.0.3" "@types/node" "*" chalk "^4.0.0" co "^4.6.0" dedent "^0.7.0" is-generator-fn "^2.0.0" - jest-each "^29.0.2" - jest-matcher-utils "^29.0.2" - jest-message-util "^29.0.2" - jest-runtime "^29.0.2" - jest-snapshot "^29.0.2" - jest-util "^29.0.2" + jest-each "^29.0.3" + jest-matcher-utils "^29.0.3" + jest-message-util "^29.0.3" + jest-runtime "^29.0.3" + jest-snapshot "^29.0.3" + jest-util "^29.0.3" p-limit "^3.1.0" - pretty-format "^29.0.2" + pretty-format "^29.0.3" slash "^3.0.0" stack-utils "^2.0.3" -jest-cli@^29.0.2: - version "29.0.2" - resolved "https://registry.yarnpkg.com/jest-cli/-/jest-cli-29.0.2.tgz#adf341ee3a4fd6ad1f23e3c0eb4e466847407021" - integrity sha512-tlf8b+4KcUbBGr25cywIi3+rbZ4+G+SiG8SvY552m9sRZbXPafdmQRyeVE/C/R8K+TiBAMrTIUmV2SlStRJ40g== +jest-cli@^29.0.3: + version "29.0.3" + resolved "https://registry.yarnpkg.com/jest-cli/-/jest-cli-29.0.3.tgz#fd8f0ef363a7a3d9c53ef62e0651f18eeffa77b9" + integrity sha512-aUy9Gd/Kut1z80eBzG10jAn6BgS3BoBbXyv+uXEqBJ8wnnuZ5RpNfARoskSrTIy1GY4a8f32YGuCMwibtkl9CQ== dependencies: - "@jest/core" "^29.0.2" - "@jest/test-result" "^29.0.2" - "@jest/types" "^29.0.2" + "@jest/core" "^29.0.3" + "@jest/test-result" "^29.0.3" + "@jest/types" "^29.0.3" chalk "^4.0.0" exit "^0.1.2" graceful-fs "^4.2.9" import-local "^3.0.2" - jest-config "^29.0.2" - jest-util "^29.0.2" - jest-validate "^29.0.2" + jest-config "^29.0.3" + jest-util "^29.0.3" + jest-validate "^29.0.3" prompts "^2.0.1" yargs "^17.3.1" -jest-config@^29.0.2: - version "29.0.2" - resolved "https://registry.yarnpkg.com/jest-config/-/jest-config-29.0.2.tgz#0ce168e1f74ca46c27285a7182ecb06c2d8ce7d9" - integrity sha512-RU4gzeUNZAFktYVzDGimDxeYoaiTnH100jkYYZgldqFamaZukF0IqmFx8+QrzVeEWccYg10EEJT3ox1Dq5b74w== +jest-config@^29.0.3: + version "29.0.3" + resolved "https://registry.yarnpkg.com/jest-config/-/jest-config-29.0.3.tgz#c2e52a8f5adbd18de79f99532d8332a19e232f13" + integrity sha512-U5qkc82HHVYe3fNu2CRXLN4g761Na26rWKf7CjM8LlZB3In1jadEkZdMwsE37rd9RSPV0NfYaCjHdk/gu3v+Ew== dependencies: "@babel/core" "^7.11.6" - "@jest/test-sequencer" "^29.0.2" - "@jest/types" "^29.0.2" - babel-jest "^29.0.2" + "@jest/test-sequencer" "^29.0.3" + "@jest/types" "^29.0.3" + babel-jest "^29.0.3" chalk "^4.0.0" ci-info "^3.2.0" deepmerge "^4.2.2" glob "^7.1.3" graceful-fs "^4.2.9" - jest-circus "^29.0.2" - jest-environment-node "^29.0.2" + jest-circus "^29.0.3" + jest-environment-node "^29.0.3" jest-get-type "^29.0.0" jest-regex-util "^29.0.0" - jest-resolve "^29.0.2" - jest-runner "^29.0.2" - jest-util "^29.0.2" - jest-validate "^29.0.2" + jest-resolve "^29.0.3" + jest-runner "^29.0.3" + jest-util "^29.0.3" + jest-validate "^29.0.3" micromatch "^4.0.4" parse-json "^5.2.0" - pretty-format "^29.0.2" + pretty-format "^29.0.3" slash "^3.0.0" strip-json-comments "^3.1.1" @@ -4456,15 +4442,15 @@ jest-diff@^29.0.1: jest-get-type "^29.0.0" pretty-format "^29.0.1" -jest-diff@^29.0.2: - version "29.0.2" - resolved "https://registry.yarnpkg.com/jest-diff/-/jest-diff-29.0.2.tgz#1a99419efda66f9ee72f91e580e774df95de5ddc" - integrity sha512-b9l9970sa1rMXH1owp2Woprmy42qIwwll/htsw4Gf7+WuSp5bZxNhkKHDuCGKL+HoHn1KhcC+tNEeAPYBkD2Jg== +jest-diff@^29.0.3: + version "29.0.3" + resolved "https://registry.yarnpkg.com/jest-diff/-/jest-diff-29.0.3.tgz#41cc02409ad1458ae1bf7684129a3da2856341ac" + integrity sha512-+X/AIF5G/vX9fWK+Db9bi9BQas7M9oBME7egU7psbn4jlszLFCu0dW63UgeE6cs/GANq4fLaT+8sGHQQ0eCUfg== dependencies: chalk "^4.0.0" diff-sequences "^29.0.0" jest-get-type "^29.0.0" - pretty-format "^29.0.2" + pretty-format "^29.0.3" jest-docblock@^29.0.0: version "29.0.0" @@ -4473,60 +4459,60 @@ jest-docblock@^29.0.0: dependencies: detect-newline "^3.0.0" -jest-each@^29.0.2: - version "29.0.2" - resolved "https://registry.yarnpkg.com/jest-each/-/jest-each-29.0.2.tgz#f98375a79a37761137e11d458502dfe1f00ba5b0" - integrity sha512-+sA9YjrJl35iCg0W0VCrgCVj+wGhDrrKQ+YAqJ/DHBC4gcDFAeePtRRhpJnX9gvOZ63G7gt52pwp2PesuSEx0Q== +jest-each@^29.0.3: + version "29.0.3" + resolved "https://registry.yarnpkg.com/jest-each/-/jest-each-29.0.3.tgz#7ef3157580b15a609d7ef663dd4fc9b07f4e1299" + integrity sha512-wILhZfESURHHBNvPMJ0lZlYZrvOQJxAo3wNHi+ycr90V7M+uGR9Gh4+4a/BmaZF0XTyZsk4OiYEf3GJN7Ltqzg== dependencies: - "@jest/types" "^29.0.2" + "@jest/types" "^29.0.3" chalk "^4.0.0" jest-get-type "^29.0.0" - jest-util "^29.0.2" - pretty-format "^29.0.2" + jest-util "^29.0.3" + pretty-format "^29.0.3" -jest-environment-node@^29.0.2: - version "29.0.2" - resolved "https://registry.yarnpkg.com/jest-environment-node/-/jest-environment-node-29.0.2.tgz#8196268c9f740f1d2e7ecccf212b4c1c5b0167e4" - integrity sha512-4Fv8GXVCToRlMzDO94gvA8iOzKxQ7rhAbs8L+j8GPyTxGuUiYkV+63LecGeVdVhsL2KXih1sKnoqmH6tp89J7Q== +jest-environment-node@^29.0.3: + version "29.0.3" + resolved "https://registry.yarnpkg.com/jest-environment-node/-/jest-environment-node-29.0.3.tgz#293804b1e0fa5f0e354dacbe510655caa478a3b2" + integrity sha512-cdZqRCnmIlTXC+9vtvmfiY/40Cj6s2T0czXuq1whvQdmpzAnj4sbqVYuZ4zFHk766xTTJ+Ij3uUqkk8KCfXoyg== dependencies: - "@jest/environment" "^29.0.2" - "@jest/fake-timers" "^29.0.2" - "@jest/types" "^29.0.2" + "@jest/environment" "^29.0.3" + "@jest/fake-timers" "^29.0.3" + "@jest/types" "^29.0.3" "@types/node" "*" - jest-mock "^29.0.2" - jest-util "^29.0.2" + jest-mock "^29.0.3" + jest-util "^29.0.3" jest-get-type@^29.0.0: version "29.0.0" resolved "https://registry.yarnpkg.com/jest-get-type/-/jest-get-type-29.0.0.tgz#843f6c50a1b778f7325df1129a0fd7aa713aef80" integrity sha512-83X19z/HuLKYXYHskZlBAShO7UfLFXu/vWajw9ZNJASN32li8yHMaVGAQqxFW1RCFOkB7cubaL6FaJVQqqJLSw== -jest-haste-map@^29.0.2: - version "29.0.2" - resolved "https://registry.yarnpkg.com/jest-haste-map/-/jest-haste-map-29.0.2.tgz#cac403a595e6e43982c9776b5c4dae63e38b22c5" - integrity sha512-SOorh2ysQ0fe8gsF4gaUDhoMIWAvi2hXOkwThEO48qT3JqA8GLAUieQcIvdSEd6M0scRDe1PVmKc5tXR3Z0U0A== +jest-haste-map@^29.0.3: + version "29.0.3" + resolved "https://registry.yarnpkg.com/jest-haste-map/-/jest-haste-map-29.0.3.tgz#d7f3f7180f558d760eacc5184aac5a67f20ef939" + integrity sha512-uMqR99+GuBHo0RjRhOE4iA6LmsxEwRdgiIAQgMU/wdT2XebsLDz5obIwLZm/Psj+GwSEQhw9AfAVKGYbh2G55A== dependencies: - "@jest/types" "^29.0.2" + "@jest/types" "^29.0.3" "@types/graceful-fs" "^4.1.3" "@types/node" "*" anymatch "^3.0.3" fb-watchman "^2.0.0" graceful-fs "^4.2.9" jest-regex-util "^29.0.0" - jest-util "^29.0.2" - jest-worker "^29.0.2" + jest-util "^29.0.3" + jest-worker "^29.0.3" micromatch "^4.0.4" walker "^1.0.8" optionalDependencies: fsevents "^2.3.2" -jest-leak-detector@^29.0.2: - version "29.0.2" - resolved "https://registry.yarnpkg.com/jest-leak-detector/-/jest-leak-detector-29.0.2.tgz#f88fd08e352b5fad3d33e48ecab39e97077ed8a8" - integrity sha512-5f0493qDeAxjUldkBSQg5D1cLadRgZVyWpTQvfJeQwQUpHQInE21AyVHVv64M7P2Ue8Z5EZ4BAcoDS/dSPPgMw== +jest-leak-detector@^29.0.3: + version "29.0.3" + resolved "https://registry.yarnpkg.com/jest-leak-detector/-/jest-leak-detector-29.0.3.tgz#e85cf3391106a7a250850b6766b508bfe9c7bc6f" + integrity sha512-YfW/G63dAuiuQ3QmQlh8hnqLDe25WFY3eQhuc/Ev1AGmkw5zREblTh7TCSKLoheyggu6G9gxO2hY8p9o6xbaRQ== dependencies: jest-get-type "^29.0.0" - pretty-format "^29.0.2" + pretty-format "^29.0.3" jest-matcher-utils@^29.0.1: version "29.0.1" @@ -4538,15 +4524,15 @@ jest-matcher-utils@^29.0.1: jest-get-type "^29.0.0" pretty-format "^29.0.1" -jest-matcher-utils@^29.0.2: - version "29.0.2" - resolved "https://registry.yarnpkg.com/jest-matcher-utils/-/jest-matcher-utils-29.0.2.tgz#0ffdcaec340a9810caee6c73ff90fb029b446e10" - integrity sha512-s62YkHFBfAx0JLA2QX1BlnCRFwHRobwAv2KP1+YhjzF6ZCbCVrf1sG8UJyn62ZUsDaQKpoo86XMTjkUyO5aWmQ== +jest-matcher-utils@^29.0.3: + version "29.0.3" + resolved "https://registry.yarnpkg.com/jest-matcher-utils/-/jest-matcher-utils-29.0.3.tgz#b8305fd3f9e27cdbc210b21fc7dbba92d4e54560" + integrity sha512-RsR1+cZ6p1hDV4GSCQTg+9qjeotQCgkaleIKLK7dm+U4V/H2bWedU3RAtLm8+mANzZ7eDV33dMar4pejd7047w== dependencies: chalk "^4.0.0" - jest-diff "^29.0.2" + jest-diff "^29.0.3" jest-get-type "^29.0.0" - pretty-format "^29.0.2" + pretty-format "^29.0.3" jest-message-util@^29.0.1: version "29.0.1" @@ -4563,27 +4549,27 @@ jest-message-util@^29.0.1: slash "^3.0.0" stack-utils "^2.0.3" -jest-message-util@^29.0.2: - version "29.0.2" - resolved "https://registry.yarnpkg.com/jest-message-util/-/jest-message-util-29.0.2.tgz#b2781dfb6a2d1c63830d9684c5148ae3155c6154" - integrity sha512-kcJAgms3ckJV0wUoLsAM40xAhY+pb9FVSZwicjFU9PFkaTNmqh9xd99/CzKse48wPM1ANUQKmp03/DpkY+lGrA== +jest-message-util@^29.0.3: + version "29.0.3" + resolved "https://registry.yarnpkg.com/jest-message-util/-/jest-message-util-29.0.3.tgz#f0254e1ffad21890c78355726202cc91d0a40ea8" + integrity sha512-7T8JiUTtDfppojosORAflABfLsLKMLkBHSWkjNQrjIltGoDzNGn7wEPOSfjqYAGTYME65esQzMJxGDjuLBKdOg== dependencies: "@babel/code-frame" "^7.12.13" - "@jest/types" "^29.0.2" + "@jest/types" "^29.0.3" "@types/stack-utils" "^2.0.0" chalk "^4.0.0" graceful-fs "^4.2.9" micromatch "^4.0.4" - pretty-format "^29.0.2" + pretty-format "^29.0.3" slash "^3.0.0" stack-utils "^2.0.3" -jest-mock@^29.0.2: - version "29.0.2" - resolved "https://registry.yarnpkg.com/jest-mock/-/jest-mock-29.0.2.tgz#d7810966a6338aca6a440c3cd9f19276477840ad" - integrity sha512-giWXOIT23UCxHCN2VUfUJ0Q7SmiqQwfSFXlCaIhW5anITpNQ+3vuLPQdKt5wkuwM37GrbFyHIClce8AAK9ft9g== +jest-mock@^29.0.3: + version "29.0.3" + resolved "https://registry.yarnpkg.com/jest-mock/-/jest-mock-29.0.3.tgz#4f0093f6a9cb2ffdb9c44a07a3912f0c098c8de9" + integrity sha512-ort9pYowltbcrCVR43wdlqfAiFJXBx8l4uJDsD8U72LgBcetvEp+Qxj1W9ZYgMRoeAo+ov5cnAGF2B6+Oth+ww== dependencies: - "@jest/types" "^29.0.2" + "@jest/types" "^29.0.3" "@types/node" "*" jest-pnp-resolver@^1.2.2: @@ -4596,88 +4582,88 @@ jest-regex-util@^29.0.0: resolved "https://registry.yarnpkg.com/jest-regex-util/-/jest-regex-util-29.0.0.tgz#b442987f688289df8eb6c16fa8df488b4cd007de" integrity sha512-BV7VW7Sy0fInHWN93MMPtlClweYv2qrSCwfeFWmpribGZtQPWNvRSq9XOVgOEjU1iBGRKXUZil0o2AH7Iy9Lug== -jest-resolve-dependencies@^29.0.2: - version "29.0.2" - resolved "https://registry.yarnpkg.com/jest-resolve-dependencies/-/jest-resolve-dependencies-29.0.2.tgz#2d30199ed0059ff97712f4fa6320c590bfcd2061" - integrity sha512-fSAu6eIG7wtGdnPJUkVVdILGzYAP9Dj/4+zvC8BrGe8msaUMJ9JeygU0Hf9+Uor6/icbuuzQn5See1uajLnAqg== +jest-resolve-dependencies@^29.0.3: + version "29.0.3" + resolved "https://registry.yarnpkg.com/jest-resolve-dependencies/-/jest-resolve-dependencies-29.0.3.tgz#f23a54295efc6374b86b198cf8efed5606d6b762" + integrity sha512-KzuBnXqNvbuCdoJpv8EanbIGObk7vUBNt/PwQPPx2aMhlv/jaXpUJsqWYRpP/0a50faMBY7WFFP8S3/CCzwfDw== dependencies: jest-regex-util "^29.0.0" - jest-snapshot "^29.0.2" + jest-snapshot "^29.0.3" -jest-resolve@^29.0.2: - version "29.0.2" - resolved "https://registry.yarnpkg.com/jest-resolve/-/jest-resolve-29.0.2.tgz#dd097e1c8020fbed4a8c1e1889ccb56022288697" - integrity sha512-V3uLjSA+EHxLtjIDKTBXnY71hyx+8lusCqPXvqzkFO1uCGvVpjBfuOyp+KOLBNSuY61kM2jhepiMwt4eiJS+Vw== +jest-resolve@^29.0.3: + version "29.0.3" + resolved "https://registry.yarnpkg.com/jest-resolve/-/jest-resolve-29.0.3.tgz#329a3431e3b9eb6629a2cd483e9bed95b26827b9" + integrity sha512-toVkia85Y/BPAjJasTC9zIPY6MmVXQPtrCk8SmiheC4MwVFE/CMFlOtMN6jrwPMC6TtNh8+sTMllasFeu1wMPg== dependencies: chalk "^4.0.0" graceful-fs "^4.2.9" - jest-haste-map "^29.0.2" + jest-haste-map "^29.0.3" jest-pnp-resolver "^1.2.2" - jest-util "^29.0.2" - jest-validate "^29.0.2" + jest-util "^29.0.3" + jest-validate "^29.0.3" resolve "^1.20.0" resolve.exports "^1.1.0" slash "^3.0.0" -jest-runner@^29.0.2: - version "29.0.2" - resolved "https://registry.yarnpkg.com/jest-runner/-/jest-runner-29.0.2.tgz#64e4e6c88f74387307687b73a4688f93369d8d99" - integrity sha512-+D82iPZejI8t+SfduOO1deahC/QgLFf8aJBO++Znz3l2ETtOMdM7K4ATsGWzCFnTGio5yHaRifg1Su5Ybza5Nw== +jest-runner@^29.0.3: + version "29.0.3" + resolved "https://registry.yarnpkg.com/jest-runner/-/jest-runner-29.0.3.tgz#2e47fe1e8777aea9b8970f37e8f83630b508fb87" + integrity sha512-Usu6VlTOZlCZoNuh3b2Tv/yzDpKqtiNAetG9t3kJuHfUyVMNW7ipCCJOUojzKkjPoaN7Bl1f7Buu6PE0sGpQxw== dependencies: - "@jest/console" "^29.0.2" - "@jest/environment" "^29.0.2" - "@jest/test-result" "^29.0.2" - "@jest/transform" "^29.0.2" - "@jest/types" "^29.0.2" + "@jest/console" "^29.0.3" + "@jest/environment" "^29.0.3" + "@jest/test-result" "^29.0.3" + "@jest/transform" "^29.0.3" + "@jest/types" "^29.0.3" "@types/node" "*" chalk "^4.0.0" emittery "^0.10.2" graceful-fs "^4.2.9" jest-docblock "^29.0.0" - jest-environment-node "^29.0.2" - jest-haste-map "^29.0.2" - jest-leak-detector "^29.0.2" - jest-message-util "^29.0.2" - jest-resolve "^29.0.2" - jest-runtime "^29.0.2" - jest-util "^29.0.2" - jest-watcher "^29.0.2" - jest-worker "^29.0.2" + jest-environment-node "^29.0.3" + jest-haste-map "^29.0.3" + jest-leak-detector "^29.0.3" + jest-message-util "^29.0.3" + jest-resolve "^29.0.3" + jest-runtime "^29.0.3" + jest-util "^29.0.3" + jest-watcher "^29.0.3" + jest-worker "^29.0.3" p-limit "^3.1.0" source-map-support "0.5.13" -jest-runtime@^29.0.2: - version "29.0.2" - resolved "https://registry.yarnpkg.com/jest-runtime/-/jest-runtime-29.0.2.tgz#dc3de788b8d75af346ae163d59c585027a9d809c" - integrity sha512-DO6F81LX4okOgjJLkLySv10E5YcV5NHUbY1ZqAUtofxdQE+q4hjH0P2gNsY8x3z3sqgw7O/+919SU4r18Fcuig== +jest-runtime@^29.0.3: + version "29.0.3" + resolved "https://registry.yarnpkg.com/jest-runtime/-/jest-runtime-29.0.3.tgz#5a823ec5902257519556a4e5a71a868e8fd788aa" + integrity sha512-12gZXRQ7ozEeEHKTY45a+YLqzNDR/x4c//X6AqwKwKJPpWM8FY4vwn4VQJOcLRS3Nd1fWwgP7LU4SoynhuUMHQ== dependencies: - "@jest/environment" "^29.0.2" - "@jest/fake-timers" "^29.0.2" - "@jest/globals" "^29.0.2" + "@jest/environment" "^29.0.3" + "@jest/fake-timers" "^29.0.3" + "@jest/globals" "^29.0.3" "@jest/source-map" "^29.0.0" - "@jest/test-result" "^29.0.2" - "@jest/transform" "^29.0.2" - "@jest/types" "^29.0.2" + "@jest/test-result" "^29.0.3" + "@jest/transform" "^29.0.3" + "@jest/types" "^29.0.3" "@types/node" "*" chalk "^4.0.0" cjs-module-lexer "^1.0.0" collect-v8-coverage "^1.0.0" glob "^7.1.3" graceful-fs "^4.2.9" - jest-haste-map "^29.0.2" - jest-message-util "^29.0.2" - jest-mock "^29.0.2" + jest-haste-map "^29.0.3" + jest-message-util "^29.0.3" + jest-mock "^29.0.3" jest-regex-util "^29.0.0" - jest-resolve "^29.0.2" - jest-snapshot "^29.0.2" - jest-util "^29.0.2" + jest-resolve "^29.0.3" + jest-snapshot "^29.0.3" + jest-util "^29.0.3" slash "^3.0.0" strip-bom "^4.0.0" -jest-snapshot@^29.0.2: - version "29.0.2" - resolved "https://registry.yarnpkg.com/jest-snapshot/-/jest-snapshot-29.0.2.tgz#5017d54db8369f01900d11e179513fa5839fb5ac" - integrity sha512-26C4PzGKaX5gkoKg8UzYGVy2HPVcTaROSkf0gwnHu3lGeTB7bAIJBovvVPZoiJ20IximJELQs/r8WSDRCuGX2A== +jest-snapshot@^29.0.3: + version "29.0.3" + resolved "https://registry.yarnpkg.com/jest-snapshot/-/jest-snapshot-29.0.3.tgz#0a024706986a915a6eefae74d7343069d2fc8eef" + integrity sha512-52q6JChm04U3deq+mkQ7R/7uy7YyfVIrebMi6ZkBoDJ85yEjm/sJwdr1P0LOIEHmpyLlXrxy3QP0Zf5J2kj0ew== dependencies: "@babel/core" "^7.11.6" "@babel/generator" "^7.7.2" @@ -4685,31 +4671,31 @@ jest-snapshot@^29.0.2: "@babel/plugin-syntax-typescript" "^7.7.2" "@babel/traverse" "^7.7.2" "@babel/types" "^7.3.3" - "@jest/expect-utils" "^29.0.2" - "@jest/transform" "^29.0.2" - "@jest/types" "^29.0.2" + "@jest/expect-utils" "^29.0.3" + "@jest/transform" "^29.0.3" + "@jest/types" "^29.0.3" "@types/babel__traverse" "^7.0.6" "@types/prettier" "^2.1.5" babel-preset-current-node-syntax "^1.0.0" chalk "^4.0.0" - expect "^29.0.2" + expect "^29.0.3" graceful-fs "^4.2.9" - jest-diff "^29.0.2" + jest-diff "^29.0.3" jest-get-type "^29.0.0" - jest-haste-map "^29.0.2" - jest-matcher-utils "^29.0.2" - jest-message-util "^29.0.2" - jest-util "^29.0.2" + jest-haste-map "^29.0.3" + jest-matcher-utils "^29.0.3" + jest-message-util "^29.0.3" + jest-util "^29.0.3" natural-compare "^1.4.0" - pretty-format "^29.0.2" + pretty-format "^29.0.3" semver "^7.3.5" -jest-util@^28.0.0: - version "28.1.0" - resolved "https://registry.yarnpkg.com/jest-util/-/jest-util-28.1.0.tgz#d54eb83ad77e1dd441408738c5a5043642823be5" - integrity sha512-qYdCKD77k4Hwkose2YBEqQk7PzUf/NSE+rutzceduFveQREeH6b+89Dc9+wjX9dAwHcgdx4yedGA3FQlU/qCTA== +jest-util@^29.0.0, jest-util@^29.0.3: + version "29.0.3" + resolved "https://registry.yarnpkg.com/jest-util/-/jest-util-29.0.3.tgz#06d1d77f9a1bea380f121897d78695902959fbc0" + integrity sha512-Q0xaG3YRG8QiTC4R6fHjHQPaPpz9pJBEi0AeOE4mQh/FuWOijFjGXMMOfQEaU9i3z76cNR7FobZZUQnL6IyfdQ== dependencies: - "@jest/types" "^28.1.0" + "@jest/types" "^29.0.3" "@types/node" "*" chalk "^4.0.0" ci-info "^3.2.0" @@ -4728,62 +4714,50 @@ jest-util@^29.0.1: graceful-fs "^4.2.9" picomatch "^2.2.3" -jest-util@^29.0.2: - version "29.0.2" - resolved "https://registry.yarnpkg.com/jest-util/-/jest-util-29.0.2.tgz#c75c5cab7f3b410782f9570a60c5558b5dfb6e3a" - integrity sha512-ozk8ruEEEACxqpz0hN9UOgtPZS0aN+NffwQduR5dVlhN+eN47vxurtvgZkYZYMpYrsmlAEx1XabkB3BnN0GfKQ== +jest-validate@^29.0.3: + version "29.0.3" + resolved "https://registry.yarnpkg.com/jest-validate/-/jest-validate-29.0.3.tgz#f9521581d7344685428afa0a4d110e9c519aeeb6" + integrity sha512-OebiqqT6lK8cbMPtrSoS3aZP4juID762lZvpf1u+smZnwTEBCBInan0GAIIhv36MxGaJvmq5uJm7dl5gVt+Zrw== dependencies: - "@jest/types" "^29.0.2" - "@types/node" "*" - chalk "^4.0.0" - ci-info "^3.2.0" - graceful-fs "^4.2.9" - picomatch "^2.2.3" - -jest-validate@^29.0.2: - version "29.0.2" - resolved "https://registry.yarnpkg.com/jest-validate/-/jest-validate-29.0.2.tgz#ad86e157cc1735a3a3ea88995a611ebf8544bd67" - integrity sha512-AeRKm7cEucSy7tr54r3LhiGIXYvOILUwBM1S7jQkKs6YelwAlWKsmZGVrQR7uwsd31rBTnR5NQkODi1Z+6TKIQ== - dependencies: - "@jest/types" "^29.0.2" + "@jest/types" "^29.0.3" camelcase "^6.2.0" chalk "^4.0.0" jest-get-type "^29.0.0" leven "^3.1.0" - pretty-format "^29.0.2" + pretty-format "^29.0.3" -jest-watcher@^29.0.2: - version "29.0.2" - resolved "https://registry.yarnpkg.com/jest-watcher/-/jest-watcher-29.0.2.tgz#093c044e0d7462e691ec64ca6d977014272c9bca" - integrity sha512-ds2bV0oyUdYoyrUTv4Ga5uptz4cEvmmP/JzqDyzZZanvrIn8ipxg5l3SDOAIiyuAx1VdHd2FBzeXPFO5KPH8vQ== +jest-watcher@^29.0.3: + version "29.0.3" + resolved "https://registry.yarnpkg.com/jest-watcher/-/jest-watcher-29.0.3.tgz#8e220d1cc4f8029875e82015d084cab20f33d57f" + integrity sha512-tQX9lU91A+9tyUQKUMp0Ns8xAcdhC9fo73eqA3LFxP2bSgiF49TNcc+vf3qgGYYK9qRjFpXW9+4RgF/mbxyOOw== dependencies: - "@jest/test-result" "^29.0.2" - "@jest/types" "^29.0.2" + "@jest/test-result" "^29.0.3" + "@jest/types" "^29.0.3" "@types/node" "*" ansi-escapes "^4.2.1" chalk "^4.0.0" emittery "^0.10.2" - jest-util "^29.0.2" + jest-util "^29.0.3" string-length "^4.0.1" -jest-worker@^29.0.2: - version "29.0.2" - resolved "https://registry.yarnpkg.com/jest-worker/-/jest-worker-29.0.2.tgz#46c9f2cb9a19663d22babbacf998e4b5d7c46574" - integrity sha512-EyvBlYcvd2pg28yg5A3OODQnqK9LI1kitnGUZUG5/NYIeaRgewtYBKB5wlr7oXj8zPCkzev7EmnTCsrXK7V+Xw== +jest-worker@^29.0.3: + version "29.0.3" + resolved "https://registry.yarnpkg.com/jest-worker/-/jest-worker-29.0.3.tgz#c2ba0aa7e41eec9eb0be8e8a322ae6518df72647" + integrity sha512-Tl/YWUugQOjoTYwjKdfJWkSOfhufJHO5LhXTSZC3TRoQKO+fuXnZAdoXXBlpLXKGODBL3OvdUasfDD4PcMe6ng== dependencies: "@types/node" "*" merge-stream "^2.0.0" supports-color "^8.0.0" -jest@^29.0.2: - version "29.0.2" - resolved "https://registry.yarnpkg.com/jest/-/jest-29.0.2.tgz#16e20003dbf8fb9ed7e6ab801579a77084e13fba" - integrity sha512-enziNbNUmXTcTaTP/Uq5rV91r0Yqy2UKzLUIabxMpGm9YHz8qpbJhiRnNVNvm6vzWfzt/0o97NEHH8/3udoClA== +jest@^29.0.3: + version "29.0.3" + resolved "https://registry.yarnpkg.com/jest/-/jest-29.0.3.tgz#5227a0596d30791b2649eea347e4aa97f734944d" + integrity sha512-ElgUtJBLgXM1E8L6K1RW1T96R897YY/3lRYqq9uVcPWtP2AAl/nQ16IYDh/FzQOOQ12VEuLdcPU83mbhG2C3PQ== dependencies: - "@jest/core" "^29.0.2" - "@jest/types" "^29.0.2" + "@jest/core" "^29.0.3" + "@jest/types" "^29.0.3" import-local "^3.0.2" - jest-cli "^29.0.2" + jest-cli "^29.0.3" js-stringify@^1.0.2: version "1.0.2" @@ -6143,10 +6117,10 @@ pretty-format@^29.0.0, pretty-format@^29.0.1: ansi-styles "^5.0.0" react-is "^18.0.0" -pretty-format@^29.0.2: - version "29.0.2" - resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-29.0.2.tgz#7f7666a7bf05ba2bcacde61be81c6db64f6f3be6" - integrity sha512-wp3CdtUa3cSJVFn3Miu5a1+pxc1iPIQTenOAn+x5erXeN1+ryTcLesV5pbK/rlW5EKwp27x38MoYfNGaNXDDhg== +pretty-format@^29.0.3: + version "29.0.3" + resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-29.0.3.tgz#23d5f8cabc9cbf209a77d49409d093d61166a811" + integrity sha512-cHudsvQr1K5vNVLbvYF/nv3Qy/F/BcEKxGuIeMiVMRHxPOO1RxXooP8g/ZrwAp7Dx+KdMZoOc7NxLHhMrP2f9Q== dependencies: "@jest/schemas" "^29.0.0" ansi-styles "^5.0.0" @@ -6963,10 +6937,10 @@ sprintf-js@~1.0.2: resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c" integrity sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw= -sqlite3@^5.0.11: - version "5.0.11" - resolved "https://registry.yarnpkg.com/sqlite3/-/sqlite3-5.0.11.tgz#102c835d70be66da9d95a383fd6ea084a082ef7f" - integrity sha512-4akFOr7u9lJEeAWLJxmwiV43DJcGV7w3ab7SjQFAFaTVyknY3rZjvXTKIVtWqUoY4xwhjwoHKYs2HDW2SoHVsA== +sqlite3@^5.1.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/sqlite3/-/sqlite3-5.1.0.tgz#d82bd1f85668453904ef9a6f0a6b13c70b53c837" + integrity sha512-/cZl9DfeSQMWR5g9SdluHGt+YL3URetjtvgv1XPQx7rzXOFdZx30EnZj709wCEIznjjzZdo5Lmt5GUeNyGgUbQ== dependencies: "@mapbox/node-pre-gyp" "^1.0.0" node-addon-api "^4.2.0" @@ -7354,14 +7328,14 @@ trim-newlines@^1.0.0, trim-newlines@~4.0.2: resolved "https://registry.yarnpkg.com/trim-newlines/-/trim-newlines-4.0.2.tgz#d6aaaf6a0df1b4b536d183879a6b939489808c7c" integrity sha512-GJtWyq9InR/2HRiLZgpIKv+ufIKrVrvjQWEj7PxAXNc5dwbNJkqhAUoAGgzRmULAnoOM5EIpveYd3J2VeSAIew== -ts-jest@^28.0.8: - version "28.0.8" - resolved "https://registry.yarnpkg.com/ts-jest/-/ts-jest-28.0.8.tgz#cd204b8e7a2f78da32cf6c95c9a6165c5b99cc73" - integrity sha512-5FaG0lXmRPzApix8oFG8RKjAz4ehtm8yMKOTy5HX3fY6W8kmvOrmcY0hKDElW52FJov+clhUbrKAqofnj4mXTg== +ts-jest@^29.0.1: + version "29.0.1" + resolved "https://registry.yarnpkg.com/ts-jest/-/ts-jest-29.0.1.tgz#3296b39d069dc55825ce1d059a9510b33c718b86" + integrity sha512-htQOHshgvhn93QLxrmxpiQPk69+M1g7govO1g6kf6GsjCv4uvRV0znVmDrrvjUrVCnTYeY4FBxTYYYD4airyJA== dependencies: bs-logger "0.x" fast-json-stable-stringify "2.x" - jest-util "^28.0.0" + jest-util "^29.0.0" json5 "^2.2.1" lodash.memoize "4.x" make-error "1.x" @@ -7439,10 +7413,10 @@ type-is@~1.6.18: media-typer "0.3.0" mime-types "~2.1.24" -typescript@^4.8.2: - version "4.8.2" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.8.2.tgz#e3b33d5ccfb5914e4eeab6699cf208adee3fd790" - integrity sha512-C0I1UsrrDHo2fYI5oaCGbSejwX4ch+9Y5jTQELvovfmFkK3HHSZJB8MSJcWLmCUBzQBchCrZ9rMRV6GuNrvGtw== +typescript@^4.8.3: + version "4.8.3" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.8.3.tgz#d59344522c4bc464a65a730ac695007fdb66dd88" + integrity sha512-goMHfm00nWPa8UvR/CPSvykqf6dVV8x/dp0c5mFTMTIu0u0FlGWRioyy7Nn0PGAdHxpJZnuO/ut+PpQ8UiHAig== typical@^4.0.0: version "4.0.0"