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,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.