@robohall/react-query-factory 1.0.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Robert Hall
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,232 @@
1
+ # react-query-factory
2
+
3
+ TanStack Query is very good at caching. It is less good at deciding what your query keys should be, or at fetching every page of a cursor-paginated endpoint before your component has to think about it.
4
+
5
+ This library wraps query definitions in a small factory function that handles composable key namespacing, automatic multi-page crawling, and `useInfiniteQuery` generation — so the part of your codebase that knows *what* to fetch stays separate from the part that knows *how React renders it*.
6
+
7
+ It has zero runtime dependencies — all TanStack Query imports are type-only and erased at compile time.
8
+
9
+ ---
10
+
11
+ ## Installation
12
+
13
+ ```bash
14
+ npm install react-query-factory
15
+ # peer dependency: @tanstack/react-query >= 5.0.0
16
+ ```
17
+
18
+ ---
19
+
20
+ ## Quick start
21
+
22
+ Define a factory once, call it in any component:
23
+
24
+ ```typescript
25
+ import {
26
+ EC2Client,
27
+ DescribeInstancesCommand,
28
+ type DescribeInstancesCommandInput,
29
+ } from '@aws-sdk/client-ec2';
30
+ import { queryFactory } from 'react-query-factory';
31
+ import { useQuery } from '@tanstack/react-query';
32
+
33
+ const ec2 = new EC2Client({ region: 'us-east-1' });
34
+
35
+ const describeInstances = queryFactory({
36
+ queryKey: ['ec2:DescribeInstances'],
37
+ queryFn: (params: DescribeInstancesCommandInput, ctx) =>
38
+ ec2.send(new DescribeInstancesCommand(params), { abortSignal: ctx.signal }),
39
+ staleTime: 30_000,
40
+ });
41
+
42
+ function InstanceList() {
43
+ const { data } = useQuery(
44
+ describeInstances({ Filters: [{ Name: 'instance-state-name', Values: ['running'] }] })
45
+ );
46
+ // query key: ['ec2:DescribeInstances', { Filters: [...] }]
47
+ // staleTime: 30 000 ms, no repetition required
48
+ }
49
+ ```
50
+
51
+ `describeInstances({ ... })` returns a plain object — `{ queryKey, queryFn, staleTime, … }` — that you spread or pass directly to `useQuery`. The factory does not touch your query client.
52
+
53
+ ---
54
+
55
+ ## Crawling
56
+
57
+ `DescribeInstances` is paginated. If you have more than 1000 instances, one call won't get them all. The standard approach — chaining `fetchNextPage` calls, accumulating results, checking `NextToken` — is correct but tedious to repeat everywhere.
58
+
59
+ Add `getNextPageParam`, `initialPageParam`, and `reduce` to your factory config, and the generated `queryFn` will walk every page automatically, reducing them into a single flat array:
60
+
61
+ ```typescript
62
+ import type { Instance, DescribeInstancesCommandInput } from '@aws-sdk/client-ec2';
63
+
64
+ const describeInstances = queryFactory({
65
+ queryKey: ['ec2:DescribeInstances'],
66
+ queryFn: (params: DescribeInstancesCommandInput, ctx) =>
67
+ ec2.send(
68
+ new DescribeInstancesCommand({ ...params, NextToken: ctx.pageParam }),
69
+ { abortSignal: ctx.signal },
70
+ ),
71
+ getNextPageParam: response => response.NextToken,
72
+ initialPageParam: undefined as string | undefined,
73
+ reduce: (acc, page): Instance[] => [
74
+ ...(acc ?? []),
75
+ ...(page.Reservations?.flatMap(r => r.Instances ?? []) ?? []),
76
+ ],
77
+ staleTime: 30_000,
78
+ });
79
+
80
+ function InstanceList() {
81
+ // one useQuery call; data is Instance[], not DescribeInstancesResponse[]
82
+ const { data } = useQuery(describeInstances({ MaxResults: 1000 }));
83
+ }
84
+ ```
85
+
86
+ **`shouldFetchNextPage`** lets a call site stop the crawl early based on what has been accumulated so far. The arguments are the current reduced value and a `crawlOptions` object passed at call time:
87
+
88
+ ```typescript
89
+ const describeInstances = queryFactory({
90
+ // ...
91
+ shouldFetchNextPage: (instances, opts: { minResults?: number }) =>
92
+ opts.minResults != null && (instances?.length ?? 0) < opts.minResults,
93
+ });
94
+
95
+ // stop after accumulating at least 50 instances
96
+ const { data } = useQuery(
97
+ describeInstances({ MaxResults: 1000 }, { minResults: 50 })
98
+ );
99
+ ```
100
+
101
+ `crawlOptions` is appended to the query key, so `describeInstances({}, { minResults: 50 })` and `describeInstances({}, { minResults: 200 })` are separate cache entries — they crawl independently and never collide.
102
+
103
+ ---
104
+
105
+ ## Factory composition
106
+
107
+ A factory can inherit from another factory. The child's query key is appended to the parent's, standard options are shallow-merged, and the `queryFn` and crawling config can be inherited or replaced.
108
+
109
+ **Inherit the queryFn, add a `select` transform:**
110
+
111
+ ```typescript
112
+ const runningInstances = queryFactory(describeInstances, {
113
+ select: instances => instances.filter(i => i.State?.Name === 'running'),
114
+ });
115
+
116
+ // query key: ['ec2:DescribeInstances', { MaxResults: 1000 }] (same cache entry as parent)
117
+ // data: Instance[] filtered to State.Name === 'running'
118
+ const { data } = useQuery(runningInstances({ MaxResults: 1000 }));
119
+ ```
120
+
121
+ Parent and child `select` functions compose automatically — if the parent already has a `select`, the child's `select` receives the parent's output, not the raw API response.
122
+
123
+ **Add a new queryFn under the parent's namespace:**
124
+
125
+ ```typescript
126
+ const findInstance = queryFactory(describeInstances, {
127
+ queryKey: ['find'],
128
+ // queryFn, getNextPageParam, initialPageParam, and reduce are all inherited
129
+ shouldFetchNextPage: (instances, opts: { instanceId?: string }) =>
130
+ opts.instanceId != null &&
131
+ !instances?.some(i => i.InstanceId === opts.instanceId),
132
+ });
133
+
134
+ // query key: ['ec2:DescribeInstances', 'find', { MaxResults: 100 }, { instanceId: 'i-0abc123' }]
135
+ // crawls pages until the target instance appears, then stops
136
+ const { data } = useQuery(
137
+ findInstance({ MaxResults: 100 }, { instanceId: 'i-0abc123def456' })
138
+ );
139
+ ```
140
+
141
+ Because `findInstance`'s key is nested under `['ec2:DescribeInstances']`, a single invalidation call busts the parent and all children:
142
+
143
+ ```typescript
144
+ // after a runInstances/terminateInstances mutation — invalidates everything in the namespace.
145
+ // Calling the factory with no args produces just the namespace key; TanStack prefix-matches it
146
+ // against all entries, so describeInstances, runningInstances, and findInstance are all busted.
147
+ await queryClient.invalidateQueries(describeInstances());
148
+ ```
149
+
150
+ ---
151
+
152
+ ## Infinite queries
153
+
154
+ Every factory exposes a `.infinite()` method that returns `useInfiniteQuery`-compatible options. If the factory has `reduce` configured, each virtual page is itself a crawl — TanStack loads pages one at a time, but each "page load" makes multiple API calls and reduces them before handing the result back:
155
+
156
+ ```typescript
157
+ const { data, fetchNextPage, hasNextPage } = useInfiniteQuery(
158
+ // load 50 instances per UI page, each backed by up to 5 DescribeInstances calls
159
+ describeInstances.infinite({ MaxResults: 10 }, { minResults: 50 })
160
+ );
161
+
162
+ // data.pages is Instance[][], one array per virtual page
163
+ ```
164
+
165
+ The `.infinite()` key includes an `'infinite'` segment to keep it separate from the regular `useQuery` cache entry:
166
+ - `describeInstances({ MaxResults: 10 })` → `['ec2:DescribeInstances', { MaxResults: 10 }]`
167
+ - `describeInstances.infinite({ MaxResults: 10 })` → `['ec2:DescribeInstances', 'infinite', { MaxResults: 10 }]`
168
+
169
+ ---
170
+
171
+ ## Public API
172
+
173
+ ### `queryFactory(config)`
174
+
175
+ Creates a standalone factory.
176
+
177
+ ```typescript
178
+ queryFactory<TParams, TData, TError, TSelected, TPageParam, TCrawlOptions>(
179
+ config: QueryFactoryConfig<...>
180
+ ): QueryFactory<...>
181
+ ```
182
+
183
+ ### `queryFactory(parent, config)`
184
+
185
+ Creates a child factory. Two overloads:
186
+ - **With a new `queryFn`** — inherits key namespace and standard options; crawling config must be re-declared if needed.
187
+ - **Without a `queryFn`** — inherits everything; accepts only `queryKey`, `select`, and standard options. `select` is composed with the parent's.
188
+
189
+ ### `QueryFactoryConfig`
190
+
191
+ All fields except `reduce` and `shouldFetchNextPage` are the standard TanStack Query API — the same types and semantics you'd pass to `useQuery` or `useInfiniteQuery`. The factory doesn't reinvent them; it just requires certain combinations to be present in order to activate crawling.
192
+
193
+ | Field | Type | Notes |
194
+ |---|---|---|
195
+ | `queryKey` | `QueryKey` | Namespace segments. Params are appended at call time. |
196
+ | `queryFn` | `(params: TParams, ctx: QueryFunctionContext) => TData \| Promise<TData>` | Same as TanStack, with an extra leading `params` argument. |
197
+ | `select` | `(data: TData) => TSelected` | Exact TanStack API. Composed automatically on child factories. |
198
+ | `getNextPageParam` | `GetNextPageParamFunction<TPageParam, TData>` | Exact TanStack API. Providing this together with `initialPageParam` and `reduce` activates crawling. |
199
+ | `initialPageParam` | `TPageParam` | Exact TanStack API. Required alongside `getNextPageParam` to enable crawling. |
200
+ | `getPreviousPageParam` | `GetPreviousPageParamFunction<TPageParam, TData>` | Exact TanStack API. Passed through on `.infinite()`. |
201
+ | `reduce` | `(acc: TSelected \| undefined, page: TData) => TSelected` | Library addition. Folds crawled pages into a single value; required for crawling and `.infinite()` crawling. |
202
+ | `shouldFetchNextPage` | `(combined: TSelected \| undefined, crawlOptions: TCrawlOptions) => boolean` | Library addition. Return `false` to stop the crawl early based on accumulated results. |
203
+ | + all `StandardQueryOptions` fields | | `staleTime`, `gcTime`, `retry`, `enabled`, `refetchOnWindowFocus`, etc. |
204
+
205
+ ### `QueryFactory<TParams, TData, TError, TSelected, TPageParam, TCrawlOptions>`
206
+
207
+ The callable factory returned by `queryFactory()`.
208
+
209
+ ```typescript
210
+ factory(params: TParams, crawlOptions?: TCrawlOptions): ResolvedQueryOptions // → useQuery()
211
+ factory.infinite(params, crawlOptions?) : ResolvedInfiniteOptions // → useInfiniteQuery()
212
+ ```
213
+
214
+ ### `ResolvedQueryOptions`
215
+
216
+ Return type of `factory(params)`. Pass directly to `useQuery()`. Contains an `initialPageParam?: never` field that prevents accidental use with `useInfiniteQuery`.
217
+
218
+ ### `ResolvedInfiniteOptions`
219
+
220
+ Return type of `factory.infinite(params)`. Pass directly to `useInfiniteQuery()`. The `select` field is typed to `InfiniteData<TData, TPageParam>`, which prevents accidental use with `useQuery`.
221
+
222
+ ---
223
+
224
+ ## Running the sandbox
225
+
226
+ The sandbox contains five interactive demos using a mock paginated API: basic single-page fetch, full crawl, factory composition, infinite query with per-page crawling, and a target-search that stops the crawl early.
227
+
228
+ ```bash
229
+ npm run sandbox
230
+ ```
231
+
232
+ This starts a Vite dev server. Navigate to the URL it prints (typically `http://localhost:5173`).
@@ -0,0 +1,149 @@
1
+ import { QueryKey, NotifyOnChangeProps, QueryMeta, QueryFunctionContext, InfiniteData, GetNextPageParamFunction, GetPreviousPageParamFunction } from '@tanstack/react-query';
2
+
3
+ /** Subset of TanStack Query options that apply to both regular and infinite queries. */
4
+ interface StandardQueryOptions<TError = Error> {
5
+ enabled?: boolean;
6
+ staleTime?: number | ((query: {
7
+ queryKey: QueryKey;
8
+ }) => number);
9
+ gcTime?: number;
10
+ retry?: boolean | number | ((failureCount: number, error: TError) => boolean);
11
+ retryDelay?: number | ((retryAttempt: number, error: TError) => number);
12
+ refetchOnWindowFocus?: boolean | 'always';
13
+ refetchOnReconnect?: boolean | 'always';
14
+ refetchOnMount?: boolean | 'always';
15
+ refetchInterval?: number | false | ((query: {
16
+ queryKey: QueryKey;
17
+ }) => number | false);
18
+ refetchIntervalInBackground?: boolean;
19
+ networkMode?: 'online' | 'always' | 'offlineFirst';
20
+ notifyOnChangeProps?: NotifyOnChangeProps;
21
+ throwOnError?: boolean | ((error: TError) => boolean);
22
+ meta?: QueryMeta;
23
+ structuralSharing?: boolean;
24
+ }
25
+ /**
26
+ * Configuration passed to `queryFactory()`.
27
+ *
28
+ * - Set only `queryKey` + `queryFn` for a basic single-page query.
29
+ * - Add `getNextPageParam`, `initialPageParam`, and `reduce` to enable automatic
30
+ * pagination: the generated `queryFn` crawls all pages and reduces them to a
31
+ * single `TSelected` value.
32
+ * - Call `factory.infinite(params)` to get a `useInfiniteQuery`-compatible config
33
+ * where each virtual page is itself a crawl.
34
+ */
35
+ interface QueryFactoryConfig<TParams = void, TData = unknown, TError = Error, TSelected = TData, TPageParam = unknown, TCrawlOptions extends Record<string, unknown> = Record<string, unknown>> extends StandardQueryOptions<TError> {
36
+ /** Namespace segments. Params are appended as the final element at call time,
37
+ * giving a full key of [...namespace, 'infinite'?, params, crawlOptions?]. */
38
+ queryKey: QueryKey;
39
+ queryFn?: (params: TParams, context: QueryFunctionContext<QueryKey, TPageParam>) => TData | Promise<TData>;
40
+ select?: (data: TData) => TSelected;
41
+ /** TanStack v5 generic order: GetNextPageParamFunction<TPageParam, TData> */
42
+ getNextPageParam?: GetNextPageParamFunction<TPageParam, TData>;
43
+ getPreviousPageParam?: GetPreviousPageParamFunction<TPageParam, TData>;
44
+ initialPageParam?: TPageParam;
45
+ /** Called after each page to decide whether to keep crawling.
46
+ * Receives the current combined result and the crawl options from the call site. */
47
+ shouldFetchNextPage?: (combined: TSelected | undefined, crawlOptions: TCrawlOptions) => boolean;
48
+ /** Reduces crawled pages incrementally into the final query result.
49
+ * Called once per page; accumulator is undefined on the first call.
50
+ * When set, enables crawling on both the regular and .infinite variants. */
51
+ reduce?: (accumulator: TSelected | undefined, page: TData) => TSelected;
52
+ }
53
+ /**
54
+ * What `factory(params)` returns — pass directly to `useQuery()`.
55
+ *
56
+ * The `initialPageParam?: never` field is a structural guard that makes this
57
+ * type incompatible with `useInfiniteQuery`, preventing accidental misuse.
58
+ */
59
+ type ResolvedQueryOptions<TData = unknown, TError = Error, TSelected = TData> = StandardQueryOptions<TError> & {
60
+ queryKey: QueryKey;
61
+ queryFn?: (context: QueryFunctionContext) => TData | Promise<TData>;
62
+ select?: (data: TData) => TSelected;
63
+ /** Structural guard: makes this type incompatible with useInfiniteQuery, which requires initialPageParam. */
64
+ initialPageParam?: never;
65
+ };
66
+ /**
67
+ * What `factory.infinite(params)` returns — pass directly to `useInfiniteQuery()`.
68
+ *
69
+ * The `select` field expects `InfiniteData<TData, TPageParam>`, which is a structural
70
+ * guard making this type incompatible with `useQuery`.
71
+ */
72
+ type ResolvedInfiniteOptions<TData = unknown, TError = Error, TPageParam = unknown> = StandardQueryOptions<TError> & {
73
+ queryKey: QueryKey;
74
+ queryFn?: (context: QueryFunctionContext<QueryKey, TPageParam>) => TData | Promise<TData>;
75
+ /** Structural guard: the InfiniteData parameter type makes this incompatible with useQuery,
76
+ * whose select expects (data: TData) rather than (data: InfiniteData<TData, TPageParam>). */
77
+ select?: (data: InfiniteData<TData, TPageParam>) => any;
78
+ /** Required so this type satisfies useInfiniteQuery, which requires getNextPageParam. */
79
+ getNextPageParam: GetNextPageParamFunction<TPageParam, TData>;
80
+ getPreviousPageParam?: GetPreviousPageParamFunction<TPageParam, TData>;
81
+ /** Required so this type satisfies useInfiniteQuery, which requires initialPageParam. */
82
+ initialPageParam: TPageParam;
83
+ };
84
+ /**
85
+ * A callable factory produced by `queryFactory()`.
86
+ *
87
+ * - `factory(params)` → `ResolvedQueryOptions` for `useQuery()`
88
+ * - `factory.infinite(params)` → `ResolvedInfiniteOptions` for `useInfiniteQuery()`
89
+ *
90
+ * Both signatures accept an optional `crawlOptions` object that is appended to
91
+ * the query key and passed to `shouldFetchNextPage` so different call sites can
92
+ * control crawl behavior independently.
93
+ *
94
+ * `params` is always optional. Calling with no arguments produces just the
95
+ * namespace key, which is useful for broad cache invalidation:
96
+ * `queryClient.invalidateQueries(factory())`
97
+ */
98
+ interface QueryFactory<TParams = void, TData = unknown, TError = Error, TSelected = TData, TPageParam = unknown, TCrawlOptions extends Record<string, unknown> = Record<string, unknown>> {
99
+ (params?: TParams, crawlOptions?: TCrawlOptions): ResolvedQueryOptions<TData, TError, TSelected>;
100
+ infinite(params?: TParams, crawlOptions?: TCrawlOptions): ResolvedInfiniteOptions<TData, TError, TPageParam>;
101
+ }
102
+ /**
103
+ * Creates a standalone query factory from a config object.
104
+ *
105
+ * @example
106
+ * const usersFactory = queryFactory({
107
+ * queryKey: ['users'],
108
+ * queryFn: (params: { page: number }, ctx) => fetchUsers(params, ctx.signal),
109
+ * });
110
+ * // useQuery(usersFactory({ page: 1 }))
111
+ */
112
+ declare function queryFactory<TParams = void, TData = unknown, TError = Error, TSelected = TData, TPageParam = unknown, TCrawlOptions extends Record<string, unknown> = Record<string, unknown>>(config: QueryFactoryConfig<TParams, TData, TError, TSelected, TPageParam, TCrawlOptions>): QueryFactory<TParams, TData, TError, TSelected, TPageParam, TCrawlOptions>;
113
+ /**
114
+ * Creates a child factory that inherits the query key and standard options from
115
+ * `parent` and introduces a new `queryFn`. The child's query key is appended to
116
+ * the parent's, and standard options are shallow-merged (child wins).
117
+ *
118
+ * Use this overload when the child fetches different data than the parent.
119
+ */
120
+ declare function queryFactory<TChildParams extends TParentParams, TData = unknown, TError = Error, TChildSelected = TData, TParentParams = TChildParams, TPageParam = unknown, TCrawlOptions extends Record<string, unknown> = Record<string, unknown>>(parent: QueryFactory<TParentParams, any, any, any, any, any>, config: Omit<QueryFactoryConfig<TChildParams, TData, TError, TChildSelected, TPageParam, TCrawlOptions>, 'queryKey' | 'getNextPageParam' | 'getPreviousPageParam' | 'initialPageParam' | 'shouldFetchNextPage' | 'reduce'> & {
121
+ queryKey?: QueryKey;
122
+ queryFn: NonNullable<QueryFactoryConfig<TChildParams, TData, TError, TChildSelected, TPageParam>['queryFn']>;
123
+ } & ({
124
+ getNextPageParam: GetNextPageParamFunction<TPageParam, TData>;
125
+ initialPageParam: TPageParam;
126
+ getPreviousPageParam?: GetPreviousPageParamFunction<TPageParam, TData>;
127
+ shouldFetchNextPage?: (combined: TChildSelected | undefined, crawlOptions: TCrawlOptions) => boolean;
128
+ reduce?: (accumulator: TChildSelected | undefined, page: TData) => TChildSelected;
129
+ } | {
130
+ getNextPageParam?: never;
131
+ initialPageParam?: never;
132
+ getPreviousPageParam?: never;
133
+ shouldFetchNextPage?: never;
134
+ reduce?: never;
135
+ })): QueryFactory<TChildParams, TData, TError, TChildSelected, TPageParam, TCrawlOptions>;
136
+ /**
137
+ * Creates a child factory that reuses the parent's `queryFn` and pagination
138
+ * config. Useful for adding a `select` transform, narrowing params, or
139
+ * overriding `shouldFetchNextPage` with a different crawl-options shape —
140
+ * without changing what data is fetched. Parent and child `select` functions
141
+ * are automatically composed: `child.select(parent.select(data))`.
142
+ */
143
+ declare function queryFactory<TChildParams extends TParentParams, TData = unknown, TError = Error, TParentSelected = TData, TChildSelected = TParentSelected, TParentParams = TChildParams, TPageParam = unknown, TParentCrawlOptions extends Record<string, unknown> = Record<string, unknown>, TChildCrawlOptions extends Record<string, unknown> = TParentCrawlOptions>(parent: QueryFactory<TParentParams, TData, any, TParentSelected, TPageParam, TParentCrawlOptions>, config: Omit<QueryFactoryConfig<TChildParams, TData, TError, TChildSelected, TPageParam, TChildCrawlOptions>, 'queryKey' | 'queryFn' | 'select'> & {
144
+ queryKey?: QueryKey;
145
+ queryFn?: never;
146
+ select?: (data: TParentSelected) => TChildSelected;
147
+ }): QueryFactory<TChildParams, TData, TError, TChildSelected, TPageParam, TChildCrawlOptions>;
148
+
149
+ export { type QueryFactory, type QueryFactoryConfig, type ResolvedInfiniteOptions, type ResolvedQueryOptions, type StandardQueryOptions, queryFactory };
@@ -0,0 +1,149 @@
1
+ import { QueryKey, NotifyOnChangeProps, QueryMeta, QueryFunctionContext, InfiniteData, GetNextPageParamFunction, GetPreviousPageParamFunction } from '@tanstack/react-query';
2
+
3
+ /** Subset of TanStack Query options that apply to both regular and infinite queries. */
4
+ interface StandardQueryOptions<TError = Error> {
5
+ enabled?: boolean;
6
+ staleTime?: number | ((query: {
7
+ queryKey: QueryKey;
8
+ }) => number);
9
+ gcTime?: number;
10
+ retry?: boolean | number | ((failureCount: number, error: TError) => boolean);
11
+ retryDelay?: number | ((retryAttempt: number, error: TError) => number);
12
+ refetchOnWindowFocus?: boolean | 'always';
13
+ refetchOnReconnect?: boolean | 'always';
14
+ refetchOnMount?: boolean | 'always';
15
+ refetchInterval?: number | false | ((query: {
16
+ queryKey: QueryKey;
17
+ }) => number | false);
18
+ refetchIntervalInBackground?: boolean;
19
+ networkMode?: 'online' | 'always' | 'offlineFirst';
20
+ notifyOnChangeProps?: NotifyOnChangeProps;
21
+ throwOnError?: boolean | ((error: TError) => boolean);
22
+ meta?: QueryMeta;
23
+ structuralSharing?: boolean;
24
+ }
25
+ /**
26
+ * Configuration passed to `queryFactory()`.
27
+ *
28
+ * - Set only `queryKey` + `queryFn` for a basic single-page query.
29
+ * - Add `getNextPageParam`, `initialPageParam`, and `reduce` to enable automatic
30
+ * pagination: the generated `queryFn` crawls all pages and reduces them to a
31
+ * single `TSelected` value.
32
+ * - Call `factory.infinite(params)` to get a `useInfiniteQuery`-compatible config
33
+ * where each virtual page is itself a crawl.
34
+ */
35
+ interface QueryFactoryConfig<TParams = void, TData = unknown, TError = Error, TSelected = TData, TPageParam = unknown, TCrawlOptions extends Record<string, unknown> = Record<string, unknown>> extends StandardQueryOptions<TError> {
36
+ /** Namespace segments. Params are appended as the final element at call time,
37
+ * giving a full key of [...namespace, 'infinite'?, params, crawlOptions?]. */
38
+ queryKey: QueryKey;
39
+ queryFn?: (params: TParams, context: QueryFunctionContext<QueryKey, TPageParam>) => TData | Promise<TData>;
40
+ select?: (data: TData) => TSelected;
41
+ /** TanStack v5 generic order: GetNextPageParamFunction<TPageParam, TData> */
42
+ getNextPageParam?: GetNextPageParamFunction<TPageParam, TData>;
43
+ getPreviousPageParam?: GetPreviousPageParamFunction<TPageParam, TData>;
44
+ initialPageParam?: TPageParam;
45
+ /** Called after each page to decide whether to keep crawling.
46
+ * Receives the current combined result and the crawl options from the call site. */
47
+ shouldFetchNextPage?: (combined: TSelected | undefined, crawlOptions: TCrawlOptions) => boolean;
48
+ /** Reduces crawled pages incrementally into the final query result.
49
+ * Called once per page; accumulator is undefined on the first call.
50
+ * When set, enables crawling on both the regular and .infinite variants. */
51
+ reduce?: (accumulator: TSelected | undefined, page: TData) => TSelected;
52
+ }
53
+ /**
54
+ * What `factory(params)` returns — pass directly to `useQuery()`.
55
+ *
56
+ * The `initialPageParam?: never` field is a structural guard that makes this
57
+ * type incompatible with `useInfiniteQuery`, preventing accidental misuse.
58
+ */
59
+ type ResolvedQueryOptions<TData = unknown, TError = Error, TSelected = TData> = StandardQueryOptions<TError> & {
60
+ queryKey: QueryKey;
61
+ queryFn?: (context: QueryFunctionContext) => TData | Promise<TData>;
62
+ select?: (data: TData) => TSelected;
63
+ /** Structural guard: makes this type incompatible with useInfiniteQuery, which requires initialPageParam. */
64
+ initialPageParam?: never;
65
+ };
66
+ /**
67
+ * What `factory.infinite(params)` returns — pass directly to `useInfiniteQuery()`.
68
+ *
69
+ * The `select` field expects `InfiniteData<TData, TPageParam>`, which is a structural
70
+ * guard making this type incompatible with `useQuery`.
71
+ */
72
+ type ResolvedInfiniteOptions<TData = unknown, TError = Error, TPageParam = unknown> = StandardQueryOptions<TError> & {
73
+ queryKey: QueryKey;
74
+ queryFn?: (context: QueryFunctionContext<QueryKey, TPageParam>) => TData | Promise<TData>;
75
+ /** Structural guard: the InfiniteData parameter type makes this incompatible with useQuery,
76
+ * whose select expects (data: TData) rather than (data: InfiniteData<TData, TPageParam>). */
77
+ select?: (data: InfiniteData<TData, TPageParam>) => any;
78
+ /** Required so this type satisfies useInfiniteQuery, which requires getNextPageParam. */
79
+ getNextPageParam: GetNextPageParamFunction<TPageParam, TData>;
80
+ getPreviousPageParam?: GetPreviousPageParamFunction<TPageParam, TData>;
81
+ /** Required so this type satisfies useInfiniteQuery, which requires initialPageParam. */
82
+ initialPageParam: TPageParam;
83
+ };
84
+ /**
85
+ * A callable factory produced by `queryFactory()`.
86
+ *
87
+ * - `factory(params)` → `ResolvedQueryOptions` for `useQuery()`
88
+ * - `factory.infinite(params)` → `ResolvedInfiniteOptions` for `useInfiniteQuery()`
89
+ *
90
+ * Both signatures accept an optional `crawlOptions` object that is appended to
91
+ * the query key and passed to `shouldFetchNextPage` so different call sites can
92
+ * control crawl behavior independently.
93
+ *
94
+ * `params` is always optional. Calling with no arguments produces just the
95
+ * namespace key, which is useful for broad cache invalidation:
96
+ * `queryClient.invalidateQueries(factory())`
97
+ */
98
+ interface QueryFactory<TParams = void, TData = unknown, TError = Error, TSelected = TData, TPageParam = unknown, TCrawlOptions extends Record<string, unknown> = Record<string, unknown>> {
99
+ (params?: TParams, crawlOptions?: TCrawlOptions): ResolvedQueryOptions<TData, TError, TSelected>;
100
+ infinite(params?: TParams, crawlOptions?: TCrawlOptions): ResolvedInfiniteOptions<TData, TError, TPageParam>;
101
+ }
102
+ /**
103
+ * Creates a standalone query factory from a config object.
104
+ *
105
+ * @example
106
+ * const usersFactory = queryFactory({
107
+ * queryKey: ['users'],
108
+ * queryFn: (params: { page: number }, ctx) => fetchUsers(params, ctx.signal),
109
+ * });
110
+ * // useQuery(usersFactory({ page: 1 }))
111
+ */
112
+ declare function queryFactory<TParams = void, TData = unknown, TError = Error, TSelected = TData, TPageParam = unknown, TCrawlOptions extends Record<string, unknown> = Record<string, unknown>>(config: QueryFactoryConfig<TParams, TData, TError, TSelected, TPageParam, TCrawlOptions>): QueryFactory<TParams, TData, TError, TSelected, TPageParam, TCrawlOptions>;
113
+ /**
114
+ * Creates a child factory that inherits the query key and standard options from
115
+ * `parent` and introduces a new `queryFn`. The child's query key is appended to
116
+ * the parent's, and standard options are shallow-merged (child wins).
117
+ *
118
+ * Use this overload when the child fetches different data than the parent.
119
+ */
120
+ declare function queryFactory<TChildParams extends TParentParams, TData = unknown, TError = Error, TChildSelected = TData, TParentParams = TChildParams, TPageParam = unknown, TCrawlOptions extends Record<string, unknown> = Record<string, unknown>>(parent: QueryFactory<TParentParams, any, any, any, any, any>, config: Omit<QueryFactoryConfig<TChildParams, TData, TError, TChildSelected, TPageParam, TCrawlOptions>, 'queryKey' | 'getNextPageParam' | 'getPreviousPageParam' | 'initialPageParam' | 'shouldFetchNextPage' | 'reduce'> & {
121
+ queryKey?: QueryKey;
122
+ queryFn: NonNullable<QueryFactoryConfig<TChildParams, TData, TError, TChildSelected, TPageParam>['queryFn']>;
123
+ } & ({
124
+ getNextPageParam: GetNextPageParamFunction<TPageParam, TData>;
125
+ initialPageParam: TPageParam;
126
+ getPreviousPageParam?: GetPreviousPageParamFunction<TPageParam, TData>;
127
+ shouldFetchNextPage?: (combined: TChildSelected | undefined, crawlOptions: TCrawlOptions) => boolean;
128
+ reduce?: (accumulator: TChildSelected | undefined, page: TData) => TChildSelected;
129
+ } | {
130
+ getNextPageParam?: never;
131
+ initialPageParam?: never;
132
+ getPreviousPageParam?: never;
133
+ shouldFetchNextPage?: never;
134
+ reduce?: never;
135
+ })): QueryFactory<TChildParams, TData, TError, TChildSelected, TPageParam, TCrawlOptions>;
136
+ /**
137
+ * Creates a child factory that reuses the parent's `queryFn` and pagination
138
+ * config. Useful for adding a `select` transform, narrowing params, or
139
+ * overriding `shouldFetchNextPage` with a different crawl-options shape —
140
+ * without changing what data is fetched. Parent and child `select` functions
141
+ * are automatically composed: `child.select(parent.select(data))`.
142
+ */
143
+ declare function queryFactory<TChildParams extends TParentParams, TData = unknown, TError = Error, TParentSelected = TData, TChildSelected = TParentSelected, TParentParams = TChildParams, TPageParam = unknown, TParentCrawlOptions extends Record<string, unknown> = Record<string, unknown>, TChildCrawlOptions extends Record<string, unknown> = TParentCrawlOptions>(parent: QueryFactory<TParentParams, TData, any, TParentSelected, TPageParam, TParentCrawlOptions>, config: Omit<QueryFactoryConfig<TChildParams, TData, TError, TChildSelected, TPageParam, TChildCrawlOptions>, 'queryKey' | 'queryFn' | 'select'> & {
144
+ queryKey?: QueryKey;
145
+ queryFn?: never;
146
+ select?: (data: TParentSelected) => TChildSelected;
147
+ }): QueryFactory<TChildParams, TData, TError, TChildSelected, TPageParam, TChildCrawlOptions>;
148
+
149
+ export { type QueryFactory, type QueryFactoryConfig, type ResolvedInfiniteOptions, type ResolvedQueryOptions, type StandardQueryOptions, queryFactory };
package/dist/index.js ADDED
@@ -0,0 +1,244 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/index.ts
21
+ var index_exports = {};
22
+ __export(index_exports, {
23
+ queryFactory: () => queryFactory
24
+ });
25
+ module.exports = __toCommonJS(index_exports);
26
+
27
+ // src/queryFactory.ts
28
+ var FACTORY_CONFIG = /* @__PURE__ */ Symbol("factoryConfig");
29
+ function resolveKey(namespace, params, crawlOptions) {
30
+ const base = Array.isArray(namespace) ? namespace : [namespace];
31
+ const withParams = params === void 0 ? base : [...base, params];
32
+ if (!crawlOptions) return withParams;
33
+ const defined = Object.fromEntries(Object.entries(crawlOptions).filter(([, v]) => v !== void 0));
34
+ return Object.keys(defined).length > 0 ? [...withParams, defined] : withParams;
35
+ }
36
+ function wrapGetNextPageParam(getNextPageParam, shouldFetchNextPage, crawlOptions, reduce) {
37
+ return (lastPage, allPages, lastPageParam, allPageParams) => {
38
+ const combined = reduce ? allPages.reduce((acc, page) => reduce(acc, page), void 0) : lastPage;
39
+ if (!shouldFetchNextPage(combined, crawlOptions)) return void 0;
40
+ return getNextPageParam(lastPage, allPages, lastPageParam, allPageParams);
41
+ };
42
+ }
43
+ function buildCrawlingQueryFn(queryFn, getNextPageParam, initialPageParam, shouldFetchNextPage, reduce, crawlOptions) {
44
+ return async (context) => {
45
+ var _a, _b;
46
+ const pages = [];
47
+ const pageParams = [];
48
+ let currentParam = initialPageParam;
49
+ let acc = void 0;
50
+ while (true) {
51
+ if ((_a = context.signal) == null ? void 0 : _a.aborted) break;
52
+ const page = await queryFn({ ...context, pageParam: currentParam });
53
+ pages.push(page);
54
+ pageParams.push(currentParam);
55
+ if (reduce) acc = reduce(acc, page);
56
+ if ((_b = context.signal) == null ? void 0 : _b.aborted) break;
57
+ if (shouldFetchNextPage && !shouldFetchNextPage(acc, crawlOptions)) break;
58
+ const nextParam = getNextPageParam(page, pages, currentParam, pageParams);
59
+ if (nextParam == null) break;
60
+ currentParam = nextParam;
61
+ }
62
+ if (reduce) {
63
+ if (acc === void 0) throw new DOMException("Aborted", "AbortError");
64
+ return acc;
65
+ }
66
+ return pages;
67
+ };
68
+ }
69
+ function buildInfiniteCrawlingQueryFn(queryFn, getNextPageParam, shouldFetchNextPage, reduce, crawlOptions) {
70
+ return async (context) => {
71
+ var _a, _b;
72
+ const pages = [];
73
+ const pageParams = [];
74
+ let currentParam = context.pageParam;
75
+ let acc = void 0;
76
+ let nextBatchParam = null;
77
+ while (true) {
78
+ if ((_a = context.signal) == null ? void 0 : _a.aborted) break;
79
+ const page = await queryFn({ ...context, pageParam: currentParam });
80
+ pages.push(page);
81
+ pageParams.push(currentParam);
82
+ acc = reduce(acc, page);
83
+ if ((_b = context.signal) == null ? void 0 : _b.aborted) break;
84
+ const nextParam = getNextPageParam(page, pages, currentParam, pageParams);
85
+ nextBatchParam = nextParam != null ? nextParam : null;
86
+ if (nextParam == null) break;
87
+ if (shouldFetchNextPage && !shouldFetchNextPage(acc, crawlOptions)) break;
88
+ currentParam = nextParam;
89
+ }
90
+ if (acc === void 0) throw new DOMException("Aborted", "AbortError");
91
+ return { data: acc, nextPageParam: nextBatchParam };
92
+ };
93
+ }
94
+ function buildFactory(cfg) {
95
+ const {
96
+ queryKey: namespace,
97
+ queryFn: rawQueryFn,
98
+ select,
99
+ getNextPageParam,
100
+ getPreviousPageParam,
101
+ initialPageParam,
102
+ shouldFetchNextPage,
103
+ reduce,
104
+ standardOptions
105
+ } = cfg;
106
+ const hasCrawling = rawQueryFn !== void 0 && getNextPageParam !== void 0;
107
+ const hasInfiniteCrawling = hasCrawling && reduce !== void 0;
108
+ const factory = function(params, crawlOptions = {}) {
109
+ const queryKey = resolveKey(namespace, params, crawlOptions);
110
+ const boundQueryFn = rawQueryFn ? (ctx) => rawQueryFn(params, ctx) : void 0;
111
+ const resolvedQueryFn = hasCrawling ? buildCrawlingQueryFn(boundQueryFn, getNextPageParam, initialPageParam, shouldFetchNextPage, reduce, crawlOptions) : boundQueryFn;
112
+ return {
113
+ ...standardOptions,
114
+ queryKey,
115
+ ...resolvedQueryFn !== void 0 && { queryFn: resolvedQueryFn },
116
+ ...select !== void 0 && { select },
117
+ [FACTORY_CONFIG]: cfg
118
+ };
119
+ };
120
+ factory.infinite = function(params, crawlOptions = {}) {
121
+ const queryKey = resolveKey([...resolveKey(namespace, void 0), "infinite"], params, crawlOptions);
122
+ const boundQueryFn = rawQueryFn ? (ctx) => rawQueryFn(params, ctx) : void 0;
123
+ if (hasInfiniteCrawling) {
124
+ const crawlingQueryFn = buildInfiniteCrawlingQueryFn(
125
+ boundQueryFn,
126
+ getNextPageParam,
127
+ shouldFetchNextPage,
128
+ reduce,
129
+ crawlOptions
130
+ );
131
+ const envelopeGetNextPageParam = (envelope) => envelope.nextPageParam;
132
+ const envelopeSelect = (data) => ({
133
+ ...data,
134
+ pages: data.pages.map((e) => select ? select(e.data) : e.data)
135
+ });
136
+ return {
137
+ ...standardOptions,
138
+ queryKey,
139
+ queryFn: crawlingQueryFn,
140
+ getNextPageParam: envelopeGetNextPageParam,
141
+ initialPageParam,
142
+ select: envelopeSelect,
143
+ ...getPreviousPageParam !== void 0 && { getPreviousPageParam },
144
+ [FACTORY_CONFIG]: cfg
145
+ };
146
+ }
147
+ const infiniteGetNextPageParam = getNextPageParam && shouldFetchNextPage ? wrapGetNextPageParam(getNextPageParam, shouldFetchNextPage, crawlOptions, reduce) : getNextPageParam != null ? getNextPageParam : (() => void 0);
148
+ const infiniteSelect = select ? (data) => ({
149
+ ...data,
150
+ pages: data.pages.map(select)
151
+ }) : void 0;
152
+ return {
153
+ ...standardOptions,
154
+ queryKey,
155
+ ...boundQueryFn !== void 0 && { queryFn: boundQueryFn },
156
+ ...infiniteSelect !== void 0 && { select: infiniteSelect },
157
+ getNextPageParam: infiniteGetNextPageParam,
158
+ ...getPreviousPageParam !== void 0 && { getPreviousPageParam },
159
+ ...initialPageParam !== void 0 && { initialPageParam },
160
+ [FACTORY_CONFIG]: cfg
161
+ };
162
+ };
163
+ return factory;
164
+ }
165
+ function queryFactory(configOrParent, childConfig) {
166
+ var _a, _b, _c, _d, _e;
167
+ if (childConfig !== void 0 && typeof configOrParent === "function") {
168
+ const result = configOrParent();
169
+ const parentCfg = result == null ? void 0 : result[FACTORY_CONFIG];
170
+ if (!parentCfg) {
171
+ throw new Error("queryFactory: first argument must be a factory created by queryFactory()");
172
+ }
173
+ const hasNewQueryFn = childConfig.queryFn !== void 0;
174
+ const childNamespace = childConfig.queryKey ? Array.isArray(childConfig.queryKey) ? childConfig.queryKey : [childConfig.queryKey] : [];
175
+ const composedNamespace = [...parentCfg.queryKey, ...childNamespace];
176
+ let resolvedSelect;
177
+ if (hasNewQueryFn) {
178
+ resolvedSelect = childConfig.select;
179
+ } else if (childConfig.select && parentCfg.select) {
180
+ const p = parentCfg.select;
181
+ const c = childConfig.select;
182
+ resolvedSelect = (data) => c(p(data));
183
+ } else {
184
+ resolvedSelect = (_a = childConfig.select) != null ? _a : parentCfg.select;
185
+ }
186
+ const crawling = hasNewQueryFn ? {
187
+ getNextPageParam: childConfig.getNextPageParam,
188
+ getPreviousPageParam: childConfig.getPreviousPageParam,
189
+ initialPageParam: childConfig.initialPageParam,
190
+ shouldFetchNextPage: childConfig.shouldFetchNextPage,
191
+ reduce: childConfig.reduce
192
+ } : {
193
+ getNextPageParam: (_b = childConfig.getNextPageParam) != null ? _b : parentCfg.getNextPageParam,
194
+ getPreviousPageParam: (_c = childConfig.getPreviousPageParam) != null ? _c : parentCfg.getPreviousPageParam,
195
+ initialPageParam: childConfig.initialPageParam !== void 0 ? childConfig.initialPageParam : parentCfg.initialPageParam,
196
+ shouldFetchNextPage: (_d = childConfig.shouldFetchNextPage) != null ? _d : parentCfg.shouldFetchNextPage,
197
+ reduce: (_e = childConfig.reduce) != null ? _e : parentCfg.reduce
198
+ };
199
+ const {
200
+ queryKey: _k,
201
+ queryFn: _f,
202
+ select: _s,
203
+ getNextPageParam: _g,
204
+ getPreviousPageParam: _gp,
205
+ initialPageParam: _ip,
206
+ shouldFetchNextPage: _sfnp,
207
+ reduce: _c2,
208
+ ...childStandardOptions
209
+ } = childConfig;
210
+ return buildFactory({
211
+ queryKey: composedNamespace,
212
+ queryFn: hasNewQueryFn ? childConfig.queryFn : parentCfg.queryFn,
213
+ select: resolvedSelect,
214
+ ...crawling,
215
+ standardOptions: { ...parentCfg.standardOptions, ...childStandardOptions }
216
+ });
217
+ }
218
+ const {
219
+ queryKey,
220
+ queryFn,
221
+ select,
222
+ getNextPageParam,
223
+ getPreviousPageParam,
224
+ initialPageParam,
225
+ shouldFetchNextPage,
226
+ reduce,
227
+ ...standardOptions
228
+ } = configOrParent;
229
+ return buildFactory({
230
+ queryKey,
231
+ queryFn,
232
+ select,
233
+ getNextPageParam,
234
+ getPreviousPageParam,
235
+ initialPageParam,
236
+ shouldFetchNextPage,
237
+ reduce,
238
+ standardOptions
239
+ });
240
+ }
241
+ // Annotate the CommonJS export names for ESM import in node:
242
+ 0 && (module.exports = {
243
+ queryFactory
244
+ });
package/dist/index.mjs ADDED
@@ -0,0 +1,217 @@
1
+ // src/queryFactory.ts
2
+ var FACTORY_CONFIG = /* @__PURE__ */ Symbol("factoryConfig");
3
+ function resolveKey(namespace, params, crawlOptions) {
4
+ const base = Array.isArray(namespace) ? namespace : [namespace];
5
+ const withParams = params === void 0 ? base : [...base, params];
6
+ if (!crawlOptions) return withParams;
7
+ const defined = Object.fromEntries(Object.entries(crawlOptions).filter(([, v]) => v !== void 0));
8
+ return Object.keys(defined).length > 0 ? [...withParams, defined] : withParams;
9
+ }
10
+ function wrapGetNextPageParam(getNextPageParam, shouldFetchNextPage, crawlOptions, reduce) {
11
+ return (lastPage, allPages, lastPageParam, allPageParams) => {
12
+ const combined = reduce ? allPages.reduce((acc, page) => reduce(acc, page), void 0) : lastPage;
13
+ if (!shouldFetchNextPage(combined, crawlOptions)) return void 0;
14
+ return getNextPageParam(lastPage, allPages, lastPageParam, allPageParams);
15
+ };
16
+ }
17
+ function buildCrawlingQueryFn(queryFn, getNextPageParam, initialPageParam, shouldFetchNextPage, reduce, crawlOptions) {
18
+ return async (context) => {
19
+ var _a, _b;
20
+ const pages = [];
21
+ const pageParams = [];
22
+ let currentParam = initialPageParam;
23
+ let acc = void 0;
24
+ while (true) {
25
+ if ((_a = context.signal) == null ? void 0 : _a.aborted) break;
26
+ const page = await queryFn({ ...context, pageParam: currentParam });
27
+ pages.push(page);
28
+ pageParams.push(currentParam);
29
+ if (reduce) acc = reduce(acc, page);
30
+ if ((_b = context.signal) == null ? void 0 : _b.aborted) break;
31
+ if (shouldFetchNextPage && !shouldFetchNextPage(acc, crawlOptions)) break;
32
+ const nextParam = getNextPageParam(page, pages, currentParam, pageParams);
33
+ if (nextParam == null) break;
34
+ currentParam = nextParam;
35
+ }
36
+ if (reduce) {
37
+ if (acc === void 0) throw new DOMException("Aborted", "AbortError");
38
+ return acc;
39
+ }
40
+ return pages;
41
+ };
42
+ }
43
+ function buildInfiniteCrawlingQueryFn(queryFn, getNextPageParam, shouldFetchNextPage, reduce, crawlOptions) {
44
+ return async (context) => {
45
+ var _a, _b;
46
+ const pages = [];
47
+ const pageParams = [];
48
+ let currentParam = context.pageParam;
49
+ let acc = void 0;
50
+ let nextBatchParam = null;
51
+ while (true) {
52
+ if ((_a = context.signal) == null ? void 0 : _a.aborted) break;
53
+ const page = await queryFn({ ...context, pageParam: currentParam });
54
+ pages.push(page);
55
+ pageParams.push(currentParam);
56
+ acc = reduce(acc, page);
57
+ if ((_b = context.signal) == null ? void 0 : _b.aborted) break;
58
+ const nextParam = getNextPageParam(page, pages, currentParam, pageParams);
59
+ nextBatchParam = nextParam != null ? nextParam : null;
60
+ if (nextParam == null) break;
61
+ if (shouldFetchNextPage && !shouldFetchNextPage(acc, crawlOptions)) break;
62
+ currentParam = nextParam;
63
+ }
64
+ if (acc === void 0) throw new DOMException("Aborted", "AbortError");
65
+ return { data: acc, nextPageParam: nextBatchParam };
66
+ };
67
+ }
68
+ function buildFactory(cfg) {
69
+ const {
70
+ queryKey: namespace,
71
+ queryFn: rawQueryFn,
72
+ select,
73
+ getNextPageParam,
74
+ getPreviousPageParam,
75
+ initialPageParam,
76
+ shouldFetchNextPage,
77
+ reduce,
78
+ standardOptions
79
+ } = cfg;
80
+ const hasCrawling = rawQueryFn !== void 0 && getNextPageParam !== void 0;
81
+ const hasInfiniteCrawling = hasCrawling && reduce !== void 0;
82
+ const factory = function(params, crawlOptions = {}) {
83
+ const queryKey = resolveKey(namespace, params, crawlOptions);
84
+ const boundQueryFn = rawQueryFn ? (ctx) => rawQueryFn(params, ctx) : void 0;
85
+ const resolvedQueryFn = hasCrawling ? buildCrawlingQueryFn(boundQueryFn, getNextPageParam, initialPageParam, shouldFetchNextPage, reduce, crawlOptions) : boundQueryFn;
86
+ return {
87
+ ...standardOptions,
88
+ queryKey,
89
+ ...resolvedQueryFn !== void 0 && { queryFn: resolvedQueryFn },
90
+ ...select !== void 0 && { select },
91
+ [FACTORY_CONFIG]: cfg
92
+ };
93
+ };
94
+ factory.infinite = function(params, crawlOptions = {}) {
95
+ const queryKey = resolveKey([...resolveKey(namespace, void 0), "infinite"], params, crawlOptions);
96
+ const boundQueryFn = rawQueryFn ? (ctx) => rawQueryFn(params, ctx) : void 0;
97
+ if (hasInfiniteCrawling) {
98
+ const crawlingQueryFn = buildInfiniteCrawlingQueryFn(
99
+ boundQueryFn,
100
+ getNextPageParam,
101
+ shouldFetchNextPage,
102
+ reduce,
103
+ crawlOptions
104
+ );
105
+ const envelopeGetNextPageParam = (envelope) => envelope.nextPageParam;
106
+ const envelopeSelect = (data) => ({
107
+ ...data,
108
+ pages: data.pages.map((e) => select ? select(e.data) : e.data)
109
+ });
110
+ return {
111
+ ...standardOptions,
112
+ queryKey,
113
+ queryFn: crawlingQueryFn,
114
+ getNextPageParam: envelopeGetNextPageParam,
115
+ initialPageParam,
116
+ select: envelopeSelect,
117
+ ...getPreviousPageParam !== void 0 && { getPreviousPageParam },
118
+ [FACTORY_CONFIG]: cfg
119
+ };
120
+ }
121
+ const infiniteGetNextPageParam = getNextPageParam && shouldFetchNextPage ? wrapGetNextPageParam(getNextPageParam, shouldFetchNextPage, crawlOptions, reduce) : getNextPageParam != null ? getNextPageParam : (() => void 0);
122
+ const infiniteSelect = select ? (data) => ({
123
+ ...data,
124
+ pages: data.pages.map(select)
125
+ }) : void 0;
126
+ return {
127
+ ...standardOptions,
128
+ queryKey,
129
+ ...boundQueryFn !== void 0 && { queryFn: boundQueryFn },
130
+ ...infiniteSelect !== void 0 && { select: infiniteSelect },
131
+ getNextPageParam: infiniteGetNextPageParam,
132
+ ...getPreviousPageParam !== void 0 && { getPreviousPageParam },
133
+ ...initialPageParam !== void 0 && { initialPageParam },
134
+ [FACTORY_CONFIG]: cfg
135
+ };
136
+ };
137
+ return factory;
138
+ }
139
+ function queryFactory(configOrParent, childConfig) {
140
+ var _a, _b, _c, _d, _e;
141
+ if (childConfig !== void 0 && typeof configOrParent === "function") {
142
+ const result = configOrParent();
143
+ const parentCfg = result == null ? void 0 : result[FACTORY_CONFIG];
144
+ if (!parentCfg) {
145
+ throw new Error("queryFactory: first argument must be a factory created by queryFactory()");
146
+ }
147
+ const hasNewQueryFn = childConfig.queryFn !== void 0;
148
+ const childNamespace = childConfig.queryKey ? Array.isArray(childConfig.queryKey) ? childConfig.queryKey : [childConfig.queryKey] : [];
149
+ const composedNamespace = [...parentCfg.queryKey, ...childNamespace];
150
+ let resolvedSelect;
151
+ if (hasNewQueryFn) {
152
+ resolvedSelect = childConfig.select;
153
+ } else if (childConfig.select && parentCfg.select) {
154
+ const p = parentCfg.select;
155
+ const c = childConfig.select;
156
+ resolvedSelect = (data) => c(p(data));
157
+ } else {
158
+ resolvedSelect = (_a = childConfig.select) != null ? _a : parentCfg.select;
159
+ }
160
+ const crawling = hasNewQueryFn ? {
161
+ getNextPageParam: childConfig.getNextPageParam,
162
+ getPreviousPageParam: childConfig.getPreviousPageParam,
163
+ initialPageParam: childConfig.initialPageParam,
164
+ shouldFetchNextPage: childConfig.shouldFetchNextPage,
165
+ reduce: childConfig.reduce
166
+ } : {
167
+ getNextPageParam: (_b = childConfig.getNextPageParam) != null ? _b : parentCfg.getNextPageParam,
168
+ getPreviousPageParam: (_c = childConfig.getPreviousPageParam) != null ? _c : parentCfg.getPreviousPageParam,
169
+ initialPageParam: childConfig.initialPageParam !== void 0 ? childConfig.initialPageParam : parentCfg.initialPageParam,
170
+ shouldFetchNextPage: (_d = childConfig.shouldFetchNextPage) != null ? _d : parentCfg.shouldFetchNextPage,
171
+ reduce: (_e = childConfig.reduce) != null ? _e : parentCfg.reduce
172
+ };
173
+ const {
174
+ queryKey: _k,
175
+ queryFn: _f,
176
+ select: _s,
177
+ getNextPageParam: _g,
178
+ getPreviousPageParam: _gp,
179
+ initialPageParam: _ip,
180
+ shouldFetchNextPage: _sfnp,
181
+ reduce: _c2,
182
+ ...childStandardOptions
183
+ } = childConfig;
184
+ return buildFactory({
185
+ queryKey: composedNamespace,
186
+ queryFn: hasNewQueryFn ? childConfig.queryFn : parentCfg.queryFn,
187
+ select: resolvedSelect,
188
+ ...crawling,
189
+ standardOptions: { ...parentCfg.standardOptions, ...childStandardOptions }
190
+ });
191
+ }
192
+ const {
193
+ queryKey,
194
+ queryFn,
195
+ select,
196
+ getNextPageParam,
197
+ getPreviousPageParam,
198
+ initialPageParam,
199
+ shouldFetchNextPage,
200
+ reduce,
201
+ ...standardOptions
202
+ } = configOrParent;
203
+ return buildFactory({
204
+ queryKey,
205
+ queryFn,
206
+ select,
207
+ getNextPageParam,
208
+ getPreviousPageParam,
209
+ initialPageParam,
210
+ shouldFetchNextPage,
211
+ reduce,
212
+ standardOptions
213
+ });
214
+ }
215
+ export {
216
+ queryFactory
217
+ };
package/package.json ADDED
@@ -0,0 +1,55 @@
1
+ {
2
+ "name": "@robohall/react-query-factory",
3
+ "version": "1.0.0",
4
+ "description": "A factory abstraction for TanStack Query (React Query) with composable keys, crawling support, and automatic infinite query generation",
5
+ "author": "Robert Hall",
6
+ "license": "MIT",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "https://github.com/roberth26/react-query-factory.git"
10
+ },
11
+ "bugs": "https://github.com/roberth26/react-query-factory/issues",
12
+ "homepage": "https://github.com/roberth26/react-query-factory#readme",
13
+ "main": "dist/index.js",
14
+ "module": "dist/index.mjs",
15
+ "types": "dist/index.d.ts",
16
+ "exports": {
17
+ ".": {
18
+ "import": {
19
+ "types": "./dist/index.d.mts",
20
+ "default": "./dist/index.mjs"
21
+ },
22
+ "require": {
23
+ "types": "./dist/index.d.ts",
24
+ "default": "./dist/index.js"
25
+ }
26
+ }
27
+ },
28
+ "sideEffects": false,
29
+ "files": [
30
+ "dist"
31
+ ],
32
+ "scripts": {
33
+ "build": "tsup src/index.ts --format cjs,esm --dts --clean",
34
+ "typecheck": "tsc --noEmit",
35
+ "test": "vitest run",
36
+ "test:watch": "vitest",
37
+ "prepublishOnly": "npm run build && npm test",
38
+ "sandbox": "cd sandbox && npm run dev"
39
+ },
40
+ "peerDependencies": {
41
+ "@tanstack/react-query": ">=5.0.0"
42
+ },
43
+ "devDependencies": {
44
+ "@tanstack/react-query": "^5.0.0",
45
+ "tsup": "^8.0.0",
46
+ "typescript": "^5.0.0",
47
+ "vitest": "^2.0.0"
48
+ },
49
+ "keywords": [
50
+ "react-query",
51
+ "tanstack-query",
52
+ "query-factory",
53
+ "typescript"
54
+ ]
55
+ }