@hypequery/react 0.5.0 → 0.6.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 +75 -254
- package/dist/analyticsHooks.d.ts +4 -4
- package/dist/analyticsHooks.d.ts.map +1 -1
- package/dist/client.d.ts +41 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +128 -0
- package/dist/createHooks.d.ts +5 -49
- package/dist/createHooks.d.ts.map +1 -1
- package/dist/createHooks.js +19 -134
- package/dist/index.d.ts +5 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/provider.d.ts +13 -0
- package/dist/provider.d.ts.map +1 -0
- package/dist/provider.js +18 -0
- package/dist/provider.test.d.ts +2 -0
- package/dist/provider.test.d.ts.map +1 -0
- package/dist/provider.test.js +53 -0
- package/package.json +25 -4
package/README.md
CHANGED
|
@@ -1,307 +1,128 @@
|
|
|
1
1
|
# @hypequery/react
|
|
2
2
|
|
|
3
|
-
React hooks for
|
|
3
|
+
Typed React hooks for ClickHouse analytics, powered by TanStack Query.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
`@hypequery/react` carries `@hypequery/serve` contracts into React and Next.js. Query names, inputs, metrics, dataset fields, and response rows stay inferred from the server, so frontend analytics do not drift into a second set of hand-written types.
|
|
6
|
+
|
|
7
|
+
Use it for product dashboards, embedded analytics, customer reporting, and multi-tenant SaaS metrics.
|
|
6
8
|
|
|
7
9
|
## Install
|
|
8
10
|
|
|
9
11
|
```bash
|
|
10
12
|
npm install @hypequery/react @tanstack/react-query
|
|
11
|
-
# or
|
|
12
|
-
pnpm add @hypequery/react @tanstack/react-query
|
|
13
13
|
```
|
|
14
14
|
|
|
15
|
-
##
|
|
16
|
-
|
|
17
|
-
Given a serve API:
|
|
18
|
-
|
|
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',
|
|
28
|
-
});
|
|
29
|
-
|
|
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
|
-
});
|
|
40
|
-
|
|
41
|
-
export const api = serve({
|
|
42
|
-
queries: { weeklyRevenue },
|
|
43
|
-
});
|
|
15
|
+
## Create hooks from your API
|
|
44
16
|
|
|
45
|
-
|
|
17
|
+
Generate a small route manifest during the build:
|
|
46
18
|
|
|
47
|
-
|
|
19
|
+
```bash
|
|
20
|
+
npx hypequery generate:manifest analytics/api.ts \
|
|
21
|
+
--output src/generated/hypequery-manifest.json
|
|
48
22
|
```
|
|
49
23
|
|
|
50
|
-
|
|
24
|
+
## Configure Once With `HypequeryProvider`
|
|
25
|
+
|
|
26
|
+
For hosted APIs and generated clients, create hooks without transport
|
|
27
|
+
configuration. A provider supplies one client to the application and includes a
|
|
28
|
+
TanStack Query provider by default.
|
|
51
29
|
|
|
52
30
|
```tsx
|
|
53
|
-
// client/
|
|
31
|
+
// client/hypequery.ts
|
|
54
32
|
import { createHooks } from '@hypequery/react';
|
|
55
33
|
import type { AnalyticsApi } from '../server/api.js';
|
|
56
34
|
|
|
57
|
-
export const { useQuery, useMutation } = createHooks<AnalyticsApi>(
|
|
58
|
-
baseUrl: '/api/analytics',
|
|
59
|
-
api: {} as AnalyticsApi,
|
|
60
|
-
});
|
|
35
|
+
export const { useQuery, useMutation } = createHooks<AnalyticsApi>();
|
|
61
36
|
```
|
|
62
37
|
|
|
63
|
-
Use them inside a component:
|
|
64
|
-
|
|
65
38
|
```tsx
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
39
|
+
// app/providers.tsx
|
|
40
|
+
'use client';
|
|
41
|
+
|
|
42
|
+
import {
|
|
43
|
+
createHypequeryClient,
|
|
44
|
+
HypequeryProvider,
|
|
45
|
+
} from '@hypequery/react';
|
|
46
|
+
import type { ReactNode } from 'react';
|
|
47
|
+
import type { AnalyticsApi } from '../server/api.js';
|
|
48
|
+
import manifest from '../generated/hypequery-manifest.json';
|
|
70
49
|
|
|
71
|
-
|
|
72
|
-
|
|
50
|
+
const client = createHypequeryClient<AnalyticsApi>({
|
|
51
|
+
baseUrl: 'https://acme.hypequery.cloud/v1/analytics/production',
|
|
52
|
+
manifest,
|
|
53
|
+
// Resolve a short-lived browser token. Never expose a server API key here.
|
|
54
|
+
token: async () => tokenStore.get(),
|
|
55
|
+
onUnauthorized: () => tokenStore.refresh(),
|
|
56
|
+
});
|
|
73
57
|
|
|
74
|
-
|
|
58
|
+
export function Providers({ children }: { children: ReactNode }) {
|
|
59
|
+
return <HypequeryProvider client={client}>{children}</HypequeryProvider>;
|
|
75
60
|
}
|
|
76
61
|
```
|
|
77
62
|
|
|
78
|
-
|
|
63
|
+
Applications that already own a TanStack Query client can pass it through the
|
|
64
|
+
`queryClient` prop so Hypequery shares the existing cache.
|
|
65
|
+
|
|
66
|
+
Each client instance receives an isolated cache scope. Create a new client when
|
|
67
|
+
the deployment, tenant, or authenticated user changes. A deterministic
|
|
68
|
+
`cacheKey` may be supplied when the same security context must retain cache data
|
|
69
|
+
across equivalent client instances.
|
|
70
|
+
|
|
71
|
+
## Create Dataset And Metric Hooks
|
|
79
72
|
|
|
80
73
|
`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, ...)`.
|
|
81
74
|
|
|
82
75
|
```tsx
|
|
83
76
|
import { createAnalyticsHooks } from '@hypequery/react';
|
|
84
|
-
import type {
|
|
85
|
-
import
|
|
86
|
-
|
|
87
|
-
import manifest from './hypequery-manifest.json';
|
|
88
|
-
|
|
89
|
-
type AnalyticsApi = InferApiType<typeof api>;
|
|
90
|
-
|
|
91
|
-
export const { useMetric, useDataset } = createAnalyticsHooks<AnalyticsApi>({
|
|
92
|
-
baseUrl: '/api/analytics',
|
|
93
|
-
manifest,
|
|
94
|
-
metrics: ['revenue', 'averageOrderValue'] as const,
|
|
95
|
-
});
|
|
96
|
-
|
|
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
|
-
});
|
|
77
|
+
import type { AnalyticsApi } from '../server/api.js';
|
|
78
|
+
import manifest from '../generated/hypequery-manifest.json';
|
|
104
79
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
80
|
+
export const { useMetric, useDataset } =
|
|
81
|
+
createAnalyticsHooks<AnalyticsApi>({
|
|
82
|
+
baseUrl: '/api/analytics',
|
|
83
|
+
manifest,
|
|
84
|
+
metrics: ['revenue'] as const,
|
|
108
85
|
});
|
|
109
|
-
|
|
110
|
-
return (
|
|
111
|
-
<pre>
|
|
112
|
-
{JSON.stringify({ revenue: revenue.data, orders: orders.data }, null, 2)}
|
|
113
|
-
</pre>
|
|
114
|
-
);
|
|
115
|
-
}
|
|
116
86
|
```
|
|
117
87
|
|
|
118
|
-
##
|
|
119
|
-
|
|
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.
|
|
88
|
+
## Build the dashboard
|
|
124
89
|
|
|
125
90
|
```tsx
|
|
126
|
-
function
|
|
127
|
-
const
|
|
128
|
-
dimensions: ['
|
|
129
|
-
|
|
130
|
-
limit:
|
|
91
|
+
function RevenueByRegion() {
|
|
92
|
+
const revenue = useMetric('revenue', {
|
|
93
|
+
dimensions: ['region'],
|
|
94
|
+
orderBy: [{ field: 'revenue', direction: 'desc' }],
|
|
95
|
+
limit: 10,
|
|
131
96
|
});
|
|
132
97
|
|
|
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
|
-
}
|
|
147
|
-
```
|
|
148
|
-
|
|
149
|
-
`input.limit` is the page size; `input.offset`, if provided, is the starting offset.
|
|
150
|
-
|
|
151
|
-
## Route Configuration
|
|
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`.
|
|
156
|
-
|
|
157
|
-
### Route manifest (recommended)
|
|
158
|
-
|
|
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.
|
|
163
|
-
|
|
164
|
-
```ts
|
|
165
|
-
// server side (server-only module)
|
|
166
|
-
export const manifest = api.manifest();
|
|
167
|
-
|
|
168
|
-
// client side
|
|
169
|
-
const { useQuery } = createHooks<AnalyticsApi>({
|
|
170
|
-
baseUrl: '/api/analytics',
|
|
171
|
-
manifest,
|
|
172
|
-
});
|
|
173
|
-
```
|
|
174
|
-
|
|
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.
|
|
179
|
-
|
|
180
|
-
#### Generating the manifest at build time
|
|
181
|
-
|
|
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.
|
|
184
|
-
|
|
185
|
-
```bash
|
|
186
|
-
npx hypequery generate:manifest analytics/api.ts --output src/generated/hypequery-manifest.json
|
|
187
|
-
```
|
|
98
|
+
if (revenue.isLoading) return <p>Loading revenue…</p>;
|
|
99
|
+
if (revenue.error) return <p>{revenue.error.message}</p>;
|
|
188
100
|
|
|
189
|
-
|
|
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>"
|
|
195
|
-
}
|
|
101
|
+
return <RevenueChart rows={revenue.data?.data ?? []} />;
|
|
196
102
|
}
|
|
197
103
|
```
|
|
198
104
|
|
|
199
|
-
|
|
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';
|
|
205
|
-
|
|
206
|
-
type AnalyticsApi = InferApiType<typeof api>;
|
|
207
|
-
|
|
208
|
-
const { useMetric, useDataset } = createAnalyticsHooks<AnalyticsApi>({
|
|
209
|
-
baseUrl: '/api/analytics',
|
|
210
|
-
manifest,
|
|
211
|
-
});
|
|
212
|
-
```
|
|
213
|
-
|
|
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.
|
|
217
|
-
|
|
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.
|
|
221
|
-
|
|
222
|
-
### Explicit config
|
|
223
|
-
|
|
224
|
-
If a manifest is not available at runtime, pass route config explicitly. This
|
|
225
|
-
overrides any manifest entry.
|
|
226
|
-
|
|
227
|
-
```ts
|
|
228
|
-
const { useQuery } = createHooks<AnalyticsApi>({
|
|
229
|
-
baseUrl: '/api/analytics',
|
|
230
|
-
config: {
|
|
231
|
-
weeklyRevenue: { method: 'POST', path: '/weeklyRevenue' },
|
|
232
|
-
},
|
|
233
|
-
});
|
|
234
|
-
```
|
|
105
|
+
Change the server contract and TypeScript points to the components that need updating.
|
|
235
106
|
|
|
236
|
-
|
|
107
|
+
## Included
|
|
237
108
|
|
|
238
|
-
|
|
109
|
+
- named query hooks and mutations;
|
|
110
|
+
- metric and dataset hooks with selected-field inference;
|
|
111
|
+
- infinite queries with semantic pagination metadata;
|
|
112
|
+
- TanStack Query caching, retries, and refresh behavior;
|
|
113
|
+
- static route manifests that keep server code out of browser bundles;
|
|
114
|
+
- per-request auth headers and one-time `401` refresh.
|
|
239
115
|
|
|
240
|
-
|
|
241
|
-
attaching a fresh/short-lived token.
|
|
116
|
+
## Why it matters
|
|
242
117
|
|
|
243
|
-
|
|
244
|
-
const hooks = createHooks<AnalyticsApi>({
|
|
245
|
-
baseUrl: '/api/analytics',
|
|
246
|
-
headers: async () => ({
|
|
247
|
-
authorization: `Bearer ${await getAccessToken()}`,
|
|
248
|
-
}),
|
|
249
|
-
});
|
|
250
|
-
```
|
|
251
|
-
|
|
252
|
-
Provide `onUnauthorized` to refresh credentials on a `401` and retry the request once
|
|
253
|
-
with freshly resolved headers:
|
|
254
|
-
|
|
255
|
-
```ts
|
|
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
|
|
261
|
-
},
|
|
262
|
-
});
|
|
263
|
-
```
|
|
264
|
-
|
|
265
|
-
## Query Options
|
|
266
|
-
|
|
267
|
-
TanStack Query options can be passed as the final argument.
|
|
268
|
-
|
|
269
|
-
```tsx
|
|
270
|
-
const result = useQuery(
|
|
271
|
-
'weeklyRevenue',
|
|
272
|
-
{ startDate: '2026-01-01' },
|
|
273
|
-
{
|
|
274
|
-
staleTime: 60_000,
|
|
275
|
-
refetchOnWindowFocus: false,
|
|
276
|
-
},
|
|
277
|
-
);
|
|
278
|
-
```
|
|
279
|
-
|
|
280
|
-
For no-input queries, use `queryOptions()` when an options object might look like input.
|
|
281
|
-
|
|
282
|
-
```tsx
|
|
283
|
-
import { queryOptions } from '@hypequery/react';
|
|
284
|
-
|
|
285
|
-
const result = useQuery('health', queryOptions({
|
|
286
|
-
staleTime: 30_000,
|
|
287
|
-
}));
|
|
288
|
-
```
|
|
289
|
-
|
|
290
|
-
## Mutations
|
|
291
|
-
|
|
292
|
-
`useMutation(name)` uses the same endpoint typing and sends requests as `POST` by default unless route config overrides it.
|
|
293
|
-
|
|
294
|
-
```tsx
|
|
295
|
-
const refresh = useMutation('refreshReport');
|
|
296
|
-
|
|
297
|
-
refresh.mutate({ reportId: 'revenue' });
|
|
298
|
-
```
|
|
118
|
+
The ClickHouse query, semantic metric, HTTP schema, and React component share one contract. That means fewer casts, fewer mismatched route types, and no frontend rewrite when a dashboard graduates from a prototype into a governed multi-tenant product surface.
|
|
299
119
|
|
|
300
|
-
##
|
|
120
|
+
## Learn more
|
|
301
121
|
|
|
302
|
-
-
|
|
303
|
-
-
|
|
304
|
-
-
|
|
122
|
+
- [React quick start](https://hypequery.com/docs/react/getting-started)
|
|
123
|
+
- [Using queries](https://hypequery.com/docs/react/using-queries)
|
|
124
|
+
- [Advanced patterns](https://hypequery.com/docs/react/advanced-patterns)
|
|
125
|
+
- [Current capabilities](https://hypequery.com/docs/capabilities)
|
|
305
126
|
|
|
306
127
|
## License
|
|
307
128
|
|
package/dist/analyticsHooks.d.ts
CHANGED
|
@@ -4,7 +4,7 @@ import { HttpError } from './errors.js';
|
|
|
4
4
|
import type { ExtractNames, QueryInput, QueryOutput, QueryOutputForInput } from './types.js';
|
|
5
5
|
type DatasetKey<Name extends string> = `dataset:${Name}`;
|
|
6
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];
|
|
7
|
+
type QueryKey<Name extends string, Input> = Input extends never ? ['hypequery', string, Name] : ['hypequery', string, Name, Input];
|
|
8
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
9
|
/** Infinite-query options parameterized by the projected page type. */
|
|
10
10
|
type InfiniteQueryOptions<Api, Key extends ExtractNames<Api>, TPage> = Omit<TanstackUseInfiniteQueryOptions<TPage, HttpError, InfiniteData<TPage, number>, QueryKey<Key, QueryInput<Api, Key>>, number>, 'queryKey' | 'queryFn' | 'getNextPageParam' | 'initialPageParam'>;
|
|
@@ -17,7 +17,7 @@ export interface CreateAnalyticsHooksConfig<Api extends Record<string, {
|
|
|
17
17
|
export declare function createAnalyticsHooks<Api extends Record<string, {
|
|
18
18
|
input: any;
|
|
19
19
|
output: any;
|
|
20
|
-
}>, TMetrics extends readonly Exclude<ExtractNames<Api>, `dataset:${string}`>[] = readonly Exclude<ExtractNames<Api>, `dataset:${string}`>[]>(config
|
|
20
|
+
}>, TMetrics extends readonly Exclude<ExtractNames<Api>, `dataset:${string}`>[] = readonly Exclude<ExtractNames<Api>, `dataset:${string}`>[]>(config?: CreateAnalyticsHooksConfig<Api, TMetrics>): {
|
|
21
21
|
readonly useMetric: {
|
|
22
22
|
<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>;
|
|
23
23
|
<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>;
|
|
@@ -28,9 +28,9 @@ export declare function createAnalyticsHooks<Api extends Record<string, {
|
|
|
28
28
|
};
|
|
29
29
|
readonly useInfiniteMetric: <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?: InfiniteQueryOptions<Api, Name, QueryOutputForInput<Api, Name, TInput>>) => UseInfiniteQueryResult<InfiniteData<QueryOutputForInput<Api, Name, TInput>, number>, HttpError>;
|
|
30
30
|
readonly useInfiniteDataset: <Name extends DatasetNamesFromApi<Api>, const TInput extends QueryInput<Api, Extract<ExtractNames<Api>, DatasetKey<Name>>>>(name: Name, input: TInput, options?: InfiniteQueryOptions<Api, Extract<ExtractNames<Api>, DatasetKey<Name>>, QueryOutputForInput<Api, Extract<ExtractNames<Api>, DatasetKey<Name>>, TInput>>) => UseInfiniteQueryResult<InfiniteData<QueryOutputForInput<Api, Extract<ExtractNames<Api>, DatasetKey<Name>>, TInput>, number>, HttpError>;
|
|
31
|
-
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>;
|
|
31
|
+
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", string, Name] : ["hypequery", string, 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", string, Name] : ["hypequery", string, Name, QueryInput<Api, Name>]>, "queryKey" | "queryFn"> | undefined]) => UseQueryResult<QueryOutput<Api, Name>, HttpError>;
|
|
32
32
|
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>>;
|
|
33
|
-
readonly useInfiniteQuery: <Name extends ExtractNames<Api>>(name: Name, input: QueryInput<Api, Name>, options?: Omit<TanstackUseInfiniteQueryOptions<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>;
|
|
33
|
+
readonly useInfiniteQuery: <Name extends ExtractNames<Api>>(name: Name, input: QueryInput<Api, Name>, options?: Omit<TanstackUseInfiniteQueryOptions<QueryOutput<Api, Name>, HttpError, InfiniteData<QueryOutput<Api, Name>, number>, QueryInput<Api, Name> extends never ? ["hypequery", string, Name] : ["hypequery", string, Name, QueryInput<Api, Name>], number>, "queryKey" | "queryFn" | "getNextPageParam" | "initialPageParam"> | undefined) => UseInfiniteQueryResult<InfiniteData<QueryOutput<Api, Name>, number>, HttpError>;
|
|
34
34
|
};
|
|
35
35
|
export {};
|
|
36
36
|
//# sourceMappingURL=analyticsHooks.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"analyticsHooks.d.ts","sourceRoot":"","sources":["../src/analyticsHooks.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,eAAe,IAAI,uBAAuB,EAC1C,cAAc,EACd,uBAAuB,IAAI,+BAA+B,EAC1D,sBAAsB,EACtB,YAAY,EACb,MAAM,uBAAuB,CAAC;AAC/B,OAAO,EAAe,KAAK,iBAAiB,EAAE,MAAM,kBAAkB,CAAC;AACvE,OAAO,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AACxC,OAAO,KAAK,EAAE,YAAY,EAAE,UAAU,EAAE,WAAW,EAAE,mBAAmB,EAAE,MAAM,YAAY,CAAC;AAE7F,KAAK,UAAU,CAAC,IAAI,SAAS,MAAM,IAAI,WAAW,IAAI,EAAE,CAAC;AACzD,KAAK,mBAAmB,CAAC,GAAG,IAC1B,YAAY,CAAC,GAAG,CAAC,SAAS,MAAM,GAAG,GAC/B,GAAG,SAAS,WAAW,MAAM,IAAI,EAAE,GACjC,IAAI,GACJ,KAAK,GACP,KAAK,CAAC;AAEZ,KAAK,QAAQ,CAAC,IAAI,SAAS,MAAM,EAAE,KAAK,IAAI,KAAK,SAAS,KAAK,GAC3D,CAAC,WAAW,EAAE,IAAI,CAAC,
|
|
1
|
+
{"version":3,"file":"analyticsHooks.d.ts","sourceRoot":"","sources":["../src/analyticsHooks.tsx"],"names":[],"mappings":"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"}
|
package/dist/client.d.ts
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
export type ApiContract = Record<string, {
|
|
2
|
+
input: any;
|
|
3
|
+
output: any;
|
|
4
|
+
}>;
|
|
5
|
+
export interface QueryMethodConfig {
|
|
6
|
+
method?: string;
|
|
7
|
+
path?: string;
|
|
8
|
+
}
|
|
9
|
+
export interface RouteManifestEntry {
|
|
10
|
+
method?: string;
|
|
11
|
+
path?: string;
|
|
12
|
+
}
|
|
13
|
+
export type RouteManifest = Record<string, RouteManifestEntry>;
|
|
14
|
+
type HeaderMap = Record<string, string | undefined>;
|
|
15
|
+
export type HeadersInput = HeaderMap | (() => HeaderMap | Promise<HeaderMap>);
|
|
16
|
+
export type TokenInput = string | (() => string | undefined | Promise<string | undefined>);
|
|
17
|
+
export interface HypequeryClientConfig<Api extends ApiContract = ApiContract> {
|
|
18
|
+
baseUrl: string;
|
|
19
|
+
/**
|
|
20
|
+
* Stable cache scope for this deployment and authentication context. Omit it
|
|
21
|
+
* to create an isolated scope for this client instance.
|
|
22
|
+
*/
|
|
23
|
+
cacheKey?: string;
|
|
24
|
+
fetchFn?: typeof fetch;
|
|
25
|
+
headers?: HeadersInput;
|
|
26
|
+
/** A bearer token or per-request token resolver. Never put a server API key in browser code. */
|
|
27
|
+
token?: TokenInput;
|
|
28
|
+
config?: Record<string, QueryMethodConfig>;
|
|
29
|
+
manifest?: RouteManifest;
|
|
30
|
+
/** Refresh authentication state after a 401. The request is retried once. */
|
|
31
|
+
onUnauthorized?: () => void | Promise<void>;
|
|
32
|
+
api?: Api;
|
|
33
|
+
}
|
|
34
|
+
export interface HypequeryClient<Api extends ApiContract = ApiContract> {
|
|
35
|
+
readonly config: Readonly<HypequeryClientConfig<Api>>;
|
|
36
|
+
readonly cacheKey: string;
|
|
37
|
+
request(name: string, input?: unknown, defaultMethod?: string, extraHeaders?: Record<string, string>): Promise<unknown>;
|
|
38
|
+
}
|
|
39
|
+
export declare function createHypequeryClient<Api extends ApiContract = ApiContract>(config: HypequeryClientConfig<Api>): HypequeryClient<Api>;
|
|
40
|
+
export {};
|
|
41
|
+
//# sourceMappingURL=client.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"AAEA,MAAM,MAAM,WAAW,GAAG,MAAM,CAAC,MAAM,EAAE;IAAE,KAAK,EAAE,GAAG,CAAC;IAAC,MAAM,EAAE,GAAG,CAAA;CAAE,CAAC,CAAC;AAEtE,MAAM,WAAW,iBAAiB;IAChC,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,kBAAkB;IACjC,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAED,MAAM,MAAM,aAAa,GAAG,MAAM,CAAC,MAAM,EAAE,kBAAkB,CAAC,CAAC;AAE/D,KAAK,SAAS,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,CAAC;AACpD,MAAM,MAAM,YAAY,GAAG,SAAS,GAAG,CAAC,MAAM,SAAS,GAAG,OAAO,CAAC,SAAS,CAAC,CAAC,CAAC;AAC9E,MAAM,MAAM,UAAU,GAAG,MAAM,GAAG,CAAC,MAAM,MAAM,GAAG,SAAS,GAAG,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAAC,CAAC;AAE3F,MAAM,WAAW,qBAAqB,CAAC,GAAG,SAAS,WAAW,GAAG,WAAW;IAC1E,OAAO,EAAE,MAAM,CAAC;IAChB;;;OAGG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,OAAO,CAAC,EAAE,OAAO,KAAK,CAAC;IACvB,OAAO,CAAC,EAAE,YAAY,CAAC;IACvB,gGAAgG;IAChG,KAAK,CAAC,EAAE,UAAU,CAAC;IACnB,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,iBAAiB,CAAC,CAAC;IAC3C,QAAQ,CAAC,EAAE,aAAa,CAAC;IACzB,6EAA6E;IAC7E,cAAc,CAAC,EAAE,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC5C,GAAG,CAAC,EAAE,GAAG,CAAC;CACX;AAqED,MAAM,WAAW,eAAe,CAAC,GAAG,SAAS,WAAW,GAAG,WAAW;IACpE,QAAQ,CAAC,MAAM,EAAE,QAAQ,CAAC,qBAAqB,CAAC,GAAG,CAAC,CAAC,CAAC;IACtD,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,OAAO,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,OAAO,EAAE,aAAa,CAAC,EAAE,MAAM,EAAE,YAAY,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;CACzH;AAID,wBAAgB,qBAAqB,CAAC,GAAG,SAAS,WAAW,GAAG,WAAW,EACzE,MAAM,EAAE,qBAAqB,CAAC,GAAG,CAAC,GACjC,eAAe,CAAC,GAAG,CAAC,CAwFtB"}
|
package/dist/client.js
ADDED
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import { HttpError } from './errors.js';
|
|
2
|
+
const normalizeMethodConfig = (source) => {
|
|
3
|
+
if (!source)
|
|
4
|
+
return {};
|
|
5
|
+
return Object.fromEntries(Object.entries(source).map(([key, value]) => [key, { method: value.method ?? 'GET', path: value.path }]));
|
|
6
|
+
};
|
|
7
|
+
const deriveMethodConfig = (api) => {
|
|
8
|
+
if (typeof api !== 'object' || api === null)
|
|
9
|
+
return {};
|
|
10
|
+
if (typeof api.manifest === 'function') {
|
|
11
|
+
const manifest = api.manifest();
|
|
12
|
+
if (manifest && typeof manifest === 'object')
|
|
13
|
+
return normalizeMethodConfig(manifest);
|
|
14
|
+
}
|
|
15
|
+
if ('_routeConfig' in api &&
|
|
16
|
+
typeof api._routeConfig === 'object' &&
|
|
17
|
+
api._routeConfig !== null) {
|
|
18
|
+
return normalizeMethodConfig(api._routeConfig);
|
|
19
|
+
}
|
|
20
|
+
if ('queries' in api &&
|
|
21
|
+
typeof api.queries === 'object' &&
|
|
22
|
+
api.queries !== null) {
|
|
23
|
+
return normalizeMethodConfig(api.queries);
|
|
24
|
+
}
|
|
25
|
+
return {};
|
|
26
|
+
};
|
|
27
|
+
const isAbsoluteHttpUrl = (value) => /^https?:\/\//.test(value);
|
|
28
|
+
const ensureTrailingSlash = (value) => value.endsWith('/') ? value : `${value}/`;
|
|
29
|
+
const buildUrl = (baseUrl, name, path) => {
|
|
30
|
+
if (path) {
|
|
31
|
+
if (isAbsoluteHttpUrl(path))
|
|
32
|
+
return path;
|
|
33
|
+
if (isAbsoluteHttpUrl(baseUrl))
|
|
34
|
+
return new URL(path, ensureTrailingSlash(baseUrl)).toString();
|
|
35
|
+
if (path.startsWith('/'))
|
|
36
|
+
return path;
|
|
37
|
+
if (!baseUrl)
|
|
38
|
+
throw new Error('baseUrl is required');
|
|
39
|
+
return `${ensureTrailingSlash(baseUrl)}${path}`;
|
|
40
|
+
}
|
|
41
|
+
if (!baseUrl)
|
|
42
|
+
throw new Error('baseUrl is required');
|
|
43
|
+
return `${ensureTrailingSlash(baseUrl)}${name}`;
|
|
44
|
+
};
|
|
45
|
+
const parseResponse = async (response) => {
|
|
46
|
+
const text = await response.text();
|
|
47
|
+
try {
|
|
48
|
+
return JSON.parse(text);
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
return text;
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
const resolveHeaders = async (headers) => {
|
|
55
|
+
if (!headers)
|
|
56
|
+
return {};
|
|
57
|
+
const raw = typeof headers === 'function' ? await headers() : headers;
|
|
58
|
+
return Object.fromEntries(Object.entries(raw).filter(([, value]) => value !== undefined));
|
|
59
|
+
};
|
|
60
|
+
const resolveToken = async (token) => typeof token === 'function' ? token() : token;
|
|
61
|
+
let clientSequence = 0;
|
|
62
|
+
export function createHypequeryClient(config) {
|
|
63
|
+
const { baseUrl, fetchFn = fetch, headers, token, config: explicitConfig = {}, manifest, onUnauthorized, api, } = config;
|
|
64
|
+
const cacheKey = config.cacheKey ?? `client:${++clientSequence}`;
|
|
65
|
+
const finalConfig = {
|
|
66
|
+
...deriveMethodConfig(api),
|
|
67
|
+
...(manifest ? normalizeMethodConfig(manifest) : {}),
|
|
68
|
+
...explicitConfig,
|
|
69
|
+
};
|
|
70
|
+
const request = async (name, input, defaultMethod = 'GET', extraHeaders) => {
|
|
71
|
+
const methodConfig = finalConfig[name];
|
|
72
|
+
if (name.includes(':') && !methodConfig?.path) {
|
|
73
|
+
throw new Error(`No route configured for "${name}". Pass \`manifest\` (from the serve ` +
|
|
74
|
+
`api.manifest()) or an explicit \`config\` entry to createHypequeryClient().`);
|
|
75
|
+
}
|
|
76
|
+
const url = buildUrl(baseUrl, name, methodConfig?.path);
|
|
77
|
+
const method = methodConfig?.method ?? defaultMethod;
|
|
78
|
+
let finalUrl = url;
|
|
79
|
+
let body;
|
|
80
|
+
if (method === 'GET' && input && typeof input === 'object') {
|
|
81
|
+
const params = new URLSearchParams();
|
|
82
|
+
for (const [key, value] of Object.entries(input)) {
|
|
83
|
+
if (value === undefined || value === null)
|
|
84
|
+
continue;
|
|
85
|
+
if (Array.isArray(value))
|
|
86
|
+
value.forEach((item) => params.append(key, String(item)));
|
|
87
|
+
else
|
|
88
|
+
params.append(key, String(value));
|
|
89
|
+
}
|
|
90
|
+
const queryString = params.toString();
|
|
91
|
+
finalUrl = queryString ? `${url}?${queryString}` : url;
|
|
92
|
+
}
|
|
93
|
+
else if (input !== undefined) {
|
|
94
|
+
body = JSON.stringify(input);
|
|
95
|
+
}
|
|
96
|
+
const attempt = async () => {
|
|
97
|
+
const [resolvedHeaders, resolvedToken] = await Promise.all([
|
|
98
|
+
resolveHeaders(headers),
|
|
99
|
+
resolveToken(token),
|
|
100
|
+
]);
|
|
101
|
+
return fetchFn(finalUrl, {
|
|
102
|
+
method,
|
|
103
|
+
headers: {
|
|
104
|
+
...resolvedHeaders,
|
|
105
|
+
...(resolvedToken ? { authorization: `Bearer ${resolvedToken}` } : {}),
|
|
106
|
+
...(extraHeaders ?? {}),
|
|
107
|
+
...(body ? { 'content-type': 'application/json' } : {}),
|
|
108
|
+
},
|
|
109
|
+
body,
|
|
110
|
+
});
|
|
111
|
+
};
|
|
112
|
+
let response = await attempt();
|
|
113
|
+
if (response.status === 401 && onUnauthorized) {
|
|
114
|
+
await onUnauthorized();
|
|
115
|
+
response = await attempt();
|
|
116
|
+
}
|
|
117
|
+
if (!response.ok) {
|
|
118
|
+
const errorBody = await parseResponse(response);
|
|
119
|
+
throw new HttpError(`${method} request to ${finalUrl} failed with status ${response.status}`, response.status, errorBody);
|
|
120
|
+
}
|
|
121
|
+
return response.json();
|
|
122
|
+
};
|
|
123
|
+
return Object.freeze({
|
|
124
|
+
cacheKey,
|
|
125
|
+
config: Object.freeze({ ...config }),
|
|
126
|
+
request,
|
|
127
|
+
});
|
|
128
|
+
}
|
package/dist/createHooks.d.ts
CHANGED
|
@@ -1,60 +1,16 @@
|
|
|
1
1
|
import { type UseQueryOptions as TanstackUseQueryOptions, type UseMutationOptions as TanstackUseMutationOptions, type UseInfiniteQueryOptions as TanstackUseInfiniteQueryOptions, type UseMutationResult, type UseQueryResult, type UseInfiniteQueryResult, type InfiniteData } from '@tanstack/react-query';
|
|
2
2
|
import type { ExtractNames, QueryInput, QueryOutput } from './types.js';
|
|
3
3
|
import { HttpError } from './errors.js';
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
path?: string;
|
|
7
|
-
}
|
|
8
|
-
/** A single route as produced by `@hypequery/serve`'s `api.manifest()`. */
|
|
9
|
-
export interface RouteManifestEntry {
|
|
10
|
-
method?: string;
|
|
11
|
-
path?: string;
|
|
12
|
-
}
|
|
13
|
-
/**
|
|
14
|
-
* Map of query/metric/dataset keys to their HTTP method and full path. Pair
|
|
15
|
-
* with `InferAPIType` and `api.manifest()` from `@hypequery/serve` to resolve
|
|
16
|
-
* routes on the client without importing server code into the bundle.
|
|
17
|
-
*/
|
|
18
|
-
export type RouteManifest = Record<string, RouteManifestEntry>;
|
|
19
|
-
type HeaderMap = Record<string, string | undefined>;
|
|
20
|
-
type HeadersInput = HeaderMap | (() => HeaderMap | Promise<HeaderMap>);
|
|
21
|
-
export interface CreateHooksConfig<TApi = Record<string, {
|
|
22
|
-
input: unknown;
|
|
23
|
-
output: unknown;
|
|
24
|
-
}>> {
|
|
25
|
-
baseUrl: string;
|
|
26
|
-
fetchFn?: typeof fetch;
|
|
27
|
-
/**
|
|
28
|
-
* Static headers, or a (optionally async) function returning headers. The
|
|
29
|
-
* function is invoked per request, so it can supply a fresh/short-lived token.
|
|
30
|
-
*/
|
|
31
|
-
headers?: HeadersInput;
|
|
32
|
-
config?: Record<string, QueryMethodConfig>;
|
|
33
|
-
/**
|
|
34
|
-
* Route manifest from `@hypequery/serve`'s `api.manifest()`. Resolves the
|
|
35
|
-
* method and full path for each key — required for metric/dataset endpoints,
|
|
36
|
-
* which are POST routes whose paths differ from their map keys.
|
|
37
|
-
*/
|
|
38
|
-
manifest?: RouteManifest;
|
|
39
|
-
/**
|
|
40
|
-
* Called when a request returns 401. Use it to refresh credentials (e.g. a
|
|
41
|
-
* token). If it resolves without throwing, the request is retried once with
|
|
42
|
-
* freshly resolved headers.
|
|
43
|
-
*/
|
|
44
|
-
onUnauthorized?: () => void | Promise<void>;
|
|
45
|
-
api?: TApi;
|
|
46
|
-
}
|
|
4
|
+
import { type ApiContract, type HypequeryClientConfig } from './client.js';
|
|
5
|
+
export type CreateHooksConfig<TApi extends ApiContract = ApiContract> = HypequeryClientConfig<TApi>;
|
|
47
6
|
declare const OPTIONS_SYMBOL: unique symbol;
|
|
48
7
|
export declare function queryOptions<T extends object>(opts: T): T & {
|
|
49
8
|
[OPTIONS_SYMBOL]: true;
|
|
50
9
|
};
|
|
51
|
-
export declare function createHooks<Api extends
|
|
52
|
-
input:
|
|
53
|
-
output: any;
|
|
54
|
-
}>>(config: CreateHooksConfig<Api>): {
|
|
55
|
-
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>;
|
|
10
|
+
export declare function createHooks<Api extends ApiContract>(config?: CreateHooksConfig<Api>): {
|
|
11
|
+
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", string, Name] : ["hypequery", string, 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", string, Name] : ["hypequery", string, Name, QueryInput<Api, Name>]>, "queryKey" | "queryFn"> | undefined]) => UseQueryResult<QueryOutput<Api, Name>, HttpError>;
|
|
56
12
|
readonly useMutation: <Name extends ExtractNames<Api>>(name: Name, options?: Omit<TanstackUseMutationOptions<QueryOutput<Api, Name>, HttpError, QueryInput<Api, Name>, unknown>, "mutationFn">) => UseMutationResult<QueryOutput<Api, Name>, HttpError, QueryInput<Api, Name>>;
|
|
57
|
-
readonly useInfiniteQuery: <Name extends ExtractNames<Api>>(name: Name, input: QueryInput<Api, Name>, options?: Omit<TanstackUseInfiniteQueryOptions<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">) => UseInfiniteQueryResult<InfiniteData<QueryOutput<Api, Name>, number>, HttpError>;
|
|
13
|
+
readonly useInfiniteQuery: <Name extends ExtractNames<Api>>(name: Name, input: QueryInput<Api, Name>, options?: Omit<TanstackUseInfiniteQueryOptions<QueryOutput<Api, Name>, HttpError, InfiniteData<QueryOutput<Api, Name>, number>, QueryInput<Api, Name> extends never ? ["hypequery", string, Name] : ["hypequery", string, Name, QueryInput<Api, Name>], number>, "queryKey" | "queryFn" | "getNextPageParam" | "initialPageParam">) => UseInfiniteQueryResult<InfiniteData<QueryOutput<Api, Name>, number>, HttpError>;
|
|
58
14
|
};
|
|
59
15
|
export {};
|
|
60
16
|
//# sourceMappingURL=createHooks.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"createHooks.d.ts","sourceRoot":"","sources":["../src/createHooks.tsx"],"names":[],"mappings":"AAAA,OAAO,EAIL,KAAK,eAAe,IAAI,uBAAuB,EAC/C,KAAK,kBAAkB,IAAI,0BAA0B,EACrD,KAAK,uBAAuB,IAAI,+BAA+B,EAC/D,KAAK,iBAAiB,EACtB,KAAK,cAAc,EACnB,KAAK,sBAAsB,EAC3B,KAAK,YAAY,EAClB,MAAM,uBAAuB,CAAC;AAC/B,OAAO,KAAK,EAAE,YAAY,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AACxE,OAAO,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;
|
|
1
|
+
{"version":3,"file":"createHooks.d.ts","sourceRoot":"","sources":["../src/createHooks.tsx"],"names":[],"mappings":"AAAA,OAAO,EAIL,KAAK,eAAe,IAAI,uBAAuB,EAC/C,KAAK,kBAAkB,IAAI,0BAA0B,EACrD,KAAK,uBAAuB,IAAI,+BAA+B,EAC/D,KAAK,iBAAiB,EACtB,KAAK,cAAc,EACnB,KAAK,sBAAsB,EAC3B,KAAK,YAAY,EAClB,MAAM,uBAAuB,CAAC;AAC/B,OAAO,KAAK,EAAE,YAAY,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AACxE,OAAO,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AACxC,OAAO,EAEL,KAAK,WAAW,EAEhB,KAAK,qBAAqB,EAC3B,MAAM,aAAa,CAAC;AAWrB,MAAM,MAAM,iBAAiB,CAAC,IAAI,SAAS,WAAW,GAAG,WAAW,IAClE,qBAAqB,CAAC,IAAI,CAAC,CAAC;AAE9B,QAAA,MAAM,cAAc,eAAkC,CAAC;AAEvD,wBAAgB,YAAY,CAAC,CAAC,SAAS,MAAM,EAAE,IAAI,EAAE,CAAC,GAAG,CAAC,GAAG;IAAE,CAAC,cAAc,CAAC,EAAE,IAAI,CAAA;CAAE,CAEtF;AAcD,wBAAgB,WAAW,CAAC,GAAG,SAAS,WAAW,EACjD,MAAM,CAAC,EAAE,iBAAiB,CAAC,GAAG,CAAC;wBA0Bb,IAAI,SAAS,YAAY,CAAC,GAAG,CAAC,+mBAE7C,cAAc,CAAC,WAAW,CAAC,GAAG,EAAE,IAAI,CAAC,EAAE,SAAS,CAAC;2BAyC/B,IAAI,SAAS,YAAY,CAAC,GAAG,CAAC,QAC3C,IAAI,kIAET,iBAAiB,CAAC,WAAW,CAAC,GAAG,EAAE,IAAI,CAAC,EAAE,SAAS,EAAE,UAAU,CAAC,GAAG,EAAE,IAAI,CAAC,CAAC;gCAyBpD,IAAI,SAAS,YAAY,CAAC,GAAG,CAAC,QAChD,IAAI,SACH,UAAU,CAAC,GAAG,EAAE,IAAI,CAAC,yUAE3B,sBAAsB,CAAC,YAAY,CAAC,WAAW,CAAC,GAAG,EAAE,IAAI,CAAC,EAAE,MAAM,CAAC,EAAE,SAAS,CAAC;EA6BnF"}
|
package/dist/createHooks.js
CHANGED
|
@@ -1,69 +1,10 @@
|
|
|
1
1
|
import { useQuery as useTanstackQuery, useMutation as useTanstackMutation, useInfiniteQuery as useTanstackInfiniteQuery, } from '@tanstack/react-query';
|
|
2
|
-
import {
|
|
2
|
+
import { createHypequeryClient, } from './client.js';
|
|
3
|
+
import { useOptionalHypequeryClient } from './provider.js';
|
|
3
4
|
const OPTIONS_SYMBOL = Symbol.for('hypequery-options');
|
|
4
5
|
export function queryOptions(opts) {
|
|
5
6
|
return { ...opts, [OPTIONS_SYMBOL]: true };
|
|
6
7
|
}
|
|
7
|
-
const normalizeMethodConfig = (source) => {
|
|
8
|
-
if (!source)
|
|
9
|
-
return {};
|
|
10
|
-
return Object.fromEntries(Object.entries(source).map(([key, value]) => [key, { method: value.method ?? 'GET', path: value.path }]));
|
|
11
|
-
};
|
|
12
|
-
const deriveMethodConfig = (api) => {
|
|
13
|
-
if (typeof api !== 'object' || api === null) {
|
|
14
|
-
return {};
|
|
15
|
-
}
|
|
16
|
-
// Preferred: the serve API exposes a manifest() with full method + path.
|
|
17
|
-
if (typeof api.manifest === 'function') {
|
|
18
|
-
const manifest = api.manifest();
|
|
19
|
-
if (manifest && typeof manifest === 'object') {
|
|
20
|
-
return normalizeMethodConfig(manifest);
|
|
21
|
-
}
|
|
22
|
-
}
|
|
23
|
-
if ('_routeConfig' in api &&
|
|
24
|
-
typeof api._routeConfig === 'object' &&
|
|
25
|
-
api._routeConfig !== null) {
|
|
26
|
-
return normalizeMethodConfig(api._routeConfig);
|
|
27
|
-
}
|
|
28
|
-
if ('queries' in api &&
|
|
29
|
-
typeof api.queries === 'object' &&
|
|
30
|
-
api.queries !== null) {
|
|
31
|
-
return normalizeMethodConfig(api.queries);
|
|
32
|
-
}
|
|
33
|
-
return {};
|
|
34
|
-
};
|
|
35
|
-
const isAbsoluteHttpUrl = (value) => /^https?:\/\//.test(value);
|
|
36
|
-
const ensureTrailingSlash = (value) => value.endsWith('/') ? value : `${value}/`;
|
|
37
|
-
const buildUrl = (baseUrl, name, path) => {
|
|
38
|
-
if (path) {
|
|
39
|
-
if (isAbsoluteHttpUrl(path)) {
|
|
40
|
-
return path;
|
|
41
|
-
}
|
|
42
|
-
if (isAbsoluteHttpUrl(baseUrl)) {
|
|
43
|
-
return new URL(path, ensureTrailingSlash(baseUrl)).toString();
|
|
44
|
-
}
|
|
45
|
-
if (path.startsWith('/')) {
|
|
46
|
-
return path;
|
|
47
|
-
}
|
|
48
|
-
if (!baseUrl) {
|
|
49
|
-
throw new Error('baseUrl is required');
|
|
50
|
-
}
|
|
51
|
-
return `${ensureTrailingSlash(baseUrl)}${path}`;
|
|
52
|
-
}
|
|
53
|
-
if (!baseUrl) {
|
|
54
|
-
throw new Error('baseUrl is required');
|
|
55
|
-
}
|
|
56
|
-
return `${ensureTrailingSlash(baseUrl)}${name}`;
|
|
57
|
-
};
|
|
58
|
-
const parseResponse = async (res) => {
|
|
59
|
-
const text = await res.text();
|
|
60
|
-
try {
|
|
61
|
-
return JSON.parse(text);
|
|
62
|
-
}
|
|
63
|
-
catch {
|
|
64
|
-
return text;
|
|
65
|
-
}
|
|
66
|
-
};
|
|
67
8
|
const isOptionsBag = (value) => {
|
|
68
9
|
return Boolean(value && typeof value === 'object' && OPTIONS_SYMBOL in value);
|
|
69
10
|
};
|
|
@@ -76,76 +17,18 @@ const looksLikeQueryOptions = (value) => {
|
|
|
76
17
|
const matches = optionKeys.filter((key) => key in value).length;
|
|
77
18
|
return matches >= 2;
|
|
78
19
|
};
|
|
79
|
-
const resolveHeaders = async (headers) => {
|
|
80
|
-
if (!headers)
|
|
81
|
-
return {};
|
|
82
|
-
const raw = typeof headers === 'function' ? await headers() : headers;
|
|
83
|
-
return Object.fromEntries(Object.entries(raw).filter(([, value]) => value !== undefined));
|
|
84
|
-
};
|
|
85
20
|
export function createHooks(config) {
|
|
86
|
-
const
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
};
|
|
93
|
-
const fetchQuery = async (name, input, defaultMethod = 'GET', extraHeaders) => {
|
|
94
|
-
const methodConfig = finalConfig[name];
|
|
95
|
-
// Semantic endpoints (dataset:<name>) live at paths that differ from their
|
|
96
|
-
// map key, so without a resolved path we'd call the wrong URL. Fail loudly
|
|
97
|
-
// instead of silently requesting `${baseUrl}/dataset:<name>`.
|
|
98
|
-
if (name.includes(':') && !methodConfig?.path) {
|
|
99
|
-
throw new Error(`No route configured for "${name}". Pass \`manifest\` (from the serve ` +
|
|
100
|
-
`api.manifest()) or an explicit \`config\` entry to createHooks().`);
|
|
21
|
+
const configuredClient = config ? createHypequeryClient(config) : null;
|
|
22
|
+
function useResolvedClient() {
|
|
23
|
+
const providerClient = useOptionalHypequeryClient();
|
|
24
|
+
const client = configuredClient ?? providerClient;
|
|
25
|
+
if (!client) {
|
|
26
|
+
throw new Error('Configure createHooks() directly or render inside <HypequeryProvider>.');
|
|
101
27
|
}
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
let finalUrl = url;
|
|
105
|
-
let body;
|
|
106
|
-
if (method === 'GET' && input && typeof input === 'object') {
|
|
107
|
-
const params = new URLSearchParams();
|
|
108
|
-
for (const [key, value] of Object.entries(input)) {
|
|
109
|
-
if (value === undefined || value === null)
|
|
110
|
-
continue;
|
|
111
|
-
if (Array.isArray(value)) {
|
|
112
|
-
value.forEach((item) => params.append(key, String(item)));
|
|
113
|
-
}
|
|
114
|
-
else {
|
|
115
|
-
params.append(key, String(value));
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
const queryString = params.toString();
|
|
119
|
-
finalUrl = queryString ? `${url}?${queryString}` : url;
|
|
120
|
-
}
|
|
121
|
-
else if (input !== undefined) {
|
|
122
|
-
body = JSON.stringify(input);
|
|
123
|
-
}
|
|
124
|
-
const attempt = async () => {
|
|
125
|
-
const resolvedHeaders = await resolveHeaders(headers);
|
|
126
|
-
return fetchFn(finalUrl, {
|
|
127
|
-
method,
|
|
128
|
-
headers: {
|
|
129
|
-
...resolvedHeaders,
|
|
130
|
-
...(extraHeaders ?? {}),
|
|
131
|
-
...(body ? { 'content-type': 'application/json' } : {}),
|
|
132
|
-
},
|
|
133
|
-
body,
|
|
134
|
-
});
|
|
135
|
-
};
|
|
136
|
-
let res = await attempt();
|
|
137
|
-
// On 401, give the caller a chance to refresh credentials and retry once.
|
|
138
|
-
if (res.status === 401 && onUnauthorized) {
|
|
139
|
-
await onUnauthorized();
|
|
140
|
-
res = await attempt();
|
|
141
|
-
}
|
|
142
|
-
if (!res.ok) {
|
|
143
|
-
const errorBody = await parseResponse(res);
|
|
144
|
-
throw new HttpError(`${method} request to ${finalUrl} failed with status ${res.status}`, res.status, errorBody);
|
|
145
|
-
}
|
|
146
|
-
return res.json();
|
|
147
|
-
};
|
|
28
|
+
return client;
|
|
29
|
+
}
|
|
148
30
|
function useQuery(...args) {
|
|
31
|
+
const client = useResolvedClient();
|
|
149
32
|
const [name, potentialInputOrOptions, maybeOptions] = args;
|
|
150
33
|
let input;
|
|
151
34
|
let options;
|
|
@@ -163,19 +46,20 @@ export function createHooks(config) {
|
|
|
163
46
|
}
|
|
164
47
|
const queryKey = (() => {
|
|
165
48
|
if (input === undefined) {
|
|
166
|
-
return ['hypequery', name];
|
|
49
|
+
return ['hypequery', client.cacheKey, name];
|
|
167
50
|
}
|
|
168
|
-
return ['hypequery', name, input];
|
|
51
|
+
return ['hypequery', client.cacheKey, name, input];
|
|
169
52
|
})();
|
|
170
53
|
return useTanstackQuery({
|
|
171
54
|
queryKey,
|
|
172
|
-
queryFn: () =>
|
|
55
|
+
queryFn: () => client.request(name, input),
|
|
173
56
|
...(options ?? {}),
|
|
174
57
|
});
|
|
175
58
|
}
|
|
176
59
|
function useMutation(name, options) {
|
|
60
|
+
const client = useResolvedClient();
|
|
177
61
|
return useTanstackMutation({
|
|
178
|
-
mutationFn: (input) =>
|
|
62
|
+
mutationFn: (input) => client.request(name, input, 'POST'),
|
|
179
63
|
...options,
|
|
180
64
|
});
|
|
181
65
|
}
|
|
@@ -186,12 +70,13 @@ export function createHooks(config) {
|
|
|
186
70
|
* provided, is the starting offset.
|
|
187
71
|
*/
|
|
188
72
|
function useInfiniteQuery(name, input, options) {
|
|
73
|
+
const client = useResolvedClient();
|
|
189
74
|
const initialOffset = input?.offset ?? 0;
|
|
190
|
-
const queryKey = ['hypequery', name, input];
|
|
75
|
+
const queryKey = ['hypequery', client.cacheKey, name, input];
|
|
191
76
|
return useTanstackInfiniteQuery({
|
|
192
77
|
queryKey,
|
|
193
78
|
initialPageParam: initialOffset,
|
|
194
|
-
queryFn: ({ pageParam }) =>
|
|
79
|
+
queryFn: ({ pageParam }) => client.request(name, { ...input, offset: pageParam }, 'POST', { 'x-include-meta': 'true' }),
|
|
195
80
|
getNextPageParam: (lastPage) => {
|
|
196
81
|
const pagination = lastPage.meta?.pagination;
|
|
197
82
|
if (!pagination || !pagination.hasMore)
|
package/dist/index.d.ts
CHANGED
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
export { createHooks, queryOptions } from './createHooks.js';
|
|
2
2
|
export { createAnalyticsHooks } from './analyticsHooks.js';
|
|
3
|
+
export { createHypequeryClient } from './client.js';
|
|
4
|
+
export { HypequeryProvider, useHypequeryClient } from './provider.js';
|
|
3
5
|
export { HttpError } from './errors.js';
|
|
4
6
|
export type { QueryInput, QueryOutput, HttpMethod } from './types.js';
|
|
5
|
-
export type { CreateHooksConfig
|
|
7
|
+
export type { CreateHooksConfig } from './createHooks.js';
|
|
8
|
+
export type { ApiContract, HeadersInput, HypequeryClient, HypequeryClientConfig, QueryMethodConfig, RouteManifest, RouteManifestEntry, TokenInput, } from './client.js';
|
|
9
|
+
export type { HypequeryProviderProps } from './provider.js';
|
|
6
10
|
export type { CreateAnalyticsHooksConfig } from './analyticsHooks.js';
|
|
7
11
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,kBAAkB,CAAC;AAC7D,OAAO,EAAE,oBAAoB,EAAE,MAAM,qBAAqB,CAAC;AAC3D,OAAO,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AACxC,YAAY,EAAE,UAAU,EAAE,WAAW,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AACtE,YAAY,EAAE,iBAAiB,EAAE,iBAAiB,EAAE,MAAM,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,kBAAkB,CAAC;AAC7D,OAAO,EAAE,oBAAoB,EAAE,MAAM,qBAAqB,CAAC;AAC3D,OAAO,EAAE,qBAAqB,EAAE,MAAM,aAAa,CAAC;AACpD,OAAO,EAAE,iBAAiB,EAAE,kBAAkB,EAAE,MAAM,eAAe,CAAC;AACtE,OAAO,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AACxC,YAAY,EAAE,UAAU,EAAE,WAAW,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AACtE,YAAY,EAAE,iBAAiB,EAAE,MAAM,kBAAkB,CAAC;AAC1D,YAAY,EACV,WAAW,EACX,YAAY,EACZ,eAAe,EACf,qBAAqB,EACrB,iBAAiB,EACjB,aAAa,EACb,kBAAkB,EAClB,UAAU,GACX,MAAM,aAAa,CAAC;AACrB,YAAY,EAAE,sBAAsB,EAAE,MAAM,eAAe,CAAC;AAC5D,YAAY,EAAE,0BAA0B,EAAE,MAAM,qBAAqB,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
1
|
export { createHooks, queryOptions } from './createHooks.js';
|
|
2
2
|
export { createAnalyticsHooks } from './analyticsHooks.js';
|
|
3
|
+
export { createHypequeryClient } from './client.js';
|
|
4
|
+
export { HypequeryProvider, useHypequeryClient } from './provider.js';
|
|
3
5
|
export { HttpError } from './errors.js';
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { QueryClient } from '@tanstack/react-query';
|
|
2
|
+
import { type ReactNode } from 'react';
|
|
3
|
+
import type { ApiContract, HypequeryClient } from './client.js';
|
|
4
|
+
export interface HypequeryProviderProps<Api extends ApiContract = ApiContract> {
|
|
5
|
+
client: HypequeryClient<Api>;
|
|
6
|
+
children: ReactNode;
|
|
7
|
+
/** Reuse an application's existing TanStack Query client when supplied. */
|
|
8
|
+
queryClient?: QueryClient;
|
|
9
|
+
}
|
|
10
|
+
export declare function HypequeryProvider<Api extends ApiContract>({ client, children, queryClient, }: HypequeryProviderProps<Api>): import("react/jsx-runtime").JSX.Element;
|
|
11
|
+
export declare function useHypequeryClient<Api extends ApiContract = ApiContract>(): HypequeryClient<Api>;
|
|
12
|
+
export declare function useOptionalHypequeryClient<Api extends ApiContract = ApiContract>(): HypequeryClient<Api> | null;
|
|
13
|
+
//# sourceMappingURL=provider.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"provider.d.ts","sourceRoot":"","sources":["../src/provider.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAuB,MAAM,uBAAuB,CAAC;AACzE,OAAO,EAAuC,KAAK,SAAS,EAAE,MAAM,OAAO,CAAC;AAC5E,OAAO,KAAK,EAAE,WAAW,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAIhE,MAAM,WAAW,sBAAsB,CAAC,GAAG,SAAS,WAAW,GAAG,WAAW;IAC3E,MAAM,EAAE,eAAe,CAAC,GAAG,CAAC,CAAC;IAC7B,QAAQ,EAAE,SAAS,CAAC;IACpB,2EAA2E;IAC3E,WAAW,CAAC,EAAE,WAAW,CAAC;CAC3B;AAED,wBAAgB,iBAAiB,CAAC,GAAG,SAAS,WAAW,EAAE,EACzD,MAAM,EACN,QAAQ,EACR,WAAW,GACZ,EAAE,sBAAsB,CAAC,GAAG,CAAC,2CAS7B;AAED,wBAAgB,kBAAkB,CAAC,GAAG,SAAS,WAAW,GAAG,WAAW,0BAMvE;AAED,wBAAgB,0BAA0B,CAAC,GAAG,SAAS,WAAW,GAAG,WAAW,KACvC,eAAe,CAAC,GAAG,CAAC,GAAG,IAAI,CACnE"}
|
package/dist/provider.js
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
|
3
|
+
import { createContext, useContext, useState } from 'react';
|
|
4
|
+
const HypequeryContext = createContext(null);
|
|
5
|
+
export function HypequeryProvider({ client, children, queryClient, }) {
|
|
6
|
+
const [defaultQueryClient] = useState(() => new QueryClient());
|
|
7
|
+
return (_jsx(HypequeryContext.Provider, { value: client, children: _jsx(QueryClientProvider, { client: queryClient ?? defaultQueryClient, children: children }) }));
|
|
8
|
+
}
|
|
9
|
+
export function useHypequeryClient() {
|
|
10
|
+
const client = useOptionalHypequeryClient();
|
|
11
|
+
if (!client) {
|
|
12
|
+
throw new Error('Hypequery hooks must be rendered inside <HypequeryProvider>.');
|
|
13
|
+
}
|
|
14
|
+
return client;
|
|
15
|
+
}
|
|
16
|
+
export function useOptionalHypequeryClient() {
|
|
17
|
+
return useContext(HypequeryContext);
|
|
18
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"provider.test.d.ts","sourceRoot":"","sources":["../src/provider.test.tsx"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { renderHook, waitFor } from '@testing-library/react';
|
|
3
|
+
import { QueryClient } from '@tanstack/react-query';
|
|
4
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
5
|
+
import { createHypequeryClient } from './client.js';
|
|
6
|
+
import { createHooks } from './createHooks.js';
|
|
7
|
+
import { HypequeryProvider } from './provider.js';
|
|
8
|
+
function success(data) {
|
|
9
|
+
return { ok: true, status: 200, json: async () => data };
|
|
10
|
+
}
|
|
11
|
+
describe('HypequeryProvider', () => {
|
|
12
|
+
it('configures generated hooks once and supplies their TanStack Query client', async () => {
|
|
13
|
+
const fetchFn = vi.fn().mockResolvedValue(success({ message: 'Hello Luke' }));
|
|
14
|
+
const client = createHypequeryClient({
|
|
15
|
+
baseUrl: 'https://acme.hypequery.cloud/v1/analytics/production',
|
|
16
|
+
token: async () => 'browser-token',
|
|
17
|
+
fetchFn: fetchFn,
|
|
18
|
+
manifest: { greeting: { method: 'GET', path: 'queries/greeting' } },
|
|
19
|
+
});
|
|
20
|
+
const { useQuery } = createHooks();
|
|
21
|
+
const wrapper = ({ children }) => (_jsx(HypequeryProvider, { client: client, children: children }));
|
|
22
|
+
const { result } = renderHook(() => useQuery('greeting', { name: 'Luke' }), { wrapper });
|
|
23
|
+
await waitFor(() => expect(result.current.data).toEqual({ message: 'Hello Luke' }));
|
|
24
|
+
expect(fetchFn).toHaveBeenCalledWith('https://acme.hypequery.cloud/v1/analytics/production/queries/greeting?name=Luke', expect.objectContaining({
|
|
25
|
+
headers: expect.objectContaining({ authorization: 'Bearer browser-token' }),
|
|
26
|
+
}));
|
|
27
|
+
});
|
|
28
|
+
it('isolates cached responses when the provider client changes', async () => {
|
|
29
|
+
const firstFetch = vi.fn().mockResolvedValue(success({ message: 'First user' }));
|
|
30
|
+
const secondFetch = vi.fn().mockResolvedValue(success({ message: 'Second user' }));
|
|
31
|
+
const createClient = (fetchFn) => createHypequeryClient({
|
|
32
|
+
baseUrl: 'https://acme.hypequery.cloud/v1/analytics/production',
|
|
33
|
+
fetchFn: fetchFn,
|
|
34
|
+
manifest: { greeting: { method: 'GET', path: 'queries/greeting' } },
|
|
35
|
+
});
|
|
36
|
+
const firstClient = createClient(firstFetch);
|
|
37
|
+
const secondClient = createClient(secondFetch);
|
|
38
|
+
const queryClient = new QueryClient({
|
|
39
|
+
defaultOptions: { queries: { retry: false } },
|
|
40
|
+
});
|
|
41
|
+
const { useQuery } = createHooks();
|
|
42
|
+
let activeClient = firstClient;
|
|
43
|
+
const wrapper = ({ children }) => (_jsx(HypequeryProvider, { client: activeClient, queryClient: queryClient, children: children }));
|
|
44
|
+
const view = renderHook(() => useQuery('greeting', { name: 'Luke' }), { wrapper });
|
|
45
|
+
await waitFor(() => expect(view.result.current.data).toEqual({ message: 'First user' }));
|
|
46
|
+
activeClient = secondClient;
|
|
47
|
+
view.rerender();
|
|
48
|
+
await waitFor(() => expect(view.result.current.data).toEqual({ message: 'Second user' }));
|
|
49
|
+
expect(firstClient.cacheKey).not.toBe(secondClient.cacheKey);
|
|
50
|
+
expect(firstFetch).toHaveBeenCalledTimes(1);
|
|
51
|
+
expect(secondFetch).toHaveBeenCalledTimes(1);
|
|
52
|
+
});
|
|
53
|
+
});
|
package/package.json
CHANGED
|
@@ -1,7 +1,19 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hypequery/react",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "React hooks for
|
|
3
|
+
"version": "0.6.0",
|
|
4
|
+
"description": "Type-safe React and TanStack Query hooks for ClickHouse analytics, metrics, and datasets",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"react",
|
|
7
|
+
"tanstack-query",
|
|
8
|
+
"clickhouse",
|
|
9
|
+
"analytics",
|
|
10
|
+
"semantic-layer",
|
|
11
|
+
"typescript",
|
|
12
|
+
"query-builder",
|
|
13
|
+
"orm",
|
|
14
|
+
"dashboard",
|
|
15
|
+
"embedded-analytics"
|
|
16
|
+
],
|
|
5
17
|
"type": "module",
|
|
6
18
|
"main": "./dist/index.js",
|
|
7
19
|
"exports": {
|
|
@@ -30,8 +42,17 @@
|
|
|
30
42
|
"react-dom": "^18.2.0",
|
|
31
43
|
"typescript": "^5.7.3",
|
|
32
44
|
"vitest": "^3.2.6",
|
|
33
|
-
"@hypequery/
|
|
34
|
-
"@hypequery/
|
|
45
|
+
"@hypequery/serve": "0.15.3",
|
|
46
|
+
"@hypequery/datasets": "0.13.4"
|
|
47
|
+
},
|
|
48
|
+
"repository": {
|
|
49
|
+
"type": "git",
|
|
50
|
+
"url": "git+https://github.com/hypequery/hypequery.git",
|
|
51
|
+
"directory": "packages/react"
|
|
52
|
+
},
|
|
53
|
+
"homepage": "https://hypequery.com",
|
|
54
|
+
"bugs": {
|
|
55
|
+
"url": "https://github.com/hypequery/hypequery/issues"
|
|
35
56
|
},
|
|
36
57
|
"publishConfig": {
|
|
37
58
|
"access": "public"
|