Entity
{
Article: {
'1': {
id: '1',
title: 'Entities define data',
}
}
}
Entity define um único objeto único.
Entity.key + Entity.pk() (chave primária) viabilizam um store de tabela de busca plana, permitindo alto desempenho, consistência dos dados e mutações atômicas.
Entities permitem personalizar o ciclo de vida do processamento de dados ao definir seus membros estáticos, como schema,
e ao sobrescrever seus métodos de 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 é uma definição declarativa dos campos a serem processados.
Neste caso, author é outra Entity a ser extraída, e createdAt será convertido
de string para um objeto Date.
Entities são vinculadas a Endpoints usando resource.schema ou RestEndpoint.schema
Se você já tem suas classes definidas, o EntityMixin também pode ser usado para criar Entities.
Outras sobrescritas de membros estáticos permitem personalizar o ciclo de vida dos dados, como visto abaixo.
Membros
pk(parent?, key?, args?): string | number | undefined
pk vem de primary key (chave primária) e identifica de forma única uma instância de Entity.
Por padrão, retorna o campo id da Entity.
Sobrescreva este método para usar outros campos ou para outros casos, como chaves primárias com várias colunas.
Valor undefined
undefined pode ser usado como padrão para indicar que a entity ainda não foi criada.
Isso é útil ao inicializar um formulário de criação usando Entity.fromJS()
diretamente. Se pk() retornar undefined, considera-se que a entity não foi persistida no servidor
e, portanto, ela não será mantida no cache.
Outros usos
Como pk() é único, ele oferece uma forma consistente de definir keys de listas JSX
//....
return (
<div>
{results.map(result => (
<TheThing key={result.pk()} thing={result} />
))}
</div>
);
Chaves primárias compostas
Quando um único campo não basta para identificar uma entity de forma única, você pode combinar vários campos em uma chave composta. Isso é comum em recursos aninhados ou em recursos com identificadores de várias 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';
}
Quando os dados da entity não incluem diretamente todas as partes da chave, você pode extraí-las de campos relacionados ou dos argumentos do 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
E se existir apenas uma instância de uma Entity em toda a sua aplicação? Você
não precisa realmente distinguir entre as instâncias, então provavelmente a API não definiu um id ou
um campo semelhante. Nesses casos, você pode simplesmente retornar um literal como
'the_only_one'.
pk() {
return 'the_only_one';
}
Caso você tenha
const get = new RestEndpoint({
path: '/options',
schema: OptionsEntity,
});
export const OptionsResource = {
get,
partialUpdate: get.extend({ method: 'PATCH' }),
};
static key: string
Define a chave do tipo de Entity, e não de uma instância. Precisa ser um valor globalmente único.
O padrão é this.name; porém, isso pode quebrar em builds de produção que alteram nomes de classes.
Isso costuma ser conhecido como class name mangling.
Nesses casos, você pode sobrescrever key ou desativar o mangling de nomes de classes.
class User extends Entity {
id = '';
username = '';
pk() {
return this.id;
}
static key = 'User';
}
static schema: { [k: keyof this]: Schema }
Define membros de entities relacionadas ou a desserialização de campos, como Date e BigNumber.
{"id":"5","author":{"id":"123","name":"Jim"},"content":"Happy day","createdAt":"2019-01-23T06:07:48.311Z"}
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, }; }
Membros opcionais
Referências a Entities cujos valores padrão na própria definição do Record são consideradas 'opcionais'
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,
};
}
static indexes?: (keyof this)[]
Os índices aumentam o desempenho de buscas baseadas nesses parâmetros. Adicione à lista
os nomes de campos (como slug, username) que você quiser enviar como parâmetros para buscas
posteriores.
Não adicione sua chave primária, como id, à lista de índices, pois ela já é otimizada.
useSuspense()
Com useSuspense(), isso inferirá antecipadamente os resultados a partir da tabela de entities, se possível, renderizando sem precisar esperar a conclusão do fetch. Isso costuma ser útil quando o cache de entities já foi preenchido por outra requisição, como a de uma 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()
Com useQuery(), isso permite acessar resultados obtidos dentro de outras requisições - mesmo que não exista um endpoint de onde ele possa ser buscado.
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],
});
Algum componente de nível superior:
import { useSuspense } from '@data-client/react';
import { getAssets } from './resources/Asset';
const assets = useSuspense(getAssets);
Aninhado abaixo:
import { useQuery } from '@data-client/react';
import { LatestPrice } from './resources/LatestPrice';
const price = useQuery(LatestPrice, { symbol: 'BTC' });
static maxEntityDepth?: number
Limita a profundidade de aninhamento de entities durante a desnormalização para evitar estouro de pilha em grafos de entities bidirecionais grandes. Padrão: 64
Quando relacionamentos bidirecionais criam cadeias com muitas entities únicas
(por exemplo, Department → Building → Department → ...), a desnormalização pode recursar
por milhares de níveis. maxEntityDepth interrompe a resolução na profundidade
especificada — entities além do limite são retornadas com as chaves estrangeiras aninhadas mantidas como
ids não resolvidos, em vez de objetos totalmente desnormalizados.
class Department extends Entity {
id = '';
name = '';
buildings: Building[] = [];
pk() {
return this.id;
}
static key = 'Department';
static maxEntityDepth = 16;
static schema = {
buildings: [Building],
};
}
Defina isto nas entities que participam de relacionamentos bidirecionais profundos ou amplos. Grafos de entities normais (profundidade < 10) nunca se aproximam do limite padrão.
Para relacionamentos que não precisam de desnormalização antecipada, Lazy ignora a resolução por completo e permite resolver sob demanda via useQuery.
Ciclo de vida
static fromJS(props): Entity
Método de fábrica que copia props para uma nova instância. Use-o em vez de new MyEntity(),
para garantir que as props padrão sejam sobrescritas.
static process(input, parent, key, args): processedEntity
Executado no início da normalização desta entity. O valor de retorno é salvo no store e enviado para pk().
O padrão é simplesmente copiar a resposta ({...input})
Como sobrescrever para construir buscas reversas para dados relacionais
O caso do id ausente
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;
}
}
Invalidação dinâmica
Retornar undefined de Entity.process
fará com que a Entity seja invalidada.
Isso nos permite invalidar dinamicamente, com base nos dados específicos da resposta.
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() é chamado durante a normalização quando uma entity processada já é encontrada no store.
Ele chama shouldUpdate(), shouldReorder() e, possivelmente, 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;
}
Impedindo atualizações
shouldUpdate também pode ser usado para interromper a atualização de uma entity.
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;
}
Um valor de retorno true inverterá a ordem dos argumentos de entity recebida e entity do store no merge. Com
o merge padrão, isso fará com que os campos das entities existentes sobrescrevam os das recebidas,
e não o contrário.
Exemplo
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; } }
Explore o exemplo coin-app
static merge(existing, incoming): mergedValue
static merge(existing: any, incoming: any) {
return {
...existing,
...incoming,
};
}
O merge é usado para tratar os casos em que uma entity recebida já foi encontrada. É chamado diretamente quando a mesma entity é encontrada em uma única resposta. Por padrão, também é chamado quando mergeWithStore() determina que a entity recebida deve ser mesclada com uma entity já persistida no store do Reactive Data Client.
Como sobrescrever para construir buscas reversas para dados relacionais
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() é chamado durante a normalização quando uma entity processada já é encontrada no store.
static queryKey(args, queryKey, getEntity, getIndex): pk?
Este método permite que Entities sejam Queryable - permitindo acesso ao store sem um endpoint.
Sobrescrevê-lo permite personalizar ou desativar esse comportamento por completo.
Retornar undefined desabilita esse comportamento.
Retornar uma string pk tentará buscar essa entity e usá-la na resposta.
Quando usada, a política de expiração é calculada com base nos metadados da própria entity.
Por padrão, usa o primeiro argumento para buscar em pk() e indexes
getEntity(key, pk?)
Obtém todas as entities de um tipo com um argumento, ou uma única entity com dois
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)
Retorna a entrada do índice (mapa valor->pk)
const value = args[0][indexName];
return getIndex(schema.key, indexName, value)[value];
static createIfValid(processedEntity): Entity | undefined
Chamado ao desnormalizar uma entity. Cria uma instância desta classe se ela for considerada 'válida'.
Um retorno undefined resultará em status de expiração Invalid,
como Invalidate.
A expiração Invalid geralmente significa que hooks entrarão em um estado de carregamento e tentarão um novo fetch.
static createIfValid(props): AbstractInstanceType<this> | undefined {
if (this.validate(props)) {
return undefined as any;
}
return this.fromJS(props);
}
static validate(processedEntity): errorMessage?
Executado tanto na normalização quanto na desnormalização. Retornar uma string indica um erro (a string é a mensagem).
Durante a normalização, uma falha de validação resultará em erro para aquele fetch.
Durante a desnormalização, uma falha de validação marcará aquele resultado como 'inválido' e, portanto, bloqueará até que um resultado seja buscado.
Por padrão, faz algumas verificações básicas de existência de campos apenas em modo de desenvolvimento. Sobrescreva para desativar ou personalizar.