schema.Entity
If you already have classes for your data-types, schema.Entity
mixin may be for you.
import { schema } from '@data-client/graphql'; export class Article { id = ''; title = ''; content = ''; tags: string[] = []; } export class ArticleEntity extends schema.Entity(Article) {}
Options
The second argument to the mixin can be used to conveniently customize construction. If not specified the Base
class' static members will be used. Alternatively, just like with Entity, you can always specify
these as static members of the final class.
class User { username = ''; createdAt = Temporal.Instant.fromEpochSeconds(0); } class UserEntity extends schema.Entity(User, { pk: 'username', key: 'User', schema: { createdAt: Temporal.Instant.from }, }) {}
pk: string | (value, parent?, key?) => string | undefined = 'id'
Specifies the Entity.pk
A string
indicates the field to use for pk.
A function
is used just like Entity.pk, but the first argument (value
) is this
Defaults to 'id'; which means pk is a required option unless the Base
class has a serializable id
member.
class Thread { forum = ''; slug = ''; content = ''; } class ThreadEntity extends schema.Entity(Thread, { pk(value) { return [value.forum, value.slug].join(','); }, }) {}
key: string
Specifies the Entity.key
schema: {[k:string]: Schema}
Specifies the Entity.schema
Methods
schema.Entity
has the same methods as Entity with an improved mergeWithStore()
lifecycle.
This method uses shouldReorder() to handle race conditions rather than useIncoming(), which is better able to handle partial field entities.
Eventually Entity
will also be converted to use this default implementation. You can prepare for this by copying
the mergeWithStore default implementation below.
static mergeWithStore(existingMeta, incomingMeta, existing, incoming): mergedValue
static mergeWithStore(
existingMeta: {
date: number;
fetchedAt: number;
},
incomingMeta: { date: number; fetchedAt: number },
existing: any,
incoming: any,
) {
const useIncoming = this.useIncoming(
existingMeta,
incomingMeta,
existing,
incoming,
);
if (useIncoming) {
// distinct types are not mergeable (like delete symbol), so just replace
if (typeof incoming !== typeof existing) {
return incoming;
} else {
return this.shouldReorder(
existingMeta,
incomingMeta,
existing,
incoming,
)
? this.merge(incoming, existing)
: this.merge(existing, incoming);
}
} else {
return existing;
}
}
mergeWithStore()
is called during normalization when a processed entity is already found in the store.
This calls useIncoming(), shouldReorder() and potentially merge()
static useIncoming(existingMeta, incomingMeta, existing, incoming): boolean
static useIncoming(
existingMeta: { date: number; fetchedAt: number },
incomingMeta: { date: number; fetchedAt: number },
existing: any,
incoming: any,
) {
return true;
}
Preventing updates
useIncoming can also be used to short-circuit an entity update.
import deepEqual from 'deep-equal';
class ArticleEntity extends schema.Entity(
class {
id = '';
title = '';
content = '';
published = false;
},
) {
static useIncoming(
existingMeta: { date: number; fetchedAt: number },
incomingMeta: { date: number; fetchedAt: number },
existing: any,
incoming: any,
) {
return !deepEqual(incoming, existing);
}
}
static shouldReorder(existingMeta, incomingMeta, existing, incoming): boolean
static shouldReorder(
existingMeta: { date: number; fetchedAt: number },
incomingMeta: { date: number; fetchedAt: number },
existing: any,
incoming: any,
) {
return incomingMeta.fetchedAt < existingMeta.fetchedAt;
}
true
return value will reorder incoming vs in-store entity argument order in merge. With
the default merge, this will cause the fields of existing entities to override those of incoming,
rather than the other way around.
Example
class LatestPriceEntity extends schema.Entity( class { id = ''; updatedAt = 0; price = '0.0'; symbol = ''; }, ) { static shouldReorder( existingMeta: { date: number; fetchedAt: number }, incomingMeta: { date: number; fetchedAt: number }, existing: { updatedAt: number }, incoming: { updatedAt: number }, ) { return incoming.updatedAt < existing.updatedAt; } }
static merge(existing, incoming): mergedValue
static merge(existing: any, incoming: any) {
return {
...existing,
...incoming,
};
}
Merge is used to handle cases when an incoming entity is already found. This is called directly when the same entity is found in one response. By default it is also called when mergeWithStore() determines the incoming entity should be merged with an entity already persisted in the Reactive Data Client store.
const vs class
If you don't need to further customize the entity, you can use a const
declaration instead
of extend
to another class.
There is a subtle difference when referring to the class token
in TypeScript - as
class
declarations will refer to the instance type; whereas const tokens
refer to the value, so you
must use typeof
, but additionally typeof gives the class type, so you must layer InstanceType
on top.
import { schema } from '@data-client/graphql'; export class Article { id = ''; title = ''; content = ''; tags: string[] = []; } export class ArticleEntity extends schema.Entity(Article) {} export const ArticleEntity2 = schema.Entity(Article); const article: ArticleEntity = ArticleEntity.fromJS(); const articleFails: ArticleEntity2 = ArticleEntity2.fromJS(); const articleWorks: InstanceType<typeof ArticleEntity2> = ArticleEntity2.fromJS();