@questpie/tanstack-query 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/README.md +303 -0
- package/dist/index.d.mts +93 -0
- package/dist/index.mjs +171 -0
- package/package.json +35 -0
package/README.md
ADDED
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
# @questpie/tanstack-query
|
|
2
|
+
|
|
3
|
+
Type-safe TanStack React Query integration for QUESTPIE.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
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
|
|
10
|
+
|
|
11
|
+
## Installation
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
bun add @questpie/tanstack-query questpie @tanstack/react-query
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## Quick Start
|
|
18
|
+
|
|
19
|
+
### 1. Create Query Options
|
|
20
|
+
|
|
21
|
+
```typescript
|
|
22
|
+
// src/lib/queries.ts
|
|
23
|
+
import { createClient } from "questpie/client";
|
|
24
|
+
import { createQuestpieQueryOptions } from "@questpie/tanstack-query";
|
|
25
|
+
import type { AppCMS } from "@/cms";
|
|
26
|
+
|
|
27
|
+
const cmsClient = createClient<AppCMS>({
|
|
28
|
+
baseURL: "http://localhost:3000",
|
|
29
|
+
basePath: "/api/cms",
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
export const q = createQuestpieQueryOptions(cmsClient, {
|
|
33
|
+
keyPrefix: ["cms"], // Optional: prefix for all query keys
|
|
34
|
+
});
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
### 2. Use in Components
|
|
38
|
+
|
|
39
|
+
```typescript
|
|
40
|
+
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"
|
|
41
|
+
import { q } from "@/lib/queries"
|
|
42
|
+
|
|
43
|
+
function PostsList() {
|
|
44
|
+
// Queries
|
|
45
|
+
const { data: posts } = useQuery(q.collections.posts.find({ limit: 10 }))
|
|
46
|
+
|
|
47
|
+
// Mutations
|
|
48
|
+
const queryClient = useQueryClient()
|
|
49
|
+
|
|
50
|
+
const createPost = useMutation({
|
|
51
|
+
...q.collections.posts.create(),
|
|
52
|
+
onSuccess: () => {
|
|
53
|
+
queryClient.invalidateQueries({ queryKey: ["cms", "collections", "posts"] })
|
|
54
|
+
},
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
return (
|
|
58
|
+
<div>
|
|
59
|
+
{posts?.docs.map((post) => (
|
|
60
|
+
<div key={post.id}>{post.title}</div>
|
|
61
|
+
))}
|
|
62
|
+
<button onClick={() => createPost.mutate({ title: "New Post" })}>
|
|
63
|
+
Create Post
|
|
64
|
+
</button>
|
|
65
|
+
</div>
|
|
66
|
+
)
|
|
67
|
+
}
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
## Query Options API
|
|
71
|
+
|
|
72
|
+
### Collections
|
|
73
|
+
|
|
74
|
+
```typescript
|
|
75
|
+
// Find many
|
|
76
|
+
q.collections.posts.find({
|
|
77
|
+
where: { published: { eq: true } },
|
|
78
|
+
orderBy: { createdAt: "desc" },
|
|
79
|
+
limit: 10,
|
|
80
|
+
offset: 0,
|
|
81
|
+
with: { author: true },
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
// Find one
|
|
85
|
+
q.collections.posts.findOne({
|
|
86
|
+
where: { id: "post-id" },
|
|
87
|
+
with: { author: true, comments: true },
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
// Create
|
|
91
|
+
q.collections.posts.create();
|
|
92
|
+
// Usage: mutation.mutate({ title: "...", content: "..." })
|
|
93
|
+
|
|
94
|
+
// Update
|
|
95
|
+
q.collections.posts.update();
|
|
96
|
+
// Usage: mutation.mutate({ id: "...", data: { title: "..." } })
|
|
97
|
+
|
|
98
|
+
// Delete
|
|
99
|
+
q.collections.posts.delete();
|
|
100
|
+
// Usage: mutation.mutate({ id: "..." })
|
|
101
|
+
|
|
102
|
+
// Restore (soft delete)
|
|
103
|
+
q.collections.posts.restore();
|
|
104
|
+
// Usage: mutation.mutate({ id: "..." })
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
### Globals
|
|
108
|
+
|
|
109
|
+
```typescript
|
|
110
|
+
// Get
|
|
111
|
+
q.globals.siteSettings.get();
|
|
112
|
+
|
|
113
|
+
// Update
|
|
114
|
+
q.globals.siteSettings.update();
|
|
115
|
+
// Usage: mutation.mutate({ data: { siteName: "..." } })
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
### Custom Queries
|
|
119
|
+
|
|
120
|
+
```typescript
|
|
121
|
+
const searchQuery = q.custom.query({
|
|
122
|
+
key: ["search", query],
|
|
123
|
+
queryFn: () => fetch(`/api/search?q=${query}`).then((r) => r.json()),
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
useQuery(searchQuery);
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
## SSR Prefetching
|
|
130
|
+
|
|
131
|
+
### TanStack Start
|
|
132
|
+
|
|
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"
|
|
138
|
+
|
|
139
|
+
export const Route = createFileRoute("/posts")({
|
|
140
|
+
loader: async ({ context }) => {
|
|
141
|
+
await context.queryClient.ensureQueryData(
|
|
142
|
+
q.collections.posts.find({ limit: 10 })
|
|
143
|
+
)
|
|
144
|
+
},
|
|
145
|
+
component: PostsPage,
|
|
146
|
+
})
|
|
147
|
+
|
|
148
|
+
function PostsPage() {
|
|
149
|
+
const { data: posts } = useSuspenseQuery(
|
|
150
|
+
q.collections.posts.find({ limit: 10 })
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
return (
|
|
154
|
+
<ul>
|
|
155
|
+
{posts.docs.map((post) => (
|
|
156
|
+
<li key={post.id}>{post.title}</li>
|
|
157
|
+
))}
|
|
158
|
+
</ul>
|
|
159
|
+
)
|
|
160
|
+
}
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
### Next.js
|
|
164
|
+
|
|
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"
|
|
171
|
+
|
|
172
|
+
export default async function PostsPage() {
|
|
173
|
+
const queryClient = getQueryClient()
|
|
174
|
+
|
|
175
|
+
await queryClient.prefetchQuery(
|
|
176
|
+
q.collections.posts.find({ limit: 10 })
|
|
177
|
+
)
|
|
178
|
+
|
|
179
|
+
return (
|
|
180
|
+
<HydrationBoundary state={dehydrate(queryClient)}>
|
|
181
|
+
<PostsList />
|
|
182
|
+
</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
|
+
)
|
|
259
|
+
}
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
## Query Key Structure
|
|
263
|
+
|
|
264
|
+
All query keys follow this structure:
|
|
265
|
+
|
|
266
|
+
```typescript
|
|
267
|
+
// Collections
|
|
268
|
+
[...keyPrefix, "collections", collectionName, "find", options]
|
|
269
|
+
[...keyPrefix, "collections", collectionName, "findOne", options]
|
|
270
|
+
|
|
271
|
+
// 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"]>;
|
|
288
|
+
|
|
289
|
+
// Query result types are automatically inferred
|
|
290
|
+
const { data } = useQuery(q.collections.posts.find({ limit: 10 }));
|
|
291
|
+
// ^? { docs: Post[], totalDocs: number, ... }
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
## Related Packages
|
|
295
|
+
|
|
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
|
|
300
|
+
|
|
301
|
+
## License
|
|
302
|
+
|
|
303
|
+
MIT
|
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { DefaultError, QueryKey, UseMutationOptions, UseQueryOptions } from "@tanstack/react-query";
|
|
2
|
+
import { Questpie } from "questpie";
|
|
3
|
+
import { QuestpieClient } from "questpie/client";
|
|
4
|
+
|
|
5
|
+
//#region src/index.d.ts
|
|
6
|
+
type QuestpieQueryErrorMap = (error: unknown) => unknown;
|
|
7
|
+
type QuestpieQueryOptionsConfig = {
|
|
8
|
+
keyPrefix?: QueryKey;
|
|
9
|
+
errorMap?: QuestpieQueryErrorMap;
|
|
10
|
+
locale?: string;
|
|
11
|
+
};
|
|
12
|
+
type AnyAsyncFn = (...args: any[]) => Promise<any>;
|
|
13
|
+
type QueryData<TFn extends AnyAsyncFn> = Awaited<ReturnType<TFn>>;
|
|
14
|
+
type FirstArg<TFn extends AnyAsyncFn> = Parameters<TFn> extends [infer TFirst, ...any[]] ? TFirst : never;
|
|
15
|
+
type HasArgs<TFn extends AnyAsyncFn> = Parameters<TFn> extends [] ? false : true;
|
|
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 MutationBuilder<TVariables, TData> = () => UseMutationOptions<TData, DefaultError, TVariables>;
|
|
18
|
+
/** Extract collection keys from QuestpieClient */
|
|
19
|
+
type CollectionKeys<T extends Questpie<any>> = Extract<keyof QuestpieClient<T>["collections"], string>;
|
|
20
|
+
/** 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 {
|
|
30
|
+
get: infer TGet;
|
|
31
|
+
} ? TGet extends AnyAsyncFn ? TGet : never : never;
|
|
32
|
+
type GlobalUpdate<T extends Questpie<any>, K$1 extends GlobalKeys<T>> = QuestpieClient<T>["globals"][K$1] extends {
|
|
33
|
+
update: infer TUpdate;
|
|
34
|
+
} ? 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>>>;
|
|
50
|
+
};
|
|
51
|
+
type GlobalQueryOptionsAPI<T extends Questpie<any>, K$1 extends GlobalKeys<T>> = {
|
|
52
|
+
get: QueryBuilder<GlobalGet<T, K$1>>;
|
|
53
|
+
update: MutationBuilder<{
|
|
54
|
+
data: Parameters<GlobalUpdate<T, K$1>>[0];
|
|
55
|
+
options?: Parameters<GlobalUpdate<T, K$1>>[1];
|
|
56
|
+
}, QueryData<GlobalUpdate<T, K$1>>>;
|
|
57
|
+
};
|
|
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> };
|
|
61
|
+
custom: {
|
|
62
|
+
query: <TData>(config: {
|
|
63
|
+
key: QueryKey;
|
|
64
|
+
queryFn: () => Promise<TData>;
|
|
65
|
+
}) => UseQueryOptions<TData>;
|
|
66
|
+
mutation: <TVariables, TData>(config: {
|
|
67
|
+
key: QueryKey;
|
|
68
|
+
mutationFn: (variables: TVariables) => Promise<TData>;
|
|
69
|
+
}) => UseMutationOptions<TData, DefaultError, TVariables>;
|
|
70
|
+
};
|
|
71
|
+
key: (parts: QueryKey) => QueryKey;
|
|
72
|
+
};
|
|
73
|
+
/**
|
|
74
|
+
* Create type-safe query options proxy for TanStack Query
|
|
75
|
+
*
|
|
76
|
+
* @example
|
|
77
|
+
* ```ts
|
|
78
|
+
* import { createQuestpieQueryOptions } from "@questpie/tanstack-query"
|
|
79
|
+
* import { createClient } from "questpie/client"
|
|
80
|
+
*
|
|
81
|
+
* const client = createClient<typeof cms>({ baseURL: "http://localhost:3000" })
|
|
82
|
+
* const qpo = createQuestpieQueryOptions(client)
|
|
83
|
+
*
|
|
84
|
+
* // Use with useQuery
|
|
85
|
+
* const { data } = useQuery(qpo.collections.posts.find({ limit: 10 }))
|
|
86
|
+
*
|
|
87
|
+
* // Use with useMutation
|
|
88
|
+
* const mutation = useMutation(qpo.collections.posts.create())
|
|
89
|
+
* ```
|
|
90
|
+
*/
|
|
91
|
+
declare function createQuestpieQueryOptions<T extends Questpie<any>>(client: QuestpieClient<T>, config?: QuestpieQueryOptionsConfig): QuestpieQueryOptionsProxy<T>;
|
|
92
|
+
//#endregion
|
|
93
|
+
export { type DefaultError, type QueryKey, QuestpieQueryErrorMap, QuestpieQueryOptionsConfig, QuestpieQueryOptionsProxy, type UseMutationOptions, type UseQueryOptions, createQuestpieQueryOptions };
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import { mutationOptions, queryOptions } from "@tanstack/react-query";
|
|
2
|
+
|
|
3
|
+
//#region src/index.ts
|
|
4
|
+
const defaultErrorMap = (error) => error instanceof Error ? error : new Error(typeof error === "string" ? error : "Unknown error");
|
|
5
|
+
const buildKey = (prefix, parts) => prefix.length ? [...prefix, ...parts] : parts;
|
|
6
|
+
const sanitizeKeyPart = (value) => {
|
|
7
|
+
if (value === null || value === void 0) return value;
|
|
8
|
+
if (typeof value === "function") return void 0;
|
|
9
|
+
if (Array.isArray(value)) return value.map((item) => sanitizeKeyPart(item));
|
|
10
|
+
if (typeof value === "object") {
|
|
11
|
+
const sanitized = {};
|
|
12
|
+
for (const [key, entry] of Object.entries(value)) {
|
|
13
|
+
if (typeof entry === "function") continue;
|
|
14
|
+
const nextValue = sanitizeKeyPart(entry);
|
|
15
|
+
if (nextValue !== void 0) sanitized[key] = nextValue;
|
|
16
|
+
}
|
|
17
|
+
return sanitized;
|
|
18
|
+
}
|
|
19
|
+
return value;
|
|
20
|
+
};
|
|
21
|
+
const normalizeQueryKeyOptions = (options) => sanitizeKeyPart(options ?? {});
|
|
22
|
+
const wrapQueryFn = (queryFn, errorMap) => {
|
|
23
|
+
return async () => {
|
|
24
|
+
try {
|
|
25
|
+
return await queryFn();
|
|
26
|
+
} catch (error) {
|
|
27
|
+
throw errorMap(error);
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
};
|
|
31
|
+
const wrapMutationFn = (mutationFn, errorMap) => {
|
|
32
|
+
return async (variables) => {
|
|
33
|
+
try {
|
|
34
|
+
return await mutationFn(variables);
|
|
35
|
+
} catch (error) {
|
|
36
|
+
throw errorMap(error);
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
};
|
|
40
|
+
/**
|
|
41
|
+
* Create type-safe query options proxy for TanStack Query
|
|
42
|
+
*
|
|
43
|
+
* @example
|
|
44
|
+
* ```ts
|
|
45
|
+
* import { createQuestpieQueryOptions } from "@questpie/tanstack-query"
|
|
46
|
+
* import { createClient } from "questpie/client"
|
|
47
|
+
*
|
|
48
|
+
* const client = createClient<typeof cms>({ baseURL: "http://localhost:3000" })
|
|
49
|
+
* const qpo = createQuestpieQueryOptions(client)
|
|
50
|
+
*
|
|
51
|
+
* // Use with useQuery
|
|
52
|
+
* const { data } = useQuery(qpo.collections.posts.find({ limit: 10 }))
|
|
53
|
+
*
|
|
54
|
+
* // Use with useMutation
|
|
55
|
+
* const mutation = useMutation(qpo.collections.posts.create())
|
|
56
|
+
* ```
|
|
57
|
+
*/
|
|
58
|
+
function createQuestpieQueryOptions(client, config = {}) {
|
|
59
|
+
const keyPrefix = config.keyPrefix ?? ["questpie"];
|
|
60
|
+
const errorMap = config.errorMap ?? defaultErrorMap;
|
|
61
|
+
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
|
+
]),
|
|
75
|
+
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
|
+
]),
|
|
84
|
+
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
|
+
]),
|
|
141
|
+
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
|
+
};
|
|
155
|
+
} }),
|
|
156
|
+
custom: {
|
|
157
|
+
query: (customConfig) => queryOptions({
|
|
158
|
+
queryKey: buildKey(keyPrefix, customConfig.key),
|
|
159
|
+
queryFn: wrapQueryFn(customConfig.queryFn, errorMap)
|
|
160
|
+
}),
|
|
161
|
+
mutation: (customConfig) => mutationOptions({
|
|
162
|
+
mutationKey: buildKey(keyPrefix, customConfig.key),
|
|
163
|
+
mutationFn: wrapMutationFn(customConfig.mutationFn, errorMap)
|
|
164
|
+
})
|
|
165
|
+
},
|
|
166
|
+
key: (parts) => buildKey(keyPrefix, parts)
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
//#endregion
|
|
171
|
+
export { createQuestpieQueryOptions };
|
package/package.json
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@questpie/tanstack-query",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"repository": {
|
|
6
|
+
"type": "git",
|
|
7
|
+
"url": "https://github.com/questpie/questpie-cms.git",
|
|
8
|
+
"directory": "packages/tanstack-query"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"dist"
|
|
12
|
+
],
|
|
13
|
+
"scripts": {
|
|
14
|
+
"build": "tsdown",
|
|
15
|
+
"check-types": "tsc --noEmit"
|
|
16
|
+
},
|
|
17
|
+
"devDependencies": {
|
|
18
|
+
"@questpie/typescript-config": "workspace:*",
|
|
19
|
+
"bun-types": "latest",
|
|
20
|
+
"tsdown": "^0.18.3"
|
|
21
|
+
},
|
|
22
|
+
"peerDependencies": {
|
|
23
|
+
"questpie": "workspace:*",
|
|
24
|
+
"@tanstack/react-query": "^5.0.0"
|
|
25
|
+
},
|
|
26
|
+
"exports": {
|
|
27
|
+
".": {
|
|
28
|
+
"import": "./dist/index.mjs",
|
|
29
|
+
"types": "./dist/index.d.mts"
|
|
30
|
+
}
|
|
31
|
+
},
|
|
32
|
+
"publishConfig": {
|
|
33
|
+
"access": "public"
|
|
34
|
+
}
|
|
35
|
+
}
|