@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.
- package/dist/createHooks.d.ts +9 -25
- package/dist/createHooks.d.ts.map +1 -1
- package/dist/createHooks.js +60 -105
- package/dist/createHooks.test.js +8 -0
- package/package.json +5 -1
- package/dist/errors.d.ts +0 -9
- package/dist/errors.js +0 -13
- package/dist/index.d.ts +0 -5
- package/dist/types.d.ts +0 -9
- package/dist/types.js +0 -1
package/dist/createHooks.d.ts
CHANGED
|
@@ -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 =
|
|
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
|
-
|
|
37
|
-
|
|
38
|
-
|
|
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,
|
|
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"}
|
package/dist/createHooks.js
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
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:
|
|
72
|
-
|
|
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
|
|
63
|
+
if (method === 'GET' && input && typeof input === 'object') {
|
|
89
64
|
const params = new URLSearchParams();
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
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,
|
|
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
|
-
|
|
135
|
-
|
|
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
|
-
|
|
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
|
|
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'),
|
package/dist/createHooks.test.js
CHANGED
|
@@ -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.
|
|
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 {};
|