diff --git a/server/logger.test.ts b/server/logger.test.ts index d3c946c..db93643 100644 --- a/server/logger.test.ts +++ b/server/logger.test.ts @@ -1,6 +1,5 @@ -import {LogLevel, LogLevels, isLogLevel} from "./types"; +import {isLogLevel, LogLevel, LogLevels} from "./types"; import {ActivatableLoggerImpl} from "./logger"; -import _ from "lodash"; class TestableLogger extends ActivatableLoggerImpl { private logs: any[][] = []; @@ -35,13 +34,13 @@ function parseLogEntry(logEntry: any[]): ParsedLogEntry { `Empty log entry. Should always start with log message: ${logEntry}` ); } - + const logMessage = logEntry[0]; if (typeof logMessage !== "string") { throw new Error( `Expected log entry to start with string, but got: ${logMessage}` ); - } + } const regexp = /^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2} ([A-Z]+) - (\[[^\]]*\])? *(.*)$/; const groups = logMessage.match(regexp); @@ -59,13 +58,13 @@ function parseLogEntry(logEntry: any[]): ParsedLogEntry { } const tagsStr = groups[2].substring(1, groups[2].length - 1); - const tags = tagsStr ? _.split(tagsStr, ", ") : []; + const tags = tagsStr ? tagsStr.split(", "): []; const message = groups[3]; const args = logEntry.slice(1); return { level: level as LogLevel, - tags, + tags, message, args, }; @@ -158,7 +157,7 @@ for (const level of LogLevels) { message: "%s %d %f %o %%", args: [], }]); - }); + }); test(`should not escape ${level} message arguments`, () => { // given @@ -174,7 +173,7 @@ for (const level of LogLevels) { message: "message", args: [1, "%s", "%d", "%f", "%o", "%"], }]); - }); + }); test(`should not log ${level} message on disabled logger`, () => { // given @@ -185,6 +184,6 @@ for (const level of LogLevels) { // then expect(parseLogs(logger.getLogs())).toEqual([]); - }); + }); } diff --git a/server/logger.ts b/server/logger.ts index edaadc7..f787c8a 100644 --- a/server/logger.ts +++ b/server/logger.ts @@ -1,6 +1,5 @@ -import {Logger, TaggedLogger, LogLevel} from './types'; +import {isString, Logger, LogLevel, TaggedLogger} from './types'; import moment from 'moment'; -import _ from 'lodash'; export type LoggingFunction = (...args: any[]) => void; @@ -36,15 +35,15 @@ export class ActivatableLoggerImpl implements ActivatableLogger { log(level: LogLevel, ...args: any[]): void { const timeStr = moment().format('YYYY-MM-DD HH:mm:ss'); const levelStr = level.toUpperCase(); - const tagsStr = tags ? '[' + _.join(tags, ', ') + ']' : ''; + const tagsStr = tags ? '[' + tags.join(', ') + ']' : ''; const messagePrefix = `${timeStr} ${levelStr} - ${tagsStr}`; // Make sure to only replace %s, etc. in real log message // but not in tags. const escapedMessagePrefix = messagePrefix.replace(/%/g, '%%'); - + let message = ''; - if (args && _.isString(args[0])) { + if (args && isString(args[0])) { message = args[0]; args.shift(); } diff --git a/server/resources/monitoringResource.ts b/server/resources/monitoringResource.ts index ec54a3f..4b44c0c 100644 --- a/server/resources/monitoringResource.ts +++ b/server/resources/monitoringResource.ts @@ -1,5 +1,3 @@ -import _ from "lodash"; - import CONSTRAINTS from "../validation/constraints"; import ErrorTypes from "../utils/errorTypes"; import * as MonitoringService from "../services/monitoringService"; @@ -17,8 +15,8 @@ async function doGetAll(req: Request): Promise<{ total: number, result: any }> { const {monitoringStates, total} = await MonitoringService.getAll(restParams); return { total, - result: _.map(monitoringStates, function (state) { - state.mapId = _.toLower(state.mac).replace(/:/g, ''); + result: monitoringStates.map(state => { + state.mapId = state.mac.toLowerCase().replace(/:/g, ""); return state; }) }; diff --git a/server/resources/nodeResource.ts b/server/resources/nodeResource.ts index 92d9d14..afb754a 100644 --- a/server/resources/nodeResource.ts +++ b/server/resources/nodeResource.ts @@ -1,5 +1,3 @@ -import _ from "lodash"; - import Constraints from "../validation/constraints"; import ErrorTypes from "../utils/errorTypes"; import * as MonitoringService from "../services/monitoringService"; @@ -13,12 +11,12 @@ import { CreateOrUpdateNode, DomainSpecificNodeResponse, isNodeSortField, - isToken, JSONObject, + isToken, + JSONObject, MAC, NodeResponse, NodeStateData, NodeTokenResponse, - StoredNode, toDomainSpecificNodeResponse, Token, toNodeResponse, @@ -27,15 +25,18 @@ import { const nodeFields = ['hostname', 'key', 'email', 'nickname', 'mac', 'coords', 'monitoring']; +// TODO: Rename function getNormalizedNodeData(reqData: any): CreateOrUpdateNode { const node: { [key: string]: any } = {}; - _.each(nodeFields, function (field) { + for (const field of nodeFields) { let value = normalizeString(reqData[field]); if (field === 'mac') { value = normalizeMac(value as MAC); } node[field] = value; - }); + } + + // TODO: Add typeguard before cast. return node as CreateOrUpdateNode; } @@ -90,15 +91,15 @@ async function doGetAll(req: Request): Promise<{ total: number; pageNodes: any } const nodes = await NodeService.getAllNodes(); - const realNodes = _.filter(nodes, node => + const realNodes = nodes.filter(node => // We ignore nodes without tokens as those are only manually added ones like gateways. - !!node.token + !!node.token // FIXME: As node.token may not be undefined or null here, handle this when loading! ); - const macs: MAC[] = _.map(realNodes, (node: StoredNode): MAC => node.mac); + const macs: MAC[] = realNodes.map(node => node.mac); const nodeStateByMac = await MonitoringService.getByMacs(macs); - const domainSpecificNodes: DomainSpecificNodeResponse[] = _.map(realNodes, (node: StoredNode): DomainSpecificNodeResponse => { + const domainSpecificNodes: DomainSpecificNodeResponse[] = realNodes.map(node => { const nodeState: NodeStateData = nodeStateByMac[node.mac] || {}; return toDomainSpecificNodeResponse(node, nodeState); }); diff --git a/server/resources/taskResource.ts b/server/resources/taskResource.ts index e598bcb..6020d05 100644 --- a/server/resources/taskResource.ts +++ b/server/resources/taskResource.ts @@ -1,9 +1,7 @@ -import _ from "lodash"; - import CONSTRAINTS from "../validation/constraints"; import ErrorTypes from "../utils/errorTypes"; import * as Resources from "../utils/resources"; -import {Entity, handleJSONWithData, RequestData} from "../utils/resources"; +import {handleJSONWithData, RequestData} from "../utils/resources"; import {getTasks, Task, TaskState} from "../jobs/scheduler"; import {normalizeString} from "../utils/strings"; import {forConstraint} from "../validation/validator"; @@ -77,11 +75,11 @@ async function setTaskEnabled(data: RequestData, enable: boolean): Promise { +async function doGetAll(req: Request): Promise<{ total: number, pageTasks: Task[] }> { const restParams = await Resources.getValidRestParams('list', null, req); const tasks = Resources.sort( - _.values(getTasks()), + Object.values(getTasks()), isTaskSortField, restParams ); @@ -104,7 +102,7 @@ export function getAll(req: Request, res: Response): void { doGetAll(req) .then(({total, pageTasks}) => { res.set('X-Total-Count', total.toString(10)); - Resources.success(res, _.map(pageTasks, toTaskResponse)); + Resources.success(res, pageTasks.map(toTaskResponse)); }) .catch(err => Resources.error(res, err)); } diff --git a/server/services/mailService.ts b/server/services/mailService.ts index 1068375..1060c62 100644 --- a/server/services/mailService.ts +++ b/server/services/mailService.ts @@ -170,7 +170,7 @@ export async function getPendingMails(restParams: RestParams): Promise<{ mails: const mails = await db.all( 'SELECT * FROM email_queue WHERE ' + filter.query, - _.concat([], filter.params), + filter.params, ); return { diff --git a/server/services/mailTemplateService.ts b/server/services/mailTemplateService.ts index e9c617f..d2289c4 100644 --- a/server/services/mailTemplateService.ts +++ b/server/services/mailTemplateService.ts @@ -97,7 +97,7 @@ export async function render(mailOptions: Mail): Promise<{subject: string, body: try { return { - subject: _.trim(_.template(subject.toString())(data)), + subject: _.template(subject.toString())(data).trim(), body: _.template(body.toString())(data) }; } catch (error) { diff --git a/server/services/monitoringService.ts b/server/services/monitoringService.ts index 65d0a37..5f6c2d5 100644 --- a/server/services/monitoringService.ts +++ b/server/services/monitoringService.ts @@ -19,10 +19,13 @@ import { Domain, DurationSeconds, Hostname, + isBoolean, isDomain, isMonitoringSortField, isOnlineState, isSite, + isString, + isUndefined, MAC, MailType, MonitoringSortField, @@ -164,7 +167,7 @@ async function storeNodeInformation(nodeData: ParsedNode, node: StoredNode): Pro const row = await db.get('SELECT * FROM node_state WHERE mac = ?', [node.mac]); - if (_.isUndefined(row)) { + if (isUndefined(row)) { return await insertNodeInformation(nodeData, node); } else { return await updateNodeInformation(nodeData, node, row); @@ -188,7 +191,7 @@ export function parseNode(importTimestamp: UnixTimestampSeconds, nodeData: any): } const nodeId = nodeData.nodeinfo.node_id; - if (!nodeId || !_.isString(nodeId)) { + if (!nodeId || !isString(nodeId)) { throw new Error( `Invalid node id of type "${typeof nodeId}": ${nodeId}` ); @@ -212,7 +215,7 @@ export function parseNode(importTimestamp: UnixTimestampSeconds, nodeData: any): 'Node ' + nodeId + ': Unexpected flags type: ' + (typeof nodeData.flags) ); } - if (!_.isBoolean(nodeData.flags.online)) { + if (!isBoolean(nodeData.flags.online)) { throw new Error( 'Node ' + nodeId + ': Unexpected flags.online type: ' + (typeof nodeData.flags.online) ); @@ -558,6 +561,7 @@ async function retrieveNodeInformationForUrls(urls: string[]): Promise { const filterFields = [ 'hostname', @@ -571,7 +575,7 @@ export async function getAll(restParams: RestParams): Promise<{ total: number, m const row = await db.get<{ total: number }>( 'SELECT count(*) AS total FROM node_state WHERE ' + where.query, - _.concat([], where.params), + where.params, ); const total = row?.total || 0; @@ -585,7 +589,7 @@ export async function getAll(restParams: RestParams): Promise<{ total: number, m const monitoringStates = await db.all( 'SELECT * FROM node_state WHERE ' + filter.query, - _.concat([], filter.params), + filter.params, ); return {monitoringStates, total}; @@ -603,7 +607,7 @@ export async function getByMacs(macs: MAC[]): Promise const rows = await db.all( 'SELECT * FROM node_state WHERE ' + inCondition.query, - _.concat([], inCondition.params), + inCondition.params, ); for (const row of rows) { @@ -734,7 +738,7 @@ async function deleteNeverOnlineNodesBefore(deleteBefore: UnixTimestampSeconds): deletionCandidates.length ); - const deletionCandidateMacs: MAC[] = _.map(deletionCandidates, node => node.mac); + const deletionCandidateMacs: MAC[] = deletionCandidates.map(node => node.mac); const chunks: MAC[][] = _.chunk(deletionCandidateMacs, NEVER_ONLINE_NODES_DELETION_CHUNK_SIZE); Logger @@ -753,10 +757,7 @@ async function deleteNeverOnlineNodesBefore(deleteBefore: UnixTimestampSeconds): ' MACs for deletion.' ); - const placeholders = _.join( - _.map(macs, () => '?'), - ',' - ); + const placeholders = macs.map(() => '?').join(','); const rows: { mac: MAC }[] = await db.all( `SELECT * FROM node_state WHERE mac IN (${placeholders})`, @@ -773,7 +774,7 @@ async function deleteNeverOnlineNodesBefore(deleteBefore: UnixTimestampSeconds): ' nodes found in monitoring database. Those should be skipped.' ); - const seenMacs: MAC[] = _.map(rows, (row: { mac: MAC }) => row.mac as MAC); + const seenMacs: MAC[] = rows.map(row => row.mac); const neverSeenMacs = _.difference(macs, seenMacs); Logger diff --git a/server/services/nodeService.ts b/server/services/nodeService.ts index 8807f8d..7a12def 100644 --- a/server/services/nodeService.ts +++ b/server/services/nodeService.ts @@ -108,15 +108,15 @@ async function findFilesInPeersPath(): Promise { } function parseNodeFilename(filename: string): NodeFilenameParsed { - const parts = _.split(filename, '@', filenameParts.length); + const parts = filename.split('@', filenameParts.length); const parsed: { [key: string]: string | undefined } = {}; const zippedParts = _.zip(filenameParts, parts); - _.each(zippedParts, part => { + for (const part of zippedParts) { const key = part[0]; if (key) { parsed[key] = part[1]; } - }); + } return parsed; } @@ -565,14 +565,14 @@ export async function fixNodeFilenames(): Promise { export async function findNodesModifiedBefore(timestamp: UnixTimestampSeconds): Promise { const nodes = await getAllNodes(); - return _.filter(nodes, node => node.modifiedAt < timestamp); + return nodes.filter(node => node.modifiedAt < timestamp); } export async function getNodeStatistics(): Promise { const nodes = await getAllNodes(); const nodeStatistics: NodeStatistics = { - registered: _.size(nodes), + registered: nodes.length, withVPN: 0, withCoords: 0, monitoring: { diff --git a/server/types/shared.ts b/server/types/shared.ts index 62c1a58..e1b31e0 100644 --- a/server/types/shared.ts +++ b/server/types/shared.ts @@ -116,6 +116,10 @@ export function toIsEnum(enumDef: E): EnumTypeGuard { return (arg): arg is EnumValue => 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); } diff --git a/server/utils/databaseUtil.ts b/server/utils/databaseUtil.ts index 0865577..aad4b66 100644 --- a/server/utils/databaseUtil.ts +++ b/server/utils/databaseUtil.ts @@ -2,7 +2,7 @@ import _ from "lodash"; export function inCondition(field: string, list: T[]): {query: string, params: T[]} { return { - query: '(' + field + ' IN (' + _.join(_.times(list.length, _.constant('?')), ', ') + '))', + query: '(' + field + ' IN (' + _.times(list.length, () =>'?').join(', ') + '))', params: list, } } diff --git a/server/utils/resources.ts b/server/utils/resources.ts index 94722fc..3d459c5 100644 --- a/server/utils/resources.ts +++ b/server/utils/resources.ts @@ -184,7 +184,7 @@ export function filter(entities: E[], allowedFilterFields: string[], restPara if (!query) { return true; } - return _.some(allowedFilterFields, (field: string): boolean => { + return allowedFilterFields.some((field: string): boolean => { if (!query) { return true; } @@ -209,15 +209,15 @@ export function filter(entities: E[], allowedFilterFields: string[], restPara const filters = restParams.filters; function filtersMatch(entity: Entity): boolean { - if (_.isEmpty(filters)) { + if (isUndefined(filters) || _.isEmpty(filters)) { return true; } - return _.every(filters, (value: any, key: string): boolean => { + return Object.entries(filters).every(([key, value]) => { if (isUndefined(value)) { return true; } - if (_.startsWith(key, 'has')) { + if (key.startsWith('has')) { const entityKey = key.substring(3, 4).toLowerCase() + key.substring(4); return _.isEmpty(entity[entityKey]).toString() !== value; } @@ -225,9 +225,7 @@ export function filter(entities: E[], allowedFilterFields: string[], restPara }); } - return _.filter(entities, function (entity) { - return queryMatches(entity) && filtersMatch(entity); - }); + return entities.filter(entity => queryMatches(entity) && filtersMatch(entity)); } export function sort, S extends string>(entities: T[], isSortField: TypeGuard, restParams: RestParams): T[] { @@ -262,7 +260,7 @@ export function sort, S extends string>(entities: T[], return sorted; } -export function getPageEntities(entities: Entity[], restParams: RestParams) { +export function getPageEntities(entities: Entity[], restParams: RestParams): Entity[] { const page = restParams._page; const perPage = restParams._perPage; @@ -291,7 +289,7 @@ export function filterClause( return { query: filter.query + ' ' + orderBy.query + ' ' + limitOffset.query, - params: _.concat(filter.params, orderBy.params, limitOffset.params) + params: [...filter.params, ...orderBy.params, ...limitOffset.params] }; } diff --git a/server/utils/strings.ts b/server/utils/strings.ts index 12ba84d..b19f155 100644 --- a/server/utils/strings.ts +++ b/server/utils/strings.ts @@ -1,8 +1,8 @@ import _ from "lodash" -import {MAC} from "../types"; +import {isString, MAC} from "../types"; export function normalizeString(str: string): string { - return _.isString(str) ? str.trim().replace(/\s+/g, ' ') : str; + return isString(str) ? str.trim().replace(/\s+/g, ' ') : str; } export function normalizeMac(mac: MAC): MAC { diff --git a/server/utils/time.ts b/server/utils/time.ts index 2dd4a42..83bcdf8 100644 --- a/server/utils/time.ts +++ b/server/utils/time.ts @@ -1,5 +1,4 @@ -import {DurationSeconds, UnixTimestampSeconds} from "../types"; -import _ from "lodash"; +import {DurationSeconds, isString, UnixTimestampSeconds} from "../types"; import moment, {Moment} from "moment"; export function now(): UnixTimestampSeconds { @@ -45,7 +44,7 @@ export function formatTimestamp(timestamp: UnixTimestampSeconds): string { } export function parseTimestamp(timestamp: any): UnixTimestampSeconds | null { - if (!_.isString(timestamp)) { + if (!isString(timestamp)) { return null; } const parsed = moment.utc(timestamp); diff --git a/server/utils/urlBuilder.ts b/server/utils/urlBuilder.ts index 79b037e..384537c 100644 --- a/server/utils/urlBuilder.ts +++ b/server/utils/urlBuilder.ts @@ -1,4 +1,3 @@ -import _ from "lodash" import {config} from "../config" import {MonitoringToken, Url} from "../types" @@ -12,15 +11,10 @@ function formUrl(route: string, queryParams?: { [key: string]: string }): Url { } if (queryParams) { url += '?'; - url += _.join( - _.map( - queryParams, - function (value, key) { - return encodeURIComponent(key) + '=' + encodeURIComponent(value); - } - ), - '&' - ); + url += + Object.entries(queryParams) + .map(([key, value]) => encodeURIComponent(key) + '=' + encodeURIComponent(value)) + .join("&"); } return url as Url; } diff --git a/server/validation/validator.ts b/server/validation/validator.ts index 1879f0e..523ac14 100644 --- a/server/validation/validator.ts +++ b/server/validation/validator.ts @@ -1,7 +1,6 @@ -import _ from "lodash"; - import {parseInteger} from "../utils/strings"; import Logger from "../logger"; +import {isArray, isBoolean, isNumber, isObject, isRegExp, isString, isUndefined} from "../types"; export interface Constraint { type: string, @@ -18,52 +17,48 @@ export interface Constraint { regex?: RegExp, } -export type Constraints = {[key: string]: Constraint}; -export type Values = {[key: string]: any}; - -function isStringArray(arr: any): arr is string[] { - return _.isArray(arr) && _.every(arr, (val: any) => _.isString(val)); -} +export type Constraints = { [key: string]: Constraint }; +export type Values = { [key: string]: any }; export function isConstraint(val: any): val is Constraint { - if (!_.isObject(val)) { + if (!isObject(val)) { return false; } - const constraint = val as {[key: string]: any}; + const constraint = val as { [key: string]: any }; - if (!("type" in constraint) || !_.isString(constraint.type)) { + if (!("type" in constraint) || !isString(constraint.type)) { return false; } if ("optional" in constraint - && !_.isUndefined(constraint.optional) - && !_.isBoolean(constraint.optional)) { + && !isUndefined(constraint.optional) + && !isBoolean(constraint.optional)) { return false; } if ("allowed" in constraint - && !_.isUndefined(constraint.allowed) - && !isStringArray(constraint.allowed)) { + && !isUndefined(constraint.allowed) + && !isArray(constraint.allowed, isString)) { return false; } if ("min" in constraint - && !_.isUndefined(constraint.min) - && !_.isNumber(constraint.min)) { + && !isUndefined(constraint.min) + && !isNumber(constraint.min)) { return false; } if ("max" in constraint - && !_.isUndefined(constraint.max) - && !_.isNumber(constraint.max)) { + && !isUndefined(constraint.max) + && !isNumber(constraint.max)) { return false; } // noinspection RedundantIfStatementJS if ("regex" in constraint - && !_.isUndefined(constraint.regex) - && !_.isRegExp(constraint.regex)) { + && !isUndefined(constraint.regex) + && !isRegExp(constraint.regex)) { return false; } @@ -71,41 +66,38 @@ export function isConstraint(val: any): val is Constraint { } export function isConstraints(constraints: any): constraints is Constraints { - if (!_.isObject(constraints)) { + if (!isObject(constraints)) { return false; } - return _.every( - constraints, - (constraint: any, key: any) => _.isString(key) && isConstraint(constraint) - ); + return Object.entries(constraints).every(([key, constraint]) => isString(key) && isConstraint(constraint)); } // TODO: sanitize input for further processing as specified by constraints (correct types, trimming, etc.) function isValidBoolean(value: any): boolean { - return _.isBoolean(value) || value === 'true' || value === 'false'; + return isBoolean(value) || value === 'true' || value === 'false'; } function isValidNumber(constraint: Constraint, value: any): boolean { - if (_.isString(value)) { + if (isString(value)) { value = parseInteger(value); } - if (!_.isNumber(value)) { + if (!isNumber(value)) { return false; } - if (_.isNaN(value) || !_.isFinite(value)) { + if (isNaN(value) || !isFinite(value)) { return false; } - if (_.isNumber(constraint.min) && value < constraint.min) { + if (isNumber(constraint.min) && value < constraint.min) { return false; } // noinspection RedundantIfStatementJS - if (_.isNumber(constraint.max) && value > constraint.max) { + if (isNumber(constraint.max) && value > constraint.max) { return false; } @@ -113,11 +105,12 @@ function isValidNumber(constraint: Constraint, value: any): boolean { } function isValidEnum(constraint: Constraint, value: any): boolean { - if (!_.isString(value)) { + if (!isString(value)) { return false; } - return _.indexOf(constraint.allowed, value) >= 0; + const allowed = constraint.allowed || []; + return allowed.indexOf(value) >= 0; } function isValidString(constraint: Constraint, value: any): boolean { @@ -125,7 +118,7 @@ function isValidString(constraint: Constraint, value: any): boolean { throw new Error("String constraints must have regex set: " + constraint); } - if (!_.isString(value)) { + if (!isString(value)) { return false; } @@ -174,10 +167,10 @@ function areValid(constraints: Constraints, acceptUndefined: boolean, values: Va return true; } -export function forConstraint (constraint: Constraint, acceptUndefined: boolean): (value: any) => boolean { +export function forConstraint(constraint: Constraint, acceptUndefined: boolean): (value: any) => boolean { return ((value: any): boolean => isValid(constraint, acceptUndefined, value)); } -export function forConstraints (constraints: Constraints, acceptUndefined: boolean): (values: Values) => boolean { +export function forConstraints(constraints: Constraints, acceptUndefined: boolean): (values: Values) => boolean { return ((values: Values): boolean => areValid(constraints, acceptUndefined, values)); }