Resource
Los Resources son una colección de RestEndpoints que operan sobre datos
comunes al compartir un schema
Uso
export class Todo extends Entity {
id = '';
title = '';
completed = false;
static key = 'Todo';
}
const TodoResource = resource({
urlPrefix: 'https://jsonplaceholder.typicode.com',
path: '/todos/:id',
schema: Todo,
});
const todo = useSuspense(TodoResource.get, { id: '5' });
const todos = useSuspense(TodoResource.getList);
controller.fetch(TodoResource.getList.push, {
title: 'finish installing reactive data client',
});
controller.fetch(
TodoResource.update,
{ id: '5' },
{ ...todo, completed: true },
);
controller.fetch(
TodoResource.partialUpdate,
{ id: '5' },
{ completed: true },
);
controller.fetch(TodoResource.delete, { id: '5' });
Argumentos
{
path: string;
schema: Schema;
urlPrefix?: string;
body?: any;
searchParams?: any;
paginationField?: string;
optimistic?: boolean;
Endpoint?: typeof RestEndpoint;
Collection?: typeof Collection;
} & EndpointExtraOptions
path
Se pasa a RestEndpoint.path para los endpoints de un solo elemento. Usa la sintaxis de path-to-regexp v8; consulta RestEndpoint.path para ver todos los detalles sobre parámetros opcionales, comodines, nombres entre comillas y escape.
Las creaciones (getList.push/getList.unshift) y getList eliminan el último token :param o *wildcard.
const PostResource = resource({
schema: Post,
path: '/:group/posts/:id',
});
// GET /react/posts/abc
PostResource.get({ group: 'react', id: 'abc' });
// PATCH /react/posts/abc
PostResource.partialUpdate({ group: 'react', id: 'abc' }, { title: 'This new title' });
// GET /react/posts
PostResource.getList({ group: 'react' });
Los parámetros opcionales usan la sintaxis {}:
const PostResource = resource({
schema: Post,
path: '/:group/posts{/:id}',
});
PostResource.get({ group: 'react', id: 'abc' });
PostResource.getList({ group: 'react' });
Los parámetros comodín también se admiten como último token:
const FileResource = resource({
schema: File,
path: '/repos/:owner/*path',
});
// GET /repos/john/src/index.ts
FileResource.get({ owner: 'john', path: ['src', 'index.ts'] });
// GET /repos/john
FileResource.getList({ owner: 'john' });
schema
Se pasa a RestEndpoint.schema y representa un solo elemento. Normalmente es una Entity o una Union.
- getList usa una Collection de Array del schema.
- delete usa un Invalidate del schema.
urlPrefix
Se pasa a RestEndpoint.urlPrefix
searchParams
Se pasa a RestEndpoint.searchParams para getList y getList.push
body
Se pasa a RestEndpoint.body para getList.push, update y partialUpdate
paginationField
Si se especifica, agregará el método Resource.getList.getPage al Resource.
nonFilterArgumentKeys
Opción que se pasa directamente a Collection.nonFilterArgumentKeys para el schema de getList.
const PostResource = resource({
path: '/:group/posts/:id',
searchParams: {} as { orderBy?: string; author?: string },
schema: Post,
nonFilterArgumentKeys: ['orderBy'],
});
También se admiten las formas RegExp y función:
resource({
path: '/:group/posts/:id',
searchParams: {} as { orderBy?: string; author?: string },
schema: Post,
nonFilterArgumentKeys: /orderBy/,
});
optimistic
true hace que todos los endpoints de mutación sean optimistas, de modo que las actualizaciones de la UI
sean inmediatas, incluso antes de que el fetch termine.
Endpoint
Clase usada para construir los miembros.
import { RestEndpoint } from '@data-client/rest';
export default class AuthdEndpoint<
O extends RestGenerics = any,
> extends RestEndpoint<O> {
async getRequestInit(body: any): Promise<RequestInit> {
return {
...(await super.getRequestInit(body)),
credentials: 'same-origin',
};
}
}
const TodoResource = resource({
path: '/todos/:id',
schema: Todo,
Endpoint: AuthdEndpoint,
});
Collection
Clase Collection usada para construir el schema de getList.
Úsala cuando necesites personalizar el comportamiento de la colección más allá de
nonFilterArgumentKeys, como cambiar la lógica de combinación de move.
import { resource, Collection, unshift } from '@data-client/rest';
class MyCollection<
S extends any[] | PolymorphicInterface = any,
Parent extends any[] = [urlParams: any, body?: any],
> extends Collection<S, Parent> {
constructor(schema: S) {
super(schema);
// prepend moved items instead of appending
this.move = this.moveWith(unshift);
}
}
const TodoResource = resource({
path: '/todos/:id',
searchParams: {} as { userId?: string; orderBy?: string } | undefined,
schema: Todo,
Collection: MyCollection,
});
EndpointExtraOptions
Opciones: dataExpiryLength, errorExpiryLength, errorPolicy, invalidIfStale, pollFrequency
Miembros
Estos proporcionan los endpoints CRUD estándar, comunes en las APIs REST. Siéntete libre de personalizar o añadir nuevos endpoints para que se ajusten a tu API.
const PostResource = resource({
schema: Post,
path: '/:group/posts/:id',
searchParams: {} as { author?: string },
paginationField: 'page',
});
| Nombre | Método | Args | Schema |
|---|---|---|---|
| get | GET | [{group: string; id: string}] | Post |
| getList | GET | [{group: string; author?: string}] | Collection([Post]) |
| getList.push | POST | [{group: string; author?: string}, Partial<Post>] | Collection([Post]).push |
| getList.unshift | POST | [{group: string; author?: string}, Partial<Post>] | Collection([Post]).unshift |
| getList.getPage | GET | [{group: string; author?: string; page: string}] | Collection([Post]).addWith |
| getList.move | PATCH | [{group: string; id: string }, Partial<Post>] | Collection([Post]).move |
| update | PUT | [{group: string; id: string }, Partial<Post>] | Post |
| partialUpdate | PATCH | [{group: string; id: string }, Partial<Post>] | Post |
| delete | DELETE | [{group: string; id: string }] | Invalidate(Post) |
get
Obtiene una sola entidad.
import { resource } from '@data-client/rest'; import Post from './Post'; export const PostResource = resource({ schema: Post, path: '/:group/posts/:id', searchParams: {} as { author?: string }, });
import { PostResource } from './Resource'; PostResource.get({ group: 'react', id: '1', });
GET /react/posts/1
content-type: application/json
{
"id": "1",
"group": "react",
"title": "this post",
"author": "clara"
}
Se usa comúnmente con useSuspense(), Controller.invalidate y Controller.expireAll
getList
Obtiene una lista de entidades.
import { resource } from '@data-client/rest'; import Post from './Post'; export const PostResource = resource({ schema: Post, path: '/:group/posts/:id', searchParams: {} as { author?: string }, });
import { PostResource } from './Resource'; PostResource.getList({ group: 'react', author: 'clara', });
GET /react/posts?author=clara
content-type: application/json
[
{
"id": "1",
"group": "react",
"title": "this post",
"author": "clara"
}
]
| Campo | Valor |
|---|---|
| method | 'GET' |
| path | removeLastArg(path) |
| searchParams | searchParams |
| paginationField | paginationField |
| schema | new Collection([schema]) |
resource({ path: '/:first/:second' }).getList.path === '/:first';
resource({ path: '/:first' }).getList.path === '/';
resource({ path: '/:owner/*path' }).getList.path === '/:owner';
Se usa comúnmente con useSuspense(), Controller.invalidate y Controller.expireAll
getList.push
RestEndpoint.push crea una nueva entidad y la agrega al final de getList. Usa getList.unshift para colocarla al principio. Pasa un array como cuerpo para crear varias a la vez.
import { resource } from '@data-client/rest'; import Post from './Post'; export const PostResource = resource({ schema: Post, path: '/:group/posts/:id', searchParams: {} as { author?: string }, });
import { PostResource } from './Resource'; PostResource.getList.push( { group: 'react', author: 'clara' }, { title: 'winning' }, );
POST /react/posts?author=clara
content-type: application/json
Body: {"title":"winning"}
{
"id": "2",
"group": "react",
"title": "winning",
"author": "clara"
}
| Campo | Valor |
|---|---|
| method | 'POST' |
| path | removeLastArg(path) |
| searchParams | searchParams |
| body | body |
| schema | getList.schema.push |
Se usa comúnmente con Controller.fetch
getList.unshift
RestEndpoint.unshift crea una nueva entidad y la agrega al principio de getList.
import { resource } from '@data-client/rest'; import Post from './Post'; export const PostResource = resource({ schema: Post, path: '/:group/posts/:id', searchParams: {} as { author?: string }, });
import { PostResource } from './Resource'; PostResource.getList.unshift( { group: 'react', author: 'clara' }, { title: 'winning' }, );
POST /react/posts?author=clara
content-type: application/json
Body: {"title":"winning"}
{
"id": "2",
"group": "react",
"title": "winning",
"author": "clara"
}
| Campo | Valor |
|---|---|
| method | 'POST' |
| path | removeLastArg(path) |
| searchParams | searchParams |
| body | body |
| schema | getList.schema.unshift |
Se usa comúnmente con Controller.fetch
getList.getPage
RestEndpoint.getPage obtiene otra página y la añade a getList asegurando que no haya duplicados.
Este miembro solo está disponible cuando se especifica paginationField.
import { resource } from '@data-client/rest'; import Post from './Post'; export const PostResource = resource({ schema: Post, path: '/:group/posts/:id', searchParams: {} as { author?: string }, paginationField: 'page', });
import { PostResource } from './Resource'; PostResource.getList.getPage({ group: 'react', author: 'clara', page: 2, });
GET /react/posts?author=clara&page=2
content-type: application/json
[
{
"id": "5",
"group": "react",
"title": "second page",
"author": "clara"
}
]
| Campo | Valor |
|---|---|
| method | 'GET' |
| path | removeLastArg(path) |
| searchParams | searchParams |
| paginationField | paginationField |
| schema | getList.schema.addWith |
args: PathToArgs(shortenPath(path)) & searchParams & \{ [paginationField]: string | number \}
Se usa comúnmente con Controller.fetch
getList.move
RestEndpoint.move mueve una entidad entre Collections: la elimina de las colecciones que coinciden con su estado anterior y la agrega a las colecciones que coinciden con los nuevos valores del cuerpo.
import { resource } from '@data-client/rest'; import Post from './Post'; export const PostResource = resource({ schema: Post, path: '/:group/posts/:id', searchParams: {} as { author?: string }, });
import { PostResource } from './Resource'; PostResource.getList.move( { group: 'react', id: '1' }, { group: 'vue' }, );
PATCH /react/posts/1
content-type: application/json
Body: {"group":"vue"}
{
"id": "1",
"group": "vue",
"title": "this post",
"author": "clara"
}
| Campo | Valor |
|---|---|
| method | 'PATCH' |
| path | path |
| body | body |
| schema | getList.schema.move |
Se usa comúnmente con Controller.fetch
update
Actualiza una entidad.
import { resource } from '@data-client/rest'; import Post from './Post'; export const PostResource = resource({ schema: Post, path: '/:group/posts/:id', searchParams: {} as { author?: string }, });
import { PostResource } from './Resource'; PostResource.update( { group: 'react', id: '1' }, { title: 'updated title', author: 'clara' }, );
PUT /react/posts/1
content-type: application/json
Body: {"title":"updated title","author":"clara"}
{
"id": "1",
"group": "react",
"title": "updated title",
"author": "clara"
}
Se usa comúnmente con Controller.fetch
partialUpdate
Actualiza un subconjunto de los campos de una entidad.
import { resource } from '@data-client/rest'; import Post from './Post'; export const PostResource = resource({ schema: Post, path: '/:group/posts/:id', searchParams: {} as { author?: string }, });
import { PostResource } from './Resource'; PostResource.partialUpdate( { group: 'react', id: '1' }, { title: 'updated title' }, );
PATCH /react/posts/1
content-type: application/json
Body: {"title":"updated title"}
{
"id": "1",
"group": "react",
"title": "updated title",
"author": "clara"
}
Se usa comúnmente con Controller.fetch
delete
Elimina una entidad.
import { resource } from '@data-client/rest'; import Post from './Post'; export const PostResource = resource({ schema: Post, path: '/:group/posts/:id', searchParams: {} as { author?: string }, });
import { PostResource } from './Resource'; PostResource.delete({ group: 'react', id: '1' });
DELETE /react/posts/1
content-type: application/json
{
"id": "1"
}
| Campo | Valor |
|---|---|
| method | 'DELETE' |
| path | path |
| schema | new Invalidate(schema) |
| process | |
Se usa comúnmente con Controller.fetch
Respuesta
{ "id": "xyz" }
La respuesta debe ser la pk como string (como 'xyz'), o un objeto con los miembros necesarios para calcular
Entity.pk (como {id: 'xyz'}).
Si no se proporciona una respuesta, la implementación de process intentará usar los parámetros de la URL enviados como un objeto para calcular
la Entity.pk. Esto permite que la implementación por defecto siga funcionando sin respuesta, siempre que se usen
los argumentos estándar.
Esto permite que Invalidate elimine la entidad de la tabla de entidades
extend()
resource es un excelente punto de partida, pero a menudo los endpoints necesitan personalizarse más.
extend() es polimórfico y tiene tres formas:
Forma de función (para obtener BaseResource/super)
Es la más flexible, pero también la más verbosa.
export const IssueResource= resource({
path: '/repos/:owner/:repo/issues/:number',
schema: Issue,
pollFrequency: 60000,
searchParams: {} as IssueFilters | undefined,
}).extend(BaseResource => ({
search: BaseResource.getList.extend({
path: '/search/issues?{q=:q}%20repo\\::owner/:repo{&page=:page}',
schema: {
results: {
incompleteResults: false,
items: BaseIssueResource.getList.schema.results,
totalCount: 0,
},
link: '',
},
})
)});
Extensión por lotes de miembros conocidos
Esto solo funciona con miembros existentes.
export const CommentResource = resource({
path: '/repos/:owner/:repo/issues/comments/:id',
schema: Comment,
}).extend({
getList: { path: '/repos/:owner/:repo/issues/:number/comments' },
update: { body: { body: '' } },
});
Añadir nuevos miembros
Esto solo puede añadir un endpoint a la vez.
export const UserResource = createGithubResource({
path: '/users/:login',
schema: User,
}).extend('current', {
path: '/user',
schema: User,
});
Github CommentResource
Explora el ejemplo github-app
Patrones de herencia de funciones
Para reutilizar código relacionado con las definiciones de Resource, puedes crear tu propia función que llame a resource().
Esto tiene efectos similares a la herencia basada en clases, con el beneficio añadido de permitir sobrescribir
los tipos por completo.
import {
resource,
RestEndpoint,
Collection,
type EndpointExtraOptions,
type RestGenerics,
type ResourceGenerics,
type ResourceOptions,
} from '@data-client/rest';
export class AuthdEndpoint<
O extends RestGenerics = any,
> extends RestEndpoint<O> {
urlPrefix = process.env.API_SERVER ?? 'http://localhost:8000';
async getRequestInit(body: any): Promise<RequestInit> {
return {
...(await super.getRequestInit(body)),
credentials: 'same-origin',
};
}
}
export function myResource<O extends ResourceGenerics = any>({
schema,
Endpoint = AuthdEndpoint,
...extraOptions
}: Readonly<O> & ResourceOptions) {
return resource({
Endpoint,
schema,
...extraOptions,
}).extend({
getList: {
schema: {
results: new Collection([schema]),
total: 0,
limit: 0,
skip: 0,
},
},
});
}
GraphQL + REST híbrido
Cuando tu API ofrece endpoints tanto REST como GraphQL, puedes combinarlos en un solo resource. Usa Entity.process() para normalizar las distintas formas de respuesta.
import { GQLEndpoint } from '@data-client/graphql';
import { Entity, resource } from '@data-client/rest';
const gql = new GQLEndpoint('https://api.myservice.com/graphql');
export class Repository extends Entity {
id = '';
name = '';
owner = { login: '' };
stargazersCount = 0;
forksCount = 0;
pk() {
return `${this.owner.login}/${this.name}`;
}
static key = 'Repository';
}
/** Normalizes GraphQL response shape to match REST Entity */
export class GqlRepository extends Repository {
static process(input: any, parent: any, key: string | undefined) {
// GraphQL uses different field names than REST
if ('stargazerCount' in input) {
return {
...input,
stargazersCount: input.stargazerCount,
forksCount: input.forkCount,
};
}
return input;
}
}
export const RepositoryResource = resource({
path: '/repos/:owner/:repo',
schema: Repository,
}).extend(base => ({
// REST endpoint for single repo
get: base.get,
// GraphQL endpoint for user's pinned repos
getByPinned: gql.query(
(v: { login: string }) => `query ($login: String!) {
user(login: $login) {
pinnedItems(first: 6, types: REPOSITORY) {
nodes {
... on Repository {
id
name
owner { login }
stargazerCount
forkCount
}
}
}
}
}`,
{ user: { pinnedItems: { nodes: [GqlRepository] } } },
),
}));
Ejemplo de Github
Explora el ejemplo github-app