Files
ukraine-timemap/src/reducers/validate/validators.js

183 lines
5.1 KiB
JavaScript

import Joi from "joi";
import createEventSchema from "./eventSchema";
import siteSchema from "./siteSchema";
import associationsSchema from "./associationsSchema";
import sourceSchema from "./sourceSchema";
import shapeSchema from "./shapeSchema";
import { calcDatetime, capitalize } from "../../common/utilities";
/*
* Create an error notification object
* Types: ['error', 'warning', 'good', 'neural']
*/
function makeError(type, id, message) {
return {
type: "error",
id,
message: `${type} ${id}: ${message}`,
};
}
function isValidDate(d) {
return d instanceof Date && !isNaN(d);
}
function findDuplicateAssociations(associations) {
const seenSet = new Set([]);
const duplicates = [];
associations.forEach((item) => {
if (seenSet.has(item.id)) {
duplicates.push({
id: item.id,
error: makeError(
"Association",
item.id,
"association was found more than once. Ignoring duplicate."
),
});
} else {
seenSet.add(item.id);
}
});
return duplicates;
}
/*
* Validate domain schema
*/
export function validateDomain(domain, features) {
const sanitizedDomain = {
events: [],
sites: [],
associations: [],
sources: {},
shapes: [],
notifications: domain ? domain.notifications : null,
};
if (domain === undefined) {
return sanitizedDomain;
}
const discardedDomain = {
events: [],
sites: [],
associations: [],
sources: [],
shapes: [],
};
function validateArrayItem(item, domainKey, schema) {
const result = Joi.validate(item, schema);
if (result.error !== null) {
const id = item.id || "-";
const domainStr = capitalize(domainKey);
const error = makeError(domainStr, id, result.error.message);
discardedDomain[domainKey].push(Object.assign(item, { error }));
} else {
sanitizedDomain[domainKey].push(item);
}
}
function validateArray(items, domainKey, schema) {
items.forEach((item) => {
validateArrayItem(item, domainKey, schema);
});
}
function validateObject(obj, domainKey, itemSchema) {
Object.keys(obj).forEach((key) => {
const vl = obj[key];
const result = Joi.validate(vl, itemSchema);
if (result.error !== null) {
const id = vl.id || "-";
const domainStr = capitalize(domainKey);
discardedDomain[domainKey].push({
...vl,
error: makeError(domainStr, id, result.error.message),
});
} else {
sanitizedDomain[domainKey][key] = vl;
}
});
}
if (!Array.isArray(features.CUSTOM_EVENT_FIELDS)) {
features.CUSTOM_EVENT_FIELDS = [];
}
const eventSchema = createEventSchema(features.CUSTOM_EVENT_FIELDS);
validateArray(domain.events, "events", eventSchema);
validateArray(domain.sites, "sites", siteSchema);
validateArray(domain.associations, "associations", associationsSchema);
validateObject(domain.sources, "sources", sourceSchema);
validateObject(domain.shapes, "shapes", shapeSchema);
// NB: [lat, lon] array is best format for projecting into map
sanitizedDomain.shapes = sanitizedDomain.shapes.map((shape) => ({
name: shape.name,
points: shape.items.map((coords) => coords.replace(/\s/g, "").split(",")),
}));
const duplicateAssociations = findDuplicateAssociations(domain.associations);
// Duplicated associations
if (duplicateAssociations.length > 0) {
sanitizedDomain.notifications.push({
message:
"Associations are required to be unique. Ignoring duplicates for now.",
items: duplicateAssociations,
type: "error",
});
}
sanitizedDomain.associations = domain.associations;
// append events with datetime and sort
sanitizedDomain.events = sanitizedDomain.events.filter((event, idx) => {
event.id = idx;
// event.associations comes in as a [association.ids...]; convert to actual association objects
event.associations = event.associations.reduce((acc, id) => {
const foundAssociation = sanitizedDomain.associations.find(
(elem) => elem.id === id
);
if (foundAssociation) acc.push(foundAssociation);
return acc;
}, []);
// if lat, long come in with commas, replace with decimal format
event.latitude = event.latitude.replace(",", ".");
event.longitude = event.longitude.replace(",", ".");
event.datetime = calcDatetime(event.date, event.time);
if (!isValidDate(event.datetime)) {
discardedDomain.events.push({
...event,
error: makeError(
"events",
event.id,
"Invalid date. It's been dropped, as otherwise timemap won't work as expected."
),
});
return false;
}
return true;
});
sanitizedDomain.events.sort((a, b) => a.datetime - b.datetime);
// Message the number of failed items in domain
Object.keys(discardedDomain).forEach((disc) => {
const len = discardedDomain[disc].length;
if (len) {
sanitizedDomain.notifications.push({
message: `${len} invalid ${disc} not displayed.`,
items: discardedDomain[disc],
type: "error",
});
}
});
return sanitizedDomain;
}