Saltar al contenido principal

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.

multi-column primary key
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

More Demos

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

One argument
const entitiesEntry = getEntity(this.schema.key);
if (entitiesEntry === undefined) return INVALID;
return Object.values(entitiesEntry).map(
entity => entity && this.schema.pk(entity),
);
Two arguments
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.

Usar la validación en endpoints con campos incompletos