Get rid of lots of unnecessary lodash calls.

This commit is contained in:
baldo 2022-07-28 13:16:13 +02:00
parent b734a422a7
commit 5592892f0d
16 changed files with 99 additions and 115 deletions

View file

@ -1,6 +1,5 @@
import {LogLevel, LogLevels, isLogLevel} from "./types"; import {isLogLevel, LogLevel, LogLevels} from "./types";
import {ActivatableLoggerImpl} from "./logger"; import {ActivatableLoggerImpl} from "./logger";
import _ from "lodash";
class TestableLogger extends ActivatableLoggerImpl { class TestableLogger extends ActivatableLoggerImpl {
private logs: any[][] = []; private logs: any[][] = [];
@ -35,13 +34,13 @@ function parseLogEntry(logEntry: any[]): ParsedLogEntry {
`Empty log entry. Should always start with log message: ${logEntry}` `Empty log entry. Should always start with log message: ${logEntry}`
); );
} }
const logMessage = logEntry[0]; const logMessage = logEntry[0];
if (typeof logMessage !== "string") { if (typeof logMessage !== "string") {
throw new Error( throw new Error(
`Expected log entry to start with string, but got: ${logMessage}` `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 regexp = /^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2} ([A-Z]+) - (\[[^\]]*\])? *(.*)$/;
const groups = logMessage.match(regexp); 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 tagsStr = groups[2].substring(1, groups[2].length - 1);
const tags = tagsStr ? _.split(tagsStr, ", ") : []; const tags = tagsStr ? tagsStr.split(", "): [];
const message = groups[3]; const message = groups[3];
const args = logEntry.slice(1); const args = logEntry.slice(1);
return { return {
level: level as LogLevel, level: level as LogLevel,
tags, tags,
message, message,
args, args,
}; };
@ -158,7 +157,7 @@ for (const level of LogLevels) {
message: "%s %d %f %o %%", message: "%s %d %f %o %%",
args: [], args: [],
}]); }]);
}); });
test(`should not escape ${level} message arguments`, () => { test(`should not escape ${level} message arguments`, () => {
// given // given
@ -174,7 +173,7 @@ for (const level of LogLevels) {
message: "message", message: "message",
args: [1, "%s", "%d", "%f", "%o", "%"], args: [1, "%s", "%d", "%f", "%o", "%"],
}]); }]);
}); });
test(`should not log ${level} message on disabled logger`, () => { test(`should not log ${level} message on disabled logger`, () => {
// given // given
@ -185,6 +184,6 @@ for (const level of LogLevels) {
// then // then
expect(parseLogs(logger.getLogs())).toEqual([]); expect(parseLogs(logger.getLogs())).toEqual([]);
}); });
} }

View file

@ -1,6 +1,5 @@
import {Logger, TaggedLogger, LogLevel} from './types'; import {isString, Logger, LogLevel, TaggedLogger} from './types';
import moment from 'moment'; import moment from 'moment';
import _ from 'lodash';
export type LoggingFunction = (...args: any[]) => void; export type LoggingFunction = (...args: any[]) => void;
@ -36,15 +35,15 @@ export class ActivatableLoggerImpl implements ActivatableLogger {
log(level: LogLevel, ...args: any[]): void { log(level: LogLevel, ...args: any[]): void {
const timeStr = moment().format('YYYY-MM-DD HH:mm:ss'); const timeStr = moment().format('YYYY-MM-DD HH:mm:ss');
const levelStr = level.toUpperCase(); const levelStr = level.toUpperCase();
const tagsStr = tags ? '[' + _.join(tags, ', ') + ']' : ''; const tagsStr = tags ? '[' + tags.join(', ') + ']' : '';
const messagePrefix = `${timeStr} ${levelStr} - ${tagsStr}`; const messagePrefix = `${timeStr} ${levelStr} - ${tagsStr}`;
// Make sure to only replace %s, etc. in real log message // Make sure to only replace %s, etc. in real log message
// but not in tags. // but not in tags.
const escapedMessagePrefix = messagePrefix.replace(/%/g, '%%'); const escapedMessagePrefix = messagePrefix.replace(/%/g, '%%');
let message = ''; let message = '';
if (args && _.isString(args[0])) { if (args && isString(args[0])) {
message = args[0]; message = args[0];
args.shift(); args.shift();
} }

View file

@ -1,5 +1,3 @@
import _ from "lodash";
import CONSTRAINTS from "../validation/constraints"; import CONSTRAINTS from "../validation/constraints";
import ErrorTypes from "../utils/errorTypes"; import ErrorTypes from "../utils/errorTypes";
import * as MonitoringService from "../services/monitoringService"; 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); const {monitoringStates, total} = await MonitoringService.getAll(restParams);
return { return {
total, total,
result: _.map(monitoringStates, function (state) { result: monitoringStates.map(state => {
state.mapId = _.toLower(state.mac).replace(/:/g, ''); state.mapId = state.mac.toLowerCase().replace(/:/g, "");
return state; return state;
}) })
}; };

View file

@ -1,5 +1,3 @@
import _ from "lodash";
import Constraints from "../validation/constraints"; import Constraints from "../validation/constraints";
import ErrorTypes from "../utils/errorTypes"; import ErrorTypes from "../utils/errorTypes";
import * as MonitoringService from "../services/monitoringService"; import * as MonitoringService from "../services/monitoringService";
@ -13,12 +11,12 @@ import {
CreateOrUpdateNode, CreateOrUpdateNode,
DomainSpecificNodeResponse, DomainSpecificNodeResponse,
isNodeSortField, isNodeSortField,
isToken, JSONObject, isToken,
JSONObject,
MAC, MAC,
NodeResponse, NodeResponse,
NodeStateData, NodeStateData,
NodeTokenResponse, NodeTokenResponse,
StoredNode,
toDomainSpecificNodeResponse, toDomainSpecificNodeResponse,
Token, Token,
toNodeResponse, toNodeResponse,
@ -27,15 +25,18 @@ import {
const nodeFields = ['hostname', 'key', 'email', 'nickname', 'mac', 'coords', 'monitoring']; const nodeFields = ['hostname', 'key', 'email', 'nickname', 'mac', 'coords', 'monitoring'];
// TODO: Rename
function getNormalizedNodeData(reqData: any): CreateOrUpdateNode { function getNormalizedNodeData(reqData: any): CreateOrUpdateNode {
const node: { [key: string]: any } = {}; const node: { [key: string]: any } = {};
_.each(nodeFields, function (field) { for (const field of nodeFields) {
let value = normalizeString(reqData[field]); let value = normalizeString(reqData[field]);
if (field === 'mac') { if (field === 'mac') {
value = normalizeMac(value as MAC); value = normalizeMac(value as MAC);
} }
node[field] = value; node[field] = value;
}); }
// TODO: Add typeguard before cast.
return node as CreateOrUpdateNode; return node as CreateOrUpdateNode;
} }
@ -90,15 +91,15 @@ async function doGetAll(req: Request): Promise<{ total: number; pageNodes: any }
const nodes = await NodeService.getAllNodes(); 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. // 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 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] || {}; const nodeState: NodeStateData = nodeStateByMac[node.mac] || {};
return toDomainSpecificNodeResponse(node, nodeState); return toDomainSpecificNodeResponse(node, nodeState);
}); });

View file

@ -1,9 +1,7 @@
import _ from "lodash";
import CONSTRAINTS from "../validation/constraints"; import CONSTRAINTS from "../validation/constraints";
import ErrorTypes from "../utils/errorTypes"; import ErrorTypes from "../utils/errorTypes";
import * as Resources from "../utils/resources"; import * as Resources from "../utils/resources";
import {Entity, handleJSONWithData, RequestData} from "../utils/resources"; import {handleJSONWithData, RequestData} from "../utils/resources";
import {getTasks, Task, TaskState} from "../jobs/scheduler"; import {getTasks, Task, TaskState} from "../jobs/scheduler";
import {normalizeString} from "../utils/strings"; import {normalizeString} from "../utils/strings";
import {forConstraint} from "../validation/validator"; import {forConstraint} from "../validation/validator";
@ -77,11 +75,11 @@ async function setTaskEnabled(data: RequestData, enable: boolean): Promise<TaskR
return toTaskResponse(task); return toTaskResponse(task);
} }
async function doGetAll(req: Request): Promise<{ total: number, pageTasks: Entity[] }> { async function doGetAll(req: Request): Promise<{ total: number, pageTasks: Task[] }> {
const restParams = await Resources.getValidRestParams('list', null, req); const restParams = await Resources.getValidRestParams('list', null, req);
const tasks = Resources.sort( const tasks = Resources.sort(
_.values(getTasks()), Object.values(getTasks()),
isTaskSortField, isTaskSortField,
restParams restParams
); );
@ -104,7 +102,7 @@ export function getAll(req: Request, res: Response): void {
doGetAll(req) doGetAll(req)
.then(({total, pageTasks}) => { .then(({total, pageTasks}) => {
res.set('X-Total-Count', total.toString(10)); 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)); .catch(err => Resources.error(res, err));
} }

View file

@ -170,7 +170,7 @@ export async function getPendingMails(restParams: RestParams): Promise<{ mails:
const mails = await db.all( const mails = await db.all(
'SELECT * FROM email_queue WHERE ' + filter.query, 'SELECT * FROM email_queue WHERE ' + filter.query,
_.concat([], filter.params), filter.params,
); );
return { return {

View file

@ -97,7 +97,7 @@ export async function render(mailOptions: Mail): Promise<{subject: string, body:
try { try {
return { return {
subject: _.trim(_.template(subject.toString())(data)), subject: _.template(subject.toString())(data).trim(),
body: _.template(body.toString())(data) body: _.template(body.toString())(data)
}; };
} catch (error) { } catch (error) {

View file

@ -19,10 +19,13 @@ import {
Domain, Domain,
DurationSeconds, DurationSeconds,
Hostname, Hostname,
isBoolean,
isDomain, isDomain,
isMonitoringSortField, isMonitoringSortField,
isOnlineState, isOnlineState,
isSite, isSite,
isString,
isUndefined,
MAC, MAC,
MailType, MailType,
MonitoringSortField, 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]); const row = await db.get('SELECT * FROM node_state WHERE mac = ?', [node.mac]);
if (_.isUndefined(row)) { if (isUndefined(row)) {
return await insertNodeInformation(nodeData, node); return await insertNodeInformation(nodeData, node);
} else { } else {
return await updateNodeInformation(nodeData, node, row); return await updateNodeInformation(nodeData, node, row);
@ -188,7 +191,7 @@ export function parseNode(importTimestamp: UnixTimestampSeconds, nodeData: any):
} }
const nodeId = nodeData.nodeinfo.node_id; const nodeId = nodeData.nodeinfo.node_id;
if (!nodeId || !_.isString(nodeId)) { if (!nodeId || !isString(nodeId)) {
throw new Error( throw new Error(
`Invalid node id of type "${typeof nodeId}": ${nodeId}` `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) 'Node ' + nodeId + ': Unexpected flags type: ' + (typeof nodeData.flags)
); );
} }
if (!_.isBoolean(nodeData.flags.online)) { if (!isBoolean(nodeData.flags.online)) {
throw new Error( throw new Error(
'Node ' + nodeId + ': Unexpected flags.online type: ' + (typeof nodeData.flags.online) 'Node ' + nodeId + ': Unexpected flags.online type: ' + (typeof nodeData.flags.online)
); );
@ -558,6 +561,7 @@ async function retrieveNodeInformationForUrls(urls: string[]): Promise<RetrieveN
} }
} }
// FIXME: Replace any[] by type.
export async function getAll(restParams: RestParams): Promise<{ total: number, monitoringStates: any[] }> { export async function getAll(restParams: RestParams): Promise<{ total: number, monitoringStates: any[] }> {
const filterFields = [ const filterFields = [
'hostname', 'hostname',
@ -571,7 +575,7 @@ export async function getAll(restParams: RestParams): Promise<{ total: number, m
const row = await db.get<{ total: number }>( const row = await db.get<{ total: number }>(
'SELECT count(*) AS total FROM node_state WHERE ' + where.query, 'SELECT count(*) AS total FROM node_state WHERE ' + where.query,
_.concat([], where.params), where.params,
); );
const total = row?.total || 0; const total = row?.total || 0;
@ -585,7 +589,7 @@ export async function getAll(restParams: RestParams): Promise<{ total: number, m
const monitoringStates = await db.all( const monitoringStates = await db.all(
'SELECT * FROM node_state WHERE ' + filter.query, 'SELECT * FROM node_state WHERE ' + filter.query,
_.concat([], filter.params), filter.params,
); );
return {monitoringStates, total}; return {monitoringStates, total};
@ -603,7 +607,7 @@ export async function getByMacs(macs: MAC[]): Promise<Record<MAC, NodeStateData>
const rows = await db.all<NodeStateRow>( const rows = await db.all<NodeStateRow>(
'SELECT * FROM node_state WHERE ' + inCondition.query, 'SELECT * FROM node_state WHERE ' + inCondition.query,
_.concat([], inCondition.params), inCondition.params,
); );
for (const row of rows) { for (const row of rows) {
@ -734,7 +738,7 @@ async function deleteNeverOnlineNodesBefore(deleteBefore: UnixTimestampSeconds):
deletionCandidates.length 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); const chunks: MAC[][] = _.chunk(deletionCandidateMacs, NEVER_ONLINE_NODES_DELETION_CHUNK_SIZE);
Logger Logger
@ -753,10 +757,7 @@ async function deleteNeverOnlineNodesBefore(deleteBefore: UnixTimestampSeconds):
' MACs for deletion.' ' MACs for deletion.'
); );
const placeholders = _.join( const placeholders = macs.map(() => '?').join(',');
_.map(macs, () => '?'),
','
);
const rows: { mac: MAC }[] = await db.all( const rows: { mac: MAC }[] = await db.all(
`SELECT * FROM node_state WHERE mac IN (${placeholders})`, `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.' ' 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); const neverSeenMacs = _.difference(macs, seenMacs);
Logger Logger

View file

@ -108,15 +108,15 @@ async function findFilesInPeersPath(): Promise<string[]> {
} }
function parseNodeFilename(filename: string): NodeFilenameParsed { function parseNodeFilename(filename: string): NodeFilenameParsed {
const parts = _.split(filename, '@', filenameParts.length); const parts = filename.split('@', filenameParts.length);
const parsed: { [key: string]: string | undefined } = {}; const parsed: { [key: string]: string | undefined } = {};
const zippedParts = _.zip<string, string>(filenameParts, parts); const zippedParts = _.zip<string, string>(filenameParts, parts);
_.each(zippedParts, part => { for (const part of zippedParts) {
const key = part[0]; const key = part[0];
if (key) { if (key) {
parsed[key] = part[1]; parsed[key] = part[1];
} }
}); }
return parsed; return parsed;
} }
@ -565,14 +565,14 @@ export async function fixNodeFilenames(): Promise<void> {
export async function findNodesModifiedBefore(timestamp: UnixTimestampSeconds): Promise<StoredNode[]> { export async function findNodesModifiedBefore(timestamp: UnixTimestampSeconds): Promise<StoredNode[]> {
const nodes = await getAllNodes(); const nodes = await getAllNodes();
return _.filter(nodes, node => node.modifiedAt < timestamp); return nodes.filter(node => node.modifiedAt < timestamp);
} }
export async function getNodeStatistics(): Promise<NodeStatistics> { export async function getNodeStatistics(): Promise<NodeStatistics> {
const nodes = await getAllNodes(); const nodes = await getAllNodes();
const nodeStatistics: NodeStatistics = { const nodeStatistics: NodeStatistics = {
registered: _.size(nodes), registered: nodes.length,
withVPN: 0, withVPN: 0,
withCoords: 0, withCoords: 0,
monitoring: { monitoring: {

View file

@ -116,6 +116,10 @@ export function toIsEnum<E>(enumDef: E): EnumTypeGuard<E> {
return (arg): arg is EnumValue<E> => Object.values(enumDef).includes(arg as [keyof E]); return (arg): arg is EnumValue<E> => Object.values(enumDef).includes(arg as [keyof E]);
} }
export function isRegExp(arg: unknown): arg is RegExp {
return isObject(arg) && arg instanceof RegExp;
}
export function isOptional<T>(arg: unknown, isT: TypeGuard<T>): arg is (T | undefined) { export function isOptional<T>(arg: unknown, isT: TypeGuard<T>): arg is (T | undefined) {
return arg === undefined || isT(arg); return arg === undefined || isT(arg);
} }

View file

@ -2,7 +2,7 @@ import _ from "lodash";
export function inCondition<T>(field: string, list: T[]): {query: string, params: T[]} { export function inCondition<T>(field: string, list: T[]): {query: string, params: T[]} {
return { return {
query: '(' + field + ' IN (' + _.join(_.times(list.length, _.constant('?')), ', ') + '))', query: '(' + field + ' IN (' + _.times(list.length, () =>'?').join(', ') + '))',
params: list, params: list,
} }
} }

View file

@ -184,7 +184,7 @@ export function filter<E>(entities: E[], allowedFilterFields: string[], restPara
if (!query) { if (!query) {
return true; return true;
} }
return _.some(allowedFilterFields, (field: string): boolean => { return allowedFilterFields.some((field: string): boolean => {
if (!query) { if (!query) {
return true; return true;
} }
@ -209,15 +209,15 @@ export function filter<E>(entities: E[], allowedFilterFields: string[], restPara
const filters = restParams.filters; const filters = restParams.filters;
function filtersMatch(entity: Entity): boolean { function filtersMatch(entity: Entity): boolean {
if (_.isEmpty(filters)) { if (isUndefined(filters) || _.isEmpty(filters)) {
return true; return true;
} }
return _.every(filters, (value: any, key: string): boolean => { return Object.entries(filters).every(([key, value]) => {
if (isUndefined(value)) { if (isUndefined(value)) {
return true; return true;
} }
if (_.startsWith(key, 'has')) { if (key.startsWith('has')) {
const entityKey = key.substring(3, 4).toLowerCase() + key.substring(4); const entityKey = key.substring(3, 4).toLowerCase() + key.substring(4);
return _.isEmpty(entity[entityKey]).toString() !== value; return _.isEmpty(entity[entityKey]).toString() !== value;
} }
@ -225,9 +225,7 @@ export function filter<E>(entities: E[], allowedFilterFields: string[], restPara
}); });
} }
return _.filter(entities, function (entity) { return entities.filter(entity => queryMatches(entity) && filtersMatch(entity));
return queryMatches(entity) && filtersMatch(entity);
});
} }
export function sort<T extends Record<S, any>, S extends string>(entities: T[], isSortField: TypeGuard<S>, restParams: RestParams): T[] { export function sort<T extends Record<S, any>, S extends string>(entities: T[], isSortField: TypeGuard<S>, restParams: RestParams): T[] {
@ -262,7 +260,7 @@ export function sort<T extends Record<S, any>, S extends string>(entities: T[],
return sorted; return sorted;
} }
export function getPageEntities(entities: Entity[], restParams: RestParams) { export function getPageEntities<Entity>(entities: Entity[], restParams: RestParams): Entity[] {
const page = restParams._page; const page = restParams._page;
const perPage = restParams._perPage; const perPage = restParams._perPage;
@ -291,7 +289,7 @@ export function filterClause<S>(
return { return {
query: filter.query + ' ' + orderBy.query + ' ' + limitOffset.query, query: filter.query + ' ' + orderBy.query + ' ' + limitOffset.query,
params: _.concat(filter.params, orderBy.params, limitOffset.params) params: [...filter.params, ...orderBy.params, ...limitOffset.params]
}; };
} }

View file

@ -1,8 +1,8 @@
import _ from "lodash" import _ from "lodash"
import {MAC} from "../types"; import {isString, MAC} from "../types";
export function normalizeString(str: string): string { 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 { export function normalizeMac(mac: MAC): MAC {

View file

@ -1,5 +1,4 @@
import {DurationSeconds, UnixTimestampSeconds} from "../types"; import {DurationSeconds, isString, UnixTimestampSeconds} from "../types";
import _ from "lodash";
import moment, {Moment} from "moment"; import moment, {Moment} from "moment";
export function now(): UnixTimestampSeconds { export function now(): UnixTimestampSeconds {
@ -45,7 +44,7 @@ export function formatTimestamp(timestamp: UnixTimestampSeconds): string {
} }
export function parseTimestamp(timestamp: any): UnixTimestampSeconds | null { export function parseTimestamp(timestamp: any): UnixTimestampSeconds | null {
if (!_.isString(timestamp)) { if (!isString(timestamp)) {
return null; return null;
} }
const parsed = moment.utc(timestamp); const parsed = moment.utc(timestamp);

View file

@ -1,4 +1,3 @@
import _ from "lodash"
import {config} from "../config" import {config} from "../config"
import {MonitoringToken, Url} from "../types" import {MonitoringToken, Url} from "../types"
@ -12,15 +11,10 @@ function formUrl(route: string, queryParams?: { [key: string]: string }): Url {
} }
if (queryParams) { if (queryParams) {
url += '?'; url += '?';
url += _.join( url +=
_.map( Object.entries(queryParams)
queryParams, .map(([key, value]) => encodeURIComponent(key) + '=' + encodeURIComponent(value))
function (value, key) { .join("&");
return encodeURIComponent(key) + '=' + encodeURIComponent(value);
}
),
'&'
);
} }
return url as Url; return url as Url;
} }

View file

@ -1,7 +1,6 @@
import _ from "lodash";
import {parseInteger} from "../utils/strings"; import {parseInteger} from "../utils/strings";
import Logger from "../logger"; import Logger from "../logger";
import {isArray, isBoolean, isNumber, isObject, isRegExp, isString, isUndefined} from "../types";
export interface Constraint { export interface Constraint {
type: string, type: string,
@ -18,52 +17,48 @@ export interface Constraint {
regex?: RegExp, regex?: RegExp,
} }
export type Constraints = {[key: string]: Constraint}; export type Constraints = { [key: string]: Constraint };
export type Values = {[key: string]: any}; export type Values = { [key: string]: any };
function isStringArray(arr: any): arr is string[] {
return _.isArray(arr) && _.every(arr, (val: any) => _.isString(val));
}
export function isConstraint(val: any): val is Constraint { export function isConstraint(val: any): val is Constraint {
if (!_.isObject(val)) { if (!isObject(val)) {
return false; 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; return false;
} }
if ("optional" in constraint if ("optional" in constraint
&& !_.isUndefined(constraint.optional) && !isUndefined(constraint.optional)
&& !_.isBoolean(constraint.optional)) { && !isBoolean(constraint.optional)) {
return false; return false;
} }
if ("allowed" in constraint if ("allowed" in constraint
&& !_.isUndefined(constraint.allowed) && !isUndefined(constraint.allowed)
&& !isStringArray(constraint.allowed)) { && !isArray(constraint.allowed, isString)) {
return false; return false;
} }
if ("min" in constraint if ("min" in constraint
&& !_.isUndefined(constraint.min) && !isUndefined(constraint.min)
&& !_.isNumber(constraint.min)) { && !isNumber(constraint.min)) {
return false; return false;
} }
if ("max" in constraint if ("max" in constraint
&& !_.isUndefined(constraint.max) && !isUndefined(constraint.max)
&& !_.isNumber(constraint.max)) { && !isNumber(constraint.max)) {
return false; return false;
} }
// noinspection RedundantIfStatementJS // noinspection RedundantIfStatementJS
if ("regex" in constraint if ("regex" in constraint
&& !_.isUndefined(constraint.regex) && !isUndefined(constraint.regex)
&& !_.isRegExp(constraint.regex)) { && !isRegExp(constraint.regex)) {
return false; return false;
} }
@ -71,41 +66,38 @@ export function isConstraint(val: any): val is Constraint {
} }
export function isConstraints(constraints: any): constraints is Constraints { export function isConstraints(constraints: any): constraints is Constraints {
if (!_.isObject(constraints)) { if (!isObject(constraints)) {
return false; return false;
} }
return _.every( return Object.entries(constraints).every(([key, constraint]) => isString(key) && isConstraint(constraint));
constraints,
(constraint: any, key: any) => _.isString(key) && isConstraint(constraint)
);
} }
// TODO: sanitize input for further processing as specified by constraints (correct types, trimming, etc.) // TODO: sanitize input for further processing as specified by constraints (correct types, trimming, etc.)
function isValidBoolean(value: any): boolean { 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 { function isValidNumber(constraint: Constraint, value: any): boolean {
if (_.isString(value)) { if (isString(value)) {
value = parseInteger(value); value = parseInteger(value);
} }
if (!_.isNumber(value)) { if (!isNumber(value)) {
return false; return false;
} }
if (_.isNaN(value) || !_.isFinite(value)) { if (isNaN(value) || !isFinite(value)) {
return false; return false;
} }
if (_.isNumber(constraint.min) && value < constraint.min) { if (isNumber(constraint.min) && value < constraint.min) {
return false; return false;
} }
// noinspection RedundantIfStatementJS // noinspection RedundantIfStatementJS
if (_.isNumber(constraint.max) && value > constraint.max) { if (isNumber(constraint.max) && value > constraint.max) {
return false; return false;
} }
@ -113,11 +105,12 @@ function isValidNumber(constraint: Constraint, value: any): boolean {
} }
function isValidEnum(constraint: Constraint, value: any): boolean { function isValidEnum(constraint: Constraint, value: any): boolean {
if (!_.isString(value)) { if (!isString(value)) {
return false; return false;
} }
return _.indexOf(constraint.allowed, value) >= 0; const allowed = constraint.allowed || [];
return allowed.indexOf(value) >= 0;
} }
function isValidString(constraint: Constraint, value: any): boolean { 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); throw new Error("String constraints must have regex set: " + constraint);
} }
if (!_.isString(value)) { if (!isString(value)) {
return false; return false;
} }
@ -174,10 +167,10 @@ function areValid(constraints: Constraints, acceptUndefined: boolean, values: Va
return true; 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)); 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)); return ((values: Values): boolean => areValid(constraints, acceptUndefined, values));
} }