Resource
Resources são uma coleção de RestEndpoints que operam sobre dados em comum
ao compartilhar um 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
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.
- getList usa uma Collection de Array do schema.
- delete usa um Invalidate do schema.
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',
});
| Nome | 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
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 }, });
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"
}
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 }, });
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';
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 }, });
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 |
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 }, });
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 |
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', });
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 \}
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 }, });
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 |
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 }, });
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"
}
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 }, });
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"
}
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 }, });
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 | |
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
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