/* eslint-disable @typescript-eslint/explicit-module-boundary-types */ import { AbstractInstanceType, Schema, schema } from '@rest-hooks/endpoint'; const DefinedMembersKey = Symbol('Defined Members'); const UniqueIdentifierKey = Symbol('unq'); type Filter = T extends U ? T : never; interface SimpleResourceMembers { [DefinedMembersKey]: Filter, string>[]; } /** Immutable record that keeps track of which members are defined vs defaults. */ export default abstract class SimpleRecord { toString(): string { // we don't make _unq a member so it doesn't play a role in type compatibility return (this as any)[UniqueIdentifierKey]; } static toJSON() { return { name: this.name, schema: this.schema, }; } /** Defines nested entities */ static schema: { [k: string]: Schema } = {}; /** Factory method to convert from Plain JS Objects. * * @param [props] Plain Object of properties to assign. * @param [parent] When normalizing, the object which included the record * @param [key] When normalizing, the key where this record was found */ static fromJS( this: T, // TODO: this should only accept members that are not functions props: Partial> = {}, parent?: any, key?: string, ) { // we type guarded abstract case above, so ok to force typescript to allow constructor call const instance = new (this as any)(props) as AbstractInstanceType; if (props instanceof SimpleRecord) { props = (props.constructor as any).toObjectDefined(props); } Object.assign(instance, props); Object.defineProperty(instance, DefinedMembersKey, { value: Object.keys(props), writable: false, }); // a 'unique' identifier to make referential equality comparisons easy Object.defineProperty(instance, UniqueIdentifierKey, { value: `${Math.random()}`, writable: false, }); return instance; } /** Creates new instance copying over defined values of arguments */ static merge( this: T, existing: AbstractInstanceType, incoming: AbstractInstanceType, ) { const props = Object.assign( this.toObjectDefined(existing), this.toObjectDefined(incoming), ); return this.fromJS(props); } /** Whether key is non-default */ static hasDefined( this: T, instance: AbstractInstanceType, key: Filter, string>, ) { return (instance as any as SimpleResourceMembers)[ DefinedMembersKey ].includes(key); } /** Returns simple object with all the non-default members */ static toObjectDefined( this: T, instance: AbstractInstanceType, ) { const defined: Partial> = {}; for (const member of (instance as any as SimpleResourceMembers)[ DefinedMembersKey ]) { defined[member] = instance[member]; } return defined; } /** Returns array of all keys that have values defined in instance */ static keysDefined( this: T, instance: AbstractInstanceType, ) { return (instance as any as SimpleResourceMembers)[DefinedMembersKey]; } static normalize( this: T, ...args: [ input: any, parent: any, key: any, visit: (...args: any) => any, addEntity: (...args: any) => any, visitedEntities: Record, storeEntities: Record, args?: any[], ] ): NormalizedEntity { return schema.Object.prototype.normalize.call(this, ...args) as any; } static infer( this: T, args: readonly any[], indexes: any, recurse: any, entities?: any, ): NormalizedEntity { return (schema.Object.prototype.infer as any).call( this, args, indexes, recurse, entities, ); } static denormalize( this: T, input: any, unvisit: any, ): [AbstractInstanceType, boolean, boolean] { const object = { ...input }; let deleted = false; let found = true; Object.keys(this.schema).forEach(key => { const [item, foundItem, deletedItem] = unvisit( object[key], this.schema[key], ); if (object[key] !== undefined) { object[key] = item; } // members who default to falsy values are considered 'optional' // if falsy value, and default is actually set then it is optional so pass through if (!foundItem && !(key in this.defaults && !this.defaults[key])) { found = false; } if (deletedItem && !(key in this.defaults && !this.defaults[key])) { deleted = true; } }); // useDenormalized will memo based on entities, so creating a new object each time is fine return [this.fromJS(object) as any, found, deleted]; } declare static readonly __defaults: any; /** All instance defaults set */ static get defaults() { if (!Object.hasOwn(this, '__defaults')) (this as any).__defaults = new (this as any)(); return this.__defaults; } } type NormalizedEntity = T extends { prototype: infer U; schema: infer S; } ? { [K in Exclude]: U[K] } & { [K in keyof S]: string } : never;