Pular para o conteúdo principal

RestEndpoint

RestEndpoints são para protocolos baseados em HTTP, como o REST.

estende

RestEndpoint estende Endpoint

Interface
interface RestGenerics {
readonly path: string;
readonly schema?: Schema | undefined;
readonly method?: string;
readonly body?: any;
readonly searchParams?: any;
readonly paginationField?: string;
readonly content?: 'json' | 'blob' | 'text' | 'arrayBuffer' | 'stream';
process?(value: any, ...args: any): any;
}

export class RestEndpoint<O extends RestGenerics = any> extends Endpoint {
/* Prepare fetch */
readonly path: string;
readonly urlPrefix: string;
readonly requestInit: RequestInit;
readonly method: string;
readonly paginationField?: string;
readonly content?: 'json' | 'blob' | 'text' | 'arrayBuffer' | 'stream';
readonly signal: AbortSignal | undefined;
url(...args: Parameters<F>): string;
searchToString(searchParams: Record<string, any>): string;
getRequestInit(
this: any,
body?: RequestInit['body'] | Record<string, unknown>,
): Promise<RequestInit> | RequestInit;
getHeaders(headers: HeadersInit): Promise<HeadersInit> | HeadersInit;

/* Perform/process fetch */
fetchResponse(input: RequestInfo, init: RequestInit): Promise<Response>;
parseResponse(response: Response): Promise<any>;
process(value: any, ...args: Parameters<F>): any;

testKey(key: string): boolean;
}

Uso​

Todas as opções são aceitas como argumentos do construtor, de extend e como sobrescritas ao usar herança

Busca mais simples​

const getTodo = new RestEndpoint({
path: '/todos/:id',
});
const todo = await getTodo({ id: 1 });

Compartilhamento de configuração​

Use RestEndpoint.extend() em vez de {...getTodo} (spread de objeto)

const updateTodo = getTodo.extend({ method: 'PUT' });

Gerenciando o estado​

export class Todo extends Entity {
  id = '';
  title = '';
  completed = false;
}

export const getTodo = new RestEndpoint({
  urlPrefix: 'https://jsonplaceholder.typicode.com',
  path: '/todos/:id',
  schema: Todo,
});
export const updateTodo = getTodo.extend({ method: 'PUT' });

Usar um Schema possibilita a consistência automática dos dados sem a necessidade de prejudicar o desempenho com novos fetches.

Tipagem​

import { Comment } from './Comment';

const getComments = new RestEndpoint({
  path: '/posts/:postId/comments',
  schema: new Collection([Comment]),
  searchParams: {} as { sortBy?: 'votes' | 'recent' } | undefined,
});

// Hover your mouse over 'comments' to see its type
const comments = useSuspense(getComments, {
  postId: '5',
  sortBy: 'votes',
});

const ctrl = useController();
const createComment = async data =>
  ctrl.fetch(getComments.push, { postId: '5' }, data);

Resolução/Retorno​

schema determina o valor de retorno quando usado com hooks de vinculação de dados, como useSuspense, useDLE, useCache, ou quando usado com Controller.fetch

import { Todo } from './Todo';

const getTodo = new RestEndpoint({ path: '/', schema: Todo });
// Hover your mouse over 'todo' to see its type
const todo = useSuspense(getTodo);

async () => {
  const ctrl = useController();
  const todo2 = await ctrl.fetch(getTodo);
};

process determina o valor de resolução quando o endpoint é chamado diretamente. Para RestEndpoints sem schema, também determina o tipo de retorno dos hooks e de Controller.fetch.

interface TodoInterface {
  title: string;
  completed: boolean;
}
const getTodo = new RestEndpoint({
  path: '/',
  process(value): TodoInterface {
    return value;
  },
});
async () => {
  // todo is TodoInterface
  const todo = await getTodo();

  const ctrl = useController();
  const todo2 = await ctrl.fetch(getTodo);
};

Parâmetros da função​

path, usado para construir a url, determina o tipo do primeiro argumento. Se não tiver padrões, o 'primeiro' argumento é omitido.

const getRoot = new RestEndpoint({ path: '/' });
getRoot();
const getById = new RestEndpoint({ path: '/:id' });
// both number and string types work as they are serialized into strings to construct the url
getById({ id: 5 });
getById({ id: '5' });

method determina se existe um segundo argumento a ser enviado como body.

export const update = new RestEndpoint({
  path: '/:id',
  method: 'PUT',
});
update({ id: 5 }, { title: 'updated', completed: true });

No entanto, ele é tipado como 'any', então não detecta erros de digitação.

body pode ser usado para tipar o argumento após os parâmetros da url. Ele é usado apenas para tipagem, então o valor enviado não importa. O valor undefined pode ser usado para 'desabilitar' o segundo argumento.

export const update = new RestEndpoint({
  path: '/:id',
  method: 'PUT',
  body: {} as TodoInterface,
});
update({ id: 5 }, { title: 'updated', completed: true });
// `undefined` disables 'body' argument
const rpc = new RestEndpoint({
  path: '/:id',
  method: 'PUT',
  body: undefined,
});
rpc({ id: 5 });

searchParams pode ser usado de forma semelhante a body para especificar os tipos de parâmetros extras, usados nos searchParams/queryParams do GET em uma url().

const getUsers = new RestEndpoint({
path: '/:group/user/:id',
searchParams: {} as { isAdmin?: boolean; sort: 'asc' | 'desc' },
});
getUsers.url({ group: 'big', id: '5', sort: 'asc' }) ===
'/big/user/5?sort=asc';
getUsers.url({
group: 'big',
id: '5',
sort: 'desc',
isAdmin: true,
}) === '/big/user/5?isAdmin=true&sort=desc';

Ciclo de vida do fetch​

RestEndpoint acrescenta ao Endpoint personalizações para um método de fetch fornecido, usando herança ou .extend().

fetch implementation for RestEndpoint
function fetch(...args) {
const urlParams = this.#hasBody && args.length < 2 ? {} : args[0] || {};
const body = this.#hasBody ? args[args.length - 1] : undefined;
return this.fetchResponse(
this.url(urlParams),
await this.getRequestInit(body),
)
.then(response => this.parseResponse(response))
.then(res => this.process(res, ...args));
}

Preparar o fetch​

Os membros também servem como opções (segundo argumento do construtor). Embora nenhum seja obrigatório, os primeiros têm valores padrão.

url(params): string​

urlPrefix + path template + '?' + searchToString(searchParams)

url() usa os params para preencher o path template. Os membros de params não utilizados são então usados como searchParams (também chamados de params do 'GET' - o que vem depois de ?).

Implementação
import { getUrlBase, getUrlTokens } from '@data-client/rest';

url(urlParams = {}) {
const urlBase = getUrlBase(this.path)(urlParams);
const tokens = getUrlTokens(this.path);
const searchParams = {};
Object.keys(urlParams).forEach(k => {
if (!tokens.has(k)) {
searchParams[k] = urlParams[k];
}
});
if (Object.keys(searchParams).length) {
return `${this.urlPrefix}${urlBase}?${this.searchToString(searchParams)}`;
}
return `${this.urlPrefix}${urlBase}`;
}

searchToString(searchParams): string​

Constrói o componente searchParams da url.

Por padrão, usa o global padrão URLSearchParams.

Os searchParams (também chamados de queryParams) são ordenados para manter o determinismo.

Implementação
searchToString(searchParams) {
const params = new URLSearchParams(searchParams);
params.sort();
return params.toString();
}

Usando a biblioteca qs​

Para codificar objetos complexos nos searchParams, você pode usar a biblioteca qs.

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

class QSEndpoint<O extends RestGenerics = any> extends RestEndpoint<O> {
searchToString(searchParams) {
return qs.stringify(searchParams);
}
}
import QSEndpoint from './QSEndpoint';

const getFoo = new QSEndpoint({
  path: '/foo',
  searchParams: {} as { a: Record<string, string> },
});

getFoo({ a: { b: 'c' } });
Request
GET /foo?a%5Bb%5D=c
content-type: application/json

path: string​

Usa o path-to-regexp v8 para construir urls a partir dos parâmetros passados. Isso também informa os tipos, de modo que sejam aplicados corretamente.

Parâmetros​

Palavras com o prefixo : são nomes de parâmetros. Tanto strings quanto números são aceitos como valores, pois são serializados na string da url.

const getThing = new RestEndpoint({ path: '/:group/things/:id' });
getThing({ group: 'first', id: 77 });

Parâmetros opcionais​

Envolva o segmento opcional (incluindo seu prefixo) em {} para torná-lo opcional. O tipo dos parâmetros opcionais passa a ser string | number | undefined.

const optional = new RestEndpoint({
  path: '/:group/things{/:number}',
});
optional({ group: 'first' });
optional({ group: 'first', number: 'fifty' });

Vários segmentos opcionais podem ser encadeados com prefixos diferentes:

const ep = new RestEndpoint({
path: '{/:attr1}{-:attr2}{-:attr3}',
});

ep({ attr1: 'hi' });
ep({ attr2: 'hi' });
ep({ attr1: 'hi', attr3: 'ho' });

Wildcards (parâmetros repetidos)​

*name corresponde a um ou mais segmentos do path. Envolva em {} para torná-lo zero ou mais (opcional). Parâmetros wildcard são tipados como string[] (arrays), pois representam vários segmentos do path.

const files = new RestEndpoint({ path: '/files/*path' });
files({ path: ['documents', 'reports', 'q4'] });
// URL: /files/documents/reports/q4

const optionalFiles = new RestEndpoint({ path: '/files{/*path}' });
optionalFiles({});
// URL: /files
optionalFiles({ path: ['documents'] });
// URL: /files/documents

Nomes de parâmetros entre aspas​

Os nomes de parâmetros devem ser identificadores JavaScript válidos. Nomes que contenham caracteres especiais como - ou . devem ser colocados entre aspas duplas:

const ep = new RestEndpoint({ path: '/:"with-dash"/:"my.param"' });
ep({ 'with-dash': 'hello', 'my.param': 'world' });

Escapando caracteres especiais​

Os caracteres {}()*: e \\ são especiais no path-to-regexp e devem ser escapados com \\ quando usados como literais.

const getSite = new RestEndpoint({
  path: 'https\\://site.com/:slug',
});
getSite({ slug: 'first' });

? e + não são especiais no path-to-regexp v8 e não precisam ser escapados. Isso significa que query strings podem ser incorporadas ao path sem escapar ?:

const search = new RestEndpoint({
path: '/search?{q=:q}{&page=:page}',
});
search({ q: 'test', page: 1 });
// URL: /search?q=test&page=1
informação

Os tipos são inferidos automaticamente a partir de path.

Parâmetros adicionais podem ser especificados com searchParams e body.

searchParams​

searchParams pode ser usado para especificar os tipos de parâmetros extras, usados nos searchParams/queryParams do GET em uma url().

O valor em si não é usado de nenhuma forma - isso apenas determina a tipagem.

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

const getReactSite = new RestEndpoint({
  path: 'https\\://site.com/:slug',
  searchParams: {} as { isReact: boolean },
});

getReactSite({ slug: 'cool', isReact: true });
Request
GET https://site.com/cool?isReact=true
content-type: application/json

body​

body pode ser usado para definir um segundo argumento para endpoints de mutação. O valor em si não é usado de nenhuma forma - isso apenas determina a tipagem.

Isso só é usado por endpoints com um método que usa body: 'POST', 'PUT', 'PATCH'.

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

const updateSite = new RestEndpoint({
  path: 'https\\://site.com/:slug',
  method: 'POST',
  body: {} as { url: string },
});

updateSite({ slug: 'cool' }, { url: '/' });
Request
POST https://site.com/cool
content-type: application/json
Body: { "url": "/" }

paginationField​

Se especificado, adiciona o método getPage ao RestEndpoint. Guia de paginação. O schema também deve conter uma Collection.

urlPrefix: string = ''​

Adiciona este valor como prefixo ao path compilado

Padrões por herança​

export class MyEndpoint<
O extends RestGenerics = any,
> extends RestEndpoint<O> {
// this allows us to override the prefix in production environments, with a dev fallback
urlPrefix = process.env.API_SERVER ?? 'http://localhost:8000';
}

Saiba mais sobre padrões de herança para RestEndpoint

Sobrescritas na instância​

export const getTicker = new RestEndpoint({
urlPrefix: 'https://api.exchange.coinbase.com',
path: '/products/:product_id/ticker',
schema: Ticker,
});

Prefixo dinâmico​

dica

Para um prefixo dinâmico, tente sobrescrever o método url():

const getTodo = new RestEndpoint({
path: '/todo/:id',
url(...args) {
return dynamicPrefix() + super.url(...args);
},
});

method: string = 'GET'​

O Method faz parte do protocolo HTTP. Os protocolos REST os usam para indicar o tipo de operação. Por isso, o RestEndpoint usa isso para informar sideEffect e se o endpoint deve usar um payload body. Definir sideEffect explicitamente sobrescreve esse comportamento, permitindo designs de API fora do padrão.

GET é 'somente leitura'; os demais métodos implicam efeitos colaterais (sideEffects).

GET e DELETE têm, por padrão, nenhum body.

Como method afeta os parâmetros da função

method só influencia os parâmetros no construtor do RestEndpoint e não em .extend(). Isso permite combinações de método e body fora do padrão.

body terá any como padrão. Você sempre pode definir body explicitamente para ter controle total. undefined pode ser usado para indicar que não há body.

(id: string, myPayload: Record<string, unknown>) => {
  const standardCreate = new RestEndpoint({
    path: '/:id',
    method: 'POST',
  });
  standardCreate({ id }, myPayload);
  const nonStandardEndpoint = new RestEndpoint({
    path: '/:id',
    method: 'POST',
    body: undefined,
  });
  // no second 'body' argument, because body was set to 'undefined'
  nonStandardEndpoint({ id });
};

getRequestInit(body): RequestInit​

Prepara o RequestInit usado no fetch. Ele é enviado para fetchResponse

Um body que seja um objeto simples ou um array é codificado como JSON, com um header Content-Type: application/json, a menos que requestInit ou getHeaders defina um. Qualquer outro body, como FormData, Blob, URLSearchParams ou uma string, é passado ao fetch() como está.

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

export default class AuthdEndpoint<
  O extends RestGenerics = any,
> extends RestEndpoint<O> {
  async getRequestInit(body) {
    return {
      ...(await super.getRequestInit(body)),
      method: await getMethod(),
    };
  }
}

async function getMethod() {
  return 'GET';
}

getHeaders(headers: HeadersInit): HeadersInit​

Chamado por getRequestInit para determinar os headers HTTP

Isso costuma ser útil para autenticação

aviso

Não use hooks aqui. Se você precisar usar hooks, tente usar hookifyResource

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

export default class AuthdEndpoint<
  O extends RestGenerics = any,
> extends RestEndpoint<O> {
  async getHeaders(headers: HeadersInit) {
    return {
      ...headers,
      'Access-Token': await getAuthToken(),
    };
  }
}

async function getAuthToken() {
  return 'example';
}

Tratar o fetch​

fetchResponse(input, init): Promise​

Executa a chamada fetch(input, init). Quando response.ok não é true (como em um 404), lança um NetworkError.

content​

Controla como o body da Response é interpretado. Quando definido, o tipo de retorno é inferido automaticamente e schema é restringido a undefined para tipos de conteúdo que não sejam JSON.

ValorInterpretado viaTipo de retorno
'json'response.json()any
'blob'response.blob()Blob
'text'response.text()string
'arrayBuffer'response.arrayBuffer()ArrayBuffer
'stream'response.bodyReadableStream<Uint8Array>
não definidoDetecção automática pelo header Content-Typeany

Quando content não está definido, parseResponse detecta automaticamente o tipo da resposta pelo header Content-Type: tipos JSON chamam .json(), tipos binários (imagens, application/octet-stream, PDFs etc.) chamam .blob() e tipos textuais chamam .text().

Download de arquivos​

Para downloads de arquivos, defina content: 'blob'. O tipo de retorno é Blob e schema deve ser undefined (dados binários não podem ser normalizados). Use dataExpiryLength: 0 para evitar manter blobs grandes em cache na memória.

const downloadFile = new RestEndpoint({
path: '/files/:id/download',
content: 'blob',
dataExpiryLength: 0,
});

Para extrair o nome do arquivo do header Content-Disposition, sobrescreva parseResponse:

const downloadFile = new RestEndpoint({
path: '/files/:id/download',
content: 'blob',
dataExpiryLength: 0,
async parseResponse(response) {
const blob = await response.blob();
const disposition = response.headers.get('Content-Disposition');
const filename =
disposition?.match(/filename="?(.+?)"?$/)?.[1] ?? 'download';
return { blob, filename };
},
process(value): { blob: Blob; filename: string } {
return value;
},
});

Veja o guia de download de arquivos para o uso completo, com o disparo do download no navegador.

parseResponse(response): Promise​

Recebe a Response e interpreta o body.

Quando content está definido, ele controla diretamente a interpretação. Caso contrário, a detecção automática é executada com base no header Content-Type: tipos JSON chamam .json(), tipos binários chamam .blob() e tipos textuais chamam .text().

Se status for 204, resolve como null.

Sobrescreva isto para casos avançados, como extrair headers junto com o body.

process(value, ...args): any​

Realiza quaisquer transformações com o resultado interpretado. Por padrão é a função identidade (não faz nada).

args são os argumentos com os quais o endpoint foi chamado. Eles são tipados a partir de path, searchParams e body do endpoint, incluindo os definidos na mesma chamada de extend().

const getUser = new RestEndpoint({ path: '/users/:id' });

const getUserWithId = getUser.extend({
process(value, params) {
// params is { id: string | number }
return { ...value, id: `${params.id}` };
},
});
dica

O tipo de retorno de process pode ser usado para definir o tipo de retorno do fetch do endpoint:

▶getTodo.ts
export const getTodo = new RestEndpoint({
  path: '/todos/:id',
  // The identity function is the default value; so we aren't changing any runtime behavior
  process(value): TodoInterface {
    return value;
  },
});

interface TodoInterface {
  id: string;
  title: string;
  completed: boolean;
}
▶useTodo.ts
import { getTodo } from './getTodo';

async (id: string) => {
  // hover title to see it is a string
  // see TS autocomplete by deleting `.title` and retyping the `.`
  const title = (await getTodo({ id })).title;
};

Ciclo de vida do Endpoint​

schema?: Schema​

Ciclo de vida declarativo dos dados

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

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

const getUser = new RestEndpoint({
path: '/users/:id',
schema: User,
});

key(urlParams): string​

Serializa os parâmetros. É usado para construir uma chave de busca nas stores globais.

Padrão:

`${this.method} ${this.url(urlParams)}`;

testKey(key): boolean​

Retorna true se a key (de fetch) fornecida corresponde a este endpoint.

Isso é usado para interceptors de mock com o <MockResolver />, Controller.expireAll(), and Controller.invalidateAll().

dataExpiryLength?: number​

Tempo de vida personalizado, no cache, dos dados do recurso buscado. Substitui o valor definido no NetworkManager.

Saiba mais sobre o tempo de expiração

errorExpiryLength?: number​

Tempo de vida personalizado dos erros de dados do recurso buscado. Substitui o valor definido no NetworkManager.

errorPolicy?: (error: any) => 'soft' | undefined​

'soft' usará dados desatualizados (se existirem) em caso de erro; undefined, ou não informar a opção, resultará em erro.

Saiba mais sobre errorPolicy

errorPolicy(error) {
return error.status >= 500 ? 'soft' : undefined;
}

invalidIfStale: boolean​

Indica que dados desatualizados devem ser considerados inutilizáveis e, portanto, não ser retornados do cache. Isso significa que useSuspense() vai suspender quando os dados estiverem desatualizados, mesmo que já existam no cache.

pollFrequency: number​

Frequência, em milissegundos, do polling. Requer o uso de useSubscription() ou useLive() para ter efeito.

getOptimisticResponse: (snap, ...args) => expectedResponse​

Quando informado, qualquer fetch com este endpoint se comportará como se o valor de retorno expectedResponse desta função fosse uma resposta de rede bem-sucedida. Quando o fetch real for concluído (com falha ou com sucesso), a atualização otimista será substituída pela resposta real da rede.

import { resource } from '@data-client/rest';
import { Post } from './Post';

export { Post };

export const PostResource = resource({
  path: '/posts/:id',
  searchParams: {} as { userId?: string | number } | undefined,
  schema: Post,
}).extend('vote', {
  path: '/posts/:id/vote',
  method: 'POST',
  body: undefined,
  schema: Post,
  getOptimisticResponse(snapshot, { id }) {
    const post = snapshot.get(Post, { id });
    if (!post) throw snapshot.abort;
    return {
      id,
      votes: post.votes + 1,
    };
  },
});
Resultado
Store▶
Guia de atualizações otimistas

update()​

(normalizedResponseOfThis, ...args) =>
({ [endpointKey]: (normalizedResponseOfEndpointToUpdate) => updatedNormalizedResponse) })
dica

Experimente usar Collections no lugar.

Elas são muito mais fáceis de usar e mais robustas!

UpdateType.ts
type UpdateFunction<
Source extends EndpointInterface,
Updaters extends Record<string, any> = Record<string, any>,
> = (
source: ResultEntry<Source>,
...args: Parameters<Source>
) => { [K in keyof Updaters]: (result: Updaters[K]) => Updaters[K] };

Caso mais simples:

userEndpoint.ts
const createUser = new RestEndpoint({
path: '/user',
method: 'POST',
schema: User,
update: (newUserId: string) => ({
[userList.key()]: (users = []) => [newUserId, ...users],
}),
});

Mais atualizações:

Component.tsx
const allusers = useSuspense(userList);
const adminUsers = useSuspense(userList, { admin: true });

O endpoint abaixo garante que o novo usuário apareça imediatamente nos usos acima.

userEndpoint.ts
const createUser = new RestEndpoint({
path: '/user',
method: 'POST',
schema: User,
update: (newUserId, newUser) => {
const updates = {
[userList.key()]: (users = []) => [newUserId, ...users],
];
if (newUser.isAdmin) {
updates[userList.key({ admin: true })] = (users = []) => [newUserId, ...users];
}
return updates;
},
});

extend(options): RestEndpoint​

Pode ser usado para personalizar ainda mais a definição do endpoint

const getUser = new RestEndpoint({ path: '/users/:id' });

const UserDetailNormalized = getUser.extend({
schema: User,
getHeaders(headers: HeadersInit): HeadersInit {
return {
...headers,
'Access-Token': getAuth(),
};
},
});

Extensores especializados​

Estes acessores de conveniência criam novos endpoints para operações comuns de Collection. Só funcionam quando o schema do RestEndpoint contém uma Collection.

push​

Cria um endpoint POST que coloca as Entities recém-criadas no fim de uma Collection.

Retorna um novo RestEndpoint com method: 'POST' e schema: Collection.push

import { RestEndpoint, Collection } from '@data-client/rest';
import { useController } from '@data-client/react';
import { Todo } from './resources';

const getTodos = new RestEndpoint({
path: '/todos',
searchParams: {} as { userId?: string },
schema: new Collection([Todo]),
});
const ctrl = useController();

// POST /todos - adds new Todo to the end of the list
const newTodo = await ctrl.fetch(
getTodos.push,
{ userId: '1' },
{ title: 'Buy groceries' },
);
import { resource } from '@data-client/rest';
import { useController } from '@data-client/react';
import { User } from './resources';

const UserResource = resource({
path: '/groups/:group/users/:id',
schema: User,
});
const ctrl = useController();

// POST /groups/five/users - adds new User to the end of the list
const newUser = await ctrl.fetch(
UserResource.getList.push,
{ group: 'five' },
{ username: 'newuser', email: '[email protected]' },
);

// Send an array to create several at once; they're added to the end in order
await ctrl.fetch(
UserResource.getList.push,
{ group: 'five' },
[{ username: 'ana' }, { username: 'bo' }],
);

unshift​

Cria um endpoint POST que coloca as Entities recém-criadas no início de uma Collection.

Retorna um novo RestEndpoint com method: 'POST' e schema: Collection.unshift

import { RestEndpoint, Collection } from '@data-client/rest';
import { useController } from '@data-client/react';
import { Todo } from './resources';

const getTodos = new RestEndpoint({
path: '/todos',
searchParams: {} as { userId?: string },
schema: new Collection([Todo]),
});
const ctrl = useController();

// POST /todos - adds new Todo to the beginning of the list
const newTodo = await ctrl.fetch(
getTodos.unshift,
{ userId: '1' },
{ title: 'Urgent task' },
);
import { resource } from '@data-client/rest';
import { useController } from '@data-client/react';
import { User } from './resources';

const UserResource = resource({
path: '/groups/:group/users/:id',
schema: User,
});
const ctrl = useController();

// POST /groups/five/users - adds new User to the start of the list
const newUser = await ctrl.fetch(
UserResource.getList.unshift,
{ group: 'five' },
{ username: 'priorityuser', email: '[email protected]' },
);

assign​

Cria um endpoint POST que mescla Entities em uma Collection de Values.

Retorna um novo RestEndpoint com method: 'POST' e schema: Collection.assign

import { RestEndpoint, Collection, Values } from '@data-client/rest';
import { useController } from '@data-client/react';
import { Stats } from './resources';

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

// POST /products/stats - add/update entries in the Values collection
await ctrl.fetch(getStats.assign, {
'BTC-USD': { product_id: 'BTC-USD', volume: 1000 },
'ETH-USD': { product_id: 'ETH-USD', volume: 500 },
});
import { resource, Collection, Values } from '@data-client/rest';
import { useController } from '@data-client/react';
import { Stats } from './resources';

const StatsResource = resource({
urlPrefix: 'https://api.exchange.example.com',
path: '/products/:product_id/stats',
schema: Stats,
}).extend({
getList: {
path: '/products/stats',
schema: new Collection(new Values(Stats)),
},
});
const ctrl = useController();

// POST /products/stats - add/update entries
await ctrl.fetch(StatsResource.getList.assign, {
'BTC-USD': { product_id: 'BTC-USD', volume: 1000 },
});

remove​

Cria um endpoint PATCH que remove Entities de uma Collection e as atualiza com a resposta.

Retorna um novo RestEndpoint com method: 'PATCH' e schema: Collection.remove

import { RestEndpoint, Collection } from '@data-client/rest';
import { useController } from '@data-client/react';
import { Todo } from './resources';

const getTodos = new RestEndpoint({
path: '/todos',
schema: new Collection([Todo]),
});
const ctrl = useController();

// PATCH /todos - removes Todo from collection AND updates the entity
await ctrl.fetch(getTodos.remove, { id: '123', completed: true });
import { resource } from '@data-client/rest';
import { useController } from '@data-client/react';
import { User } from './resources';

const UserResource = resource({
path: '/groups/:group/users/:id',
schema: User,
});
const ctrl = useController();

// PATCH /groups/five/users - removes user from 'five' group list
// AND updates the user entity with response data (e.g., new group)
await ctrl.fetch(
UserResource.getList.remove,
{ group: 'five' },
{ id: '2', group: 'newgroup' },
);

Para usar o schema de remoção com um endpoint diferente (por exemplo, DELETE):

const deleteAndRemove = MyResource.delete.extend({
schema: MyResource.getList.schema.remove,
});

move​

Cria um endpoint PATCH que move Entities entre Collections. Ele remove das collections que correspondem ao estado existente da entidade e adiciona às collections que correspondem aos novos valores (do body/último argumento).

Retorna um novo RestEndpoint com method: 'PATCH' e schema: Collection.move

import { useController } from '@data-client/react';
import { TaskResource, type Task } from './TaskResource';

export default function TaskCard({ task }: { task: Task }) {
  const handleMove = () => ctrl.fetch(
    TaskResource.getList.move,
    { id: task.id },
    { id: task.id, status: task.status === 'backlog' ? 'in-progress' : 'backlog' },
  );
  const ctrl = useController();
  return (
    <div className="listItem">
      <span style={{ flex: 1 }}>{task.title}</span>
      <button onClick={handleMove}>
        {task.status === 'backlog' ? '\u25bc' : '\u25b2'}
      </button>
    </div>
  );
}
Resultado
Store▶

O filtro de remoção se baseia nos valores existentes da entidade na store. O filtro de adição se baseia nos valores mesclados da entidade (existentes + body). Isso usa a mesma lógica de createCollectionFilter que push/remove.

import { resource } from '@data-client/rest';
import { useController } from '@data-client/react';
import { User } from './resources';

const UserResource = resource({
path: '/groups/:group/users/:id',
schema: User,
});
const ctrl = useController();

// PATCH /groups/five/users/5 - moves user 5 from 'five' group to 'ten' group
await ctrl.fetch(
UserResource.getList.move,
{ group: 'five', id: '2' },
{ id: '2', group: 'ten' },
);

getPage​

Um endpoint para obter a próxima página usando paginationField como chave do searchParameter. O schema também deve conter uma Collection

const getTodos = new RestEndpoint({
path: '/todos',
schema: Todo,
paginationField: 'page',
});

const todos = useSuspense(getTodos);
const ctrl = useController();
return (
<PaginatedList
items={todos}
fetchNextPage={() =>
// fetches url `/todos?page=${nextPage}`
ctrl.fetch(getTodos.getPage, { page: nextPage })
}
/>
);

Veja o guia de paginação para mais informações.

paginated(paginationfield)​

Cria um novo endpoint com uma string extra paginationfield que será usada para encontrar a página específica a ser anexada a este endpoint. Veja Paginação com rolagem infinita para mais informações.

const getNextPage = getList.paginated('cursor');

O schema também deve conter uma Collection

paginated(removeCursor)​

function paginated<E, A extends any[]>(
this: E,
removeCursor: (...args: A) => readonly [...Parameters<E>],
): PaginationEndpoint<E, A>;

A forma de função permite qualquer processamento de argumentos. É o equivalente a enviar a string cursor, como acima.

const getNextPage = getList.paginated(
({ cursor, ...rest }: { cursor: string | number }) =>
(Object.keys(rest).length ? [rest] : []) as any,
);

removeCusor é uma função que recebe os argumentos enviados no fetch de getNextPage e retorna os argumentos para atualizar getList.

O schema também deve conter uma Collection

Herança​

Certifique-se de usar RestGenerics para que os tipos continuem funcionando.

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

class GithubEndpoint<
O extends RestGenerics = any,
> extends RestEndpoint<O> {
urlPrefix = 'https://api.github.com';

getHeaders(headers: HeadersInit): HeadersInit {
return {
...headers,
'Access-Token': getAuth(),
};
}
}