Resource
Resources
are a collection of RestEndpoints that operate on a common
data by sharing a schema
Usage
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' });
Arguments
{
path: string;
schema: Schema;
urlPrefix?: string;
body?: any;
searchParams?: any;
paginationField?: string;
optimistic?: boolean;
Endpoint?: typeof RestEndpoint;
Collection?: typeof Collection;
} & EndpointExtraOptions
path
Passed to RestEndpoint.path for single item .
Create (getList.push/getList.unshift) and getList remove the last argument.
const PostResource = resource({
schema: Post,
path: '/:group/posts/:id',
});
// GET /react/posts/abc
PostResource.get({ group: 'react', id: 'abc' });
// GET /react/posts
PostResource.getList({ group: 'react' });
schema
Passed to RestEndpoint.schema representing a single item. This is usually an Entity or Union.
- getList uses an Array Collection of the schema.
- delete uses a Invalidate of the schema.
urlPrefix
Passed to RestEndpoint.urlPrefix
searchParams
Passed to RestEndpoint.searchParams for getList and getList.push
body
Passed to RestEndpoint.body for getList.push update and partialUpdate
paginationField
If specified, will add Resource.getList.getPage method on the Resource
.
optimistic
true
makes all mutation endpoints optimistic, making UI
updates immediate, even before fetch completion.
Endpoint
Class used to construct the members.
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
Collection Class used to construct getList schema.
import { schema, resource } from '@data-client/rest';
class MyCollection<
S extends any[] | PolymorphicInterface = any,
Parent extends any[] = [urlParams: any, body?: any],
> extends schema.Collection<S, Parent> {
// getList.push should add to Collections regardless of its 'orderBy' argument
// in other words: `orderBy` is a non-filtering argument - it does not influence which results are returned
nonFilterArgumentKeys(key: string) {
return key === 'orderBy';
}
}
const TodoResource = resource({
path: '/todos/:id',
searchParams: {} as { userId?: string; orderBy?: string } | undefined,
schema: Todo,
Collection: MyCollection,
});
EndpointExtraOptions
dataExpiryLength, errorExpiryLength, errorPolicy, invalidIfStale, pollFrequency
Members
These provide the standard CRUD endpointss common in REST APIs. Feel free to customize or add new endpoints based to match your API.
const PostResource = resource({
schema: Post,
path: '/:group/posts/:id',
searchParams: {} as { author?: string },
paginationField: 'page',
});
Name | Method | 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 |
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
Retrieve a singular entity.
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"
}
Field | Value |
---|---|
method | 'GET' |
path | path |
schema | schema |
Commonly used with useSuspense(), Controller.invalidate, Controller.expireAll
getList
Retrieve a list of entities.
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"
}
]
Field | Value |
---|---|
method | 'GET' |
path | removeLastArg(path) |
searchParams | searchParams |
paginationField | paginationField |
schema | new schema.Collection([schema]) |
resource({ path: '/:first/:second' }).getList.path === '/:first';
resource({ path: '/:first' }).getList.path === '/';
Commonly used with useSuspense(), Controller.invalidate, Controller.expireAll
getList.push
RestEndpoint.push creates a new entity and pushes it to the end of getList. Use getList.unshift to place at the beginning instead.
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"
}
Field | Value |
---|---|
method | 'POST' |
path | removeLastArg(path) |
searchParams | searchParams |
body | body |
schema | getList.schema.push |
Commonly used with Controller.fetch
getList.unshift
RestEndpoint.unshift creates a new entity and pushes it to the beginning of getList.
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"
}
Field | Value |
---|---|
method | 'POST' |
path | removeLastArg(path) |
searchParams | searchParams |
body | body |
schema | getList.schema.unshift |
Commonly used with Controller.fetch
getList.getPage
RestEndpoint.getPage retrieves another page appending to getList ensuring there are no duplicates.
This member is only available when paginationField is specified.
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"
}
]
Field | Value |
---|---|
method | 'GET' |
path | removeLastArg(path) |
searchParams | searchParams |
paginationField | paginationField |
schema | getList.schema.addWith |
args: PathToArgs(shortenPath(path)) & searchParams & \{ [paginationField]: string | number \}
Commonly used with Controller.fetch
update
Update an entity.
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"
}
Field | Value |
---|---|
method | 'PUT' |
path | path |
body | body |
schema | schema |
Commonly used with Controller.fetch
partialUpdate
Update some subset of fields of an entity.
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"
}
Field | Value |
---|---|
method | 'PATCH' |
path | path |
body | body |
schema | schema |
Commonly used with Controller.fetch
delete
Deletes an entity.
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"
}
Field | Value |
---|---|
method | 'DELETE' |
path | path |
schema | new schema.Invalidate(schema) |
process |
|
Commonly used with Controller.fetch
Response
{ "id": "xyz" }
Response should either be the pk as a string (like 'xyz'
). Or an object with the members needed to compute
Entity.pk (like {id: 'xyz'}
).
If no response is provided, the process
implementation will attempt to use the url parameters sent as an object to compute
the Entity.pk. This enables the default implementation to still work with no response, so long as standard
arguments are used.
This allows schema.Invalidate to remove the entity from the entity table
extend()
resource
builds a great starting point, but often endpoints need to be further customized.
extend()
is polymorphic with three forms:
Batch extension of known members
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: '' } },
});
Adding new members
export const UserResource = createGithubResource({
path: '/users/:login',
schema: User,
}).extend('current', {
path: '/user',
schema: User,
});
Function form (to get BaseResource/super)
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: '',
},
})
)});
Github CommentResource
Function Inheritance Patterns
To reuse code related to Resource
definitions, you can create your own function that calls resource().
This has similar effects as class-based inheritance, with the added benefit of allowing for complete
typing overrides.
import {
resource,
RestEndpoint,
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 schema.Collection([schema]),
total: 0,
limit: 0,
skip: 0,
},
},
});
}