# Query

`Query` provides programmatic access to the Reactive Data Client cache while maintaining
the same high performance and referential equality guarantees expected of Reactive Data Client.

`Query` can be rendered using [schema lookup hook useQuery()](https://dataclient.io/docs/api/useQuery.md)

## Query members

### schema

[Schema](https://dataclient.io/rest/api/schema.md) used to retrieve/denormalize data from the Reactive Data Client cache.
This accepts any [Queryable](https://dataclient.io/rest/api/schema.md#queryable) schema: [Entity](https://dataclient.io/rest/api/Entity.md), [All](https://dataclient.io/rest/api/All.md), [Collection](https://dataclient.io/rest/api/Collection.md), [Query](https://dataclient.io/rest/api/Query.md),
[Union](https://dataclient.io/rest/api/Union.md), [Scalar](https://dataclient.io/rest/api/Scalar.md), and [Object](https://dataclient.io/rest/api/Object.md) schemas for joining multiple entities.
[Lazy](https://dataclient.io/rest/api/Lazy.md) fields produce a Queryable via their [`.query`](https://dataclient.io/rest/api/Lazy.md#query) accessor.

### process(entries, ...args) {#process}

Takes the (denormalized) response as entries and arguments and returns the new
response for use with [useQuery](https://dataclient.io/docs/api/useQuery.md)

## Usage

### Maintaining sort after creates {#sorting}

```ts title="getPosts" {17-24}
import { Collection, Entity, Query, RestEndpoint } from '@data-client/rest';

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

export const getPosts = new RestEndpoint({
  path: '/:group/posts',
  searchParams: {} as { orderBy?: string; author?: string },
  schema: new Query(
    new Collection([Post], {
      nonFilterArgumentKeys: /orderBy/,
    }),
    (posts, { orderBy } = {}) => {
      if (orderBy) {
        return [...posts].sort((a, b) =>
          a[orderBy].localeCompare(b[orderBy]),
        );
      }
      return posts;
    },
  ),
});
```

```tsx title="NewPost"
import { useController, useLoading } from '@data-client/react';
import { getPosts } from './getPosts';

export default function NewPost({ author }: Props) {
  const ctrl = useController();

  const [handlePress, loading] = useLoading(async e => {
    if (e.key === 'Enter') {
      const title = e.currentTarget.value;
      e.currentTarget.value = '';
      await ctrl.fetch(
        getPosts.push,
        { group: 'react' },
        {
          title,
          author,
        },
      );
    }
  });

  return <TextInput onKeyDown={handlePress} loading={loading} placeholder="Post title" />;
}
interface Props {
  author: string;
}
```

```tsx title="PostList" {8}
import { useSuspense } from '@data-client/react';
import { getPosts } from './getPosts';
import NewPost from './NewPost';

export default function PostList({ author }: Props) {
  const posts = useSuspense(getPosts, {
    author,
    orderBy: 'title',
    group: 'react',
  });
  return (
    <div>
      {posts.map(post => (
        <div key={post.pk()}>{post.title}</div>
      ))}
      <NewPost author={author} />
    </div>
  );
}
interface Props {
  author: string;
}
```

```tsx title="UserList"
import PostList from './PostList';

function UserList() {
  const users = ['bob', 'clara'];
  return (
    <div>
      {users.map(user => (
        <section key={user}>
          <h3>{user}</h3>
          <PostList author={user} />
        </section>
      ))}
    </div>
  );
}
render(<UserList />);
```

### Aggregates

```ts title="resources/User"
import { Entity, resource } from '@data-client/rest';

export class User extends Entity {
  id = '';
  name = '';
  isAdmin = false;
}
export const UserResource = resource({
  path: '/users/:id',
  schema: User,
});
```

```tsx title="UsersPage"
import { All, Query } from '@data-client/rest';
import { useQuery, useFetch } from '@data-client/react';
import { UserResource, User } from './resources/User';

const countUsers = new Query(
  new All(User),
  (entries, { isAdmin } = {}) => {
    if (isAdmin !== undefined)
      return entries.filter(user => user.isAdmin === isAdmin).length;
    return entries.length;
  },
);

function UsersPage() {
  useFetch(UserResource.getList);
  const userCount = useQuery(countUsers);
  const adminCount = useQuery(countUsers, { isAdmin: true });
  if (userCount === undefined) return <div>No users in cache yet</div>;
  return (
    <div>
      <div>Total users: {userCount}</div>
      <div>Total admins: {adminCount}</div>
    </div>
  );
}
render(<UsersPage />);
```

### Rearranging data with groupBy aggregations {#groupby}

```ts title="resources/User"
import { Entity, resource } from '@data-client/rest';

export class User extends Entity {
  id = 0;
  username = '';
  name = '';
  email = '';
  website = '';
}
export const UserResource = resource({
  urlPrefix: 'https://jsonplaceholder.typicode.com',
  path: '/users/:id',
  schema: User,
});
```

```ts title="resources/Todo"
import { Entity, resource } from '@data-client/rest';
import { User } from './User';

export class Todo extends Entity {
  id = 0;
  userId = 0;
  user? = User.fromJS({});
  title = '';
  completed = false;

  static schema = {
    user: User,
  };
  static process(input) {
    return { ...input, user: input.userId };
  }
}
export const TodoResource = resource({
  urlPrefix: 'https://jsonplaceholder.typicode.com',
  path: '/todos/:id',
  schema: Todo,
  searchParams: {} as { userId?: string | number } | undefined,
});
```

```tsx title="TodoByUser"
import { useQuery } from '@data-client/react';
import { User } from './resources/User';
import type { Todo } from './resources/Todo';

export default function TodoByUser({ userId, todos }: Props) {
  const user = useQuery(User, { id: userId });
  // don't bother if no user is loaded yet
  if (!user) return null;
  return (
    <div>
      <h3>
        {user.name} has {tasksRemaining(todos)} tasks left
      </h3>
      {todos.slice(0, 3).map(todo => (
        <div key={todo.pk()}>
          {todo.title} by {todo.user === user ? todo.user.name : ''}
        </div>
      ))}
    </div>
  );
}
function tasksRemaining(todos: Todo[]) {
  return todos.filter(({ completed }) => !completed).length;
}
interface Props {
  userId: string;
  todos: Todo[];
}
```

```tsx title="TodoJoined"
import { Query } from '@data-client/rest';
import { useQuery, useFetch, useSuspense } from '@data-client/react';
import { TodoResource } from './resources/Todo';
import { UserResource } from './resources/User';
import TodoByUser from './TodoByUser';

const groupTodoByUser = new Query(
  TodoResource.getList.schema,
  todos => Object.groupBy(todos, todo => todo.userId),
);

function TodosPage() {
  useFetch(UserResource.getList);
  useSuspense(TodoResource.getList);
  useSuspense(UserResource.getList);
  const todosByUser = useQuery(groupTodoByUser);
  if (!todosByUser) return <div>Todos not found</div>;
  return (
    <div>
      {Object.keys(todosByUser).slice(5).map(userId => (
        <TodoByUser
          key={userId}
          userId={userId}
          todos={todosByUser[userId]}
        />
      ))}
    </div>
  );
}
render(<TodosPage />);
```

### Object Schema Joins {#object-schema-joins}

`Query` can take [Object Schemas](https://dataclient.io/rest/api/Object.md), enabling joins across multiple entity types. This allows you to combine data from different entities in a single query.

```ts title="resources/Ticker"
import { Entity, resource } from '@data-client/rest';

export class Ticker extends Entity {
  product_id = '';
  price = 0;
  pk() { return this.product_id; }
}

export const TickerResource = resource({
  path: '/tickers/:product_id',
  schema: Ticker,
});
```

```ts title="resources/Stats"
import { Entity, resource } from '@data-client/rest';

export class Stats extends Entity {
  product_id = '';
  last = 0;
  pk() { return this.product_id; }
}

export const StatsResource = resource({
  path: '/stats/:product_id',
  schema: Stats,
});
```

```tsx title="PriceDisplay"
import { Query } from '@data-client/rest';
import { useQuery, useFetch } from '@data-client/react';
import { TickerResource, Ticker } from './resources/Ticker';
import { StatsResource, Stats } from './resources/Stats';

// Join Ticker and Stats by product_id
const queryPrice = new Query(
  { ticker: Ticker, stats: Stats },
  ({ ticker, stats }) => ticker?.price ?? stats?.last,
);

function PriceDisplay({ productId }: { productId: string }) {
  useFetch(TickerResource.get, { product_id: productId });
  useFetch(StatsResource.get, { product_id: productId });
  const price = useQuery(queryPrice, { product_id: productId });
  
  if (price === undefined) return <div>Loading...</div>;
  return <div>Price: ${price}</div>;
}

render(<PriceDisplay productId="BTC-USD" />);
```

### Fallback joins

In this case `Ticker` is constantly updated from a websocket stream. However, there is no bulk/list
fetch for `Ticker` - making it inefficient for getting the prices on a list view.

So in this case we can fetch a list of `Stats` as a fallback since it has price data as well.

Example app: [coin-app](https://github.com/reactive/data-client/tree/master/examples/coin-app) ([`src/pages/Home/CurrencyList.tsx`](https://github.com/reactive/data-client/blob/master/examples/coin-app/src/pages/Home/CurrencyList.tsx), [`src/pages/Home/AssetPrice.tsx`](https://github.com/reactive/data-client/blob/master/examples/coin-app/src/pages/Home/AssetPrice.tsx), [`src/resources/fallbackQueries.ts`](https://github.com/reactive/data-client/blob/master/examples/coin-app/src/resources/fallbackQueries.ts))
