@hypequery/react 0.1.1 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +205 -274
- package/dist/analyticsHooks.d.ts +34 -0
- package/dist/analyticsHooks.d.ts.map +1 -0
- package/dist/analyticsHooks.js +27 -0
- package/dist/analyticsHooks.test.d.ts +2 -0
- package/dist/analyticsHooks.test.d.ts.map +1 -0
- package/dist/analyticsHooks.test.js +98 -0
- package/dist/createHooks.d.ts +31 -2
- package/dist/createHooks.d.ts.map +1 -1
- package/dist/createHooks.js +89 -20
- package/dist/createHooks.test.js +190 -2
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/types.d.ts +54 -1
- package/dist/types.d.ts.map +1 -1
- package/package.json +7 -4
package/README.md
CHANGED
|
@@ -1,377 +1,308 @@
|
|
|
1
1
|
# @hypequery/react
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
React hooks for typed Hypequery HTTP endpoints, built on TanStack Query.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
Use `@hypequery/react` with APIs created by `@hypequery/serve`. The hooks call generated routes and infer input/output types from the API type you pass in.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
6
8
|
|
|
7
9
|
```bash
|
|
8
10
|
npm install @hypequery/react @tanstack/react-query
|
|
11
|
+
# or
|
|
12
|
+
pnpm add @hypequery/react @tanstack/react-query
|
|
9
13
|
```
|
|
10
14
|
|
|
11
|
-
Peer dependencies: `react@^18`, `@tanstack/react-query@^5`.
|
|
12
|
-
|
|
13
15
|
## Quick Start
|
|
14
16
|
|
|
15
|
-
|
|
16
|
-
// lib/analytics.ts
|
|
17
|
-
import { createHooks } from '@hypequery/react';
|
|
18
|
-
import type { Api } from '@/analytics/queries';
|
|
17
|
+
Given a serve API:
|
|
19
18
|
|
|
20
|
-
|
|
21
|
-
|
|
19
|
+
```ts
|
|
20
|
+
// server/api.ts
|
|
21
|
+
import { initServe } from '@hypequery/serve';
|
|
22
|
+
import { z } from 'zod';
|
|
23
|
+
import { db } from './db.js';
|
|
24
|
+
|
|
25
|
+
const { query, serve } = initServe({
|
|
26
|
+
context: () => ({ db }),
|
|
27
|
+
basePath: '/api/analytics',
|
|
22
28
|
});
|
|
23
|
-
```
|
|
24
29
|
|
|
25
|
-
|
|
30
|
+
const weeklyRevenue = query({
|
|
31
|
+
description: 'Weekly revenue',
|
|
32
|
+
input: z.object({ startDate: z.string() }),
|
|
33
|
+
query: ({ ctx, input }) =>
|
|
34
|
+
ctx.db
|
|
35
|
+
.table('orders')
|
|
36
|
+
.where('created_at', 'gte', input.startDate)
|
|
37
|
+
.sum('total', 'revenue')
|
|
38
|
+
.execute(),
|
|
39
|
+
});
|
|
26
40
|
|
|
27
|
-
|
|
28
|
-
|
|
41
|
+
export const api = serve({
|
|
42
|
+
queries: { weeklyRevenue },
|
|
43
|
+
});
|
|
29
44
|
|
|
30
|
-
|
|
45
|
+
api.route('/weeklyRevenue', api.queries.weeklyRevenue);
|
|
31
46
|
|
|
32
|
-
export
|
|
33
|
-
return (
|
|
34
|
-
<QueryClientProvider client={queryClient}>
|
|
35
|
-
{children}
|
|
36
|
-
</QueryClientProvider>
|
|
37
|
-
);
|
|
38
|
-
}
|
|
47
|
+
export type AnalyticsApi = typeof api;
|
|
39
48
|
```
|
|
40
49
|
|
|
41
|
-
|
|
50
|
+
Create hooks on the client:
|
|
42
51
|
|
|
43
52
|
```tsx
|
|
44
|
-
//
|
|
45
|
-
|
|
53
|
+
// client/analytics-hooks.ts
|
|
54
|
+
import { createHooks } from '@hypequery/react';
|
|
55
|
+
import type { AnalyticsApi } from '../server/api.js';
|
|
46
56
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
57
|
+
export const { useQuery, useMutation } = createHooks<AnalyticsApi>({
|
|
58
|
+
baseUrl: '/api/analytics',
|
|
59
|
+
api: {} as AnalyticsApi,
|
|
60
|
+
});
|
|
50
61
|
```
|
|
51
62
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
## API Reference
|
|
55
|
-
|
|
56
|
-
### `createHooks<Api>(config)`
|
|
63
|
+
Use them inside a component:
|
|
57
64
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
65
|
+
```tsx
|
|
66
|
+
function RevenuePanel() {
|
|
67
|
+
const revenue = useQuery('weeklyRevenue', {
|
|
68
|
+
startDate: '2026-01-01',
|
|
69
|
+
});
|
|
61
70
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
baseUrl: string; // Required: API base URL (e.g., '/api' or 'https://api.example.com')
|
|
65
|
-
fetchFn?: typeof fetch; // Optional: Custom fetch implementation (defaults to global fetch)
|
|
66
|
-
headers?: Record<string, string | undefined> | (() => Record<string, string | undefined>); // Optional: Default headers or a resolver function
|
|
67
|
-
config?: Record<string, QueryMethodConfig>; // Optional: Per-route HTTP method overrides
|
|
68
|
-
api?: TApi; // Optional: API object to auto-extract HTTP methods
|
|
69
|
-
}
|
|
71
|
+
if (revenue.isLoading) return <p>Loading...</p>;
|
|
72
|
+
if (revenue.error) return <p>{revenue.error.message}</p>;
|
|
70
73
|
|
|
71
|
-
|
|
72
|
-
method?: string; // HTTP method: 'GET', 'POST', 'PUT', 'DELETE', etc.
|
|
74
|
+
return <pre>{JSON.stringify(revenue.data, null, 2)}</pre>;
|
|
73
75
|
}
|
|
74
76
|
```
|
|
75
77
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
```ts
|
|
79
|
-
{
|
|
80
|
-
useQuery: <Name>(name, input?, options?) => UseQueryResult<Output, HttpError>,
|
|
81
|
-
useMutation: <Name>(name, options?) => UseMutationResult<Output, HttpError, Input>
|
|
82
|
-
}
|
|
83
|
-
```
|
|
78
|
+
## Dataset And Metric Hooks
|
|
84
79
|
|
|
85
|
-
|
|
80
|
+
`createAnalyticsHooks` adds convenience wrappers for semantic endpoint names. Metrics use their endpoint name directly. Dataset endpoints are addressed as `dataset:<name>` in the API type and exposed through `useDataset(name, ...)`.
|
|
86
81
|
|
|
87
|
-
```
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
'X-API-Key': process.env.API_KEY,
|
|
92
|
-
},
|
|
93
|
-
config: {
|
|
94
|
-
// Override HTTP method for specific routes
|
|
95
|
-
uploadFile: { method: 'POST' },
|
|
96
|
-
},
|
|
97
|
-
});
|
|
98
|
-
```
|
|
82
|
+
```tsx
|
|
83
|
+
import { createAnalyticsHooks } from '@hypequery/react';
|
|
84
|
+
import type { InferApiType } from '@hypequery/serve';
|
|
85
|
+
import type { api } from '../server/api.js';
|
|
99
86
|
|
|
100
|
-
|
|
87
|
+
import manifest from './hypequery-manifest.json';
|
|
101
88
|
|
|
102
|
-
|
|
89
|
+
type AnalyticsApi = InferApiType<typeof api>;
|
|
103
90
|
|
|
104
|
-
|
|
91
|
+
export const { useMetric, useDataset } = createAnalyticsHooks<AnalyticsApi>({
|
|
92
|
+
baseUrl: '/api/analytics',
|
|
93
|
+
manifest,
|
|
94
|
+
metrics: ['revenue', 'averageOrderValue'] as const,
|
|
95
|
+
});
|
|
105
96
|
|
|
106
|
-
|
|
97
|
+
function Dashboard() {
|
|
98
|
+
const revenue = useMetric('revenue', {
|
|
99
|
+
dimensions: ['country'],
|
|
100
|
+
filters: [{ field: 'status', operator: 'eq', value: 'completed' }],
|
|
101
|
+
orderBy: [{ field: 'revenue', direction: 'desc' }],
|
|
102
|
+
limit: 10,
|
|
103
|
+
});
|
|
107
104
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
105
|
+
const orders = useDataset('orders', {
|
|
106
|
+
dimensions: ['status'],
|
|
107
|
+
measures: ['revenue', 'orderCount'],
|
|
108
|
+
});
|
|
112
109
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
110
|
+
return (
|
|
111
|
+
<pre>
|
|
112
|
+
{JSON.stringify({ revenue: revenue.data, orders: orders.data }, null, 2)}
|
|
113
|
+
</pre>
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
116
|
```
|
|
117
117
|
|
|
118
|
-
|
|
118
|
+
## Pagination
|
|
119
119
|
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
**Returns:** TanStack Query's `UseQueryResult<Output, HttpError>`
|
|
125
|
-
|
|
126
|
-
**Examples:**
|
|
120
|
+
Semantic queries that set a `limit` return `meta.pagination = { limit, offset, hasMore }`
|
|
121
|
+
(`hasMore` is exact — the server over-fetches one row rather than running a count query).
|
|
122
|
+
`useInfiniteQuery`, and the `useInfiniteMetric` / `useInfiniteDataset` wrappers, build on
|
|
123
|
+
this to page through results. They advance the offset automatically and request meta for you.
|
|
127
124
|
|
|
128
125
|
```tsx
|
|
129
|
-
|
|
130
|
-
const
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
126
|
+
function OrdersTable() {
|
|
127
|
+
const orders = useInfiniteDataset('orders', {
|
|
128
|
+
dimensions: ['status'],
|
|
129
|
+
measures: ['revenue'],
|
|
130
|
+
limit: 50,
|
|
131
|
+
});
|
|
134
132
|
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
133
|
+
return (
|
|
134
|
+
<>
|
|
135
|
+
{orders.data?.pages.flatMap((page) => page.data).map((row, i) => (
|
|
136
|
+
<Row key={i} row={row} />
|
|
137
|
+
))}
|
|
138
|
+
<button
|
|
139
|
+
onClick={() => orders.fetchNextPage()}
|
|
140
|
+
disabled={!orders.hasNextPage || orders.isFetchingNextPage}
|
|
141
|
+
>
|
|
142
|
+
Load more
|
|
143
|
+
</button>
|
|
144
|
+
</>
|
|
145
|
+
);
|
|
146
|
+
}
|
|
144
147
|
```
|
|
145
148
|
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
### `useMutation(name, options?)`
|
|
149
|
+
`input.limit` is the page size; `input.offset`, if provided, is the starting offset.
|
|
149
150
|
|
|
150
|
-
|
|
151
|
+
## Route Configuration
|
|
151
152
|
|
|
152
|
-
|
|
153
|
+
Hooks need to know each endpoint's HTTP method and path. There are three ways to
|
|
154
|
+
supply that, in increasing precedence: a route manifest, a runtime `api` object,
|
|
155
|
+
or explicit `config`.
|
|
153
156
|
|
|
154
|
-
|
|
155
|
-
- `options` - TanStack Query mutation options (`onSuccess`, `onError`, `retry`, etc.)
|
|
157
|
+
### Route manifest (recommended)
|
|
156
158
|
|
|
157
|
-
|
|
159
|
+
`@hypequery/serve`'s `api.manifest()` returns a serializable map of every
|
|
160
|
+
query/metric/dataset key to its `{ method, path }`. Export it from a server-only
|
|
161
|
+
module and pass it to the hooks — this avoids importing server code into the
|
|
162
|
+
browser bundle while keeping client routes in sync with the server.
|
|
158
163
|
|
|
159
|
-
|
|
164
|
+
```ts
|
|
165
|
+
// server side (server-only module)
|
|
166
|
+
export const manifest = api.manifest();
|
|
160
167
|
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
// Mutation with callbacks
|
|
167
|
-
const updateProfile = useMutation('updateProfile', {
|
|
168
|
-
onSuccess: (data) => {
|
|
169
|
-
console.log('Profile updated:', data);
|
|
170
|
-
queryClient.invalidateQueries(['hypequery', 'getProfile']);
|
|
171
|
-
},
|
|
172
|
-
onError: (error) => {
|
|
173
|
-
console.error('Failed to update profile:', error.message);
|
|
174
|
-
},
|
|
168
|
+
// client side
|
|
169
|
+
const { useQuery } = createHooks<AnalyticsApi>({
|
|
170
|
+
baseUrl: '/api/analytics',
|
|
171
|
+
manifest,
|
|
175
172
|
});
|
|
176
|
-
|
|
177
|
-
updateProfile.mutate({ bio: 'New bio' });
|
|
178
173
|
```
|
|
179
174
|
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
Helper function to explicitly mark an object as query options (not input). Useful when your query input and TanStack Query options have overlapping property names.
|
|
185
|
-
|
|
186
|
-
**Parameters:**
|
|
187
|
-
|
|
188
|
-
- `options` - TanStack Query options object
|
|
189
|
-
|
|
190
|
-
**Returns:** The same object with an internal symbol marker
|
|
175
|
+
> Metric and dataset endpoints are POST routes whose paths differ from their map
|
|
176
|
+
> keys (e.g. `dataset:orders` → `POST /api/analytics/datasets/orders/query`). They
|
|
177
|
+
> require a `manifest` (or explicit `config`); calling them without one throws a
|
|
178
|
+
> clear error rather than hitting the wrong URL.
|
|
191
179
|
|
|
192
|
-
|
|
180
|
+
#### Generating the manifest at build time
|
|
193
181
|
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
```tsx
|
|
197
|
-
// ❌ Ambiguous - could be input or options?
|
|
198
|
-
useQuery('getConfig', { enabled: true, staleTime: 5000 });
|
|
199
|
-
|
|
200
|
-
// ✅ Explicit - this is input
|
|
201
|
-
useQuery('getConfig', { enabled: true, staleTime: 5000 });
|
|
202
|
-
|
|
203
|
-
// ✅ Explicit - these are options
|
|
204
|
-
import { queryOptions } from '@hypequery/react';
|
|
205
|
-
useQuery('getConfig', queryOptions({ enabled: true, staleTime: 5000 }));
|
|
206
|
-
```
|
|
182
|
+
The cleanest way to keep server code out of the browser bundle is to generate a
|
|
183
|
+
JSON file at build time and import that on the client.
|
|
207
184
|
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
### `HttpError`
|
|
211
|
-
|
|
212
|
-
Custom error class thrown by failed HTTP requests. Extends the standard `Error` class with additional properties.
|
|
213
|
-
|
|
214
|
-
**Properties:**
|
|
215
|
-
|
|
216
|
-
```ts
|
|
217
|
-
class HttpError extends Error {
|
|
218
|
-
readonly status: number; // HTTP status code (404, 500, etc.)
|
|
219
|
-
readonly body: unknown; // Parsed response body (JSON or text)
|
|
220
|
-
}
|
|
185
|
+
```bash
|
|
186
|
+
npx hypequery generate:manifest analytics/api.ts --output src/generated/hypequery-manifest.json
|
|
221
187
|
```
|
|
222
188
|
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
console.log(error.message); // "GET request to /api/getUser failed with status 404"
|
|
230
|
-
console.log(error.status); // 404
|
|
231
|
-
console.log(error.body); // { message: "User not found" }
|
|
232
|
-
|
|
233
|
-
// Type-safe instanceof check
|
|
234
|
-
if (error instanceof HttpError) {
|
|
235
|
-
if (error.status === 404) {
|
|
236
|
-
return <NotFound />;
|
|
237
|
-
}
|
|
189
|
+
```jsonc
|
|
190
|
+
// package.json
|
|
191
|
+
{
|
|
192
|
+
"scripts": {
|
|
193
|
+
"gen:manifest": "hypequery generate:manifest analytics/api.ts --output src/generated/hypequery-manifest.json",
|
|
194
|
+
"build": "npm run gen:manifest && <your client build>"
|
|
238
195
|
}
|
|
239
196
|
}
|
|
240
197
|
```
|
|
241
198
|
|
|
242
|
-
|
|
199
|
+
```ts
|
|
200
|
+
// client side — imports plain JSON, no server code in the bundle
|
|
201
|
+
import { createAnalyticsHooks } from '@hypequery/react';
|
|
202
|
+
import type { InferApiType } from '@hypequery/serve';
|
|
203
|
+
import type { api } from '../analytics/api.js';
|
|
204
|
+
import manifest from './generated/hypequery-manifest.json';
|
|
243
205
|
|
|
244
|
-
|
|
245
|
-
import { HttpError } from '@hypequery/react';
|
|
246
|
-
|
|
247
|
-
function UserProfile({ userId }: { userId: string }) {
|
|
248
|
-
const { data, error, isLoading } = useQuery('getUser', { id: userId });
|
|
249
|
-
|
|
250
|
-
if (isLoading) return <Spinner />;
|
|
251
|
-
|
|
252
|
-
if (error instanceof HttpError) {
|
|
253
|
-
switch (error.status) {
|
|
254
|
-
case 404:
|
|
255
|
-
return <NotFound message="User not found" />;
|
|
256
|
-
case 403:
|
|
257
|
-
return <Unauthorized />;
|
|
258
|
-
case 500:
|
|
259
|
-
return <ServerError details={error.body} />;
|
|
260
|
-
default:
|
|
261
|
-
return <Error message={error.message} />;
|
|
262
|
-
}
|
|
263
|
-
}
|
|
206
|
+
type AnalyticsApi = InferApiType<typeof api>;
|
|
264
207
|
|
|
265
|
-
|
|
266
|
-
|
|
208
|
+
const { useMetric, useDataset } = createAnalyticsHooks<AnalyticsApi>({
|
|
209
|
+
baseUrl: '/api/analytics',
|
|
210
|
+
manifest,
|
|
211
|
+
});
|
|
267
212
|
```
|
|
268
213
|
|
|
269
|
-
|
|
214
|
+
The manifest is derived entirely from your serve config (`basePath`, route keys,
|
|
215
|
+
and `semanticPaths`), so the generated file is deterministic and only changes when
|
|
216
|
+
your API shape does — commit it or regenerate it on every build.
|
|
270
217
|
|
|
271
|
-
|
|
218
|
+
> `baseUrl` supplies the origin/host; the per-endpoint path comes from the
|
|
219
|
+
> manifest (it already includes the server's `basePath`), so there's no
|
|
220
|
+
> double-prefixing. Keep `baseUrl` aligned with where the API is mounted.
|
|
272
221
|
|
|
273
|
-
###
|
|
222
|
+
### Explicit config
|
|
223
|
+
|
|
224
|
+
If a manifest is not available at runtime, pass route config explicitly. This
|
|
225
|
+
overrides any manifest entry.
|
|
274
226
|
|
|
275
227
|
```ts
|
|
276
|
-
const { useQuery } = createHooks<
|
|
277
|
-
baseUrl: '/api',
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
const token = await getAuthToken();
|
|
281
|
-
return fetch(url, {
|
|
282
|
-
...init,
|
|
283
|
-
headers: {
|
|
284
|
-
...init?.headers,
|
|
285
|
-
Authorization: `Bearer ${token}`,
|
|
286
|
-
},
|
|
287
|
-
});
|
|
228
|
+
const { useQuery } = createHooks<AnalyticsApi>({
|
|
229
|
+
baseUrl: '/api/analytics',
|
|
230
|
+
config: {
|
|
231
|
+
weeklyRevenue: { method: 'POST', path: '/weeklyRevenue' },
|
|
288
232
|
},
|
|
289
233
|
});
|
|
290
234
|
```
|
|
291
235
|
|
|
292
|
-
|
|
236
|
+
`path` may be relative to `baseUrl`, absolute within the same origin, or an absolute HTTP URL.
|
|
293
237
|
|
|
294
|
-
|
|
295
|
-
const { useQuery } = createHooks<Api>({
|
|
296
|
-
baseUrl: '/api',
|
|
297
|
-
headers: {
|
|
298
|
-
'X-API-Key': process.env.API_KEY,
|
|
299
|
-
'X-Client-Version': '1.0.0',
|
|
300
|
-
},
|
|
301
|
-
});
|
|
302
|
-
```
|
|
238
|
+
## Headers & auth
|
|
303
239
|
|
|
304
|
-
|
|
240
|
+
Pass static headers, or a function (sync or async) invoked per request — handy for
|
|
241
|
+
attaching a fresh/short-lived token.
|
|
305
242
|
|
|
306
243
|
```ts
|
|
307
|
-
const
|
|
308
|
-
baseUrl: '/api',
|
|
309
|
-
headers: () => {
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
},
|
|
244
|
+
const hooks = createHooks<AnalyticsApi>({
|
|
245
|
+
baseUrl: '/api/analytics',
|
|
246
|
+
headers: async () => ({
|
|
247
|
+
authorization: `Bearer ${await getAccessToken()}`,
|
|
248
|
+
}),
|
|
313
249
|
});
|
|
314
250
|
```
|
|
315
251
|
|
|
316
|
-
|
|
252
|
+
Provide `onUnauthorized` to refresh credentials on a `401` and retry the request once
|
|
253
|
+
with freshly resolved headers:
|
|
317
254
|
|
|
318
255
|
```ts
|
|
319
|
-
const
|
|
320
|
-
baseUrl: '/api',
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
deleteUser: { method: 'DELETE' },
|
|
256
|
+
const hooks = createHooks<AnalyticsApi>({
|
|
257
|
+
baseUrl: '/api/analytics',
|
|
258
|
+
headers: () => ({ authorization: `Bearer ${tokenStore.access}` }),
|
|
259
|
+
onUnauthorized: async () => {
|
|
260
|
+
await tokenStore.refresh(); // throw to abort; resolve to retry once
|
|
325
261
|
},
|
|
326
262
|
});
|
|
327
263
|
```
|
|
328
264
|
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
If your API definition includes HTTP method information, pass it to auto-configure:
|
|
265
|
+
## Query Options
|
|
332
266
|
|
|
333
|
-
|
|
334
|
-
import { api } from '@/analytics/queries';
|
|
267
|
+
TanStack Query options can be passed as the final argument.
|
|
335
268
|
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
}
|
|
269
|
+
```tsx
|
|
270
|
+
const result = useQuery(
|
|
271
|
+
'weeklyRevenue',
|
|
272
|
+
{ startDate: '2026-01-01' },
|
|
273
|
+
{
|
|
274
|
+
staleTime: 60_000,
|
|
275
|
+
refetchOnWindowFocus: false,
|
|
276
|
+
},
|
|
277
|
+
);
|
|
340
278
|
```
|
|
341
279
|
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
## TypeScript
|
|
280
|
+
For no-input queries, use `queryOptions()` when an options object might look like input.
|
|
345
281
|
|
|
346
|
-
|
|
282
|
+
```tsx
|
|
283
|
+
import { queryOptions } from '@hypequery/react';
|
|
347
284
|
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
output: { name: string; email: string };
|
|
353
|
-
};
|
|
354
|
-
weeklyRevenue: {
|
|
355
|
-
input: { startDate: string };
|
|
356
|
-
output: { total: number };
|
|
357
|
-
};
|
|
358
|
-
}
|
|
285
|
+
const result = useQuery('health', queryOptions({
|
|
286
|
+
staleTime: 30_000,
|
|
287
|
+
}));
|
|
288
|
+
```
|
|
359
289
|
|
|
360
|
-
|
|
290
|
+
## Mutations
|
|
361
291
|
|
|
362
|
-
|
|
363
|
-
const { data } = useQuery('getUser', { id: '123' });
|
|
364
|
-
// ^? { name: string; email: string } | undefined
|
|
292
|
+
`useMutation(name)` uses the same endpoint typing and sends requests as `POST` by default unless route config overrides it.
|
|
365
293
|
|
|
366
|
-
|
|
367
|
-
const
|
|
294
|
+
```tsx
|
|
295
|
+
const refresh = useMutation('refreshReport');
|
|
368
296
|
|
|
369
|
-
|
|
370
|
-
const { data } = useQuery('getUser', { userId: '123' });
|
|
297
|
+
refresh.mutate({ reportId: 'revenue' });
|
|
371
298
|
```
|
|
372
299
|
|
|
373
|
-
|
|
300
|
+
## Notes
|
|
301
|
+
|
|
302
|
+
- This package does not define datasets. Use `@hypequery/datasets` for semantic definitions.
|
|
303
|
+
- This package does not generate SQL. It calls HTTP routes exposed by `@hypequery/serve`.
|
|
304
|
+
- Dataset relationship JOIN execution is not implied by the React hooks; hooks consume whatever endpoint behavior the server exposes.
|
|
374
305
|
|
|
375
306
|
## License
|
|
376
307
|
|
|
377
|
-
|
|
308
|
+
Apache-2.0.
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { UseQueryOptions as TanstackUseQueryOptions, UseQueryResult, UseInfiniteQueryResult, InfiniteData } from '@tanstack/react-query';
|
|
2
|
+
import { type CreateHooksConfig } from './createHooks.js';
|
|
3
|
+
import { HttpError } from './errors.js';
|
|
4
|
+
import type { ExtractNames, QueryInput, QueryOutput, QueryOutputForInput } from './types.js';
|
|
5
|
+
type DatasetKey<Name extends string> = `dataset:${Name}`;
|
|
6
|
+
type DatasetNamesFromApi<Api> = ExtractNames<Api> extends infer Key ? Key extends `dataset:${infer Name}` ? Name : never : never;
|
|
7
|
+
type QueryKey<Name extends string, Input> = Input extends never ? ['hypequery', Name] : ['hypequery', Name, Input];
|
|
8
|
+
type QueryOptions<Api, Key extends ExtractNames<Api>> = Omit<TanstackUseQueryOptions<QueryOutput<Api, Key>, HttpError, QueryOutput<Api, Key>, QueryKey<Key, QueryInput<Api, Key>>>, 'queryKey' | 'queryFn'>;
|
|
9
|
+
export interface CreateAnalyticsHooksConfig<Api extends Record<string, {
|
|
10
|
+
input: any;
|
|
11
|
+
output: any;
|
|
12
|
+
}>, TMetrics extends readonly Exclude<ExtractNames<Api>, `dataset:${string}`>[] = readonly Exclude<ExtractNames<Api>, `dataset:${string}`>[]> extends CreateHooksConfig<Api> {
|
|
13
|
+
metrics?: TMetrics;
|
|
14
|
+
}
|
|
15
|
+
export declare function createAnalyticsHooks<Api extends Record<string, {
|
|
16
|
+
input: any;
|
|
17
|
+
output: any;
|
|
18
|
+
}>, TMetrics extends readonly Exclude<ExtractNames<Api>, `dataset:${string}`>[] = readonly Exclude<ExtractNames<Api>, `dataset:${string}`>[]>(config: CreateAnalyticsHooksConfig<Api, TMetrics>): {
|
|
19
|
+
readonly useMetric: {
|
|
20
|
+
<Name extends TMetrics extends readonly (infer U)[] ? U extends string ? U : never : Exclude<ExtractNames<Api>, `dataset:${string}`>>(...args: QueryInput<Api, Name> extends never ? [name: Name, options?: QueryOptions<Api, Name>] : never): UseQueryResult<QueryOutput<Api, Name>, HttpError>;
|
|
21
|
+
<Name extends TMetrics extends readonly (infer U)[] ? U extends string ? U : never : Exclude<ExtractNames<Api>, `dataset:${string}`>, const TInput extends QueryInput<Api, Name>>(name: Name, input: TInput, options?: QueryOptions<Api, Name>): UseQueryResult<QueryOutputForInput<Api, Name, TInput>, HttpError>;
|
|
22
|
+
};
|
|
23
|
+
readonly useDataset: {
|
|
24
|
+
<Name extends DatasetNamesFromApi<Api>>(...args: QueryInput<Api, Extract<ExtractNames<Api>, DatasetKey<Name>>> extends never ? [name: Name, options?: QueryOptions<Api, Extract<ExtractNames<Api>, DatasetKey<Name>>>] : never): UseQueryResult<QueryOutput<Api, Extract<ExtractNames<Api>, DatasetKey<Name>>>, HttpError>;
|
|
25
|
+
<Name extends DatasetNamesFromApi<Api>, const TInput extends QueryInput<Api, Extract<ExtractNames<Api>, DatasetKey<Name>>>>(name: Name, input: TInput, options?: QueryOptions<Api, Extract<ExtractNames<Api>, DatasetKey<Name>>>): UseQueryResult<QueryOutputForInput<Api, Extract<ExtractNames<Api>, DatasetKey<Name>>, TInput>, HttpError>;
|
|
26
|
+
};
|
|
27
|
+
readonly useInfiniteMetric: <Name extends TMetrics extends readonly (infer U)[] ? U extends string ? U : never : Exclude<ExtractNames<Api>, `dataset:${string}`>>(name: Name, input: QueryInput<Api, Name>, options?: Parameters<(<Name_1 extends ExtractNames<Api>>(name: Name_1, input: QueryInput<Api, Name_1>, options?: Omit<import("@tanstack/react-query").UseInfiniteQueryOptions<QueryOutput<Api, Name_1>, HttpError, InfiniteData<QueryOutput<Api, Name_1>, number>, QueryInput<Api, Name_1> extends never ? ["hypequery", Name_1] : ["hypequery", Name_1, QueryInput<Api, Name_1>], number>, "queryKey" | "queryFn" | "getNextPageParam" | "initialPageParam"> | undefined) => UseInfiniteQueryResult<InfiniteData<QueryOutput<Api, Name_1>, number>, HttpError>)>[2]) => UseInfiniteQueryResult<InfiniteData<QueryOutput<Api, Name>, number>, HttpError>;
|
|
28
|
+
readonly useInfiniteDataset: <Name extends DatasetNamesFromApi<Api>>(name: Name, input: QueryInput<Api, Extract<ExtractNames<Api>, DatasetKey<Name>>>, options?: Parameters<(<Name_1 extends ExtractNames<Api>>(name: Name_1, input: QueryInput<Api, Name_1>, options?: Omit<import("@tanstack/react-query").UseInfiniteQueryOptions<QueryOutput<Api, Name_1>, HttpError, InfiniteData<QueryOutput<Api, Name_1>, number>, QueryInput<Api, Name_1> extends never ? ["hypequery", Name_1] : ["hypequery", Name_1, QueryInput<Api, Name_1>], number>, "queryKey" | "queryFn" | "getNextPageParam" | "initialPageParam"> | undefined) => UseInfiniteQueryResult<InfiniteData<QueryOutput<Api, Name_1>, number>, HttpError>)>[2]) => UseInfiniteQueryResult<InfiniteData<QueryOutput<Api, Extract<ExtractNames<Api>, DatasetKey<Name>>>, number>, HttpError>;
|
|
29
|
+
readonly useQuery: <Name extends ExtractNames<Api>>(...args: QueryInput<Api, Name> extends never ? [name: Name, options?: Omit<TanstackUseQueryOptions<QueryOutput<Api, Name>, HttpError, QueryOutput<Api, Name>, QueryInput<Api, Name> extends never ? ["hypequery", Name] : ["hypequery", Name, QueryInput<Api, Name>]>, "queryKey" | "queryFn"> | undefined] : [name: Name, input: QueryInput<Api, Name>, options?: Omit<TanstackUseQueryOptions<QueryOutput<Api, Name>, HttpError, QueryOutput<Api, Name>, QueryInput<Api, Name> extends never ? ["hypequery", Name] : ["hypequery", Name, QueryInput<Api, Name>]>, "queryKey" | "queryFn"> | undefined]) => UseQueryResult<QueryOutput<Api, Name>, HttpError>;
|
|
30
|
+
readonly useMutation: <Name extends ExtractNames<Api>>(name: Name, options?: Omit<import("@tanstack/react-query").UseMutationOptions<QueryOutput<Api, Name>, HttpError, QueryInput<Api, Name>, unknown>, "mutationFn"> | undefined) => import("@tanstack/react-query").UseMutationResult<QueryOutput<Api, Name>, HttpError, QueryInput<Api, Name>>;
|
|
31
|
+
readonly useInfiniteQuery: <Name extends ExtractNames<Api>>(name: Name, input: QueryInput<Api, Name>, options?: Omit<import("@tanstack/react-query").UseInfiniteQueryOptions<QueryOutput<Api, Name>, HttpError, InfiniteData<QueryOutput<Api, Name>, number>, QueryInput<Api, Name> extends never ? ["hypequery", Name] : ["hypequery", Name, QueryInput<Api, Name>], number>, "queryKey" | "queryFn" | "getNextPageParam" | "initialPageParam"> | undefined) => UseInfiniteQueryResult<InfiniteData<QueryOutput<Api, Name>, number>, HttpError>;
|
|
32
|
+
};
|
|
33
|
+
export {};
|
|
34
|
+
//# sourceMappingURL=analyticsHooks.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"analyticsHooks.d.ts","sourceRoot":"","sources":["../src/analyticsHooks.tsx"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { createHooks } from './createHooks.js';
|
|
2
|
+
export function createAnalyticsHooks(config) {
|
|
3
|
+
const hooks = createHooks(config);
|
|
4
|
+
function useMetric(...args) {
|
|
5
|
+
const [name, ...rest] = args;
|
|
6
|
+
return hooks.useQuery(name, ...rest);
|
|
7
|
+
}
|
|
8
|
+
function useDataset(...args) {
|
|
9
|
+
const [name, ...rest] = args;
|
|
10
|
+
const key = `dataset:${String(name)}`;
|
|
11
|
+
return hooks.useQuery(key, ...rest);
|
|
12
|
+
}
|
|
13
|
+
function useInfiniteMetric(name, input, options) {
|
|
14
|
+
return hooks.useInfiniteQuery(name, input, options);
|
|
15
|
+
}
|
|
16
|
+
function useInfiniteDataset(name, input, options) {
|
|
17
|
+
const key = `dataset:${String(name)}`;
|
|
18
|
+
return hooks.useInfiniteQuery(key, input, options);
|
|
19
|
+
}
|
|
20
|
+
return {
|
|
21
|
+
...hooks,
|
|
22
|
+
useMetric,
|
|
23
|
+
useDataset,
|
|
24
|
+
useInfiniteMetric,
|
|
25
|
+
useInfiniteDataset,
|
|
26
|
+
};
|
|
27
|
+
}
|