@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 CHANGED
@@ -1,307 +1,128 @@
1
1
  # @hypequery/react
2
2
 
3
- React hooks for typed Hypequery HTTP endpoints, built on TanStack Query.
3
+ Typed React hooks for ClickHouse analytics, powered by TanStack Query.
4
4
 
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.
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
- ## Quick Start
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
- api.route('/weeklyRevenue', api.queries.weeklyRevenue);
17
+ Generate a small route manifest during the build:
46
18
 
47
- export type AnalyticsApi = typeof api;
19
+ ```bash
20
+ npx hypequery generate:manifest analytics/api.ts \
21
+ --output src/generated/hypequery-manifest.json
48
22
  ```
49
23
 
50
- Create hooks on the client:
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/analytics-hooks.ts
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
- function RevenuePanel() {
67
- const revenue = useQuery('weeklyRevenue', {
68
- startDate: '2026-01-01',
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
- if (revenue.isLoading) return <p>Loading...</p>;
72
- if (revenue.error) return <p>{revenue.error.message}</p>;
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
- return <pre>{JSON.stringify(revenue.data, null, 2)}</pre>;
58
+ export function Providers({ children }: { children: ReactNode }) {
59
+ return <HypequeryProvider client={client}>{children}</HypequeryProvider>;
75
60
  }
76
61
  ```
77
62
 
78
- ## Dataset And Metric Hooks
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 { InferApiType } from '@hypequery/serve';
85
- import type { api } from '../server/api.js';
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
- const orders = useDataset('orders', {
106
- dimensions: ['status'],
107
- measures: ['revenue', 'orderCount'],
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
- ## Pagination
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 OrdersTable() {
127
- const orders = useInfiniteDataset('orders', {
128
- dimensions: ['status'],
129
- measures: ['revenue'],
130
- limit: 50,
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
- ```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>"
195
- }
101
+ return <RevenueChart rows={revenue.data?.data ?? []} />;
196
102
  }
197
103
  ```
198
104
 
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';
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
- `path` may be relative to `baseUrl`, absolute within the same origin, or an absolute HTTP URL.
107
+ ## Included
237
108
 
238
- ## Headers & auth
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
- Pass static headers, or a function (sync or async) invoked per request — handy for
241
- attaching a fresh/short-lived token.
116
+ ## Why it matters
242
117
 
243
- ```ts
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
- ## Notes
120
+ ## Learn more
301
121
 
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.
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
 
@@ -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: CreateAnalyticsHooksConfig<Api, TMetrics>): {
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":"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"}
1
+ {"version":3,"file":"analyticsHooks.d.ts","sourceRoot":"","sources":["../src/analyticsHooks.tsx"],"names":[],"mappings":"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"}
@@ -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
+ }
@@ -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
- export interface QueryMethodConfig {
5
- method?: string;
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 Record<string, {
52
- input: any;
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;AAUxC,MAAM,WAAW,iBAAiB;IAChC,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAED,2EAA2E;AAC3E,MAAM,WAAW,kBAAkB;IACjC,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAED;;;;GAIG;AACH,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,KAAK,YAAY,GACb,SAAS,GACT,CAAC,MAAM,SAAS,GAAG,OAAO,CAAC,SAAS,CAAC,CAAC,CAAC;AAE3C,MAAM,WAAW,iBAAiB,CAAC,IAAI,GAAG,MAAM,CAAC,MAAM,EAAE;IAAE,KAAK,EAAE,OAAO,CAAC;IAAC,MAAM,EAAE,OAAO,CAAA;CAAE,CAAC;IAC3F,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,CAAC,EAAE,OAAO,KAAK,CAAC;IACvB;;;OAGG;IACH,OAAO,CAAC,EAAE,YAAY,CAAC;IACvB,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,iBAAiB,CAAC,CAAC;IAC3C;;;;OAIG;IACH,QAAQ,CAAC,EAAE,aAAa,CAAC;IACzB;;;;OAIG;IACH,cAAc,CAAC,EAAE,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC5C,GAAG,CAAC,EAAE,IAAI,CAAC;CACZ;AAED,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;AAqGD,wBAAgB,WAAW,CAAC,GAAG,SAAS,MAAM,CAAC,MAAM,EAAE;IAAE,KAAK,EAAE,GAAG,CAAC;IAAC,MAAM,EAAE,GAAG,CAAA;CAAE,CAAC,EACjF,MAAM,EAAE,iBAAiB,CAAC,GAAG,CAAC;wBAgGZ,IAAI,SAAS,YAAY,CAAC,GAAG,CAAC,+kBAE7C,cAAc,CAAC,WAAW,CAAC,GAAG,EAAE,IAAI,CAAC,EAAE,SAAS,CAAC;2BAwC/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;gCAwBpD,IAAI,SAAS,YAAY,CAAC,GAAG,CAAC,QAChD,IAAI,SACH,UAAU,CAAC,GAAG,EAAE,IAAI,CAAC,yTAE3B,sBAAsB,CAAC,YAAY,CAAC,WAAW,CAAC,GAAG,EAAE,IAAI,CAAC,EAAE,MAAM,CAAC,EAAE,SAAS,CAAC;EA4BnF"}
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"}
@@ -1,69 +1,10 @@
1
1
  import { useQuery as useTanstackQuery, useMutation as useTanstackMutation, useInfiniteQuery as useTanstackInfiniteQuery, } from '@tanstack/react-query';
2
- import { HttpError } from './errors.js';
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 { baseUrl, fetchFn = fetch, headers = {}, config: explicitConfig = {}, manifest, onUnauthorized, api } = config;
87
- // Precedence: explicit config > manifest > derived from a runtime api object.
88
- const finalConfig = {
89
- ...deriveMethodConfig(api),
90
- ...(manifest ? normalizeMethodConfig(manifest) : {}),
91
- ...explicitConfig,
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
- const url = buildUrl(baseUrl, name, methodConfig?.path);
103
- const method = methodConfig?.method ?? defaultMethod;
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: () => fetchQuery(name, input),
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) => fetchQuery(name, input, 'POST'),
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 }) => fetchQuery(name, { ...input, offset: pageParam }, 'POST', { 'x-include-meta': 'true' }),
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, QueryMethodConfig } from './createHooks.js';
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
@@ -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,kBAAkB,CAAC;AAC7E,YAAY,EAAE,0BAA0B,EAAE,MAAM,qBAAqB,CAAC"}
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"}
@@ -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,2 @@
1
+ export {};
2
+ //# sourceMappingURL=provider.test.d.ts.map
@@ -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.5.0",
4
- "description": "React hooks for consuming hypequery APIs",
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/datasets": "0.8.0",
34
- "@hypequery/serve": "0.8.0"
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"