| name | data-client-schema |
| description | Model data with @data-client schemas (Entity, EntityMixin, Collection, Union, Query, Values, All, Invalidate, Lazy, Scalar) for atomic, consistent, referentially-equal async data via normalization, identity-based caching, and a single source of truth. Use when defining or editing pk, static schema, resource()/RestEndpoint schema, mutable lists/maps (push/unshift/assign/remove/move), polymorphic/discriminated types, memoized selectors / derived data, partial/supplementary entities, relational/nested/joined data, optimistic updates, or cache invalidation across @data-client/rest, /endpoint, /graphql, or /normalizr. Apply proactively when discussing data models, remote data shape, caching, normalization, identity, joins, polymorphism, mutable collections, or store consistency. |
| license | Apache 2.0 |
1. Defining Schemas
Define schemas to represent the JSON returned by an endpoint. Compose these
to represent the data expected.
Object
List
Map
new Collection(Values(Schema)) - mutable/growable maps
- new Values(Schema) - immutable maps
Lens-dependent entity fields
Derived / selector pattern
-
new Query(Queryable) - memoized programmatic selectors
const queryRemainingTodos = new Query(
TodoResource.getList.schema,
entries => entries.filter(todo => !todo.completed).length,
);
const groupTodoByUser = new Query(
TodoResource.getList.schema,
todos => Object.groupBy(todos, todo => todo.userId),
);
2. Entity best practices
- Every
Entity subclass defines defaults for all non-optional serialised fields.
- Override
pk() only when the primary key ≠ id.
pk() return type is number | string | undefined
- Override
Entity.process(value, parent, key, args) to insert fields based on args/url
static schema (optional) for nested schemas or deserialization functions
- When designing APIs, prefer nesting entities
3. Entity lifecycle methods
- Normalize (JSON response → cache): operates on POJOs; output is JSON-serializable plain data stored in the normalized cache. Order:
process() → pk() → validate() → visit nested schemas (recurse into schema fields) → if existing: mergeWithStore() which calls shouldUpdate() and maybe shouldReorder() + merge(); metadata via mergeMetaWithStore().
- Denormalize (cache → component): creates Entity class instances via
fromJS(), restoring prototype chain so getters, methods, and schema processing work. Order: createIfValid() → validate() → fromJS() → unvisit nested schemas (recurse into schema fields).
4. Union Types (Polymorphic Schemas)
To define polymorphic resources (e.g., events), use Union and a discriminator field.
import { Union } from '@data-client/rest';
export abstract class Event extends Entity {
type: EventType = 'Issue';
}
export class PullRequestEvent extends Event { }
export class IssuesEvent extends Event { }
export const EventResource = resource({
path: '/users/:login/events/public/:id',
schema: new Union(
{
PullRequestEvent,
IssuesEvent,
},
'type',
),
});
5. Collections (Mutable Lists & Maps)
Collections wrap Array or Values schemas to enable mutations (add/remove/move).
pk routing
pk() uses nestKey(parent, key) when nested in an Entity and available; otherwise it uses argsKey(...args), then serializes the result. Without options, it defaults to argsKey: params => ({ ...params }), using all endpoint args as the collection key.
argsKey — derive pk from endpoint arguments (default)
nestKey — derive pk from parent entity for nested shared-state collections
Define both on the same Collection to reuse one definition top-level and nested. When argsKey(args) and nestKey(parent) produce the same object shape, the top-level fetch and the nested read resolve to the same (referentially equal) array/map — push/unshift/assign/move/remove on either updates both:
const userTodos = new Collection([Todo], {
argsKey: ({ userId }: { userId?: string }) => ({ userId }),
nestKey: (parent: User) => ({ userId: parent.id }),
});
nonFilterArgumentKeys
Default createCollectionFilter uses nonFilterArgumentKeys (default: keys starting with 'order') to exclude non-filter args when matching collections. This affects which existing collections receive new items from push/unshift/assign/move.
Override as function, RegExp, or string[]:
new Collection([Todo], { nonFilterArgumentKeys: /orderBy|sortDir/ })
Extenders
All usable with ctrl.set() (local-only) or via RestEndpoint extenders (network).
| Method | Type | Description |
|---|
push | Array | Entity |
unshift | Array | Entity |
assign | Values | Merge entries into map |
remove | Both | Remove items by value from matching collections |
move | Both | Remove from collections matching existing state, add to collections matching new state |
addWith(merge, filter?) | Both | Custom creation schema (used internally by push/unshift/assign) |
moveWith(merge) | Both | Custom move schema (control insertion order, e.g., unshift merge for prepending) |
6. Supplementary Endpoints (enrich existing entities)
When an endpoint returns partial or differently-shaped data for an entity already in cache
(e.g., a metadata endpoint, a stats endpoint, a lazy-load expansion endpoint),
use the same Entity as the schema — don't create a wrapper entity.
See partial-entities for patterns and examples.
7. Best Practices & Notes
- Always set up
schema on every resource/entity/collection for normalization
- Normalize deeply nested or relational data by defining proper schemas
- Use
Entity.schema for client-side joins
- Use
Denormalize<> type from rest/endpoint/graphql instead of InstanceType<>. This will handle all schemas like Unions, not just Entity.
8. Common Mistakes to Avoid
- The normalized cache stores plain JSON-serializable objects (POJOs), not class instances.
- Don't forget to use
fromJS() or assign default properties for class fields — bare TS field types emit no runtime defaults, so schema inference breaks
- Manually merging or 'enriching' data; instead use
Entity.schema for client-side joins
References
For detailed API documentation, see the references directory: