Pular para o conteúdo principal

Collection

Collections definem Listas (Array) ou Mapas (Values) mutáveis.

Isso significa que elas podem crescer e encolher. Você pode adicionar a Collection(Array) com .push ou .unshift, remover de Collection(Array) com .remove, adicionar a Collections(Values) com .assign e mover entre collections com .move.

RestEndpoint fornece os extenders .push, .unshift, .assign, .remove, .move e .getPage/ .paginated() ao usar Collections

Uso​

import React from 'react';
import { useController } from '@data-client/react';
import { getTodos } from './api/Todo';

export default function NewTodo({ userId }: { userId?: string }) {
  const ctrl = useController();
  const [unshift, setUnshift] = React.useState(false);

  const handlePress = async e => {
    if (e.key === 'Enter') {
      const createTodo = unshift ? getTodos.unshift : getTodos.push;
      ctrl.fetch(createTodo, {
        title: e.currentTarget.value,
        userId,
      });
      e.currentTarget.value = '';
    }
  };

  return (
    <div className="listItem nogap">
      <TextInput size="small" onKeyDown={handlePress} />
      <label>
        <input
          type="checkbox"
          checked={unshift}
          onChange={e => setUnshift(e.currentTarget.checked)}
        />{' '}
        unshift
      </label>
    </div>
  );
}
Resultado
Store▶

Collection com Values​

Quando uma API retorna objetos indexados por chave em vez de arrays, combine Collection com Values para habilitar mutações no resultado.

import { Entity, resource, Collection, Values } from '@data-client/rest';

class Stats extends Entity {
product_id = '';
volume = 0;
price = 0;

pk() {
return this.product_id;
}

static key = 'Stats';
}

export const StatsResource = resource({
urlPrefix: 'https://api.exchange.example.com',
path: '/products/:product_id/stats',
schema: Stats,
}).extend({
getList: {
path: '/products/stats',
// Collection wraps Values to enable .push, .assign, etc.
schema: new Collection(new Values(Stats)),
process(value) {
// Transform nested response structure
Object.keys(value).forEach(key => {
value[key] = {
...value[key].stats_24hour,
product_id: key,
};
});
return value;
},
},
});

Isso permite adicionar ou atualizar entradas com .assign. O corpo é um objeto em que as chaves são as chaves da collection e os valores são os dados da entity a mesclar:

// Local-only update with ctrl.set()
ctrl.set(StatsResource.getList.schema.assign, {}, {
'BTC-USD': { product_id: 'BTC-USD', volume: 1000 },
});

// Network request with ctrl.fetch() - see RestEndpoint.assign
await ctrl.fetch(StatsResource.getList.assign, {
'BTC-USD': { product_id: 'BTC-USD', volume: 1000 },
});

Opções​

argsKey e nestKey calculam a pk de uma Collection. argsKey é usado quando uma Collection é normalizada como resultado de endpoint de nível superior; nestKey é usado quando a mesma Collection está aninhada em uma Entity. Forneça ambos para reutilizar uma única definição de Collection nos dois contextos.

argsKey(...args): Object​

Retorna um Object serializável cujos membros definem de forma única esta collection com base nos argumentos do Endpoint.

import { RestEndpoint, Collection } from '@data-client/rest';

const userTodos = new Collection([Todo], {
argsKey: (urlParams: { userId?: string }) => ({
...urlParams,
}),
nestKey: (parent: { id: string }) => ({
userId: parent.id,
}),
});

const getTodos = new RestEndpoint({
path: '/todos',
searchParams: {} as { userId?: string },
schema: userTodos,
});

Quando omitido, argsKey assume o padrão params => ({ ...params }).

nestKey(parent, key): Object​

Retorna um Object serializável cujos membros definem de forma única esta collection com base no pai dentro do qual ela está aninhada.

A pk de uma Collection aninhada geralmente é melhor definida pelo que a contém. Isso permite que instâncias aninhadas de Collection compartilhem estado quando suas chaves têm o mesmo valor. Quando argsKey e nestKey retornam objetos com o mesmo formato, as leituras de nível superior e as aninhadas resolvem para o mesmo estado da collection.

import { Entity } from '@data-client/rest';
import { Todo, userTodos } from './Todo';

class User extends Entity {
id = '';
name = '';
username = '';
email = '';
todos: Todo[] = [];

static key = 'User';
static schema = {
todos: userTodos,
};
}

Nesse caso, user.todos e a resposta de getTodos() do exemplo de argsKey são sempre o mesmo array (referencialmente igual). Adicione as duas funções de chave à definição compartilhada da Collection:

const userTodos = new Collection([Todo], {
argsKey: ({ userId }: { userId?: string }) => ({ userId }),
nestKey: (parent: User) => ({ userId: parent.id }),
});

nonFilterArgumentKeys?​

Uma alternativa conveniente a argsKey

nonFilterArgumentKeys define um teste para determinar quais chaves de argumento não são usadas para filtrar os resultados. Por exemplo, se sua API usa 'orderBy' para escolher uma ordenação, esse argumento não influenciaria quais entities são incluídas na resposta.

const getPosts = new RestEndpoint({
path: '/:group/posts',
searchParams: {} as { orderBy?: string; author?: string },
schema: new Collection([Post], {
nonFilterArgumentKeys(key) {
return key === 'orderBy';
},
}),
});

Por conveniência, você também pode usar uma RegExp ou uma lista de strings:

const getPosts = new RestEndpoint({
path: '/:group/posts',
searchParams: {} as { orderBy?: string; author?: string },
schema: new Collection([Post], {
nonFilterArgumentKeys: /orderBy/,
}),
});
const getPosts = new RestEndpoint({
path: '/:group/posts',
searchParams: {} as { orderBy?: string; author?: string },
schema: new Collection([Post], {
nonFilterArgumentKeys: ['orderBy'],
}),
});

Nesse caso, author e group são consideradas chaves de argumento de 'filtro', o que significa que influenciarão se um item recém-criado deve ser adicionado a essas listas. Por outro lado, orderBy não precisa corresponder quando push é chamado.

import { Entity, Query, Collection, RestEndpoint } from '@data-client/rest';

class Post extends Entity {
  id = '';
  title = '';
  group = '';
  author = '';
}
export const getPosts = new RestEndpoint({
  path: '/:group/posts',
  searchParams: {} as { orderBy?: string; author?: string },
  schema: new Query(
    new Collection([Post], {
      nonFilterArgumentKeys: /orderBy/,
    }),
    (posts, { orderBy } = {}) => {
      if (orderBy) {
        return [...posts].sort((a, b) => a[orderBy].localeCompare(b[orderBy]));
      }
      return posts;
    },
  )
});
Resultado
Store▶

createCollectionFilter?​

Define um createCollectionFilter padrão para addWith(), push, unshift e assign.

Ele é usado por esses schemas de criação para determinar a quais collections adicionar.

Padrão:

createCollectionFilter(...args: Args) {
return (collectionKey: Record<string, string>) =>
Object.entries(collectionKey).every(
([key, value]) =>
this.nonFilterArgumentKeys(key) ||
// strings are canonical form. See pk() above for value transformation
`${args[0][key]}` === value ||
`${args[1]?.[key]}` === value,
);
}

Métodos​

Esses schemas de criação/remoção podem ser usados com Controller.set() para atualizações somente locais, sem requisições de rede. Para mutações baseadas em rede, veja os extenders especializados do RestEndpoint.

push​

Um schema de criação que coloca novos itens no fim desta collection.

// Add a new todo to the end of the list (local only, no network request)
ctrl.set(getTodos.schema.push, { userId: '1' }, { id: '999', title: 'New Todo' });

unshift​

Um schema de criação que coloca novos itens no início desta collection.

// Add a new todo to the beginning of the list (local only)
ctrl.set(getTodos.schema.unshift, { userId: '1' }, { id: '999', title: 'New Todo' });

remove​

Um schema que remove itens de uma collection por valor.

O valor da entity é normalizado para extrair sua pk, que é então comparada com os membros da collection. Os itens são removidos de todas as collections que correspondem aos args fornecidos (filtradas por createCollectionFilter).

// Remove from collections matching { userId: '1' } (local only)
ctrl.set(getTodos.schema.remove, { userId: '1' }, { id: '123' });
// Remove from all collections (empty args matches all)
ctrl.set(getTodos.schema.remove, {}, { id: '123' });

Para remoção baseada em rede que também atualiza a entity, veja RestEndpoint.remove.

move​

Um schema que move itens entre collections. Ele remove a entity das collections que correspondem ao seu estado existente e a adiciona às collections que correspondem ao novo estado da entity (derivado do último arg).

Isso funciona tanto para Collection(Array) quanto para Collection(Values).

// Move todo from userId '1' collection to userId '2' collection (local only)
ctrl.set(
getTodos.schema.move,
{ id: '10', userId: '2', title: 'Moved todo' },
[{ id: '10' }, { userId: '2' }],
);

O filtro de remoção usa os valores existentes da entity no store para determinar a quais collections ela pertence atualmente. O filtro de adição usa os valores mesclados da entity (existentes + último arg) para determinar onde ela deve ser colocada.

Para moves baseados em rede, veja RestEndpoint.move.

assign​

Um schema de criação que atribui seus membros a uma Collection(Values). Disponível apenas para Collections que envolvem Values.

const getStats = new RestEndpoint({
path: '/products/stats',
schema: new Collection(new Values(Stats)),
});

// Add/update entries in a Values collection (local only)
ctrl.set(getStats.schema.assign, {}, {
'BTC-USD': { product_id: 'BTC-USD', volume: 1000 },
'ETH-USD': { product_id: 'ETH-USD', volume: 500 },
});

addWith(merge, createCollectionFilter): CreationSchema​

Constrói um schema de criação personalizado para esta collection. Ele é usado por push, unshift, assign e paginate

merge(collection, creation)​

Isso mescla o valor com a collection existente

createCollectionFilter​

Esta função é usada para determinar a quais collections adicionar. Ela usa o Object retornado por argsKey ou nestKey para determinar se essa collection deve receber os valores recém-criados por este schema.

Como os argumentos podem ser tipos serializáveis como number, recomendamos usar comparações com ==, por exemplo, '10' == 10

(...args) =>
collectionKey =>
boolean;

moveWith(merge): MoveSchema​

Constrói um schema de move personalizado para esta collection. É análogo a addWith, mas para operações de move. A função merge controla como as entities são adicionadas à collection de destino, enquanto o comportamento de remoção é derivado automaticamente do tipo da collection (Array ou Values).

Isso é útil quando você precisa controlar a posição de inserção dos itens movidos (por exemplo, inserir no início em vez de no fim).

merge(collection, moved)​

Controla como a entity movida é adicionada à collection de destino.

A função de merge unshift, exportada, coloca os itens no início:

import { Collection, unshift, type CollectionOptions } from '@data-client/rest';
import type { PolymorphicInterface } from '@data-client/endpoint';

class MyCollection<
S extends any[] | PolymorphicInterface = any,
Args extends any[] = any[],
Parent = any,
> extends Collection<S, Args, Parent> {
constructor(schema: S, options?: CollectionOptions<Args, Parent>) {
super(schema, options);
// Prepend moved items instead of appending
this.move = this.moveWith(unshift);
}
}

unshift (merge function)​

Uma função de merge que coloca os itens recebidos no início da collection. Use com moveWith ou addWith para controlar a ordem de inserção.

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

Métodos de ciclo de vida​

Método estático 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 reordenará a ordem dos argumentos entre a entity recebida e a 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.

static merge(existing, incoming): mergedValue​

static merge(existing: any, incoming: any) {
return incoming;
}

Método estático mergeWithStore(existingMeta, incomingMeta, existing, incoming): mergedValue​

static mergeWithStore(
existingMeta: { date: number; fetchedAt: number },
incomingMeta: { date: number; fetchedAt: number },
existing: any,
incoming: any,
): any;

mergeWithStore() é chamado durante a normalização quando uma entity processada já é encontrada no store.

pk: (parent?, key?, args?, parentEntity?): pk?​

pk() chama nestKey quando está aninhada em uma Entity e ele está disponível; caso contrário, chama argsKey. Em seguida, serializa o resultado para a string da pk.

pk(
value: any,
parent: any,
key: string,
args: readonly any[],
parentEntity?: any,
) {
const obj =
parentEntity && this.nestKey
? this.nestKey(parent, key)
: this.argsKey(...args);
for (const key in obj) {
if (typeof obj[key] !== 'string') obj[key] = `${obj[key]}`;
}
return JSON.stringify(obj);
}