RestEndpoint
RestEndpoints são para protocolos baseados em HTTP, como o REST.
RestEndpoint estende Endpoint
Interface
- RestEndpoint
- Endpoint
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;
}
class Endpoint<F extends (...args: any) => Promise<any>> {
constructor(fetchFunction: F, options: EndpointOptions);
key(...args: Parameters<F>): string;
readonly sideEffect?: true;
readonly schema?: Schema;
/** Default data expiry length, will fall back to NetworkManager default if not defined */
readonly dataExpiryLength?: number;
/** Default error expiry length, will fall back to NetworkManager default if not defined */
readonly errorExpiryLength?: number;
/** Poll with at least this frequency in milliseconds */
readonly pollFrequency?: number;
/** Marks cached resources as invalid if they are stale */
readonly invalidIfStale?: boolean;
/** Enables optimistic updates for this request - uses return value as assumed network response */
readonly getOptimisticResponse?: (
snap: SnapshotInterface,
...args: Parameters<F>
) => ResolveType<F>;
/** Determines whether to throw or fallback to */
readonly errorPolicy?: (error: any) => 'soft' | undefined;
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().
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' } });
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
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 });
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: '/' });
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
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.
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á.
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
Não use hooks aqui. Se você precisar usar hooks, tente usar hookifyResource
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.
| Valor | Interpretado via | Tipo de retorno |
|---|---|---|
'json' | response.json() | any |
'blob' | response.blob() | Blob |
'text' | response.text() | string |
'arrayBuffer' | response.arrayBuffer() | ArrayBuffer |
'stream' | response.body | ReadableStream<Uint8Array> |
| não definido | Detecção automática pelo header Content-Type | any |
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}` };
},
});
O tipo de retorno de process pode ser usado para definir o tipo de retorno do fetch do endpoint:
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; }
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
- Consistência global dos dados e desempenho com estado DRY: onde esperar Entities
- Funções para desserializar campos
- Tratamento de condições de corrida
- Validação
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.
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, }; }, });
update()
(normalizedResponseOfThis, ...args) =>
({ [endpointKey]: (normalizedResponseOfEndpointToUpdate) => updatedNormalizedResponse) })
Experimente usar Collections no lugar.
Elas são muito mais fáceis de usar e mais robustas!
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:
const createUser = new RestEndpoint({
path: '/user',
method: 'POST',
schema: User,
update: (newUserId: string) => ({
[userList.key()]: (users = []) => [newUserId, ...users],
}),
});
Mais atualizações:
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.
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' },
);
// 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' },
);
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> ); }
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(),
};
}
}