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 Mar 4, 2024
1 parent 3e0e917 commit f8d6158
Show file tree
Hide file tree
Showing 101 changed files with 3,454 additions and 1,081 deletions.
11 changes: 11 additions & 0 deletions .changeset/quick-flies-taste.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
---
"@data-client/normalizr": patch
"@data-client/endpoint": patch
"@data-client/react": patch
"@data-client/redux": patch
"@data-client/core": patch
"@data-client/rest": patch
"@data-client/test": patch
---

Update README
7 changes: 4 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 Expand Up @@ -271,5 +271,6 @@ For the small price of 9kb gziped.    [🏁Get started now](https://da
- [Values](https://dataclient.io/rest/api/Values)
- [All](https://dataclient.io/rest/api/All)
- [Collection](https://dataclient.io/rest/api/Collection)
- [Query](https://dataclient.io/rest/api/Query)
- [Union](https://dataclient.io/rest/api/Union)
- [Invalidate](https://dataclient.io/rest/api/Invalidate)
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 ***************/
get(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.

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

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.get(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 {
get(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

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

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 f8d6158

@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: f8d6158 Previous: 72762cf Ratio
normalizeLong 433 ops/sec (±1.83%) 442 ops/sec (±2.04%) 1.02
infer All 9390 ops/sec (±0.48%) 9205 ops/sec (±1.62%) 0.98
denormalizeLong 232 ops/sec (±3.29%) 234 ops/sec (±3.73%) 1.01
denormalizeLong donotcache 822 ops/sec (±1.44%) 853 ops/sec (±0.51%) 1.04
denormalizeShort donotcache 500x 1370 ops/sec (±0.25%) 1363 ops/sec (±0.19%) 0.99
denormalizeShort 500x 733 ops/sec (±2.37%) 721 ops/sec (±2.37%) 0.98
denormalizeShort 500x withCache 4455 ops/sec (±0.23%) 4445 ops/sec (±0.29%) 1.00
denormalizeLong with mixin Entity 224 ops/sec (±2.53%) 231 ops/sec (±2.56%) 1.03
denormalizeLong withCache 7886 ops/sec (±0.20%) 7007 ops/sec (±0.79%) 0.89
denormalizeLongAndShort withEntityCacheOnly 1581 ops/sec (±0.30%) 1585 ops/sec (±0.33%) 1.00
denormalizeLong All withCache 5844 ops/sec (±0.13%) 6137 ops/sec (±0.48%) 1.05
denormalizeLong Query-sorted withCache 6047 ops/sec (±0.24%) 6590 ops/sec (±0.17%) 1.09
getResponse 4918 ops/sec (±0.90%) 4936 ops/sec (±0.91%) 1.00
getResponse (null) 3204157 ops/sec (±0.30%) 2914817 ops/sec (±0.24%) 0.91
getResponse (clear cache) 223 ops/sec (±2.36%) 232 ops/sec (±2.39%) 1.04
getSmallResponse 2331 ops/sec (±0.34%) 2226 ops/sec (±0.26%) 0.95
getSmallInferredResponse 1901 ops/sec (±0.32%) 1784 ops/sec (±0.32%) 0.94
getResponse Query-sorted 1026 ops/sec (±0.40%) 650 ops/sec (±1.14%) 0.63
getResponse Collection 5036 ops/sec (±1.27%) 5242 ops/sec (±1.42%) 1.04
setLong 418 ops/sec (±2.11%) 428 ops/sec (±2.77%) 1.02
setLongWithMerge 182 ops/sec (±0.48%) 186 ops/sec (±0.28%) 1.02
setLongWithSimpleMerge 195 ops/sec (±0.30%) 195 ops/sec (±0.28%) 1

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

Please sign in to comment.