@hypequery/react 0.6.3 → 0.7.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
@@ -85,6 +85,39 @@ export const { useMetric, useDataset } =
85
85
  });
86
86
  ```
87
87
 
88
+ ## Use Python datasets
89
+
90
+ Generate a TypeScript client file from the Python build contract with
91
+ `hypequery generate:client client-contract.json --output src/hypequery-client.ts`.
92
+ Use its API type and route map directly:
93
+
94
+ ```tsx
95
+ import { createAnalyticsHooks } from '@hypequery/react';
96
+ import { clientManifest, type ClientApi } from './hypequery-client.js';
97
+
98
+ export const { useDataset, useMetric, useInfiniteDataset } =
99
+ createAnalyticsHooks<ClientApi>({
100
+ baseUrl: 'https://analytics.example.com',
101
+ manifest: clientManifest,
102
+ token: () => getShortLivedUserToken(),
103
+ });
104
+ ```
105
+
106
+ Use the endpoint origin as `baseUrl`: generated paths already contain the full
107
+ installed mount prefix. Every request sends its endpoint schema hash. A `409`
108
+ with `CLIENT_SCHEMA_CHANGED` requires explicit regeneration and review; the client
109
+ does not fetch a replacement schema or refresh authentication for that response.
110
+ TanStack Query retry/refetch policies remain configurable by your application.
111
+ Use a new client or cache scope when the deployment, user or authorization scope
112
+ changes; a token resolver changing its return value does not invalidate cached data.
113
+
114
+ Literal selections expose their selected fields. Omitted measures use the exported
115
+ `defaultMeasures`; `measures: []` selects none. Numeric dimension values may be
116
+ numbers or strings, unknown/related nullability includes null, and aggregates use
117
+ strings or null. A time bucket may be null. Widened query inputs retain optional
118
+ fields because their actual selection is known only at runtime. Existing server
119
+ inference and handwritten API types remain supported.
120
+
88
121
  ## Build the dashboard
89
122
 
90
123
  ```tsx
package/dist/client.d.ts CHANGED
@@ -5,10 +5,14 @@ export type ApiContract = Record<string, {
5
5
  export interface QueryMethodConfig {
6
6
  method?: string;
7
7
  path?: string;
8
+ /** Generated endpoint schema precondition. A mismatch requires explicit regeneration/refresh. */
9
+ schemaHash?: string;
8
10
  }
9
11
  export interface RouteManifestEntry {
10
12
  method?: string;
11
13
  path?: string;
14
+ /** Generated endpoint schema precondition. A mismatch requires explicit regeneration/refresh. */
15
+ schemaHash?: string;
12
16
  }
13
17
  export type RouteManifest = Record<string, RouteManifestEntry>;
14
18
  type HeaderMap = Record<string, string | undefined>;
@@ -1 +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"}
1
+ {"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"AAGA,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;IACd,iGAAiG;IACjG,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,WAAW,kBAAkB;IACjC,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,iGAAiG;IACjG,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;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,CAyFtB"}
package/dist/client.js CHANGED
@@ -1,8 +1,9 @@
1
+ import { clientSchemaHeaders } from './utils/client-schema-headers.js';
1
2
  import { HttpError } from './errors.js';
2
3
  const normalizeMethodConfig = (source) => {
3
4
  if (!source)
4
5
  return {};
5
- return Object.fromEntries(Object.entries(source).map(([key, value]) => [key, { method: value.method ?? 'GET', path: value.path }]));
6
+ return Object.fromEntries(Object.entries(source).map(([key, value]) => [key, { method: value.method ?? 'GET', path: value.path, schemaHash: value.schemaHash }]));
6
7
  };
7
8
  const deriveMethodConfig = (api) => {
8
9
  if (typeof api !== 'object' || api === null)
@@ -62,11 +63,10 @@ let clientSequence = 0;
62
63
  export function createHypequeryClient(config) {
63
64
  const { baseUrl, fetchFn = fetch, headers, token, config: explicitConfig = {}, manifest, onUnauthorized, api, } = config;
64
65
  const cacheKey = config.cacheKey ?? `client:${++clientSequence}`;
65
- const finalConfig = {
66
- ...deriveMethodConfig(api),
67
- ...(manifest ? normalizeMethodConfig(manifest) : {}),
68
- ...explicitConfig,
69
- };
66
+ const finalConfig = Object.assign(Object.create(null), deriveMethodConfig(api), manifest ? normalizeMethodConfig(manifest) : {});
67
+ for (const [name, override] of Object.entries(explicitConfig)) {
68
+ finalConfig[name] = { ...finalConfig[name], ...override };
69
+ }
70
70
  const request = async (name, input, defaultMethod = 'GET', extraHeaders) => {
71
71
  const methodConfig = finalConfig[name];
72
72
  if (name.includes(':') && !methodConfig?.path) {
@@ -100,12 +100,12 @@ export function createHypequeryClient(config) {
100
100
  ]);
101
101
  return fetchFn(finalUrl, {
102
102
  method,
103
- headers: {
103
+ headers: clientSchemaHeaders({
104
104
  ...resolvedHeaders,
105
105
  ...(resolvedToken ? { authorization: `Bearer ${resolvedToken}` } : {}),
106
106
  ...(extraHeaders ?? {}),
107
107
  ...(body ? { 'content-type': 'application/json' } : {}),
108
- },
108
+ }, methodConfig?.schemaHash),
109
109
  body,
110
110
  });
111
111
  };
package/dist/index.d.ts CHANGED
@@ -3,7 +3,7 @@ export { createAnalyticsHooks } from './analyticsHooks.js';
3
3
  export { createHypequeryClient } from './client.js';
4
4
  export { HypequeryProvider, useHypequeryClient } from './provider.js';
5
5
  export { HttpError } from './errors.js';
6
- export type { QueryInput, QueryOutput, HttpMethod } from './types.js';
6
+ export type { QueryInput, QueryOutput, QueryOutputForInput, HttpMethod } from './types.js';
7
7
  export type { CreateHooksConfig } from './createHooks.js';
8
8
  export type { ApiContract, HeadersInput, HypequeryClient, HypequeryClientConfig, QueryMethodConfig, RouteManifest, RouteManifestEntry, TokenInput, } from './client.js';
9
9
  export type { HypequeryProviderProps } from './provider.js';
@@ -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,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"}
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,mBAAmB,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAC3F,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"}
@@ -7,7 +7,7 @@ export interface HypequeryProviderProps<Api extends ApiContract = ApiContract> {
7
7
  /** Reuse an application's existing TanStack Query client when supplied. */
8
8
  queryClient?: QueryClient;
9
9
  }
10
- export declare function HypequeryProvider<Api extends ApiContract>({ client, children, queryClient, }: HypequeryProviderProps<Api>): import("react/jsx-runtime").JSX.Element;
10
+ export declare function HypequeryProvider<Api extends ApiContract>({ client, children, queryClient, }: HypequeryProviderProps<Api>): import("react").JSX.Element;
11
11
  export declare function useHypequeryClient<Api extends ApiContract = ApiContract>(): HypequeryClient<Api>;
12
12
  export declare function useOptionalHypequeryClient<Api extends ApiContract = ApiContract>(): HypequeryClient<Api> | null;
13
13
  //# sourceMappingURL=provider.d.ts.map
@@ -1 +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"}
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,+BAS7B;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/types.d.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import type { ClientOutputForInput } from './utils/client-projection.js';
1
2
  export type HttpMethod = "GET" | "POST" | "PUT" | "PATCH" | "DELETE";
2
3
  export type KnownStringKeys<T> = {
3
4
  [K in keyof T]: string extends K ? never : K extends string ? K : never;
@@ -53,10 +54,12 @@ type MetricOutputForInput<TInfo, TInput, TFallback> = TInfo extends {
53
54
  } & {
54
55
  [K in TMetricName]?: string | null;
55
56
  } & PeriodSelection<TInput>> : TFallback;
56
- export type QueryOutputForInput<Api, Name extends ExtractNames<Api>, TInput> = SemanticInfo<Api, Name> extends infer Info ? Info extends {
57
+ type LegacyOutputForInput<Api, Name extends ExtractNames<Api>, TInput> = [SemanticInfo<Api, Name>] extends [never] ? QueryOutput<Api, Name> : SemanticInfo<Api, Name> extends infer Info ? Info extends {
57
58
  kind: 'dataset';
58
59
  } ? DatasetOutputForInput<Info, TInput, QueryOutput<Api, Name>> : Info extends {
59
60
  kind: 'metric';
60
61
  } ? MetricOutputForInput<Info, TInput, QueryOutput<Api, Name>> : QueryOutput<Api, Name> : QueryOutput<Api, Name>;
62
+ /** Narrow generated client rows while retaining the legacy semantic and ordinary API contracts. */
63
+ export type QueryOutputForInput<Api, Name extends ExtractNames<Api>, TInput> = '__hypequeryClientSemantic' extends keyof Api[Name] ? ClientOutputForInput<NonNullable<Api[Name]['__hypequeryClientSemantic']>, TInput, QueryOutput<Api, Name>> : LegacyOutputForInput<Api, Name, TInput>;
61
64
  export {};
62
65
  //# sourceMappingURL=types.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,48 @@
1
+ /** Projection for the versioned data-only client contract emitted by generate:client. */
2
+ type WireValue<Definition> = Definition extends {
3
+ wireType: infer Wire;
4
+ nullable: infer Nullable;
5
+ } ? (Wire extends 'number-or-string' ? number | string : Wire extends 'number' ? number : Wire extends 'boolean' ? boolean : string) | (Nullable extends false ? never : null) : unknown;
6
+ type ArrayNames<Value> = NonNullable<Value> extends readonly (infer Name)[] ? Name : never;
7
+ type IsUnion<T, U = T> = T extends unknown ? ([U] extends [T] ? false : true) : never;
8
+ /** Names one fixed tuple always selects: an element typed as a union may name any of them. */
9
+ type TupleNames<Tuple> = Tuple extends readonly [infer Head, ...infer Rest] ? (IsUnion<Head> extends true ? never : Head) | TupleNames<Rest> : never;
10
+ /** Names every alternative of a (possibly union-typed) tuple selection selects. */
11
+ type CertainNames<Selection> = (Selection extends unknown ? (names: TupleNames<Selection>) => void : never) extends (names: infer Names) => void ? Names : never;
12
+ type SelectedFields<Definitions, Input, Key extends string, Defaults = never> = Key extends keyof Input ? Input extends Record<Key, readonly unknown[]> ? number extends Input[Key]['length'] ? {
13
+ [Name in Extract<ArrayNames<Input[Key]>, keyof Definitions>]?: WireValue<Definitions[Name]>;
14
+ } : {
15
+ [Name in Extract<CertainNames<Input[Key]>, keyof Definitions>]-?: WireValue<Definitions[Name]>;
16
+ } & {
17
+ [Name in Exclude<Extract<ArrayNames<Input[Key]>, keyof Definitions>, CertainNames<Input[Key]>>]?: WireValue<Definitions[Name]>;
18
+ } : {
19
+ [Name in Extract<ArrayNames<Input[Key]> | Defaults, keyof Definitions>]?: WireValue<Definitions[Name]>;
20
+ } : {
21
+ [Name in Extract<Defaults, keyof Definitions>]-?: WireValue<Definitions[Name]>;
22
+ };
23
+ type Period<Input> = Input extends {
24
+ by: string;
25
+ } ? {
26
+ period: string | null;
27
+ } : 'by' extends keyof Input ? {
28
+ period?: string | null;
29
+ } : {};
30
+ type ProjectedOutput<Output, Row> = Omit<Output, 'data'> & {
31
+ data: Row[];
32
+ };
33
+ export type ClientOutputForInput<Info, Input, Fallback> = [Info] extends [never] ? Fallback : Input extends unknown ? Info extends {
34
+ version: 1;
35
+ kind: 'dataset';
36
+ dimensions: infer Dimensions;
37
+ measures: infer Measures;
38
+ defaultMeasures: readonly (infer Default)[];
39
+ } ? ProjectedOutput<Fallback, SelectedFields<Dimensions, Input, 'dimensions'> & SelectedFields<Measures, Input, 'measures', Default> & Period<Input>> : Info extends {
40
+ version: 1;
41
+ kind: 'metric';
42
+ dimensions: infer Dimensions;
43
+ metricName: infer Name extends string;
44
+ } ? ProjectedOutput<Fallback, SelectedFields<Dimensions, Input, 'dimensions'> & {
45
+ [Key in Name]: string | null;
46
+ } & Period<Input>> : Fallback : never;
47
+ export {};
48
+ //# sourceMappingURL=client-projection.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"client-projection.d.ts","sourceRoot":"","sources":["../../src/utils/client-projection.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,3 @@
1
+ /** A generated precondition wins over differently cased user/header overrides. */
2
+ export declare function clientSchemaHeaders(headers: Record<string, string>, schemaHash?: string): Record<string, string>;
3
+ //# sourceMappingURL=client-schema-headers.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"client-schema-headers.d.ts","sourceRoot":"","sources":["../../src/utils/client-schema-headers.ts"],"names":[],"mappings":"AAAA,kFAAkF;AAClF,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,EAAE,UAAU,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAMhH"}
@@ -0,0 +1,9 @@
1
+ /** A generated precondition wins over differently cased user/header overrides. */
2
+ export function clientSchemaHeaders(headers, schemaHash) {
3
+ if (!schemaHash)
4
+ return headers;
5
+ return {
6
+ ...Object.fromEntries(Object.entries(headers).filter(([name]) => name.toLowerCase() !== 'x-hypequery-client-schema')),
7
+ 'X-Hypequery-Client-Schema': schemaHash,
8
+ };
9
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hypequery/react",
3
- "version": "0.6.3",
3
+ "version": "0.7.0",
4
4
  "description": "Type-safe React and TanStack Query hooks for ClickHouse analytics, metrics, and datasets",
5
5
  "keywords": [
6
6
  "react",
@@ -23,27 +23,33 @@
23
23
  }
24
24
  },
25
25
  "files": [
26
- "dist"
26
+ "dist",
27
+ "!dist/**/*.test.*",
28
+ "!dist/**/*.type-test.*",
29
+ "!dist/**/__tests__/**",
30
+ "!dist/**/tests/**",
31
+ "!dist/**/type-tests/**"
27
32
  ],
28
33
  "sideEffects": false,
29
34
  "peerDependencies": {
30
- "@tanstack/react-query": "^5.104.0",
35
+ "@tanstack/react-query": "^5.104.5",
31
36
  "react": "^18.0.0 || ^19.0.0",
32
37
  "react-dom": "^18.0.0 || ^19.0.0"
33
38
  },
34
39
  "devDependencies": {
35
- "@tanstack/react-query": "^5.104.0",
36
- "@testing-library/react": "^14.2.1",
37
- "@types/react": "^18.2.43",
38
- "@types/react-dom": "^18.2.18",
39
- "@vitest/coverage-v8": "^5.0.2",
40
- "jsdom": "^27.4.0",
41
- "react": "^18.2.0",
42
- "react-dom": "^18.2.0",
43
- "typescript": "^5.7.3",
44
- "vitest": "^5.0.2",
45
- "@hypequery/serve": "0.19.3",
46
- "@hypequery/datasets": "0.21.0"
40
+ "@tanstack/react-query": "^5.104.5",
41
+ "@testing-library/dom": "^10.4.2",
42
+ "@testing-library/react": "^16.3.3",
43
+ "@types/react": "^19.3.0",
44
+ "@types/react-dom": "^19.3.0",
45
+ "@vitest/coverage-v8": "^5.0.5",
46
+ "jsdom": "^30.1.3",
47
+ "react": "^19.3.1",
48
+ "react-dom": "^19.3.1",
49
+ "typescript": "^6.0.3",
50
+ "vitest": "^5.0.5",
51
+ "@hypequery/datasets": "0.24.0",
52
+ "@hypequery/serve": "0.22.0"
47
53
  },
48
54
  "repository": {
49
55
  "type": "git",
@@ -1,2 +0,0 @@
1
- export {};
2
- //# sourceMappingURL=analyticsHooks.test.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"analyticsHooks.test.d.ts","sourceRoot":"","sources":["../src/analyticsHooks.test.tsx"],"names":[],"mappings":""}
@@ -1,98 +0,0 @@
1
- import { jsx as _jsx } from "react/jsx-runtime";
2
- import { describe, it, expect, vi, beforeEach } from 'vitest';
3
- import { renderHook, waitFor } from '@testing-library/react';
4
- import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
5
- import { createAnalyticsHooks } from './analyticsHooks.js';
6
- function createWrapper() {
7
- const queryClient = new QueryClient({
8
- defaultOptions: {
9
- queries: { retry: false },
10
- },
11
- });
12
- return ({ children }) => (_jsx(QueryClientProvider, { client: queryClient, children: children }));
13
- }
14
- function mockSuccessResponse(data) {
15
- return {
16
- ok: true,
17
- status: 200,
18
- json: () => Promise.resolve(data),
19
- };
20
- }
21
- describe('createAnalyticsHooks', () => {
22
- const fetchMock = vi.fn();
23
- beforeEach(() => {
24
- fetchMock.mockReset();
25
- });
26
- it('queries metrics through useMetric with metric names only', async () => {
27
- fetchMock.mockResolvedValue(mockSuccessResponse({ data: [{ totalRevenue: 42 }] }));
28
- const { useMetric } = createAnalyticsHooks({
29
- baseUrl: '/api/analytics',
30
- fetchFn: fetchMock,
31
- metrics: ['totalRevenue', 'monthlyRevenue'],
32
- config: {
33
- totalRevenue: { method: 'POST', path: '/api/analytics/metrics/totalRevenue' },
34
- },
35
- });
36
- const { result } = renderHook(() => useMetric('totalRevenue', { dimensions: ['country'] }), { wrapper: createWrapper() });
37
- await waitFor(() => {
38
- expect(result.current.data).toEqual({ data: [{ totalRevenue: 42 }] });
39
- });
40
- expect(fetchMock).toHaveBeenCalledWith('/api/analytics/metrics/totalRevenue', expect.objectContaining({
41
- method: 'POST',
42
- body: JSON.stringify({ dimensions: ['country'] }),
43
- }));
44
- });
45
- it('queries datasets through useDataset without exposing dataset: keys', async () => {
46
- fetchMock.mockResolvedValue(mockSuccessResponse({ data: [{ country: 'US', revenue: 120 }] }));
47
- const { useDataset } = createAnalyticsHooks({
48
- baseUrl: '/api/analytics',
49
- fetchFn: fetchMock,
50
- metrics: [],
51
- config: {
52
- 'dataset:orders': { method: 'POST', path: '/api/analytics/datasets/orders/query' },
53
- },
54
- });
55
- const { result } = renderHook(() => useDataset('orders', { dimensions: ['country'], measures: ['revenue'] }), { wrapper: createWrapper() });
56
- await waitFor(() => {
57
- expect(result.current.data).toEqual({ data: [{ country: 'US', revenue: 120 }] });
58
- });
59
- expect(fetchMock).toHaveBeenCalledWith('/api/analytics/datasets/orders/query', expect.objectContaining({
60
- method: 'POST',
61
- body: JSON.stringify({ dimensions: ['country'], measures: ['revenue'] }),
62
- }));
63
- });
64
- it('supports metric names from the declared semantic metric list', async () => {
65
- fetchMock.mockResolvedValue(mockSuccessResponse({ data: [{ period: '2025-01-01', monthlyRevenue: 90 }] }));
66
- const { useMetric } = createAnalyticsHooks({
67
- baseUrl: '/api/analytics',
68
- fetchFn: fetchMock,
69
- metrics: ['totalRevenue', 'monthlyRevenue'],
70
- config: {
71
- monthlyRevenue: { method: 'POST', path: '/api/analytics/metrics/monthlyRevenue' },
72
- },
73
- });
74
- const { result } = renderHook(() => useMetric('monthlyRevenue', { by: 'month' }), { wrapper: createWrapper() });
75
- await waitFor(() => {
76
- expect(result.current.data).toEqual({ data: [{ period: '2025-01-01', monthlyRevenue: 90 }] });
77
- });
78
- });
79
- it('resolves semantic config paths against an absolute baseUrl host', async () => {
80
- fetchMock.mockResolvedValue(mockSuccessResponse({ data: [{ totalRevenue: 42 }] }));
81
- const { useMetric } = createAnalyticsHooks({
82
- baseUrl: 'https://api.example.com/hypequery',
83
- fetchFn: fetchMock,
84
- metrics: ['totalRevenue', 'monthlyRevenue'],
85
- config: {
86
- totalRevenue: { method: 'POST', path: '/api/analytics/metrics/totalRevenue' },
87
- },
88
- });
89
- const { result } = renderHook(() => useMetric('totalRevenue', { dimensions: ['country'] }), { wrapper: createWrapper() });
90
- await waitFor(() => {
91
- expect(result.current.data).toEqual({ data: [{ totalRevenue: 42 }] });
92
- });
93
- expect(fetchMock).toHaveBeenCalledWith('https://api.example.com/api/analytics/metrics/totalRevenue', expect.objectContaining({
94
- method: 'POST',
95
- body: JSON.stringify({ dimensions: ['country'] }),
96
- }));
97
- });
98
- });
@@ -1,2 +0,0 @@
1
- export {};
2
- //# sourceMappingURL=createHooks.test.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"createHooks.test.d.ts","sourceRoot":"","sources":["../src/createHooks.test.tsx"],"names":[],"mappings":""}
@@ -1,772 +0,0 @@
1
- import { jsx as _jsx } from "react/jsx-runtime";
2
- import { describe, it, expect, vi, beforeEach } from 'vitest';
3
- import { renderHook, waitFor, act } from '@testing-library/react';
4
- import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
5
- import { createHooks, queryOptions } from './createHooks.js';
6
- import { HttpError } from './errors.js';
7
- function createWrapper() {
8
- const queryClient = new QueryClient({
9
- defaultOptions: {
10
- queries: { retry: false },
11
- mutations: { retry: false },
12
- },
13
- });
14
- return ({ children }) => (_jsx(QueryClientProvider, { client: queryClient, children: children }));
15
- }
16
- function mockSuccessResponse(data) {
17
- return {
18
- ok: true,
19
- status: 200,
20
- json: () => Promise.resolve(data),
21
- };
22
- }
23
- function mockErrorResponse(status, body) {
24
- return {
25
- ok: false,
26
- status,
27
- text: () => Promise.resolve(JSON.stringify(body)),
28
- };
29
- }
30
- describe('createHooks', () => {
31
- describe('Basic functionality', () => {
32
- const fetchMock = vi.fn();
33
- const { useQuery, useMutation } = createHooks({
34
- baseUrl: 'https://example.com/api',
35
- fetchFn: fetchMock,
36
- });
37
- beforeEach(() => {
38
- fetchMock.mockReset();
39
- });
40
- it('executes queries via fetch', async () => {
41
- fetchMock.mockResolvedValue(mockSuccessResponse({ total: 42 }));
42
- const { result } = renderHook(() => useQuery('weeklyRevenue', { startDate: '2025-01-01' }), {
43
- wrapper: createWrapper(),
44
- });
45
- await waitFor(() => {
46
- expect(result.current.data).toEqual({ total: 42 });
47
- });
48
- expect(fetchMock).toHaveBeenCalledWith('https://example.com/api/weeklyRevenue?startDate=2025-01-01', expect.objectContaining({ method: 'GET' }));
49
- });
50
- it('executes mutations via fetch', async () => {
51
- fetchMock.mockResolvedValue(mockSuccessResponse({ success: true }));
52
- const { result } = renderHook(() => useMutation('rebuildMetrics'), {
53
- wrapper: createWrapper()
54
- });
55
- await result.current.mutateAsync({ force: true });
56
- // Mutations default to POST
57
- expect(fetchMock).toHaveBeenCalledWith('https://example.com/api/rebuildMetrics', expect.objectContaining({
58
- method: 'POST',
59
- body: JSON.stringify({ force: true }),
60
- }));
61
- });
62
- it('allows queries with no input parameters', async () => {
63
- fetchMock.mockResolvedValue(mockSuccessResponse({ status: 'ok' }));
64
- const { result } = renderHook(() => useQuery('noInput'), {
65
- wrapper: createWrapper(),
66
- });
67
- await waitFor(() => expect(result.current.data).toEqual({ status: 'ok' }));
68
- expect(fetchMock).toHaveBeenCalledWith('https://example.com/api/noInput', expect.objectContaining({ method: 'GET' }));
69
- });
70
- it('invokes header factory per request', async () => {
71
- const headerFactory = vi.fn()
72
- .mockReturnValueOnce({ 'x-token': 'first' })
73
- .mockReturnValueOnce({ 'x-token': 'second' });
74
- const { useQuery } = createHooks({
75
- baseUrl: 'https://example.com/api',
76
- fetchFn: fetchMock,
77
- headers: headerFactory,
78
- });
79
- fetchMock.mockResolvedValue(mockSuccessResponse({ name: 'User' }));
80
- const first = renderHook(() => useQuery('getUser', { id: '1' }), {
81
- wrapper: createWrapper(),
82
- });
83
- await waitFor(() => {
84
- expect(first.result.current.data).toEqual({ name: 'User' });
85
- });
86
- const second = renderHook(() => useQuery('getUser', { id: '2' }), {
87
- wrapper: createWrapper(),
88
- });
89
- await waitFor(() => {
90
- expect(second.result.current.data).toEqual({ name: 'User' });
91
- });
92
- expect(headerFactory).toHaveBeenCalledTimes(2);
93
- expect(fetchMock.mock.calls[0][1]?.headers).toMatchObject({ 'x-token': 'first' });
94
- expect(fetchMock.mock.calls[1][1]?.headers).toMatchObject({ 'x-token': 'second' });
95
- });
96
- it('supports an async header factory', async () => {
97
- fetchMock.mockResolvedValue(mockSuccessResponse({ name: 'User' }));
98
- const { useQuery } = createHooks({
99
- baseUrl: 'https://example.com/api',
100
- fetchFn: fetchMock,
101
- headers: async () => ({ authorization: 'Bearer fresh-token' }),
102
- });
103
- const { result } = renderHook(() => useQuery('getUser', { id: '1' }), {
104
- wrapper: createWrapper(),
105
- });
106
- await waitFor(() => expect(result.current.isSuccess).toBe(true));
107
- expect(fetchMock.mock.calls[0][1]?.headers).toMatchObject({ authorization: 'Bearer fresh-token' });
108
- });
109
- it('calls onUnauthorized and retries once on 401', async () => {
110
- fetchMock
111
- .mockResolvedValueOnce(mockErrorResponse(401, { error: 'expired' }))
112
- .mockResolvedValueOnce(mockSuccessResponse({ name: 'User' }));
113
- let token = 'stale';
114
- const onUnauthorized = vi.fn(async () => { token = 'refreshed'; });
115
- const { useQuery } = createHooks({
116
- baseUrl: 'https://example.com/api',
117
- fetchFn: fetchMock,
118
- headers: () => ({ authorization: `Bearer ${token}` }),
119
- onUnauthorized,
120
- });
121
- const { result } = renderHook(() => useQuery('getUser', { id: '1' }), {
122
- wrapper: createWrapper(),
123
- });
124
- await waitFor(() => expect(result.current.isSuccess).toBe(true));
125
- expect(onUnauthorized).toHaveBeenCalledTimes(1);
126
- expect(fetchMock).toHaveBeenCalledTimes(2);
127
- // The retry uses freshly resolved headers.
128
- expect(fetchMock.mock.calls[1][1]?.headers).toMatchObject({ authorization: 'Bearer refreshed' });
129
- expect(result.current.data).toEqual({ name: 'User' });
130
- });
131
- it('throws after a single 401 retry still fails', async () => {
132
- fetchMock.mockResolvedValue(mockErrorResponse(401, { error: 'nope' }));
133
- const onUnauthorized = vi.fn(async () => { });
134
- const { useQuery } = createHooks({
135
- baseUrl: 'https://example.com/api',
136
- fetchFn: fetchMock,
137
- onUnauthorized,
138
- });
139
- const { result } = renderHook(() => useQuery('getUser', { id: '1' }), {
140
- wrapper: createWrapper(),
141
- });
142
- await waitFor(() => expect(result.current.isError).toBe(true));
143
- expect(onUnauthorized).toHaveBeenCalledTimes(1);
144
- expect(fetchMock).toHaveBeenCalledTimes(2);
145
- expect(result.current.error?.status).toBe(401);
146
- });
147
- });
148
- describe('HTTP Method Handling', () => {
149
- const fetchMock = vi.fn();
150
- beforeEach(() => {
151
- fetchMock.mockReset();
152
- fetchMock.mockResolvedValue(mockSuccessResponse({ success: true }));
153
- });
154
- it('defaults to GET when no config provided', async () => {
155
- const { useQuery } = createHooks({
156
- baseUrl: 'https://example.com/api',
157
- fetchFn: fetchMock,
158
- });
159
- const { result } = renderHook(() => useQuery('getUser', { id: '123' }), {
160
- wrapper: createWrapper(),
161
- });
162
- await waitFor(() => expect(result.current.isSuccess).toBe(true));
163
- expect(fetchMock).toHaveBeenCalledWith(expect.stringContaining('?'), expect.objectContaining({ method: 'GET' }));
164
- });
165
- it('uses POST when explicitly configured', async () => {
166
- const { useQuery } = createHooks({
167
- baseUrl: 'https://example.com/api',
168
- fetchFn: fetchMock,
169
- config: {
170
- getUser: { method: 'POST' },
171
- },
172
- });
173
- const { result } = renderHook(() => useQuery('getUser', { id: '123' }), {
174
- wrapper: createWrapper(),
175
- });
176
- await waitFor(() => expect(result.current.isSuccess).toBe(true));
177
- expect(fetchMock).toHaveBeenCalledWith('https://example.com/api/getUser', expect.objectContaining({
178
- method: 'POST',
179
- body: JSON.stringify({ id: '123' }),
180
- }));
181
- });
182
- it('extracts method config from API object', async () => {
183
- const mockApi = {
184
- queries: {
185
- getUser: { method: 'GET' },
186
- updateUser: { method: 'PATCH' },
187
- },
188
- };
189
- const { useQuery } = createHooks({
190
- baseUrl: 'https://example.com/api',
191
- fetchFn: fetchMock,
192
- api: mockApi,
193
- });
194
- const { result } = renderHook(() => useQuery('updateUser', { id: '123', name: 'John' }), {
195
- wrapper: createWrapper(),
196
- });
197
- await waitFor(() => expect(result.current.isSuccess).toBe(true));
198
- expect(fetchMock).toHaveBeenCalledWith(expect.any(String), expect.objectContaining({ method: 'PATCH' }));
199
- });
200
- it('prefers route-level config over endpoint config', async () => {
201
- const mockApi = {
202
- queries: {
203
- getUser: { method: 'POST' },
204
- },
205
- _routeConfig: {
206
- getUser: { method: 'GET' },
207
- },
208
- };
209
- const { useQuery } = createHooks({
210
- baseUrl: 'https://example.com/api',
211
- fetchFn: fetchMock,
212
- api: mockApi,
213
- });
214
- const { result } = renderHook(() => useQuery('getUser', { id: '123' }), {
215
- wrapper: createWrapper(),
216
- });
217
- await waitFor(() => expect(result.current.isSuccess).toBe(true));
218
- expect(fetchMock).toHaveBeenCalledWith(expect.any(String), expect.objectContaining({ method: 'GET' }));
219
- });
220
- it('allows explicit config to override API config', async () => {
221
- const mockApi = {
222
- queries: {
223
- getUser: { method: 'GET' },
224
- },
225
- };
226
- const { useQuery } = createHooks({
227
- baseUrl: 'https://example.com/api',
228
- fetchFn: fetchMock,
229
- api: mockApi,
230
- config: {
231
- getUser: { method: 'POST' },
232
- },
233
- });
234
- const { result } = renderHook(() => useQuery('getUser', { id: '123' }), {
235
- wrapper: createWrapper(),
236
- });
237
- await waitFor(() => expect(result.current.isSuccess).toBe(true));
238
- expect(fetchMock).toHaveBeenCalledWith('https://example.com/api/getUser', expect.objectContaining({ method: 'POST' }));
239
- });
240
- it('resolves root-relative config paths against an absolute baseUrl host', async () => {
241
- const { useQuery } = createHooks({
242
- baseUrl: 'https://api.example.com/hypequery',
243
- fetchFn: fetchMock,
244
- config: {
245
- getUser: { method: 'POST', path: '/api/analytics/queries/getUser' },
246
- },
247
- });
248
- const { result } = renderHook(() => useQuery('getUser', { id: '123' }), {
249
- wrapper: createWrapper(),
250
- });
251
- await waitFor(() => expect(result.current.isSuccess).toBe(true));
252
- expect(fetchMock).toHaveBeenCalledWith('https://api.example.com/api/analytics/queries/getUser', expect.objectContaining({
253
- method: 'POST',
254
- body: JSON.stringify({ id: '123' }),
255
- }));
256
- });
257
- it('resolves relative config paths against baseUrl', async () => {
258
- const { useQuery } = createHooks({
259
- baseUrl: 'https://api.example.com/hypequery',
260
- fetchFn: fetchMock,
261
- config: {
262
- getUser: { method: 'POST', path: 'queries/getUser' },
263
- },
264
- });
265
- const { result } = renderHook(() => useQuery('getUser', { id: '123' }), {
266
- wrapper: createWrapper(),
267
- });
268
- await waitFor(() => expect(result.current.isSuccess).toBe(true));
269
- expect(fetchMock).toHaveBeenCalledWith('https://api.example.com/hypequery/queries/getUser', expect.objectContaining({
270
- method: 'POST',
271
- body: JSON.stringify({ id: '123' }),
272
- }));
273
- });
274
- });
275
- describe('Route manifest', () => {
276
- const fetchMock = vi.fn();
277
- beforeEach(() => {
278
- fetchMock.mockReset();
279
- fetchMock.mockResolvedValue(mockSuccessResponse({ success: true }));
280
- });
281
- it('resolves method and path from a manifest', async () => {
282
- const { useQuery } = createHooks({
283
- baseUrl: 'https://api.example.com',
284
- fetchFn: fetchMock,
285
- manifest: {
286
- getUser: { method: 'POST', path: '/api/analytics/metrics/getUser' },
287
- },
288
- });
289
- const { result } = renderHook(() => useQuery('getUser', { id: '123' }), {
290
- wrapper: createWrapper(),
291
- });
292
- await waitFor(() => expect(result.current.isSuccess).toBe(true));
293
- expect(fetchMock).toHaveBeenCalledWith('https://api.example.com/api/analytics/metrics/getUser', expect.objectContaining({
294
- method: 'POST',
295
- body: JSON.stringify({ id: '123' }),
296
- }));
297
- });
298
- it('lets explicit config override the manifest', async () => {
299
- const { useQuery } = createHooks({
300
- baseUrl: 'https://api.example.com',
301
- fetchFn: fetchMock,
302
- manifest: {
303
- getUser: { method: 'POST', path: '/manifest/getUser' },
304
- },
305
- config: {
306
- getUser: { method: 'POST', path: '/override/getUser' },
307
- },
308
- });
309
- const { result } = renderHook(() => useQuery('getUser', { id: '123' }), {
310
- wrapper: createWrapper(),
311
- });
312
- await waitFor(() => expect(result.current.isSuccess).toBe(true));
313
- expect(fetchMock).toHaveBeenCalledWith('https://api.example.com/override/getUser', expect.objectContaining({ method: 'POST' }));
314
- });
315
- it('derives method config from an api object exposing manifest()', async () => {
316
- const mockApi = {
317
- manifest: () => ({
318
- getUser: { method: 'PATCH', path: '/api/analytics/queries/getUser' },
319
- }),
320
- };
321
- const { useQuery } = createHooks({
322
- baseUrl: 'https://api.example.com',
323
- fetchFn: fetchMock,
324
- api: mockApi,
325
- });
326
- const { result } = renderHook(() => useQuery('getUser', { id: '123' }), {
327
- wrapper: createWrapper(),
328
- });
329
- await waitFor(() => expect(result.current.isSuccess).toBe(true));
330
- expect(fetchMock).toHaveBeenCalledWith('https://api.example.com/api/analytics/queries/getUser', expect.objectContaining({ method: 'PATCH' }));
331
- });
332
- it('throws for an unresolved semantic (dataset:) key', async () => {
333
- const { useQuery } = createHooks({
334
- baseUrl: 'https://api.example.com',
335
- fetchFn: fetchMock,
336
- });
337
- const { result } = renderHook(() => useQuery('dataset:orders', { dimensions: ['country'] }), { wrapper: createWrapper() });
338
- await waitFor(() => expect(result.current.isError).toBe(true));
339
- expect(result.current.error?.message).toContain('No route configured for "dataset:orders"');
340
- expect(fetchMock).not.toHaveBeenCalled();
341
- });
342
- });
343
- describe('Query Parameter Serialization', () => {
344
- const fetchMock = vi.fn();
345
- beforeEach(() => {
346
- fetchMock.mockReset();
347
- fetchMock.mockResolvedValue(mockSuccessResponse({ success: true }));
348
- });
349
- it('serializes simple objects as query params for GET requests', async () => {
350
- const { useQuery } = createHooks({
351
- baseUrl: 'https://example.com/api',
352
- fetchFn: fetchMock,
353
- });
354
- const { result } = renderHook(() => useQuery('getUser', { id: '123' }), {
355
- wrapper: createWrapper(),
356
- });
357
- await waitFor(() => expect(result.current.isSuccess).toBe(true));
358
- expect(fetchMock).toHaveBeenCalledWith('https://example.com/api/getUser?id=123', expect.objectContaining({ method: 'GET' }));
359
- });
360
- it('serializes arrays as multiple query params', async () => {
361
- const { useQuery } = createHooks({
362
- baseUrl: 'https://example.com/api',
363
- fetchFn: fetchMock,
364
- });
365
- const { result } = renderHook(() => useQuery('listItems', { tags: ['react', 'typescript'], limit: 10 }), { wrapper: createWrapper() });
366
- await waitFor(() => expect(result.current.isSuccess).toBe(true));
367
- const callUrl = fetchMock.mock.calls[0][0];
368
- expect(callUrl).toContain('tags=react');
369
- expect(callUrl).toContain('tags=typescript');
370
- expect(callUrl).toContain('limit=10');
371
- });
372
- it('skips undefined and null values in query params', async () => {
373
- const { useQuery } = createHooks({
374
- baseUrl: 'https://example.com/api',
375
- fetchFn: fetchMock,
376
- });
377
- const { result } = renderHook(() => useQuery('rebuildMetrics', { force: undefined }), { wrapper: createWrapper() });
378
- await waitFor(() => expect(result.current.isSuccess).toBe(true));
379
- const callUrl = fetchMock.mock.calls[0][0];
380
- expect(callUrl).not.toContain('force');
381
- });
382
- it('handles empty objects in GET requests', async () => {
383
- const { useQuery } = createHooks({
384
- baseUrl: 'https://example.com/api',
385
- fetchFn: fetchMock,
386
- });
387
- const { result } = renderHook(() => useQuery('rebuildMetrics', {}), { wrapper: createWrapper() });
388
- await waitFor(() => expect(result.current.isSuccess).toBe(true));
389
- expect(fetchMock).toHaveBeenCalledWith('https://example.com/api/rebuildMetrics', expect.objectContaining({ method: 'GET' }));
390
- });
391
- it('uses JSON body for POST requests', async () => {
392
- const { useQuery } = createHooks({
393
- baseUrl: 'https://example.com/api',
394
- fetchFn: fetchMock,
395
- config: {
396
- getUser: { method: 'POST' },
397
- },
398
- });
399
- const { result } = renderHook(() => useQuery('getUser', { id: '123' }), {
400
- wrapper: createWrapper(),
401
- });
402
- await waitFor(() => expect(result.current.isSuccess).toBe(true));
403
- expect(fetchMock).toHaveBeenCalledWith('https://example.com/api/getUser', expect.objectContaining({
404
- method: 'POST',
405
- body: JSON.stringify({ id: '123' }),
406
- headers: expect.objectContaining({
407
- 'content-type': 'application/json',
408
- }),
409
- }));
410
- });
411
- });
412
- describe('Error Handling', () => {
413
- const fetchMock = vi.fn();
414
- beforeEach(() => {
415
- fetchMock.mockReset();
416
- });
417
- it('throws error with detailed message on failed request', async () => {
418
- fetchMock.mockResolvedValue(mockErrorResponse(404, { message: 'Not found' }));
419
- const { useQuery } = createHooks({
420
- baseUrl: 'https://example.com/api',
421
- fetchFn: fetchMock,
422
- });
423
- const { result } = renderHook(() => useQuery('getUser', { id: '123' }), {
424
- wrapper: createWrapper(),
425
- });
426
- await waitFor(() => expect(result.current.isError).toBe(true));
427
- expect(result.current.error).toBeInstanceOf(HttpError);
428
- expect(result.current.error?.message).toContain('GET request to');
429
- expect(result.current.error?.message).toContain('failed with status 404');
430
- expect(result.current.error?.status).toBe(404);
431
- expect(result.current.error?.body).toEqual({ message: 'Not found' });
432
- });
433
- it('parses JSON error responses', async () => {
434
- fetchMock.mockResolvedValue(mockErrorResponse(400, { error: 'Bad request' }));
435
- const { useQuery } = createHooks({
436
- baseUrl: 'https://example.com/api',
437
- fetchFn: fetchMock,
438
- });
439
- const { result } = renderHook(() => useQuery('getUser', { id: '123' }), {
440
- wrapper: createWrapper(),
441
- });
442
- await waitFor(() => expect(result.current.isError).toBe(true));
443
- expect(result.current.error).toBeInstanceOf(HttpError);
444
- expect(result.current.error?.body).toEqual({ error: 'Bad request' });
445
- });
446
- it('parses text error responses', async () => {
447
- fetchMock.mockResolvedValue({
448
- ok: false,
449
- status: 500,
450
- text: () => Promise.resolve('Internal server error'),
451
- });
452
- const { useQuery } = createHooks({
453
- baseUrl: 'https://example.com/api',
454
- fetchFn: fetchMock,
455
- });
456
- const { result } = renderHook(() => useQuery('getUser', { id: '123' }), {
457
- wrapper: createWrapper(),
458
- });
459
- await waitFor(() => expect(result.current.isError).toBe(true));
460
- expect(result.current.error).toBeInstanceOf(HttpError);
461
- expect(result.current.error?.body).toBe('Internal server error');
462
- });
463
- it('handles network errors', async () => {
464
- fetchMock.mockRejectedValue(new Error('Network error'));
465
- const { useQuery } = createHooks({
466
- baseUrl: 'https://example.com/api',
467
- fetchFn: fetchMock,
468
- });
469
- const { result } = renderHook(() => useQuery('getUser', { id: '123' }), {
470
- wrapper: createWrapper(),
471
- });
472
- await waitFor(() => expect(result.current.isError).toBe(true));
473
- expect(result.current.error?.message).toBe('Network error');
474
- });
475
- });
476
- describe('useQuery Argument Overloads', () => {
477
- const fetchMock = vi.fn();
478
- beforeEach(() => {
479
- fetchMock.mockReset();
480
- fetchMock.mockResolvedValue(mockSuccessResponse({ status: 'ok' }));
481
- });
482
- it('handles useQuery(name) with no input', async () => {
483
- const { useQuery } = createHooks({
484
- baseUrl: 'https://example.com/api',
485
- fetchFn: fetchMock,
486
- });
487
- const { result } = renderHook(() => useQuery('noInput'), {
488
- wrapper: createWrapper(),
489
- });
490
- await waitFor(() => expect(result.current.isSuccess).toBe(true));
491
- expect(fetchMock).toHaveBeenCalledWith('https://example.com/api/noInput', expect.any(Object));
492
- });
493
- it('handles useQuery(name, input)', async () => {
494
- const { useQuery } = createHooks({
495
- baseUrl: 'https://example.com/api',
496
- fetchFn: fetchMock,
497
- });
498
- const { result } = renderHook(() => useQuery('getUser', { id: '123' }), {
499
- wrapper: createWrapper(),
500
- });
501
- await waitFor(() => expect(result.current.isSuccess).toBe(true));
502
- expect(fetchMock).toHaveBeenCalled();
503
- });
504
- it('handles useQuery(name, options) - options only', async () => {
505
- const { useQuery } = createHooks({
506
- baseUrl: 'https://example.com/api',
507
- fetchFn: fetchMock,
508
- });
509
- const { result } = renderHook(() => useQuery('noInput', { enabled: false, staleTime: 5000 }), { wrapper: createWrapper() });
510
- // Should not fetch because enabled: false
511
- expect(result.current.isSuccess).toBe(false);
512
- expect(fetchMock).not.toHaveBeenCalled();
513
- });
514
- it('handles useQuery(name, input, options)', async () => {
515
- const { useQuery } = createHooks({
516
- baseUrl: 'https://example.com/api',
517
- fetchFn: fetchMock,
518
- });
519
- const { result } = renderHook(() => useQuery('getUser', { id: '123' }, { staleTime: 5000 }), { wrapper: createWrapper() });
520
- await waitFor(() => expect(result.current.isSuccess).toBe(true));
521
- expect(fetchMock).toHaveBeenCalled();
522
- });
523
- it('correctly distinguishes input from options using isQueryOptions', async () => {
524
- const { useQuery } = createHooks({
525
- baseUrl: 'https://example.com/api',
526
- fetchFn: fetchMock,
527
- });
528
- // Input with 'enabled' field should be treated as input, not options
529
- const { result: result1 } = renderHook(() => useQuery('rebuildMetrics', { force: true }), { wrapper: createWrapper() });
530
- await waitFor(() => expect(result1.current.isSuccess).toBe(true));
531
- expect(fetchMock).toHaveBeenCalled();
532
- fetchMock.mockClear();
533
- // Options object should disable the query
534
- renderHook(() => useQuery('noInput', { enabled: false, staleTime: 1000 }), { wrapper: createWrapper() });
535
- expect(fetchMock).not.toHaveBeenCalled();
536
- });
537
- it('uses queryOptions() helper for explicit option marking', async () => {
538
- const { useQuery } = createHooks({
539
- baseUrl: 'https://example.com/api',
540
- fetchFn: fetchMock,
541
- });
542
- // Using queryOptions() helper ensures options are recognized
543
- const { result } = renderHook(() => useQuery('noInput', queryOptions({ enabled: false })), { wrapper: createWrapper() });
544
- // Query should be disabled
545
- expect(result.current.isLoading).toBe(false);
546
- expect(fetchMock).not.toHaveBeenCalled();
547
- });
548
- });
549
- describe('Configuration', () => {
550
- const fetchMock = vi.fn();
551
- beforeEach(() => {
552
- fetchMock.mockReset();
553
- fetchMock.mockResolvedValue(mockSuccessResponse({ success: true }));
554
- });
555
- it('includes custom headers in requests', async () => {
556
- const { useQuery } = createHooks({
557
- baseUrl: 'https://example.com/api',
558
- fetchFn: fetchMock,
559
- headers: {
560
- 'Authorization': 'Bearer token123',
561
- 'X-Custom': 'value',
562
- },
563
- });
564
- const { result } = renderHook(() => useQuery('getUser', { id: '123' }), {
565
- wrapper: createWrapper(),
566
- });
567
- await waitFor(() => expect(result.current.isSuccess).toBe(true));
568
- expect(fetchMock).toHaveBeenCalledWith(expect.any(String), expect.objectContaining({
569
- headers: expect.objectContaining({
570
- 'Authorization': 'Bearer token123',
571
- 'X-Custom': 'value',
572
- }),
573
- }));
574
- });
575
- it('handles baseUrl with trailing slash', async () => {
576
- const { useQuery } = createHooks({
577
- baseUrl: 'https://example.com/api/',
578
- fetchFn: fetchMock,
579
- });
580
- const { result } = renderHook(() => useQuery('getUser', { id: '123' }), {
581
- wrapper: createWrapper(),
582
- });
583
- await waitFor(() => expect(result.current.isSuccess).toBe(true));
584
- const callUrl = fetchMock.mock.calls[0][0];
585
- expect(callUrl).toContain('https://example.com/api/getUser');
586
- expect(callUrl).not.toContain('//getUser');
587
- });
588
- it('handles baseUrl without trailing slash', async () => {
589
- const { useQuery } = createHooks({
590
- baseUrl: 'https://example.com/api',
591
- fetchFn: fetchMock,
592
- });
593
- const { result } = renderHook(() => useQuery('getUser', { id: '123' }), {
594
- wrapper: createWrapper(),
595
- });
596
- await waitFor(() => expect(result.current.isSuccess).toBe(true));
597
- const callUrl = fetchMock.mock.calls[0][0];
598
- expect(callUrl).toContain('https://example.com/api/getUser');
599
- });
600
- it('throws error when baseUrl is missing', () => {
601
- expect(() => {
602
- createHooks({
603
- baseUrl: '',
604
- fetchFn: fetchMock,
605
- });
606
- }).not.toThrow();
607
- const { useQuery } = createHooks({
608
- baseUrl: '',
609
- fetchFn: fetchMock,
610
- });
611
- const { result } = renderHook(() => useQuery('getUser', { id: '123' }), {
612
- wrapper: createWrapper(),
613
- });
614
- // Should throw during query execution
615
- waitFor(() => {
616
- expect(result.current.error?.message).toContain('baseUrl is required');
617
- });
618
- });
619
- });
620
- describe('Edge Cases', () => {
621
- const fetchMock = vi.fn();
622
- beforeEach(() => {
623
- fetchMock.mockReset();
624
- });
625
- it('handles empty response body', async () => {
626
- fetchMock.mockResolvedValue({
627
- ok: true,
628
- status: 204,
629
- json: () => Promise.resolve(null),
630
- });
631
- const { useQuery } = createHooks({
632
- baseUrl: 'https://example.com/api',
633
- fetchFn: fetchMock,
634
- });
635
- const { result } = renderHook(() => useQuery('noInput'), {
636
- wrapper: createWrapper(),
637
- });
638
- await waitFor(() => expect(result.current.isSuccess).toBe(true));
639
- expect(result.current.data).toBe(null);
640
- });
641
- it('handles non-JSON text responses', async () => {
642
- fetchMock.mockResolvedValue({
643
- ok: true,
644
- status: 200,
645
- json: () => Promise.reject(new Error('Not JSON')),
646
- text: () => Promise.resolve('Plain text response'),
647
- });
648
- const { useQuery } = createHooks({
649
- baseUrl: 'https://example.com/api',
650
- fetchFn: fetchMock,
651
- });
652
- const { result } = renderHook(() => useQuery('noInput'), {
653
- wrapper: createWrapper(),
654
- });
655
- // This will fail because json() rejects, but shows the edge case
656
- await waitFor(() => expect(result.current.isError).toBe(true));
657
- });
658
- it('passes through TanStack Query options correctly', async () => {
659
- fetchMock.mockResolvedValue(mockSuccessResponse({ status: 'ok' }));
660
- const { useQuery } = createHooks({
661
- baseUrl: 'https://example.com/api',
662
- fetchFn: fetchMock,
663
- });
664
- const { result, rerender } = renderHook(({ enabled }) => useQuery('noInput', { enabled, staleTime: 1000 }), {
665
- wrapper: createWrapper(),
666
- initialProps: { enabled: false },
667
- });
668
- // Should not fetch when disabled (now requires 2+ option keys to be recognized)
669
- expect(fetchMock).not.toHaveBeenCalled();
670
- // Enable the query
671
- rerender({ enabled: true });
672
- await waitFor(() => expect(result.current.isSuccess).toBe(true));
673
- expect(fetchMock).toHaveBeenCalled();
674
- });
675
- });
676
- describe('useMutation', () => {
677
- const fetchMock = vi.fn();
678
- beforeEach(() => {
679
- fetchMock.mockReset();
680
- fetchMock.mockResolvedValue(mockSuccessResponse({ success: true }));
681
- });
682
- it('defaults to POST method', async () => {
683
- const { useMutation } = createHooks({
684
- baseUrl: 'https://example.com/api',
685
- fetchFn: fetchMock,
686
- });
687
- const { result } = renderHook(() => useMutation('updateUser'), {
688
- wrapper: createWrapper(),
689
- });
690
- await result.current.mutateAsync({ id: '123', name: 'John' });
691
- expect(fetchMock).toHaveBeenCalledWith('https://example.com/api/updateUser', expect.objectContaining({
692
- method: 'POST',
693
- body: JSON.stringify({ id: '123', name: 'John' }),
694
- }));
695
- });
696
- it('allows custom HTTP methods via config', async () => {
697
- const { useMutation } = createHooks({
698
- baseUrl: 'https://example.com/api',
699
- fetchFn: fetchMock,
700
- config: {
701
- updateUser: { method: 'PATCH' },
702
- },
703
- });
704
- const { result } = renderHook(() => useMutation('updateUser'), {
705
- wrapper: createWrapper(),
706
- });
707
- await result.current.mutateAsync({ id: '123', name: 'John' });
708
- expect(fetchMock).toHaveBeenCalledWith('https://example.com/api/updateUser', expect.objectContaining({
709
- method: 'PATCH',
710
- body: JSON.stringify({ id: '123', name: 'John' }),
711
- }));
712
- });
713
- it('passes through mutation options', async () => {
714
- const onSuccess = vi.fn();
715
- const onError = vi.fn();
716
- const { useMutation } = createHooks({
717
- baseUrl: 'https://example.com/api',
718
- fetchFn: fetchMock,
719
- });
720
- const { result } = renderHook(() => useMutation('updateUser', { onSuccess, onError }), { wrapper: createWrapper() });
721
- await result.current.mutateAsync({ id: '123', name: 'John' });
722
- expect(onSuccess).toHaveBeenCalled();
723
- expect(onError).not.toHaveBeenCalled();
724
- });
725
- it('handles mutation errors', async () => {
726
- fetchMock.mockResolvedValue(mockErrorResponse(400, { error: 'Invalid data' }));
727
- const { useMutation } = createHooks({
728
- baseUrl: 'https://example.com/api',
729
- fetchFn: fetchMock,
730
- });
731
- const { result } = renderHook(() => useMutation('updateUser'), {
732
- wrapper: createWrapper(),
733
- });
734
- await expect(result.current.mutateAsync({ id: '123', name: 'John' })).rejects.toThrow();
735
- });
736
- });
737
- describe('useInfiniteQuery', () => {
738
- const fetchMock = vi.fn();
739
- beforeEach(() => {
740
- fetchMock.mockReset();
741
- });
742
- it('paginates using meta.pagination and advances the offset', async () => {
743
- fetchMock
744
- .mockResolvedValueOnce(mockSuccessResponse({
745
- data: [{ id: 'a' }],
746
- meta: { pagination: { limit: 1, offset: 0, hasMore: true } },
747
- }))
748
- .mockResolvedValueOnce(mockSuccessResponse({
749
- data: [{ id: 'b' }],
750
- meta: { pagination: { limit: 1, offset: 1, hasMore: false } },
751
- }));
752
- const { useInfiniteQuery } = createHooks({
753
- baseUrl: 'https://example.com/api',
754
- fetchFn: fetchMock,
755
- config: { listItems: { method: 'POST', path: '/datasets/items/query' } },
756
- });
757
- const { result } = renderHook(() => useInfiniteQuery('listItems', { tags: [], limit: 1 }), { wrapper: createWrapper() });
758
- await waitFor(() => expect(result.current.isSuccess).toBe(true));
759
- // First page requests offset 0 and opts into meta.
760
- expect(fetchMock.mock.calls[0][1]?.headers['x-include-meta']).toBe('true');
761
- expect(fetchMock.mock.calls[0][1]?.body).toBe(JSON.stringify({ tags: [], limit: 1, offset: 0 }));
762
- expect(result.current.hasNextPage).toBe(true);
763
- await act(async () => {
764
- await result.current.fetchNextPage();
765
- });
766
- await waitFor(() => expect(result.current.hasNextPage).toBe(false));
767
- // Second page advances offset by the page size.
768
- expect(fetchMock.mock.calls[1][1]?.body).toBe(JSON.stringify({ tags: [], limit: 1, offset: 1 }));
769
- expect(result.current.data?.pages).toHaveLength(2);
770
- });
771
- });
772
- });
@@ -1,2 +0,0 @@
1
- export {};
2
- //# sourceMappingURL=provider.test.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"provider.test.d.ts","sourceRoot":"","sources":["../src/provider.test.tsx"],"names":[],"mappings":""}
@@ -1,53 +0,0 @@
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
- });