docudb
Version:
Document-based NoSQL database for NodeJS
201 lines • 9.11 kB
JavaScript
/**
* Schema module for data validation
* Allows defining structure and validating documents
*/
import { MCO_ERROR, DocuDBError } from '../errors/errors.js';
class Schema {
/** Schema definition */
definition;
/** Schema options */
options;
/**
* Creates a new schema for document validation
* @param definition - Schema definition with field types and validation rules
* @param options - Additional schema options
*/
constructor(definition, options = { idType: 'mongo' }) {
this.definition = definition;
this.options = {
strict: options.strict !== false,
timestamps: options.timestamps === true,
...options
};
}
/**
* Validates a document against the schema
* @param document - Document to validate
* @returns Validated and normalized document
* @throws {DocuDBError} - If the document does not comply with the schema
*/
validate(document) {
if (typeof document !== 'object') {
throw new DocuDBError('The document must be an object', MCO_ERROR.SCHEMA.INVALID_DOCUMENT);
}
const validatedDoc = {};
// Validate each field according to the schema definition
for (const [field, fieldDef] of Object.entries(this.definition)) {
const value = document[field];
// Check if the field is required
if (fieldDef.required === true && (value === undefined || value === null)) {
throw new DocuDBError(`The '${field}' field is required`, MCO_ERROR.SCHEMA.REQUIRED_FIELD, { field });
}
// If the value is not defined and not required, use default value or skip
if (value === undefined || value === null) {
if ('default' in fieldDef) {
// Support for custom functions as default values
if (typeof fieldDef.default === 'function') {
// Pass the current document and field name to the function
validatedDoc[field] = fieldDef.default(document, field);
}
else {
validatedDoc[field] = fieldDef.default;
}
}
continue;
}
// Validate type
if (!this._validateType(value, fieldDef.type)) {
throw new DocuDBError(`The '${field}' field must be of type ${fieldDef.type}`, MCO_ERROR.SCHEMA.INVALID_TYPE, { field, type: fieldDef.type, value });
}
// Validate additional rules
if (fieldDef.validate != null) {
try {
this._runValidators(value, fieldDef.validate, field, document);
}
catch (error) {
throw new DocuDBError(error.message, error.code ?? MCO_ERROR.SCHEMA.VALIDATION_ERROR, { field, ...error.details });
}
}
// Apply transformations if they exist
if (fieldDef.transform != null &&
typeof fieldDef.transform === 'function') {
validatedDoc[field] = fieldDef.transform(value);
}
else {
validatedDoc[field] = value;
}
}
// In strict mode, verify there are no additional fields
if (this.options?.strict === true) {
for (const field in document) {
if (!(field in this.definition) && !field.startsWith('_')) {
throw new DocuDBError(`Field not allowed: '${field}'`, MCO_ERROR.SCHEMA.INVALID_FIELD, { field });
}
}
}
// Add additional fields if not in strict mode
if (this.options?.strict !== true) {
for (const field in document) {
if (!(field in this.definition) && !field.startsWith('_')) {
validatedDoc[field] = document[field];
}
}
}
// Add timestamps if enabled
if (this.options?.timestamps === true) {
const now = new Date();
if (document?._createdAt === undefined) {
validatedDoc._createdAt = now;
}
else {
validatedDoc._createdAt = document._createdAt;
}
validatedDoc._updatedAt = now;
}
return validatedDoc;
}
/**
* Validates the type of a value
* @param {*} value - Value to validate
* @param {string|Function} type - Expected type
* @returns {boolean} - Indicates if the value is of the expected type
* @private
*/
_validateType(value, type) {
if (typeof type === 'function') {
return value instanceof type;
}
switch (type) {
case 'string':
return typeof value === 'string';
case 'number':
return typeof value === 'number' && !isNaN(value);
case 'boolean':
return typeof value === 'boolean';
case 'date':
return value instanceof Date;
case 'array':
return Array.isArray(value);
case 'object':
return (typeof value === 'object' && value !== null && !Array.isArray(value));
default:
return true; // Unknown type, assume valid
}
}
/**
* Executes custom validators
* @param {*} value - Value to validate
* @param {Function|Array|Object} validators - Validators to execute
* @param {string} field - Field name being validated
* @param {DocumentStructure} document - The complete document being validated
* @returns {void} - Throws an error if validation fails
* @private
*/
_runValidators(value, validators, field, document) {
// Validate min/max for numbers
if (typeof value === 'number') {
if ((validators.min !== undefined) && value < validators.min) {
throw new DocuDBError(validators.message ?? `The value must be greater than or equal to ${validators.min}`, MCO_ERROR.SCHEMA.INVALID_VALUE, { field, value, min: validators.min });
}
if ((validators.max !== undefined) && value > validators.max) {
throw new DocuDBError(validators.message ?? `The value must be less than or equal to ${validators.max}`, MCO_ERROR.SCHEMA.INVALID_VALUE, { field, value, max: validators.max });
}
}
// Validate minLength/maxLength for strings and arrays
if (typeof value === 'string' || Array.isArray(value)) {
if ((validators.minLength !== undefined) && value.length < validators.minLength) {
throw new DocuDBError(validators.message ?? `The length must be greater than or equal to ${validators.minLength}`, MCO_ERROR.SCHEMA.INVALID_LENGTH, {
field,
value,
minLength: validators.minLength,
currentLength: value.length
});
}
if ((validators.maxLength !== undefined) && value.length > validators.maxLength) {
throw new DocuDBError(validators.message ?? `The length must be less than or equal to ${validators.maxLength}`, MCO_ERROR.SCHEMA.INVALID_LENGTH, {
field,
value,
maxLength: validators.maxLength,
currentLength: value.length
});
}
}
// Validate pattern for strings
if (typeof value === 'string' && validators.pattern != null) {
const pattern = validators.pattern instanceof RegExp
? validators.pattern
: new RegExp(validators.pattern);
if (!pattern.test(value)) {
throw new DocuDBError(validators.message ?? 'Does not match the required pattern', MCO_ERROR.SCHEMA.INVALID_REGEX, { field, value, pattern: pattern.toString() });
}
}
// Validate enum
if (validators.enum != null && !validators.enum.includes(value)) {
throw new DocuDBError(validators.message ?? `The value must be one of: ${validators.enum.join(', ')}`, MCO_ERROR.SCHEMA.INVALID_ENUM, { field, value, allowedValues: validators.enum });
}
// Run custom validator if provided
if (validators.custom != null) {
const result = validators.custom(value, document);
// If result is a string, it's an error message
if (typeof result === 'string') {
throw new DocuDBError(result, MCO_ERROR.SCHEMA.CUSTOM_VALIDATION_ERROR, { field, value });
}
// If result is false, validation failed
if (result === false) {
throw new DocuDBError(validators.message ?? 'Failed custom validation', MCO_ERROR.SCHEMA.CUSTOM_VALIDATION_ERROR, { field, value });
}
}
}
}
export default Schema;
//# sourceMappingURL=schema.js.map