UNPKG

@eluvio/elv-utils-js

Version:

Utilities for the Eluvio Content Fabric

224 lines (193 loc) 6.83 kB
// Defines basic data validation models used to build more complex validators const curry = require('crocks/helpers/curry') const Result = require('crocks/Result') const {Err, Ok} = Result const kindOf = require('kind-of') // Object Model basic and common derived types const Om = require('objectmodel') const Model = Om.Model const ArrayModel = Om.ArrayModel const BasicModel = Om.BasicModel const FunctionModel = Om.FunctionModel const ObjectModel = Om.ObjectModel const Primitive = BasicModel([Boolean, Number, String, Symbol]).as('Primitive') const NonNull = BasicModel([Primitive, Object]).assert(function notNullOrUndefined(x) { return x !== null && x !== undefined }).as('NonNull') // Booleans-like const Falsy = BasicModel([Primitive, null, undefined]).assert(function isFalsy(x) { return !x }).as('Falsy') const Truthy = BasicModel([Primitive, Object]).assert(function isTruthy(x) { return !!x }).as('Truthy') // Numbers const Integer = BasicModel(Number).assert(Number.isInteger).as('Integer') const SafeInteger = BasicModel(Number).assert(Number.isSafeInteger).as('SafeInteger') const FiniteNumber = BasicModel(Number).assert(Number.isFinite).as('FiniteNumber') const PositiveNumber = BasicModel(Number).assert(function isPositive(n) { return n > 0 }).as('PositiveNumber') const NegativeNumber = BasicModel(Number).assert(function isNegative(n) { return n < 0 }).as('NegativeNumber') const NonNegativeNumber = BasicModel(Number).assert(function isNonNegative(n) { return n >= 0 }).as('NonNegativeNumber') const PositiveInteger = PositiveNumber.extend().assert(Number.isInteger).as('PositiveInteger') const NegativeInteger = NegativeNumber.extend().assert(Number.isInteger).as('NegativeInteger') const NonNegativeInteger = NonNegativeNumber.extend().assert(Number.isInteger).as('NonNegativeInteger') // Strings const NonBlankString = BasicModel(String).assert(function isNotBlank(str) { return kindOf(str) === 'string' && str.trim().length > 0 }).as('NonBlankString') const NormalizedString = BasicModel(String).assert(function isNormalized(str) { return str.normalize() === str }).as('NormalizedString') const TrimmedString = BasicModel(String).assert(function isTrimmed(str) { return str.trim() === str }).as('TrimmedString') // Dates const PastDate = BasicModel(Date).assert(function isInThePast(date) { return date.getTime() < Date.now() }).as('PastDate') const FutureDate = BasicModel(Date).assert(function isInTheFuture(date) { return date.getTime() > Date.now() }).as('FutureDate') // Arrays const ArrayNotEmpty = BasicModel(Array).assert(function isNotEmpty(arr) { return arr.length > 0 }).as('ArrayNotEmpty') const ArrayUnique = BasicModel(Array).assert(function hasNoDuplicates(arr) { return arr.every((x, i) => arr.indexOf(x) === i) }).as('ArrayUnique') const ArrayDense = BasicModel(Array).assert(function hasNoHoles(arr) { return arr.filter(() => true).length === arr.length }).as('ArrayDense') const TypedArrayNonEmpty = (def) => ArrayModel(def).assert(function isNotEmpty(arr) { return arr.length > 0 }).as('TypedArrayNonEmpty') const NonBlankStringOrArrayOfSame = BasicModel([NonBlankString, ArrayModel(NonBlankString)]) // Others const PromiseOf = model => p => BasicModel(Promise)(p).then(x => model(x)) const SealedModel = def => { let model = ObjectModel(def) model.sealed = true model.extend = () => { throw new Error('Sealed models cannot be extended') } const checkUndeclaredProps = (obj, def, undeclaredProps, path) => { Object.keys(obj).forEach(key => { let val = obj[key], subpath = path ? path + '.' + key : key if(!Object.prototype.hasOwnProperty.call(def, key)) { undeclaredProps.push(subpath) } else if( val && typeof val === 'object' && Object.getPrototypeOf(val) === Object.prototype ) { checkUndeclaredProps(val, def[key], undeclaredProps, subpath) } }) } return model.assert( function hasNoUndeclaredProps(obj) { if(!model.sealed) return true let undeclaredProps = [] checkUndeclaredProps(obj, this.definition, undeclaredProps) return undeclaredProps.length === 0 ? true : undeclaredProps }, undeclaredProps => `Unrecognized property name(s): ${undeclaredProps}` ) } // ============================================== // Custom Error Collector for models // ============================================== const ErrCollect = function(errors){ let errLines = [] errors.forEach(error => { // eslint-disable-next-line quotes if(error.message.indexOf(`assertion "checkType"`)===-1) errLines.push(error.message) }) if(errLines.length > 0) { // logger.warn("Error collector caught these errors:"); // logger.warn(errLines.join("\n")); } } // Model.prototype.errorCollector = ErrCollect; // ============================================== // Support for KVMap schemas (objects where any // key is allowed but all values must be of same // type // ============================================== function KVMapModelFactory(model) { return BasicModel(Object).assert(function checkType(obj) { for(const key in obj) { if(!model.test(obj[key], ErrCollect)) { return false } } return true }) } // returns function that tests 'a' against model, returns Result wrapping either 'a' or error // CheckedResult :: Model => (a => Result e a) const CheckedResult = model => a => { try { model(a) return Ok(a) } catch(e) { return Err(Error('Not a valid ' + model.name + ': ' + e.message)) } } const CheckedAbsentPropName = curry((errorMessage, object, propertyName) => !Object.keys(object).includes(propertyName) ? Ok(propertyName) : Err(Error(`"${propertyName}" ${errorMessage || ' already exists'}`)) ) const CheckedPresentPropName = curry((errorMessage, object, propertyName) => Object.keys(object).includes(propertyName) ? Ok(propertyName) : Err(Error(`"${propertyName}" ${errorMessage || ' not found'}`)) ) // tests 'a' against NonBlankString model, returns Result wrapping 'a' or error // CheckedNonBlankString :: a => Result e a const CheckedNonBlankString = CheckedResult(NonBlankString) module.exports = { Model, ArrayModel, BasicModel, FunctionModel, ObjectModel, Primitive, NonNull, Falsy, Truthy, Integer, SafeInteger, FiniteNumber, PositiveNumber, NegativeNumber, NonNegativeNumber, PositiveInteger, NegativeInteger, NonNegativeInteger, NonBlankString, NormalizedString, TrimmedString, PastDate, FutureDate, ArrayNotEmpty, ArrayUnique, ArrayDense, NonBlankStringOrArrayOfSame, TypedArrayNonEmpty, PromiseOf, SealedModel, KVMapModelFactory, CheckedResult, CheckedNonBlankString, CheckedAbsentPropName, CheckedPresentPropName }