UNPKG

localgoose

Version:

A lightweight, file-based ODM Database for Node.js, inspired by Mongoose

592 lines (500 loc) 13.8 kB
class SchemaType { // === Core Functionality === constructor(path, options = {}, instance) { this.path = path; this.instance = instance; this.validators = []; this.setters = []; this.getters = []; this.options = options; this._index = null; this.selected = true; this._default = undefined; this._ref = null; this._sparse = false; this._text = false; this._unique = false; this._immutable = false; this._embedded = null; this._enum = null; this._min = null; this._max = null; this._trim = false; this._lowercase = false; this._uppercase = false; this._match = null; this._validate = null; this._isArray = false; this._arrayType = null; if (Array.isArray(instance)) { this._isArray = true; this._arrayType = instance[0]; } if (options.required) { this.required(options.required); } if (options.default != null) { this.default(options.default); } if (options.select != null) { this.select(options.select); } if (options.validate != null) { this.validate(options.validate); } if (options.get) { this.get(options.get); } if (options.set) { this.set(options.set); } if (options.transform) { this.transform(options.transform); } if (options.ref) { this.ref(options.ref); } if (options.immutable) { this.immutable(options.immutable); } if (options.sparse) { this.sparse(options.sparse); } if (options.unique) { this.unique(options.unique); } if (options.text) { this.text(options.text); } if (options.index) { this.index(options.index); } if (options.enum) { this.enum(options.enum); } if (options.min != null) { this.min(options.min); } if (options.max != null) { this.max(options.max); } if (options.trim) { this.trim(options.trim); } if (options.lowercase) { this.lowercase(options.lowercase); } if (options.uppercase) { this.uppercase(options.uppercase); } if (options.match) { this.match(options.match); } } // === Static Methods === static cast(val) { return val; } static checkRequired(val) { return val != null; } static get(fn) { if (!this._getters) this._getters = []; this._getters.push(fn); return this; } static set(fn) { if (!this._setters) this._setters = []; this._setters.push(fn); return this; } // === Type Validation and Casting === cast(val) { if (val == null) { return val; } if (this._isArray) { if (!Array.isArray(val)) { throw new Error(`${this.path} must be an array`); } return val.map(item => this._castArrayItem(item)); } // Type-specific casting switch (this.instance) { case String: val = String(val); if (this._trim) val = val.trim(); if (this._lowercase) val = val.toLowerCase(); if (this._uppercase) val = val.toUpperCase(); break; case Number: val = Number(val); break; case Date: val = val instanceof Date ? val : new Date(val); break; case Boolean: val = Boolean(val); break; case BigInt: try { val = BigInt(val); } catch (e) { throw new Error(`Cast to BigInt failed for value "${val}" at path "${this.path}"`); } break; case Buffer: if (!(val instanceof Buffer)) { if (typeof val === 'string') val = Buffer.from(val, 'utf8'); else if (Array.isArray(val)) val = Buffer.from(val); else throw new Error(`Cast to Buffer failed for value "${val}" at path "${this.path}"`); } break; } // Enum validation if (this._enum && !this._enum.includes(val)) { throw new Error(`${val} is not a valid enum value for ${this.path}`); } // Min/Max validation if (this._min != null && val < this._min) { throw new Error(`${val} is less than the minimum value ${this._min} for ${this.path}`); } if (this._max != null && val > this._max) { throw new Error(`${val} is greater than the maximum value ${this._max} for ${this.path}`); } // Regex matching if (this._match && typeof val === 'string' && !this._match.test(val)) { throw new Error(`${val} does not match the required pattern for ${this.path}`); } // Apply static and instance setters let value = val; if (this.constructor._setters) { for (const setter of this.constructor._setters) { value = setter(value); } } for (const setter of this.setters) { value = setter(value); } return value; } _castArrayItem(item) { if (this._arrayType instanceof SchemaType) { return this._arrayType.cast(item); } return item; } castFunction() { return (val) => this.cast(val); } // === Validation Methods === validate(obj, message) { if (obj == null) { return this; } const validator = this._createValidator(obj, message); // Ensure this is the last validator added const existingValidatorIndex = this.validators.findIndex(v => v.type === validator.type); if (existingValidatorIndex !== -1) { this.validators.splice(existingValidatorIndex, 1); } this.validators.push(validator); return this; } async validateAll() { const results = await Promise.all( this.validators.map(validator => { try { const result = validator.validator(); return Promise.resolve(result); } catch (error) { return Promise.reject(error); } }) ); return results.every(result => result === true); } async doValidate(value, fn, context) { let err = null; const validatorCount = this.validators.length; if (validatorCount === 0) { return fn(null); } let validatorsCompleted = 0; const handleValidationResult = (ok) => { validatorsCompleted++; if (ok === false && !err) { err = new Error(`Validation failed for path \`${this.path}\``); } if (validatorsCompleted === validatorCount) { fn(err); } }; for (const validator of this.validators) { try { const result = validator.validator.call(context, value); if (result && typeof result.then === 'function') { await result.then( ok => handleValidationResult(ok), error => handleValidationResult(false) ); } else { handleValidationResult(result); } } catch (error) { handleValidationResult(false); } } } _createValidator(obj, message) { try { if (typeof obj === 'function') { return { validator: obj, message: message || `Validation failed for path \`${this.path}\``, type: 'user defined' }; } if (obj.validator) { return { validator: obj.validator, message: obj.message || message || `Validation failed for path \`${this.path}\``, type: obj.type || 'user defined' }; } throw new Error('Invalid validator'); } catch (error) { console.error(`Validator creation error for ${this.path}:`, error); throw error; } } // === Field Constraints === enum(values) { if (Array.isArray(values)) { this._enum = values; this.validate({ validator: (val) => values.includes(val), message: `{PATH} must be one of: ${values.join(', ')}` }); } return this; } min(value, message) { this._min = value; this.validate({ validator: (val) => { if (typeof val === 'string' || typeof val === 'number') { return val >= value; } return true; }, message: message || `{PATH} must be at least ${value}` }); return this; } max(value, message) { this._max = value; this.validate({ validator: (val) => { if (typeof val === 'string' || typeof val === 'number') { return val <= value; } return true; }, message: message || `{PATH} must be no more than ${value}` }); return this; } minlength(value, message) { this._minlength = value; this.validate({ validator: (val) => { if (typeof val === 'string') return val.length >= value; return true; }, message: message || `{PATH} must be at least ${value} characters` }); return this; } maxlength(value, message) { this._maxlength = value; this.validate({ validator: (val) => { if (typeof val === 'string') return val.length <= value; return true; }, message: message || `{PATH} must be at most ${value} characters` }); return this; } match(regex, message) { this._match = regex; this.validate({ validator: (val) => { if (typeof val === 'string') { return regex.test(val); } return true; }, message: message || `{PATH} does not match the required pattern` }); return this; } required(required = true, message) { if (arguments.length === 0) { return this.validators.some(v => v.isRequired); } if (required) { const validator = { validator: v => v != null, message: message || `Path \`${this.path}\` is required.`, type: 'required', isRequired: true }; // Remove any existing required validators this.validators = this.validators.filter(v => !v.isRequired); // Add new required validator at the beginning this.validators.unshift(validator); } else { // Remove all required validators this.validators = this.validators.filter(v => !v.isRequired); } return this; } // === String Modifiers === trim(value = true) { this._trim = value; if (value) { this.set(val => typeof val === 'string' ? val.trim() : val); } return this; } lowercase(value = true) { this._lowercase = value; if (value) { this.set(val => typeof val === 'string' ? val.toLowerCase() : val); } return this; } uppercase(value = true) { this._uppercase = value; if (value) { this.set(val => typeof val === 'string' ? val.toUpperCase() : val); } return this; } // === Schema Options === default(val) { if (arguments.length === 0) { return this._default; } if (val === null) { this._default = null; return this; } this._default = val; return this; } sparse(val = true) { this._sparse = val; return this; } unique(val = true) { this._unique = val; return this; } text(val = true) { this._text = val; return this; } index(val) { this._index = val; return this; } immutable(value = true) { this._immutable = value; return this; } ref(ref) { this._ref = ref; return this; } transform(fn) { this._transform = fn; return this; } // === Getters and Setters === get(fn) { this.getters.push(fn); return this; } set(fn) { this.setters.push(fn); return this; } select(val) { this.selected = val; return this; } getDefault() { if (typeof this._default === 'function') { return this._default(); } return this._default; } getEmbeddedSchemaType() { return this._embedded; } // === Property Accessors === get validators() { return this._validators || []; } set validators(v) { this._validators = v; } get isRequired() { return this.validators.some(v => v.isRequired); } toJSONSchema(options = {}) { const jsonSchema = {}; // Handle basic type mapping switch (this.instance) { case String: jsonSchema.type = options.useBsonType ? 'string' : 'string'; break; case Number: jsonSchema.type = options.useBsonType ? 'number' : 'number'; break; case Date: jsonSchema.type = options.useBsonType ? 'date' : 'string'; jsonSchema.format = 'date-time'; break; case Boolean: jsonSchema.type = options.useBsonType ? 'bool' : 'boolean'; break; default: jsonSchema.type = 'object'; } // Handle array types if (this._isArray) { jsonSchema.type = 'array'; if (this._arrayType && typeof this._arrayType.toJSONSchema === 'function') { jsonSchema.items = this._arrayType.toJSONSchema(options); } } // Add validation constraints if (this._enum) { jsonSchema.enum = this._enum; } if (this._min != null) { jsonSchema.minimum = this._min; } if (this._max != null) { jsonSchema.maximum = this._max; } if (this._match) { jsonSchema.pattern = this._match.source; } if (this.isRequired) { jsonSchema.required = true; } return jsonSchema; } } module.exports = { SchemaType };