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; // 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; }