create-questpie 2.0.1 → 2.0.2

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.
Files changed (37) hide show
  1. package/README.md +10 -6
  2. package/dist/index.mjs +139 -24
  3. package/package.json +5 -3
  4. package/skills/questpie/AGENTS.md +2664 -0
  5. package/skills/questpie/SKILL.md +181 -0
  6. package/skills/questpie/references/auth.md +121 -0
  7. package/skills/questpie/references/business-logic.md +550 -0
  8. package/skills/questpie/references/codegen-plugin-api.md +382 -0
  9. package/skills/questpie/references/crud-api.md +378 -0
  10. package/skills/questpie/references/data-modeling.md +489 -0
  11. package/skills/questpie/references/extend.md +493 -0
  12. package/skills/questpie/references/field-types.md +386 -0
  13. package/skills/questpie/references/infrastructure-adapters.md +545 -0
  14. package/skills/questpie/references/multi-tenancy.md +364 -0
  15. package/skills/questpie/references/production.md +475 -0
  16. package/skills/questpie/references/query-operators.md +125 -0
  17. package/skills/questpie/references/quickstart.md +549 -0
  18. package/skills/questpie/references/rules.md +327 -0
  19. package/skills/questpie/references/tanstack-query.md +520 -0
  20. package/skills/questpie-admin/AGENTS.md +1442 -0
  21. package/skills/questpie-admin/SKILL.md +410 -0
  22. package/skills/questpie-admin/references/blocks.md +307 -0
  23. package/skills/questpie-admin/references/custom-ui.md +305 -0
  24. package/skills/questpie-admin/references/views.md +433 -0
  25. package/templates/tanstack-start/AGENTS.md +17 -13
  26. package/templates/tanstack-start/CLAUDE.md +15 -12
  27. package/templates/tanstack-start/README.md +19 -13
  28. package/templates/tanstack-start/env.example +1 -1
  29. package/templates/tanstack-start/package.json +20 -6
  30. package/templates/tanstack-start/src/lib/env.ts +1 -1
  31. package/templates/tanstack-start/src/questpie/server/config/admin.ts +27 -30
  32. package/templates/tanstack-start/src/routeTree.gen.ts +138 -0
  33. package/templates/tanstack-start/src/routes/__root.tsx +0 -2
  34. package/templates/tanstack-start/src/routes/admin.tsx +8 -1
  35. package/templates/tanstack-start/src/tanstack-start.d.ts +1 -0
  36. package/templates/tanstack-start/src/vite-env.d.ts +1 -0
  37. package/templates/tanstack-start/vite.config.ts +1 -3
@@ -0,0 +1,520 @@
1
+ ---
2
+ name: questpie-tanstack-query
3
+ description: QUESTPIE TanStack Query integration - createQuestpieQueryOptions option builders, useQuery useMutation queryOptions mutationOptions, collections globals routes, streamedQuery SSE realtime subscriptions, batch helpers, type inference AppConfig createClient, React data fetching caching, framework adapters TanStack Start Next.js Hono Elysia, frontend client SDK querying where orderBy pagination with select
4
+ - questpie-core
5
+ ---
6
+
7
+ ## Overview
8
+
9
+ `@questpie/tanstack-query` provides type-safe TanStack Query option builders for QUESTPIE. It creates `queryOptions()` and `mutationOptions()` objects that you pass directly to `useQuery()` and `useMutation()`. Full type inference flows from your server schema to React components.
10
+
11
+ ## Installation
12
+
13
+ ```bash
14
+ bun add @questpie/tanstack-query @tanstack/react-query
15
+ ```
16
+
17
+ ## Setup
18
+
19
+ ### 1. Create the QUESTPIE Client
20
+
21
+ ```ts title="lib/client.ts"
22
+ import { createClient } from "questpie/client";
23
+ import type { AppConfig } from "#questpie";
24
+
25
+ export const client = createClient<AppConfig>({
26
+ baseURL:
27
+ typeof window !== "undefined"
28
+ ? window.location.origin
29
+ : process.env.APP_URL || "http://localhost:3000",
30
+ basePath: "/api",
31
+ });
32
+ ```
33
+
34
+ ### 2. Create Query Options Proxy
35
+
36
+ ```ts title="lib/queries.ts"
37
+ import { createQuestpieQueryOptions } from "@questpie/tanstack-query";
38
+ import { client } from "./client";
39
+
40
+ export const q = createQuestpieQueryOptions(client, {
41
+ keyPrefix: ["questpie"], // optional, default: ["questpie"]
42
+ locale: "en", // optional, sets locale for all queries
43
+ stage: undefined, // optional, workflow stage filter
44
+ });
45
+ ```
46
+
47
+ ### 3. Wrap App with QueryClientProvider
48
+
49
+ ```tsx title="app.tsx"
50
+ import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
51
+
52
+ const queryClient = new QueryClient();
53
+
54
+ function App() {
55
+ return (
56
+ <QueryClientProvider client={queryClient}>
57
+ <YourApp />
58
+ </QueryClientProvider>
59
+ );
60
+ }
61
+ ```
62
+
63
+ ## Collection Queries
64
+
65
+ ### Find (list)
66
+
67
+ ```tsx
68
+ import { useQuery } from "@tanstack/react-query";
69
+ import { q } from "@/lib/queries";
70
+
71
+ function PostList() {
72
+ const { data, isLoading } = useQuery(
73
+ q.collections.posts.find({
74
+ where: { status: "published" },
75
+ orderBy: { createdAt: "desc" },
76
+ limit: 10,
77
+ offset: 0,
78
+ }),
79
+ );
80
+
81
+ if (isLoading) return <div>Loading...</div>;
82
+
83
+ return (
84
+ <ul>
85
+ {data?.docs.map((post) => (
86
+ <li key={post.id}>{post.title}</li>
87
+ ))}
88
+ <p>Total: {data?.totalDocs}</p>
89
+ </ul>
90
+ );
91
+ }
92
+ ```
93
+
94
+ ### Find with Realtime
95
+
96
+ Pass `{ realtime: true }` as the second argument to enable SSE-based live updates via `streamedQuery`:
97
+
98
+ ```tsx
99
+ function LivePostList() {
100
+ const { data } = useQuery(
101
+ q.collections.posts.find(
102
+ { where: { status: "published" }, limit: 20 },
103
+ { realtime: true },
104
+ ),
105
+ );
106
+ // data auto-updates when posts change on the server
107
+ return (
108
+ <ul>
109
+ {data?.docs.map((p) => (
110
+ <li key={p.id}>{p.title}</li>
111
+ ))}
112
+ </ul>
113
+ );
114
+ }
115
+ ```
116
+
117
+ ### Find One
118
+
119
+ ```tsx
120
+ function PostDetail({ id }: { id: string }) {
121
+ const { data: post } = useQuery(
122
+ q.collections.posts.findOne({
123
+ where: { id },
124
+ with: { author: true, categories: true },
125
+ }),
126
+ );
127
+
128
+ if (!post) return null;
129
+ return <article>{post.title}</article>;
130
+ }
131
+ ```
132
+
133
+ ### Count
134
+
135
+ ```tsx
136
+ const { data: count } = useQuery(
137
+ q.collections.posts.count({ where: { status: "draft" } }),
138
+ );
139
+ ```
140
+
141
+ ## Collection Mutations
142
+
143
+ ### Create
144
+
145
+ ```tsx
146
+ import { useMutation, useQueryClient } from "@tanstack/react-query";
147
+
148
+ function CreatePostForm() {
149
+ const queryClient = useQueryClient();
150
+ const create = useMutation({
151
+ ...q.collections.posts.create(),
152
+ onSuccess: () => {
153
+ queryClient.invalidateQueries({
154
+ queryKey: ["questpie", "collections", "posts"],
155
+ });
156
+ },
157
+ });
158
+
159
+ return (
160
+ <form
161
+ onSubmit={(e) => {
162
+ e.preventDefault();
163
+ create.mutate({
164
+ title: "New Post",
165
+ body: "Content here",
166
+ status: "draft",
167
+ });
168
+ }}
169
+ >
170
+ <button type="submit">Create</button>
171
+ </form>
172
+ );
173
+ }
174
+ ```
175
+
176
+ ### Update
177
+
178
+ ```tsx
179
+ const update = useMutation(q.collections.posts.update());
180
+
181
+ update.mutate({ id: "post-id", data: { status: "published" } });
182
+ ```
183
+
184
+ ### Delete
185
+
186
+ ```tsx
187
+ const remove = useMutation(q.collections.posts.delete());
188
+
189
+ remove.mutate({ id: "post-id" });
190
+ ```
191
+
192
+ ### Bulk Operations
193
+
194
+ ```tsx
195
+ // Update many
196
+ const updateMany = useMutation(q.collections.posts.updateMany());
197
+ updateMany.mutate({ where: { status: "draft" }, data: { status: "archived" } });
198
+
199
+ // Delete many
200
+ const deleteMany = useMutation(q.collections.posts.deleteMany());
201
+ deleteMany.mutate({ where: { status: "archived" } });
202
+ ```
203
+
204
+ ### Versioning and Workflow Stages
205
+
206
+ ```tsx
207
+ const { data: versions } = useQuery(
208
+ q.collections.posts.findVersions({ id: "post-id", limit: 10 }),
209
+ );
210
+ const revert = useMutation(q.collections.posts.revertToVersion());
211
+ revert.mutate({ id: "post-id", version: 3 });
212
+ const transition = useMutation(q.collections.posts.transitionStage());
213
+ transition.mutate({ id: "post-id", stage: "published" });
214
+ ```
215
+
216
+ ## Global Queries
217
+
218
+ ```tsx
219
+ function SiteSettings() {
220
+ const { data: settings } = useQuery(q.globals.siteSettings.get());
221
+ const update = useMutation(q.globals.siteSettings.update());
222
+
223
+ return (
224
+ <div>
225
+ <h1>{settings?.shopName}</h1>
226
+ <button onClick={() => update.mutate({ shopName: "New Name" })}>
227
+ Update
228
+ </button>
229
+ </div>
230
+ );
231
+ }
232
+ ```
233
+
234
+ ### Globals with Realtime
235
+
236
+ ```tsx
237
+ const { data } = useQuery(
238
+ q.globals.siteSettings.get(undefined, { realtime: true }),
239
+ );
240
+ ```
241
+
242
+ ## Routes
243
+
244
+ Route calls support nested namespaces matching your `routes/` directory structure.
245
+
246
+ ```tsx
247
+ // routes/get-stats.ts -> routes.getStats
248
+ const { data: stats } = useQuery(q.routes.getStats.query({ period: "week" }));
249
+
250
+ // routes/booking/create.ts -> routes.booking.create
251
+ const createBooking = useMutation(q.routes.booking.create.mutation());
252
+
253
+ createBooking.mutate({
254
+ barberId: "abc",
255
+ serviceId: "def",
256
+ scheduledAt: "2025-03-15T10:00:00Z",
257
+ });
258
+ ```
259
+
260
+ ### Route Query Keys
261
+
262
+ Access query keys for manual invalidation:
263
+
264
+ ```tsx
265
+ const queryClient = useQueryClient();
266
+
267
+ // Get the query key for a specific route call
268
+ const key = q.routes.getStats.key({ period: "week" });
269
+ queryClient.invalidateQueries({ queryKey: key });
270
+ ```
271
+
272
+ ## Custom Queries
273
+
274
+ For queries that don't fit the standard collection/global/route pattern:
275
+
276
+ ```tsx
277
+ const { data } = useQuery(
278
+ q.custom.query({
279
+ key: ["custom", "analytics"],
280
+ queryFn: () => fetch("/analytics").then((r) => r.json()),
281
+ }),
282
+ );
283
+
284
+ const mutation = useMutation(
285
+ q.custom.mutation({
286
+ key: ["custom", "import"],
287
+ mutationFn: (file: File) => uploadFile(file),
288
+ }),
289
+ );
290
+ ```
291
+
292
+ ## Key Builder
293
+
294
+ Build prefixed query keys for manual cache operations:
295
+
296
+ ```tsx
297
+ const key = q.key(["collections", "posts"]);
298
+ // -> ["questpie", "collections", "posts"]
299
+
300
+ queryClient.invalidateQueries({ queryKey: key });
301
+ ```
302
+
303
+ ## Query Operators (Where Clauses)
304
+
305
+ All operators are type-safe based on your field definitions:
306
+
307
+ ```ts
308
+ // Equality
309
+ where: { status: "published" }
310
+
311
+ // Comparison
312
+ where: { price: { gt: 1000, lte: 5000 } }
313
+
314
+ // Date ranges
315
+ where: { createdAt: { gte: new Date("2025-01-01"), lte: new Date("2025-12-31") } }
316
+
317
+ // Text operations
318
+ where: { title: { contains: "hello" } }
319
+ where: { email: { startsWith: "john" } }
320
+
321
+ // In
322
+ where: { status: { in: ["draft", "published"] } }
323
+
324
+ // Relations
325
+ where: { author: "user-id-123" }
326
+ ```
327
+
328
+ ### Operators by Field Type
329
+
330
+ | Field Type | Operators |
331
+ | ------------------- | ---------------------------------------------------- |
332
+ | `text` | `equals`, `contains`, `startsWith`, `endsWith`, `in` |
333
+ | `number` | `equals`, `gt`, `gte`, `lt`, `lte`, `in` |
334
+ | `boolean` | `equals` |
335
+ | `date` / `datetime` | `equals`, `gt`, `gte`, `lt`, `lte` |
336
+ | `select` | `equals`, `in` |
337
+ | `relation` | `equals` (by ID) |
338
+
339
+ ### OrderBy, Pagination, Relations, Select
340
+
341
+ ```ts
342
+ // OrderBy
343
+ q.collections.posts.find({ orderBy: { createdAt: "desc" } });
344
+
345
+ // Pagination
346
+ q.collections.posts.find({ limit: 10, offset: 20 });
347
+
348
+ // Include relations
349
+ q.collections.posts.findOne({
350
+ where: { id: "abc" },
351
+ with: { author: true, comments: { with: { user: true } } },
352
+ });
353
+
354
+ // Select specific fields
355
+ q.collections.posts.find({
356
+ select: { id: true, title: true, status: true },
357
+ });
358
+ ```
359
+
360
+ ## Type Inference
361
+
362
+ Types flow end-to-end from schema definition to client SDK:
363
+
364
+ ```text
365
+ Field Definition Codegen Client SDK
366
+ f.text().required() -> AppConfig type -> q.collections.posts.find()
367
+ f.number() -> with field types -> where: { price: { gte: 1000 } }
368
+ f.select([...]) -> and operators -> data.status === "published"
369
+ ```
370
+
371
+ The generated `AppConfig` type includes collections, globals, and routes:
372
+
373
+ ```ts
374
+ export type AppConfig = {
375
+ collections: {
376
+ posts: {
377
+ select: { id: string; title: string; status: "draft" | "published" };
378
+ insert: { title: string; status?: "draft" | "published" };
379
+ where: {
380
+ title?: string | { contains?: string };
381
+ status?: "draft" | "published";
382
+ };
383
+ orderBy: { title?: "asc" | "desc"; createdAt?: "asc" | "desc" };
384
+ };
385
+ };
386
+ };
387
+ ```
388
+
389
+ ## Direct Client Usage (without TanStack Query)
390
+
391
+ The client can be used directly without the query options proxy:
392
+
393
+ ```ts
394
+ const { docs, totalDocs } = await client.collections.posts.find({
395
+ where: { status: "published" },
396
+ orderBy: { createdAt: "desc" },
397
+ limit: 10,
398
+ });
399
+ const post = await client.collections.posts.findOne({
400
+ where: { id: "abc" },
401
+ with: { author: true },
402
+ });
403
+ await client.collections.posts.create({ title: "Hello", status: "draft" });
404
+ await client.collections.posts.update({
405
+ id: "abc",
406
+ data: { status: "published" },
407
+ });
408
+ await client.collections.posts.delete({ id: "abc" });
409
+ const settings = await client.globals.siteSettings.get();
410
+ const result = await client.routes.createBooking({
411
+ barberId: "abc",
412
+ serviceId: "def",
413
+ });
414
+ client.setLocale("sk"); // Set locale for localized content
415
+ ```
416
+
417
+ ## Realtime
418
+
419
+ Pass `{ realtime: true }` as the second argument to `find()`, `count()`, or `get()` to enable SSE-based live updates. Requires a realtime adapter in `questpie.config.ts`:
420
+
421
+ ```ts
422
+ import { runtimeConfig } from "questpie";
423
+ import { pgNotifyAdapter } from "questpie";
424
+
425
+ export default runtimeConfig({
426
+ realtime: {
427
+ adapter: pgNotifyAdapter({ connectionString: process.env.DATABASE_URL }),
428
+ },
429
+ });
430
+ ```
431
+
432
+ Channel patterns: `collections:<name>:*` (all changes), `collections:<name>:<id>` (specific record), `globals:<name>`.
433
+
434
+ For multi-instance deployments, create a Redis client and use `redisStreamsAdapter({ client })`.
435
+
436
+ ## Framework Adapters
437
+
438
+ **TanStack Start** (no adapter package needed):
439
+
440
+ ```ts title="src/routes/api/$.ts"
441
+ import { createAPIFileRoute } from "@tanstack/react-start/api";
442
+ import { createFetchHandler } from "questpie";
443
+ import { app } from "#questpie";
444
+ const handler = createFetchHandler(app, { basePath: "/api" });
445
+ export const Route = createAPIFileRoute("/api/$")({
446
+ GET: ({ request }) => handler(request),
447
+ POST: ({ request }) => handler(request),
448
+ });
449
+ ```
450
+
451
+ **Next.js**: `import { questpieNextRouteHandlers } from "@questpie/next"` -- export `GET`, `POST`, `PATCH`, `DELETE` from `app/api/[...slug]/route.ts`.
452
+
453
+ **Hono**: `import { questpieHono } from "@questpie/hono/server"` -- `server.route("/api", questpieHono(app))`.
454
+
455
+ **Elysia**: `import { questpieElysia } from "@questpie/elysia/server"` -- `.use(questpieElysia(app, { basePath: "/api" }))`.
456
+
457
+ ## Common Mistakes
458
+
459
+ ### HIGH: Creating the QUESTPIE client without proper base URL
460
+
461
+ API calls fail silently or hit the wrong server. Always set `baseURL` correctly for both server and client environments:
462
+
463
+ ```ts
464
+ // WRONG -- hardcoded localhost breaks in production
465
+ const client = createClient<AppConfig>({ baseURL: "http://localhost:3000" });
466
+
467
+ // CORRECT -- environment-aware
468
+ const client = createClient<AppConfig>({
469
+ baseURL:
470
+ typeof window !== "undefined"
471
+ ? window.location.origin
472
+ : process.env.APP_URL || "http://localhost:3000",
473
+ basePath: "/api",
474
+ });
475
+ ```
476
+
477
+ ### HIGH: Not wrapping app with QueryClientProvider
478
+
479
+ Hooks throw "No QueryClient set" error. Always wrap your root component with `<QueryClientProvider client={new QueryClient()}>`.
480
+
481
+ ### MEDIUM: Using raw fetch instead of the typed client
482
+
483
+ Loses type safety and auth handling:
484
+
485
+ ```ts
486
+ // WRONG -- no types, no auth token forwarding
487
+ const posts = await fetch("/api/collections/posts").then((r) => r.json());
488
+
489
+ // CORRECT -- fully typed, auth handled
490
+ const { docs } = await client.collections.posts.find({ limit: 10 });
491
+ ```
492
+
493
+ ### MEDIUM: Not setting up realtime adapter
494
+
495
+ Collection changes do not auto-refresh when realtime is enabled but no adapter is configured. Add a realtime adapter in `questpie.config.ts`:
496
+
497
+ ```ts
498
+ import { runtimeConfig } from "questpie";
499
+ import { pgNotifyAdapter } from "questpie";
500
+
501
+ export default runtimeConfig({
502
+ realtime: {
503
+ adapter: pgNotifyAdapter({ connectionString: process.env.DATABASE_URL }),
504
+ },
505
+ });
506
+ ```
507
+
508
+ ### MEDIUM: Importing from `questpie/client` in server code or vice versa
509
+
510
+ Violates the server/client boundary. Server code should import from `questpie`, client code from `questpie/client`:
511
+
512
+ ```ts
513
+ // WRONG -- client import in server handler
514
+ import { createClient } from "questpie/client";
515
+
516
+ // CORRECT -- server uses context-injected collections
517
+ handler: async ({ collections }) => {
518
+ return await collections.posts.find({});
519
+ };
520
+ ```