Custom Schema
Custom schemas participate in normalization, denormalization, and query key building 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.
Usage
This custom schema wraps another schema under a data field while preserving the
rest of the object.
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),
};
}
}
Use delegate.visit() to recursively normalize nested schemas and
delegate.unvisit() to recursively denormalize them. delegate.args exposes the
endpoint args for the current operation.
Interfaces
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;
SchemaSimple
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. parentEntity is the
nearest enclosing entity-like schema, when present. Most custom schemas can
ignore it.
denormalize() receives normalized input and returns the denormalized value.
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;
}
Use mergeEntity() when updating an entity-like schema with merge lifecycles. Use
setEntity() when the incoming value should overwrite previous normalized data.
Use invalidate() when the schema represents an invalid entity result.
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.