EntityMixin
Entity define un único objeto único.
Si ya tienes clases para tus tipos de datos, EntityMixin puede ser para ti.
import { EntityMixin } from '@data-client/rest'; export class Article { id = ''; title = ''; content = ''; tags: string[] = []; } export class ArticleEntity extends EntityMixin(Article) {}
Opciones
El segundo argumento del mixin sirve para personalizar cómodamente la construcción. Si no se especifica, se usarán los
miembros estáticos de la clase Base. Alternativamente, igual que con Entity, siempre puedes especificarlos
como miembros estáticos de la clase final.
class User { username = ''; createdAt = Temporal.Instant.fromEpochMilliseconds(0); } class UserEntity extends EntityMixin(User, { pk: 'username', key: 'User', schema: { createdAt: Temporal.Instant.from }, }) {}
pk: string | (value, parent?, key?, args?) => string | number | undefined = 'id'
Especifica el Entity.pk
Un string indica el campo que se usará como pk.
Una function se usa igual que Entity.pk, pero el primer argumento (value) es this
Por defecto es 'id'; lo que significa que pk es una opción obligatoria a menos que la clase Base tenga un miembro id serializable.
class Thread { forum = ''; slug = ''; content = ''; } class ThreadEntity extends EntityMixin(Thread, { pk(value) { return [value.forum, value.slug].join(','); }, }) {}
key: string
Especifica el Entity.key
schema: {[k:string]: Schema}
Especifica el Entity.schema
const vs class
Si no necesitas personalizar más la entidad, puedes usar una declaración const en lugar
de extend a otra clase.
Hay una diferencia sutil al referirse al class token en TypeScript: las
declaraciones class se refieren al tipo de la instancia; mientras que los const tokens se refieren al valor, por lo que
debes usar typeof, pero además typeof da el tipo de la clase, así que debes aplicar InstanceType
encima.
import { schema } from '@data-client/rest'; export class Article { id = ''; title = ''; content = ''; tags: string[] = []; } export class ArticleEntity extends EntityMixin(Article) {} export const ArticleEntity2 = EntityMixin(Article); const article: ArticleEntity = ArticleEntity.fromJS(); const articleFails: ArticleEntity2 = ArticleEntity2.fromJS(); const articleWorks: InstanceType<typeof ArticleEntity2> = ArticleEntity2.fromJS();
Ciclo de vida
Para sobrescribir métodos del ciclo de vida como process(), debes usar la forma class ... extends EntityMixin(...) {}.
Las opciones de EntityMixin() solo incluyen pk, key y schema; las sobrescrituras del ciclo de vida van en la propia clase.
import { EntityMixin } from '@data-client/rest'; export class Article { id = ''; title = ''; content = ''; tags: string[] = []; } // ❌ Not supported (lifecycle methods are not EntityMixin options) // export const ArticleEntity = EntityMixin(Article, { // process(input) { // return input; // }, // }); // ✅ Use a class when adding lifecycle methods export class ArticleEntity extends EntityMixin(Article) { static process(input: any, parent: any, key: string | undefined, args: any[]) { const processed = super.process(input, parent, key, args); processed.tags ??= []; return processed; } }
static fromJS(props): Entity
Método de fábrica que copia las props a una nueva instancia. Úsalo en lugar de new MyEntity(),
para asegurar que se sobrescriban las props por defecto.
static process(input, parent, key, args): processedEntity
Se ejecuta al inicio de la normalización de esta entidad. El valor de retorno se guarda en el store y se envía a pk().
Por defecto simplemente copia la respuesta ({...input})
Cómo sobrescribirlo para construir búsquedas inversas para datos relacionales
El caso del id faltante
import { EntityMixin } from '@data-client/rest';
class Stream {
username = '';
title = '';
game = '';
currentViewers = 0;
live = false;
}
class StreamEntity extends EntityMixin(Stream) {
static key = 'Stream';
static process(value, parent, key, args) {
// super.process creates a copy of value
const processed = super.process(value, parent, key, args);
processed.username = args[0]?.username;
return processed;
}
}
Invalidación dinámica
Devolver undefined desde Entity.process
hará que la Entity sea invalidada.
Esto nos permite invalidar de forma dinámica, según los datos particulares de la respuesta.
import { EntityMixin } from '@data-client/rest';
class PriceLevel {
price = 0;
amount = 0;
}
class PriceLevelEntity extends EntityMixin(PriceLevel) {
static process(
input: [number, number],
parent: any,
key: string | undefined,
): any {
const [price, amount] = input;
if (amount === 0) return undefined;
return { price, amount };
}
}
static mergeWithStore(existingMeta, incomingMeta, existing, incoming): mergedValue
static mergeWithStore(
existingMeta: {
date: number;
fetchedAt: number;
},
incomingMeta: { date: number; fetchedAt: number },
existing: any,
incoming: any,
) {
const shouldUpdate = this.shouldUpdate(
existingMeta,
incomingMeta,
existing,
incoming,
);
if (shouldUpdate) {
// 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() se llama durante la normalización cuando una entidad procesada ya existe en el store.
Esto llama a shouldUpdate(), shouldReorder() y, potencialmente, a merge()
static shouldUpdate(existingMeta, incomingMeta, existing, incoming): boolean
static shouldUpdate(
existingMeta: { date: number; fetchedAt: number },
incomingMeta: { date: number; fetchedAt: number },
existing: any,
incoming: any,
) {
return existingMeta.fetchedAt <= incomingMeta.fetchedAt;
}
Evitar actualizaciones
shouldUpdate también se puede usar para interrumpir la actualización de una entidad.
import deepEqual from 'deep-equal';
import { EntityMixin } from '@data-client/rest';
class Article {
id = '';
title = '';
content = '';
published = false;
}
class ArticleEntity extends EntityMixin(Article) {
static shouldUpdate(
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;
}
Un valor de retorno true invertirá el orden de los argumentos de la entidad entrante y la que está en el store en el merge. Con
el merge por defecto, esto hará que los campos de las entidades existentes sobrescriban a los de las entrantes,
en lugar de al revés.
Ejemplo
import { EntityMixin } from '@data-client/rest'; class LatestPrice { id = ''; updatedAt = 0; price = '0.0'; symbol = ''; } class LatestPriceEntity extends EntityMixin(LatestPrice) { static shouldReorder( existingMeta: { date: number; fetchedAt: number }, incomingMeta: { date: number; fetchedAt: number }, existing: { updatedAt: number }, incoming: { updatedAt: number }, ) { return incoming.updatedAt < existing.updatedAt; } }
Explora el ejemplo coin-app
static merge(existing, incoming): mergedValue
static merge(existing: any, incoming: any) {
return {
...existing,
...incoming,
};
}
Merge se usa para manejar los casos en que ya existe una entidad entrante. Se llama directamente cuando se encuentra la misma entidad en una sola respuesta. Por defecto también se llama cuando mergeWithStore() determina que la entidad entrante debe fusionarse con una entidad ya persistida en el store de Reactive Data Client.
Cómo sobrescribirlo para construir búsquedas inversas para datos relacionales
static mergeMetaWithStore(existingMeta, incomingMeta, existing, incoming): meta
static mergeMetaWithStore(
existingMeta: {
expiresAt: number;
date: number;
fetchedAt: number;
},
incomingMeta: { expiresAt: number; date: number; fetchedAt: number },
existing: any,
incoming: any,
) {
return this.shouldReorder(existingMeta, incomingMeta, existing, incoming)
? existingMeta
: incomingMeta;
}
mergeMetaWithStore() se llama durante la normalización cuando una entidad procesada ya existe en el store.
static queryKey(args, queryKey, getEntity, getIndex): pk?
Este método permite que las Entities sean Queryable, es decir, que se pueda acceder al store sin un endpoint.
Sobrescribirlo permite personalizar o deshabilitar por completo este comportamiento.
Devolver undefined deshabilitará este comportamiento.
Devolver un string pk intentará buscar esta entidad y usarla en la respuesta.
Cuando se usa, la política de caducidad se calcula a partir de los metadatos propios de la entidad.
Por defecto usa el primer argumento para buscar en pk() e indexes
getEntity(key, pk?)
Obtiene todas las entidades de un tipo con un argumento, o una sola entidad con dos
const entitiesEntry = getEntity(this.schema.key);
if (entitiesEntry === undefined) return INVALID;
return Object.values(entitiesEntry).map(
entity => entity && this.schema.pk(entity),
);
if (getEntity(this.key, id)) return id;
getIndex(key, indexName, value)
Devuelve la entrada del índice (mapa valor->pk)
const value = args[0][indexName];
return getIndex(schema.key, indexName, value)[value];
static createIfValid(processedEntity): Entity | undefined
Se llama al desnormalizar una entidad. Crea una instancia de esta clase si se considera 'válida'.
Un retorno undefined resultará en un estado de caducidad Invalid,
like Invalidate.
La caducidad Invalid generalmente significa que los hooks entrarán en estado de carga e intentarán un nuevo fetch.
static createIfValid(props): AbstractInstanceType<this> | undefined {
if (this.validate(props)) {
return undefined as any;
}
return this.fromJS(props);
}
static validate(processedEntity): errorMessage?
Se ejecuta tanto en la normalización como en la desnormalización. Devolver un string indica un error (el string es el mensaje).
Durante la normalización, un fallo de validación producirá un error para ese fetch.
Durante la desnormalización, un fallo de validación marcará ese resultado como 'invalid' y, por tanto, bloqueará mientras se obtiene un resultado.
Por defecto hace algunas comprobaciones básicas de existencia de campos, solo en modo de desarrollo. Sobrescríbelo para deshabilitarlo o personalizarlo.