Saltar al contenido principal

RestEndpoint

Los RestEndpoints son para protocolos basados en HTTP como REST.

extends

RestEndpoint extiende Endpoint

Interfaz
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 las opciones se admiten como argumentos del constructor, de extend y como sobrescrituras al usar herencia

La obtención más simple​

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

Compartir configuración​

Usa RestEndpoint.extend() en lugar de {...getTodo} (Object spread)

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

Gestionar el 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 un Schema habilita la consistencia automática de los datos sin necesidad de perjudicar el rendimiento con volver a obtener los datos.

Tipado​

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

Resolución/Retorno​

schema determina el valor de retorno cuando se usa con hooks de enlace de datos como useSuspense, useDLE, useCache o cuando se usa con 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 el valor de resolución cuando el endpoint se llama directamente. En los RestEndpoints sin schema, también determina el tipo de retorno de hooks y 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 de la función​

path, que se usa para construir la url, determina el tipo del primer argumento. Si no tiene patrones, se omite el 'primer' argumento.

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 si hay un segundo argumento que se envía como body.

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

Sin embargo, este se tipa como 'any', por lo que no detectará errores tipográficos.

body se puede usar para tipar el argumento que sigue a los parámetros de la url. Solo se usa para el tipado, así que el valor enviado no importa. El valor undefined se puede usar para 'deshabilitar' el 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 se puede usar de forma similar a body para especificar los tipos de parámetros adicionales, usados para los searchParams/queryParams del GET en un 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 del fetch​

RestEndpoint amplía Endpoint al ofrecer personalizaciones para un método fetch proporcionado mediante herencia o .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 el fetch​

Los miembros funcionan también como opciones (segundo argumento del constructor). Aunque ninguno es obligatorio, los primeros tienen valores por defecto.

url(params): string​

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

url() usa los params para rellenar la plantilla de path. Los miembros de params que no se usen se emplean después como searchParams (también llamados params 'GET', lo que va después de ?).

Implementación
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​

Construye el componente searchParams de la url.

Por defecto usa el global estándar URLSearchParams.

Los searchParams (también llamados queryParams) se ordenan para mantener el determinismo.

Implementación
searchToString(searchParams) {
const params = new URLSearchParams(searchParams);
params.sort();
return params.toString();
}

Usar la librería qs​

Para codificar objetos complejos en los searchParams, puedes usar la librería 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 path-to-regexp v8 para construir urls con los parámetros pasados. Esto también define los tipos, de modo que se apliquen correctamente.

Parámetros​

Las palabras con prefijo : son nombres de parámetros. Se aceptan tanto strings como números como valores, ya que se serializan en el string de la url.

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

Parámetros opcionales​

Envuelve el segmento opcional (incluido su prefijo) en {} para hacerlo opcional. El tipo de los parámetros opcionales pasa a ser string | number | undefined.

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

Se pueden encadenar varios segmentos opcionales con distintos prefijos:

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

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

Comodines (parámetros repetidos)​

*name coincide con uno o más segmentos de la ruta. Envuélvelo en {} para que coincida con cero o más (opcional). Los parámetros comodín se tipan como string[] (arrays), ya que representan varios segmentos de la ruta.

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

Nombres de parámetros entre comillas​

Los nombres de parámetros deben ser identificadores válidos de JavaScript. Los nombres que contienen caracteres especiales como - o . deben ir entre comillas dobles:

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

Escapar caracteres especiales​

Los caracteres {}()*: y \\ son especiales en path-to-regexp y deben escaparse con \\ cuando se usan como literales.

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

? y + no son especiales en path-to-regexp v8 y no necesitan escaparse. Esto significa que los query strings se pueden incrustar en la ruta sin escapar ?:

const search = new RestEndpoint({
path: '/search?{q=:q}{&page=:page}',
});
search({ q: 'test', page: 1 });
// URL: /search?q=test&page=1
información

Los tipos se infieren automáticamente a partir de path.

Se pueden especificar parámetros adicionales con searchParams y body.

searchParams​

searchParams se puede usar para especificar los tipos de parámetros adicionales, usados para los searchParams/queryParams del GET en un url().

El valor real no se usa de ninguna manera; esto solo determina el tipado.

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 se puede usar para definir un segundo argumento en endpoints de mutación. El valor real no se usa de ninguna manera; esto solo determina el tipado.

Solo lo usan los endpoints con un método que utiliza 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​

Si se especifica, agregará el método getPage al RestEndpoint. Guía de paginación. El schema también debe contener una Collection.

urlPrefix: string = ''​

Antepone esto al path compilado

Valores por defecto mediante herencia​

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

Más información sobre los patrones de herencia para RestEndpoint

Sobrescrituras por instancia​

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

Prefijo dinámico​

consejo

Para un prefijo dinámico, prueba mejor sobrescribir el método url():

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

method: string = 'GET'​

El método es parte del protocolo HTTP. Los protocolos REST lo usan para indicar el tipo de operación. Por eso RestEndpoint lo usa para determinar sideEffect y si el endpoint debe usar un payload body. Establecer sideEffect explícitamente sobrescribirá este comportamiento, lo que permite diseños de API no estándar.

GET es 'de solo lectura'; los demás métodos implican sideEffects.

GET y DELETE no tienen body por defecto.

Cómo afecta method a los parámetros de la función

method solo influye en los parámetros del constructor de RestEndpoint y no en .extend(). Esto permite combinaciones no estándar de método y body.

body será any por defecto. Siempre puedes establecer body explícitamente para tener el control total. Se puede usar undefined para indicar que no hay 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 el RequestInit que se usa en el fetch. Se envía a fetchResponse

Un body que sea un objeto plano o un array se codifica como JSON, con un encabezado Content-Type: application/json, a menos que requestInit o getHeaders establezcan uno. Cualquier otro body, como FormData, Blob, URLSearchParams o un string, se pasa a fetch() tal cual.

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​

Lo llama getRequestInit para determinar los encabezados HTTP

Esto suele ser útil para la autenticación

aviso

No uses hooks aquí. Si necesitas usar hooks, prueba con 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';
}

Manejar el fetch​

fetchResponse(input, init): Promise​

Realiza la llamada fetch(input, init). Cuando response.ok no es true (como en un 404), lanza un NetworkError.

content​

Controla cómo se interpreta el cuerpo de la Response. Cuando se establece, el tipo de retorno se infiere automáticamente y schema queda restringido a undefined para los tipos de contenido que no son JSON.

ValorSe interpreta conTipo de retorno
'json'response.json()any
'blob'response.blob()Blob
'text'response.text()string
'arrayBuffer'response.arrayBuffer()ArrayBuffer
'stream'response.bodyReadableStream<Uint8Array>
sin establecerDetección automática según el encabezado Content-Typeany

Cuando content no está establecido, parseResponse detecta automáticamente el tipo de respuesta a partir del encabezado Content-Type: los tipos JSON llaman a .json(), los tipos binarios (imágenes, application/octet-stream, PDFs, etc.) llaman a .blob() y los tipos de texto llaman a .text().

Descargas de archivos​

Para descargar archivos, establece content: 'blob'. El tipo de retorno es Blob y schema debe ser undefined (los datos binarios no se pueden normalizar). Usa dataExpiryLength: 0 para evitar guardar en caché blobs grandes en memoria.

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

Para extraer el nombre del archivo del encabezado Content-Disposition, sobrescribe 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;
},
});

Consulta la guía de descarga de archivos para ver el uso completo con el disparador de descarga del navegador.

parseResponse(response): Promise​

Toma la Response e interpreta el cuerpo.

Cuando content está establecido, controla directamente la interpretación. En caso contrario, se ejecuta la detección automática según el encabezado Content-Type: los tipos JSON llaman a .json(), los tipos binarios llaman a .blob() y los tipos de texto llaman a .text().

Si status es 204, se resuelve como null.

Sobrescríbelo para casos avanzados, como extraer los encabezados junto con el cuerpo.

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

Aplica cualquier transformación al resultado ya interpretado. Por defecto es la función identidad (no hace nada).

args son los argumentos con los que se llamó al endpoint. Se tipan a partir de path, searchParams y body del endpoint, incluidos los definidos en la misma llamada a 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}` };
},
});
consejo

El tipo de retorno de process se puede usar para establecer el tipo de retorno del fetch del 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 del Endpoint​

schema?: Schema​

Ciclo de vida declarativo de los datos

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 los parámetros. Se usa para construir una clave de búsqueda en stores globales.

Por defecto:

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

testKey(key): boolean​

Devuelve true si la key (de fetch) proporcionada coincide con este endpoint.

Se usa para los interceptors de mock con <MockResolver />, Controller.expireAll(), and Controller.invalidateAll().

dataExpiryLength?: number​

Tiempo de vida personalizado en la caché de los datos del recurso obtenido. Reemplazará el valor establecido en NetworkManager.

Más información sobre el tiempo de caducidad

errorExpiryLength?: number​

Tiempo de vida personalizado de los errores del recurso obtenido. Reemplazará el valor establecido en NetworkManager.

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

'soft' usará los datos obsoletos (si existen) en caso de error; undefined o no proporcionar la opción provocará un error.

Más información sobre errorPolicy

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

invalidIfStale: boolean​

Indica que los datos obsoletos deben considerarse inutilizables y, por tanto, no devolverse desde la caché. Esto significa que useSuspense() se suspenderá cuando los datos estén obsoletos aunque ya existan en la caché.

pollFrequency: number​

Frecuencia en milisegundos con la que se realiza el sondeo. Requiere usar useSubscription() o useLive() para tener efecto.

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

Cuando se proporciona, cualquier fetch con este endpoint se comportará como si el valor de retorno expectedResponse de esta función fuera una respuesta de red exitosa. Cuando el fetch real se completa (ya sea con fallo o con éxito), la actualización optimista se reemplaza por la respuesta de red real.

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▶
Guía de actualizaciones optimistas

update()​

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

Prueba a usar Collections en su lugar.

¡Son mucho más fáciles de usar y más 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] };

El caso más sencillo:

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

Más actualizaciones:

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

El endpoint siguiente garantiza que el nuevo usuario aparezca de inmediato en los usos anteriores.

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​

Se puede usar para personalizar aún más la definición del 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​

Estos accesores de conveniencia crean nuevos endpoints para operaciones comunes de Collection. Solo funcionan cuando el schema del RestEndpoint contiene una Collection.

push​

Crea un endpoint POST que coloca las Entities recién creadas al final de una Collection.

Devuelve un nuevo RestEndpoint con method: 'POST' y 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​

Crea un endpoint POST que coloca las Entities recién creadas al inicio de una Collection.

Devuelve un nuevo RestEndpoint con method: 'POST' y 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​

Crea un endpoint POST que fusiona Entities en una Collection de Values.

Devuelve un nuevo RestEndpoint con method: 'POST' y 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​

Crea un endpoint PATCH que elimina Entities de una Collection y las actualiza con la respuesta.

Devuelve un nuevo RestEndpoint con method: 'PATCH' y 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 el schema remove con un endpoint distinto (por ejemplo, DELETE):

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

move​

Crea un endpoint PATCH que mueve Entities entre Collections. Elimina de las colecciones que coinciden con el estado actual de la entidad y agrega a las colecciones que coinciden con los nuevos valores (del body o último argumento).

Devuelve un nuevo RestEndpoint con method: 'PATCH' y 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▶

El filtro de eliminación se basa en los valores existentes de la entidad en el store. El filtro de adición se basa en los valores combinados de la entidad (existentes + body). Esto usa la misma 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​

Un endpoint para obtener la página siguiente usando paginationField como clave del searchParameter. El schema también debe contener una 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 })
}
/>
);

Consulta la guía de paginación para más información.

paginated(paginationfield)​

Crea un nuevo endpoint con un string paginationfield adicional que se usará para encontrar la página específica que se agregará a este endpoint. Consulta Paginación con scroll infinito para más información.

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

El schema también debe contener una Collection

paginated(removeCursor)​

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

La forma de función permite cualquier procesamiento de argumentos. Es el equivalente a enviar el string cursor como arriba.

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

removeCusor es una función que toma los argumentos enviados en el fetch de getNextPage y devuelve los argumentos para actualizar getList.

El schema también debe contener una Collection

Herencia​

Asegúrate de usar RestGenerics para que los tipos sigan 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(),
};
}
}