Pular para o conteúdo principal

EntityMixin

Entity define um único objeto único.

Se você já tem classes para os seus tipos de dados, o EntityMixin pode ser para você.

import { EntityMixin } from '@data-client/rest';

export class Article {
  id = '';
  title = '';
  content = '';
  tags: string[] = [];
}

export class ArticleEntity extends EntityMixin(Article) {}

Opções​

O segundo argumento do mixin pode ser usado para personalizar a construção de forma conveniente. Se não for especificado, os membros estáticos da classe Base serão usados. Alternativamente, assim como com Entity, você sempre pode especificá-los como membros estáticos da classe 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, padrão 'id'​

Especifica o Entity.pk

Uma string indica o campo a ser usado como pk.

Uma function é usada exatamente como Entity.pk, mas o primeiro argumento (value) é this

O padrão é 'id'; o que significa que pk é uma opção obrigatória a menos que a classe Base tenha um membro id serializável.

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 o Entity.key

schema: {[k:string]: Schema}​

Especifica o Entity.schema

const vs class​

Se você não precisa personalizar mais a entity, pode usar uma declaração const em vez de estender outra classe com extend.

Há uma diferença sutil ao se referir ao class token em TypeScript: declarações class se referem ao tipo da instância, enquanto const tokens se referem ao valor, então você precisa usar typeof; além disso, typeof fornece o tipo da classe, então é preciso aplicar InstanceType por cima.

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 sobrescrever métodos de ciclo de vida como process(), você deve usar a forma class ... extends EntityMixin(...) {}. As opções de EntityMixin() incluem apenas pk, key e schema; as sobrescritas de ciclo de vida ficam na própria classe.

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 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​

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

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.

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() é 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';
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;
}

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​

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

Explore o exemplo coin-app

More Demos

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

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)​

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.

Usando validação em endpoints com campos incompletos