Skip to content

Commit

Permalink
feat: Queries, Queryable, and useQuery
Browse files Browse the repository at this point in the history
  • Loading branch information
ntucker committed Feb 23, 2024
1 parent 7580500 commit 5d3a298
Show file tree
Hide file tree
Showing 91 changed files with 1,967 additions and 911 deletions.
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,7 @@ ctrl.fetch(ArticleResource.get, { id });
### [Programmatic queries](https://dataclient.io/rest/api/Query)

```tsx
const queryTotalVotes = new Query(
const queryTotalVotes = new schema.Query(
new schema.All(Post),
(posts, { userId } = {}) => {
if (userId !== undefined)
Expand All @@ -144,8 +144,8 @@ const queryTotalVotes = new Query(
},
);

const totalVotes = useCache(queryTotalVotes);
const totalVotesForUser = useCache(queryTotalVotes, { userId });
const totalVotes = useQuery(queryTotalVotes);
const totalVotesForUser = useQuery(queryTotalVotes, { userId });
```

### [Powerful Middlewares](https://dataclient.io/docs/concepts/managers)
Expand Down
22 changes: 14 additions & 8 deletions __tests__/new.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import {
schema,
Endpoint,
Index,
createResource,
RestEndpoint,
Schema,
Expand Down Expand Up @@ -188,6 +187,13 @@ export class Article extends Entity {
author: User,
};
}

export class ArticleWithSlug extends Article {
readonly slug: string = '';

static indexes = ['slug'] as const;
}

class ArticleData {
readonly id: number | undefined = undefined;
readonly title: string = '';
Expand Down Expand Up @@ -265,6 +271,9 @@ function createArticleResource<O extends ArticleGenerics>({
return resource as any;
}
export const ArticleResource = createArticleResource({ schema: Article });
export const ArticleSlugResource = createArticleResource({
schema: ArticleWithSlug,
});

export const AuthContext = createContext('');

Expand Down Expand Up @@ -518,13 +527,10 @@ export const CoolerArticleDetail = new Endpoint(
export class IndexedUser extends User {
static readonly indexes = ['username'];
}
export const IndexedUserResource = {
...createResource({
path: 'http\\://test.com/user/:id',
schema: IndexedUser,
}),
getIndex: new Index(IndexedUser),
};
export const IndexedUserResource = createResource({
path: 'http\\://test.com/user/:id',
schema: IndexedUser,
});

class InvalidIfStaleEndpoint<
O extends RestGenerics = any,
Expand Down
46 changes: 42 additions & 4 deletions docs/core/api/Controller.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ class Controller {
subscribe(endpoint, ...args): Promise<void>;
unsubscribe(endpoint, ...args): Promise<void>;
/*************** Data Access ***************/
query(queryable, ...args, state): Denormalized<typeof queryable>;
getResponse(endpoint, ...args, state): { data; expiryStatus; expiresAt };
getError(endpoint, ...args, state): ErrorTypes | undefined;
snapshot(state: State<unknown>, fetchedAt?: number): SnapshotInterface;
Expand Down Expand Up @@ -124,7 +125,10 @@ When using schemas, the denormalized value is returned
// highlight-next-line
import { useController } from '@data-client/react';

const post = await controller.fetch(PostResource.getList.push, createPayload);
const post = await controller.fetch(
PostResource.getList.push,
createPayload,
);
post.title;
post.pk();
```
Expand Down Expand Up @@ -364,7 +368,11 @@ useEffect(() => {
const websocket = new Websocket(url);

websocket.onmessage = event =>
ctrl.setResponse(EndpointLookup[event.endpoint], ...event.args, event.data);
ctrl.setResponse(
EndpointLookup[event.endpoint],
...event.args,
event.data,
);

return () => websocket.close();
});
Expand Down Expand Up @@ -412,6 +420,36 @@ decrement the subscription and if the count reaches 0, more updates won't be rec

[useSubscription](./useSubscription.md) and [useLive](./useLive.md) call this on unmount.

## query(schema, ...args, state) {#query}

Looks up any [Queryable](./useQuery.md#queryable) [Schema](/rest/api/schema#schema-overview) in `state`.

### Example

This is used in [useQuery](./useQuery.md) and can be used in
[Managers](./Manager.md) to safely access the store.

```tsx title="useQuery.ts"
import {
useController,
useCacheState,
type Queryable,
type SchemaArgs,
type DenormalizeNullable,
} from '@data-client/core';

/** Oversimplified useQuery */
function useQuery<S extends Queryable>(
schema: S,
...args: SchemaArgs<S>
): DenormalizeNullable<S> | undefined {
const state = useCacheState();
const controller = useController();

return controller.query(schema, ...args, state);
}
```

## getResponse(endpoint, ...args, state) {#getResponse}

```ts title="returns"
Expand Down Expand Up @@ -487,15 +525,15 @@ import type { EndpointInterface } from '@data-client/endpoint';
export default class MyManager implements Manager {
protected declare middleware: Middleware;
constructor() {
this.middleware = ({ controller, getState }) => {
this.middleware = controller => {
return next => async action => {
if (action.type === actionTypes.FETCH_TYPE) {
console.log('The existing response of the requested fetch');
console.log(
controller.getResponse(
action.endpoint,
...(action.meta.args as Parameters<typeof action.endpoint>),
getState(),
controller.getState(),
).data,
);
}
Expand Down
6 changes: 5 additions & 1 deletion docs/core/api/Snapshot.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ allow safe and performant access to the denormalized data based on the current s

```ts
interface Snapshot {
query(schema, ...args)​ => DenormalizeNullable<typeof schema> | undefined;
getResponse(endpoint, ...args)​ => { data, expiryStatus, expiresAt };
getError(endpoint, ...args)​ => ErrorTypes | undefined;
fetchedAt: number;
Expand All @@ -37,6 +38,10 @@ Use [Controller.snapshot()](./Controller.md#snapshot) to construct a snapshot

## Members

### query(schema, ...args) {#query}

Looks up any [Queryable](./useQuery.md#queryable) [Schema](/rest/api/schema#schema-overview).

### getResponse(endpoint, ...args) {#getResponse}

```ts title="returns"
Expand Down Expand Up @@ -82,7 +87,6 @@ export enum ExpiryStatus {

A number representing time when it expires. Compare to Date.now().


### getError(endpoint, ...args) {#getError}

Gets the error, if any, for a given endpoint. Returns undefined for no errors.
Expand Down
73 changes: 2 additions & 71 deletions docs/core/api/useCache.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,8 @@ import { RestEndpoint } from '@data-client/rest';

Data rendering without the fetch.

General purpose store access can be useful when the data's existance is of interest (like if a user is authenticated),
or general purpose store access like [Query](/rest/api/Query).
Access any [Endpoint](/rest/api/Endpoint)'s response. If the response does not exist, returns
`undefined`. This can be used to check for an `Endpoint's` existance like for authentication.

`useCache()` is reactive to data [mutations](../getting-started/mutations.md); rerendering only when necessary.

Expand Down Expand Up @@ -147,75 +147,6 @@ function useCache<

## Examples

### Query arbitrary Entities

[Query](/rest/api/Query) provides programmatic access to the Reactive Data Client store.

<HooksPlayground fixtures={[
{
endpoint: new RestEndpoint({path: '/users'}),
args: [],
response: [
{ id: '123', name: 'Jim' },
{ id: '456', name: 'Jane' },
{ id: '777', name: 'Albatras', isAdmin: true },
],
delay: 150,
},
]} row>

```ts title="UserResource" collapsed
export class User extends Entity {
id = '';
name = '';
isAdmin = false;
pk() {
return this.id;
}
static key = 'User';
}
export const UserResource = createResource({
path: '/users/:id',
schema: User,
});
```

```tsx title="UsersPage" {17}
import { Query, schema } from '@data-client/rest';
import { UserResource, User } from './UserResource';

const sortedUsers = new Query(
new schema.All(User),
(entries, { asc } = { asc: false }) => {
const sorted = [...entries].sort((a, b) =>
a.name.localeCompare(b.name),
);
if (asc) return sorted;
return sorted.reverse();
},
);

function UsersPage() {
useFetch(UserResource.getList);
const users = useCache(sortedUsers, { asc: true });
if (!users) return <div>No users in cache yet</div>;
return (
<div>
{users.map(user => (
<div key={user.pk()}>{user.name}</div>
))}
</div>
);
}
render(<UsersPage />);
```

</HooksPlayground>

### Remaining Todo Query

[Queries](/rest/api/Query) can also be used to compute aggregates

<StackBlitz app="todo-app" file="src/resources/TodoResource.ts,src/pages/Home/TodoStats.tsx" />

### Github Navbar login/logout
Expand Down
Loading

1 comment on commit 5d3a298

@github-actions
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Benchmark

Benchmark suite Current: 5d3a298 Previous: 8377e0a Ratio
normalizeLong 446 ops/sec (±1.81%) 437 ops/sec (±1.67%) 0.98
infer All 9449 ops/sec (±0.30%) 9151 ops/sec (±1.17%) 0.97
denormalizeLong 238 ops/sec (±3.08%) 321 ops/sec (±2.64%) 1.35
denormalizeLong donotcache 851 ops/sec (±1.34%) 855 ops/sec (±0.46%) 1.00
denormalizeShort donotcache 500x 1331 ops/sec (±0.11%) 1358 ops/sec (±0.36%) 1.02
denormalizeShort 500x 699 ops/sec (±2.66%) 959 ops/sec (±0.34%) 1.37
denormalizeShort 500x withCache 4509 ops/sec (±0.18%) 4370 ops/sec (±0.18%) 0.97
denormalizeLong with mixin Entity 229 ops/sec (±2.33%) 296 ops/sec (±0.67%) 1.29
denormalizeLong withCache 7126 ops/sec (±0.17%) 7191 ops/sec (±0.07%) 1.01
denormalizeLongAndShort withEntityCacheOnly 1583 ops/sec (±0.62%) 1625 ops/sec (±0.30%) 1.03
denormalizeLong All withCache 6278 ops/sec (±0.11%) 6282 ops/sec (±0.15%) 1.00
denormalizeLong Query-sorted withCache 6548 ops/sec (±0.14%) 6584 ops/sec (±0.21%) 1.01
getResponse 5403 ops/sec (±1.68%) 5214 ops/sec (±1.53%) 0.97
getResponse (null) 3207021 ops/sec (±0.30%) 2918129 ops/sec (±0.41%) 0.91
getResponse (clear cache) 221 ops/sec (±2.39%) 299 ops/sec (±0.52%) 1.35
getSmallResponse 2328 ops/sec (±0.34%) 2199 ops/sec (±0.41%) 0.94
getSmallInferredResponse 1885 ops/sec (±0.30%) 1788 ops/sec (±0.34%) 0.95
getResponse Query-sorted 1100 ops/sec (±0.51%) 687 ops/sec (±1.71%) 0.62
getResponse Collection 5062 ops/sec (±1.17%) 5114 ops/sec (±1.64%) 1.01
setLong 428 ops/sec (±2.04%) 433 ops/sec (±2.24%) 1.01
setLongWithMerge 185 ops/sec (±0.34%) 188 ops/sec (±0.23%) 1.02
setLongWithSimpleMerge 192 ops/sec (±0.47%) 201 ops/sec (±0.18%) 1.05

This comment was automatically generated by workflow using github-action-benchmark.

Please sign in to comment.