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,327 @@
1
+ ---
2
+ name: questpie-core-rules
3
+ description: QUESTPIE access control hooks validation lifecycle beforeValidate beforeChange afterChange beforeDelete afterDelete access rules field-level row-level secure-by-default Zod schema refinements collection global
4
+ - questpie-core
5
+ ---
6
+
7
+ # QUESTPIE Rules — Access Control, Hooks, Validation
8
+
9
+ This skill builds on questpie-core. It covers collection/global access control, lifecycle hooks, and validation — the three rule layers that govern data flow.
10
+
11
+ ## Access Control
12
+
13
+ Access rules are defined per-collection via `.access()`. Each operation accepts a static `boolean` or a function receiving `AppContext` that returns `boolean` or a where clause (row-level filtering).
14
+
15
+ ### Default Behavior
16
+
17
+ When no `.access()` is defined, all operations default to `({ session }) => !!session` — **authenticated users only**. You must explicitly set `read: true` for public collections.
18
+
19
+ ### Collection Access
20
+
21
+ ```ts
22
+ // collections/posts.collection.ts
23
+ import { collection } from "#questpie/factories";
24
+
25
+ export default collection("posts")
26
+ .fields(({ f }) => ({
27
+ title: f.text().label("Title").required(),
28
+ content: f.richText().label("Content"),
29
+ author: f.relation("users"),
30
+ }))
31
+ .access({
32
+ read: true, // Public read
33
+ create: ({ session }) => !!session, // Authenticated
34
+ update: ({ session }) => session?.user?.role === "admin", // Admin only
35
+ delete: ({ session }) => session?.user?.role === "admin",
36
+ });
37
+ ```
38
+
39
+ ### Operations
40
+
41
+ | Operation | When checked |
42
+ | --------- | ---------------------------- |
43
+ | `read` | Listing and fetching records |
44
+ | `create` | Creating new records |
45
+ | `update` | Updating existing records |
46
+ | `delete` | Deleting records |
47
+
48
+ ### Global Access
49
+
50
+ Globals support `read` and `update` only (singletons have no create/delete):
51
+
52
+ ```ts
53
+ // globals/site-settings.global.ts
54
+ import { global } from "#questpie/factories";
55
+
56
+ export default global("siteSettings")
57
+ .fields(({ f }) => ({
58
+ siteName: f.text().label("Site Name").required(),
59
+ logo: f.upload().label("Logo"),
60
+ }))
61
+ .access({
62
+ read: true,
63
+ update: ({ session }) => session?.user?.role === "admin",
64
+ });
65
+ ```
66
+
67
+ ### Row-Level Access (AccessWhere)
68
+
69
+ Return a where clause object instead of a boolean to restrict operations to matching rows:
70
+
71
+ ```ts
72
+ .access({
73
+ read: true,
74
+ update: ({ session }) => {
75
+ if (!session) return false;
76
+ // Only allow updating own records
77
+ return { author: session.user.id };
78
+ },
79
+ })
80
+ ```
81
+
82
+ ### Access Function Context
83
+
84
+ Access functions receive `AppContext` with these properties:
85
+
86
+ | Property | Description |
87
+ | ------------- | --------------------------------------- |
88
+ | `session` | Current auth session (null if unauthed) |
89
+ | `db` | Database instance |
90
+ | `collections` | Typed collection API |
91
+
92
+ ### System Access Mode
93
+
94
+ Server-side code can bypass all access checks:
95
+
96
+ ```ts
97
+ const ctx = await app.createContext({ accessMode: "system" });
98
+ const allPosts = await app.collections.posts.find({}, ctx);
99
+ ```
100
+
101
+ HTTP requests always use session-based access. System mode is for background jobs, seeds, and internal server logic only.
102
+
103
+ ## Hooks
104
+
105
+ Hooks run logic at specific points in the collection lifecycle. They receive the full typed `AppContext` through context injection.
106
+
107
+ ### Lifecycle Order
108
+
109
+ For create/update:
110
+
111
+ ```text
112
+ API Request
113
+ |
114
+ beforeValidate -- Modify/validate data before schema validation
115
+ |
116
+ Schema Validation -- Zod validation from field definitions
117
+ |
118
+ beforeChange -- Transform data before database write
119
+ |
120
+ Database Write -- Insert or update
121
+ |
122
+ afterChange -- Side effects after successful write
123
+ ```
124
+
125
+ For delete:
126
+
127
+ ```text
128
+ beforeDelete --> Database Delete --> afterDelete
129
+ ```
130
+
131
+ ### Defining Hooks
132
+
133
+ ```ts
134
+ // collections/appointments.collection.ts
135
+ import { collection } from "#questpie/factories";
136
+
137
+ export default collection("appointments")
138
+ .fields(({ f }) => ({
139
+ customer: f.relation("users"),
140
+ barber: f.relation("barbers"),
141
+ service: f.relation("services"),
142
+ scheduledAt: f.datetime().required(),
143
+ status: f.select([
144
+ { value: "pending", label: "Pending" },
145
+ { value: "confirmed", label: "Confirmed" },
146
+ { value: "cancelled", label: "Cancelled" },
147
+ ]),
148
+ slug: f.text().required().inputOptional(),
149
+ name: f.text().required(),
150
+ }))
151
+ .hooks({
152
+ beforeValidate: async (ctx) => {
153
+ if (ctx.data.name && !ctx.data.slug) {
154
+ ctx.data.slug = slugify(ctx.data.name);
155
+ }
156
+ },
157
+
158
+ beforeChange: async ({ data, operation, original }) => {
159
+ if (operation === "create") {
160
+ // Set defaults on create
161
+ }
162
+ if (operation === "update" && original) {
163
+ // Compare with original data
164
+ }
165
+ },
166
+
167
+ afterChange: async ({ data, operation, original, queue }) => {
168
+ if (operation === "create") {
169
+ await queue.sendAppointmentConfirmation.publish({
170
+ appointmentId: data.id,
171
+ customerId: data.customer,
172
+ });
173
+ }
174
+ if (operation === "update" && data.status === "cancelled") {
175
+ await queue.sendAppointmentCancellation.publish({
176
+ appointmentId: data.id,
177
+ customerId: data.customer,
178
+ });
179
+ }
180
+ },
181
+
182
+ beforeDelete: async ({ id }) => {
183
+ // Prevent deletion or clean up
184
+ },
185
+
186
+ afterDelete: async ({ id }) => {
187
+ // Clean up related data
188
+ },
189
+ });
190
+ ```
191
+
192
+ ### Hook Context Properties
193
+
194
+ | Property | Available in | Description |
195
+ | ------------- | ----------------------------------------- | -------------------------------- |
196
+ | `data` | beforeValidate, beforeChange, afterChange | The record data being written |
197
+ | `operation` | beforeChange, afterChange | `"create"` or `"update"` |
198
+ | `original` | beforeChange, afterChange (update) | Previous record state |
199
+ | `id` | beforeDelete, afterDelete | ID of record being deleted |
200
+ | `collections` | All hooks | Typed collection API |
201
+ | `globals` | All hooks | Typed globals API |
202
+ | `queue` | All hooks | Queue client for publishing jobs |
203
+ | `email` | All hooks | Email service |
204
+ | `db` | All hooks | Database instance |
205
+ | `session` | All hooks | Current auth session |
206
+ | `services` | All hooks | Custom services from `services/` |
207
+
208
+ ### Context-First Pattern
209
+
210
+ All dependencies come through destructuring. No need to import the app instance:
211
+
212
+ ```ts
213
+ .hooks({
214
+ beforeChange: async ({ data, services }) => {
215
+ const { blog } = services;
216
+ data.slug = blog.generateSlug(data.title);
217
+ data.readingTime = blog.computeReadingTime(data.content);
218
+ },
219
+
220
+ afterChange: async ({ data, operation, original, queue }) => {
221
+ if (
222
+ operation === "update" &&
223
+ original?.status !== "published" &&
224
+ data.status === "published"
225
+ ) {
226
+ await queue.notifyBlogSubscribers.publish({
227
+ postId: data.id,
228
+ title: data.title,
229
+ });
230
+ }
231
+ },
232
+ })
233
+ ```
234
+
235
+ ## Validation
236
+
237
+ QUESTPIE validates at three levels: field constraints, auto-generated Zod schemas, and custom hooks.
238
+
239
+ ### Field-Level Constraints
240
+
241
+ Built-in constraints on field definitions generate Zod schemas automatically:
242
+
243
+ ```ts
244
+ .fields(({ f }) => ({
245
+ name: f.text(255).required(),
246
+ email: f.email().required(),
247
+ website: f.url(),
248
+ rating: f.number().min(1).max(5),
249
+ tags: f.text().array().maxItems(10),
250
+ }))
251
+ ```
252
+
253
+ | Constraint | Fields | Description |
254
+ | ----------- | ------------------ | ----------------------- |
255
+ | `required` | All | Field must have a value |
256
+ | `maxLength` | `text`, `textarea` | Maximum string length |
257
+ | `min`/`max` | `number` | Numeric range |
258
+ | `maxItems` | `array` | Maximum array length |
259
+ | `mimeTypes` | `upload` | Allowed file types |
260
+ | `maxSize` | `upload` | Max file size in bytes |
261
+
262
+ ### Input Modifier
263
+
264
+ The `input` option controls API input behavior for fields computed by hooks:
265
+
266
+ ```ts
267
+ slug: f.text().required().inputOptional(),
268
+ ```
269
+
270
+ ### Custom Validation via Hooks
271
+
272
+ Use `beforeValidate` to transform data or reject operations:
273
+
274
+ ```ts
275
+ .hooks({
276
+ beforeValidate: async ({ data, operation }) => {
277
+ // Transform data before validation
278
+ if (operation === "create" && !data.slug) {
279
+ data.slug = slugify(data.name);
280
+ }
281
+ },
282
+ })
283
+ ```
284
+
285
+ To reject an operation, throw an error:
286
+
287
+ ```ts
288
+ .hooks({
289
+ beforeValidate: async ({ data }) => {
290
+ if (data.scheduledAt && new Date(data.scheduledAt) < new Date()) {
291
+ throw new Error("Cannot schedule appointments in the past");
292
+ }
293
+ },
294
+ })
295
+ ```
296
+
297
+ ## Common Mistakes
298
+
299
+ 1. **HIGH: Forgetting default access is `!!session`.**
300
+ Collections without `.access()` require authentication for all operations. For public read access, explicitly set `read: true`.
301
+
302
+ 2. **HIGH: Using `accessMode: "system"` in HTTP handlers.**
303
+ System mode bypasses all access checks. Only use it for background jobs, seeds, and internal server scripts — never in request handlers.
304
+
305
+ 3. **MEDIUM: Mutating `data` in `afterChange` hooks.**
306
+ Changes to `data` in `afterChange` are NOT persisted to the database. Only mutations in `beforeValidate` and `beforeChange` are saved.
307
+
308
+ 4. **MEDIUM: Not awaiting async access control functions.**
309
+ Access functions can be async and must return `boolean` or a where clause object (`AccessWhere`).
310
+
311
+ 5. **HIGH: Wrong context usage in access rules.**
312
+ Use the destructured `session` parameter from `AppContext`, not a standalone import. Access functions receive `({ session, db, collections })`.
313
+
314
+ ## Access Control for Preview Sessions
315
+
316
+ Live preview sessions use token-based authentication. When a preview iframe loads, it receives a short-lived preview token that authorizes read access to the collection being previewed.
317
+
318
+ ### Key Points
319
+
320
+ - Preview tokens are scoped to a specific collection and record — they do not grant broad access.
321
+ - Preview does **not** bypass access rules. The token resolves to a session with the same permissions as the user who initiated the preview.
322
+ - Access rules (`.access()`) still apply to all data fetched during preview, including prefetched relations and block data.
323
+ - Row-level access (AccessWhere) filters are enforced even in preview context — a user cannot preview records they cannot read.
324
+
325
+ ### System Access and Preview
326
+
327
+ Do not use `accessMode: "system"` to serve preview data. Preview requests should go through normal session-based access, with the preview token resolving to the editor's session. This ensures previewed content respects the same visibility rules as the final published page.