@jsiqle/core
Advanced tools
Comparing version 1.3.5 to 1.4.0
@@ -1,1 +0,1 @@ | ||
!function(root,factory){"object"==typeof exports&&"object"==typeof module?module.exports=factory():"function"==typeof define&&define.amd?define([],factory):"object"==typeof exports?exports["@jsiqle/core"]=factory():root["@jsiqle/core"]=factory()}(global,function(){return(()=>{"use strict";var __webpack_require__={n:module=>{var getter=module&&module.__esModule?()=>module.default:()=>module;return __webpack_require__.d(getter,{a:getter}),getter},d:(exports,definition)=>{for(var key in definition)__webpack_require__.o(definition,key)&&!__webpack_require__.o(exports,key)&&Object.defineProperty(exports,key,{enumerable:!0,get:definition[key]})},o:(obj,prop)=>Object.prototype.hasOwnProperty.call(obj,prop),r:exports=>{"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(exports,"__esModule",{value:!0})}},__webpack_exports__={};__webpack_require__.r(__webpack_exports__),__webpack_require__.d(__webpack_exports__,{default:()=>src});var isUndefined=require("events"),external_events_default=__webpack_require__.n(isUndefined),symbols=["fields","key","keyType","properties","cachedProperties","methods","scopes","relationships","relationshipField","validators","recordModel","recordValue","wrappedRecordValue","recordHandler","recordTag","defaultValue","addScope","addRelationshipAsField","addRelationshipAsProperty","getField","getProperty","removeScope","copyScopes","instances","isRecord","groupTag","get","handleExperimentalAPIMessage"].reduce((acc,curr)=>(acc["$"+curr]=Symbol.for(curr),acc),{});class NameError extends Error{constructor(message){super(message),this.name="NameError"}}class ValidationError extends Error{constructor(message){super(message),this.name="ValidationError"}}class DuplicationError extends Error{constructor(message){super(message),this.name="DuplicationError"}}class DefaultValueError extends Error{constructor(message){super(message),this.name="DefaultValueError"}}class ExperimentalAPIUsageError extends Error{constructor(message){super(message),this.name="ExperimentalAPIUsageError"}}class Validator{static unique(field){return(value,data)=>data.every(item=>item[field]!==value[field])}static length(field,[min,max]){return value=>value[field].length>=min&&value[field].length<=max}static minLength(field,min){return value=>value[field].length>=min}static maxLength(field,max){return value=>value[field].length<=max}static range(field,[min,max]){return value=>value[field]>=min&&value[field]<=max}static min(field,min){return value=>value[field]>=min}static max(field,max){return value=>value[field]<=max}static integer(field){return value=>Number.isInteger(value[field])}static regex(field,regex){return value=>regex.test(value[field])}static uniqueValues(field){return value=>new Set(value[field]).size===value[field].length}static sortedAscending(field){return value=>value[field].every((item,index)=>0===index||item>=value[field][index-1])}static sortedDescending(field){return value=>value[field].every((item,index)=>0===index||item<=value[field][index-1])}static custom(field,fn){return(value,data)=>fn(value[field],data.map(item=>item[field]))}}const restrictedNames={Model:["toString","toObject","toJSON"],Field:["toString","toObject","toJSON"],Property:["toString","toObject","toJSON"],Method:["toString","toObject","toJSON"],Relationship:["toString","toObject","toJSON"]},validateName=(objectType,name)=>{var[isValid,message]=((name,restrictedNames=[])=>"string"!=typeof name?[!1,"must be a string"]:name?/^\d/.test(name)?[!1,"cannot start with a number"]:restrictedNames.includes(name)?[!1,"is reserved"]:[/^\w+$/.test(name),"must contain only alphanumeric characters, numbers or underscores"]:[!1,"is required"])(name,restrictedNames[objectType]);if(!isValid)throw new NameError(objectType+` name ${message}.`);return name},capitalize=([first,...rest])=>first.toUpperCase()+rest.join(""),reverseCapitalize=([first,...rest])=>first.toLowerCase()+rest.join(""),deepClone=obj=>{if(null===obj)return null;if(obj instanceof Date)return new Date(obj);let clone=Object.assign({},obj);return Object.entries(clone).forEach(([key,value])=>clone[key]="object"==typeof obj[key]?deepClone(value):value),Array.isArray(obj)?(clone.length=obj.length,Array.from(clone)):clone},validateObjectWithUniqueName=({objectType,parentType,parentName},obj,collection)=>{if(!(obj=>obj&&"object"==typeof obj)(obj))throw new TypeError(objectType+` ${obj} is not an object.`);if(((collection,item)=>collection.includes(item))(collection,obj.name))throw new DuplicationError(`${parentType} ${parentName} already has a ${objectType.toLowerCase()} named ${obj.name}.`);return!0};var isBoolean=val=>"boolean"==typeof val,isNumber=val=>"number"==typeof val&&val==val,isString=val=>"string"==typeof val,isDate=val=>val instanceof Date,and=(...types)=>val=>types.every(type=>type(val));const or=(...types)=>val=>types.some(type=>type(val));var isPositive=val=>0<=val;const isArrayOf=type=>val=>Array.isArray(val)&&val.every(type);var standardTypes=shape=>{const props=Object.keys(shape);return val=>{return null!=val&&"object"==typeof val&&(0===props.length||Object.keys(val).length===props.length&&props.every(prop=>shape[prop](val[prop])))}},isObjectOf=type=>val=>null!=val&&"object"==typeof val&&Object.keys(val).every(prop=>type(val[prop])),isNull=val=>null===val,isUndefined=val=>void 0===val;const isNil=or(isNull,isUndefined);const types={bool:isBoolean,number:isNumber,positiveNumber:and(isNumber,isPositive),string:isString,date:isDate,stringOrNumber:or(isString,isNumber),numberOrString:or(isString,isNumber),enum:(...values)=>val=>values.includes(val),boolArray:isArrayOf(isBoolean),numberArray:isArrayOf(isNumber),stringArray:isArrayOf(isString),dateArray:isArrayOf(isDate),oneOf:or,arrayOf:isArrayOf,oneOrArrayOf:type=>val=>or(isArrayOf(type),type)(val),object:standardTypes,objectOf:isObjectOf,optional:type=>val=>or(isNil,type)(val),null:isNull,undefined:isUndefined,nil:isNil};standardTypes={boolean:{type:isBoolean,defaultValue:!1},number:{type:isNumber,defaultValue:0},positiveNumber:{type:and(isNumber,isPositive),defaultValue:0},string:{type:isString,defaultValue:""},date:{type:isDate,defaultValue:new Date},stringOrNumber:{type:or(isString,isNumber),defaultValue:""},numberOrString:{type:or(isString,isNumber),defaultValue:0},booleanArray:{type:isArrayOf(isBoolean),defaultValue:[]},numberArray:{type:isArrayOf(isNumber),defaultValue:[]},stringArray:{type:isArrayOf(isString),defaultValue:[]},dateArray:{type:isArrayOf(isDate),defaultValue:[]},object:{type:standardTypes({}),defaultValue:{}},booleanObject:{type:isObjectOf(isBoolean),defaultValue:{a:!0}},numberObject:{type:isObjectOf(isNumber),defaultValue:{}},stringObject:{type:isObjectOf(isString),defaultValue:{}},dateObject:{type:isObjectOf(isDate),defaultValue:{}},objectArray:{type:isArrayOf(standardTypes({})),defaultValue:[]}};const key=and(isString,val=>0!==val.trim().length),{$defaultValue,$validators}=symbols;class Field{#name;#defaultValue;#required;#type;#validators;constructor({name,type,required=!1,defaultValue=null,validators={}}){this.#name=validateName("Field",name),this.#required=Field.#validateRequired(required),this.#type=Field.#validateType(type,required),this.#defaultValue=Field.#validateDefaultValue(defaultValue,this.#type,this.#required),this.#validators=new Map,Object.entries(validators).forEach(([validatorName,validator])=>{this.addValidator(validatorName,validator)})}addValidator(validatorName,validator){this.#validators.set(...Field.#parseFieldValidator(this.#name,validatorName,validator))}get name(){return this.#name}get required(){return this.#required}typeCheck(value){return this.#type(value)}get[$defaultValue](){return this.#defaultValue}get[$validators](){return this.#validators}static#validateType(type,required){if("function"!=typeof type)throw new TypeError("Field type must be a function.");return required?type:types.optional(type)}static#validateRequired(required){if("boolean"!=typeof required)throw new TypeError("Field required must be a boolean.");return required}static#validateDefaultValue(defaultValue,type,required){if(required&&types.nil(defaultValue))throw new ValidationError("Default value cannot be null or undefined.");if(!type(defaultValue))throw new ValidationError("Default value must be valid.");return defaultValue}static#parseFieldValidator(fieldName,validatorName,validator){if(void 0!==Validator[validatorName])return[""+fieldName+capitalize(validatorName),Validator[validatorName](fieldName,validator)];if("function"!=typeof validator)throw new TypeError(`Validator ${validatorName} is not defined.`);return[""+fieldName+capitalize(validatorName),Validator.custom(fieldName,validator)]}}Object.entries(standardTypes).forEach(([typeName,standardType])=>{const{type,defaultValue:typeDefaultValue}=standardType;Field[typeName]=options=>"string"==typeof options?new Field({name:options,type:type}):new Field({...options,type:type}),Field[typeName+"Required"]=options=>{if("string"==typeof options)return new Field({name:options,type:type,required:!0,defaultValue:typeDefaultValue});var defaultValue=options.defaultValue||typeDefaultValue;return new Field({...options,type:type,required:!0,defaultValue:defaultValue})}}),Field.enum=({name,values})=>new Field({name:name,type:types.enum(...values)}),Field.enumRequired=({name,values,defaultValue=values[0]})=>new Field({name:name,type:types.enum(...values),required:!0,defaultValue:defaultValue}),Field.auto=autoField=>{autoField="string"==typeof autoField?autoField:autoField.name;const generator=function*(){let i=0;for(;;)yield i++}();let currentValue=0;autoField=new Field({name:autoField,type:value=>value===currentValue,required:!0,defaultValue:currentValue});return Object.defineProperty(autoField,$defaultValue,{get(){var value=generator.next().value;return currentValue=value}}),autoField};const{$recordValue,$wrappedRecordValue,$recordHandler,$recordModel,$recordTag,$cachedProperties,$key}=symbols;class Record{#recordValue;#recordHandler;#proxiedRecord;#cachedProperties;constructor(value,handler){return this.#recordValue=value,this.#recordHandler=handler,this.#cachedProperties=new Map,this.#proxiedRecord=new Proxy(this,this.#recordHandler),this.#proxiedRecord}get[$cachedProperties](){return this.#cachedProperties}get[$recordHandler](){return this.#recordHandler}get[$recordValue](){return this.#recordValue}get[$wrappedRecordValue](){return this.#proxiedRecord}get[$recordModel](){return this.#recordHandler.model}get[$recordTag](){var model=this[$recordModel],key=model[$key].name;return model.name+"#"+this[$recordValue][key]}get[Symbol.toStringTag](){return this[$recordTag]}}const record=Record,partial_$recordTag=symbols["$recordTag"];class PartialRecord{#tag;constructor(value,tag){Object.keys(value).forEach(key=>{this[key]=value[key]}),this.#tag=tag}get[partial_$recordTag](){return this.#tag}get[Symbol.toStringTag](){return this[partial_$recordTag]}toObject(){return{...this}}toJSON(){return this.toObject()}}const fragment_$recordTag=symbols["$recordTag"];class RecordFragment extends Array{#tag;constructor(values,tag){super(),values.forEach(value=>{this.push(value)}),this.#tag=tag}get[fragment_$recordTag](){return this.#tag}get[Symbol.toStringTag](){return this[fragment_$recordTag]}toObject(){return[...this]}toJSON(){return this.toObject()}}const{$recordModel:set_$recordModel,$recordTag:set_$recordTag,$scopes,$addScope,$removeScope,$copyScopes,$isRecord,$key:set_$key}=symbols;class RecordSet extends Map{#frozen;#scopes;constructor({iterable=[],copyScopesFrom=null}={}){super();for(var[key,value]of iterable)this.set(key,value);this.#scopes=new Map,copyScopesFrom&&this[$copyScopes](copyScopesFrom),this.#frozen=!1}freeze(){return this.#frozen=!0,this}set(key,value){if(this.#frozen)throw new TypeError("Cannot modify a frozen RecordSet.");return super.set(key,value),this}delete(key){if(this.#frozen)throw new TypeError("Cannot modify a frozen RecordSet.");return super.delete(key)}clear(){if(this.#frozen)throw new TypeError("Cannot modify a frozen RecordSet.");super.clear()}map(callbackFn){return[...this.entries()].reduce((newMap,[key,value])=>(newMap[key]=callbackFn(value,key,this),newMap),{})}flatMap(callbackFn){return[...this.entries()].map(([key,value])=>callbackFn(value,key,this))}reduce(callbackFn,initialValue){return[...this.entries()].reduce((acc,[key,value])=>callbackFn(acc,value,key,this),initialValue)}filter(callbackFn){return[...this.entries()].reduce((newMap,[key,value])=>(callbackFn(value,key,this)&&newMap.set(key,value),newMap),new RecordSet({copyScopesFrom:this})).freeze()}flatFilter(callbackFn){return[...this.entries()].reduce((arr,[key,value])=>(callbackFn(value,key,this)&&arr.push(value),arr),[])}find(callbackFn){var match=[...this.entries()].find(([key,value])=>callbackFn(value,key,this));if(match)return match[1]}findKey(callbackFn){var match=[...this.entries()].find(([key,value])=>callbackFn(value,key,this));if(match)return match[0]}only(...keys){return new RecordSet({iterable:keys.reduce((itr,key)=>(this.has(key)&&itr.push([key,this.get(key)]),itr),[]),copyScopesFrom:this}).freeze()}except(...keys){return new RecordSet({iterable:[...this.entries()].filter(([key])=>!keys.includes(key)),copyScopesFrom:this}).freeze()}sort(comparatorFn){var sorted=[...this.entries()].sort(([key1,value1],[key2,value2])=>comparatorFn(value1,value2,key1,key2));return new RecordSet({iterable:sorted,copyScopesFrom:this}).freeze()}every(callbackFn){return 0===this.size||[...this.entries()].every(([key,value])=>callbackFn(value,key,this))}some(callbackFn){return 0!==this.size&&[...this.entries()].some(([key,value])=>callbackFn(value,key,this))}select(...keys){return new RecordSet({iterable:[...this.entries()].map(([key,value])=>{const obj={};return keys.forEach(key=>obj[key]=value[key]),[key,new PartialRecord(obj,value[set_$recordTag])]}),copyScopesFrom:this}).freeze()}flatSelect(...keys){return[...this.values()].map(value=>keys.reduce((obj,key)=>({...obj,[key]:value[key]}),{}))}pluck(...keys){return new RecordSet({iterable:[...this.entries()].map(([key,value])=>{var values=keys.map(key=>value[key]);return[key,new RecordFragment(values,value[set_$recordTag])]}),copyScopesFrom:this}).freeze()}flatPluck(...keys){const isSingleKey=1===keys.length;return[...this.values()].map(value=>isSingleKey?value[keys[0]]:keys.map(key=>value[key]))}groupBy(key){const res=new RecordSet({copyScopesFrom:this,iterable:[]});for(var[recordKey,value]of this.entries()){let keyValue=value[key];void 0!==keyValue&&null!==keyValue&&keyValue[$isRecord]&&(keyValue=value[key][set_$key]),res.has(keyValue)||res.set(keyValue,new RecordGroup({copyScopesFrom:this,iterable:[],groupName:keyValue})),res.get(keyValue).set(recordKey,value)}for(const value of res.values())value.freeze();return res.freeze()}duplicate(){return new RecordSet({iterable:[...this.entries()],copyScopesFrom:this}).freeze()}merge(...recordSets){const res=new Map([...this.entries()]);for(const recordSet of recordSets)for(var[key,value]of recordSet.entries()){if(res.has(key))throw new DuplicationError(`Key ${key} already exists in the record set.`);res.set(key,value)}return new RecordSet({iterable:[...res.entries()],copyScopesFrom:this}).freeze()}append(...records){const res=new RecordSet({iterable:[...this.entries()],copyScopesFrom:this});for(const record of records)res.set(record[set_$key],record);return res.freeze()}where(callbackFn){return this.filter(callbackFn)}whereNot(callbackFn){return this.filter((value,key,map)=>!callbackFn(value,key,map))}*flatBatchIterator(batchSize){let batch=[];for(var[,value]of this)batch.push(value),batch.length===batchSize&&(yield batch,batch=[]);batch.length&&(yield batch)}*batchIterator(batchSize){let batch=[];for(var[key,value]of this)batch.push([key,value]),batch.length===batchSize&&(yield new RecordSet({copyScopesFrom:this,iterable:batch}).freeze(),batch=[]);batch.length&&(yield new RecordSet({copyScopesFrom:this,iterable:batch}).freeze())}limit(n){let records=[];for(var[key,value]of this)if(records.push([key,value]),records.length===n)break;return new RecordSet({iterable:records,copyScopesFrom:this}).freeze()}offset(n){let counter=0,records=[];for(var[key,value]of this)counter<n?counter++:records.push([key,value]);return new RecordSet({iterable:records,copyScopesFrom:this}).freeze()}slice(start,end){return new RecordSet({iterable:[...this.entries()].slice(start,end),copyScopesFrom:this}).freeze()}get first(){for(var[,value]of this)return value}get last(){if(0!==this.size)return[...this.entries()].pop()[1]}get count(){return this.size}get length(){return this.size}toArray(){return[...this.values()]}toFlatArray(){return[...this.values()].map(value=>value instanceof RecordGroup?value.toFlatArray():value.toObject())}toObject(){return[...this.entries()].reduce((obj,[key,value])=>(obj[key]=value,obj),{})}toFlatObject(){return[...this.entries()].reduce((obj,[key,value])=>(obj[key]=value instanceof RecordGroup?value.toFlatArray():value.toObject(),obj),{})}toJSON(){return this.toObject()}get[Symbol.toStringTag](){var records=[...this.values()];try{const firstModel=records[0][set_$recordModel].name;if(((arr,fn)=>{const eql=fn(arr[0]);return arr.every(val=>fn(val)===eql)})(records,value=>value[set_$recordModel].name===firstModel))return firstModel}catch(e){return""}return""}static get[Symbol.species](){return Map}[$addScope](name,scope){if(RecordSet.#validateProperty("Scope",name,scope,this.#scopes),this[name]||Object.getOwnPropertyNames(RecordSet.prototype).includes(name))throw new NameError(`Scope name ${name} is already in use.`);this.#scopes.set(name,scope),Object.defineProperty(this,name,{configurable:!0,get:()=>this.where(this.#scopes.get(name))})}[$removeScope](name){this.#scopes.delete(RecordSet.#validateContains("Scope",name,this.#scopes)),delete this[name]}[$copyScopes](otherRecordSet){otherRecordSet[$scopes].forEach((scope,name)=>{this[$addScope](name,scope)})}get[$scopes](){return this.#scopes}static#validateProperty(callbackType,callbackName,callback,callbacks){if("function"!=typeof callback)throw new TypeError(callbackType+` ${callbackName} is not a function.`);if(callbacks.has(callbackName))throw new DuplicationError(callbackType+` ${callbackName} already exists.`);return callback}static#validateContains(objectType,objectName,objects){if(!objects.has(objectName))throw new ReferenceError(objectType+` ${objectName} does not exist.`);return objectName}}const set=RecordSet,$groupTag=symbols["$groupTag"];class RecordGroup extends set{#groupName;constructor({iterable=[],copyScopesFrom=null,groupName=""}={}){super({iterable:iterable,copyScopesFrom:copyScopesFrom}),this.#groupName=groupName}get[$groupTag](){return this.#groupName}get[Symbol.toStringTag](){return this[$groupTag]}}const{$fields,$defaultValue:handler_$defaultValue,$key:handler_$key,$keyType,$properties,$cachedProperties:handler_$cachedProperties,$methods,$relationships,$validators:handler_$validators,$recordValue:handler_$recordValue,$wrappedRecordValue:handler_$wrappedRecordValue,$recordModel:handler_$recordModel,$recordTag:handler_$recordTag,$isRecord:handler_$isRecord,$get}=symbols;class RecordHandler{#model;constructor(model){this.#model=model}get model(){return this.#model}createRecord(recordData){if(!recordData)throw new TypeError("Record data cannot be empty.");if("object"!=typeof recordData)throw new TypeError("Record data must be an object.");const modelName=this.#getModelName(),newRecordKey=RecordHandler.#validateNewRecordKey(modelName,this.#getKey(),recordData[this.#getKey().name],this.#model.records),clonedRecord=deepClone(recordData),extraProperties=Object.keys(clonedRecord).filter(property=>!this.#hasField(property)&&!this.#isModelKey(property));0<extraProperties.length&&console.warn(`${modelName} record has extra fields: ${extraProperties.join(", ")}.`);const newRecord=new record({[this.#getKey().name]:newRecordKey,...extraProperties.reduce((obj,property)=>({...obj,[property]:clonedRecord[property]}),{})},this);return this.#getFieldNames().forEach(field=>{this.set(newRecord,field,clonedRecord[field],newRecord,!0)}),this.#getValidators().forEach((validator,validatorName)=>{if(!validator(newRecord,this.#model.records))throw new RangeError(`${modelName} record with key ${newRecordKey} failed validation for ${validatorName}.`)}),[newRecordKey,newRecord]}get(record,property){return this.#hasRelationshipField(property)?this.#getRelationship(record,property):this.#isModelKey(property)||this.#hasField(property)?this.#getFieldValue(record,property):this.#hasProperty(property)?this.#getProperty(record,property):this.#hasMethod(property)?this.#getMethod(record,property):this.#isCallToSerialize(property)?RecordHandler.#recordToObject(record,this.#model,this):this.#isCallToString(property)?()=>this.#getKeyValue(record):this.#isKnownSymbol(property)?this.#getKnownSymbol(record,property):void 0}set(record,property,value,receiver,skipValidation){const recordValue=record[handler_$recordValue],recordKey=this.#getKeyValue(record),otherRecords=this.#model.records.except(recordKey);if(this.#hasProperty(property))throw new TypeError(`${this.#getModelName()} record ${recordKey} cannot set property ${property}.`);if(this.#hasMethod(property))throw new TypeError(`${this.#getModelName()} record ${recordKey} cannot set method ${property}.`);if(this.#hasField(property)){const field=this.#getField(property);RecordHandler.#setRecordField(this.#model.name,record,field,value),field[handler_$validators].forEach((validator,validatorName)=>{if(![null,void 0].includes(recordValue[property])&&!validator(recordValue,otherRecords))throw new RangeError(`${this.#getModelName()} record with key ${recordKey} failed validation for ${validatorName}.`)})}else console.warn(this.#model.name+` record has extra field: ${property}.`),recordValue[property]=value;return skipValidation||this.#getValidators().forEach((validator,validatorName)=>{if(!validator(recordValue,otherRecords))throw new RangeError(`${this.#getModelName()} record with key ${recordKey} failed validation for ${validatorName}.`)}),!0}static#setRecordField(modelName,record,field,recordValue){recordValue=field.required&&types.nil(recordValue)?field[handler_$defaultValue]:recordValue;if(!field.typeCheck(recordValue))throw new TypeError(`${modelName} record has invalid value for field ${field.name}.`);record[handler_$wrappedRecordValue]&&record[handler_$cachedProperties].clear(),record[handler_$recordValue][field.name]=recordValue}static#recordToObject(record,key,handler){const recordValue=record[handler_$recordValue],fields=key[$fields],properties=key[$properties];key=key[handler_$key].name;const object={[key]:recordValue[key]};fields.forEach(field=>{void 0!==recordValue[field.name]&&(object[field.name]=recordValue[field.name])});return({include=[]}={})=>{var result=object;const included=include.map(name=>{const[field,...props]=name.split(".");return[field,props.join(".")]});return included.forEach(([includedField,props])=>{if(object[includedField])if(Array.isArray(object[includedField])){const records=handler.get(record,includedField);object[includedField]=records.map(record=>record.toObject({include:[props]}))}else object[includedField]=handler.get(record,includedField).toObject({include:[props]});else properties.has(includedField)&&(object[includedField]=handler.get(record,includedField))}),result}}static#validateNewRecordKey=(modelName,modelKey,recordKey,records)=>{let newRecordKey=recordKey;if("string"===modelKey[$keyType]&&!modelKey.typeCheck(newRecordKey))throw new TypeError(`${modelName} record has invalid value for key ${modelKey.name}.`);if("auto"===modelKey[$keyType]&&(newRecordKey=modelKey[handler_$defaultValue]),records.has(newRecordKey))throw new DuplicationError(`${modelName} record with key ${newRecordKey} already exists.`);return newRecordKey};#getModelName(){return this.#model.name}#getFieldNames(){return[...this.#model[$fields].keys()]}#getValidators(){return this.#model[handler_$validators]}#isModelKey(property){return this.#model[handler_$key].name===property}#getKey(){return this.#model[handler_$key]}#getKeyValue(record){return record[handler_$recordValue][this.#model[handler_$key].name]}#hasField(property){return this.#model[$fields].has(property)}#getField(property){return this.#model[$fields].get(property)}#getFieldValue(record,property){return record[handler_$recordValue][property]}#hasProperty(property){return this.#model[$properties].has(property)}#getProperty(record,property){if(this.#model[handler_$cachedProperties].has(property)){if(record[handler_$cachedProperties]&&record[handler_$cachedProperties].has(property))return record[handler_$cachedProperties].get(property);var value=this.#model[$properties].get(property)(record[handler_$wrappedRecordValue]);return record[handler_$cachedProperties].set(property,value),value}return this.#model[$properties].get(property)(record[handler_$wrappedRecordValue])}#hasMethod(method){return this.#model[$methods].has(method)}#getMethod(record,method){const methodFn=this.#model[$methods].get(method);return(...args)=>methodFn(record[handler_$wrappedRecordValue],...args)}#hasRelationshipField(property){return!!this.#hasField(property)&&this.#model[$relationships].has(property+"."+property)}#getRelationship(record,property){return this.#model[$relationships].get(property+"."+property)[$get](this.#getModelName(),property,record[handler_$recordValue])}#isCallToSerialize(property){return"toObject"===property||"toJSON"===property}#isCallToString(property){return"toString"===property}#isKnownSymbol(property){return[handler_$recordModel,handler_$recordTag,handler_$recordValue,handler_$isRecord,handler_$key].includes(property)}#getKnownSymbol(record,property){return property===handler_$isRecord||(property===handler_$key&&this.#getKey(),record[property])}}const handler=RecordHandler,{$fields:model_$fields,$defaultValue:model_$defaultValue,$key:model_$key,$keyType:model_$keyType,$properties:model_$properties,$cachedProperties:model_$cachedProperties,$methods:model_$methods,$scopes:model_$scopes,$relationships:model_$relationships,$validators:model_$validators,$recordHandler:model_$recordHandler,$addScope:model_$addScope,$addRelationshipAsField,$addRelationshipAsProperty,$getField,$getProperty,$removeScope:model_$removeScope,$instances,$handleExperimentalAPIMessage}=symbols,allStandardTypes=[...Object.keys(standardTypes),...Object.keys(standardTypes).map(type=>type+"Required"),"enum","enumRequired","auto"];class Model extends external_events_default(){#records;#recordHandler;#fields;#key;#properties;#methods;#relationships;#validators;#updatingField=!1;#cachedProperties;static#instances=new Map;constructor({name,fields=[],key="id",properties={},methods={},scopes={},validators={},cacheProperties=[]}={}){if(super(),this.name=validateName("Model",name),Model.#instances.has(name))throw new DuplicationError(`A model named ${name} already exists.`);this.#records=new set,this.#recordHandler=new handler(this),this.#key=Model.#parseKey(this.name,key),this.#fields=new Map,this.#properties=new Map,this.#methods=new Map,this.#relationships=new Map,this.#validators=new Map,this.#cachedProperties=new Set,fields.forEach(field=>this.addField(field)),Object.entries(properties).forEach(([propertyName,property])=>{this.addProperty(propertyName,property,cacheProperties.includes(propertyName))}),Object.entries(methods).forEach(([methodName,method])=>{this.addMethod(methodName,method)}),Object.entries(scopes).forEach(([scopeName,scope])=>{this.addScope(scopeName,scope)}),Object.entries(validators).forEach(([validatorName,validator])=>{this.addValidator(validatorName,validator)}),Model.#instances.set(this.name,this)}addField(fieldOptions,retrofill){this.#updatingField||this.emit("beforeAddField",{field:fieldOptions,model:this});var field=Model.#parseField(this.name,fieldOptions,[...this.#fields.keys(),this.#key.name,...this.#properties.keys(),...this.#methods.keys()]);return this.#fields.set(fieldOptions.name,field),this.#updatingField||this.emit("fieldAdded",{field:field,model:this}),this.emit("beforeRetrofillField",{field:field,retrofill:retrofill,model:this}),Model.#applyFieldRetrofill(field,this.#records,retrofill),this.emit("fieldRetrofilled",{field:field,retrofill:retrofill,model:this}),this.#updatingField||this.emit("change",{type:"fieldAdded",field:field,model:this}),field}removeField(name){if(!Model.#validateContains(this.name,"Field",name,this.#fields))return!1;var field=this.#fields.get(name);return this.#updatingField||this.emit("beforeRemoveField",{field:field,model:this}),this.#fields.delete(name),this.#updatingField||(this.emit("fieldRemoved",{field:{name:name},model:this}),this.emit("change",{type:"fieldRemoved",field:field,model:this})),!0}updateField(name,field,newField){if(field.name!==name)throw new NameError(`Field name ${field.name} does not match ${name}.`);if(!Model.#validateContains(this.name,"Field",name,this.#fields))throw new ReferenceError(`Field ${name} does not exist.`);var prevField=this.#fields.get(name);this.#updatingField=!0,this.emit("beforeUpdateField",{prevField:prevField,field:field,model:this}),this.removeField(name);newField=this.addField(field,newField);this.emit("fieldUpdated",{field:newField,model:this}),this.#updatingField=!1,this.emit("change",{type:"fieldUpdated",field:newField,model:this})}addProperty(name,property,cache=!1){this.emit("beforeAddProperty",{property:{name:name,body:property},model:this});var propertyName=validateName("Property",name);this.#properties.set(propertyName,Model.#validateFunction("Property",name,property,[...this.#fields.keys(),this.#key.name,...this.#properties.keys(),...this.#methods.keys()])),cache&&this.#cachedProperties.add(propertyName),this.emit("propertyAdded",{property:{name:propertyName,body:property},model:this}),this.emit("change",{type:"propertyAdded",property:{name:propertyName,body:property},model:this})}removeProperty(name){if(!Model.#validateContains(this.name,"Property",name,this.#properties))return!1;var property=this.#properties.get(name);return this.emit("beforeRemoveProperty",{property:{name:name,body:property},model:this}),this.#properties.delete(name),this.#cachedProperties.has(name)&&this.#cachedProperties.delete(name),this.emit("propertyRemoved",{property:{name:name},model:this}),this.emit("change",{type:"propertyRemoved",property:{name:name,body:property},model:this}),!0}addMethod(name,method){this.emit("beforeAddMethod",{method:{name:name,body:method},model:this});var methodName=validateName("Method",name);this.#methods.set(methodName,Model.#validateFunction("Method",name,method,[...this.#fields.keys(),this.#key.name,...this.#properties.keys(),...this.#methods.keys()])),this.emit("methodAdded",{method:{name:methodName,body:method},model:this}),this.emit("change",{type:"methodAdded",method:{name:methodName,body:method},model:this})}removeMethod(name){if(!Model.#validateContains(this.name,"Method",name,this.#methods))return!1;var method=this.#methods.get(name);return this.emit("beforeRemoveMethod",{method:{name:name,body:method},model:this}),this.#methods.delete(name),this.emit("methodRemoved",{method:{name:name},model:this}),this.emit("change",{type:"methodRemoved",method:{name:name,body:method},model:this}),!0}addScope(scopeName,scope){this.emit("beforeAddScope",{scope:{name:scopeName,body:scope},model:this});scopeName=validateName("Scope",scopeName);this.#records[model_$addScope](scopeName,scope),this.emit("scopeAdded",{scope:{name:scopeName,body:scope},model:this}),this.emit("change",{type:"scopeAdded",scope:{name:scopeName,body:scope},model:this})}removeScope(name){if(!Model.#validateContains(this.name,"Scope",name,this.#records[model_$scopes]))return!1;var scope=this.#records[model_$scopes].get(name);return this.emit("beforeRemoveScope",{scope:{name:name,body:scope},model:this}),this.#records[model_$removeScope](name),this.emit("scopeRemoved",{scope:{name:name},model:this}),this.emit("change",{type:"scopeRemoved",scope:{name:name,body:scope},model:this}),!0}addValidator(name,validator){this.emit("beforeAddValidator",{validator:{name:name,body:validator},model:this}),this.#validators.set(name,Model.#validateFunction("Validator",name,validator,[...this.#validators.keys()])),this.emit("validatorAdded",{validator:{name:name,body:validator},model:this}),this.emit("change",{type:"validatorAdded",validator:{name:name,body:validator},model:this})}removeValidator(name){if(!Model.#validateContains(this.name,"Validator",name,this.#validators))return!1;var validator=this.#validators.get(name);return this.emit("beforeRemoveValidator",{validator:{name:name,body:validator},model:this}),this.#validators.delete(name),this.emit("validatorRemoved",{validator:{name:name},model:this}),this.emit("change",{type:"validatorRemoved",validator:{name:name,body:validator},model:this}),!0}createRecord(newRecord){this.emit("beforeCreateRecord",{record:newRecord,model:this});var[newRecordKey,newRecord]=this.#recordHandler.createRecord(newRecord);return this.#records.set(newRecordKey,newRecord),this.emit("recordCreated",{newRecord:newRecord,model:this}),newRecord}removeRecord(recordKey){if(!this.#records.has(recordKey))return console.warn(`Record ${recordKey} does not exist.`),!1;var record=this.#records.get(recordKey);return this.emit("beforeRemoveRecord",{record:record,model:this}),this.#records.delete(recordKey),this.emit("recordRemoved",{record:{[this.#key.name]:recordKey},model:this}),!0}updateRecord(recordKey,record){if("object"!=typeof record)throw new TypeError("Record data must be an object.");if(!this.#records.has(recordKey))throw new ReferenceError(`Record ${recordKey} does not exist.`);const oldRecord=this.#records.get(recordKey);return this.emit("beforeUpdateRecord",{record:oldRecord,newRecord:{[this.#key.name]:recordKey,...record},model:this}),Object.entries(record).forEach(([fieldName,fieldValue])=>{oldRecord[fieldName]=fieldValue}),this.emit("recordUpdated",{record:oldRecord,model:this}),oldRecord}get records(){return this.#records}static get[$instances](){return Model.#instances}get[model_$recordHandler](){return this.#recordHandler}get[model_$fields](){return this.#fields}get[model_$key](){return this.#key}get[model_$properties](){return this.#properties}get[model_$cachedProperties](){return this.#cachedProperties}get[model_$methods](){return this.#methods}get[model_$relationships](){return this.#relationships}get[model_$validators](){return this.#validators}[$addRelationshipAsField](relationship){var{name,type,fieldName,field}=relationship[$getField](),relationshipName=name+"."+fieldName;if(this.emit("beforeAddRelationship",{relationship:{name:name,type:type},model:this}),[...this.#fields.keys(),this.#key.name,...this.#properties.keys(),...this.#methods.keys()].includes(fieldName))throw new NameError(`Relationship field ${fieldName} is already in use.`);if(this.#relationships.has(relationshipName))throw new NameError(`Relationship ${relationshipName} is already in use.`);this.#fields.set(fieldName,field),this.#relationships.set(relationshipName,relationship),this.emit("relationshipAdded",{relationship:{name:name,type:type},model:this}),this.emit("change",{type:"relationshipAdded",relationship:{relationship:{name:name,type:type},model:this},model:this})}[$addRelationshipAsProperty](relationship){var{name,type,propertyName,property}=relationship[$getProperty](),relationshipName=name+"."+propertyName;if(this.emit("beforeAddRelationship",{relationship:{name:name,type:type},model:this}),[...this.#fields.keys(),this.#key.name,...this.#properties.keys(),...this.#methods.keys()].includes(propertyName))throw new NameError(`Relationship property ${propertyName} is already in use.`);if(this.#relationships.has(relationshipName))throw new NameError(`Relationship ${name} is already in use.`);this.#properties.set(propertyName,property),this.#relationships.set(relationshipName,relationship),this.emit("relationshipAdded",{relationship:{name:name,type:type},model:this}),this.emit("change",{type:"relationshipAdded",relationship:{relationship:{name:name,type:type},model:this},model:this})}static#createKey(options){let name="id",type="string";"string"==typeof options?name=options:"object"==typeof options&&(name=options.name||name,type=options.type||type);let keyField;return"string"===type?(keyField=new Field({name:name,type:key,required:!0,defaultValue:"__emptyKey__"}),Object.defineProperty(keyField,model_$defaultValue,{get(){throw new DefaultValueError(`Key field ${name} does not have a default value.`)}})):"auto"===type&&(keyField=Field.auto(name)),Object.defineProperty(keyField,model_$keyType,{get(){return type}}),keyField}static#parseKey(modelName,key){if("string"!=typeof key&&"object"!=typeof key)throw new TypeError(modelName+` key ${key} is not a string or object.`);if("object"==typeof key&&!key.name)throw new TypeError(modelName+` key ${key} is missing a name.`);if("object"==typeof key&&!["auto","string"].includes(key.type))throw new TypeError(modelName+` key ${key} type must be either "string" or "auto".`);return Model.#createKey(key)}static#parseField(modelName,field,restrictedNames){return validateObjectWithUniqueName({objectType:"Field",parentType:"Model",parentName:modelName},field,restrictedNames),allStandardTypes.includes(field.type)?Field[field.type](field):("function"==typeof field.type&&Schema[$handleExperimentalAPIMessage](`The provided type for ${field.name} is not part of the standard types. Function types are experimental and may go away in a later release.`),new Field(field))}static#validateFunction(callbackType,callbackName,callback,restrictedNames){if("function"!=typeof callback)throw new TypeError(callbackType+` ${callbackName} is not a function.`);if(restrictedNames.includes(callbackName))throw new DuplicationError(callbackType+` ${callbackName} already exists.`);return callback}static#validateContains(modelName,objectType,objectName,objects){return!!objects.has(objectName)||(console.warn(`Model ${modelName} does not contain a ${objectType.toLowerCase()} named ${objectName}.`),!1)}static#applyFieldRetrofill(field,records,retrofill){if(field.required||void 0!==retrofill){const retrofillFunction=void 0!==retrofill?"function"==typeof retrofill?retrofill:()=>retrofill:record=>record[field.name]||field[model_$defaultValue];records.forEach(record=>{record[field.name]=retrofillFunction(record)})}}}const{$key:relationship_$key,$recordValue:relationship_$recordValue,$fields:relationship_$fields,$getField:relationship_$getField,$getProperty:relationship_$getProperty,$get:relationship_$get,$defaultValue:relationship_$defaultValue,$instances:relationship_$instances,$handleExperimentalAPIMessage:relationship_$handleExperimentalAPIMessage}=symbols,relationshipEnum={oneToOne:"oneToOne",oneToMany:"oneToMany",manyToOne:"manyToOne",manyToMany:"manyToMany"};class Relationship{#type;#from;#to;#name;#reverseName;#relationshipField;#relationshipProperty;constructor({from:fromName,to:toModel,type:toName}={}){Schema[relationship_$handleExperimentalAPIMessage]("Relationships are experimental in the current version. There is neither validation of existence in foreign tables nor guarantee that associations work. Please use with caution."),this.#type=Relationship.#validateType(toName);var[fromModel,fromName,toModel,toName]=Relationship.#parseModelsAndNames(fromName,toModel,toName);if(this.#from=fromModel,this.#to=toModel,this.#name=fromName,this.#reverseName=toName,this.#to===this.#from&&Relationship.#isSymmetric(this.#type)&&this.#name===this.#reverseName)throw new RangeError("Relationship cannot be symmetric if the from and to models are the same and no name is provided for either one.");this.#relationshipField=Relationship.#createField(this.#name,this.#type,this.#to[relationship_$key]),this.#relationshipProperty=record=>this.#getAssociatedRecordsReverse(record)}[relationship_$getField](){return{name:this.#name,type:this.#type,fieldName:this.#name,field:this.#relationshipField}}[relationship_$getProperty](){return{name:this.#name,type:this.#type,propertyName:this.#reverseName,property:this.#relationshipProperty}}[relationship_$get](modelName,property,record){return modelName===this.#from.name&&property===this.#name?this.#getAssociatedRecords(record):modelName===this.#to.name&&property===this.#reverseName?(console.warn("Relationship getter called by the receiver model. This might indicate an issue with the library and should be reported."),this.#getAssociatedRecordsReverse(record)):void 0}#getAssociatedRecords(associationValues){if(Relationship.#isToOne(this.#type)){var associationValue=associationValues[this.#name];return this.#to.records.get(associationValue)}associationValues=associationValues[this.#name]||[];return this.#to.records.only(...associationValues)}#getAssociatedRecordsReverse(matcher){const associationValue=matcher[this.#to[relationship_$key].name];matcher=Relationship.#isToOne(this.#type)?associatedRecord=>associatedRecord[relationship_$recordValue][this.#name]===associationValue:associatedRecord=>{var associatedRecordValue=associatedRecord[relationship_$recordValue][this.#name];return![void 0,null].includes(associatedRecordValue)&&associatedRecord[relationship_$recordValue][this.#name].includes(associationValue)};return Relationship.#isFromOne(this.#type)?this.#from.records.find(matcher):this.#from.records.where(matcher)}static#isToOne(type){return[relationshipEnum.oneToOne,relationshipEnum.manyToOne].includes(type)}static#isToMany(type){return[relationshipEnum.oneToMany,relationshipEnum.manyToMany].includes(type)}static#isFromOne(type){return[relationshipEnum.oneToMany,relationshipEnum.oneToOne].includes(type)}static#isFromMany(type){return[relationshipEnum.manyToOne,relationshipEnum.manyToMany].includes(type)}static#isSymmetric(type){return[relationshipEnum.oneToOne,relationshipEnum.manyToMany].includes(type)}static#createField(name,type,foreignField){var isSingleSource=Relationship.#isFromOne(type),relationshipField=Relationship.#isToMany(type),type=relationshipField?types.arrayOf(value=>foreignField.typeCheck(value)):value=>foreignField.typeCheck(value);const validators={};isSingleSource&&!relationshipField&&(validators.unique=!0),relationshipField&&(validators.uniqueValues=!0);relationshipField=new Field({name:name,type:type,required:!1,defaultValue:relationshipField?[]:null,validators:validators});return Object.defineProperty(relationshipField,relationship_$defaultValue,{get(){throw new DefaultValueError("Relationship field does not have a default value.")}}),relationshipField}static#validateType(relationshipType){if(!Object.values(relationshipEnum).includes(relationshipType))throw new TypeError(`Invalid relationship type: ${relationshipType}.`);return relationshipType}static#validateModel(modelName){modelName="string"==typeof modelName?modelName:modelName.model;if(!Model[relationship_$instances].has(modelName))throw new ReferenceError(`Model ${modelName} does not exist.`);return Model[relationship_$instances].get(modelName)}static#createName(type,to){return Relationship.#isToOne(type)?reverseCapitalize(to):Relationship.#isToMany(type)?reverseCapitalize(to)+"Set":void 0}static#createReverseName=(type,from)=>Relationship.#isFromOne(type)?reverseCapitalize(from):Relationship.#isFromMany(type)?reverseCapitalize(from)+"Set":void 0;static#validateModelParams(name){const model=Relationship.#validateModel(name);name="string"==typeof name?null:validateName("Field",name.name);if(null!==name&&model[relationship_$fields].has(name))throw new DuplicationError(`Field ${name} already exists in ${model.name}.`);return[model,name]}static#parseModelsAndNames(from,to,type){let fromModel,fromName,toModel,toName;return[fromModel,fromName]=Relationship.#validateModelParams(from),[toModel,toName]=Relationship.#validateModelParams(to),null===fromName&&(fromName=Relationship.#createName(type,toModel.name)),null===toName&&(toName=Relationship.#createReverseName(type,fromModel.name)),[fromModel,fromName,toModel,toName]}}class Serializer{#name;#attributes;#methods;constructor({name,attributes=[],methods={}}){this.#name=validateName("Serializer",name),this.#attributes=new Map,this.#methods=new Map,attributes.forEach(attributeName=>{var[attributeValue,attributeName]="string"==typeof attributeName?[attributeName,attributeName]:attributeName;this.#attributes.set(Serializer.#validateAttribute(attributeName,[...this.#attributes.keys()]),attributeValue)}),Object.entries(methods).forEach(([methodName,methodBody])=>{this.addMethod(methodName,methodBody)})}addMethod(methodName,method){method=Serializer.#validateFunction(methodName,method,[...this.#methods.keys()]);this.#methods.set(methodName,method)}serialize(object,options){const serialized={};return this.#attributes.forEach((value,attributeName)=>{value=this.#methods.has(value)?this.#methods.get(value)(object,options):object[value];void 0!==value&&(serialized[attributeName]=value)}),serialized}serializeArray(objects,options){return objects.map(object=>this.serialize(object,options))}serializeRecordSet(objects,options,keyMapFn){const serialized={};return objects.forEach((value,mappedKey)=>{mappedKey=keyMapFn(mappedKey,value);void 0!==mappedKey&&(serialized[mappedKey]=this.serialize(value,options))}),serialized}get name(){return this.#name}static#validateAttribute(attributeName,restrictedNames){if("string"!=typeof attributeName)throw new TypeError(`Attribute ${attributeName} is not a string.`);if(restrictedNames.includes(attributeName))throw new DuplicationError(`Attribute ${attributeName} already exists.`);return attributeName}static#validateFunction(callbackName,callback,restrictedNames){if("function"!=typeof callback)throw new TypeError(`Method ${callbackName} is not a function.`);if(restrictedNames.includes(callbackName))throw new DuplicationError(`Method ${callbackName} already exists.`);return callback}}const{$addRelationshipAsField:schema_$addRelationshipAsField,$addRelationshipAsProperty:schema_$addRelationshipAsProperty,$handleExperimentalAPIMessage:schema_$handleExperimentalAPIMessage,$key:schema_$key,$keyType:schema_$keyType,$instances:schema_$instances}=symbols;class Schema extends external_events_default(){#name;#models;#serializers;static defaultConfig={experimentalAPIMessages:"warn"};static config={...Schema.defaultConfig};static#schemas=new Map;constructor({name,models=[],relationships=[],serializers=[],config={}}={}){super(),this.#name=validateName("Schema",name),this.#models=new Map,this.#serializers=new Map,Schema.#parseConfig(config),Schema.#schemas.set(this.#name,this),models.forEach(model=>this.createModel(model)),relationships.forEach(relationship=>this.createRelationship(relationship)),serializers.forEach(serializer=>this.createSerializer(serializer));const schemaData={models:Object.fromEntries([...this.#models.entries()]),serializers:Object.fromEntries([...this.#serializers.entries()])};models.forEach(model=>{const modelRecord=this.getModel(model.name),cachedProperties=model.cacheProperties||[];model.lazyProperties&&Object.entries(model.lazyProperties).forEach(([propertyName,propertyInitializer])=>{modelRecord.addProperty(propertyName,propertyInitializer(schemaData),cachedProperties.includes(propertyName))}),model.lazyMethods&&Object.entries(model.lazyMethods).forEach(([methodName,methodInitializer])=>{modelRecord.addMethod(methodName,methodInitializer(schemaData))})}),serializers.forEach(serializer=>{const serializerRecord=this.getSerializer(serializer.name);serializer.lazyMethods&&Object.entries(serializer.lazyMethods).forEach(([methodName,methodInitializer])=>{serializerRecord.addMethod(methodName,methodInitializer(schemaData))})})}createModel(modelData){this.emit("beforeCreateModel",{model:modelData,schema:this});const model=Schema.#parseModel(this.#name,modelData,this.#models);return this.#models.set(model.name,model),model.on("change",({type,...eventData})=>{this.emit("change",{type:"model"+capitalize(type),...eventData,schema:this})}),this.emit("modelCreated",{model:model,schema:this}),this.emit("change",{type:"modelCreated",model:model,schema:this}),model}getModel(name){return this.#models.get(name)}removeModel(name){var model=this.getModel(name);if(this.emit("beforeRemoveModel",{model:model,schema:this}),!this.#models.has(name))throw new ReferenceError(`Model ${name} does not exist in schema ${this.#name}.`);this.#models.delete(name),Model[schema_$instances].delete(name),this.emit("modelRemoved",{model:{name:name},schema:this}),this.emit("change",{type:"modelRemoved",model:model,schema:this})}createRelationship(relationship){this.emit("beforeCreateRelationship",{relationship:relationship,schema:this});relationship=Schema.#applyRelationship(this.#name,relationship,this.#models);return this.emit("relationshipCreated",{relationship:relationship,schema:this}),this.emit("change",{type:"relationshipCreated",relationship:relationship,schema:this}),relationship}createSerializer(serializer){this.emit("beforeCreateSerializer",{serializer:serializer,schema:this});serializer=Schema.#parseSerializer(this.#name,serializer,this.#serializers);return this.#serializers.set(serializer.name,serializer),this.emit("serializerCreated",{serializer:serializer,schema:this}),this.emit("change",{type:"serializerCreated",serializer:serializer,schema:this}),serializer}getSerializer(name){return this.#serializers.get(name)}get name(){return this.#name}get models(){return this.#models}static create(schemaData){return new Schema(schemaData)}static get(name){return Schema.#schemas.get(name)}get(pathName){this.emit("beforeGet",{pathName:pathName,schema:this});const[modelName,recordKey,...rest]=pathName.split("."),model=this.getModel(modelName);if(!model)throw new ReferenceError(`Model ${modelName} does not exist in schema ${this.#name}.`);if(void 0===recordKey)return model;var result=model[schema_$key][schema_$keyType],result=model.records.get("string"===result?recordKey:Number.parseInt(recordKey));if(!rest.length)return result;if(!result)throw new ReferenceError(`Record ${recordKey} does not exist in model ${modelName}.`);result=rest.reduce((acc,key)=>acc[key],result);return this.emit("got",{pathName:pathName,result:result,schema:this}),result}static[schema_$handleExperimentalAPIMessage](message){var experimentalAPIMessages=Schema.config["experimentalAPIMessages"];if("warn"===experimentalAPIMessages)console.warn(message);else if("error"===experimentalAPIMessages)throw new ExperimentalAPIUsageError(message)}static#parseModel(schemaName,modelData,models){return validateObjectWithUniqueName({objectType:"Model",parentType:"Schema",parentName:schemaName},modelData,[...models.keys()]),new Model(modelData)}static#applyRelationship(relationship,toModelName,models){const{from,to,type}=toModelName;[from,to].forEach(model=>{if(!["string","object"].includes(typeof model))throw new TypeError(`Invalid relationship model: ${model}.`)});var fromModelName="string"==typeof from?from:from.model,toModelName="string"==typeof to?to:to.model;const fromModel=models.get(fromModelName),toModel=models.get(toModelName);if(!fromModel)throw new ReferenceError(`Model ${fromModelName} not found in schema ${relationship} when attempting to create a relationship.`);if(!toModel)throw new ReferenceError(`Model ${toModelName} not found in schema ${relationship} when attempting to create a relationship.`);relationship=new Relationship({from:from,to:to,type:type});return fromModel[schema_$addRelationshipAsField](relationship),toModel[schema_$addRelationshipAsProperty](relationship),relationship}static#parseSerializer(schemaName,serializerData,serializers){return validateObjectWithUniqueName({objectType:"Serializer",parentType:"Schema",parentName:schemaName},serializerData,[...serializers.keys()]),new Serializer(serializerData)}static#parseConfig(config={}){config&&["experimentalAPIMessages"].forEach(key=>{void 0!==config[key]&&["warn","error","off"].includes(config[key])&&(Schema.config[key]=config[key])})}}const src=Schema;return __webpack_exports__})()}); | ||
!function(root,factory){"object"==typeof exports&&"object"==typeof module?module.exports=factory():"function"==typeof define&&define.amd?define([],factory):"object"==typeof exports?exports["@jsiqle/core"]=factory():root["@jsiqle/core"]=factory()}(global,function(){return(()=>{"use strict";var __webpack_require__={n:module=>{var getter=module&&module.__esModule?()=>module.default:()=>module;return __webpack_require__.d(getter,{a:getter}),getter},d:(exports,definition)=>{for(var key in definition)__webpack_require__.o(definition,key)&&!__webpack_require__.o(exports,key)&&Object.defineProperty(exports,key,{enumerable:!0,get:definition[key]})},o:(obj,prop)=>Object.prototype.hasOwnProperty.call(obj,prop),r:exports=>{"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(exports,"__esModule",{value:!0})}},__webpack_exports__={};__webpack_require__.r(__webpack_exports__),__webpack_require__.d(__webpack_exports__,{default:()=>src});var isUndefined=require("events"),external_events_default=__webpack_require__.n(isUndefined),symbols=["fields","key","keyType","properties","cachedProperties","methods","scopes","relationships","relationshipField","validators","recordModel","recordValue","wrappedRecordValue","recordHandler","recordTag","defaultValue","addScope","addRelationshipAsField","addRelationshipAsProperty","getField","getProperty","removeScope","instances","isRecord","groupTag","get","handleExperimentalAPIMessage"].reduce((acc,curr)=>(acc["$"+curr]=Symbol.for(curr),acc),{});class NameError extends Error{constructor(message){super(message),this.name="NameError"}}class ValidationError extends Error{constructor(message){super(message),this.name="ValidationError"}}class DuplicationError extends Error{constructor(message){super(message),this.name="DuplicationError"}}class DefaultValueError extends Error{constructor(message){super(message),this.name="DefaultValueError"}}class ExperimentalAPIUsageError extends Error{constructor(message){super(message),this.name="ExperimentalAPIUsageError"}}class Validator{static unique(field){return(value,data)=>data.every(item=>item[field]!==value[field])}static length(field,[min,max]){return value=>value[field].length>=min&&value[field].length<=max}static minLength(field,min){return value=>value[field].length>=min}static maxLength(field,max){return value=>value[field].length<=max}static range(field,[min,max]){return value=>value[field]>=min&&value[field]<=max}static min(field,min){return value=>value[field]>=min}static max(field,max){return value=>value[field]<=max}static integer(field){return value=>Number.isInteger(value[field])}static regex(field,regex){return value=>regex.test(value[field])}static uniqueValues(field){return value=>new Set(value[field]).size===value[field].length}static sortedAscending(field){return value=>value[field].every((item,index)=>0===index||item>=value[field][index-1])}static sortedDescending(field){return value=>value[field].every((item,index)=>0===index||item<=value[field][index-1])}static custom(field,fn){return(value,data)=>fn(value[field],data.map(item=>item[field]))}}const restrictedNames={Model:["toString","toObject","toJSON"],Field:["toString","toObject","toJSON"],Property:["toString","toObject","toJSON"],Method:["toString","toObject","toJSON"],Relationship:["toString","toObject","toJSON"]},validateName=(objectType,name)=>{var[isValid,message]=((name,restrictedNames=[])=>"string"!=typeof name?[!1,"must be a string"]:name?/^\d/.test(name)?[!1,"cannot start with a number"]:restrictedNames.includes(name)?[!1,"is reserved"]:[/^\w+$/.test(name),"must contain only alphanumeric characters, numbers or underscores"]:[!1,"is required"])(name,restrictedNames[objectType]);if(!isValid)throw new NameError(objectType+` name ${message}.`);return name},capitalize=([first,...rest])=>first.toUpperCase()+rest.join(""),reverseCapitalize=([first,...rest])=>first.toLowerCase()+rest.join(""),deepClone=obj=>{if(null===obj)return null;if(obj instanceof Date)return new Date(obj);let clone=Object.assign({},obj);return Object.entries(clone).forEach(([key,value])=>clone[key]="object"==typeof obj[key]?deepClone(value):value),Array.isArray(obj)?(clone.length=obj.length,Array.from(clone)):clone},validateObjectWithUniqueName=({objectType,parentType,parentName},obj,collection)=>{if(!(obj=>obj&&"object"==typeof obj)(obj))throw new TypeError(objectType+` ${obj} is not an object.`);if(((collection,item)=>collection.includes(item))(collection,obj.name))throw new DuplicationError(`${parentType} ${parentName} already has a ${objectType.toLowerCase()} named ${obj.name}.`);return!0};var isBoolean=val=>"boolean"==typeof val,isNumber=val=>"number"==typeof val&&val==val,isString=val=>"string"==typeof val,isDate=val=>val instanceof Date,and=(...types)=>val=>types.every(type=>type(val));const or=(...types)=>val=>types.some(type=>type(val));var isPositive=val=>0<=val;const isArrayOf=type=>val=>Array.isArray(val)&&val.every(type);var standardTypes=shape=>{const props=Object.keys(shape);return val=>{return null!=val&&"object"==typeof val&&(0===props.length||Object.keys(val).length===props.length&&props.every(prop=>shape[prop](val[prop])))}},isObjectOf=type=>val=>null!=val&&"object"==typeof val&&Object.keys(val).every(prop=>type(val[prop])),isNull=val=>null===val,isUndefined=val=>void 0===val;const isNil=or(isNull,isUndefined);const types={bool:isBoolean,number:isNumber,positiveNumber:and(isNumber,isPositive),string:isString,date:isDate,stringOrNumber:or(isString,isNumber),numberOrString:or(isString,isNumber),enum:(...values)=>val=>values.includes(val),boolArray:isArrayOf(isBoolean),numberArray:isArrayOf(isNumber),stringArray:isArrayOf(isString),dateArray:isArrayOf(isDate),oneOf:or,arrayOf:isArrayOf,oneOrArrayOf:type=>val=>or(isArrayOf(type),type)(val),object:standardTypes,objectOf:isObjectOf,optional:type=>val=>or(isNil,type)(val),null:isNull,undefined:isUndefined,nil:isNil};standardTypes={boolean:{type:isBoolean,defaultValue:!1},number:{type:isNumber,defaultValue:0},positiveNumber:{type:and(isNumber,isPositive),defaultValue:0},string:{type:isString,defaultValue:""},date:{type:isDate,defaultValue:new Date},stringOrNumber:{type:or(isString,isNumber),defaultValue:""},numberOrString:{type:or(isString,isNumber),defaultValue:0},booleanArray:{type:isArrayOf(isBoolean),defaultValue:[]},numberArray:{type:isArrayOf(isNumber),defaultValue:[]},stringArray:{type:isArrayOf(isString),defaultValue:[]},dateArray:{type:isArrayOf(isDate),defaultValue:[]},object:{type:standardTypes({}),defaultValue:{}},booleanObject:{type:isObjectOf(isBoolean),defaultValue:{a:!0}},numberObject:{type:isObjectOf(isNumber),defaultValue:{}},stringObject:{type:isObjectOf(isString),defaultValue:{}},dateObject:{type:isObjectOf(isDate),defaultValue:{}},objectArray:{type:isArrayOf(standardTypes({})),defaultValue:[]}};const key=and(isString,val=>0!==val.trim().length),{$defaultValue,$validators}=symbols;class Field{#name;#defaultValue;#required;#type;#validators;constructor({name,type,required=!1,defaultValue=null,validators={}}){this.#name=validateName("Field",name),this.#required=Field.#validateRequired(required),this.#type=Field.#validateType(type,required),this.#defaultValue=Field.#validateDefaultValue(defaultValue,this.#type,this.#required),this.#validators=new Map,Object.entries(validators).forEach(([validatorName,validator])=>{this.addValidator(validatorName,validator)})}addValidator(validatorName,validator){this.#validators.set(...Field.#parseFieldValidator(this.#name,validatorName,validator))}get name(){return this.#name}get required(){return this.#required}typeCheck(value){return this.#type(value)}get[$defaultValue](){return this.#defaultValue}get[$validators](){return this.#validators}static#validateType(type,required){if("function"!=typeof type)throw new TypeError("Field type must be a function.");return required?type:types.optional(type)}static#validateRequired(required){if("boolean"!=typeof required)throw new TypeError("Field required must be a boolean.");return required}static#validateDefaultValue(defaultValue,type,required){if(required&&types.nil(defaultValue))throw new ValidationError("Default value cannot be null or undefined.");if(!type(defaultValue))throw new ValidationError("Default value must be valid.");return defaultValue}static#parseFieldValidator(fieldName,validatorName,validator){if(void 0!==Validator[validatorName])return[""+fieldName+capitalize(validatorName),Validator[validatorName](fieldName,validator)];if("function"!=typeof validator)throw new TypeError(`Validator ${validatorName} is not defined.`);return[""+fieldName+capitalize(validatorName),Validator.custom(fieldName,validator)]}}Object.entries(standardTypes).forEach(([typeName,standardType])=>{const{type,defaultValue:typeDefaultValue}=standardType;Field[typeName]=options=>"string"==typeof options?new Field({name:options,type:type}):new Field({...options,type:type}),Field[typeName+"Required"]=options=>{if("string"==typeof options)return new Field({name:options,type:type,required:!0,defaultValue:typeDefaultValue});var defaultValue=options.defaultValue||typeDefaultValue;return new Field({...options,type:type,required:!0,defaultValue:defaultValue})}}),Field.enum=({name,values})=>new Field({name:name,type:types.enum(...values)}),Field.enumRequired=({name,values,defaultValue=values[0]})=>new Field({name:name,type:types.enum(...values),required:!0,defaultValue:defaultValue}),Field.auto=autoField=>{autoField="string"==typeof autoField?autoField:autoField.name;const generator=function*(){let i=0;for(;;)yield i++}();let currentValue=0;autoField=new Field({name:autoField,type:value=>value===currentValue,required:!0,defaultValue:currentValue});return Object.defineProperty(autoField,$defaultValue,{get(){var value=generator.next().value;return currentValue=value}}),autoField};const{$recordValue,$wrappedRecordValue,$recordHandler,$recordModel,$recordTag,$cachedProperties,$key}=symbols;class Record{#recordValue;#recordHandler;#proxiedRecord;#cachedProperties;constructor(value,handler){return this.#recordValue=value,this.#recordHandler=handler,this.#cachedProperties=new Map,this.#proxiedRecord=new Proxy(this,this.#recordHandler),this.#proxiedRecord}get[$cachedProperties](){return this.#cachedProperties}get[$recordHandler](){return this.#recordHandler}get[$recordValue](){return this.#recordValue}get[$wrappedRecordValue](){return this.#proxiedRecord}get[$recordModel](){return this.#recordHandler.model}get[$recordTag](){var model=this[$recordModel],key=model[$key].name;return model.name+"#"+this[$recordValue][key]}get[Symbol.toStringTag](){return this[$recordTag]}}const record=Record,partial_$recordTag=symbols["$recordTag"];class PartialRecord{#tag;constructor(value,tag){Object.keys(value).forEach(key=>{this[key]=value[key]}),this.#tag=tag}get[partial_$recordTag](){return this.#tag}get[Symbol.toStringTag](){return this[partial_$recordTag]}toObject(){return{...this}}toJSON(){return this.toObject()}}const fragment_$recordTag=symbols["$recordTag"];class RecordFragment extends Array{#tag;constructor(values,tag){super(),values.forEach(value=>{this.push(value)}),this.#tag=tag}get[fragment_$recordTag](){return this.#tag}get[Symbol.toStringTag](){return this[fragment_$recordTag]}toObject(){return[...this]}toJSON(){return this.toObject()}}const{$recordModel:set_$recordModel,$recordTag:set_$recordTag,$scopes,$addScope,$removeScope,$isRecord,$key:set_$key}=symbols;class RecordSet extends Map{#frozen;#scopes;constructor({iterable=[],copyScopesFrom=null}={}){super();for(var[key,value]of iterable)this.set(key,value);this.#scopes=new Map,copyScopesFrom&&this.#copyScopes(copyScopesFrom),this.#frozen=!1}freeze(){return this.#frozen=!0,this}set(key,value){if(this.#frozen)throw new TypeError("Cannot modify a frozen RecordSet.");return super.set(key,value),this}delete(key){if(this.#frozen)throw new TypeError("Cannot modify a frozen RecordSet.");return super.delete(key)}clear(){if(this.#frozen)throw new TypeError("Cannot modify a frozen RecordSet.");super.clear()}map(callbackFn){return[...this.entries()].reduce((newMap,[key,value])=>(newMap[key]=callbackFn(value,key,this),newMap),{})}flatMap(callbackFn){return[...this.entries()].map(([key,value])=>callbackFn(value,key,this))}reduce(callbackFn,initialValue){return[...this.entries()].reduce((acc,[key,value])=>callbackFn(acc,value,key,this),initialValue)}filter(callbackFn){return[...this.entries()].reduce((newMap,[key,value])=>(callbackFn(value,key,this)&&newMap.set(key,value),newMap),new RecordSet({copyScopesFrom:this})).freeze()}flatFilter(callbackFn){return[...this.entries()].reduce((arr,[key,value])=>(callbackFn(value,key,this)&&arr.push(value),arr),[])}find(callbackFn){var match=[...this.entries()].find(([key,value])=>callbackFn(value,key,this));if(match)return match[1]}findKey(callbackFn){var match=[...this.entries()].find(([key,value])=>callbackFn(value,key,this));if(match)return match[0]}only(...keys){return new RecordSet({iterable:keys.reduce((itr,key)=>(this.has(key)&&itr.push([key,this.get(key)]),itr),[]),copyScopesFrom:this}).freeze()}except(...keys){return new RecordSet({iterable:[...this.entries()].filter(([key])=>!keys.includes(key)),copyScopesFrom:this}).freeze()}sort(comparatorFn){var sorted=[...this.entries()].sort(([key1,value1],[key2,value2])=>comparatorFn(value1,value2,key1,key2));return new RecordSet({iterable:sorted,copyScopesFrom:this}).freeze()}every(callbackFn){return 0===this.size||[...this.entries()].every(([key,value])=>callbackFn(value,key,this))}some(callbackFn){return 0!==this.size&&[...this.entries()].some(([key,value])=>callbackFn(value,key,this))}select(...keys){return new RecordSet({iterable:[...this.entries()].map(([key,value])=>{const obj={};return keys.forEach(key=>obj[key]=value[key]),[key,new PartialRecord(obj,value[set_$recordTag])]}),copyScopesFrom:this}).freeze()}flatSelect(...keys){return[...this.values()].map(value=>keys.reduce((obj,key)=>({...obj,[key]:value[key]}),{}))}pluck(...keys){return new RecordSet({iterable:[...this.entries()].map(([key,value])=>{var values=keys.map(key=>value[key]);return[key,new RecordFragment(values,value[set_$recordTag])]}),copyScopesFrom:this}).freeze()}flatPluck(...keys){const isSingleKey=1===keys.length;return[...this.values()].map(value=>isSingleKey?value[keys[0]]:keys.map(key=>value[key]))}groupBy(key){const res=new RecordSet({copyScopesFrom:this,iterable:[]});for(var[recordKey,value]of this.entries()){let keyValue=value[key];void 0!==keyValue&&null!==keyValue&&keyValue[$isRecord]&&(keyValue=value[key][set_$key]),res.has(keyValue)||res.set(keyValue,new RecordGroup({copyScopesFrom:this,iterable:[],groupName:keyValue})),res.get(keyValue).set(recordKey,value)}for(const value of res.values())value.freeze();return res.freeze()}duplicate(){return new RecordSet({iterable:[...this.entries()],copyScopesFrom:this}).freeze()}merge(...recordSets){const res=new Map([...this.entries()]);for(const recordSet of recordSets)for(var[key,value]of recordSet.entries()){if(res.has(key))throw new DuplicationError(`Key ${key} already exists in the record set.`);res.set(key,value)}return new RecordSet({iterable:[...res.entries()],copyScopesFrom:this}).freeze()}append(...records){const res=new RecordSet({iterable:[...this.entries()],copyScopesFrom:this});for(const record of records)res.set(record[set_$key],record);return res.freeze()}where(callbackFn){return this.filter(callbackFn)}whereNot(callbackFn){return this.filter((value,key,map)=>!callbackFn(value,key,map))}*flatBatchIterator(batchSize){let batch=[];for(var[,value]of this)batch.push(value),batch.length===batchSize&&(yield batch,batch=[]);batch.length&&(yield batch)}*batchIterator(batchSize){let batch=[];for(var[key,value]of this)batch.push([key,value]),batch.length===batchSize&&(yield new RecordSet({copyScopesFrom:this,iterable:batch}).freeze(),batch=[]);batch.length&&(yield new RecordSet({copyScopesFrom:this,iterable:batch}).freeze())}limit(n){let records=[];for(var[key,value]of this)if(records.push([key,value]),records.length===n)break;return new RecordSet({iterable:records,copyScopesFrom:this}).freeze()}offset(n){let counter=0,records=[];for(var[key,value]of this)counter<n?counter++:records.push([key,value]);return new RecordSet({iterable:records,copyScopesFrom:this}).freeze()}slice(start,end){return new RecordSet({iterable:[...this.entries()].slice(start,end),copyScopesFrom:this}).freeze()}get first(){for(var[,value]of this)return value}get last(){if(0!==this.size)return[...this.entries()].pop()[1]}get count(){return this.size}get length(){return this.size}toArray(){return[...this.values()]}toFlatArray(){return[...this.values()].map(value=>value instanceof RecordGroup?value.toFlatArray():value.toObject())}toObject(){return[...this.entries()].reduce((obj,[key,value])=>(obj[key]=value,obj),{})}toFlatObject(){return[...this.entries()].reduce((obj,[key,value])=>(obj[key]=value instanceof RecordGroup?value.toFlatArray():value.toObject(),obj),{})}toJSON(){return this.toObject()}get[Symbol.toStringTag](){var records=[...this.values()];try{const firstModel=records[0][set_$recordModel].name;if(((arr,fn)=>{const eql=fn(arr[0]);return arr.every(val=>fn(val)===eql)})(records,value=>value[set_$recordModel].name===firstModel))return firstModel}catch(e){return""}return""}static get[Symbol.species](){return Map}[$addScope](name,scope,sortFn){if(RecordSet.#validateProperty("Scope",name,scope,this.#scopes),sortFn&&RecordSet.#validateFunction("Scope comparator",name,sortFn),this[name]||Object.getOwnPropertyNames(RecordSet.prototype).includes(name))throw new NameError(`Scope name ${name} is already in use.`);this.#scopes.set(name,[scope,sortFn]),Object.defineProperty(this,name,{configurable:!0,get:()=>this.#scopedWhere(name)})}[$removeScope](name){this.#scopes.delete(RecordSet.#validateContains("Scope",name,this.#scopes)),delete this[name]}get[$scopes](){return this.#scopes}#copyScopes(otherRecordSet){otherRecordSet[$scopes].forEach((scope,name)=>{this.#scopes.set(name,scope)})}#scopedWhere(scopeName){const[matcherFn,comparatorFn]=this.#scopes.get(scopeName);let matches=[];for(var[key,value]of this.entries())matcherFn(value,key,this)&&matches.push([key,value]);return comparatorFn&&matches.sort(([key1,value1],[key2,value2])=>comparatorFn(value1,value2,key1,key2)),new RecordSet({iterable:matches,copyScopesFrom:this}).freeze()}static#validateProperty(callbackType,callbackName,callback,callbacks){if("function"!=typeof callback)throw new TypeError(callbackType+` ${callbackName} is not a function.`);if(callbacks.has(callbackName))throw new DuplicationError(callbackType+` ${callbackName} already exists.`);return callback}static#validateFunction(callbackType,callbackName,callback){if("function"!=typeof callback)throw new TypeError(callbackType+` ${callbackName} is not a function.`);return callback}static#validateContains(objectType,objectName,objects){if(!objects.has(objectName))throw new ReferenceError(objectType+` ${objectName} does not exist.`);return objectName}}const set=RecordSet,$groupTag=symbols["$groupTag"];class RecordGroup extends set{#groupName;constructor({iterable=[],copyScopesFrom=null,groupName=""}={}){super({iterable:iterable,copyScopesFrom:copyScopesFrom}),this.#groupName=groupName}get[$groupTag](){return this.#groupName}get[Symbol.toStringTag](){return this[$groupTag]}}const{$fields,$defaultValue:handler_$defaultValue,$key:handler_$key,$keyType,$properties,$cachedProperties:handler_$cachedProperties,$methods,$relationships,$validators:handler_$validators,$recordValue:handler_$recordValue,$wrappedRecordValue:handler_$wrappedRecordValue,$recordModel:handler_$recordModel,$recordTag:handler_$recordTag,$isRecord:handler_$isRecord,$get}=symbols;class RecordHandler{#model;constructor(model){this.#model=model}get model(){return this.#model}createRecord(recordData){if(!recordData)throw new TypeError("Record data cannot be empty.");if("object"!=typeof recordData)throw new TypeError("Record data must be an object.");const modelName=this.#getModelName(),newRecordKey=RecordHandler.#validateNewRecordKey(modelName,this.#getKey(),recordData[this.#getKey().name],this.#model.records),clonedRecord=deepClone(recordData),extraProperties=Object.keys(clonedRecord).filter(property=>!this.#hasField(property)&&!this.#isModelKey(property));0<extraProperties.length&&console.warn(`${modelName} record has extra fields: ${extraProperties.join(", ")}.`);const newRecord=new record({[this.#getKey().name]:newRecordKey,...extraProperties.reduce((obj,property)=>({...obj,[property]:clonedRecord[property]}),{})},this);return this.#getFieldNames().forEach(field=>{this.set(newRecord,field,clonedRecord[field],newRecord,!0)}),this.#getValidators().forEach((validator,validatorName)=>{if(!validator(newRecord,this.#model.records))throw new RangeError(`${modelName} record with key ${newRecordKey} failed validation for ${validatorName}.`)}),[newRecordKey,newRecord]}get(record,property){return this.#hasRelationshipField(property)?this.#getRelationship(record,property):this.#isModelKey(property)||this.#hasField(property)?this.#getFieldValue(record,property):this.#hasProperty(property)?this.#getProperty(record,property):this.#hasMethod(property)?this.#getMethod(record,property):this.#isCallToSerialize(property)?RecordHandler.#recordToObject(record,this.#model,this):this.#isCallToString(property)?()=>this.#getKeyValue(record):this.#isKnownSymbol(property)?this.#getKnownSymbol(record,property):void 0}set(record,property,value,receiver,skipValidation){const recordValue=record[handler_$recordValue],recordKey=this.#getKeyValue(record),otherRecords=this.#model.records.except(recordKey);if(this.#hasProperty(property))throw new TypeError(`${this.#getModelName()} record ${recordKey} cannot set property ${property}.`);if(this.#hasMethod(property))throw new TypeError(`${this.#getModelName()} record ${recordKey} cannot set method ${property}.`);if(this.#hasField(property)){const field=this.#getField(property);RecordHandler.#setRecordField(this.#model.name,record,field,value),field[handler_$validators].forEach((validator,validatorName)=>{if(![null,void 0].includes(recordValue[property])&&!validator(recordValue,otherRecords))throw new RangeError(`${this.#getModelName()} record with key ${recordKey} failed validation for ${validatorName}.`)})}else console.warn(this.#model.name+` record has extra field: ${property}.`),recordValue[property]=value;return skipValidation||this.#getValidators().forEach((validator,validatorName)=>{if(!validator(recordValue,otherRecords))throw new RangeError(`${this.#getModelName()} record with key ${recordKey} failed validation for ${validatorName}.`)}),!0}static#setRecordField(modelName,record,field,recordValue){recordValue=field.required&&types.nil(recordValue)?field[handler_$defaultValue]:recordValue;if(!field.typeCheck(recordValue))throw new TypeError(`${modelName} record has invalid value for field ${field.name}.`);record[handler_$wrappedRecordValue]&&record[handler_$cachedProperties].clear(),record[handler_$recordValue][field.name]=recordValue}static#recordToObject(record,key,handler){const recordValue=record[handler_$recordValue],fields=key[$fields],properties=key[$properties];key=key[handler_$key].name;const object={[key]:recordValue[key]};fields.forEach(field=>{void 0!==recordValue[field.name]&&(object[field.name]=recordValue[field.name])});return({include=[]}={})=>{var result=object;const included=include.map(name=>{const[field,...props]=name.split(".");return[field,props.join(".")]});return included.forEach(([includedField,props])=>{if(object[includedField])if(Array.isArray(object[includedField])){const records=handler.get(record,includedField);object[includedField]=records.map(record=>record.toObject({include:[props]}))}else object[includedField]=handler.get(record,includedField).toObject({include:[props]});else properties.has(includedField)&&(object[includedField]=handler.get(record,includedField))}),result}}static#validateNewRecordKey=(modelName,modelKey,recordKey,records)=>{let newRecordKey=recordKey;if("string"===modelKey[$keyType]&&!modelKey.typeCheck(newRecordKey))throw new TypeError(`${modelName} record has invalid value for key ${modelKey.name}.`);if("auto"===modelKey[$keyType]&&(newRecordKey=modelKey[handler_$defaultValue]),records.has(newRecordKey))throw new DuplicationError(`${modelName} record with key ${newRecordKey} already exists.`);return newRecordKey};#getModelName(){return this.#model.name}#getFieldNames(){return[...this.#model[$fields].keys()]}#getValidators(){return this.#model[handler_$validators]}#isModelKey(property){return this.#model[handler_$key].name===property}#getKey(){return this.#model[handler_$key]}#getKeyValue(record){return record[handler_$recordValue][this.#model[handler_$key].name]}#hasField(property){return this.#model[$fields].has(property)}#getField(property){return this.#model[$fields].get(property)}#getFieldValue(record,property){return record[handler_$recordValue][property]}#hasProperty(property){return this.#model[$properties].has(property)}#getProperty(record,property){if(this.#model[handler_$cachedProperties].has(property)){if(record[handler_$cachedProperties]&&record[handler_$cachedProperties].has(property))return record[handler_$cachedProperties].get(property);var value=this.#model[$properties].get(property)(record[handler_$wrappedRecordValue]);return record[handler_$cachedProperties].set(property,value),value}return this.#model[$properties].get(property)(record[handler_$wrappedRecordValue])}#hasMethod(method){return this.#model[$methods].has(method)}#getMethod(record,method){const methodFn=this.#model[$methods].get(method);return(...args)=>methodFn(record[handler_$wrappedRecordValue],...args)}#hasRelationshipField(property){return!!this.#hasField(property)&&this.#model[$relationships].has(property+"."+property)}#getRelationship(record,property){return this.#model[$relationships].get(property+"."+property)[$get](this.#getModelName(),property,record[handler_$recordValue])}#isCallToSerialize(property){return"toObject"===property||"toJSON"===property}#isCallToString(property){return"toString"===property}#isKnownSymbol(property){return[handler_$recordModel,handler_$recordTag,handler_$recordValue,handler_$isRecord,handler_$key].includes(property)}#getKnownSymbol(record,property){return property===handler_$isRecord||(property===handler_$key&&this.#getKey(),record[property])}}const handler=RecordHandler,{$fields:model_$fields,$defaultValue:model_$defaultValue,$key:model_$key,$keyType:model_$keyType,$properties:model_$properties,$cachedProperties:model_$cachedProperties,$methods:model_$methods,$scopes:model_$scopes,$relationships:model_$relationships,$validators:model_$validators,$recordHandler:model_$recordHandler,$addScope:model_$addScope,$addRelationshipAsField,$addRelationshipAsProperty,$getField,$getProperty,$removeScope:model_$removeScope,$instances,$handleExperimentalAPIMessage}=symbols,allStandardTypes=[...Object.keys(standardTypes),...Object.keys(standardTypes).map(type=>type+"Required"),"enum","enumRequired","auto"];class Model extends external_events_default(){#records;#recordHandler;#fields;#key;#properties;#methods;#relationships;#validators;#updatingField=!1;#cachedProperties;static#instances=new Map;constructor({name,fields=[],key="id",properties={},methods={},scopes={},validators={},cacheProperties=[]}={}){if(super(),this.name=validateName("Model",name),Model.#instances.has(name))throw new DuplicationError(`A model named ${name} already exists.`);this.#records=new set,this.#recordHandler=new handler(this),this.#key=Model.#parseKey(this.name,key),this.#fields=new Map,this.#properties=new Map,this.#methods=new Map,this.#relationships=new Map,this.#validators=new Map,this.#cachedProperties=new Set,fields.forEach(field=>this.addField(field)),Object.entries(properties).forEach(([propertyName,property])=>{this.addProperty(propertyName,property,cacheProperties.includes(propertyName))}),Object.entries(methods).forEach(([methodName,method])=>{this.addMethod(methodName,method)}),Object.entries(scopes).forEach(([scopeName,scope])=>{this.addScope(scopeName,...Model.#parseScope(scope))}),Object.entries(validators).forEach(([validatorName,validator])=>{this.addValidator(validatorName,validator)}),Model.#instances.set(this.name,this)}addField(fieldOptions,retrofill){this.#updatingField||this.emit("beforeAddField",{field:fieldOptions,model:this});var field=Model.#parseField(this.name,fieldOptions,[...this.#fields.keys(),this.#key.name,...this.#properties.keys(),...this.#methods.keys()]);return this.#fields.set(fieldOptions.name,field),this.#updatingField||this.emit("fieldAdded",{field:field,model:this}),this.emit("beforeRetrofillField",{field:field,retrofill:retrofill,model:this}),Model.#applyFieldRetrofill(field,this.#records,retrofill),this.emit("fieldRetrofilled",{field:field,retrofill:retrofill,model:this}),this.#updatingField||this.emit("change",{type:"fieldAdded",field:field,model:this}),field}removeField(name){if(!Model.#validateContains(this.name,"Field",name,this.#fields))return!1;var field=this.#fields.get(name);return this.#updatingField||this.emit("beforeRemoveField",{field:field,model:this}),this.#fields.delete(name),this.#updatingField||(this.emit("fieldRemoved",{field:{name:name},model:this}),this.emit("change",{type:"fieldRemoved",field:field,model:this})),!0}updateField(name,field,newField){if(field.name!==name)throw new NameError(`Field name ${field.name} does not match ${name}.`);if(!Model.#validateContains(this.name,"Field",name,this.#fields))throw new ReferenceError(`Field ${name} does not exist.`);var prevField=this.#fields.get(name);this.#updatingField=!0,this.emit("beforeUpdateField",{prevField:prevField,field:field,model:this}),this.removeField(name);newField=this.addField(field,newField);this.emit("fieldUpdated",{field:newField,model:this}),this.#updatingField=!1,this.emit("change",{type:"fieldUpdated",field:newField,model:this})}addProperty(name,property,cache=!1){this.emit("beforeAddProperty",{property:{name:name,body:property},model:this});var propertyName=validateName("Property",name);this.#properties.set(propertyName,Model.#validateFunction("Property",name,property,[...this.#fields.keys(),this.#key.name,...this.#properties.keys(),...this.#methods.keys()])),cache&&this.#cachedProperties.add(propertyName),this.emit("propertyAdded",{property:{name:propertyName,body:property},model:this}),this.emit("change",{type:"propertyAdded",property:{name:propertyName,body:property},model:this})}removeProperty(name){if(!Model.#validateContains(this.name,"Property",name,this.#properties))return!1;var property=this.#properties.get(name);return this.emit("beforeRemoveProperty",{property:{name:name,body:property},model:this}),this.#properties.delete(name),this.#cachedProperties.has(name)&&this.#cachedProperties.delete(name),this.emit("propertyRemoved",{property:{name:name},model:this}),this.emit("change",{type:"propertyRemoved",property:{name:name,body:property},model:this}),!0}addMethod(name,method){this.emit("beforeAddMethod",{method:{name:name,body:method},model:this});var methodName=validateName("Method",name);this.#methods.set(methodName,Model.#validateFunction("Method",name,method,[...this.#fields.keys(),this.#key.name,...this.#properties.keys(),...this.#methods.keys()])),this.emit("methodAdded",{method:{name:methodName,body:method},model:this}),this.emit("change",{type:"methodAdded",method:{name:methodName,body:method},model:this})}removeMethod(name){if(!Model.#validateContains(this.name,"Method",name,this.#methods))return!1;var method=this.#methods.get(name);return this.emit("beforeRemoveMethod",{method:{name:name,body:method},model:this}),this.#methods.delete(name),this.emit("methodRemoved",{method:{name:name},model:this}),this.emit("change",{type:"methodRemoved",method:{name:name,body:method},model:this}),!0}addScope(scopeName,scope,sortFn){this.emit("beforeAddScope",{scope:{name:scopeName,body:scope},model:this});scopeName=validateName("Scope",scopeName);this.#records[model_$addScope](scopeName,scope,sortFn),this.emit("scopeAdded",{scope:{name:scopeName,body:scope},model:this}),this.emit("change",{type:"scopeAdded",scope:{name:scopeName,body:scope},model:this})}removeScope(name){if(!Model.#validateContains(this.name,"Scope",name,this.#records[model_$scopes]))return!1;var scope=this.#records[model_$scopes].get(name);return this.emit("beforeRemoveScope",{scope:{name:name,body:scope},model:this}),this.#records[model_$removeScope](name),this.emit("scopeRemoved",{scope:{name:name},model:this}),this.emit("change",{type:"scopeRemoved",scope:{name:name,body:scope},model:this}),!0}addValidator(name,validator){this.emit("beforeAddValidator",{validator:{name:name,body:validator},model:this}),this.#validators.set(name,Model.#validateFunction("Validator",name,validator,[...this.#validators.keys()])),this.emit("validatorAdded",{validator:{name:name,body:validator},model:this}),this.emit("change",{type:"validatorAdded",validator:{name:name,body:validator},model:this})}removeValidator(name){if(!Model.#validateContains(this.name,"Validator",name,this.#validators))return!1;var validator=this.#validators.get(name);return this.emit("beforeRemoveValidator",{validator:{name:name,body:validator},model:this}),this.#validators.delete(name),this.emit("validatorRemoved",{validator:{name:name},model:this}),this.emit("change",{type:"validatorRemoved",validator:{name:name,body:validator},model:this}),!0}createRecord(newRecord){this.emit("beforeCreateRecord",{record:newRecord,model:this});var[newRecordKey,newRecord]=this.#recordHandler.createRecord(newRecord);return this.#records.set(newRecordKey,newRecord),this.emit("recordCreated",{newRecord:newRecord,model:this}),newRecord}removeRecord(recordKey){if(!this.#records.has(recordKey))return console.warn(`Record ${recordKey} does not exist.`),!1;var record=this.#records.get(recordKey);return this.emit("beforeRemoveRecord",{record:record,model:this}),this.#records.delete(recordKey),this.emit("recordRemoved",{record:{[this.#key.name]:recordKey},model:this}),!0}updateRecord(recordKey,record){if("object"!=typeof record)throw new TypeError("Record data must be an object.");if(!this.#records.has(recordKey))throw new ReferenceError(`Record ${recordKey} does not exist.`);const oldRecord=this.#records.get(recordKey);return this.emit("beforeUpdateRecord",{record:oldRecord,newRecord:{[this.#key.name]:recordKey,...record},model:this}),Object.entries(record).forEach(([fieldName,fieldValue])=>{oldRecord[fieldName]=fieldValue}),this.emit("recordUpdated",{record:oldRecord,model:this}),oldRecord}get records(){return this.#records}static get[$instances](){return Model.#instances}get[model_$recordHandler](){return this.#recordHandler}get[model_$fields](){return this.#fields}get[model_$key](){return this.#key}get[model_$properties](){return this.#properties}get[model_$cachedProperties](){return this.#cachedProperties}get[model_$methods](){return this.#methods}get[model_$relationships](){return this.#relationships}get[model_$validators](){return this.#validators}[$addRelationshipAsField](relationship){var{name,type,fieldName,field}=relationship[$getField](),relationshipName=name+"."+fieldName;if(this.emit("beforeAddRelationship",{relationship:{name:name,type:type},model:this}),[...this.#fields.keys(),this.#key.name,...this.#properties.keys(),...this.#methods.keys()].includes(fieldName))throw new NameError(`Relationship field ${fieldName} is already in use.`);if(this.#relationships.has(relationshipName))throw new NameError(`Relationship ${relationshipName} is already in use.`);this.#fields.set(fieldName,field),this.#relationships.set(relationshipName,relationship),this.emit("relationshipAdded",{relationship:{name:name,type:type},model:this}),this.emit("change",{type:"relationshipAdded",relationship:{relationship:{name:name,type:type},model:this},model:this})}[$addRelationshipAsProperty](relationship){var{name,type,propertyName,property}=relationship[$getProperty](),relationshipName=name+"."+propertyName;if(this.emit("beforeAddRelationship",{relationship:{name:name,type:type},model:this}),[...this.#fields.keys(),this.#key.name,...this.#properties.keys(),...this.#methods.keys()].includes(propertyName))throw new NameError(`Relationship property ${propertyName} is already in use.`);if(this.#relationships.has(relationshipName))throw new NameError(`Relationship ${name} is already in use.`);this.#properties.set(propertyName,property),this.#relationships.set(relationshipName,relationship),this.emit("relationshipAdded",{relationship:{name:name,type:type},model:this}),this.emit("change",{type:"relationshipAdded",relationship:{relationship:{name:name,type:type},model:this},model:this})}static#createKey(options){let name="id",type="string";"string"==typeof options?name=options:"object"==typeof options&&(name=options.name||name,type=options.type||type);let keyField;return"string"===type?(keyField=new Field({name:name,type:key,required:!0,defaultValue:"__emptyKey__"}),Object.defineProperty(keyField,model_$defaultValue,{get(){throw new DefaultValueError(`Key field ${name} does not have a default value.`)}})):"auto"===type&&(keyField=Field.auto(name)),Object.defineProperty(keyField,model_$keyType,{get(){return type}}),keyField}static#parseKey(modelName,key){if("string"!=typeof key&&"object"!=typeof key)throw new TypeError(modelName+` key ${key} is not a string or object.`);if("object"==typeof key&&!key.name)throw new TypeError(modelName+` key ${key} is missing a name.`);if("object"==typeof key&&!["auto","string"].includes(key.type))throw new TypeError(modelName+` key ${key} type must be either "string" or "auto".`);return Model.#createKey(key)}static#parseField(modelName,field,restrictedNames){return validateObjectWithUniqueName({objectType:"Field",parentType:"Model",parentName:modelName},field,restrictedNames),allStandardTypes.includes(field.type)?Field[field.type](field):("function"==typeof field.type&&Schema[$handleExperimentalAPIMessage](`The provided type for ${field.name} is not part of the standard types. Function types are experimental and may go away in a later release.`),new Field(field))}static#parseScope(sorter){if("function"==typeof sorter)return[sorter];if("object"!=typeof sorter)throw new TypeError("The provided scope is not a function or valid object.");var{matcher,sorter}=sorter;if("function"!=typeof matcher)throw new TypeError("The provided matcher for the scope is not a function.");if(sorter&&"function"!=typeof sorter)throw new TypeError("The provided sorter for the scope is not a function.");return[matcher,sorter]}static#validateFunction(callbackType,callbackName,callback,restrictedNames){if("function"!=typeof callback)throw new TypeError(callbackType+` ${callbackName} is not a function.`);if(restrictedNames.includes(callbackName))throw new DuplicationError(callbackType+` ${callbackName} already exists.`);return callback}static#validateContains(modelName,objectType,objectName,objects){return!!objects.has(objectName)||(console.warn(`Model ${modelName} does not contain a ${objectType.toLowerCase()} named ${objectName}.`),!1)}static#applyFieldRetrofill(field,records,retrofill){if(field.required||void 0!==retrofill){const retrofillFunction=void 0!==retrofill?"function"==typeof retrofill?retrofill:()=>retrofill:record=>record[field.name]||field[model_$defaultValue];records.forEach(record=>{record[field.name]=retrofillFunction(record)})}}}const{$key:relationship_$key,$recordValue:relationship_$recordValue,$fields:relationship_$fields,$getField:relationship_$getField,$getProperty:relationship_$getProperty,$get:relationship_$get,$defaultValue:relationship_$defaultValue,$instances:relationship_$instances,$handleExperimentalAPIMessage:relationship_$handleExperimentalAPIMessage}=symbols,relationshipEnum={oneToOne:"oneToOne",oneToMany:"oneToMany",manyToOne:"manyToOne",manyToMany:"manyToMany"};class Relationship{#type;#from;#to;#name;#reverseName;#relationshipField;#relationshipProperty;constructor({from:fromName,to:toModel,type:toName}={}){Schema[relationship_$handleExperimentalAPIMessage]("Relationships are experimental in the current version. There is neither validation of existence in foreign tables nor guarantee that associations work. Please use with caution."),this.#type=Relationship.#validateType(toName);var[fromModel,fromName,toModel,toName]=Relationship.#parseModelsAndNames(fromName,toModel,toName);if(this.#from=fromModel,this.#to=toModel,this.#name=fromName,this.#reverseName=toName,this.#to===this.#from&&Relationship.#isSymmetric(this.#type)&&this.#name===this.#reverseName)throw new RangeError("Relationship cannot be symmetric if the from and to models are the same and no name is provided for either one.");this.#relationshipField=Relationship.#createField(this.#name,this.#type,this.#to[relationship_$key]),this.#relationshipProperty=record=>this.#getAssociatedRecordsReverse(record)}[relationship_$getField](){return{name:this.#name,type:this.#type,fieldName:this.#name,field:this.#relationshipField}}[relationship_$getProperty](){return{name:this.#name,type:this.#type,propertyName:this.#reverseName,property:this.#relationshipProperty}}[relationship_$get](modelName,property,record){return modelName===this.#from.name&&property===this.#name?this.#getAssociatedRecords(record):modelName===this.#to.name&&property===this.#reverseName?(console.warn("Relationship getter called by the receiver model. This might indicate an issue with the library and should be reported."),this.#getAssociatedRecordsReverse(record)):void 0}#getAssociatedRecords(associationValues){if(Relationship.#isToOne(this.#type)){var associationValue=associationValues[this.#name];return this.#to.records.get(associationValue)}associationValues=associationValues[this.#name]||[];return this.#to.records.only(...associationValues)}#getAssociatedRecordsReverse(matcher){const associationValue=matcher[this.#to[relationship_$key].name];matcher=Relationship.#isToOne(this.#type)?associatedRecord=>associatedRecord[relationship_$recordValue][this.#name]===associationValue:associatedRecord=>{var associatedRecordValue=associatedRecord[relationship_$recordValue][this.#name];return![void 0,null].includes(associatedRecordValue)&&associatedRecord[relationship_$recordValue][this.#name].includes(associationValue)};return Relationship.#isFromOne(this.#type)?this.#from.records.find(matcher):this.#from.records.where(matcher)}static#isToOne(type){return[relationshipEnum.oneToOne,relationshipEnum.manyToOne].includes(type)}static#isToMany(type){return[relationshipEnum.oneToMany,relationshipEnum.manyToMany].includes(type)}static#isFromOne(type){return[relationshipEnum.oneToMany,relationshipEnum.oneToOne].includes(type)}static#isFromMany(type){return[relationshipEnum.manyToOne,relationshipEnum.manyToMany].includes(type)}static#isSymmetric(type){return[relationshipEnum.oneToOne,relationshipEnum.manyToMany].includes(type)}static#createField(name,type,foreignField){var isSingleSource=Relationship.#isFromOne(type),relationshipField=Relationship.#isToMany(type),type=relationshipField?types.arrayOf(value=>foreignField.typeCheck(value)):value=>foreignField.typeCheck(value);const validators={};isSingleSource&&!relationshipField&&(validators.unique=!0),relationshipField&&(validators.uniqueValues=!0);relationshipField=new Field({name:name,type:type,required:!1,defaultValue:relationshipField?[]:null,validators:validators});return Object.defineProperty(relationshipField,relationship_$defaultValue,{get(){throw new DefaultValueError("Relationship field does not have a default value.")}}),relationshipField}static#validateType(relationshipType){if(!Object.values(relationshipEnum).includes(relationshipType))throw new TypeError(`Invalid relationship type: ${relationshipType}.`);return relationshipType}static#validateModel(modelName){modelName="string"==typeof modelName?modelName:modelName.model;if(!Model[relationship_$instances].has(modelName))throw new ReferenceError(`Model ${modelName} does not exist.`);return Model[relationship_$instances].get(modelName)}static#createName(type,to){return Relationship.#isToOne(type)?reverseCapitalize(to):Relationship.#isToMany(type)?reverseCapitalize(to)+"Set":void 0}static#createReverseName=(type,from)=>Relationship.#isFromOne(type)?reverseCapitalize(from):Relationship.#isFromMany(type)?reverseCapitalize(from)+"Set":void 0;static#validateModelParams(name){const model=Relationship.#validateModel(name);name="string"==typeof name?null:validateName("Field",name.name);if(null!==name&&model[relationship_$fields].has(name))throw new DuplicationError(`Field ${name} already exists in ${model.name}.`);return[model,name]}static#parseModelsAndNames(from,to,type){let fromModel,fromName,toModel,toName;return[fromModel,fromName]=Relationship.#validateModelParams(from),[toModel,toName]=Relationship.#validateModelParams(to),null===fromName&&(fromName=Relationship.#createName(type,toModel.name)),null===toName&&(toName=Relationship.#createReverseName(type,fromModel.name)),[fromModel,fromName,toModel,toName]}}class Serializer{#name;#attributes;#methods;constructor({name,attributes=[],methods={}}){this.#name=validateName("Serializer",name),this.#attributes=new Map,this.#methods=new Map,attributes.forEach(attributeName=>{var[attributeValue,attributeName]="string"==typeof attributeName?[attributeName,attributeName]:attributeName;this.#attributes.set(Serializer.#validateAttribute(attributeName,[...this.#attributes.keys()]),attributeValue)}),Object.entries(methods).forEach(([methodName,methodBody])=>{this.addMethod(methodName,methodBody)})}addMethod(methodName,method){method=Serializer.#validateFunction(methodName,method,[...this.#methods.keys()]);this.#methods.set(methodName,method)}serialize(object,options){const serialized={};return this.#attributes.forEach((value,attributeName)=>{value=this.#methods.has(value)?this.#methods.get(value)(object,options):object[value];void 0!==value&&(serialized[attributeName]=value)}),serialized}serializeArray(objects,options){return objects.map(object=>this.serialize(object,options))}serializeRecordSet(objects,options,keyMapFn){const serialized={};return objects.forEach((value,mappedKey)=>{mappedKey=keyMapFn(mappedKey,value);void 0!==mappedKey&&(serialized[mappedKey]=this.serialize(value,options))}),serialized}get name(){return this.#name}static#validateAttribute(attributeName,restrictedNames){if("string"!=typeof attributeName)throw new TypeError(`Attribute ${attributeName} is not a string.`);if(restrictedNames.includes(attributeName))throw new DuplicationError(`Attribute ${attributeName} already exists.`);return attributeName}static#validateFunction(callbackName,callback,restrictedNames){if("function"!=typeof callback)throw new TypeError(`Method ${callbackName} is not a function.`);if(restrictedNames.includes(callbackName))throw new DuplicationError(`Method ${callbackName} already exists.`);return callback}}const{$addRelationshipAsField:schema_$addRelationshipAsField,$addRelationshipAsProperty:schema_$addRelationshipAsProperty,$handleExperimentalAPIMessage:schema_$handleExperimentalAPIMessage,$key:schema_$key,$keyType:schema_$keyType,$instances:schema_$instances}=symbols;class Schema extends external_events_default(){#name;#models;#serializers;static defaultConfig={experimentalAPIMessages:"warn"};static config={...Schema.defaultConfig};static#schemas=new Map;constructor({name,models=[],relationships=[],serializers=[],config={}}={}){super(),this.#name=validateName("Schema",name),this.#models=new Map,this.#serializers=new Map,Schema.#parseConfig(config),Schema.#schemas.set(this.#name,this),models.forEach(model=>this.createModel(model)),relationships.forEach(relationship=>this.createRelationship(relationship)),serializers.forEach(serializer=>this.createSerializer(serializer));const schemaData={models:Object.fromEntries([...this.#models.entries()]),serializers:Object.fromEntries([...this.#serializers.entries()])};models.forEach(model=>{const modelRecord=this.getModel(model.name),cachedProperties=model.cacheProperties||[];model.lazyProperties&&Object.entries(model.lazyProperties).forEach(([propertyName,propertyInitializer])=>{modelRecord.addProperty(propertyName,propertyInitializer(schemaData),cachedProperties.includes(propertyName))}),model.lazyMethods&&Object.entries(model.lazyMethods).forEach(([methodName,methodInitializer])=>{modelRecord.addMethod(methodName,methodInitializer(schemaData))})}),serializers.forEach(serializer=>{const serializerRecord=this.getSerializer(serializer.name);serializer.lazyMethods&&Object.entries(serializer.lazyMethods).forEach(([methodName,methodInitializer])=>{serializerRecord.addMethod(methodName,methodInitializer(schemaData))})})}createModel(modelData){this.emit("beforeCreateModel",{model:modelData,schema:this});const model=Schema.#parseModel(this.#name,modelData,this.#models);return this.#models.set(model.name,model),model.on("change",({type,...eventData})=>{this.emit("change",{type:"model"+capitalize(type),...eventData,schema:this})}),this.emit("modelCreated",{model:model,schema:this}),this.emit("change",{type:"modelCreated",model:model,schema:this}),model}getModel(name){return this.#models.get(name)}removeModel(name){var model=this.getModel(name);if(this.emit("beforeRemoveModel",{model:model,schema:this}),!this.#models.has(name))throw new ReferenceError(`Model ${name} does not exist in schema ${this.#name}.`);this.#models.delete(name),Model[schema_$instances].delete(name),this.emit("modelRemoved",{model:{name:name},schema:this}),this.emit("change",{type:"modelRemoved",model:model,schema:this})}createRelationship(relationship){this.emit("beforeCreateRelationship",{relationship:relationship,schema:this});relationship=Schema.#applyRelationship(this.#name,relationship,this.#models);return this.emit("relationshipCreated",{relationship:relationship,schema:this}),this.emit("change",{type:"relationshipCreated",relationship:relationship,schema:this}),relationship}createSerializer(serializer){this.emit("beforeCreateSerializer",{serializer:serializer,schema:this});serializer=Schema.#parseSerializer(this.#name,serializer,this.#serializers);return this.#serializers.set(serializer.name,serializer),this.emit("serializerCreated",{serializer:serializer,schema:this}),this.emit("change",{type:"serializerCreated",serializer:serializer,schema:this}),serializer}getSerializer(name){return this.#serializers.get(name)}get name(){return this.#name}get models(){return this.#models}static create(schemaData){return new Schema(schemaData)}static get(name){return Schema.#schemas.get(name)}get(pathName){this.emit("beforeGet",{pathName:pathName,schema:this});const[modelName,recordKey,...rest]=pathName.split("."),model=this.getModel(modelName);if(!model)throw new ReferenceError(`Model ${modelName} does not exist in schema ${this.#name}.`);if(void 0===recordKey)return model;var result=model[schema_$key][schema_$keyType],result=model.records.get("string"===result?recordKey:Number.parseInt(recordKey));if(!rest.length)return result;if(!result)throw new ReferenceError(`Record ${recordKey} does not exist in model ${modelName}.`);result=rest.reduce((acc,key)=>acc[key],result);return this.emit("got",{pathName:pathName,result:result,schema:this}),result}static[schema_$handleExperimentalAPIMessage](message){var experimentalAPIMessages=Schema.config["experimentalAPIMessages"];if("warn"===experimentalAPIMessages)console.warn(message);else if("error"===experimentalAPIMessages)throw new ExperimentalAPIUsageError(message)}static#parseModel(schemaName,modelData,models){return validateObjectWithUniqueName({objectType:"Model",parentType:"Schema",parentName:schemaName},modelData,[...models.keys()]),new Model(modelData)}static#applyRelationship(relationship,toModelName,models){const{from,to,type}=toModelName;[from,to].forEach(model=>{if(!["string","object"].includes(typeof model))throw new TypeError(`Invalid relationship model: ${model}.`)});var fromModelName="string"==typeof from?from:from.model,toModelName="string"==typeof to?to:to.model;const fromModel=models.get(fromModelName),toModel=models.get(toModelName);if(!fromModel)throw new ReferenceError(`Model ${fromModelName} not found in schema ${relationship} when attempting to create a relationship.`);if(!toModel)throw new ReferenceError(`Model ${toModelName} not found in schema ${relationship} when attempting to create a relationship.`);relationship=new Relationship({from:from,to:to,type:type});return fromModel[schema_$addRelationshipAsField](relationship),toModel[schema_$addRelationshipAsProperty](relationship),relationship}static#parseSerializer(schemaName,serializerData,serializers){return validateObjectWithUniqueName({objectType:"Serializer",parentType:"Schema",parentName:schemaName},serializerData,[...serializers.keys()]),new Serializer(serializerData)}static#parseConfig(config={}){config&&["experimentalAPIMessages"].forEach(key=>{void 0!==config[key]&&["warn","error","off"].includes(config[key])&&(Schema.config[key]=config[key])})}}const src=Schema;return __webpack_exports__})()}); |
{ | ||
"name": "@jsiqle/core", | ||
"version": "1.3.5", | ||
"version": "1.4.0", | ||
"description": "JavaScript In-memory Query Language with Events.", | ||
@@ -5,0 +5,0 @@ "main": "./dist/main.js", |
@@ -128,3 +128,3 @@ # @jsiqle/core | ||
- `cacheProperties`: (Optional) An array containing property names that should be cached. Properties are cached as long as a record doesn't have any field changes. More information about property caching can be found in one of the following sections. | ||
- `scopes`: (Optional) An object containing key-value pairs for getter properties to be defined on the record set of the model. All scopes expect a single argument representing the record set or a subset of records from the current model. More information about scope definitions can be found in one of the following sections. | ||
- `scopes`: (Optional) An object containing key-value pairs for getter properties to be defined on the record set of the model. All scopes expect a single argument representing the record set or a subset of records from the current model. Alternatively, an object with a `matcher` and `sorter` key can be supplied for ordered scopes. More information about scope definitions can be found in one of the following sections. | ||
- `validators`: (Optional) An object containing key-value pairs for validation properties that return a boolean value depending on the validation's result. All validators expect two arguments, the current record and the record set of the current model. More information about validators and field validators can be found in one of the following sections. | ||
@@ -409,8 +409,8 @@ | ||
MyModel.addSchema('does', record => record.lastName === 'Doe'); | ||
MyModel.addScope('does', record => record.lastName === 'Doe'); | ||
``` | ||
Scopes defined as part of the model definition as specified as key-value pairs, whereas scopes defined in `Model.prototype.addscope()` are passed as two separate arguments, the name and the scope body. | ||
Scopes defined as part of the model definition as specified as key-value pairs, whereas scopes defined in `Model.prototype.addScope()` are passed as two separate arguments, the name and the scope body. | ||
Scopes expect one argument, the current record, and must return a boolean indicating if the scope should include the record or not. | ||
Scopes expect one argument, the current record, and must return a boolean indicating if the scope should include the record or not. Alternatively, scopes can be specified as objects when defined as part of the model definition with a `matcher` function and a `sorter` function. This will create an ordered scope that will always apply the `sorter` to matched records before returning them. Ordered scopes can also be created by supplying a third argument to `Model.prototype.addScope()` which will act as the `sorter` function. | ||
@@ -417,0 +417,0 @@ You can remove a scope from a model using `Model.prototype.removeScope()`: |
@@ -108,3 +108,3 @@ import EventEmitter from 'events'; | ||
Object.entries(scopes).forEach(([scopeName, scope]) => { | ||
this.addScope(scopeName, scope); | ||
this.addScope(scopeName, ...Model.#parseScope(scope)); | ||
}); | ||
@@ -270,3 +270,3 @@ | ||
addScope(name, scope) { | ||
addScope(name, scope, sortFn) { | ||
this.emit('beforeAddScope', { | ||
@@ -277,3 +277,3 @@ scope: { name, body: scope }, | ||
const scopeName = validateName('Scope', name); | ||
this.#records[$addScope](scopeName, scope); | ||
this.#records[$addScope](scopeName, scope, sortFn); | ||
this.emit('scopeAdded', { | ||
@@ -611,2 +611,21 @@ scope: { name: scopeName, body: scope }, | ||
static #parseScope(scope) { | ||
if (typeof scope === 'function') return [scope]; | ||
if (typeof scope === 'object') { | ||
const { matcher, sorter } = scope; | ||
if (typeof matcher !== 'function') | ||
throw new TypeError( | ||
`The provided matcher for the scope is not a function.` | ||
); | ||
if (sorter && typeof sorter !== 'function') | ||
throw new TypeError( | ||
`The provided sorter for the scope is not a function.` | ||
); | ||
return [matcher, sorter]; | ||
} | ||
throw new TypeError( | ||
`The provided scope is not a function or valid object.` | ||
); | ||
} | ||
static #validateFunction( | ||
@@ -613,0 +632,0 @@ callbackType, |
@@ -14,3 +14,2 @@ import { allEqualBy } from 'src/utils'; | ||
$removeScope, | ||
$copyScopes, | ||
$isRecord, | ||
@@ -37,3 +36,3 @@ $key, | ||
this.#scopes = new Map(); | ||
if (copyScopesFrom) this[$copyScopes](copyScopesFrom); | ||
if (copyScopesFrom) this.#copyScopes(copyScopesFrom); | ||
@@ -661,4 +660,5 @@ this.#frozen = false; | ||
[$addScope](name, scope) { | ||
[$addScope](name, scope, sortFn) { | ||
RecordSet.#validateProperty('Scope', name, scope, this.#scopes); | ||
if (sortFn) RecordSet.#validateFunction('Scope comparator', name, sortFn); | ||
if ( | ||
@@ -670,7 +670,7 @@ this[name] || | ||
this.#scopes.set(name, scope); | ||
this.#scopes.set(name, [scope, sortFn]); | ||
Object.defineProperty(this, name, { | ||
configurable: true, // Allows deletion in $removeScope | ||
get: () => { | ||
return this.where(this.#scopes.get(name)); | ||
return this.#scopedWhere(name); | ||
}, | ||
@@ -687,14 +687,28 @@ }); | ||
[$copyScopes](otherRecordSet) { | ||
get [$scopes]() { | ||
return this.#scopes; | ||
} | ||
// Private | ||
#copyScopes(otherRecordSet) { | ||
otherRecordSet[$scopes].forEach((scope, name) => { | ||
this[$addScope](name, scope); | ||
// No need to verify that the scope is valid, it must be verified by the | ||
// other record set already. | ||
this.#scopes.set(name, scope); | ||
}); | ||
} | ||
get [$scopes]() { | ||
return this.#scopes; | ||
#scopedWhere(scopeName) { | ||
const [matcherFn, comparatorFn] = this.#scopes.get(scopeName); | ||
let matches = []; | ||
for (const [key, value] of this.entries()) | ||
if (matcherFn(value, key, this)) matches.push([key, value]); | ||
if (comparatorFn) | ||
matches.sort(([key1, value1], [key2, value2]) => | ||
comparatorFn(value1, value2, key1, key2) | ||
); | ||
return new RecordSet({ iterable: matches, copyScopesFrom: this }).freeze(); | ||
} | ||
// Private | ||
static #validateProperty(callbackType, callbackName, callback, callbacks) { | ||
@@ -712,2 +726,8 @@ if (typeof callback !== 'function') | ||
static #validateFunction(callbackType, callbackName, callback) { | ||
if (typeof callback !== 'function') | ||
throw new TypeError(`${callbackType} ${callbackName} is not a function.`); | ||
return callback; | ||
} | ||
static #validateContains(objectType, objectName, objects) { | ||
@@ -714,0 +734,0 @@ if (!objects.has(objectName)) |
@@ -35,3 +35,2 @@ /** | ||
'removeScope', | ||
'copyScopes', | ||
'instances', | ||
@@ -38,0 +37,0 @@ 'isRecord', |
@@ -177,2 +177,6 @@ import { Model } from 'src/model'; | ||
bScope: () => null, | ||
cScope: { | ||
matcher: () => null, | ||
sorter: (a, b) => a - b, | ||
}, | ||
}; | ||
@@ -183,2 +187,3 @@ | ||
expect(model.records[$scopes].has('bScope')).toEqual(true); | ||
expect(model.records[$scopes].has('cScope')).toEqual(true); | ||
}); | ||
@@ -434,2 +439,11 @@ | ||
}); | ||
it('creates an appropriate scope with a sorter', () => { | ||
model.addScope( | ||
'aScope', | ||
() => null, | ||
(a, b) => a.id - b.id | ||
); | ||
expect(model.records[$scopes].has('aScope')).toEqual(true); | ||
}); | ||
}); | ||
@@ -689,2 +703,8 @@ | ||
], | ||
scopes: { | ||
sortedNamedRecords: { | ||
matcher: record => record.name.length > 2, | ||
sorter: (a, b) => b.name.localeCompare(a.name), | ||
}, | ||
}, | ||
}); | ||
@@ -707,2 +727,13 @@ }); | ||
}); | ||
it('returns the correct records in the correct order for a given scope', () => { | ||
[ | ||
{ id: 'a', name: 'aName' }, | ||
{ id: 'b', name: 'bName' }, | ||
{ id: 'c', name: 'c' }, | ||
].forEach(record => model.createRecord(record)); | ||
expect( | ||
model.records.sortedNamedRecords.flatMap(record => record.id) | ||
).toEqual(['b', 'a']); | ||
}); | ||
}); | ||
@@ -709,0 +740,0 @@ |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
303101
6235