UNPKG

@data-client/normalizr

Version:

Normalizes and denormalizes JSON according to schema for Redux and Flux applications

183 lines (167 loc) 5.3 kB
import { getVisit } from './getVisit.js'; import type { Schema } from '../interface.js'; import type { NormalizeMeta, NormalizeNullable } from '../types.js'; import { ImmNormalizeDelegate, ImmutableJSMutableTable, } from './NormalizeDelegate.imm.js'; /** ImmutableJS store data structure */ export interface ImmutableStoreData { entities: ImmutableJSMutableTable; indexes: ImmutableJSMutableTable; entitiesMeta: ImmutableJSMutableTable; } /** Result of normalizing into ImmutableJS state */ export interface ImmutableNormalizedSchema<R> { entities: ImmutableJSMutableTable; result: R; indexes: ImmutableJSMutableTable; entitiesMeta: ImmutableJSMutableTable; } export const normalize = <S extends Schema = Schema, R = NormalizeNullable<S>>( schema: S | undefined, input: any, args: readonly any[] = [], { entities, indexes, entitiesMeta }: ImmutableStoreData = emptyStore, meta: NormalizeMeta = { fetchedAt: 0, date: Date.now(), expiresAt: Infinity }, ): ImmutableNormalizedSchema<R> => { // no schema means we don't process at all if (schema === undefined || schema === null) return { result: input, entities, indexes, entitiesMeta, }; const schemaType = expectedSchemaType(schema); if ( input === null || (typeof input !== schemaType && // we will allow a Invalidate schema to be a string or object !( (schema as any).key !== undefined && (schema as any).pk === undefined && typeof input === 'string' )) ) { /* istanbul ignore else */ if (process.env.NODE_ENV !== 'production') { const parseWorks = (input: string) => { try { return typeof JSON.parse(input) !== 'string'; } catch { return false; } }; if (typeof input === 'string' && parseWorks(input)) { throw new Error(`Normalizing a string, but this does match schema. Parsing this input string as JSON worked. This likely indicates fetch function did not parse the JSON. By default, this only happens if "content-type" header includes "json". See https://dataclient.io/rest/api/RestEndpoint#parseResponse for more information Schema: ${JSON.stringify(schema, undefined, 2)} Input: "${input}"`); } else { throw new Error( `Unexpected input given to normalize. Expected type to be "${schemaType}", found "${ input === null ? 'null' : typeof input }". Schema: ${JSON.stringify(schema, undefined, 2)} Input: "${input}"`, ); } } else { throw new Error( `Unexpected input given to normalize. Expected type to be "${schemaType}", found "${ input === null ? 'null' : typeof input }".`, ); } } const delegate = new ImmNormalizeDelegate( { entities, indexes, entitiesMeta }, meta, ); const visit = getVisit(delegate); const result = visit(schema, input, input, undefined, args); return { result, entities: delegate.entities, indexes: delegate.indexes, entitiesMeta: delegate.entitiesMeta, }; }; function expectedSchemaType(schema: Schema) { return ['object', 'function'].includes(typeof schema) ? 'object' : ( typeof schema ); } // Default empty ImmutableJS-like store // Users should provide their own Immutable.Map instances const emptyImmutableLike: ImmutableJSMutableTable = { get() { return undefined; }, getIn() { return undefined; }, setIn(k: readonly string[], value: any) { // Create a simple nested structure for the empty case // This is a minimal implementation for default empty state const result = { ...this } as any; let current = result; for (let i = 0; i < k.length - 1; i++) { if (!current[k[i]]) { current[k[i]] = {}; } current = current[k[i]]; } current[k[k.length - 1]] = value; // Return a proper ImmutableJS-like object return createNestedImmutable(result); }, }; function createNestedImmutable(obj: any): ImmutableJSMutableTable { return { get(key: string) { const value = obj[key]; if (value && typeof value === 'object') { return createNestedImmutable(value); } return value; }, getIn(path: readonly string[]) { let current = obj; for (const key of path) { if (current === undefined || current === null) return undefined; current = current[key]; } return current; }, setIn(path: readonly string[], value: any) { const result = deepClone(obj); let current = result; for (let i = 0; i < path.length - 1; i++) { if (!current[path[i]]) { current[path[i]] = {}; } current = current[path[i]]; } current[path[path.length - 1]] = value; return createNestedImmutable(result); }, }; } function deepClone(obj: any): any { if (obj === null || typeof obj !== 'object') return obj; if (Array.isArray(obj)) return obj.map(deepClone); const result: any = {}; for (const key in obj) { result[key] = deepClone(obj[key]); } return result; } const emptyStore: ImmutableStoreData = { entities: emptyImmutableLike, indexes: emptyImmutableLike, entitiesMeta: emptyImmutableLike, };