nox-validation
Version:
validate dynamic schema
1,053 lines (985 loc) • 31.6 kB
JavaScript
const constants = require("./constant");
const {
findDisallowedKeys,
formatLabel,
getLastChildKey,
buildNestedStructure,
getValue,
setValue,
isEmpty,
getParentKey,
extractRelationalParents,
} = require("./helpers");
const choices = ["radio", "checkboxes", "dropdown_multiple", "dropdown"];
const typeChecks = {
date: (val, data) => {
if (val instanceof Date && !isNaN(val)) return true;
if (typeof val === "string" && !isNaN(Date.parse(val))) {
if (data && data?.key && data?.updateValue) {
data.updateValue(data.key, new Date(val));
}
return true;
}
return false;
},
[constants.types.DATE]: (val, data) => {
if (val instanceof Date && !isNaN(val)) return true;
if (typeof val === "string" && !isNaN(Date.parse(val))) {
if (data && data?.key && data?.updateValue) {
data.updateValue(data.key, new Date(val));
}
return true;
}
return false;
},
[constants.types.BOOLEAN]: (val, data) => {
if (typeof val === "boolean") return true;
if (typeof val === "string") {
const lowerVal = val.toLowerCase();
if (lowerVal === "true" || lowerVal === "false") {
if (data && data?.key && data?.updateValue) {
data.updateValue(data.key, lowerVal === "true");
}
return true;
}
}
return false;
},
[constants.types.DATE_TIME]: (val, data) => {
if (val instanceof Date && !isNaN(val)) return true;
if (typeof val === "string" && !isNaN(Date.parse(val))) {
if (data && data?.key && data?.updateValue) {
data.updateValue(data.key, new Date(val));
}
return true;
}
return false;
},
[constants.types.NUMBER]: (val, data) => {
if (typeof val === "number" && !isNaN(val)) return true;
if (typeof val === "string" && !isNaN(parseFloat(val))) {
if (data && data?.key && data?.updateValue) {
data.updateValue(data.key, parseFloat(val));
}
return true;
}
return false;
},
[constants.types.TIME]: (val) =>
typeof val === "string" && /^([01]\d|2[0-3]):([0-5]\d):([0-5]\d)$/.test(val),
[constants.types.STRING]: (val) => typeof val === "string",
[constants.types.OBJECT]: (val) => typeof val === "object" && val !== null && !Array.isArray(val),
[constants.types.ARRAY]: (val) => Array.isArray(val),
[constants.types.OBJECT_ID]: (val) => typeof val === "string" && /^[0-9a-fA-F]{24}$/.test(val),
[constants.types.MIXED]: (val) => (val ? true : false),
[constants.types.BUFFER]: (val) => val instanceof Buffer,
[constants.types.ALIAS]: (val) => (val ? true : false),
};
const handleMinMaxValidation = (
fieldValue,
ruleValue,
ruleType,
field,
addError,
currentPath,
error_messages,
custom_message
) => {
let message = "";
const fieldLabel = formatLabel(field.display_label);
if (ruleType === constants.rulesTypes.MIN) {
if (
field.type === constants.types.STRING &&
typeChecks[constants.types.STRING](fieldValue) &&
fieldValue?.length < ruleValue[0]
) {
message = custom_message ?? error_messages.MIN_STRING;
message = message?.replace(`{field}`, fieldLabel).replace(`{min}`, ruleValue[0]);
} else if (
field.type === constants.types.NUMBER &&
typeChecks[constants.types.NUMBER](fieldValue) &&
fieldValue < ruleValue[0]
) {
message = custom_message ?? error_messages.MIN_NUMBER;
message = message?.replace(`{field}`, fieldLabel).replace(`{min}`, ruleValue[0]);
}
}
if (ruleType === constants.rulesTypes.MAX) {
if (
field.type === constants.types.STRING &&
typeChecks[constants.types.STRING](fieldValue) &&
fieldValue?.length > ruleValue[0]
) {
message = custom_message ?? error_messages.MAX_STRING;
message = message?.replace(`{field}`, fieldLabel).replace(`{max}`, ruleValue[0]);
} else if (
field.type === constants.types.NUMBER &&
typeChecks[constants.types.NUMBER](fieldValue) &&
fieldValue > ruleValue[0]
) {
message = custom_message ?? error_messages.MAX_NUMBER;
message = message?.replace(`{field}`, fieldLabel).replace(`{max}`, ruleValue[0]);
}
}
if (message) {
addError(
currentPath,
{ label: fieldLabel, fieldPath: currentPath, description: "", message },
field
);
}
};
const isEmptyRelational = ({ api_version, value, interface }) => {
if (api_version === constants.API_VERSION.V1 && interface !== constants.interfaces.TRANSLATIONS) {
if (interface === constants.interfaces.MANY_TO_ANY) {
return (
value &&
typeChecks[constants.types.OBJECT](value) &&
value.hasOwnProperty("collection") &&
value.hasOwnProperty("sort") &&
value.hasOwnProperty("item") &&
value.collection !== null &&
value.collection !== "" &&
value.sort !== null &&
value.sort !== "" &&
value.item !== null &&
value.item !== ""
);
} else {
return value?.length > 0;
}
} else {
return value?.create?.length > 0 || value?.existing?.length > 0;
}
return false;
};
const validateMetaRules = (
field,
addError,
providedValue,
currentPath,
updateValue,
error_messages,
onlyFormFields,
apiVersion,
fieldOptions,
formData
) => {
const fieldValue = providedValue;
const { required = false, nullable = false, options } = field?.meta ?? {};
const isRelational = [
constants.interfaces.FILES,
constants.interfaces.FILE,
constants.interfaces.FILE_IMAGE,
constants.interfaces.MANY_TO_MANY,
constants.interfaces.ONE_TO_MANY,
constants.interfaces.MANY_TO_ONE,
constants.interfaces.MANY_TO_ANY,
constants.interfaces.TRANSLATIONS,
].includes(field?.meta?.interface);
if (
choices.includes(field?.meta?.interface) &&
(!options?.choices || !options?.choices?.length > 0)
) {
const message = error_messages.ADD_CHOICE.replace(`{field}`, formatLabel(field.display_label));
addError(
currentPath,
{
label: formatLabel(field.display_label),
fieldPath: currentPath,
description: "",
message,
},
field
);
}
const isValidRelational =
required && isRelational && !onlyFormFields
? isEmptyRelational({
api_version: apiVersion,
value: fieldValue,
interface: field?.meta?.interface,
})
: true;
if ((required && isEmpty(fieldValue)) || !isValidRelational) {
const message = error_messages.REQUIRED.replace(`{field}`, formatLabel(field.display_label));
addError(
currentPath,
{
label: formatLabel(field.display_label),
fieldPath: currentPath,
description: "",
message,
},
field
);
}
if (!nullable && fieldValue === null) {
const message = error_messages.NOT_NULLABLE.replace(
`{field}`,
formatLabel(field.display_label)
);
addError(
currentPath,
{
label: formatLabel(field.display_label),
fieldPath: currentPath,
description: "",
message,
},
field
);
}
if (
!isEmpty(fieldValue) &&
typeChecks[field.type] &&
!typeChecks[field.type](fieldValue, { key: currentPath, updateValue })
) {
const message = error_messages.INVALID_TYPE.replace(
`{field}`,
formatLabel(field.display_label)
).replace(`{type}`, field?.type);
addError(
currentPath,
{
label: formatLabel(field.display_label),
fieldPath: currentPath,
description: "",
message,
},
field
);
}
};
const handleRegexValidation = (
fieldValue,
rule,
field,
addError,
currentPath,
error_messages,
custom_message
) => {
let message = "";
const fieldLabel = formatLabel(field.display_label);
const flags = `${rule.options.case_sensitive ? "" : "i"}${rule.options.multiline ? "m" : ""}${
rule.options.global ? "g" : ""
}`;
const regex = new RegExp(rule.value[0], flags);
const isValid = (() => {
switch (rule.options.type) {
case constants.regexTypes.MATCH:
message = custom_message ?? error_messages.REGEX_MATCH;
return regex.test(fieldValue);
case constants.regexTypes.START_WITH:
message = custom_message ?? error_messages.REGEX_START_WITH;
return fieldValue?.startsWith(rule.value[0].replace(/^\//, "").replace(/\/$/, ""));
case constants.regexTypes.ENDS_WITH:
message = custom_message ?? error_messages.REGEX_ENDS_WITH;
return fieldValue?.endsWith(rule.value[0].replace(/^\//, "").replace(/\/$/, ""));
case constants.regexTypes.CONTAINS:
message = custom_message ?? error_messages.REGEX_CONTAINS;
return regex.test(fieldValue);
case constants.regexTypes.EXACT:
message = custom_message ?? error_messages.REGEX_EXACT;
return fieldValue === rule.value[0].replace(/^\//, "").replace(/\/$/, "");
case constants.regexTypes.NOT_START_WITH:
message = custom_message ?? error_messages.REGEX_NOT_START_WITH;
return !fieldValue?.startsWith(rule.value[0].replace(/^\//, "").replace(/\/$/, ""));
case constants.regexTypes.NOT_ENDS_WITH:
message = custom_message ?? error_messages.REGEX_NOT_ENDS_WITH;
return !fieldValue?.endsWith(rule.value[0].replace(/^\//, "").replace(/\/$/, ""));
case constants.regexTypes.NOT_CONTAINS:
message = custom_message ?? error_messages.REGEX_NOT_CONTAINS;
return !regex.test(fieldValue);
default:
return false;
}
})();
if (!isValid) {
message = message?.replace(`{field}`, fieldLabel)?.replace(`{value}`, rule.value[0]);
addError(
currentPath,
{ label: fieldLabel, fieldPath: currentPath, description: "", message },
field
);
}
};
const validateOperatorRule = (
fieldValue,
rule,
field,
addError,
currentPath,
formData,
error_messages,
custom_message
) => {
const { isFieldCompare = false } = rule?.options ?? {};
let valid = false;
const isNumber = field.type === constants.types.NUMBER;
const isDate = field.type === constants.types.DATE || field.type === "date";
const isDateTime = field.type === constants.types.DATE_TIME;
const isTime = field.type === constants.types.TIME;
const isArray = field.type === constants.types.ARRAY;
const parseTime = (timeStr) => {
if (!timeStr || typeof timeStr !== "string") return null;
const [hh, mm, ss] = timeStr.split(":").map(Number);
return hh * 3600 + mm * 60 + ss;
};
const getComparableValue = (value, forMessage = false) => {
if (isNumber) return Number(value);
if (isDate) {
const date = new Date(value);
date.setHours(0, 0, 0, 0);
if (forMessage) {
return `${date.getUTCFullYear()}-${String(date.getUTCMonth() + 1).padStart(
2,
"0"
)}-${String(date.getUTCDate()).padStart(2, "0")}`;
}
return Math.floor(date.getTime() / 1000);
}
if (isDateTime) {
const date = new Date(value);
if (forMessage) {
return (
`${date.getUTCFullYear()}-${String(date.getUTCMonth() + 1).padStart(2, "0")}-${String(
date.getUTCDate()
).padStart(2, "0")} ` +
`${String(date.getUTCHours()).padStart(2, "0")}:${String(date.getUTCMinutes()).padStart(
2,
"0"
)}:${String(date.getUTCSeconds()).padStart(2, "0")}`
);
}
return Math.floor(date.getTime() / 1000);
}
if (isTime) {
if (forMessage) {
return value;
}
return parseTime(value);
}
return value;
};
rule.value = rule.value?.map((key) => {
const value = isFieldCompare
? getComparableValue(getValue(formData, key), true)
: getComparableValue(key, true);
return value ? value : key;
});
const fieldValueParsed = getComparableValue(fieldValue);
const messageValue = Array.isArray(rule.value) ? rule.value.join(", ") : String(rule.value);
let message = "";
switch (rule.options.operator) {
case constants.operatorTypes.AND:
message = custom_message ?? error_messages.AND;
message = message
?.replace(`{field}`, formatLabel(field.display_label))
?.replace("{value}", messageValue);
valid = isArray
? rule.value.every((val) => fieldValue.includes(val))
: rule.value.every((val) => val === fieldValueParsed);
break;
case constants.operatorTypes.OR:
message = custom_message ?? error_messages.OR;
message = message
?.replace(`{field}`, formatLabel(field.display_label))
?.replace("{value}", messageValue);
valid = isArray
? rule.value.some((val) => fieldValue.includes(val))
: rule.value.some((val) => val === fieldValueParsed);
break;
case constants.operatorTypes.EQUAL:
message = custom_message ?? error_messages.EQUAL;
message = message
?.replace(`{field}`, formatLabel(field.display_label))
?.replace("{value}", rule.value[0]);
valid = fieldValueParsed === getComparableValue(rule.value[0]);
break;
case constants.operatorTypes.NOT_EQUAL:
message = custom_message ?? error_messages.NOT_EQUAL;
message = message
?.replace(`{field}`, formatLabel(field.display_label))
?.replace("{value}", rule.value[0]);
valid = fieldValueParsed !== getComparableValue(rule.value[0]);
break;
case constants.operatorTypes.LESS_THAN:
message = custom_message ?? error_messages.LESS_THAN;
message = message
?.replace(`{field}`, formatLabel(field.display_label))
?.replace("{value}", rule.value[0]);
valid = fieldValueParsed < getComparableValue(rule.value[0]);
break;
case constants.operatorTypes.LESS_THAN_EQUAL:
message = custom_message ?? error_messages.LESS_THAN_EQUAL;
message = message
?.replace(`{field}`, formatLabel(field.display_label))
?.replace("{value}", rule.value[0]);
valid = fieldValueParsed <= getComparableValue(rule.value[0]);
break;
case constants.operatorTypes.GREATER_THAN:
message = custom_message ?? error_messages.GREATER_THAN;
message = message
?.replace(`{field}`, formatLabel(field.display_label))
?.replace("{value}", rule.value[0]);
valid = fieldValueParsed > getComparableValue(rule.value[0]);
break;
case constants.operatorTypes.GREATER_THAN_EQUAL:
message = custom_message ?? error_messages.GREATER_THAN_EQUAL;
message = message
?.replace(`{field}`, formatLabel(field.display_label))
?.replace("{value}", rule.value[0]);
valid = fieldValueParsed >= getComparableValue(rule.value[0]);
break;
case constants.operatorTypes.IN:
message = custom_message ?? error_messages.IN;
message = message
?.replace(`{field}`, formatLabel(field.display_label))
?.replace("{value}", messageValue);
valid = rule.value.includes(fieldValue);
break;
case constants.operatorTypes.NOT_IN:
message = custom_message ?? error_messages.NOT_IN;
message = message
?.replace(`{field}`, formatLabel(field.display_label))
?.replace("{value}", messageValue);
valid = !rule.value.includes(fieldValue);
break;
case constants.operatorTypes.EXISTS:
message = custom_message ?? error_messages.EXISTS;
message = message?.replace(`{field}`, formatLabel(field.display_label));
valid = rule.value[0] ? fieldValue !== undefined : fieldValue === undefined;
break;
case constants.operatorTypes.TYPE:
message = custom_message ?? error_messages.TYPE;
message = message
?.replace(`{field}`, formatLabel(field.display_label))
?.replace("{value}", rule.value[0]);
valid = typeChecks[rule.value[0]](fieldValue);
break;
case constants.operatorTypes.MOD:
message = custom_message ?? error_messages.MOD;
message = message
?.replace(`{field}`, formatLabel(field.display_label))
?.replace("{value[1]}", rule.value[1])
?.replace("{value[0]}", rule.value[0]);
valid = isNumber && fieldValue % rule.value[0] === rule.value[1];
break;
case constants.operatorTypes.ALL:
message = custom_message ?? error_messages.ALL;
message = message
?.replace(`{field}`, formatLabel(field.display_label))
?.replace("{value}", messageValue);
valid = isArray && rule.value.every((val) => fieldValue.includes(val));
break;
case constants.operatorTypes.SIZE:
message = custom_message ?? error_messages.SIZE;
message = message
?.replace(`{field}`, formatLabel(field.display_label))
?.replace("{value}", rule.value[0]);
valid = isArray && fieldValue.length === rule.value[0];
break;
default:
console.log(`Unknown Operator : ${rule.options.operator}`);
break;
}
if (!valid) {
addError(
currentPath,
{
label: formatLabel(field.display_label),
fieldPath: currentPath,
description: "",
message,
},
field
);
}
};
const generateErrorMessage = (
ruleKey,
fieldLabel,
additionalValues = {},
error_messages,
custom_message
) => {
let message = custom_message ?? error_messages[ruleKey];
message = message?.replace(`{field}`, fieldLabel);
Object.entries(additionalValues).forEach(([key, value]) => {
message = message.replace(`{${key}}`, value);
});
return message;
};
const handleRule = (rule, field, value, addError, currentPath, formData, error_messages) => {
const ruleValue = rule?.value;
const messageValue = Array.isArray(ruleValue) ? ruleValue.join(", ") : String(ruleValue);
const fieldLabel = formatLabel(field.display_label);
const custom_message = rule?.custom_message;
const addValidationError = (ruleKey, extraValues = {}, custom_message) =>
addError(
currentPath,
{
label: fieldLabel,
fieldPath: currentPath,
description: "",
message: generateErrorMessage(
ruleKey,
fieldLabel,
extraValues,
error_messages,
custom_message
),
},
field
);
switch (rule.case) {
case constants.rulesTypes.EMPTY:
if (value) addValidationError("EMPTY", {}, custom_message);
break;
case constants.rulesTypes.NOT_EMPTY:
if (!value || value.length === 0) addValidationError("NOT_EMPTY", {}, custom_message);
break;
case constants.rulesTypes.ONE_OF:
if (!ruleValue?.includes(value))
addValidationError("ONE_OF", { value: messageValue }, custom_message);
break;
case constants.rulesTypes.NOT_ONE_OF:
if (ruleValue?.includes(value))
addValidationError("NOT_ONE_OF", { value: messageValue }, custom_message);
break;
case constants.rulesTypes.NOT_ALLOWED:
if (ruleValue?.includes(value)) addValidationError("NOT_ALLOWED", {}, custom_message);
break;
case constants.rulesTypes.MIN:
case constants.rulesTypes.MAX:
handleMinMaxValidation(
value,
ruleValue,
rule.case,
field,
addError,
currentPath,
error_messages,
custom_message
);
break;
case constants.rulesTypes.REGEX:
handleRegexValidation(
value,
rule,
field,
addError,
currentPath,
error_messages,
custom_message
);
break;
case constants.rulesTypes.OPERATOR:
validateOperatorRule(
value,
rule,
field,
addError,
currentPath,
formData,
error_messages,
custom_message
);
break;
default:
console.warn(`Unknown Rule: ${rule.case}`, fieldLabel);
}
};
const validateField = (
field,
value,
fieldPath = "",
addError,
formData,
updateValue,
error_messages,
onlyFormFields,
apiVersion,
fieldOptions
) => {
if (onlyFormFields == true && (value === undefined || value === null)) return;
const currentPath = fieldPath ? `${fieldPath}.${field.key.split(".").pop()}` : field.key;
const fieldLabel = formatLabel(field.display_label);
validateMetaRules(
field,
addError,
value,
currentPath,
updateValue,
error_messages,
onlyFormFields,
apiVersion,
fieldOptions,
formData
);
if (
field.type === constants.types.OBJECT &&
field.children &&
value &&
typeChecks[constants.types.OBJECT](value)
) {
const isManyToAnyItem =
field.display_label === "item" && field?.meta?.interface === constants.interfaces.MANY_TO_ANY;
const defaultKeys = new Set([
"_id",
"created_at",
"updated_at",
"__v",
"created_by",
"updated_by",
"nox_created_by",
"nox_updated_by",
"nox_created_at",
"nox_updated_at",
]);
let itemSchemaId = null;
if (isManyToAnyItem) {
const itemKey = Object.keys(value).find((key) => !defaultKeys.has(key));
if (itemKey) {
const childField = field.children.find((child) => child.display_label === itemKey);
itemSchemaId = childField?.schema_id ?? null;
}
}
const childrenToValidate = itemSchemaId
? field.children.filter((child) => child.schema_id === itemSchemaId)
: field.children;
childrenToValidate.forEach((child) =>
validateField(
child,
value[child.key.split(".").pop()],
currentPath,
addError,
formData,
updateValue,
error_messages,
onlyFormFields,
apiVersion,
fieldOptions
)
);
} else if (field.type === constants.types.ARRAY && Array.isArray(value)) {
const itemType = field?.array_type || field?.schema_definition?.type;
if (itemType) {
value.forEach((item, index) => {
const itemPath = `${currentPath}[${index}]`;
if (choices.includes(field?.meta?.interface) && !isEmpty(item)) {
applyValidations(field, item, addError, itemPath, formData, error_messages);
}
if (!typeChecks[itemType](item)) {
addError(itemPath, {
label: fieldLabel,
fieldPath: `${currentPath}[${index}]`,
description: "",
message: generateErrorMessage(
"INVALID_TYPE",
fieldLabel,
{
type: itemType,
},
error_messages
),
});
}
});
}
if (field.children?.length > 0) {
value.forEach((item, index) => {
field.children.forEach((child) =>
validateField(
child,
item[child.key.split(".").pop()],
`${currentPath}[${index}]`,
addError,
formData,
updateValue,
error_messages,
onlyFormFields,
apiVersion,
fieldOptions
)
);
});
}
} else {
if (!applyValidations(field, value, addError, currentPath, formData, error_messages)) {
addError(
currentPath,
{
label: fieldLabel,
fieldPath: currentPath,
description: "",
message: error_messages.INVALID_VALUE?.replace(`{field}`, fieldLabel),
},
field
);
}
}
};
const applyValidations = (field, value, addError, currentPath, formData, error_messages) => {
if (!field.validations || value === null || value === undefined || value === "") return true;
return !field.validations.some(
(rule) =>
handleRule(rule, field, value, addError, currentPath, formData, error_messages) === false
);
};
const schema = {
formData: { type: constants.types.OBJECT, array_type: null },
formId: { type: constants.types.OBJECT_ID, array_type: null },
isSeparatedFields: { type: constants.types.BOOLEAN, array_type: null },
relations: {
type: constants.types.ARRAY,
array_type: constants.types.OBJECT,
},
fields: { type: constants.types.ARRAY, array_type: constants.types.OBJECT },
relationalFields: { type: constants.types.OBJECT, array_type: null },
abortEarly: { type: constants.types.BOOLEAN, array_type: null },
byPassKeys: {
type: constants.types.ARRAY,
array_type: constants.types.STRING,
},
apiVersion: { type: constants.types.STRING, array_type: null },
language: { type: constants.types.STRING, array_type: null },
maxLevel: { type: constants.types.NUMBER, array_type: null },
onlyFormFields: { type: constants.types.BOOLEAN, array_type: null },
};
const validate = (data) => {
if (!data?.language) {
data.language = constants.LANGUAGES.en;
}
if (data.onlyFormFields === undefined) {
data.onlyFormFields = false;
}
let {
formData,
isSeparatedFields,
fields,
relationalFields,
relations,
formId,
abortEarly,
byPassKeys,
apiVersion,
language,
maxLevel,
onlyFormFields,
} = data;
const error_messages =
constants.LOCALE_MESSAGES[language] ?? constants.LOCALE_MESSAGES[constants.LANGUAGES.en];
let result = { status: true, errors: {}, data: structuredClone(formData) };
const updateValue = (key, value) => {
setValue(result.data, key, value);
};
const addError = (fieldPath, obj, field) => {
if (byPassKeys.some((key) => fieldPath.startsWith(key))) return;
if (!result.errors[fieldPath] && !field?.meta?.hidden) {
const pathResult = extractRelationalParents(fieldPath);
if (pathResult) {
const { firstParent, secondParent } = pathResult;
const secondParentField = fields.find((f) => f.path === secondParent);
if (
secondParentField &&
secondParentField?.meta?.interface === constants.interfaces.TRANSLATIONS
) {
const languageKey = secondParentField?.meta?.options?.language_field;
const firstParentValue = getValue(formData, firstParent);
if (firstParentValue && typeChecks[constants.types.OBJECT](firstParentValue)) {
const codeKey = Object.keys(firstParentValue).find((key) => key.includes(languageKey));
const codeValue = codeKey ? firstParentValue[codeKey] : null;
if (codeValue) {
const translation_key = fieldPath.replace(
firstParent,
`${secondParent}.${codeValue}`
);
if (translation_key) {
obj.translation_path = translation_key;
}
}
}
}
}
result.errors[fieldPath] = obj;
result.status = false;
}
if (abortEarly && !result.status) return result;
};
// Validate Parameters Starts
// Validate Data
const defaultField = { meta: { hidden: false } };
if (!data) {
const message = error_messages.REQUIRED.replace(`{field}`, formatLabel("data"));
addError(
"data",
{
label: formatLabel("data"),
fieldPath: "data",
description: "",
message,
},
defaultField
);
return result;
}
// validate data type
if (!typeChecks[constants.types.OBJECT](data)) {
const message = error_messages.INVALID_TYPE.replace(`{field}`, formatLabel("data")).replace(
`{type}`,
constants.types.OBJECT
);
addError(
"data",
{
label: formatLabel("data"),
fieldPath: "data",
description: "",
message,
},
defaultField
);
return result;
}
// Validate Parameters
Object.keys(schema).forEach((key) => {
const expectedType = schema[key].type;
const fieldValue = data[key];
// Skip empty values
if (fieldValue == null || fieldValue == undefined) {
const message = error_messages.REQUIRED.replace(`{field}`, formatLabel(key));
addError(
key,
{
label: formatLabel(key),
fieldPath: key,
description: "",
message,
},
defaultField
);
return;
}
// Validate field type
if (!typeChecks[expectedType] || !typeChecks[expectedType](fieldValue)) {
const message = error_messages.INVALID_TYPE.replace(`{field}`, key).replace(
`{type}`,
expectedType
);
addError(
key,
{
label: formatLabel(key),
fieldPath: key,
description: "",
message,
},
defaultField
);
return;
}
// Check array items if the field is an array
if (expectedType === constants.types.ARRAY && typeChecks[expectedType]) {
fieldValue.forEach((item, index) => {
// Determine the expected type of array items
const arrayItemType = schema[key].array_type; // Define item types like "relations[]": "object"
if (arrayItemType && typeChecks[arrayItemType] && !typeChecks[arrayItemType](item)) {
const message = error_messages.INVALID_TYPE.replace(
`{field}`,
`${key}[${index}]`
).replace(`{type}`, arrayItemType);
addError(
`${key}[${index}]`,
{
label: formatLabel(`${key}[${index}]`),
fieldPath: `${key}[${index}]`,
description: "",
message,
},
defaultField
);
}
});
}
});
// Validate API Version
if (!constants.API_VERSIONS.includes(apiVersion)) {
const message = error_messages.IN.replace(`{field}`, formatLabel("apiVersion")).replace(
`{value}`,
constants.API_VERSIONS.join(", ")
);
addError(
"apiVersion",
{
label: formatLabel("apiVersion"),
fieldPath: "apiVersion",
description: "",
message,
},
defaultField
);
}
if (!result.status) {
return result;
}
// Validate Parameters END
// Get Relational Field
let schemaFields = fields;
let allFields = isSeparatedFields ? [] : fields;
if (!isSeparatedFields) {
schemaFields = fields.filter((field) => field?.schema_id?.toString() === formId?.toString());
}
let currentDepthMap = new Map();
const fieldOptions =
buildNestedStructure({
schemaFields: schemaFields || [],
allFields: allFields,
relations: relations,
relational_fields: relationalFields,
isSeparatedFields,
apiVersion,
maxLevel,
currentDepthMap,
rootPath: "",
isRoot: true,
}) || [];
findDisallowedKeys(formData, fieldOptions, maxLevel).forEach((fieldPath) => {
if (abortEarly && !result.status) return result;
const fieldKey = getLastChildKey(fieldPath);
if (fieldKey && !result.errors[fieldPath]) {
addError(fieldPath, {
label: formatLabel(fieldKey),
fieldPath,
description: "",
message: generateErrorMessage(
"NOT_ALLOWED_FIELD",
formatLabel(fieldKey),
{
field: formatLabel(fieldKey),
},
error_messages
),
});
}
});
fieldOptions.forEach((field) => {
if (abortEarly && !result.status) return result;
validateField(
field,
formData[field.value],
"",
addError,
formData,
updateValue,
error_messages,
onlyFormFields,
apiVersion,
fieldOptions
);
});
return result;
};
module.exports = { validate, validateField, typeChecks };