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,378 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: questpie-core/crud-api
|
|
3
|
+
description: QUESTPIE CRUD API find findOne create update delete count updateMany deleteMany query operators where filter sort orderBy pagination limit offset with select relations depth context accessMode collections globals client server typesafe
|
|
4
|
+
- questpie-core
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
This skill builds on questpie-core.
|
|
8
|
+
|
|
9
|
+
## Two API Surfaces
|
|
10
|
+
|
|
11
|
+
QUESTPIE exposes CRUD operations in two ways depending on where you call them:
|
|
12
|
+
|
|
13
|
+
### 1. Handler Context (routes, hooks, jobs)
|
|
14
|
+
|
|
15
|
+
Inside any handler, `collections` and `globals` are injected via context. The current request context (session, locale, access mode) is implicit:
|
|
16
|
+
|
|
17
|
+
```ts
|
|
18
|
+
// routes/get-published.ts
|
|
19
|
+
import { route } from "questpie";
|
|
20
|
+
|
|
21
|
+
export default route()
|
|
22
|
+
.get()
|
|
23
|
+
.handler(async ({ collections }) => {
|
|
24
|
+
const result = await collections.posts.find({
|
|
25
|
+
where: { status: "published" },
|
|
26
|
+
limit: 10,
|
|
27
|
+
orderBy: { createdAt: "desc" },
|
|
28
|
+
});
|
|
29
|
+
return result.docs;
|
|
30
|
+
});
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
### 2. App Instance (scripts, seeds, external)
|
|
34
|
+
|
|
35
|
+
Outside handlers, use `app.collections.*` and pass an explicit context as the second argument:
|
|
36
|
+
|
|
37
|
+
```ts
|
|
38
|
+
import { app } from "#questpie";
|
|
39
|
+
|
|
40
|
+
const ctx = await app.createContext({ accessMode: "system", locale: "en" });
|
|
41
|
+
|
|
42
|
+
const result = await app.collections.posts.find(
|
|
43
|
+
{ where: { status: "published" }, limit: 10 },
|
|
44
|
+
ctx,
|
|
45
|
+
);
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## Collection Operations
|
|
49
|
+
|
|
50
|
+
### `find(options)`
|
|
51
|
+
|
|
52
|
+
List documents with filtering, sorting, and pagination.
|
|
53
|
+
|
|
54
|
+
```ts
|
|
55
|
+
const result = await collections.posts.find({
|
|
56
|
+
where: { status: "published", price: { gte: 1000 } },
|
|
57
|
+
orderBy: { createdAt: "desc" },
|
|
58
|
+
limit: 20,
|
|
59
|
+
offset: 0,
|
|
60
|
+
with: { author: true, category: true },
|
|
61
|
+
select: { title: true, status: true, createdAt: true },
|
|
62
|
+
});
|
|
63
|
+
// result: { docs: T[], totalDocs: number }
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
**Return type:** `{ docs: T[], totalDocs: number }`
|
|
67
|
+
|
|
68
|
+
### `findOne(options)`
|
|
69
|
+
|
|
70
|
+
Fetch a single document. Returns `null` if not found.
|
|
71
|
+
|
|
72
|
+
```ts
|
|
73
|
+
const post = await collections.posts.findOne({
|
|
74
|
+
where: { slug: "hello-world" },
|
|
75
|
+
with: { author: true },
|
|
76
|
+
});
|
|
77
|
+
// post: T | null
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
### `create(data)`
|
|
81
|
+
|
|
82
|
+
Create a new document. Pass field values as a flat object.
|
|
83
|
+
|
|
84
|
+
```ts
|
|
85
|
+
const post = await collections.posts.create({
|
|
86
|
+
title: "Hello World",
|
|
87
|
+
body: "Content here",
|
|
88
|
+
status: "draft",
|
|
89
|
+
author: "user-id-123",
|
|
90
|
+
});
|
|
91
|
+
// post: T (created record with id)
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
### `update(options)`
|
|
95
|
+
|
|
96
|
+
Update a document matching `where`. Pass changed fields in `data`.
|
|
97
|
+
|
|
98
|
+
```ts
|
|
99
|
+
const updated = await collections.posts.update({
|
|
100
|
+
where: { id: "abc-123" },
|
|
101
|
+
data: { status: "published" },
|
|
102
|
+
});
|
|
103
|
+
// updated: T (updated record)
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
### `delete(options)`
|
|
107
|
+
|
|
108
|
+
Delete documents matching `where`.
|
|
109
|
+
|
|
110
|
+
```ts
|
|
111
|
+
await collections.posts.delete({
|
|
112
|
+
where: { id: "abc-123" },
|
|
113
|
+
});
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
### `count(options)`
|
|
117
|
+
|
|
118
|
+
Count documents matching a filter.
|
|
119
|
+
|
|
120
|
+
```ts
|
|
121
|
+
const total = await collections.posts.count({
|
|
122
|
+
where: { status: "published" },
|
|
123
|
+
});
|
|
124
|
+
// total: number
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
### `updateMany(options)`
|
|
128
|
+
|
|
129
|
+
Bulk update all documents matching `where`.
|
|
130
|
+
|
|
131
|
+
```ts
|
|
132
|
+
await collections.posts.updateMany({
|
|
133
|
+
where: { status: "draft" },
|
|
134
|
+
data: { status: "archived" },
|
|
135
|
+
});
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
### `deleteMany(options)`
|
|
139
|
+
|
|
140
|
+
Bulk delete all documents matching `where`.
|
|
141
|
+
|
|
142
|
+
```ts
|
|
143
|
+
await collections.posts.deleteMany({
|
|
144
|
+
where: { status: "archived" },
|
|
145
|
+
});
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
## Global Operations
|
|
149
|
+
|
|
150
|
+
Globals have only two operations:
|
|
151
|
+
|
|
152
|
+
```ts
|
|
153
|
+
// Read global
|
|
154
|
+
const settings = await globals.siteSettings.get({});
|
|
155
|
+
|
|
156
|
+
// Update global
|
|
157
|
+
const updated = await globals.siteSettings.update({
|
|
158
|
+
siteName: "New Name",
|
|
159
|
+
});
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
Via app instance:
|
|
163
|
+
|
|
164
|
+
```ts
|
|
165
|
+
const settings = await app.globals.siteSettings.get({}, ctx);
|
|
166
|
+
await app.globals.siteSettings.update(
|
|
167
|
+
{ siteName: "New Name" },
|
|
168
|
+
ctx,
|
|
169
|
+
);
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
## Query Operators
|
|
173
|
+
|
|
174
|
+
Operators are always nested inside field objects in `where`. See `references/query-operators.md` for the full reference.
|
|
175
|
+
|
|
176
|
+
```ts
|
|
177
|
+
// Multiple fields = AND
|
|
178
|
+
where: {
|
|
179
|
+
status: "published", // equality shorthand
|
|
180
|
+
price: { gte: 1000, lt: 5000 }, // range (AND within same field)
|
|
181
|
+
title: { contains: "guide" }, // substring
|
|
182
|
+
category: { in: ["news", "blog"] }, // one-of
|
|
183
|
+
}
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
### Equality Shorthand
|
|
187
|
+
|
|
188
|
+
All field types support direct equality:
|
|
189
|
+
|
|
190
|
+
```ts
|
|
191
|
+
where: {
|
|
192
|
+
status: "published";
|
|
193
|
+
}
|
|
194
|
+
// equivalent to: where: { status: { eq: "published" } }
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
## Sorting
|
|
198
|
+
|
|
199
|
+
Use `orderBy` with `"asc"` or `"desc"`:
|
|
200
|
+
|
|
201
|
+
```ts
|
|
202
|
+
const result = await collections.posts.find({
|
|
203
|
+
orderBy: { createdAt: "desc" },
|
|
204
|
+
});
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
## Pagination
|
|
208
|
+
|
|
209
|
+
Use `limit` and `offset`:
|
|
210
|
+
|
|
211
|
+
```ts
|
|
212
|
+
const page2 = await collections.posts.find({
|
|
213
|
+
limit: 20,
|
|
214
|
+
offset: 20,
|
|
215
|
+
});
|
|
216
|
+
// page2.totalDocs = total count across all pages
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
## Relations
|
|
220
|
+
|
|
221
|
+
Relations are NOT populated by default. Use `with` to eager-load:
|
|
222
|
+
|
|
223
|
+
```ts
|
|
224
|
+
const post = await collections.posts.findOne({
|
|
225
|
+
where: { id: "abc" },
|
|
226
|
+
with: { author: true, category: true },
|
|
227
|
+
});
|
|
228
|
+
// post.author is now the full author object, not just an ID
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
Use `select` to pick specific fields:
|
|
232
|
+
|
|
233
|
+
```ts
|
|
234
|
+
const posts = await collections.posts.find({
|
|
235
|
+
select: { title: true, status: true },
|
|
236
|
+
});
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
## Context and Access Modes
|
|
240
|
+
|
|
241
|
+
### In Handlers
|
|
242
|
+
|
|
243
|
+
Context is automatic. The current user's session determines access:
|
|
244
|
+
|
|
245
|
+
```ts
|
|
246
|
+
export default route()
|
|
247
|
+
.get()
|
|
248
|
+
.handler(async ({ collections, session }) => {
|
|
249
|
+
// Access control is enforced based on session
|
|
250
|
+
const posts = await collections.posts.find({});
|
|
251
|
+
return posts;
|
|
252
|
+
});
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
### In Scripts / Seeds
|
|
256
|
+
|
|
257
|
+
Create an explicit context with `app.createContext()`:
|
|
258
|
+
|
|
259
|
+
```ts
|
|
260
|
+
// System mode -- bypasses all access control
|
|
261
|
+
const ctx = await app.createContext({ accessMode: "system", locale: "en" });
|
|
262
|
+
|
|
263
|
+
// User mode -- enforces access control (requires session)
|
|
264
|
+
const ctx = await app.createContext({ accessMode: "user" });
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
## Client API
|
|
268
|
+
|
|
269
|
+
The client SDK mirrors server operations:
|
|
270
|
+
|
|
271
|
+
```ts
|
|
272
|
+
const posts = await client.collections.posts.find({ limit: 10 });
|
|
273
|
+
const post = await client.collections.posts.findOne({ where: { id: "abc" } });
|
|
274
|
+
const created = await client.collections.posts.create({ title: "New" });
|
|
275
|
+
const updated = await client.collections.posts.update({
|
|
276
|
+
id: "abc",
|
|
277
|
+
data: { title: "Updated" },
|
|
278
|
+
});
|
|
279
|
+
await client.collections.posts.delete({ id: "abc" });
|
|
280
|
+
const count = await client.collections.posts.count({
|
|
281
|
+
where: { status: "draft" },
|
|
282
|
+
});
|
|
283
|
+
```
|
|
284
|
+
|
|
285
|
+
### Upload (Client Only)
|
|
286
|
+
|
|
287
|
+
For upload collections:
|
|
288
|
+
|
|
289
|
+
```ts
|
|
290
|
+
const asset = await client.collections.assets.upload(file, {
|
|
291
|
+
onProgress: (percent) => console.log(`${percent}%`),
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
const assets = await client.collections.assets.uploadMany(files, {
|
|
295
|
+
onProgress: (percent) => console.log(`${percent}%`),
|
|
296
|
+
});
|
|
297
|
+
```
|
|
298
|
+
|
|
299
|
+
## Common Mistakes
|
|
300
|
+
|
|
301
|
+
### CRITICAL: Missing context in app.collections calls
|
|
302
|
+
|
|
303
|
+
When using `app.collections.*` outside handlers, you MUST pass a context. Without it, the call has no session, no locale, and no access mode.
|
|
304
|
+
|
|
305
|
+
```ts
|
|
306
|
+
// WRONG -- no context
|
|
307
|
+
const posts = await app.collections.posts.find({});
|
|
308
|
+
|
|
309
|
+
// CORRECT -- explicit context
|
|
310
|
+
const ctx = await app.createContext({ accessMode: "system" });
|
|
311
|
+
const posts = await app.collections.posts.find({}, ctx);
|
|
312
|
+
```
|
|
313
|
+
|
|
314
|
+
Inside handlers (route handlers, hooks, jobs), context is injected automatically -- use `collections.*` directly.
|
|
315
|
+
|
|
316
|
+
### HIGH: Expecting find() to return an array
|
|
317
|
+
|
|
318
|
+
`find()` returns `{ docs: T[], totalDocs: number }`, not an array.
|
|
319
|
+
|
|
320
|
+
```ts
|
|
321
|
+
// WRONG
|
|
322
|
+
const posts = await collections.posts.find({});
|
|
323
|
+
posts.forEach((p) => console.log(p.title)); // TypeError
|
|
324
|
+
|
|
325
|
+
// CORRECT
|
|
326
|
+
const { docs, totalDocs } = await collections.posts.find({});
|
|
327
|
+
docs.forEach((p) => console.log(p.title));
|
|
328
|
+
```
|
|
329
|
+
|
|
330
|
+
### HIGH: Relations not populated
|
|
331
|
+
|
|
332
|
+
Relations return only the ID by default. Use `with` to populate:
|
|
333
|
+
|
|
334
|
+
```ts
|
|
335
|
+
// Returns { author: "user-id-123" }
|
|
336
|
+
const post = await collections.posts.findOne({ where: { id: "abc" } });
|
|
337
|
+
|
|
338
|
+
// Returns { author: { id: "user-id-123", name: "John", ... } }
|
|
339
|
+
const post = await collections.posts.findOne({
|
|
340
|
+
where: { id: "abc" },
|
|
341
|
+
with: { author: true },
|
|
342
|
+
});
|
|
343
|
+
```
|
|
344
|
+
|
|
345
|
+
### MEDIUM: Using accessMode "system" in HTTP handlers
|
|
346
|
+
|
|
347
|
+
System mode bypasses all access control. Only use it in background jobs, seeds, and scripts -- never in request handlers.
|
|
348
|
+
|
|
349
|
+
```ts
|
|
350
|
+
// WRONG -- in an HTTP route handler
|
|
351
|
+
export default route()
|
|
352
|
+
.get()
|
|
353
|
+
.handler(async ({ app }) => {
|
|
354
|
+
const ctx = await app.createContext({ accessMode: "system" });
|
|
355
|
+
return app.collections.posts.find({}, ctx); // bypasses access control!
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
// CORRECT -- use injected collections (respects session access rules)
|
|
359
|
+
export default route()
|
|
360
|
+
.get()
|
|
361
|
+
.handler(async ({ collections }) => {
|
|
362
|
+
return collections.posts.find({});
|
|
363
|
+
});
|
|
364
|
+
```
|
|
365
|
+
|
|
366
|
+
### MEDIUM: Wrong create() signature
|
|
367
|
+
|
|
368
|
+
`create()` takes a flat data object, NOT `{ data: {...} }`:
|
|
369
|
+
|
|
370
|
+
```ts
|
|
371
|
+
// WRONG
|
|
372
|
+
await collections.posts.create({ data: { title: "Hello" } });
|
|
373
|
+
|
|
374
|
+
// CORRECT
|
|
375
|
+
await collections.posts.create({ title: "Hello", body: "World" });
|
|
376
|
+
```
|
|
377
|
+
|
|
378
|
+
Note: `update()` DOES use `{ where, data }` -- only `create()` is flat.
|