@gov-cy/govcy-express-services
Version:
An Express-based system that dynamically renders services using @gov-cy/govcy-frontend-renderer and posts data to a submission API.
352 lines (317 loc) • 14.6 kB
JavaScript
/**
* @module govcyValidator
* @fileoverview This module provides validation functions for form elements.
* It includes a function to validate form elements based on specified rules and conditions.
* It also handles conditional elements and checks for specific input types.
* Validation Types Breakdown:
* - `required`: Checks if the value is not null, undefined, or an empty string (after trimming).
* - `valid`: Executes the appropriate validation based on the checkValue (e.g., numeric, telCY, etc.).
* - `length`: Ensures that the value's length doesn't exceed the specified limit.
* - `regCheck`: Performs custom regex validation as per the rule's checkValue.
*/
import * as govcyResources from "../resources/govcyResources.mjs";
import { ALLOWED_FORM_ELEMENTS } from "./govcyConstants.mjs";
/**
* This function validates a value based on the provided rules.
*
* @param {string} value The value to validate
* @param {Array} rules The validation rules to apply
* @returns Error message text object if validation fails, otherwise null
*/
function validateValue(value, rules) {
const validationRules = {
// Valid validation rules
numeric: (val) => /^\d+$/.test(val),
numDecimal: (val) => /^\d+(,\d+)?$/.test(val),
currency: (val) => /^\d+(,\d{1,2})?$/.test(val),
alpha: (val) => /^[A-Za-zΑ-Ωα-ω\u0370-\u03ff\u1f00-\u1fff\s]+$/.test(val),
alphaNum: (val) => /^[A-Za-zΑ-Ωα-ω\u0370-\u03ff\u1f00-\u1fff0-9\s]+$/.test(val),
noSpecialChars: (val) => /^([0-9]|[A-Z]|[a-z]|[α-ω]|[Α-Ω]|[,]|[.]|[-]|[(]|[)]|[?]|[!]|[;]|[:]|[\n]|[\r]|[ _]|[\u0370-\u03ff\u1f00-\u1fff])+$/.test(val),
name: (val) => /^[A-Za-zΑ-Ωα-ω\u0370-\u03ff\u1f00-\u1fff\s'-]+$/.test(val),
tel: (val) => /^(?:\+|00)?[\d\s\-()]{8,20}$/.test(val.replace(/[\s\-()]/g, '')),
mobile: (val) => /^(?:\+|00)?[\d\s\-()]{8,20}$/.test(val.replace(/[\s\-()]/g, '')),
// telCY: (val) => /^(?:\+|00)?357[-\s]?(2|9)\d{7}$/.test(val.replace(/[\s\-()]/g, '')),
// telCY: (val) => /^(?:\+|00)?357?(2|9)\d{7}$/.test(val.replace(/[\s\-()]/g, '')),
telCY: (val) => {
const normalized = val.replace(/[\s\-()]/g, '');
const isValid = /^(?:\+357|00357)?(2|9)\d{7}$/.test(normalized);
return isValid;
},
// mobileCY: (val) => /^(?:\+|00)?357[-\s]?9\d{7}$/.test(val.replace(/[\s\-()]/g, '')),
mobileCY: (val) => {
const normalized = val.replace(/[\s\-()]/g, ''); // Remove spaces, hyphens, and parentheses
return /^(?:\+357|00357)?9\d{7}$/.test(normalized); // Match Cypriot mobile numbers
},
iban: (val) => {
const cleanedIBAN = val.replace(/[\s-]/g, '').toUpperCase(); // Remove spaces/hyphens and convert to uppercase
const regex = /^[A-Z]{2}\d{2}[A-Z0-9]{11,30}$/;
// Validate structure and checksum
return regex.test(cleanedIBAN) && validateIBANChecksum(cleanedIBAN);
},
email: (val) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(val),
date: (val) => !isNaN(Date.parse(val)),
dateISO: (val) => {
if (!/^\d{4}-\d{1,2}-\d{1,2}$/.test(val)) return false; // Basic format check
const [year, month, day] = val.split("-").map(Number);
const date = new Date(year, month - 1, day); // JavaScript months are 0-based
return (
date.getFullYear() === year &&
date.getMonth() === month - 1 &&
date.getDate() === day
);
},
dateDMY: (val) => {
if (!/^\d{1,2}\/\d{1,2}\/\d{4}$/.test(val)) return false; // First check format
const [day, month, year] = val.split('/').map(Number); // Convert to numbers
const date = new Date(year, month - 1, day); // Month is zero-based in JS
// Validate actual date parts
return (
date.getFullYear() === year &&
date.getMonth() === month - 1 &&
date.getDate() === day
);
},
// Other validation rules
length: (val, length) => val.length <= length,
required: (val) => !(val === null || val === undefined || (typeof val === 'string' && val.trim() === "")),
regCheck: (val, regex) => new RegExp(regex).test(val),
//Min and Max
minValue: (val, min) => {
const normalizedVal = normalizeNumber(val); // Normalize the input
if (isNaN(normalizedVal)) {
return false; // Return false if val cannot be converted to a number
}
return normalizedVal >= min;
},
maxValue: (val, max) => {
const normalizedVal = normalizeNumber(val); // Normalize the input
if (isNaN(normalizedVal)) {
return false; // Return false if val cannot be converted to a number
}
return normalizedVal <= max;
},
minValueDate: (val, minDate) => {
const valueDate = parseDate(val); // Parse the input date
const min = parseDate(minDate); // Parse the minimum date
if (isNaN(valueDate) || isNaN(min)) {
return false; // Return false if either date is invalid
}
return valueDate >= min;
},
maxValueDate: (val, maxDate) => {
const valueDate = parseDate(val); // Parse the input date
const max = parseDate(maxDate); // Parse the maximum date
if (isNaN(valueDate) || isNaN(max)) {
return false; // Return false if either date is invalid
}
return valueDate <= max;
},
minLength: (val, min) => val.length >= min
};
for (const rule of rules) {
// Extract rule parameters
const { check, params } = rule;
// Extract rule parameters
const { checkValue, message } = params;
// Handle "required" rules (check if value is not empty, null, or undefined)
if (check === "required") {
const isValid = validationRules.required(value);
if (!isValid) {
return message;
}
}
// Check for "valid" rules (e.g., numeric, telCY, etc.)
if (check === "valid" && validationRules[checkValue]) {
const isValid = validationRules[checkValue](value);
if (!isValid) {
return message;
}
}
// Check for "length" rules (e.g., max length check)
if (check === "length") {
const isValid = validationRules.length(value, checkValue);
if (!isValid) {
return message;
}
}
// Check for "regCheck" rules (custom regex checks)
if (check === "regCheck") {
const isValid = validationRules.regCheck(value, checkValue);
if (!isValid) {
return message;
}
}
// Check for "minValue"
if (check === 'minValue' && !validationRules.minValue(value, checkValue)) {
return message;
}
// Check for "maxValue"
if (check === 'maxValue' && !validationRules.maxValue(value, checkValue)) {
return message;
}
// Check for "minValueDate"
if (check === 'minValueDate' && !validationRules.minValueDate(value, checkValue)) {
return message;
}
// Check for "maxValueDate"
if (check === 'maxValueDate' && !validationRules.maxValueDate(value, checkValue)) {
return message;
}
// Check for "minLength"
if (check === 'minLength' && !validationRules.minLength(value, checkValue)) {
return message;
}
}
return null;
}
// Helper function to validate IBAN
function validateIBANChecksum(iban) {
// Move the first four characters to the end
const rearranged = iban.slice(4) + iban.slice(0, 4);
// Replace letters with numbers (A=10, B=11, ..., Z=35)
const numericIBAN = rearranged.replace(/[A-Z]/g, (char) => char.charCodeAt(0) - 55);
// Perform modulo 97 operation
let remainder = numericIBAN;
while (remainder.length > 2) {
const chunk = remainder.slice(0, 9); // Process in chunks of up to 9 digits
remainder = (parseInt(chunk, 10) % 97) + remainder.slice(chunk.length);
}
return parseInt(remainder, 10) % 97 === 1;
}
// Helper function to normalize numbers
function normalizeNumber(value) {
if (typeof value !== 'string') {
return NaN; // Ensure the input is a string
}
// Remove thousands separators (.)
const withoutThousandsSeparator = value.replace(/\./g, '');
// Replace the decimal separator (,) with a dot (.)
const normalizedValue = withoutThousandsSeparator.replace(',', '.');
return parseFloat(normalizedValue); // Convert to a number
}
function parseDate(value) {
// Check for ISO format (yyyy-mm-dd)
if (/^\d{4}-\d{1,2}-\d{1,2}$/.test(value)) {
const [year, month, day] = value.split('-').map(Number);
const parsedDate = new Date(year, month - 1, day); // JavaScript months are 0-based
if (
parsedDate.getFullYear() === year &&
parsedDate.getMonth() === month - 1 &&
parsedDate.getDate() === day
) {
return parsedDate;
}
}
// Check for DMY format (d/m/yyyy)
if (/^\d{1,2}\/\d{1,2}\/\d{4}$/.test(value)) {
const [day, month, year] = value.split('/').map(Number);
const parsedDate = new Date(year, month - 1, day); // JavaScript months are 0-based
if (
parsedDate.getFullYear() === year &&
parsedDate.getMonth() === month - 1 &&
parsedDate.getDate() === day
) {
return parsedDate;
}
}
return NaN; // Return NaN if the format is invalid
}
/**
* 🔹 Recursive function to validate form fields, including conditionally displayed fields.
* @param {Array} elements - The form elements (including conditional ones)
* @param {Object} formData - The submitted form data
* @param {string} pageUrl - Use this when linking error summary with the page instead of the element
* @returns {Object} validationErrors - The object containing validation errors
*/
export function validateFormElements(elements, formData, pageUrl) {
const validationErrors = {};
elements.forEach(field => {
const inputElements = ALLOWED_FORM_ELEMENTS;
//only validate input elements
if (inputElements.includes(field.element)) {
const fieldValue = (field.element === "dateInput")
? [formData[`${field.params.name}_year`],
formData[`${field.params.name}_month`],
formData[`${field.params.name}_day`]]
.filter(Boolean) // Remove empty values
.join("-") // Join remaining parts
: formData[field.params.name] || ""; // Get submitted value
//Autocheck: check for "checkboxes", "radios", "select" if `fieldValue` is one of the `field.params.items
if (["checkboxes", "radios", "select"].includes(field.element) && fieldValue !== "") {
const valuesToCheck = Array.isArray(fieldValue) ? fieldValue : [fieldValue]; // Ensure it's always an array
const isMatch = valuesToCheck.every(value =>
field.params.items.some(item => item.value === value)
);
if (!isMatch) {
validationErrors[(pageUrl ? pageUrl : "") + field.params.name] = {
id: field.params.id,
message: govcyResources.staticResources.text.valueNotOnList,
pageUrl: pageUrl || "",
};
}
}
if (field.validations) {
// 🔍 Validate the field using all its validation rules
const errorMessage = validateValue(fieldValue, field.validations);
if (errorMessage) {
if (!validationErrors[field.params.name]) {
validationErrors[(pageUrl ? pageUrl : "") + field.params.name] = {};
}
validationErrors[(pageUrl ? pageUrl : "") + field.params.name] = {
id: field.params.id,
message: errorMessage,
pageUrl: pageUrl || "",
};
}
}
// 🔹 Handle conditional fields (only validate them if the parent condition is met)
// Handle conditional elements inside radios
if (field.element === "radios" && field.params.items) {
field.params.items.forEach(item => {
if (item.conditionalElements && fieldValue === item.value) {
if (Array.isArray(item.conditionalElements)) {
item.conditionalElements.forEach(conditionalElement => {
const conditionalFieldValue = (conditionalElement.element === "dateInput")
? [formData[`${conditionalElement.params.name}_year`],
formData[`${conditionalElement.params.name}_month`],
formData[`${conditionalElement.params.name}_day`]]
.filter(Boolean) // Remove empty values
.join("-") // Join remaining parts
: formData[conditionalElement.params.name] || ""; // Get submitted value
//Autocheck: check for "checkboxes", "radios", "select" if `fieldValue` is one of the `field.params.items`
if (["checkboxes", "radios", "select"].includes(conditionalElement.element) && conditionalFieldValue !== "") {
const valuesToCheck = Array.isArray(conditionalFieldValue) ? conditionalFieldValue : [conditionalFieldValue]; // Ensure it's always an array
const isMatch = valuesToCheck.every(value =>
conditionalElement.params.items.some(item => item.value === value)
);
if (!isMatch) {
validationErrors[(pageUrl ? pageUrl : "") + conditionalElement.params.name] = {
id: conditionalElement.params.id,
message: govcyResources.staticResources.text.valueNotOnList,
pageUrl: pageUrl || "",
};
}
}
//if conditional element has validations
if (conditionalElement.validations) {
const errorMessage = validateValue(conditionalFieldValue, conditionalElement.validations);
if (errorMessage) {
if (!validationErrors[conditionalElement.params.name]) {
validationErrors[(pageUrl ? pageUrl : "") + conditionalElement.params.name] = {};
}
validationErrors[(pageUrl ? pageUrl : "") + conditionalElement.params.name] = {
id: conditionalElement.params.id,
message: errorMessage,
pageUrl: pageUrl || "",
};
}
}
});
}
}
});
}
}
});
return validationErrors;
}