Saltar al contenido principal

Entity

{
Article: {
'1': {
id: '1',
title: 'Entities define data',
}
}
}

Entity define un único objeto único.

Entity.key + Entity.pk() (clave primaria) permiten un store de tabla de búsqueda plana, lo que habilita un alto rendimiento, consistencia de los datos y mutaciones atómicas.

Las Entities permiten personalizar el ciclo de vida del procesamiento de datos definiendo sus miembros estáticos, como schema, y sobrescribiendo sus métodos del ciclo de vida.

Uso​

import { Entity } from '@data-client/rest';
import { User } from './User';

export class Article extends Entity {
  id = '';
  title = '';
  content = '';
  author = User.fromJS();
  tags: string[] = [];
  createdAt = Temporal.Instant.fromEpochMilliseconds(0);

  static key = 'Article';
  pk() {
    return this.id;
  }

  static schema = {
    author: User,
    createdAt: Temporal.Instant.from,
  };
}

static schema es una definición declarativa de los campos que se van a procesar. En este caso, author es otra Entity que se extraerá, y createdAt se convertirá de un string a un objeto Date.

consejo

Las Entities se vinculan a los Endpoints mediante resource.schema o RestEndpoint.schema

consejo

Si ya tienes tus clases definidas, también puedes usar EntityMixin para crear Entities.

Sobrescribir otros miembros estáticos permite personalizar el ciclo de vida de los datos, como se ve a continuación.

Miembros​

pk(parent?, key?, args?): string | number | undefined​

pk significa primary key (clave primaria) e identifica de forma única una instancia de Entity. Por defecto, devuelve el campo id de la Entity.

Sobrescribe este método para usar otros campos o para otros casos, como claves primarias de varias columnas.

Valor undefined​

Se puede usar undefined como valor por defecto para indicar que la entity aún no se ha creado. Esto es útil al inicializar un formulario de creación usando Entity.fromJS() directamente. Si pk() devuelve undefined, se considera que no se ha persistido en el servidor y, por tanto, no se conservará en la caché.

Otros usos​

Como pk() es único, ofrece una forma coherente de definir las JSX list keys

//....
return (
<div>
{results.map(result => (
<TheThing key={result.pk()} thing={result} />
))}
</div>
);

Claves primarias compuestas​

Cuando un solo campo no basta para identificar de forma única una entity, puedes combinar varios campos en una clave compuesta. Esto es habitual en recursos anidados o en recursos con identificadores de varias partes.

export class Issue extends Entity {
number = 0;
owner = '';
repo = '';
repositoryUrl = '';
title = '';

pk() {
// Composite key from owner, repo, and issue number
return `${this.owner}/${this.repo}/${this.number}`;
}

static key = 'Issue';
}

Cuando los datos de la entity no incluyen directamente todas las partes de la clave, puedes extraerlas de campos relacionados o de los argumentos del endpoint usando Entity.process():

export class Issue extends Entity {
number = 0;
owner = '';
repo = '';
repositoryUrl = ''; // Contains: https://api.github.com/repos/{owner}/{repo}
title = '';

pk() {
// Use owner/repo from process() which extracts from repositoryUrl
return `${this.owner}/${this.repo}/${this.number}`;
}

static key = 'Issue';

static process(input: any, parent: any, key: string, args: any[]) {
// Extract owner and repo from the repositoryUrl
const match = input.repositoryUrl?.match(/repos\/([^/]+)\/([^/]+)/);
const owner = args[0]?.owner ?? match?.[1];
const repo = args[0]?.repo ?? match?.[2];
return { ...input, owner, repo };
}
}

Entities singleton​

¿Y si solo existe una instancia de una Entity en toda tu aplicación? En realidad no necesitas distinguir entre instancias, por lo que probablemente la API no define un id ni un campo similar. En estos casos puedes devolver simplemente un literal como 'the_only_one'.

pk() {
return 'the_only_one';
}

Por ejemplo, si tienes

const get = new RestEndpoint({
path: '/options',
schema: OptionsEntity,
});
export const OptionsResource = {
get,
partialUpdate: get.extend({ method: 'PATCH' }),
};

Propiedad estática key: string​

Esto define la clave del tipo de Entity, en lugar de la de una instancia. Debe ser un valor único globalmente.

aviso

Por defecto es this.name; sin embargo, esto puede fallar en compilaciones de producción que cambian los nombres de las clases. Esto se conoce a menudo como class name mangling.

En estos casos puedes sobrescribir key o desactivar el class name mangling.

class User extends Entity {
id = '';
username = '';

pk() {
return this.id;
}
static key = 'User';
}

Propiedad estática schema: { [k: keyof this]: Schema }​

Define miembros de entities relacionadas, o la deserialización de campos como Date y BigNumber.

Fixtures
GET /posts/123
{"id":"5","author":{"id":"123","name":"Jim"},"content":"Happy day","createdAt":"2019-01-23T06:07:48.311Z"}
▶User
▶Post
import { Entity } from '@data-client/rest';
import { Temporal } from 'temporal-polyfill';
import { User } from './User';

export class Post extends Entity {
  id = '';
  author = User.fromJS();
  createdAt = Temporal.Instant.fromEpochMilliseconds(0);
  content = '';
  title = '';

  pk() {
    return this.id;
  }
  static key = 'Post';

  static schema = {
    author: User,
    createdAt: Temporal.Instant.from,
  };
}
▶PostPage
Resultado
Store▶

Miembros opcionales​

Las referencias a Entities aquí cuyos valores por defecto en la propia definición del Record se consideran 'opcionales'

class User extends Entity {
friend: User | null = null; // this field is optional
lastUpdated = Temporal.Instant.fromEpochMilliseconds(0);

static schema = {
friend: User,
lastUpdated: Temporal.Instant.from,
};
}

Propiedad estática indexes?: (keyof this)[]​

Los índices mejoran el rendimiento al hacer búsquedas basadas en esos parámetros. Añade a la lista los nombres de campo (como slug, username) que quieras enviar más adelante como parámetros de búsqueda.

nota

No añadas tu clave primaria, como id, a la lista de índices, ya que ya está optimizada.

useSuspense()​

Con useSuspense(), esto inferirá de forma anticipada los resultados a partir de la tabla de entities si es posible, renderizando sin esperar a que termine el fetch. Esto suele ser útil cuando la caché de entities ya ha sido rellenada por otra petición, como una petición de lista.

export class User extends Entity {
id: number | undefined = undefined;
username = '';
email = '';
isAdmin = false;

static indexes = ['username' as const];
}
export const UserResource = resource({
path: '/user/:id',
schema: User,
});
import { useSuspense } from '@data-client/react';
import { UserResource } from './resources/User';

const user = useSuspense(UserResource.get, { username: 'bob' });

useQuery()​

Con useQuery(), esto permite acceder a resultados obtenidos dentro de otras peticiones, incluso si no existe ningún endpoint desde el que se puedan obtener.

class LatestPrice extends Entity {
id = '';
symbol = '';
price = '0.0';

static indexes = ['symbol' as const];
}
class Asset extends Entity {
id = '';
price = '';

static schema = {
price: LatestPrice,
};
}
const getAssets = new RestEndpoint({
path: '/assets',
schema: [Asset],
});

Algún componente de nivel superior:

import { useSuspense } from '@data-client/react';
import { getAssets } from './resources/Asset';

const assets = useSuspense(getAssets);

Anidado debajo:

import { useQuery } from '@data-client/react';
import { LatestPrice } from './resources/LatestPrice';

const price = useQuery(LatestPrice, { symbol: 'BTC' });

Propiedad estática maxEntityDepth?: number​

Limita la profundidad de anidamiento de entities durante la desnormalización para evitar desbordamientos de pila en grafos grandes de entities bidireccionales. Por defecto: 64

Cuando las relaciones bidireccionales crean cadenas con muchas entities únicas (por ejemplo, Department → Building → Department → ...), la desnormalización puede recursar miles de niveles de profundidad. maxEntityDepth trunca la resolución a la profundidad indicada: las entities más allá del límite se devuelven con las claves foráneas anidadas sin resolver (solo ids) en lugar de objetos completamente desnormalizados.

class Department extends Entity {
id = '';
name = '';
buildings: Building[] = [];

pk() {
return this.id;
}
static key = 'Department';
static maxEntityDepth = 16;

static schema = {
buildings: [Building],
};
}
consejo

Establécelo en las entities que participan en relaciones bidireccionales profundas o amplias. Los grafos de entities normales (profundidad < 10) nunca se acercan al límite por defecto.

Para relaciones que no necesitan desnormalización anticipada, Lazy omite por completo la resolución y te permite resolver bajo demanda mediante useQuery.

Ciclo de vida​

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​

class Stream extends Entity {
username = '';
title = '';
game = '';
currentViewers = 0;
live = false;

pk() {
return this.username;
}
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.

class PriceLevel extends Entity {
price = 0;
amount = 0;

pk() {
return this.price;
}

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';

class Article extends Entity {
id = '';
title = '';
content = '';
published = false;

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​

class LatestPriceEntity extends Entity {
  id = '';
  updatedAt = 0;
  price = '0.0';
  symbol = '';

  pk() {
    return this.id;
  }

  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