Pular para o conteúdo principal

SchemaSimple

SchemaSimple é a interface que todo schema implementa. Implemente-a você mesmo para ensinar ao @data-client/rest como normalizar, desnormalizar e consultar um valor que os schemas embutidos não conseguem expressar.

A maioria dos apps nunca precisa de um, então consulte primeiro a Visão geral de Schemas. Recorra a um schema personalizado apenas quando precisar de lógica em tempo de execução que os embutidos não têm, como uma saída que depende dos args do endpoint ou a travessia limitada de grafos profundos de entities.

Uso​

Este schema armazena todas as traduções de um campo e entrega aos componentes apenas a do locale que eles pediram:

import { Entity, RestEndpoint } from '@data-client/rest';
import type { IDenormalizeDelegate } from '@data-client/rest';

const localeKey = (args: readonly any[]) => args[0]?.locale;

class LocalizedText {
normalize(input: Record<string, string>) {
return input;
}

denormalize(
input: Record<string, string>,
delegate: IDenormalizeDelegate,
) {
const locale = delegate.argsKey(localeKey) ?? 'en';
return input[locale] ?? input.en;
}

queryKey() {
return undefined;
}
}

class Product extends Entity {
id = '';
name = '';

static key = 'Product';
static schema = {
name: new LocalizedText(),
};
}

const getProduct = new RestEndpoint({
path: '/products/:id',
searchParams: {} as { locale?: string },
schema: Product,
});

useSuspense(getProduct, { id: '5', locale: 'fr' }) retorna um Product cujo name é a string em francês, enquanto o store mantém todos os locales.

delegate.argsKey() informa ao cache que a saída depende de locale, então trocar de locale recalcula o valor. Ler delegate.args diretamente retornaria resultados desatualizados. O seletor deve ser uma referência de função estável, então defina-o no escopo do módulo ou uma única vez na instância do schema.

Membros​

normalize(input, parent, key, delegate, parentEntity?)​

Transforma o valor bruto da resposta nesta posição no que é armazenado no resultado do endpoint. Chame delegate.visit() para normalizar schemas aninhados, em vez de chamar diretamente os métodos deles.

normalize(input: any, parent: any, key: string | undefined, delegate: INormalizeDelegate) {
return {
...input,
data: delegate.visit(this.schema, input.data, input, 'data'),
};
}

Para um wrapper cujo schema é User, uma resposta { data: { id: '5', name: 'Ada' }, requestId: 'abc' } é armazenada como { data: '5', requestId: 'abc' }, com o User na tabela de entities.

normalize() só é executado para entradas do tipo objeto. Para um schema sem pk, primitivos passam sem alteração, a menos que ele defina acceptsPrimitives = true, de modo que um wrapper em volta de uma entity armazena um id simples exatamente como a API o enviou. (Uma Entity simples armazena ids truthy como strings, então 5 vira '5'.) Da mesma forma, denormalize() nunca recebe null ou undefined.

parentEntity é o schema de entity envolvente mais próximo (a classe à qual este campo pertence), se houver. A maioria dos schemas o ignora; Scalar o usa para encontrar sua vinculação com a entity.

denormalize(input, delegate)​

Recebe o que normalize() retornou e constrói o valor que os hooks e o Controller retornam. Chame delegate.unvisit() para schemas aninhados.

denormalize(input: any, delegate: IDenormalizeDelegate) {
return {
...input,
data: delegate.unvisit(this.schema, input.data),
};
}

queryKey(args, unvisit, delegate)​

Constrói o valor normalizado a ser procurado quando o schema é lido do store sem fazer fetch, como em useQuery(), Controller.get ou Query. Normalmente espelha o formato que normalize() retorna; unvisit pede a um schema aninhado a sua própria query key.

queryKey(args: readonly any[], unvisit: (schema: any, args: readonly any[]) => any) {
const data = unvisit(this.schema, args);
return data === undefined ? undefined : { data };
}

Retorne undefined quando o store não tiver o suficiente para responder, e delegate.INVALID quando o resultado em cache for sabidamente inválido.

Delegates​

INormalizeDelegate​

Passado para normalize().

MembroDescrição
visit(schema, value, parent, key)Normaliza value com um schema aninhado
argsArgs do endpoint
meta{ fetchedAt, date, expiresAt } da resposta
getEntity(key, pk)Lê uma entity armazenada
getEntities(key)Lê todas as entities armazenadas de um tipo
mergeEntity(schema, pk, entity)Armazena uma entity por meio de seu ciclo de vida de merge
setEntity(schema, pk, entity, meta?)Armazena uma entity, substituindo o que havia
invalidate(schema, pk)Marca uma entity como inválida, suspendendo os componentes que dependem dela
checkLoop(key, pk, input)true quando esta entrada já foi normalizada como (key, pk) nesta chamada; pare a recursão

getEntity até invalidate só são necessários para schemas semelhantes a entity.

IDenormalizeDelegate​

Passado para denormalize().

MembroDescrição
unvisit(schema, input)Desnormaliza input com um schema aninhado
argsKey(fn)Retorna fn(args) e recalcula a saída quando esse valor muda
argsArgs do endpoint. Não rastreia mudanças; use argsKey() quando a saída depender deles

IQueryDelegate​

Passado para queryKey().

MembroDescrição
getEntity(key, pk)Lê uma entity armazenada
getEntities(key)Lê todas as entities armazenadas de um tipo
getIndex(key, index, value)Encontra uma pk por um índice de Entity
INVALIDRetorne isto para marcar o resultado como inválido

Schemas semelhantes a entity​

Qualquer schema com um membro pk é tratado como uma entity: ele é armazenado e memoizado por key e pk, deduplicado entre ciclos e sujeito a maxEntityDepth. Ele deve então fornecer também key, createIfValid() e denormalize(). Estenda Entity em vez de construir isso você mesmo.

Exemplo: relacionamentos com profundidade limitada​

Grafos bidirecionais profundos (Department ↔ Building ↔ Room) tornam a desnormalização cara. Lazy é a correção recomendada e maxEntityDepth limita a profundidade total de aninhamento de entities; um schema personalizado pode, em vez disso, limitar a travessia por relacionamento, resolvendo exatamente N níveis.

DepthLimited resolve até maxDepth níveis de um relacionamento e depois retorna as pks. Um único delegate é compartilhado em toda uma chamada de desnormalização, então um WeakMap indexado por ele guarda o estado por chamada.

import { Entity } from '@data-client/rest';
import type {
IDenormalizeDelegate,
INormalizeDelegate,
Schema,
} from '@data-client/rest';

class DepthLimited<S extends Schema> {
private readonly _state = new WeakMap<
IDenormalizeDelegate,
{ depth: number }
>();

constructor(
readonly schema: S,
readonly maxDepth: number,
) {}

normalize(
input: any,
parent: any,
key: any,
delegate: INormalizeDelegate,
) {
return delegate.visit(this.schema, input, parent, key);
}

denormalize(input: {}, delegate: IDenormalizeDelegate) {
let cell = this._state.get(delegate);
if (!cell) {
cell = { depth: 0 };
this._state.set(delegate, cell);
}
cell.depth++;
try {
if (cell.depth > this.maxDepth) return input;
return delegate.unvisit(this.schema, input);
} finally {
cell.depth--;
}
}

queryKey(): undefined {
return undefined;
}
}

class Department extends Entity {
id = '';
name = '';

static key = 'Department';
static schema = {
children: new DepthLimited([Department], 3),
parent: new DepthLimited(Department, 1),
};
}

As entities desnormalizadas são memoizadas por entity, não por profundidade. Uma entity alcançada primeiro além de maxDepth é armazenada em cache com esse relacionamento deixado como pks, e uma leitura direta posterior dela no mesmo store retorna essa forma truncada.

Veja a discussão #3828 para uma variante que detecta ciclos e os trade-offs em relação a Lazy.