Custom Schema
Custom schemas participate in normalization and denormalization by implementing the methods normalizr calls while walking a schema tree. Most applications should prefer built-in schemas like Entity, Collection, Union, and Values; use this page when building your own schema type.
The interfaces below list the properties normalizr itself reads. Schema-specific APIs, such as reordering hooks on Collection, are not included.
Minimal schema
Any object with normalize(), denormalize(), or queryKey() can be used as a
schema node. Plain arrays and objects are also schemas.
type Schema =
| null
| string
| { [K: string]: any }
| Schema[]
| SchemaSimple
| Serializable;
type Serializable<T extends { toJSON(): string } = { toJSON(): string }> = (
value: any,
) => T;
interface SchemaSimple<T = any, Args extends readonly any[] = any[]> {
normalize(
input: any,
parent: any,
key: any,
delegate: INormalizeDelegate,
parentEntity?: any,
): any;
denormalize(input: {}, delegate: IDenormalizeDelegate): T;
queryKey(
args: Args,
queryKey: (...args: any) => any,
delegate: IQueryDelegate,
): any;
}
normalize() receives the value at the current schema node and returns the
normalized representation stored in the surrounding result. Use delegate.visit()
to recursively normalize nested schemas.
denormalize() receives normalized input and returns the denormalized value. Use
delegate.unvisit() for nested schemas.
queryKey() computes the normalized key used to read from the store without
fetching. It is only needed for schemas that can be read directly with
useQuery(), Controller.get, or
schema.Query.
Normalization delegate
interface INormalizeDelegate {
visit: Visit;
readonly args: readonly any[];
readonly meta: MetaEntry;
getEntities(key: string): EntitiesInterface | undefined;
getEntity: GetEntity;
mergeEntity(
schema: Mergeable & { indexes?: any },
pk: string,
incomingEntity: any,
): void;
setEntity(
schema: { key: string; indexes?: any },
pk: string,
entity: any,
meta?: MetaEntry,
): void;
invalidate(schema: { key: string }, pk: string): void;
checkLoop(key: string, pk: string, input: object): boolean;
}
interface Visit {
(schema: any, value: any, parent: any, key: any): any;
creating?: boolean;
}
interface MetaEntry {
fetchedAt: number;
date: number;
expiresAt: number;
}
parentEntity in SchemaSimple.normalize() is the nearest enclosing entity-like
schema, when present. Most custom schemas can ignore it.
Denormalization delegate
interface IDenormalizeDelegate {
unvisit(schema: any, input: any): any;
readonly args: readonly any[];
argsKey(fn: (args: readonly any[]) => string | undefined): string | undefined;
}
Reading delegate.args does not contribute to cache invalidation. If
denormalized output changes based on endpoint args, register that dependency with
delegate.argsKey(fn). The function reference must be stable; define it at module
scope or bind it on the schema instance.
Query delegate
interface IQueryDelegate {
getEntities(key: string): EntitiesInterface | undefined;
getEntity: GetEntity;
getIndex: GetIndex;
INVALID: symbol;
}
interface Queryable<Args extends readonly any[] = readonly any[]> {
queryKey(
args: Args,
queryKey: (...args: any) => any,
delegate: IQueryDelegate,
): {};
}
Return undefined from queryKey() when the schema cannot produce a valid store
key. Return delegate.INVALID when a query result should be treated as invalid.
Store access helpers
interface EntitiesInterface {
keys(): IterableIterator<string>;
entries(): IterableIterator<[string, any]>;
}
interface GetEntity {
(key: string, pk: string): any;
}
type IndexPath = [key: string, index: string, value: string];
interface GetIndex {
(...path: IndexPath): string | undefined;
}
Entity-like schemas
Normalizr treats a schema as entity-like when it has a pk property. Entity-like
schemas are stored by key and primary key, denormalized through entity caches,
and tracked for cycle detection.
interface EntityInterface<T = any> extends SchemaSimple {
readonly key: string;
pk(
params: any,
parent: any,
key: string | undefined,
args: readonly any[],
): string | number | undefined;
createIfValid(props: any): any;
schema: Record<string, Schema>;
prototype: T;
indexes?: string[];
cacheWith?: object;
maxEntityDepth?: number;
}
interface Mergeable {
key: string;
merge(existing: any, incoming: any): any;
mergeWithStore(
existingMeta: MetaEntry,
incomingMeta: MetaEntry,
existing: any,
incoming: any,
): any;
mergeMetaWithStore(
existingMeta: MetaEntry,
incomingMeta: MetaEntry,
existing: any,
incoming: any,
): MetaEntry;
}
cacheWith lets multiple schema instances share the same entity cache identity.
maxEntityDepth limits recursive denormalization depth for very deep entity
graphs.
Example
import type {
IDenormalizeDelegate,
INormalizeDelegate,
} from '@data-client/endpoint';
class DataWrapper {
constructor(private schema: any) {}
normalize(
input: any,
parent: any,
key: string | undefined,
delegate: INormalizeDelegate,
) {
return {
...input,
data: delegate.visit(this.schema, input.data, input, 'data'),
};
}
denormalize(input: any, delegate: IDenormalizeDelegate) {
return {
...input,
data: delegate.unvisit(this.schema, input.data),
};
}
}