@hypequery/react 0.0.1 → 0.0.3

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.
@@ -4,41 +4,25 @@ import { HttpError } from './errors.js';
4
4
  export interface QueryMethodConfig {
5
5
  method?: string;
6
6
  }
7
- export interface CreateHooksConfig<TApi = any> {
7
+ export interface CreateHooksConfig<TApi = Record<string, {
8
+ input: unknown;
9
+ output: unknown;
10
+ }>> {
8
11
  baseUrl: string;
9
12
  fetchFn?: typeof fetch;
10
13
  headers?: Record<string, string>;
11
14
  config?: Record<string, QueryMethodConfig>;
12
15
  api?: TApi;
13
16
  }
14
- /**
15
- * Symbol used to mark objects as query options (not input)
16
- * @internal
17
- */
18
17
  declare const OPTIONS_SYMBOL: unique symbol;
19
- /**
20
- * Helper to explicitly mark an object as query options
21
- * Prevents ambiguity when query input and options have overlapping properties
22
- *
23
- * @example
24
- * ```ts
25
- * // Without helper - ambiguous (could be input or options)
26
- * useQuery('getData', { enabled: true, staleTime: 5000 })
27
- *
28
- * // With helper - explicit
29
- * useQuery('getData', queryOptions({ enabled: true, staleTime: 5000 }))
30
- * ```
31
- */
32
18
  export declare function queryOptions<T extends object>(opts: T): T & {
33
19
  [OPTIONS_SYMBOL]: true;
34
20
  };
35
- export declare function createHooks<Api extends {
36
- [K in keyof Api]: {
37
- input: unknown;
38
- output: unknown;
39
- };
40
- }>(config: CreateHooksConfig): {
41
- readonly useQuery: <Name extends ExtractNames<Api>>(...args: QueryInput<Api, Name> extends unknown ? unknown extends QueryInput<Api, Name> ? [name: Name, options?: Omit<TanstackUseQueryOptions<QueryOutput<Api, Name>, HttpError, QueryOutput<Api, 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>, ["hypequery", Name, QueryInput<Api, Name>]>, "queryKey" | "queryFn"> | undefined] : [name: Name, input: QueryInput<Api, Name>, options?: Omit<TanstackUseQueryOptions<QueryOutput<Api, Name>, HttpError, QueryOutput<Api, Name>, ["hypequery", Name, QueryInput<Api, Name>]>, "queryKey" | "queryFn"> | undefined]) => UseQueryResult<QueryOutput<Api, Name>, HttpError>;
21
+ export declare function createHooks<Api extends Record<string, {
22
+ input: any;
23
+ output: any;
24
+ }>>(config: CreateHooksConfig<Api>): {
25
+ 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>;
42
26
  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>>;
43
27
  };
44
28
  export {};
@@ -1 +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,EAEV,YAAY,EACZ,UAAU,EACV,WAAW,EACZ,MAAM,YAAY,CAAC;AACpB,OAAO,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AAExC,MAAM,WAAW,iBAAiB;IAChC,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAQD,MAAM,WAAW,iBAAiB,CAAC,IAAI,GAAG,GAAG;IAC3C,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,CAAC,EAAE,OAAO,KAAK,CAAC;IACvB,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACjC,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,iBAAiB,CAAC,CAAC;IAC3C,GAAG,CAAC,EAAE,IAAI,CAAC;CACZ;AAED;;;GAGG;AACH,QAAA,MAAM,cAAc,eAAkC,CAAC;AAEvD;;;;;;;;;;;;GAYG;AACH,wBAAgB,YAAY,CAAC,CAAC,SAAS,MAAM,EAAE,IAAI,EAAE,CAAC,GAAG,CAAC,GAAG;IAAE,CAAC,cAAc,CAAC,EAAE,IAAI,CAAA;CAAE,CAEtF;AA4DD,wBAAgB,WAAW,CAAC,GAAG,SAAS;KACrC,CAAC,IAAI,MAAM,GAAG,GAAG;QAChB,KAAK,EAAE,OAAO,CAAC;QACf,MAAM,EAAE,OAAO,CAAC;KACjB;CACF,EAAE,MAAM,EAAE,iBAAiB;wBAiFR,IAAI,SAAS,YAAY,CAAC,GAAG,CAAC,kuBAE7C,cAAc,CAAC,WAAW,CAAC,GAAG,EAAE,IAAI,CAAC,EAAE,SAAS,CAAC;2BAoD/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"}
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;CACjB;AAED,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,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACjC,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,iBAAiB,CAAC,CAAC;IAC3C,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;AA6DD,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;wBAkEZ,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"}
@@ -1,33 +1,37 @@
1
1
  import { useQuery as useTanstackQuery, useMutation as useTanstackMutation, } from '@tanstack/react-query';
2
2
  import { HttpError } from './errors.js';
3
- /**
4
- * Symbol used to mark objects as query options (not input)
5
- * @internal
6
- */
7
3
  const OPTIONS_SYMBOL = Symbol.for('hypequery-options');
8
- /**
9
- * Helper to explicitly mark an object as query options
10
- * Prevents ambiguity when query input and options have overlapping properties
11
- *
12
- * @example
13
- * ```ts
14
- * // Without helper - ambiguous (could be input or options)
15
- * useQuery('getData', { enabled: true, staleTime: 5000 })
16
- *
17
- * // With helper - explicit
18
- * useQuery('getData', queryOptions({ enabled: true, staleTime: 5000 }))
19
- * ```
20
- */
21
4
  export function queryOptions(opts) {
22
5
  return { ...opts, [OPTIONS_SYMBOL]: true };
23
6
  }
24
- function buildUrl(baseUrl, name) {
7
+ const normalizeMethodConfig = (source) => {
8
+ if (!source)
9
+ return {};
10
+ return Object.fromEntries(Object.entries(source).map(([key, value]) => [key, { method: value.method ?? 'GET' }]));
11
+ };
12
+ const deriveMethodConfig = (api) => {
13
+ if (typeof api !== 'object' || api === null) {
14
+ return {};
15
+ }
16
+ if ('_routeConfig' in api &&
17
+ typeof api._routeConfig === 'object' &&
18
+ api._routeConfig !== null) {
19
+ return normalizeMethodConfig(api._routeConfig);
20
+ }
21
+ if ('queries' in api &&
22
+ typeof api.queries === 'object' &&
23
+ api.queries !== null) {
24
+ return normalizeMethodConfig(api.queries);
25
+ }
26
+ return {};
27
+ };
28
+ const buildUrl = (baseUrl, name) => {
25
29
  if (!baseUrl) {
26
30
  throw new Error('baseUrl is required');
27
31
  }
28
32
  return baseUrl.endsWith('/') ? `${baseUrl}${name}` : `${baseUrl}/${name}`;
29
- }
30
- async function parseResponse(res) {
33
+ };
34
+ const parseResponse = async (res) => {
31
35
  const text = await res.text();
32
36
  try {
33
37
  return JSON.parse(text);
@@ -35,69 +39,37 @@ async function parseResponse(res) {
35
39
  catch {
36
40
  return text;
37
41
  }
38
- }
39
- /**
40
- * Type guard to check if API object has route-level config
41
- */
42
- function hasRouteConfig(api) {
43
- return (typeof api === 'object' &&
44
- api !== null &&
45
- '_routeConfig' in api &&
46
- typeof api._routeConfig === 'object' &&
47
- api._routeConfig !== null);
48
- }
49
- /**
50
- * Type guard to check if API object has queries property
51
- */
52
- function hasQueries(api) {
53
- return (typeof api === 'object' &&
54
- api !== null &&
55
- 'queries' in api &&
56
- typeof api.queries === 'object' &&
57
- api.queries !== null);
58
- }
59
- /**
60
- * Extract method configuration from a source object
61
- * Transforms { key: { method: 'POST' } } into { key: { method: 'POST' } }
62
- * with GET as default
63
- */
64
- function extractMethodConfig(source) {
65
- return Object.fromEntries(Object.entries(source).map(([key, value]) => [
66
- key,
67
- { method: value.method || 'GET' },
68
- ]));
69
- }
42
+ };
43
+ const isOptionsBag = (value) => {
44
+ return Boolean(value && typeof value === 'object' && OPTIONS_SYMBOL in value);
45
+ };
46
+ const looksLikeQueryOptions = (value) => {
47
+ if (isOptionsBag(value))
48
+ return true;
49
+ if (typeof value !== 'object' || value === null)
50
+ return false;
51
+ const optionKeys = ['enabled', 'staleTime', 'gcTime', 'refetchInterval', 'refetchOnWindowFocus', 'retry', 'retryDelay'];
52
+ const matches = optionKeys.filter((key) => key in value).length;
53
+ return matches >= 2;
54
+ };
70
55
  export function createHooks(config) {
71
- const { baseUrl, fetchFn = fetch, headers = {}, config: methodConfig = {}, api } = config;
72
- // Auto-extract method config from api object if provided
73
- const extractedConfig = api
74
- ? hasRouteConfig(api)
75
- ? extractMethodConfig(api._routeConfig) // Prefer route-level config if available
76
- : hasQueries(api)
77
- ? extractMethodConfig(api.queries) // Fallback to endpoint method
78
- : {}
79
- : {};
80
- // Merge extracted config with explicit config (explicit takes precedence)
81
- const finalConfig = { ...extractedConfig, ...methodConfig };
56
+ const { baseUrl, fetchFn = fetch, headers = {}, config: explicitConfig = {}, api } = config;
57
+ const finalConfig = { ...deriveMethodConfig(api), ...explicitConfig };
82
58
  const fetchQuery = async (name, input, defaultMethod = 'GET') => {
83
59
  const url = buildUrl(baseUrl, name);
84
60
  const method = finalConfig[name]?.method ?? defaultMethod;
85
- // For GET requests, encode input as query params; for others, use JSON body
86
61
  let finalUrl = url;
87
62
  let body;
88
- if (method === 'GET' && input !== undefined && input !== null) {
63
+ if (method === 'GET' && input && typeof input === 'object') {
89
64
  const params = new URLSearchParams();
90
- if (typeof input === 'object') {
91
- for (const [key, value] of Object.entries(input)) {
92
- if (value !== undefined && value !== null) {
93
- // Handle arrays by appending multiple values with the same key
94
- if (Array.isArray(value)) {
95
- value.forEach(item => params.append(key, String(item)));
96
- }
97
- else {
98
- params.append(key, String(value));
99
- }
100
- }
65
+ for (const [key, value] of Object.entries(input)) {
66
+ if (value === undefined || value === null)
67
+ continue;
68
+ if (Array.isArray(value)) {
69
+ value.forEach((item) => params.append(key, String(item)));
70
+ }
71
+ else {
72
+ params.append(key, String(value));
101
73
  }
102
74
  }
103
75
  const queryString = params.toString();
@@ -121,50 +93,33 @@ export function createHooks(config) {
121
93
  return res.json();
122
94
  };
123
95
  function useQuery(...args) {
124
- const [name, inputOrOptions, maybeOptions] = args;
125
- // Parse arguments based on count and type
96
+ const [name, potentialInputOrOptions, maybeOptions] = args;
126
97
  let input;
127
98
  let options;
128
99
  if (args.length === 1) {
129
- // useQuery('queryName')
130
100
  input = undefined;
131
101
  options = undefined;
132
102
  }
133
- else if (args.length === 2) {
134
- // useQuery('queryName', input) or useQuery('queryName', options)
135
- if (isQueryOptions(inputOrOptions)) {
136
- input = undefined;
137
- options = inputOrOptions;
138
- }
139
- else {
140
- input = inputOrOptions;
141
- options = undefined;
142
- }
103
+ else if (args.length === 2 && looksLikeQueryOptions(potentialInputOrOptions)) {
104
+ input = undefined;
105
+ options = potentialInputOrOptions;
143
106
  }
144
107
  else {
145
- // useQuery('queryName', input, options)
146
- input = inputOrOptions;
108
+ input = potentialInputOrOptions;
147
109
  options = maybeOptions;
148
110
  }
111
+ const queryKey = (() => {
112
+ if (input === undefined) {
113
+ return ['hypequery', name];
114
+ }
115
+ return ['hypequery', name, input];
116
+ })();
149
117
  return useTanstackQuery({
150
- queryKey: ['hypequery', name, input],
118
+ queryKey,
151
119
  queryFn: () => fetchQuery(name, input),
152
120
  ...(options ?? {}),
153
121
  });
154
122
  }
155
- function isQueryOptions(value) {
156
- if (typeof value !== 'object' || value === null)
157
- return false;
158
- // Check for explicit symbol marker first (most reliable)
159
- if (OPTIONS_SYMBOL in value)
160
- return true;
161
- // Fallback to heuristic for backward compatibility
162
- // Check for known TanStack Query option keys
163
- // Must have at least 2 matches to avoid false positives with user input
164
- const optionKeys = ['enabled', 'staleTime', 'gcTime', 'refetchInterval', 'refetchOnWindowFocus', 'retry', 'retryDelay'];
165
- const matches = optionKeys.filter(key => key in value).length;
166
- return matches >= 2;
167
- }
168
123
  function useMutation(name, options) {
169
124
  return useTanstackMutation({
170
125
  mutationFn: (input) => fetchQuery(name, input, 'POST'),
@@ -59,6 +59,14 @@ describe('createHooks', () => {
59
59
  body: JSON.stringify({ force: true }),
60
60
  }));
61
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
+ });
62
70
  });
63
71
  describe('HTTP Method Handling', () => {
64
72
  const fetchMock = vi.fn();
package/package.json CHANGED
@@ -1,8 +1,9 @@
1
1
  {
2
2
  "name": "@hypequery/react",
3
- "version": "0.0.1",
3
+ "version": "0.0.3",
4
4
  "description": "React hooks for consuming hypequery APIs",
5
5
  "type": "module",
6
+ "main": "./dist/index.js",
6
7
  "exports": {
7
8
  ".": {
8
9
  "types": "./dist/index.d.ts",
@@ -30,6 +31,9 @@
30
31
  "typescript": "^5.7.3",
31
32
  "vitest": "^2.1.6"
32
33
  },
34
+ "publishConfig": {
35
+ "access": "public"
36
+ },
33
37
  "scripts": {
34
38
  "build": "tsc -b",
35
39
  "types": "tsc -b --noEmit",
package/dist/errors.d.ts DELETED
@@ -1,9 +0,0 @@
1
- /**
2
- * HTTP error with status code and response body
3
- */
4
- export declare class HttpError extends Error {
5
- readonly status: number;
6
- readonly body: unknown;
7
- constructor(message: string, status: number, body: unknown);
8
- }
9
- //# sourceMappingURL=errors.d.ts.map
package/dist/errors.js DELETED
@@ -1,13 +0,0 @@
1
- /**
2
- * HTTP error with status code and response body
3
- */
4
- export class HttpError extends Error {
5
- constructor(message, status, body) {
6
- super(message);
7
- this.status = status;
8
- this.body = body;
9
- this.name = 'HttpError';
10
- // Maintain proper prototype chain for instanceof checks
11
- Object.setPrototypeOf(this, HttpError.prototype);
12
- }
13
- }
package/dist/index.d.ts DELETED
@@ -1,5 +0,0 @@
1
- export { createHooks, queryOptions } from './createHooks.js';
2
- export { HttpError } from './errors.js';
3
- export type { QueryInput, QueryOutput, HttpMethod } from './types.js';
4
- export type { CreateHooksConfig, QueryMethodConfig } from './createHooks.js';
5
- //# sourceMappingURL=index.d.ts.map
package/dist/types.d.ts DELETED
@@ -1,9 +0,0 @@
1
- export type HttpMethod = "GET" | "POST" | "PUT" | "PATCH" | "DELETE";
2
- export type ExtractNames<Api> = Extract<keyof Api, string>;
3
- export type QueryInput<Api, Name extends ExtractNames<Api>> = Api[Name] extends {
4
- input: infer Input;
5
- } ? Input : never;
6
- export type QueryOutput<Api, Name extends ExtractNames<Api>> = Api[Name] extends {
7
- output: infer Output;
8
- } ? Output : never;
9
- //# sourceMappingURL=types.d.ts.map
package/dist/types.js DELETED
@@ -1 +0,0 @@
1
- export {};