@hypequery/react 0.0.0-canary-20260611154523

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.
@@ -0,0 +1,49 @@
1
+ import { type UseQueryOptions as TanstackUseQueryOptions, type UseMutationOptions as TanstackUseMutationOptions, type UseMutationResult, type UseQueryResult } from '@tanstack/react-query';
2
+ import type { ExtractNames, QueryInput, QueryOutput } from './types.js';
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);
21
+ export interface CreateHooksConfig<TApi = Record<string, {
22
+ input: unknown;
23
+ output: unknown;
24
+ }>> {
25
+ baseUrl: string;
26
+ fetchFn?: typeof fetch;
27
+ headers?: HeadersInput;
28
+ config?: Record<string, QueryMethodConfig>;
29
+ /**
30
+ * Route manifest from `@hypequery/serve`'s `api.manifest()`. Resolves the
31
+ * method and full path for each key — required for metric/dataset endpoints,
32
+ * which are POST routes whose paths differ from their map keys.
33
+ */
34
+ manifest?: RouteManifest;
35
+ api?: TApi;
36
+ }
37
+ declare const OPTIONS_SYMBOL: unique symbol;
38
+ export declare function queryOptions<T extends object>(opts: T): T & {
39
+ [OPTIONS_SYMBOL]: true;
40
+ };
41
+ export declare function createHooks<Api extends Record<string, {
42
+ input: any;
43
+ output: any;
44
+ }>>(config: CreateHooksConfig<Api>): {
45
+ 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>;
46
+ 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>>;
47
+ };
48
+ export {};
49
+ //# sourceMappingURL=createHooks.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"createHooks.d.ts","sourceRoot":"","sources":["../src/createHooks.tsx"],"names":[],"mappings":"AAAA,OAAO,EAGL,KAAK,eAAe,IAAI,uBAAuB,EAC/C,KAAK,kBAAkB,IAAI,0BAA0B,EACrD,KAAK,iBAAiB,EACtB,KAAK,cAAc,EACpB,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;AAExC,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,GAAG,SAAS,GAAG,CAAC,MAAM,SAAS,CAAC,CAAC;AAElD,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,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,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;wBAoFZ,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;EAW/E"}
@@ -0,0 +1,177 @@
1
+ import { useQuery as useTanstackQuery, useMutation as useTanstackMutation, } from '@tanstack/react-query';
2
+ import { HttpError } from './errors.js';
3
+ const OPTIONS_SYMBOL = Symbol.for('hypequery-options');
4
+ export function queryOptions(opts) {
5
+ return { ...opts, [OPTIONS_SYMBOL]: true };
6
+ }
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
+ const isOptionsBag = (value) => {
68
+ return Boolean(value && typeof value === 'object' && OPTIONS_SYMBOL in value);
69
+ };
70
+ const looksLikeQueryOptions = (value) => {
71
+ if (isOptionsBag(value))
72
+ return true;
73
+ if (typeof value !== 'object' || value === null)
74
+ return false;
75
+ const optionKeys = ['enabled', 'staleTime', 'gcTime', 'refetchInterval', 'refetchOnWindowFocus', 'retry', 'retryDelay'];
76
+ const matches = optionKeys.filter((key) => key in value).length;
77
+ return matches >= 2;
78
+ };
79
+ const resolveHeaders = (headers) => {
80
+ if (!headers)
81
+ return {};
82
+ const raw = typeof headers === 'function' ? headers() : headers;
83
+ return Object.fromEntries(Object.entries(raw).filter(([, value]) => value !== undefined));
84
+ };
85
+ export function createHooks(config) {
86
+ const { baseUrl, fetchFn = fetch, headers = {}, config: explicitConfig = {}, manifest, 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') => {
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().`);
101
+ }
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 resolvedHeaders = resolveHeaders(headers);
125
+ const res = await fetchFn(finalUrl, {
126
+ method,
127
+ headers: {
128
+ ...resolvedHeaders,
129
+ ...(body ? { 'content-type': 'application/json' } : {}),
130
+ },
131
+ body,
132
+ });
133
+ if (!res.ok) {
134
+ const errorBody = await parseResponse(res);
135
+ throw new HttpError(`${method} request to ${finalUrl} failed with status ${res.status}`, res.status, errorBody);
136
+ }
137
+ return res.json();
138
+ };
139
+ function useQuery(...args) {
140
+ const [name, potentialInputOrOptions, maybeOptions] = args;
141
+ let input;
142
+ let options;
143
+ if (args.length === 1) {
144
+ input = undefined;
145
+ options = undefined;
146
+ }
147
+ else if (args.length === 2 && looksLikeQueryOptions(potentialInputOrOptions)) {
148
+ input = undefined;
149
+ options = potentialInputOrOptions;
150
+ }
151
+ else {
152
+ input = potentialInputOrOptions;
153
+ options = maybeOptions;
154
+ }
155
+ const queryKey = (() => {
156
+ if (input === undefined) {
157
+ return ['hypequery', name];
158
+ }
159
+ return ['hypequery', name, input];
160
+ })();
161
+ return useTanstackQuery({
162
+ queryKey,
163
+ queryFn: () => fetchQuery(name, input),
164
+ ...(options ?? {}),
165
+ });
166
+ }
167
+ function useMutation(name, options) {
168
+ return useTanstackMutation({
169
+ mutationFn: (input) => fetchQuery(name, input, 'POST'),
170
+ ...options,
171
+ });
172
+ }
173
+ return {
174
+ useQuery,
175
+ useMutation,
176
+ };
177
+ }
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=createHooks.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"createHooks.test.d.ts","sourceRoot":"","sources":["../src/createHooks.test.tsx"],"names":[],"mappings":""}