UNPKG

structured-elements

Version:

A TypeScript package for modelling and validating data

382 lines 17.7 kB
"use strict"; /* eslint-disable @typescript-eslint/no-explicit-any */ Object.defineProperty(exports, "__esModule", { value: true }); exports.StructuredElements = exports.Mirror = void 0; var mirror_1 = require("./mirror"); Object.defineProperty(exports, "Mirror", { enumerable: true, get: function () { return mirror_1.Mirror; } }); const array_1 = require("./attemptSalvage/array"); const collection_1 = require("./attemptSalvage/collection"); const item_1 = require("./attemptSalvage/item"); const itemViaBlanking_1 = require("./attemptSalvage/itemViaBlanking"); const mirror_2 = require("./attemptSalvage/mirror"); const referenceValidators_1 = require("./build/referenceValidators"); const structuredResultCache_1 = require("./build/structuredResultCache"); const constants_1 = require("./constants"); const referencedValidator_1 = require("./ensure/referencedValidator"); const array_2 = require("./is/array"); // This library allows an application to easily validate data by // defining a standard set of expectations for each known Type. // // The library is designed to be used with a Registry object that // contains all the models that you want to validate. You supply this // registry when you call the setup function that returns your api. // // Each model has an expectation that defines the rules for validating // the model's data. You can define expectations as inline schemas, // references to other models, or custom validation functions. // // The library caches the results of validating subjects against // expectations, so that you don't have to validate the same subject // multiple times. This is useful for performance and debugging. // // The library also provides a debug mode that logs information about // the validation process when enabled. // // The library is designed to be used with TypeScript, so that you can // define your models and expectations using the type system. This // makes it easier to catch errors and refactor your code. // // To an extent, the library can detect when a model does not match its // Type. This feature isn't perfect, but it can help you catch some // mistakes. Unfortunately, the type errors in this situation aren't // always as helpful as we would like them to be. // // You can validate a subject against an expectation by calling the // validator function on your api object. This function returns a // validator object that you can use to check if a subject is valid, // get a list of validation failures, or attempt to salvage a subject // that has failed validation. // // You can tell the library that a given expectation applies to data in // a specific structure by using the reference function on your api // object. This is useful for validating arrays, collections, and // mirrors of data. // // That same reference function can be used to create a reference to // another model, so that you specify when a model contains one or more // instances of another model. This is useful for validating nested // data structures. // Example usage: // // (lib/models.ts) // // import { StructuredElements } from 'structured-elements'; // import { type Person, PersonModel } from '@/lib/person'; // import { type Thing, ThingModel } from '@/lib/thing'; // // export type Registry = { // Person: Person; // Thing: Thing; // } // // export type Model<ModelId extends keyof Registry> = StructuredElements.Model< // Registry, // ModelId // >; // // export const Modelling = StructuredElements.setup<Registry>({ // debugEnabled: () => process.env.NODE_ENV === 'development', // models: { // Person: PersonModel, // Thing: ThingModel, // }, // }); // // (lib/person.ts) // // import { Modelling, type Model } from '@/lib/models'; // // export type Person = { // inventory: Thing[]; // name: string; // roleId?: number; // } // // export const PersonModel: Model<'Person'> = { // inventory: Modelling.reference('array', 'Thing'), // name: 'string', // roleId: ['number', undefined], // }; // // (lib/thing.ts) // // import { Modelling, type Model } from '@/lib/models'; // // export type Thing = { // id: string; // name: string; // parts: Record<string, Thing>; // type: 'widget' | 'gadget'; // weight: number | null; // } // // export const ThingModel: Model<'Thing'> = { // id: 'string', // name: 'string', // parts: Modelling.reference('collection', 'Thing'), // type: Modelling.equality('item', ['widget', 'gadget']), // weight: ['number', null], // }, // // (lib/apiConsumer.ts) // // import { Modelling } from '@/lib/models'; // // export const fetchPeople = async (ids: string) => { // const response = await fetch(`/api/people?ids=${ids}`); // const people = await response.json(); // // const validator = Modelling.validator('array', 'Person'); // // if (validator.isValid(people) { // return people; // } // // return validator.getSalvage(person, 'person'); // } var StructuredElements; (function (StructuredElements) { // Call this function to set up the validation library. // It returns an object that contains all the functions you need to interact with the library. // You can use this object to define new models, arrays, collections, and mirrors. // Make sure that you put the object in its own file and export it so that you can use it throughout your application. // The Registry type that you pass in should contain all the models that you want to validate. // The recommended approach is to match your modelId registry keys to the names of the types in your application. // Do not create more than one instance of the API object unless you have a very good reason to. StructuredElements.setup = ({ debugEnabled, logDebugMessage = console.log, models, }) => { const api = { // Supply this function when you initialize the API object. // If it returns true, the library will log debug information. debugEnabled, equality: (structure, target) => { if (api.internalCache.buildEqualityCheck) { return api.internalCache.buildEqualityCheck(structure, target); } const buildEqualityCheck = APIMethods.curryBuildEqualityCheck(); api.internalCache.buildEqualityCheck = buildEqualityCheck; return buildEqualityCheck(structure, target); }, // This cache stores all the models that have been defined. // The library uses it to look up models by their ID. // It also stores the results of validating subjects, so that we don't have to validate the same subject multiple times. // Interacting with the cache directly is not supported. registeredModels: () => { if (api.internalCache.modelRegistry) { return api.internalCache.modelRegistry; } const registryBuilders = api.internalCache.prepareModelRegistry(); const newModelRegistry = new Map(); for (const modelId in registryBuilders) { const entry = api.privateFunctions.buildRegistryEntry({ modelId, expect: registryBuilders[modelId], }); newModelRegistry.set(modelId, entry); } api.internalCache.modelRegistry = newModelRegistry; return api.internalCache.modelRegistry; }, reference: (structure, target) => { if (api.internalCache.reference) { return api.internalCache.reference(structure, target); } const reference = APIMethods.curryBuildReference(); api.internalCache.reference = reference; return reference(structure, target); }, results: new Map(), validator: (expectation, structure) => { if (api.internalCache.getValidator) { return api.internalCache.getValidator(expectation, structure || `item`); } const getValidator = APIMethods.curryGetValidator(api); api.internalCache.getValidator = getValidator; return getValidator(expectation, structure); }, // This object contains functions for attempting to salvage data in // various structures. They are mostly used internally by the library, // but exposed here because you might need to pass a specific // attemptSalvage operation into a validator. attemptSalvage: { // Salvage an array by filtering out invalid elements. array: array_1.attemptSalvageArray, // Salvage a collection by filtering out invalid entries. collection: collection_1.attemptSalvageCollection, // By default, we do not salvage invalid items. This returns undefined. item: item_1.attemptSalvageItem, // Attempt to salvage an item by discarding optional invalid fields. // This checks each invalid field against its expectation. // // If undefined is permitted, the field is omitted from the item. // // Otherwise, if null is permitted, the field's value is set to null. // // If the item has one or more invalid fields that cannot be blank, // the entire item is considered invalid and this returns undefined. itemViaBlanking: itemViaBlanking_1.attemptSalvageItemViaBlanking, // Salvage a mirror by building a new one using only the valid elements // from its collection. This process ignores the entire array, potentially // discarding some valid array elements that are not in the collection, // because we do not know what those elements would have used as keys. mirror: mirror_2.attemptSalvageMirror, }, // This object contains functions that are used internally by the library. privateFunctions: { // Validators call this function to cache the result of validating a subject against a given expectation or expectations. cacheResult: ({ expectation, result, structure }) => { if (api.internalCache.cacheResult) { return api.internalCache.cacheResult({ expectation, result, structure, }); } const cacheResult = APIMethods.curryCacheResult(api); api.internalCache.cacheResult = cacheResult; return cacheResult({ expectation, result, structure }); }, debug: logDebugMessage, // Validators call this function to check if a subject has already been validated against a given expectation. // If it has, they usually return the cached result. getCachedResult: ({ expectation, structure, subject }) => { if (api.internalCache.getCachedResult) { return api.internalCache.getCachedResult({ expectation, structure, subject, }); } const getCachedResult = APIMethods.curryGetCachedResult(api); api.internalCache.getCachedResult = getCachedResult; return getCachedResult({ expectation, structure, subject }); }, // This function is used to create each of the models in the registry. buildRegistryEntry: ({ modelId, expect: expectation }) => { if (api.internalCache.registerModel) { return api.internalCache.registerModel({ modelId, expect: expectation, }); } const registerModel = APIMethods.curryBuildRegistryEntry(api); api.internalCache.registerModel = registerModel; return registerModel({ modelId, expect: expectation }); }, }, // Interacting with this cache directly is not supported. internalCache: { buildEqualityCheck: undefined, cacheResult: undefined, getCachedResult: undefined, getValidator: undefined, modelRegistry: undefined, prepareModelRegistry: models, reference: undefined, registerModel: undefined, validators: new Map(), }, }; return api; }; // This namespace is used internally by the library to define the functions that are exposed to the application via its api object. let APIMethods; (function (APIMethods) { APIMethods.curryCacheResult = (api) => { return ({ expectation, result, structure, }) => { var _a, _b; if (StructuredElements.isCacheable(result.subject)) { const subject = result.subject; if (!api.results.has(expectation)) { const structuredResultCache = (0, structuredResultCache_1.buildStructuredResultCache)(); api.results.set(expectation, structuredResultCache); } ; (_b = (_a = api.results .get(expectation)) === null || _a === void 0 ? void 0 : _a.get(structure)) === null || _b === void 0 ? void 0 : _b.set(subject, result); } return result; }; }; APIMethods.curryGetCachedResult = (api) => { return ({ expectation, structure, subject, }) => { var _a, _b; if (StructuredElements.isCacheable(subject)) { const cachedResult = (_b = (_a = api.results .get(expectation)) === null || _a === void 0 ? void 0 : _a.get(structure)) === null || _b === void 0 ? void 0 : _b.get(subject); return cachedResult; } }; }; APIMethods.curryGetValidator = (api) => { const getValidatorFn = (expectation, structure) => { return (0, referencedValidator_1.ensureReferencedValidator)({ api, container: api.reference(structure || `item`, expectation), }); }; return getValidatorFn; }; APIMethods.curryBuildEqualityCheck = () => { return (structure, expectation) => { const equalityFunction = (subject) => { const allowedValues = (0, array_2.isArray)(expectation) ? expectation : [expectation]; return allowedValues.some((allowedValue) => { return subject === allowedValue; }); }; return { [constants_1.referenceToken]: { structure, target: equalityFunction, }, }; }; }; APIMethods.curryBuildReference = () => { return (structure, target) => { return { [constants_1.referenceToken]: { structure, target, }, }; }; }; APIMethods.curryBuildRegistryEntry = (api) => { return ({ modelId, expect, }) => { const registryEntry = { modelId, expect, validators: () => { const expectation = registryEntry.expect(); const cached = api.internalCache.validators.get(expectation); if (cached) { return cached; } const validators = (0, referenceValidators_1.buildReferenceValidators)({ api, expectation, }); api.internalCache.validators.set(expectation, validators); return validators; }, }; return registryEntry; }; }; })(APIMethods = StructuredElements.APIMethods || (StructuredElements.APIMethods = {})); // We use the WeakMap structure because it automatically cleans up // after itself when the subject is garbage collected. It stores the // results against the object's memory reference. // // We can't cache the results of validating primitive values, as they // don't have individual memory references and can be shared across // the application. WeakMap will throw an error if you try to use a // primitive value as a key. StructuredElements.isCacheable = (subject) => { return typeof subject === `object` && subject !== null; }; })(StructuredElements = exports.StructuredElements || (exports.StructuredElements = {})); //# sourceMappingURL=index.js.map