UNPKG

@normalized-db/normalizer

Version:

Normalize and restore `JavaScript` objects based on a simple schema.

4 lines (3 loc) 9.99 kB
// (c) 2024 Sandro Schleu, License MIT var LogMode=(LogMode2=>(LogMode2.Disabled="Disabled",LogMode2.Simple="simple",LogMode2.Full="full",LogMode2))(LogMode||{});function entityMap(){let entities=new Map,visitedKeys=new Set;function addEntities(type,keys){let keysToLoad=entities.get(type);keysToLoad||(keysToLoad=new Set,entities.set(type,keysToLoad));for(let key of Array.isArray(keys)?keys:[keys])keysToLoad.add(key),visitedKeys.add(`${String(type)}.${key}`)}function hasVisited(type,key){return visitedKeys.has(`${String(type)}.${key}`)}return{entities,addEntities,hasVisited}}var Ndb;(Ndb2=>{function findEntityKeys(schema,tree,rootType,{keys:rootKeys,depth:rootDepth}={}){let{entities,addEntities,hasVisited}=entityMap();traverse(rootType,rootKeys===void 0?void 0:Array.isArray(rootKeys)?new Set(rootKeys):new Set([rootKeys]),rootDepth);function traverse(type,keys,depth){let typeSchema=schema[type];if(!typeSchema)throw new Error(`Missing schema for type ${String(type)}`);let typeTree=tree[type];if(!typeTree)throw new Error(`Missing data for type ${String(type)}`);for(let entityKey in typeTree){let typedEntityKey=isNaN(+entityKey)?entityKey:+entityKey;if(keys!==void 0&&!keys.has(typedEntityKey)||hasVisited(type,entityKey)||(addEntities(type,typedEntityKey),Ndb2.isDone(depth)))continue;let nestedEntities=typeTree[typedEntityKey]?.props;if(nestedEntities)for(let nestedProperty in nestedEntities){let target=typeSchema.targets[nestedProperty];if(!target)throw new Error(`Missing target for ${String(type)}.${String(nestedProperty)}`);let nestedKeys=nestedEntities[nestedProperty];if(nestedKeys){let nextKeys=Array.isArray(nestedKeys)?new Set(nestedKeys):new Set([nestedKeys]);traverse(target.type,nextKeys,Ndb2.nextDepth(depth,nestedProperty))}}}}return entities}Ndb2.findEntityKeys=findEntityKeys;function nextDepth(depth,nestedProperty){return typeof depth=="number"?depth-1:depth?depth[nestedProperty]??0:void 0}Ndb2.nextDepth=nextDepth;function isDone(depth){return typeof depth=="number"&&depth<=0}Ndb2.isDone=isDone})(Ndb||={});var Objects;(Objects2=>{function patch(item,property,defaultValue,merge2){let existing=item[property];if(existing!=null){if(!merge2)return existing;let merged=merge2(existing);return item[property]=merged,merged}return item[property]=defaultValue,defaultValue}Objects2.patch=patch;function merge(item1,item2){let merged={...item1};for(let key of Object.keys(item2)){let existingValue=merged[key];if(existingValue==null||Array.isArray(existingValue)&&existingValue.length===0){merged[key]=item2[key];continue}typeof existingValue=="object"&&!Array.isArray(existingValue)&&!(existingValue instanceof Date)&&!(existingValue instanceof Blob)?merged[key]=merge(existingValue,item2[key]):merged[key]=item2[key]}return merged}Objects2.merge=merge})(Objects||={});function denormalizerFactory(schema,defaultOptions){return{async preload(tree,type,load,options){let entities=Ndb.findEntityKeys(schema,tree,type,options),normalizedData={tree,keyMap:{},entities:{}};for(let[type2,keys]of entities){let denormalizedItems=await load(type2,[...keys].toSorted());for(let item of denormalizedItems){let entities2=Objects.patch(normalizedData.entities,type2,[]),index=entities2.length;entities2.push(item);let key=item[schema[type2].key];Objects.patch(normalizedData.keyMap,type2,{[key]:index},map=>({...map,[key]:index}))}}return denormalizer(schema,normalizedData,defaultOptions)(type,options)},withData(normalizedData){return{ofType:denormalizer(schema,normalizedData,defaultOptions)}}}}function denormalizer(schema,normalizedData,defaultOptions){return(rootType,options)=>{let rootDepth=options?.depth!==void 0?options.depth:defaultOptions?.depth;function fromKey(type,key,depth){let{key:keyProperty,targets}=schema[type],index=normalizedData.keyMap[type]?.[key],entity=index!==void 0&&index>=0?normalizedData.entities[type]?.[index]:normalizedData.entities[type]?.find(entity2=>entity2[keyProperty]===key);if(entity==null)throw new Error(`Could not find entity ${String(type)} with key ${String(keyProperty)}=${key} in normalized data`);let denormalizedEntity={...entity};if(!Ndb.isDone(depth))for(let[nestedProperty,target]of Object.entries(targets)){if(!target)throw new Error(`${String(type)}[${key}].${nestedProperty} is missing target schema`);let nextDepth=Ndb.nextDepth(depth,nestedProperty),nestedKeys=normalizedData.tree[type]?.[key]?.props?.[nestedProperty];denormalizedEntity[nestedProperty]=Array.isArray(nestedKeys)?fromKeys(target.type,nestedKeys,nextDepth):fromKey(target.type,nestedKeys,nextDepth)}return denormalizedEntity}function fromKeys(nestedType,keys,nestedDepth){return keys.map(key=>fromKey(nestedType,key,nestedDepth))}return{normalizedData,fromKey:rootKey=>fromKey(rootType,rootKey,rootDepth),fromKeys:rootKeys=>fromKeys(rootType,rootKeys,rootDepth),all:()=>{let keyMap=normalizedData.keyMap[rootType],keys=keyMap?Object.keys(keyMap):[];return keys.length>0?fromKeys(rootType,[...keys],rootDepth):[]}}}}var Arrays;(Arrays2=>{function pushDistinct(items,item,eq){let index=eq?items.findIndex(other=>eq(other,item)):items.indexOf(item);return index<0&&(index=items.length,items.push(item)),{items,index}}Arrays2.pushDistinct=pushDistinct;function upsert(items,item,eq){let index=eq?items.findIndex(other=>eq(other,item)):items.indexOf(item);return index<0?(index=items.length,items.push(item)):items[index]=Objects.merge(items[index],item),{items,index}}Arrays2.upsert=upsert;function merge(items1,items2,eq){let merged=items1.map(other=>({...other}));for(let other of items2)upsert(merged,other,eq);return merged}Arrays2.merge=merge;function mergePrimitive(items1,items2){let merged=[...items1];for(let other of items2)pushDistinct(merged,other);return merged}Arrays2.mergePrimitive=mergePrimitive})(Arrays||={});function normalizer(schema,defaultOptions){return(rootType,rootData,options)=>{let uniqueKeyCallback=options?.uniqueKeyCallback??defaultOptions?.uniqueKeyCallback,normalizedResult={keyMap:{},tree:{},entities:{}};apply(void 0,rootType,rootData);function apply(parent,type,item){if(item!=null)return Array.isArray(item)?applyArray(parent,type,item):applyObject(parent,type,item)}function applyArray(parent,type,items){return items.map(item=>apply(parent,type,item)).filter(key=>!!key)}function applyObject(parent,type,item){let{key:keyProperty,targets}=schema[type],normalizedItem={...item},key=item[keyProperty];if(key==null){if(!uniqueKeyCallback)throw new Error(`Key ${String(type)}.${String(keyProperty)} is missing`);key=uniqueKeyCallback(type)}let entityTypeTree=Objects.patch(normalizedResult.tree,type,{}),entityTree=Objects.patch(entityTypeTree,key,{});if(parent){let refs=Objects.patch(entityTree,"refs",{});Objects.patch(refs,parent.type,[parent.key],keys=>Arrays.pushDistinct(keys,parent.key).items)}let ref={type,key};for(let[nestedProperty,target]of Object.entries(targets)){if(!target)throw new Error(`${String(type)}.${nestedProperty} has undefined target`);let nestedValue=item[nestedProperty];if(nestedValue==null)continue;if(target.isArray&&!Array.isArray(nestedValue))throw new Error(`${ref.type}[${ref.key}].${nestedProperty} expected nested array but was ${typeof nestedValue}`);if(!target.isArray&&Array.isArray(nestedValue))throw new Error(`${ref.type}[${ref.key}].${nestedProperty} expected nested object but was array`);let targetRef=apply(ref,target.type,nestedValue);if(delete normalizedItem[nestedProperty],targetRef!==void 0){let props=Objects.patch(entityTree,"props",{});Objects.patch(props,nestedProperty,targetRef,refs=>Array.isArray(refs)&&Array.isArray(targetRef)?Arrays.mergePrimitive(refs,targetRef):!Array.isArray(refs)&&Array.isArray(targetRef)?Arrays.pushDistinct(targetRef,refs):Array.isArray(refs)&&!Array.isArray(targetRef)?Arrays.pushDistinct(refs,targetRef):targetRef)}}let normalizedItemsOfType=Objects.patch(normalizedResult.entities,type,[]),{index}=Arrays.upsert(normalizedItemsOfType,normalizedItem,(a,b)=>a[keyProperty]===b[keyProperty]);return Objects.patch(normalizedResult.keyMap,type,{[key]:index},map=>({...map,[key]:index})),key}return normalizedResult}}var defaultsKey="@defaults";function buildSchema(config){let schema={};for(let key of Object.keys(config))key.charAt(0)!=="@"&&(schema[key]=buildType(config,key));return schema}function buildType(schemaConfig,entityKey){let entity=schemaConfig[entityKey];if(!entity)throw new Error(`Schema config expected to have '${String(entityKey)}' but had [${Object.keys(schemaConfig).sort()}]`);let parentEntityKey=typeof entity=="string"?entity:entity.parent,parent=entityKey===defaultsKey?void 0:parentEntityKey?buildType(schemaConfig,parentEntityKey):defaultsKey in schemaConfig?buildType(schemaConfig,String(defaultsKey)):void 0,keyPath=parent?.key,targets=parent?.targets??{},autoKey=parent?.autoKey??!1,logging=parent?.logging??{mode:"Disabled"};if(typeof entity=="object"){if(entity.key&&(keyPath=entity.key),entity.targets){let entityTargets=buildTargets(entity.targets);targets={...targets,...entityTargets}}entity.dataStore&&(entity.dataStore.autoKey!==void 0&&(autoKey=entity.dataStore.autoKey),entity.dataStore.logging!==void 0&&(logging=entity.dataStore.logging))}if(keyPath===void 0)throw new Error(`Entity '${String(entityKey)}' is missing a key path`);return{key:keyPath,targets,autoKey,logging}}function buildTargets(targets){let entityTargets={};for(let[propKey,propConfig]of Object.entries(targets)){if(!propConfig)throw new Error(`Target ${propKey} is missing its configuration`);entityTargets[propKey]=typeof propConfig=="object"?{type:propConfig.type,isArray:propConfig.isArray??!1,cascadeRemoval:propConfig.dataStore?.cascadeRemoval??!1}:{type:propConfig,isArray:!1,cascadeRemoval:!1}}return entityTargets}function normalizedDb(config,globalOptions={}){let schema=buildSchema(config);return{schema,normalize:normalizer(schema,globalOptions.normalize),denormalizer:denormalizerFactory(schema,globalOptions.denormalize)}}export{Arrays,LogMode,Objects,normalizedDb}; //# sourceMappingURL=index.js.map