Pular para o conteúdo principal

Resource

Resources são uma coleção de RestEndpoints que operam sobre dados em comum ao compartilhar um 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​

Repassado para RestEndpoint.path nos endpoints de item único. Usa a sintaxe do path-to-regexp v8; veja RestEndpoint.path para detalhes completos sobre parâmetros opcionais, wildcards, nomes entre aspas e escape.

Create (getList.push/getList.unshift) e getList removem o último token :param ou *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' });

Parâmetros opcionais usam a sintaxe {}:

const PostResource = resource({
schema: Post,
path: '/:group/posts{/:id}',
});

PostResource.get({ group: 'react', id: 'abc' });
PostResource.getList({ group: 'react' });

Parâmetros wildcard também são aceitos 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​

Repassado para RestEndpoint.schema, representando um único item. Normalmente é uma Entity ou Union.

urlPrefix​

Repassado para RestEndpoint.urlPrefix

searchParams​

Repassado para RestEndpoint.searchParams em getList e getList.push

body​

Repassado para RestEndpoint.body em getList.push, update e partialUpdate

paginationField​

Se especificado, adiciona o método Resource.getList.getPage ao Resource.

nonFilterArgumentKeys​

Opção repassada para Collection.nonFilterArgumentKeys no schema de getList.

const PostResource = resource({
path: '/:group/posts/:id',
searchParams: {} as { orderBy?: string; author?: string },
schema: Post,
nonFilterArgumentKeys: ['orderBy'],
});

As formas RegExp e função também são aceitas:

resource({
path: '/:group/posts/:id',
searchParams: {} as { orderBy?: string; author?: string },
schema: Post,
nonFilterArgumentKeys: /orderBy/,
});

optimistic​

true torna todos os endpoints de mutação otimistas, fazendo com que as atualizações da UI sejam imediatas, mesmo antes da conclusão do fetch.

Endpoint​

Classe usada para construir os membros.

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​

Classe Collection usada para construir o schema de getList. Use-a quando precisar personalizar o comportamento da collection além de nonFilterArgumentKeys, como alterar a lógica de merge do 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​

Inclui: dataExpiryLength, errorExpiryLength, errorPolicy, invalidIfStale, pollFrequency

Membros​

Eles fornecem os endpoints CRUD padrão, comuns em APIs REST. Sinta-se à vontade para personalizar ou adicionar novos endpoints para se adequar à sua API.

const PostResource = resource({
schema: Post,
path: '/:group/posts/:id',
searchParams: {} as { author?: string },
paginationField: 'page',
});
NomeMé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​

Obtém uma única entity.

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

Normalmente usado com useSuspense(), Controller.invalidate, Controller.expireAll

getList​

Obtém uma lista de entities.

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

Normalmente usado com useSuspense(), Controller.invalidate, Controller.expireAll

getList.push​

RestEndpoint.push cria uma nova entity e a adiciona ao final de getList. Use getList.unshift para colocá-la no início. Passe um array como body para criar várias de uma só 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

Normalmente usado com Controller.fetch

getList.unshift​

RestEndpoint.unshift cria uma nova entity e a adiciona ao início 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

Normalmente usado com Controller.fetch

getList.getPage​

RestEndpoint.getPage obtém outra página, acrescentando-a a getList e garantindo que não haja duplicatas.

Este membro só está disponível quando paginationField é especificado.

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 \}

Normalmente usado com Controller.fetch

getList.move​

RestEndpoint.move move uma entity entre Collections, removendo-a das collections que correspondem ao seu estado antigo e adicionando-a às collections que correspondem aos novos valores do body.

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

Normalmente usado com Controller.fetch

update​

Atualiza uma entity.

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

Normalmente usado com Controller.fetch

partialUpdate​

Atualiza um subconjunto de campos de uma entity.

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

Normalmente usado com Controller.fetch

delete​

Exclui uma entity.

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

Normalmente usado com Controller.fetch

Response​

{ "id": "xyz" }

A resposta deve ser a pk como string (como 'xyz') ou um objeto com os membros necessários para calcular a Entity.pk (como {id: 'xyz'}).

Se nenhuma resposta for fornecida, a implementação de process tentará usar os parâmetros da url enviados como objeto para calcular a Entity.pk. Isso permite que a implementação padrão continue funcionando sem resposta, desde que sejam usados argumentos padrão.

Isso permite que Invalidate remova a entity da tabela de entities

extend()​

resource constrói um ótimo ponto de partida, mas muitas vezes os endpoints precisam ser personalizados ainda mais.

extend() é polimórfico, com três formas:

Forma de função (para obter BaseResource/super)​

É a mais flexível, mas também a mais 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: '',
},
})
)});

Extensão em lote de membros conhecidos​

Funciona apenas com membros 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: '' } },
});

Adicionando novos membros​

Só consegue adicionar um endpoint por vez.

export const UserResource = createGithubResource({
path: '/users/:login',
schema: User,
}).extend('current', {
path: '/user',
schema: User,
});

CommentResource do Github​

Explore o exemplo github-app

More Demos

Padrões de herança com funções​

Para reutilizar código relacionado às definições de Resource, você pode criar sua própria função que chama resource(). Isso tem efeitos semelhantes aos da herança baseada em classes, com o benefício adicional de permitir sobrescritas completas de tipagem.

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

Híbrido GraphQL + REST​

Quando sua API oferece endpoints REST e GraphQL, você pode misturá-los em um único resource. Use Entity.process() para normalizar diferentes formatos de resposta.

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] } } },
),
}));

Exemplo do Github​

Explore o exemplo github-app

More Demos