@questpie/tanstack-query 1.1.1 → 2.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/README.md CHANGED
@@ -1,12 +1,15 @@
1
1
  # @questpie/tanstack-query
2
2
 
3
- Type-safe TanStack React Query integration for QUESTPIE.
3
+ Type-safe TanStack Query option builders for QUESTPIE — collections, globals, RPC procedures, and realtime streaming.
4
4
 
5
5
  ## Features
6
6
 
7
- - **Query Options Factory** - Pre-built query/mutation options using `queryOptions()` and `mutationOptions()` from TanStack Query
8
- - **SSR Ready** - Prefetch data on server, hydrate on client
9
- - **Type-Safe** - Full TypeScript inference from your CMS schema
7
+ - **Query Options Factory** — Pre-built `queryOptions()` and `mutationOptions()` for all CRUD operations
8
+ - **RPC Query Options** — Typed `query()` / `mutation()` builders for standalone RPC procedures with nested router support
9
+ - **Realtime Streaming** — `streamedQuery`-based options for SSE live updates on collections and globals
10
+ - **Batch Operations** — `updateMany` and `deleteMany` mutation builders
11
+ - **SSR Ready** — Prefetch data on the server, hydrate on the client
12
+ - **Full Type Inference** — Types flow from your CMS schema through to query results
10
13
 
11
14
  ## Installation
12
15
 
@@ -18,285 +21,221 @@ bun add @questpie/tanstack-query questpie @tanstack/react-query
18
21
 
19
22
  ### 1. Create Query Options
20
23
 
21
- ```typescript
22
- // src/lib/queries.ts
24
+ ```ts
23
25
  import { createClient } from "questpie/client";
24
26
  import { createQuestpieQueryOptions } from "@questpie/tanstack-query";
25
- import type { AppCMS } from "@/cms";
27
+ import type { AppCMS, AppRpc } from "@/questpie/server/cms";
26
28
 
27
- const cmsClient = createClient<AppCMS>({
29
+ const cmsClient = createClient<AppCMS, AppRpc>({
28
30
  baseURL: "http://localhost:3000",
29
31
  basePath: "/api/cms",
30
32
  });
31
33
 
32
- export const q = createQuestpieQueryOptions(cmsClient, {
33
- keyPrefix: ["cms"], // Optional: prefix for all query keys
34
- });
34
+ export const cmsQueries = createQuestpieQueryOptions(cmsClient);
35
35
  ```
36
36
 
37
37
  ### 2. Use in Components
38
38
 
39
- ```typescript
40
- import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"
41
- import { q } from "@/lib/queries"
39
+ ```tsx
40
+ import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
41
+ import { cmsQueries } from "@/lib/queries";
42
42
 
43
43
  function PostsList() {
44
- // Queries
45
- const { data: posts } = useQuery(q.collections.posts.find({ limit: 10 }))
46
-
47
- // Mutations
48
- const queryClient = useQueryClient()
44
+ const { data } = useQuery(
45
+ cmsQueries.collections.posts.find({ limit: 10 })
46
+ );
49
47
 
48
+ const queryClient = useQueryClient();
50
49
  const createPost = useMutation({
51
- ...q.collections.posts.create(),
50
+ ...cmsQueries.collections.posts.create(),
52
51
  onSuccess: () => {
53
- queryClient.invalidateQueries({ queryKey: ["cms", "collections", "posts"] })
52
+ queryClient.invalidateQueries({ queryKey: cmsQueries.collections.posts.key() });
54
53
  },
55
- })
54
+ });
56
55
 
57
56
  return (
58
57
  <div>
59
- {posts?.docs.map((post) => (
60
- <div key={post.id}>{post.title}</div>
61
- ))}
58
+ {data?.docs.map((post) => <div key={post.id}>{post.title}</div>)}
62
59
  <button onClick={() => createPost.mutate({ title: "New Post" })}>
63
- Create Post
60
+ Create
64
61
  </button>
65
62
  </div>
66
- )
63
+ );
67
64
  }
68
65
  ```
69
66
 
70
- ## Query Options API
67
+ ## Collections
71
68
 
72
- ### Collections
73
-
74
- ```typescript
75
- // Find many
76
- q.collections.posts.find({
69
+ ```ts
70
+ // Find many (paginated)
71
+ cmsQueries.collections.posts.find({
77
72
  where: { published: { eq: true } },
78
73
  orderBy: { createdAt: "desc" },
79
74
  limit: 10,
80
75
  offset: 0,
81
76
  with: { author: true },
82
- });
77
+ })
83
78
 
84
79
  // Find one
85
- q.collections.posts.findOne({
80
+ cmsQueries.collections.posts.findOne({
86
81
  where: { id: "post-id" },
87
- with: { author: true, comments: true },
88
- });
82
+ with: { author: true },
83
+ })
89
84
 
90
- // Create
91
- q.collections.posts.create();
92
- // Usage: mutation.mutate({ title: "...", content: "..." })
85
+ // Count
86
+ cmsQueries.collections.posts.count({
87
+ where: { published: { eq: true } },
88
+ })
89
+
90
+ // Create (mutation)
91
+ cmsQueries.collections.posts.create()
92
+
93
+ // Update (mutation)
94
+ cmsQueries.collections.posts.update()
93
95
 
94
- // Update
95
- q.collections.posts.update();
96
- // Usage: mutation.mutate({ id: "...", data: { title: "..." } })
96
+ // Delete (mutation)
97
+ cmsQueries.collections.posts.delete()
97
98
 
98
- // Delete
99
- q.collections.posts.delete();
100
- // Usage: mutation.mutate({ id: "..." })
99
+ // Update many (mutation)
100
+ cmsQueries.collections.posts.updateMany()
101
101
 
102
- // Restore (soft delete)
103
- q.collections.posts.restore();
104
- // Usage: mutation.mutate({ id: "..." })
102
+ // Delete many (mutation)
103
+ cmsQueries.collections.posts.deleteMany()
104
+
105
+ // Restore soft-deleted (mutation)
106
+ cmsQueries.collections.posts.restore()
107
+
108
+ // Query key for invalidation
109
+ cmsQueries.collections.posts.key()
105
110
  ```
106
111
 
107
- ### Globals
112
+ ## Globals
108
113
 
109
- ```typescript
110
- // Get
111
- q.globals.siteSettings.get();
114
+ ```ts
115
+ // Get global value
116
+ cmsQueries.globals.siteSettings.get()
112
117
 
113
- // Update
114
- q.globals.siteSettings.update();
115
- // Usage: mutation.mutate({ data: { siteName: "..." } })
118
+ // Update global (mutation)
119
+ cmsQueries.globals.siteSettings.update()
120
+
121
+ // Query key
122
+ cmsQueries.globals.siteSettings.key()
116
123
  ```
117
124
 
118
- ### Custom Queries
125
+ ## RPC
126
+
127
+ Full type-safe query/mutation options for standalone RPC procedures:
119
128
 
120
- ```typescript
121
- const searchQuery = q.custom.query({
122
- key: ["search", query],
123
- queryFn: () => fetch(`/api/search?q=${query}`).then((r) => r.json()),
129
+ ```ts
130
+ // Query options from RPC procedure
131
+ const { data } = useQuery(
132
+ cmsQueries.rpc.dashboard.getStats.query({ period: "week" })
133
+ );
134
+
135
+ // Mutation options from RPC procedure
136
+ const publish = useMutation(
137
+ cmsQueries.rpc.posts.publish.mutation()
138
+ );
139
+ publish.mutate({ id: "post_123" });
140
+
141
+ // Query key for invalidation/prefetch
142
+ queryClient.invalidateQueries({
143
+ queryKey: cmsQueries.rpc.dashboard.getStats.key({ period: "week" }),
124
144
  });
145
+ ```
146
+
147
+ ## Realtime Streaming
148
+
149
+ Collection queries support SSE-based live updates via `streamedQuery`:
150
+
151
+ ```ts
152
+ // Enable realtime on a query
153
+ const { data } = useQuery(
154
+ cmsQueries.collections.posts.find(
155
+ { limit: 10 },
156
+ { realtime: true }
157
+ )
158
+ );
159
+ ```
125
160
 
126
- useQuery(searchQuery);
161
+ Topic helpers for manual subscription:
162
+
163
+ ```ts
164
+ import { buildCollectionTopic, buildGlobalTopic } from "@questpie/tanstack-query";
165
+
166
+ const postsTopic = buildCollectionTopic("posts");
167
+ const settingsTopic = buildGlobalTopic("siteSettings");
127
168
  ```
128
169
 
129
170
  ## SSR Prefetching
130
171
 
131
172
  ### TanStack Start
132
173
 
133
- ```typescript
134
- // src/routes/posts.tsx
135
- import { createFileRoute } from "@tanstack/react-router"
136
- import { useSuspenseQuery } from "@tanstack/react-query"
137
- import { q } from "@/lib/queries"
174
+ ```tsx
175
+ import { createFileRoute } from "@tanstack/react-router";
176
+ import { useSuspenseQuery } from "@tanstack/react-query";
177
+ import { cmsQueries } from "@/lib/queries";
138
178
 
139
179
  export const Route = createFileRoute("/posts")({
140
180
  loader: async ({ context }) => {
141
181
  await context.queryClient.ensureQueryData(
142
- q.collections.posts.find({ limit: 10 })
143
- )
182
+ cmsQueries.collections.posts.find({ limit: 10 })
183
+ );
144
184
  },
145
185
  component: PostsPage,
146
- })
186
+ });
147
187
 
148
188
  function PostsPage() {
149
- const { data: posts } = useSuspenseQuery(
150
- q.collections.posts.find({ limit: 10 })
151
- )
152
-
189
+ const { data } = useSuspenseQuery(
190
+ cmsQueries.collections.posts.find({ limit: 10 })
191
+ );
153
192
  return (
154
193
  <ul>
155
- {posts.docs.map((post) => (
156
- <li key={post.id}>{post.title}</li>
157
- ))}
194
+ {data.docs.map((post) => <li key={post.id}>{post.title}</li>)}
158
195
  </ul>
159
- )
196
+ );
160
197
  }
161
198
  ```
162
199
 
163
200
  ### Next.js
164
201
 
165
- ```typescript
166
- // app/posts/page.tsx
167
- import { HydrationBoundary, dehydrate } from "@tanstack/react-query"
168
- import { getQueryClient } from "@/lib/query-client"
169
- import { q } from "@/lib/queries"
170
- import { PostsList } from "./posts-list"
202
+ ```tsx
203
+ import { HydrationBoundary, dehydrate } from "@tanstack/react-query";
204
+ import { getQueryClient } from "@/lib/query-client";
205
+ import { cmsQueries } from "@/lib/queries";
171
206
 
172
207
  export default async function PostsPage() {
173
- const queryClient = getQueryClient()
174
-
208
+ const queryClient = getQueryClient();
175
209
  await queryClient.prefetchQuery(
176
- q.collections.posts.find({ limit: 10 })
177
- )
178
-
210
+ cmsQueries.collections.posts.find({ limit: 10 })
211
+ );
179
212
  return (
180
213
  <HydrationBoundary state={dehydrate(queryClient)}>
181
214
  <PostsList />
182
215
  </HydrationBoundary>
183
- )
184
- }
185
- ```
186
-
187
- ## Optimistic Updates
188
-
189
- ```typescript
190
- const queryClient = useQueryClient();
191
-
192
- const updatePost = useMutation({
193
- ...q.collections.posts.update(),
194
- onMutate: async ({ id, data }) => {
195
- await queryClient.cancelQueries({
196
- queryKey: ["cms", "collections", "posts"],
197
- });
198
-
199
- const previousPosts = queryClient.getQueryData([
200
- "cms",
201
- "collections",
202
- "posts",
203
- ]);
204
-
205
- queryClient.setQueryData(["cms", "collections", "posts"], (old) => ({
206
- ...old,
207
- docs: old.docs.map((post) =>
208
- post.id === id ? { ...post, ...data } : post,
209
- ),
210
- }));
211
-
212
- return { previousPosts };
213
- },
214
- onError: (err, variables, context) => {
215
- queryClient.setQueryData(
216
- ["cms", "collections", "posts"],
217
- context?.previousPosts,
218
- );
219
- },
220
- onSettled: () => {
221
- queryClient.invalidateQueries({
222
- queryKey: ["cms", "collections", "posts"],
223
- });
224
- },
225
- });
226
- ```
227
-
228
- ## Infinite Queries
229
-
230
- ```typescript
231
- import { useInfiniteQuery } from "@tanstack/react-query"
232
- import { cmsClient } from "@/lib/cms-client"
233
-
234
- function InfinitePosts() {
235
- const { data, fetchNextPage, hasNextPage } = useInfiniteQuery({
236
- queryKey: ["cms", "collections", "posts", "infinite"],
237
- queryFn: ({ pageParam = 0 }) =>
238
- cmsClient.collections.posts.find({
239
- limit: 10,
240
- offset: pageParam,
241
- }),
242
- getNextPageParam: (lastPage, allPages) => {
243
- const totalFetched = allPages.flatMap((p) => p.docs).length
244
- return lastPage.docs.length === 10 ? totalFetched : undefined
245
- },
246
- initialPageParam: 0,
247
- })
248
-
249
- return (
250
- <div>
251
- {data?.pages.flatMap((page) =>
252
- page.docs.map((post) => <div key={post.id}>{post.title}</div>)
253
- )}
254
- {hasNextPage && (
255
- <button onClick={() => fetchNextPage()}>Load More</button>
256
- )}
257
- </div>
258
- )
216
+ );
259
217
  }
260
218
  ```
261
219
 
262
220
  ## Query Key Structure
263
221
 
264
- All query keys follow this structure:
265
-
266
- ```typescript
222
+ ```ts
267
223
  // Collections
268
- [...keyPrefix, "collections", collectionName, "find", options]
269
- [...keyPrefix, "collections", collectionName, "findOne", options]
224
+ ["collections", collectionName, "find", options]
225
+ ["collections", collectionName, "findOne", options]
226
+ ["collections", collectionName, "count", options]
270
227
 
271
228
  // Globals
272
- [...keyPrefix, "globals", globalName, "get", options]
273
-
274
- // Custom
275
- [...keyPrefix, ...customKey]
276
- ```
277
-
278
- ## TypeScript
279
-
280
- Full type inference from your CMS schema:
281
-
282
- ```typescript
283
- import type { CollectionSelect } from "questpie";
284
- import type { AppCMS } from "@/cms";
285
-
286
- // Collection item type
287
- type Post = CollectionSelect<AppCMS["config"]["collections"]["posts"]>;
229
+ ["globals", globalName, "get"]
288
230
 
289
- // Query result types are automatically inferred
290
- const { data } = useQuery(q.collections.posts.find({ limit: 10 }));
291
- // ^? { docs: Post[], totalDocs: number, ... }
231
+ // RPC
232
+ ["rpc", ...segments, "query", input]
233
+ ["rpc", ...segments, "mutation"]
292
234
  ```
293
235
 
294
- ## Related Packages
236
+ ## Documentation
295
237
 
296
- - [`questpie`](../questpie) - Core CMS engine
297
- - [`@questpie/hono`](../hono) - Hono adapter
298
- - [`@questpie/elysia`](../elysia) - Elysia adapter
299
- - [`@questpie/next`](../next) - Next.js adapter
238
+ Full documentation: [https://questpie.com/docs/client/tanstack-query](https://questpie.com/docs/client/tanstack-query)
300
239
 
301
240
  ## License
302
241
 
package/dist/index.d.mts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { DefaultError, QueryKey, UseMutationOptions, UseQueryOptions } from "@tanstack/react-query";
2
+ import { QuestpieClient, RealtimeAPI, TopicConfig, buildCollectionTopic, buildGlobalTopic } from "questpie/client";
2
3
  import { Questpie } from "questpie";
3
- import { QuestpieClient } from "questpie/client";
4
4
 
5
5
  //#region src/index.d.ts
6
6
  type QuestpieQueryErrorMap = (error: unknown) => unknown;
@@ -14,50 +14,62 @@ type QueryData<TFn extends AnyAsyncFn> = Awaited<ReturnType<TFn>>;
14
14
  type FirstArg<TFn extends AnyAsyncFn> = Parameters<TFn> extends [infer TFirst, ...any[]] ? TFirst : never;
15
15
  type HasArgs<TFn extends AnyAsyncFn> = Parameters<TFn> extends [] ? false : true;
16
16
  type QueryBuilder<TFn extends AnyAsyncFn> = HasArgs<TFn> extends false ? () => UseQueryOptions<QueryData<TFn>> : undefined extends FirstArg<TFn> ? (options?: FirstArg<TFn>) => UseQueryOptions<QueryData<TFn>> : (options: FirstArg<TFn>) => UseQueryOptions<QueryData<TFn>>;
17
+ type KeyBuilder<TFn extends AnyAsyncFn> = HasArgs<TFn> extends false ? () => QueryKey : undefined extends FirstArg<TFn> ? (options?: FirstArg<TFn>) => QueryKey : (options: FirstArg<TFn>) => QueryKey;
18
+ type MutationVariables<TFn extends AnyAsyncFn> = HasArgs<TFn> extends false ? void : undefined extends FirstArg<TFn> ? FirstArg<TFn> | void : FirstArg<TFn>;
17
19
  type MutationBuilder<TVariables, TData> = () => UseMutationOptions<TData, DefaultError, TVariables>;
18
20
  /** Extract collection keys from QuestpieClient */
19
- type CollectionKeys<T extends Questpie<any>> = Extract<keyof QuestpieClient<T>["collections"], string>;
21
+ type CollectionKeys<TCMS extends Questpie<any>, TRPC extends Record<string, any>> = Extract<keyof QuestpieClient<TCMS, TRPC>["collections"], string>;
20
22
  /** Extract global keys from QuestpieClient */
21
- type GlobalKeys<T extends Questpie<any>> = Extract<keyof QuestpieClient<T>["globals"], string>;
22
- type CollectionFind<T extends Questpie<any>, K$1 extends CollectionKeys<T>> = QuestpieClient<T>["collections"][K$1]["find"];
23
- type CollectionCount<T extends Questpie<any>, K$1 extends CollectionKeys<T>> = QuestpieClient<T>["collections"][K$1]["count"];
24
- type CollectionFindOne<T extends Questpie<any>, K$1 extends CollectionKeys<T>> = QuestpieClient<T>["collections"][K$1]["findOne"];
25
- type CollectionCreate<T extends Questpie<any>, K$1 extends CollectionKeys<T>> = QuestpieClient<T>["collections"][K$1]["create"];
26
- type CollectionUpdate<T extends Questpie<any>, K$1 extends CollectionKeys<T>> = QuestpieClient<T>["collections"][K$1]["update"];
27
- type CollectionDelete<T extends Questpie<any>, K$1 extends CollectionKeys<T>> = QuestpieClient<T>["collections"][K$1]["delete"];
28
- type CollectionRestore<T extends Questpie<any>, K$1 extends CollectionKeys<T>> = QuestpieClient<T>["collections"][K$1]["restore"];
29
- type GlobalGet<T extends Questpie<any>, K$1 extends GlobalKeys<T>> = QuestpieClient<T>["globals"][K$1] extends {
23
+ type GlobalKeys<TCMS extends Questpie<any>, TRPC extends Record<string, any>> = Extract<keyof QuestpieClient<TCMS, TRPC>["globals"], string>;
24
+ type CollectionFind<TCMS extends Questpie<any>, TRPC extends Record<string, any>, K$1 extends CollectionKeys<TCMS, TRPC>> = QuestpieClient<TCMS, TRPC>["collections"][K$1]["find"];
25
+ type CollectionCount<TCMS extends Questpie<any>, TRPC extends Record<string, any>, K$1 extends CollectionKeys<TCMS, TRPC>> = QuestpieClient<TCMS, TRPC>["collections"][K$1]["count"];
26
+ type CollectionFindOne<TCMS extends Questpie<any>, TRPC extends Record<string, any>, K$1 extends CollectionKeys<TCMS, TRPC>> = QuestpieClient<TCMS, TRPC>["collections"][K$1]["findOne"];
27
+ type CollectionCreate<TCMS extends Questpie<any>, TRPC extends Record<string, any>, K$1 extends CollectionKeys<TCMS, TRPC>> = QuestpieClient<TCMS, TRPC>["collections"][K$1]["create"];
28
+ type CollectionUpdate<TCMS extends Questpie<any>, TRPC extends Record<string, any>, K$1 extends CollectionKeys<TCMS, TRPC>> = QuestpieClient<TCMS, TRPC>["collections"][K$1]["update"];
29
+ type CollectionDelete<TCMS extends Questpie<any>, TRPC extends Record<string, any>, K$1 extends CollectionKeys<TCMS, TRPC>> = QuestpieClient<TCMS, TRPC>["collections"][K$1]["delete"];
30
+ type CollectionRestore<TCMS extends Questpie<any>, TRPC extends Record<string, any>, K$1 extends CollectionKeys<TCMS, TRPC>> = QuestpieClient<TCMS, TRPC>["collections"][K$1]["restore"];
31
+ type GlobalGet<TCMS extends Questpie<any>, TRPC extends Record<string, any>, K$1 extends GlobalKeys<TCMS, TRPC>> = QuestpieClient<TCMS, TRPC>["globals"][K$1] extends {
30
32
  get: infer TGet;
31
33
  } ? TGet extends AnyAsyncFn ? TGet : never : never;
32
- type GlobalUpdate<T extends Questpie<any>, K$1 extends GlobalKeys<T>> = QuestpieClient<T>["globals"][K$1] extends {
34
+ type GlobalUpdate<TCMS extends Questpie<any>, TRPC extends Record<string, any>, K$1 extends GlobalKeys<TCMS, TRPC>> = QuestpieClient<TCMS, TRPC>["globals"][K$1] extends {
33
35
  update: infer TUpdate;
34
36
  } ? TUpdate extends AnyAsyncFn ? TUpdate : never : never;
35
- type CollectionQueryOptionsAPI<T extends Questpie<any>, K$1 extends CollectionKeys<T>> = {
36
- find: QueryBuilder<CollectionFind<T, K$1>>;
37
- count: QueryBuilder<CollectionCount<T, K$1>>;
38
- findOne: QueryBuilder<CollectionFindOne<T, K$1>>;
39
- create: MutationBuilder<FirstArg<CollectionCreate<T, K$1>>, QueryData<CollectionCreate<T, K$1>>>;
40
- update: MutationBuilder<{
41
- id: Parameters<CollectionUpdate<T, K$1>>[0];
42
- data: Parameters<CollectionUpdate<T, K$1>>[1];
43
- }, QueryData<CollectionUpdate<T, K$1>>>;
44
- delete: MutationBuilder<{
45
- id: Parameters<CollectionDelete<T, K$1>>[0];
46
- }, QueryData<CollectionDelete<T, K$1>>>;
47
- restore: MutationBuilder<{
48
- id: Parameters<CollectionRestore<T, K$1>>[0];
49
- }, QueryData<CollectionRestore<T, K$1>>>;
37
+ type RpcProcedureQueryOptionsAPI<TFn extends AnyAsyncFn> = {
38
+ query: QueryBuilder<TFn>;
39
+ mutation: MutationBuilder<MutationVariables<TFn>, QueryData<TFn>>;
40
+ key: KeyBuilder<TFn>;
41
+ };
42
+ type RpcQueryOptionsAPI<TRpcNode> = TRpcNode extends AnyAsyncFn ? RpcProcedureQueryOptionsAPI<TRpcNode> : TRpcNode extends Record<string, any> ? { [K in keyof TRpcNode]: RpcQueryOptionsAPI<TRpcNode[K]> } : never;
43
+ type CollectionQueryOptionsAPI<TCMS extends Questpie<any>, TRPC extends Record<string, any>, K$1 extends CollectionKeys<TCMS, TRPC>> = {
44
+ find: QueryBuilder<CollectionFind<TCMS, TRPC, K$1>>;
45
+ count: QueryBuilder<CollectionCount<TCMS, TRPC, K$1>>;
46
+ findOne: QueryBuilder<CollectionFindOne<TCMS, TRPC, K$1>>;
47
+ create: MutationBuilder<FirstArg<CollectionCreate<TCMS, TRPC, K$1>>, QueryData<CollectionCreate<TCMS, TRPC, K$1>>>;
48
+ update: MutationBuilder<FirstArg<CollectionUpdate<TCMS, TRPC, K$1>>, QueryData<CollectionUpdate<TCMS, TRPC, K$1>>>;
49
+ delete: MutationBuilder<FirstArg<CollectionDelete<TCMS, TRPC, K$1>>, QueryData<CollectionDelete<TCMS, TRPC, K$1>>>;
50
+ restore: MutationBuilder<FirstArg<CollectionRestore<TCMS, TRPC, K$1>>, QueryData<CollectionRestore<TCMS, TRPC, K$1>>>;
51
+ updateMany: MutationBuilder<{
52
+ where: any;
53
+ data: any;
54
+ }, QueryData<CollectionUpdate<TCMS, TRPC, K$1>>>;
55
+ deleteMany: MutationBuilder<{
56
+ where: any;
57
+ }, {
58
+ success: boolean;
59
+ count: number;
60
+ }>;
50
61
  };
51
- type GlobalQueryOptionsAPI<T extends Questpie<any>, K$1 extends GlobalKeys<T>> = {
52
- get: QueryBuilder<GlobalGet<T, K$1>>;
62
+ type GlobalQueryOptionsAPI<TCMS extends Questpie<any>, TRPC extends Record<string, any>, K$1 extends GlobalKeys<TCMS, TRPC>> = {
63
+ get: QueryBuilder<GlobalGet<TCMS, TRPC, K$1>>;
53
64
  update: MutationBuilder<{
54
- data: Parameters<GlobalUpdate<T, K$1>>[0];
55
- options?: Parameters<GlobalUpdate<T, K$1>>[1];
56
- }, QueryData<GlobalUpdate<T, K$1>>>;
65
+ data: Parameters<GlobalUpdate<TCMS, TRPC, K$1>>[0];
66
+ options?: Parameters<GlobalUpdate<TCMS, TRPC, K$1>>[1];
67
+ }, QueryData<GlobalUpdate<TCMS, TRPC, K$1>>>;
57
68
  };
58
- type QuestpieQueryOptionsProxy<T extends Questpie<any>> = {
59
- collections: { [K in CollectionKeys<T>]: CollectionQueryOptionsAPI<T, K> };
60
- globals: { [K in GlobalKeys<T>]: GlobalQueryOptionsAPI<T, K> };
69
+ type QuestpieQueryOptionsProxy<TCMS extends Questpie<any>, TRPC extends Record<string, any> = Record<string, never>> = {
70
+ collections: { [K in CollectionKeys<TCMS, TRPC>]: CollectionQueryOptionsAPI<TCMS, TRPC, K> };
71
+ globals: { [K in GlobalKeys<TCMS, TRPC>]: GlobalQueryOptionsAPI<TCMS, TRPC, K> };
72
+ rpc: RpcQueryOptionsAPI<QuestpieClient<TCMS, TRPC>["rpc"]>;
61
73
  custom: {
62
74
  query: <TData>(config: {
63
75
  key: QueryKey;
@@ -77,17 +89,21 @@ type QuestpieQueryOptionsProxy<T extends Questpie<any>> = {
77
89
  * ```ts
78
90
  * import { createQuestpieQueryOptions } from "@questpie/tanstack-query"
79
91
  * import { createClient } from "questpie/client"
92
+ * import type { AppCMS, AppRpc } from "@/cms"
80
93
  *
81
- * const client = createClient<typeof cms>({ baseURL: "http://localhost:3000" })
82
- * const qpo = createQuestpieQueryOptions(client)
94
+ * const client = createClient<AppCMS, AppRpc>({ baseURL: "http://localhost:3000" })
95
+ * const cmsQueries = createQuestpieQueryOptions(client)
83
96
  *
84
97
  * // Use with useQuery
85
- * const { data } = useQuery(qpo.collections.posts.find({ limit: 10 }))
98
+ * const { data } = useQuery(cmsQueries.collections.posts.find({ limit: 10 }))
86
99
  *
87
100
  * // Use with useMutation
88
- * const mutation = useMutation(qpo.collections.posts.create())
101
+ * const mutation = useMutation(cmsQueries.collections.posts.create())
102
+ *
103
+ * // Use with RPC
104
+ * const stats = useQuery(cmsQueries.rpc.dashboard.getStats.query({ period: "week" }))
89
105
  * ```
90
106
  */
91
- declare function createQuestpieQueryOptions<T extends Questpie<any>>(client: QuestpieClient<T>, config?: QuestpieQueryOptionsConfig): QuestpieQueryOptionsProxy<T>;
107
+ declare function createQuestpieQueryOptions<TCMS extends Questpie<any>, TRPC extends Record<string, any> = Record<string, never>>(client: QuestpieClient<TCMS, TRPC>, config?: QuestpieQueryOptionsConfig): QuestpieQueryOptionsProxy<TCMS, TRPC>;
92
108
  //#endregion
93
- export { type DefaultError, type QueryKey, QuestpieQueryErrorMap, QuestpieQueryOptionsConfig, QuestpieQueryOptionsProxy, type UseMutationOptions, type UseQueryOptions, createQuestpieQueryOptions };
109
+ export { type DefaultError, type QueryKey, QuestpieQueryErrorMap, QuestpieQueryOptionsConfig, QuestpieQueryOptionsProxy, type RealtimeAPI, type TopicConfig, type UseMutationOptions, type UseQueryOptions, buildCollectionTopic, buildGlobalTopic, createQuestpieQueryOptions };
package/dist/index.mjs CHANGED
@@ -1,4 +1,5 @@
1
- import { mutationOptions, queryOptions } from "@tanstack/react-query";
1
+ import { experimental_streamedQuery, mutationOptions, queryOptions } from "@tanstack/react-query";
2
+ import { buildCollectionTopic, buildCollectionTopic as buildCollectionTopic$1, buildGlobalTopic, buildGlobalTopic as buildGlobalTopic$1 } from "questpie/client";
2
3
 
3
4
  //#region src/index.ts
4
5
  const defaultErrorMap = (error) => error instanceof Error ? error : new Error(typeof error === "string" ? error : "Unknown error");
@@ -44,114 +45,224 @@ const wrapMutationFn = (mutationFn, errorMap) => {
44
45
  * ```ts
45
46
  * import { createQuestpieQueryOptions } from "@questpie/tanstack-query"
46
47
  * import { createClient } from "questpie/client"
48
+ * import type { AppCMS, AppRpc } from "@/cms"
47
49
  *
48
- * const client = createClient<typeof cms>({ baseURL: "http://localhost:3000" })
49
- * const qpo = createQuestpieQueryOptions(client)
50
+ * const client = createClient<AppCMS, AppRpc>({ baseURL: "http://localhost:3000" })
51
+ * const cmsQueries = createQuestpieQueryOptions(client)
50
52
  *
51
53
  * // Use with useQuery
52
- * const { data } = useQuery(qpo.collections.posts.find({ limit: 10 }))
54
+ * const { data } = useQuery(cmsQueries.collections.posts.find({ limit: 10 }))
53
55
  *
54
56
  * // Use with useMutation
55
- * const mutation = useMutation(qpo.collections.posts.create())
57
+ * const mutation = useMutation(cmsQueries.collections.posts.create())
58
+ *
59
+ * // Use with RPC
60
+ * const stats = useQuery(cmsQueries.rpc.dashboard.getStats.query({ period: "week" }))
56
61
  * ```
57
62
  */
58
63
  function createQuestpieQueryOptions(client, config = {}) {
59
64
  const keyPrefix = config.keyPrefix ?? ["questpie"];
60
65
  const errorMap = config.errorMap ?? defaultErrorMap;
61
66
  const locale = config.locale;
62
- return {
63
- collections: new Proxy({}, { get: (_target, collectionName) => {
64
- if (typeof collectionName !== "string") return void 0;
65
- const collection = client.collections[collectionName];
66
- const baseKey = ["collections", collectionName];
67
- return {
68
- find: (options) => queryOptions({
69
- queryKey: buildKey(keyPrefix, [
70
- ...baseKey,
71
- "find",
72
- locale,
73
- normalizeQueryKeyOptions(options)
74
- ]),
67
+ const collections = new Proxy({}, { get: (_target, collectionName) => {
68
+ if (typeof collectionName !== "string") return void 0;
69
+ const collection = client.collections[collectionName];
70
+ const baseKey = ["collections", collectionName];
71
+ return {
72
+ find: (options, queryConfig) => {
73
+ const qKey = buildKey(keyPrefix, [
74
+ ...baseKey,
75
+ "find",
76
+ locale,
77
+ normalizeQueryKeyOptions(options)
78
+ ]);
79
+ if (queryConfig?.realtime && client.realtime) {
80
+ const topic = buildCollectionTopic$1(collectionName, options);
81
+ return queryOptions({
82
+ queryKey: qKey,
83
+ queryFn: experimental_streamedQuery({
84
+ streamFn: ({ signal }) => client.realtime.stream(topic, signal),
85
+ reducer: (_, chunk) => chunk,
86
+ initialValue: void 0,
87
+ refetchMode: "append"
88
+ })
89
+ });
90
+ }
91
+ return queryOptions({
92
+ queryKey: qKey,
75
93
  queryFn: wrapQueryFn(() => collection.find(options), errorMap)
76
- }),
77
- count: (options) => queryOptions({
78
- queryKey: buildKey(keyPrefix, [
79
- ...baseKey,
80
- "count",
81
- locale,
82
- normalizeQueryKeyOptions(options)
83
- ]),
94
+ });
95
+ },
96
+ count: (options, queryConfig) => {
97
+ const qKey = buildKey(keyPrefix, [
98
+ ...baseKey,
99
+ "count",
100
+ locale,
101
+ normalizeQueryKeyOptions(options)
102
+ ]);
103
+ if (queryConfig?.realtime && client.realtime) {
104
+ const topic = buildCollectionTopic$1(collectionName, options);
105
+ return queryOptions({
106
+ queryKey: qKey,
107
+ queryFn: experimental_streamedQuery({
108
+ streamFn: ({ signal }) => client.realtime.stream(topic, signal),
109
+ reducer: (_, chunk) => typeof chunk?.totalDocs === "number" ? chunk.totalDocs : chunk,
110
+ initialValue: void 0,
111
+ refetchMode: "append"
112
+ })
113
+ });
114
+ }
115
+ return queryOptions({
116
+ queryKey: qKey,
84
117
  queryFn: wrapQueryFn(() => collection.count(options), errorMap)
85
- }),
86
- findOne: (options) => queryOptions({
87
- queryKey: buildKey(keyPrefix, [
88
- ...baseKey,
89
- "findOne",
90
- locale,
91
- normalizeQueryKeyOptions(options)
92
- ]),
93
- queryFn: wrapQueryFn(() => collection.findOne(options), errorMap)
94
- }),
95
- create: () => mutationOptions({
96
- mutationKey: buildKey(keyPrefix, [
97
- ...baseKey,
98
- "create",
99
- locale
100
- ]),
101
- mutationFn: wrapMutationFn((data) => collection.create(data, locale ? { locale } : void 0), errorMap)
102
- }),
103
- update: () => mutationOptions({
104
- mutationKey: buildKey(keyPrefix, [
105
- ...baseKey,
106
- "update",
107
- locale
108
- ]),
109
- mutationFn: wrapMutationFn((variables) => collection.update(variables.id, variables.data, locale ? { locale } : void 0), errorMap)
110
- }),
111
- delete: () => mutationOptions({
112
- mutationKey: buildKey(keyPrefix, [
113
- ...baseKey,
114
- "delete",
115
- locale
116
- ]),
117
- mutationFn: wrapMutationFn((variables) => collection.delete(variables.id, locale ? { locale } : void 0), errorMap)
118
- }),
119
- restore: () => mutationOptions({
120
- mutationKey: buildKey(keyPrefix, [
121
- ...baseKey,
122
- "restore",
123
- locale
124
- ]),
125
- mutationFn: wrapMutationFn((variables) => collection.restore(variables.id, locale ? { locale } : void 0), errorMap)
126
- })
127
- };
128
- } }),
129
- globals: new Proxy({}, { get: (_target, globalName) => {
130
- if (typeof globalName !== "string") return void 0;
131
- const global = client.globals[globalName];
132
- const baseKey = ["globals", globalName];
133
- return {
134
- get: (options) => queryOptions({
135
- queryKey: buildKey(keyPrefix, [
136
- ...baseKey,
137
- "get",
138
- locale,
139
- normalizeQueryKeyOptions(options)
140
- ]),
118
+ });
119
+ },
120
+ findOne: (options) => queryOptions({
121
+ queryKey: buildKey(keyPrefix, [
122
+ ...baseKey,
123
+ "findOne",
124
+ locale,
125
+ normalizeQueryKeyOptions(options)
126
+ ]),
127
+ queryFn: wrapQueryFn(() => collection.findOne(options), errorMap)
128
+ }),
129
+ create: () => mutationOptions({
130
+ mutationKey: buildKey(keyPrefix, [
131
+ ...baseKey,
132
+ "create",
133
+ locale
134
+ ]),
135
+ mutationFn: wrapMutationFn((data) => collection.create(data, locale ? { locale } : void 0), errorMap)
136
+ }),
137
+ update: () => mutationOptions({
138
+ mutationKey: buildKey(keyPrefix, [
139
+ ...baseKey,
140
+ "update",
141
+ locale
142
+ ]),
143
+ mutationFn: wrapMutationFn((variables) => collection.update(variables, locale ? { locale } : void 0), errorMap)
144
+ }),
145
+ delete: () => mutationOptions({
146
+ mutationKey: buildKey(keyPrefix, [
147
+ ...baseKey,
148
+ "delete",
149
+ locale
150
+ ]),
151
+ mutationFn: wrapMutationFn((variables) => collection.delete(variables, locale ? { locale } : void 0), errorMap)
152
+ }),
153
+ restore: () => mutationOptions({
154
+ mutationKey: buildKey(keyPrefix, [
155
+ ...baseKey,
156
+ "restore",
157
+ locale
158
+ ]),
159
+ mutationFn: wrapMutationFn((variables) => collection.restore(variables, locale ? { locale } : void 0), errorMap)
160
+ }),
161
+ updateMany: () => mutationOptions({
162
+ mutationKey: buildKey(keyPrefix, [
163
+ ...baseKey,
164
+ "updateMany",
165
+ locale
166
+ ]),
167
+ mutationFn: wrapMutationFn((variables) => collection.updateMany(variables, locale ? { locale } : void 0), errorMap)
168
+ }),
169
+ deleteMany: () => mutationOptions({
170
+ mutationKey: buildKey(keyPrefix, [
171
+ ...baseKey,
172
+ "deleteMany",
173
+ locale
174
+ ]),
175
+ mutationFn: wrapMutationFn((variables) => collection.deleteMany(variables, locale ? { locale } : void 0), errorMap)
176
+ })
177
+ };
178
+ } });
179
+ const globals = new Proxy({}, { get: (_target, globalName) => {
180
+ if (typeof globalName !== "string") return void 0;
181
+ const global = client.globals[globalName];
182
+ const baseKey = ["globals", globalName];
183
+ return {
184
+ get: (options, queryConfig) => {
185
+ const qKey = buildKey(keyPrefix, [
186
+ ...baseKey,
187
+ "get",
188
+ locale,
189
+ normalizeQueryKeyOptions(options)
190
+ ]);
191
+ if (queryConfig?.realtime && client.realtime) {
192
+ const topic = buildGlobalTopic$1(globalName, options);
193
+ return queryOptions({
194
+ queryKey: qKey,
195
+ queryFn: experimental_streamedQuery({
196
+ streamFn: ({ signal }) => client.realtime.stream(topic, signal),
197
+ reducer: (_, chunk) => chunk,
198
+ initialValue: void 0,
199
+ refetchMode: "append"
200
+ })
201
+ });
202
+ }
203
+ return queryOptions({
204
+ queryKey: qKey,
141
205
  queryFn: wrapQueryFn(() => global.get(options), errorMap)
142
- }),
143
- update: () => mutationOptions({
144
- mutationKey: buildKey(keyPrefix, [
145
- ...baseKey,
146
- "update",
147
- locale
148
- ]),
149
- mutationFn: wrapMutationFn((variables) => global.update(variables.data, {
150
- ...variables.options,
151
- ...locale ? { locale } : void 0
152
- }), errorMap)
153
- })
154
- };
206
+ });
207
+ },
208
+ update: () => mutationOptions({
209
+ mutationKey: buildKey(keyPrefix, [
210
+ ...baseKey,
211
+ "update",
212
+ locale
213
+ ]),
214
+ mutationFn: wrapMutationFn((variables) => global.update(variables.data, {
215
+ ...variables.options,
216
+ ...locale ? { locale } : void 0
217
+ }), errorMap)
218
+ })
219
+ };
220
+ } });
221
+ const callRpcProcedure = async (segments, input) => {
222
+ let current = client.rpc;
223
+ for (const segment of segments) current = current?.[segment];
224
+ if (typeof current !== "function") throw new Error(`RPC procedure not found at path: ${segments.join(".") || "<root>"}`);
225
+ if (input === void 0) return current();
226
+ return current(input);
227
+ };
228
+ const createRpcNodeProxy = (segments) => {
229
+ return new Proxy({}, { get: (_target, prop) => {
230
+ if (prop === "query") return (input) => queryOptions({
231
+ queryKey: buildKey(keyPrefix, [
232
+ "rpc",
233
+ ...segments,
234
+ "query",
235
+ locale,
236
+ normalizeQueryKeyOptions(input)
237
+ ]),
238
+ queryFn: wrapQueryFn(() => callRpcProcedure(segments, input), errorMap)
239
+ });
240
+ if (prop === "mutation") return () => mutationOptions({
241
+ mutationKey: buildKey(keyPrefix, [
242
+ "rpc",
243
+ ...segments,
244
+ "mutation",
245
+ locale
246
+ ]),
247
+ mutationFn: wrapMutationFn((variables) => callRpcProcedure(segments, variables), errorMap)
248
+ });
249
+ if (prop === "key") return (input) => buildKey(keyPrefix, [
250
+ "rpc",
251
+ ...segments,
252
+ "query",
253
+ locale,
254
+ normalizeQueryKeyOptions(input)
255
+ ]);
256
+ if (typeof prop !== "string") return void 0;
257
+ return createRpcNodeProxy([...segments, prop]);
258
+ } });
259
+ };
260
+ return {
261
+ collections,
262
+ globals,
263
+ rpc: new Proxy({}, { get: (_target, prop) => {
264
+ if (typeof prop !== "string") return void 0;
265
+ return createRpcNodeProxy([prop]);
155
266
  } }),
156
267
  custom: {
157
268
  query: (customConfig) => queryOptions({
@@ -168,4 +279,4 @@ function createQuestpieQueryOptions(client, config = {}) {
168
279
  }
169
280
 
170
281
  //#endregion
171
- export { createQuestpieQueryOptions };
282
+ export { buildCollectionTopic, buildGlobalTopic, createQuestpieQueryOptions };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@questpie/tanstack-query",
3
- "version": "1.1.1",
3
+ "version": "2.0.0",
4
4
  "type": "module",
5
5
  "repository": {
6
6
  "type": "git",
@@ -19,16 +19,21 @@
19
19
  "tsdown": "^0.18.3"
20
20
  },
21
21
  "peerDependencies": {
22
- "questpie": "^1.0.0",
22
+ "questpie": "^2.0.0",
23
23
  "@tanstack/react-query": "^5.0.0"
24
24
  },
25
25
  "exports": {
26
- ".": {
27
- "import": "./dist/index.mjs",
28
- "types": "./dist/index.d.mts"
29
- }
26
+ ".": "./dist/index.mjs",
27
+ "./package.json": "./package.json"
30
28
  },
31
29
  "publishConfig": {
32
- "access": "public"
33
- }
30
+ "access": "public",
31
+ "exports": {
32
+ ".": "./dist/index.mjs",
33
+ "./package.json": "./package.json"
34
+ }
35
+ },
36
+ "main": "./dist/index.mjs",
37
+ "module": "./dist/index.mjs",
38
+ "types": "./dist/index.d.mts"
34
39
  }