# Resource

`Resources` are a collection of [RestEndpoints](https://dataclient.io/rest/api/RestEndpoint.md) that operate on a common
data by sharing a [schema](https://dataclient.io/rest/api/schema.md)

## Usage

```ts title="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,
});
```

```ts title="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' });
```

## Arguments

```ts
{
  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](https://dataclient.io/rest/api/RestEndpoint.md#path) for single item [endpoints](#members).
Uses [path-to-regexp v8](https://github.com/pillarjs/path-to-regexp) syntax — see
[RestEndpoint.path](https://dataclient.io/rest/api/RestEndpoint.md#path) for full details on
[optional parameters](https://dataclient.io/rest/api/RestEndpoint.md#path), [wildcards](https://dataclient.io/rest/api/RestEndpoint.md#path),
[quoted names](https://dataclient.io/rest/api/RestEndpoint.md#path), and [escaping](https://dataclient.io/rest/api/RestEndpoint.md#path).

Create ([getList.push](#push)/[getList.unshift](#unshift)) and [getList](#getlist) remove the last `:param` or `*wildcard` token.

```ts
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' });
```

Optional parameters use `{}` syntax:

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

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

Wildcard parameters are also supported as the last token:

```ts
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

Passed to [RestEndpoint.schema](https://dataclient.io/rest/api/RestEndpoint.md#schema) representing a single item. This is usually
an [Entity](https://dataclient.io/rest/api/Entity.md) or [Union](https://dataclient.io/rest/api/Union.md).

- [getList](#getlist) uses an [Array](https://dataclient.io/rest/api/Array.md) [Collection](https://dataclient.io/rest/api/Collection.md) of the schema.
- [delete](#delete) uses a [Invalidate](https://dataclient.io/rest/api/Invalidate.md) of the schema.

### urlPrefix

Passed to [RestEndpoint.urlPrefix](https://dataclient.io/rest/api/RestEndpoint.md#urlPrefix)

### searchParams

Passed to [RestEndpoint.searchParams](https://dataclient.io/rest/api/RestEndpoint.md#searchParams) for [getList](#getlist) and [getList.push](#push)

### body

Passed to [RestEndpoint.body](https://dataclient.io/rest/api/RestEndpoint.md#body) for [getList.push](#push) [update](#update) and [partialUpdate](#partialupdate)

### paginationField

If specified, will add [Resource.getList.getPage](#getpage) method on the `Resource`.

### nonFilterArgumentKeys

Pass-through option to [Collection.nonFilterArgumentKeys](https://dataclient.io/rest/api/Collection.md#nonFilterArgumentKeys)
for [getList](#getlist) schema.

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

`RegExp` and function forms are also supported:

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

### optimistic

`true` makes all mutation endpoints [optimistic](https://dataclient.io/rest/guides/optimistic-updates.md), making UI
updates immediate, even before fetch completion.

### Endpoint

Class used to construct the members.

```ts
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](https://dataclient.io/rest/api/Collection.md) used to construct [getList](#getlist) schema.
Use this when you need to customize collection behavior beyond
[`nonFilterArgumentKeys`](#nonfilterargumentkeys), like changing move merge logic.

```ts
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](https://dataclient.io/rest/api/RestEndpoint.md#dataexpirylength)

dataExpiryLength, errorExpiryLength, errorPolicy, invalidIfStale, pollFrequency

## Members

These provide the standard [CRUD](https://en.wikipedia.org/wiki/Create,_read,_update_and_delete)
[endpoints](https://dataclient.io/rest/api/Endpoint.md)s common in [REST](https://www.restapitutorial.com/) APIs. Feel free to [customize or add
new endpoints](#extend-new) based to match your API.

```ts
const PostResource = resource({
  schema: Post,
  path: '/:group/posts/:id',
  searchParams: {} as { author?: string },
  paginationField: 'page',
});
```

| Name                        | Method                                                                     | Args                                                | Schema                                                                               |
| --------------------------- | -------------------------------------------------------------------------- | --------------------------------------------------- | ------------------------------------------------------------------------------------ |
| [get](#get)                 | [GET](https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods/GET)       | `[{group: string; id: string}]`                     | [Post](https://dataclient.io/rest/api/Entity.md)                                     |
| [getList](#getlist)         | [GET](https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods/GET)       | `[{group: string; author?: string}]`                | [Collection(\[Post\])](https://dataclient.io/rest/api/Collection.md)                 |
| [getList.push](#push)       | [POST](https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods/POST)     | `[{group: string; author?: string}, Partial<Post>]` | [Collection(\[Post\]).push](https://dataclient.io/rest/api/Collection.md#push)       |
| [getList.unshift](#unshift) | [POST](https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods/POST)     | `[{group: string; author?: string}, Partial<Post>]` | [Collection(\[Post\]).unshift](https://dataclient.io/rest/api/Collection.md#unshift) |
| [getList.getPage](#getpage) | [GET](https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods/GET)       | `[{group: string; author?: string; page: string}]`  | [Collection(\[Post\]).addWith](https://dataclient.io/rest/api/Collection.md#addWith) |
| [getList.move](#move)       | [PATCH](https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods/PATCH)   | `[{group: string; id: string }, Partial<Post>]`     | [Collection(\[Post\]).move](https://dataclient.io/rest/api/Collection.md#move)       |
| [update](#update)           | [PUT](https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods/PUT)       | `[{group: string; id: string }, Partial<Post>]`     | [Post](https://dataclient.io/rest/api/Entity.md)                                     |
| [partialUpdate](#update)    | [PATCH](https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods/PATCH)   | `[{group: string; id: string }, Partial<Post>]`     | [Post](https://dataclient.io/rest/api/Entity.md)                                     |
| [delete](#delete)           | [DELETE](https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods/DELETE) | `[{group: string; id: string }]`                    | [Invalidate(Post)](https://dataclient.io/rest/api/Invalidate.md)                     |

### get

Retrieve a singular entity.

```typescript title="Post"
import { Entity } from '@data-client/rest';

export default class Post extends Entity {
  id = '';
  title = '';
  group = '';
  author = '';
}
```

```typescript title="Resource"
import { resource } from '@data-client/rest';
import Post from './Post';

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

```typescript title="Usage" column
import { PostResource } from './Resource';
PostResource.get({
  group: 'react',
  id: '1',
});
```

|  Field | Value             |
| :----: | ----------------- |
| method | 'GET'             |
|  path  | [path](#path)     |
| schema | [schema](#schema) |

Commonly used with [useSuspense()](https://dataclient.io/docs/api/useSuspense.md), [Controller.invalidate](https://dataclient.io/docs/api/Controller.md#invalidate), [Controller.expireAll](https://dataclient.io/docs/api/Controller.md#expireAll)

### getList

Retrieve a list of entities.

```typescript title="Post"
import { Entity } from '@data-client/rest';

export default class Post extends Entity {
  id = '';
  title = '';
  group = '';
  author = '';
}
```

```typescript title="Resource"
import { resource } from '@data-client/rest';
import Post from './Post';

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

```typescript title="Usage" column
import { PostResource } from './Resource';
PostResource.getList({
  group: 'react',
  author: 'clara',
});
```

|      Field      | Value                                                                      |
| :-------------: | -------------------------------------------------------------------------- |
|      method     | 'GET'                                                                      |
|       path      | removeLastArg([path](#path))                                               |
|   searchParams  | [searchParams](#searchparams)                                              |
| paginationField | [paginationField](#paginationfield)                                        |
|      schema     | [new Collection(\[schema\])](https://dataclient.io/rest/api/Collection.md) |

```ts
resource({ path: '/:first/:second' }).getList.path === '/:first';
resource({ path: '/:first' }).getList.path === '/';
resource({ path: '/:owner/*path' }).getList.path === '/:owner';
```

Commonly used with [useSuspense()](https://dataclient.io/docs/api/useSuspense.md), [Controller.invalidate](https://dataclient.io/docs/api/Controller.md#invalidate), [Controller.expireAll](https://dataclient.io/docs/api/Controller.md#expireAll)

### getList.push {#push}

[RestEndpoint.push](https://dataclient.io/rest/api/RestEndpoint.md#push) creates a new entity and pushes it to the end of getList. Use [getList.unshift](#unshift)
to place at the beginning instead.

```typescript title="Post"
import { Entity } from '@data-client/rest';

export default class Post extends Entity {
  id = '';
  title = '';
  group = '';
  author = '';
}
```

```typescript title="Resource"
import { resource } from '@data-client/rest';
import Post from './Post';

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

```typescript title="Usage" column
import { PostResource } from './Resource';
PostResource.getList.push(
  { group: 'react', author: 'clara' },
  { title: 'winning' },
);
```

|     Field    | Value                                                                    |
| :----------: | ------------------------------------------------------------------------ |
|    method    | 'POST'                                                                   |
|     path     | removeLastArg([path](#path))                                             |
| searchParams | [searchParams](#searchparams)                                            |
|     body     | [body](#body)                                                            |
|    schema    | getList.[schema.push](https://dataclient.io/rest/api/Collection.md#push) |

Commonly used with [Controller.fetch](https://dataclient.io/docs/api/Controller.md#fetch)

### getList.unshift {#unshift}

[RestEndpoint.unshift](https://dataclient.io/rest/api/RestEndpoint.md#unshift) creates a new entity and pushes it to the beginning of getList.

```typescript title="Post"
import { Entity } from '@data-client/rest';

export default class Post extends Entity {
  id = '';
  title = '';
  group = '';
  author = '';
}
```

```typescript title="Resource"
import { resource } from '@data-client/rest';
import Post from './Post';

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

```typescript title="Usage" column
import { PostResource } from './Resource';
PostResource.getList.unshift(
  { group: 'react', author: 'clara' },
  { title: 'winning' },
);
```

|     Field    | Value                                                                          |
| :----------: | ------------------------------------------------------------------------------ |
|    method    | 'POST'                                                                         |
|     path     | removeLastArg([path](#path))                                                   |
| searchParams | [searchParams](#searchparams)                                                  |
|     body     | [body](#body)                                                                  |
|    schema    | getList.[schema.unshift](https://dataclient.io/rest/api/Collection.md#unshift) |

Commonly used with [Controller.fetch](https://dataclient.io/docs/api/Controller.md#fetch)

### getList.getPage {#getpage}

[RestEndpoint.getPage](https://dataclient.io/rest/api/RestEndpoint.md#getpage) retrieves another [page](https://dataclient.io/rest/guides/pagination.md#infinite-scrolling) appending to getList ensuring there are no duplicates.

This member is only available when [paginationField](#paginationfield) is specified.

```typescript title="Post"
import { Entity } from '@data-client/rest';

export default class Post extends Entity {
  id = '';
  title = '';
  group = '';
  author = '';
}
```

```typescript title="Resource"
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',
});
```

```typescript title="Usage" column
import { PostResource } from './Resource';
PostResource.getList.getPage({
  group: 'react',
  author: 'clara',
  page: 2,
});
```

|      Field      | Value                                                                          |
| :-------------: | ------------------------------------------------------------------------------ |
|      method     | 'GET'                                                                          |
|       path      | removeLastArg([path](#path))                                                   |
|   searchParams  | [searchParams](#searchparams)                                                  |
| paginationField | [paginationField](#paginationfield)                                            |
|      schema     | [getList.schema.addWith](https://dataclient.io/rest/api/Collection.md#addWith) |

args: `PathToArgs(shortenPath(path)) & searchParams & \{ [paginationField]: string | number \}`

Commonly used with [Controller.fetch](https://dataclient.io/docs/api/Controller.md#fetch)

### getList.move {#move}

[RestEndpoint.move](https://dataclient.io/rest/api/RestEndpoint.md#move) moves an entity between [Collections](https://dataclient.io/rest/api/Collection.md) by removing it from
collections matching its old state and adding it to collections matching the new values from the body.

```typescript title="Post"
import { Entity } from '@data-client/rest';

export default class Post extends Entity {
  id = '';
  title = '';
  group = '';
  author = '';
}
```

```typescript title="Resource"
import { resource } from '@data-client/rest';
import Post from './Post';

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

```typescript title="Usage" column
import { PostResource } from './Resource';
PostResource.getList.move(
  { group: 'react', id: '1' },
  { group: 'vue' },
);
```

|  Field | Value                                                                    |
| :----: | ------------------------------------------------------------------------ |
| method | 'PATCH'                                                                  |
|  path  | [path](#path)                                                            |
|  body  | [body](#body)                                                            |
| schema | getList.[schema.move](https://dataclient.io/rest/api/Collection.md#move) |

Commonly used with [Controller.fetch](https://dataclient.io/docs/api/Controller.md#fetch)

### update

Update an entity.

```typescript title="Post"
import { Entity } from '@data-client/rest';

export default class Post extends Entity {
  id = '';
  title = '';
  group = '';
  author = '';
}
```

```typescript title="Resource"
import { resource } from '@data-client/rest';
import Post from './Post';

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

```typescript title="Usage" column
import { PostResource } from './Resource';
PostResource.update(
  { group: 'react', id: '1' },
  { title: 'updated title', author: 'clara' },
);
```

|  Field | Value             |
| :----: | ----------------- |
| method | 'PUT'             |
|  path  | [path](#path)     |
|  body  | [body](#body)     |
| schema | [schema](#schema) |

Commonly used with [Controller.fetch](https://dataclient.io/docs/api/Controller.md#fetch)

### partialUpdate

Update some subset of fields of an entity.

```typescript title="Post"
import { Entity } from '@data-client/rest';

export default class Post extends Entity {
  id = '';
  title = '';
  group = '';
  author = '';
}
```

```typescript title="Resource"
import { resource } from '@data-client/rest';
import Post from './Post';

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

```typescript title="Usage" column
import { PostResource } from './Resource';
PostResource.partialUpdate(
  { group: 'react', id: '1' },
  { title: 'updated title' },
);
```

|  Field | Value             |
| :----: | ----------------- |
| method | 'PATCH'           |
|  path  | [path](#path)     |
|  body  | [body](#body)     |
| schema | [schema](#schema) |

Commonly used with [Controller.fetch](https://dataclient.io/docs/api/Controller.md#fetch)

### delete

Deletes an entity.

```typescript title="Post"
import { Entity } from '@data-client/rest';

export default class Post extends Entity {
  id = '';
  title = '';
  group = '';
  author = '';
}
```

```typescript title="Resource"
import { resource } from '@data-client/rest';
import Post from './Post';

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

```typescript title="Usage" column
import { PostResource } from './Resource';
PostResource.delete({ group: 'react', id: '1' });
```

|  Field  | Value                                                                                        |
| :-----: | -------------------------------------------------------------------------------------------- |
|  method | 'DELETE'                                                                                     |
|   path  | [path](#path)                                                                                |
|  schema | [new Invalidate(schema)](https://dataclient.io/rest/api/Invalidate.md)                       |
| process | ```ts
(value, params) {
  return value && Object.keys(value).length ? value : params;
},
``` |

Commonly used with [Controller.fetch](https://dataclient.io/docs/api/Controller.md#fetch)

#### Response

```json
{ "id": "xyz" }
```

Response should either be the [pk](https://dataclient.io/rest/api/Entity.md#pk) as a string (like `'xyz'`). Or an object with the members needed to compute
[Entity.pk](https://dataclient.io/rest/api/Entity.md#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](https://dataclient.io/rest/api/Entity.md#pk). This enables the default implementation to still work with no response, so long as standard
arguments are used.

This allows [Invalidate](https://dataclient.io/rest/api/Invalidate.md) to remove the entity from the [entity table](https://dataclient.io/docs/concepts/normalization.md)

### extend() {#extend}

`resource` builds a great starting point, but often endpoints need to be [further customized](https://dataclient.io/rest/api/RestEndpoint.md#typing).

`extend()` is polymorphic with three forms:

#### Function form (to get BaseResource/super) {#extend-function}

This is the most flexible, but also the most verbose.

```ts
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: '',
    },
  })
)});
```

#### Batch extension of known members {#extend-override}

This only works with existing members.

```ts
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 {#extend-new}

This can only add one endpoint at a time.

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

#### Github CommentResource

Example app: [github-app](https://github.com/reactive/data-client/tree/master/examples/github-app) ([`src/pages/IssueDetail/CommentsList.tsx`](https://github.com/reactive/data-client/blob/master/examples/github-app/src/pages/IssueDetail/CommentsList.tsx), [`src/resources/Comment.ts`](https://github.com/reactive/data-client/blob/master/examples/github-app/src/resources/Comment.ts))

## 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.

```typescript
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 Hybrid

When your API provides both REST and GraphQL endpoints, you can mix them in a single resource.
Use [Entity.process()](https://dataclient.io/rest/api/Entity.md#process) to normalize different response shapes.

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

#### Github Example

Example app: [github-app](https://github.com/reactive/data-client/tree/master/examples/github-app) ([`src/resources/Base.ts`](https://github.com/reactive/data-client/blob/master/examples/github-app/src/resources/Base.ts))
