Saltar al contenido principal

Resource

Los Resources son una colección de RestEndpoints que operan sobre datos comunes al compartir un schema

Uso​

resources/Todo.ts
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,
});
Resources start with 6 Endpoints
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.

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',
});
NombreMétodoArgsSchema
getGET[{group: string; id: string}]Post
getListGET[{group: string; author?: string}]Collection([Post])
getList.pushPOST[{group: string; author?: string}, Partial<Post>]Collection([Post]).push
getList.unshiftPOST[{group: string; author?: string}, Partial<Post>]Collection([Post]).unshift
getList.getPageGET[{group: string; author?: string; page: string}]Collection([Post]).addWith
getList.movePATCH[{group: string; id: string }, Partial<Post>]Collection([Post]).move
updatePUT[{group: string; id: string }, Partial<Post>]Post
partialUpdatePATCH[{group: string; id: string }, Partial<Post>]Post
deleteDELETE[{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 },
});
▶Usage
import { PostResource } from './Resource';
PostResource.get({
  group: 'react',
  id: '1',
});
Request
GET /react/posts/1
content-type: application/json
Response200 OK
{
"id": "1",
"group": "react",
"title": "this post",
"author": "clara"
}
CampoValor
method'GET'
pathpath
schemaschema

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 },
});
▶Usage
import { PostResource } from './Resource';
PostResource.getList({
  group: 'react',
  author: 'clara',
});
Request
GET /react/posts?author=clara
content-type: application/json
Response200 OK
[
{
"id": "1",
"group": "react",
"title": "this post",
"author": "clara"
}
]
CampoValor
method'GET'
pathremoveLastArg(path)
searchParamssearchParams
paginationFieldpaginationField
schemanew 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 },
});
▶Usage
import { PostResource } from './Resource';
PostResource.getList.push(
  { group: 'react', author: 'clara' },
  { title: 'winning' },
);
Request
POST /react/posts?author=clara
content-type: application/json
Body: {"title":"winning"}
Response201 Created
{
"id": "2",
"group": "react",
"title": "winning",
"author": "clara"
}
CampoValor
method'POST'
pathremoveLastArg(path)
searchParamssearchParams
bodybody
schemagetList.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 },
});
▶Usage
import { PostResource } from './Resource';
PostResource.getList.unshift(
  { group: 'react', author: 'clara' },
  { title: 'winning' },
);
Request
POST /react/posts?author=clara
content-type: application/json
Body: {"title":"winning"}
Response201 Created
{
"id": "2",
"group": "react",
"title": "winning",
"author": "clara"
}
CampoValor
method'POST'
pathremoveLastArg(path)
searchParamssearchParams
bodybody
schemagetList.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',
});
▶Usage
import { PostResource } from './Resource';
PostResource.getList.getPage({
  group: 'react',
  author: 'clara',
  page: 2,
});
Request
GET /react/posts?author=clara&page=2
content-type: application/json
Response200 OK
[
{
"id": "5",
"group": "react",
"title": "second page",
"author": "clara"
}
]
CampoValor
method'GET'
pathremoveLastArg(path)
searchParamssearchParams
paginationFieldpaginationField
schemagetList.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 },
});
▶Usage
import { PostResource } from './Resource';
PostResource.getList.move(
  { group: 'react', id: '1' },
  { group: 'vue' },
);
Request
PATCH /react/posts/1
content-type: application/json
Body: {"group":"vue"}
Response200 OK
{
"id": "1",
"group": "vue",
"title": "this post",
"author": "clara"
}
CampoValor
method'PATCH'
pathpath
bodybody
schemagetList.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 },
});
▶Usage
import { PostResource } from './Resource';
PostResource.update(
  { group: 'react', id: '1' },
  { title: 'updated title', author: 'clara' },
);
Request
PUT /react/posts/1
content-type: application/json
Body: {"title":"updated title","author":"clara"}
Response200 OK
{
"id": "1",
"group": "react",
"title": "updated title",
"author": "clara"
}
CampoValor
method'PUT'
pathpath
bodybody
schemaschema

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 },
});
▶Usage
import { PostResource } from './Resource';
PostResource.partialUpdate(
  { group: 'react', id: '1' },
  { title: 'updated title' },
);
Request
PATCH /react/posts/1
content-type: application/json
Body: {"title":"updated title"}
Response200 OK
{
"id": "1",
"group": "react",
"title": "updated title",
"author": "clara"
}
CampoValor
method'PATCH'
pathpath
bodybody
schemaschema

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 },
});
▶Usage
import { PostResource } from './Resource';
PostResource.delete({ group: 'react', id: '1' });
Request
DELETE /react/posts/1
content-type: application/json
Response200 OK
{
"id": "1"
}
CampoValor
method'DELETE'
pathpath
schemanew Invalidate(schema)
process
(value, params) {
return value && Object.keys(value).length ? value : params;
},

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

More Demos

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

More Demos