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.
- package/README.md +10 -6
- package/dist/index.mjs +139 -24
- package/package.json +5 -3
- package/skills/questpie/AGENTS.md +2664 -0
- package/skills/questpie/SKILL.md +181 -0
- package/skills/questpie/references/auth.md +121 -0
- package/skills/questpie/references/business-logic.md +550 -0
- package/skills/questpie/references/codegen-plugin-api.md +382 -0
- package/skills/questpie/references/crud-api.md +378 -0
- package/skills/questpie/references/data-modeling.md +489 -0
- package/skills/questpie/references/extend.md +493 -0
- package/skills/questpie/references/field-types.md +386 -0
- package/skills/questpie/references/infrastructure-adapters.md +545 -0
- package/skills/questpie/references/multi-tenancy.md +364 -0
- package/skills/questpie/references/production.md +475 -0
- package/skills/questpie/references/query-operators.md +125 -0
- package/skills/questpie/references/quickstart.md +549 -0
- package/skills/questpie/references/rules.md +327 -0
- package/skills/questpie/references/tanstack-query.md +520 -0
- package/skills/questpie-admin/AGENTS.md +1442 -0
- package/skills/questpie-admin/SKILL.md +410 -0
- package/skills/questpie-admin/references/blocks.md +307 -0
- package/skills/questpie-admin/references/custom-ui.md +305 -0
- package/skills/questpie-admin/references/views.md +433 -0
- package/templates/tanstack-start/AGENTS.md +17 -13
- package/templates/tanstack-start/CLAUDE.md +15 -12
- package/templates/tanstack-start/README.md +19 -13
- package/templates/tanstack-start/env.example +1 -1
- package/templates/tanstack-start/package.json +20 -6
- package/templates/tanstack-start/src/lib/env.ts +1 -1
- package/templates/tanstack-start/src/questpie/server/config/admin.ts +27 -30
- package/templates/tanstack-start/src/routeTree.gen.ts +138 -0
- package/templates/tanstack-start/src/routes/__root.tsx +0 -2
- package/templates/tanstack-start/src/routes/admin.tsx +8 -1
- package/templates/tanstack-start/src/tanstack-start.d.ts +1 -0
- package/templates/tanstack-start/src/vite-env.d.ts +1 -0
- 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
|
+
```
|