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,489 @@
1
+ ---
2
+ name: questpie-core/data-modeling
3
+ ---
4
+
5
+ This skill builds on questpie-core. It covers collections, globals, fields, relations, and localization -- the data modeling layer of QUESTPIE.
6
+
7
+ ## Imports
8
+
9
+ Data model files import generated factories from the `#questpie/factories` alias:
10
+
11
+ ```ts
12
+ import { collection, global } from "#questpie/factories";
13
+ ```
14
+
15
+ Drizzle index helpers come from `drizzle-orm/pg-core`:
16
+
17
+ ```ts
18
+ import { uniqueIndex, index } from "drizzle-orm/pg-core";
19
+ ```
20
+
21
+ ## Collections
22
+
23
+ A collection is a database-backed data model. Each collection file exports a builder chain:
24
+
25
+ ```ts title="collections/posts.ts"
26
+ import { collection } from "#questpie/factories";
27
+
28
+ export default collection("posts")
29
+ .fields(({ f }) => ({
30
+ title: f.text(255).required(),
31
+ body: f.richText().localized(),
32
+ cover: f.upload({ to: "assets", mimeTypes: ["image/*"] }),
33
+ status: f
34
+ .select([
35
+ { value: "draft", label: "Draft" },
36
+ { value: "published", label: "Published" },
37
+ ])
38
+ .default("draft"),
39
+ publishedAt: f.date(),
40
+ }))
41
+ .title(({ f }) => f.title)
42
+ .admin(({ c }) => ({
43
+ label: { en: "Posts" },
44
+ icon: c.icon("ph:article"),
45
+ }))
46
+ .options({ timestamps: true, versioning: true });
47
+ ```
48
+
49
+ ### Builder Chain Methods
50
+
51
+ | Method | Purpose |
52
+ | ----------------------------------------------- | --------------------------------------- |
53
+ | `.fields(({ f }) => ({...}))` | Define data fields |
54
+ | `.title(({ f }) => f.name)` | Record display title |
55
+ | `.admin(({ c }) => ({...}))` | Admin UI metadata (label, icon, hidden) |
56
+ | `.indexes(({ table }) => [...])` | Database indexes |
57
+ | `.list(({ v, f }) => v.collectionTable({...}))` | List view config |
58
+ | `.form(({ v, f }) => v.collectionForm({...}))` | Form view config |
59
+ | `.hooks({...})` | Lifecycle hooks |
60
+ | `.access({...})` | Access control rules |
61
+ | `.preview({...})` | Live preview config |
62
+ | `.options({...})` | Timestamps, versioning, soft delete |
63
+ | `.search({...})` | Search indexing |
64
+ | `.searchable(string[])` | Searchable fields |
65
+
66
+ ### Collection Options
67
+
68
+ ```ts
69
+ .options({
70
+ timestamps: true, // adds createdAt, updatedAt
71
+ versioning: true, // track content versions
72
+ softDelete: true, // mark as deleted instead of removing
73
+ })
74
+ ```
75
+
76
+ ### Indexes
77
+
78
+ ```ts
79
+ import { uniqueIndex } from "drizzle-orm/pg-core";
80
+
81
+ .indexes(({ table }) => [
82
+ uniqueIndex("posts_slug_unique").on(table.slug),
83
+ ])
84
+ ```
85
+
86
+ ### Live Preview
87
+
88
+ ```ts
89
+ .preview({
90
+ enabled: true,
91
+ position: "right", // "right" | "bottom"
92
+ defaultWidth: 50,
93
+ url: ({ record }) => `/posts/${record.slug}?preview=true`,
94
+ })
95
+ ```
96
+
97
+ ### Access Control
98
+
99
+ ```ts
100
+ .access({
101
+ read: true,
102
+ create: ({ session }) => session?.user?.role === "admin",
103
+ update: ({ session }) => session?.user?.role === "admin",
104
+ delete: ({ session }) => session?.user?.role === "admin",
105
+ })
106
+ ```
107
+
108
+ ### CRUD Operations (Server-Side)
109
+
110
+ ```ts
111
+ const { collections } = context;
112
+
113
+ // Find many
114
+ const results = await collections.posts.find({
115
+ where: { status: "published" },
116
+ orderBy: { publishedAt: "desc" },
117
+ limit: 10,
118
+ offset: 0,
119
+ });
120
+ // results.docs: Post[], results.totalDocs: number
121
+
122
+ // Find one
123
+ const post = await collections.posts.findOne({ where: { id: "abc" } });
124
+
125
+ // Create
126
+ const newPost = await collections.posts.create({
127
+ title: "Hello",
128
+ body: "<p>World</p>",
129
+ status: "draft",
130
+ });
131
+
132
+ // Update
133
+ await collections.posts.update({
134
+ where: { id: "abc" },
135
+ data: { status: "published" },
136
+ });
137
+
138
+ // Delete
139
+ await collections.posts.delete({ where: { id: "abc" } });
140
+
141
+ // Count
142
+ const count = await collections.posts.count({ where: { status: "published" } });
143
+ ```
144
+
145
+ ## Globals
146
+
147
+ A global is a singleton -- one record, no list view. Use for site-wide settings:
148
+
149
+ ```ts title="globals/site-settings.ts"
150
+ import { global } from "#questpie/factories";
151
+
152
+ export const siteSettings = global("siteSettings")
153
+ .fields(({ f }) => ({
154
+ shopName: f.text().required().default("My App"),
155
+ tagline: f.text().localized(),
156
+ logo: f.upload({ to: "assets" }),
157
+ contactEmail: f.email().required(),
158
+ }))
159
+ .admin(({ c }) => ({
160
+ label: { en: "Site Settings" },
161
+ icon: c.icon("ph:gear"),
162
+ }))
163
+ .options({ timestamps: true, versioning: true })
164
+ .access({
165
+ read: true,
166
+ update: ({ session }) => session?.user?.role === "admin",
167
+ });
168
+ ```
169
+
170
+ ### Global Builder Methods
171
+
172
+ Globals share most methods with collections but do NOT support `.list()`, `.indexes()`, `.title()`, or `.preview()`.
173
+
174
+ | Method | Purpose |
175
+ | ------------------------------------------ | -------------------------- |
176
+ | `.fields(({ f }) => ({...}))` | Define data fields |
177
+ | `.admin(({ c }) => ({...}))` | Admin label and icon |
178
+ | `.form(({ v, f }) => v.globalForm({...}))` | Form layout |
179
+ | `.hooks({...})` | Lifecycle hooks |
180
+ | `.access({...})` | Read/update access control |
181
+ | `.options({...})` | Timestamps, versioning |
182
+
183
+ ### Global API
184
+
185
+ ```ts
186
+ // Server-side
187
+ const settings = await globals.siteSettings.get();
188
+ await globals.siteSettings.update({ shopName: "New Name" });
189
+
190
+ // Client-side
191
+ const settings = await client.globals.siteSettings.get();
192
+ await client.globals.siteSettings.update({ shopName: "New Name" });
193
+ ```
194
+
195
+ ## Fields
196
+
197
+ Fields are defined inside `.fields()` using the `f` builder. Each field drives the database column, API validation, query operators, client types, and admin UI.
198
+
199
+ ### Field Types Overview
200
+
201
+ | Field | DB Type | Use Case |
202
+ | -------------- | --------------------- | ------------------------------- |
203
+ | `f.text()` | `varchar` / `text` | Short strings, titles, slugs |
204
+ | `f.textarea()` | `text` | Long text, descriptions |
205
+ | `f.richText()` | `text` (HTML) | Rich formatted content |
206
+ | `f.email()` | `varchar` | Email addresses (validated) |
207
+ | `f.url()` | `varchar` | URLs (validated) |
208
+ | `f.number()` | `integer` / `numeric` | Counts, prices, quantities |
209
+ | `f.boolean()` | `boolean` | Flags, toggles |
210
+ | `f.date()` | `date` | Calendar dates |
211
+ | `f.time()` | `time` | Time of day |
212
+ | `f.datetime()` | `timestamp` | Date + time |
213
+ | `f.select()` | `varchar` | Single choice from list |
214
+ | `f.relation()` | FK column | Reference to another collection |
215
+ | `f.upload()` | FK column | File upload linked to storage |
216
+ | `f.object()` | `jsonb` | Nested structured data |
217
+ | `.array()` | `jsonb` | Repeatable items |
218
+ | `f.blocks()` | `jsonb` | Page builder content blocks |
219
+ | `f.json()` | `jsonb` | Raw JSON |
220
+
221
+ See `references/field-types.md` for complete config options per field type.
222
+
223
+ ### Common Field Options
224
+
225
+ Every field accepts:
226
+
227
+ | Option | Type | Description |
228
+ | ------------- | ---------------------------------- | ---------------------------------------- |
229
+ | `required` | `boolean` | Field must have a value |
230
+ | `default` | `T` | Default value |
231
+ | `label` | `string \| Record<string, string>` | Display label (supports i18n) |
232
+ | `description` | `string \| Record<string, string>` | Help text |
233
+ | `localized` | `boolean` | Enable per-locale values |
234
+ | `input` | `"optional"` | Optional in API input but required in DB |
235
+ | `meta` | `object` | Admin UI rendering hints |
236
+ | `virtual` | `SQL` | SQL expression for computed fields |
237
+
238
+ ### Virtual (Computed) Fields
239
+
240
+ ```ts
241
+ import { sql } from "questpie";
242
+
243
+ displayTitle: f.text().virtual(sql<string>`(
244
+ SELECT COALESCE(name, 'Unknown') || ' - ' ||
245
+ TO_CHAR("scheduledAt", 'YYYY-MM-DD HH24:MI')
246
+ FROM appointments WHERE id = appointments.id
247
+ )`),
248
+ ```
249
+
250
+ Virtual fields are read-only -- they appear in queries but cannot be written to.
251
+
252
+ ## Relations
253
+
254
+ All relations are defined via `f.relation()` inside `.fields()`.
255
+
256
+ ### Belongs-To (Single)
257
+
258
+ ```ts
259
+ author: f.relation("users").required(),
260
+ barber: f.relation("barbers").required().onDelete("cascade"),
261
+ ```
262
+
263
+ Creates a foreign key column pointing to the target collection's `id`.
264
+
265
+ ### Many-to-Many (Through Junction)
266
+
267
+ Requires a junction collection plus `through`, `sourceField`, and `targetField`:
268
+
269
+ ```ts title="collections/barber-services.ts"
270
+ import { collection } from "#questpie/factories";
271
+
272
+ // Junction table
273
+ export default collection("barberServices")
274
+ .fields(({ f }) => ({
275
+ barber: f.relation("barbers").required().onDelete("cascade"),
276
+ service: f.relation("services").required().onDelete("cascade"),
277
+ }))
278
+ .admin(({ c }) => ({ hidden: true }));
279
+ ```
280
+
281
+ ```ts title="collections/barbers.ts (inside .fields())"
282
+ services: f.relation("services").manyToMany({
283
+ through: "barberServices",
284
+ sourceField: "barber", // FK in junction pointing to THIS collection
285
+ targetField: "service", // FK in junction pointing to TARGET collection
286
+ }),
287
+ ```
288
+
289
+ ```ts title="collections/services.ts (inside .fields())"
290
+ barbers: f.relation("barbers").manyToMany({
291
+ through: "barberServices",
292
+ sourceField: "service",
293
+ targetField: "barber",
294
+ }),
295
+ ```
296
+
297
+ ### Querying Relations
298
+
299
+ ```ts
300
+ // Include related data
301
+ const barber = await collections.barbers.findOne({
302
+ where: { id: "abc" },
303
+ with: { services: true },
304
+ });
305
+ // barber.services: Service[]
306
+
307
+ // Filter by relation
308
+ const appointments = await collections.appointments.find({
309
+ where: { barber: barberId, status: "pending" },
310
+ });
311
+ ```
312
+
313
+ ## Localization
314
+
315
+ ### Locale Configuration
316
+
317
+ ```ts title="config/app.ts"
318
+ import { appConfig } from "questpie";
319
+
320
+ export default appConfig({
321
+ locale: {
322
+ locales: [
323
+ { code: "en", label: "English", fallback: true, flagCountryCode: "us" },
324
+ { code: "sk", label: "Slovencina" },
325
+ { code: "de", label: "Deutsch" },
326
+ ],
327
+ defaultLocale: "en",
328
+ },
329
+ });
330
+ ```
331
+
332
+ ### Localizing Fields
333
+
334
+ Chain `.localized()` on any field that needs per-locale content:
335
+
336
+ ```ts
337
+ name: f.text().required().localized(),
338
+ description: f.textarea().localized(),
339
+ price: f.number().required(), // NOT localized -- same in all locales
340
+ ```
341
+
342
+ Localizable types: `text`, `textarea`, `richText`, `select`, `array`, `blocks`.
343
+ Typically NOT localized: `number`, `boolean`, `date`, `relation`.
344
+
345
+ ### Localized Arrays
346
+
347
+ Arrays can be localized as a whole -- each locale gets its own array:
348
+
349
+ ```ts
350
+ navigation: f.object({
351
+ label: f.text().required(),
352
+ href: f.text().required(),
353
+ }).array().localized(),
354
+ ```
355
+
356
+ ### Querying Localized Content
357
+
358
+ ```ts
359
+ // Server-side -- locale comes from request context
360
+ const services = await collections.services.find({ where: { isActive: true } });
361
+
362
+ // Client-side -- set locale explicitly
363
+ client.setLocale("sk");
364
+ const services = await client.collections.services.find({
365
+ where: { isActive: true },
366
+ });
367
+ ```
368
+
369
+ ### Admin UI Locale (Separate)
370
+
371
+ The admin panel has its own locale config for the interface language:
372
+
373
+ ```ts title="config/admin.ts"
374
+ import { adminConfig } from "#questpie/factories";
375
+
376
+ export default adminConfig({
377
+ locale: {
378
+ locales: ["en", "sk"],
379
+ defaultLocale: "en",
380
+ },
381
+ });
382
+ ```
383
+
384
+ This controls the admin interface language, NOT content locales.
385
+
386
+ ## Nested Objects and Reusable Patterns
387
+
388
+ Use helper functions to avoid repetition in object fields:
389
+
390
+ ```ts
391
+ .fields(({ f }) => {
392
+ const daySchedule = () => ({
393
+ isOpen: f.boolean().default(true),
394
+ start: f.time(),
395
+ end: f.time(),
396
+ });
397
+
398
+ return {
399
+ workingHours: f.object({
400
+ fields: () => ({
401
+ monday: f.object({ fields: daySchedule }),
402
+ tuesday: f.object({ fields: daySchedule }),
403
+ wednesday: f.object({ fields: daySchedule }),
404
+ }),
405
+ }),
406
+ };
407
+ })
408
+ ```
409
+
410
+ ## Form Layout
411
+
412
+ ```ts
413
+ .form(({ v, f }) =>
414
+ v.collectionForm({
415
+ sidebar: {
416
+ position: "right",
417
+ fields: [f.isActive, f.avatar],
418
+ },
419
+ fields: [
420
+ {
421
+ type: "section",
422
+ label: { en: "Contact Information" },
423
+ layout: "grid",
424
+ columns: 2,
425
+ fields: [f.name, f.slug, f.email, f.phone],
426
+ },
427
+ {
428
+ type: "section",
429
+ label: { en: "Profile" },
430
+ fields: [f.bio],
431
+ },
432
+ ],
433
+ }),
434
+ )
435
+ ```
436
+
437
+ ## Common Mistakes
438
+
439
+ ### CRITICAL: Using `.relations()` method
440
+
441
+ The `.relations()` builder method was removed. All relations are now defined via `f.relation()` inside `.fields()`.
442
+
443
+ ```ts
444
+ // WRONG -- .relations() does not exist
445
+ collection("posts").relations({ author: belongsTo("users") });
446
+
447
+ // CORRECT -- use f.relation() inside .fields()
448
+ collection("posts").fields(({ f }) => ({
449
+ author: f.relation("users"),
450
+ }));
451
+ ```
452
+
453
+ ### HIGH: Forgetting `export default`
454
+
455
+ Codegen discovers collections/globals by their default export. Without it, the file is silently ignored.
456
+
457
+ ```ts
458
+ // WRONG -- no default export, codegen won't find it
459
+ export const posts = collection("posts").fields(/* ... */);
460
+
461
+ // CORRECT
462
+ export default collection("posts").fields(/* ... */);
463
+ ```
464
+
465
+ Note: named exports alongside default are fine (e.g., `export const posts = ...` followed by `export default posts`).
466
+
467
+ ### HIGH: manyToMany without junction table config
468
+
469
+ A many-to-many relation MUST specify `through`, `sourceField`, and `targetField`. Use `.hasMany({ foreignKey })` for a plain one-to-many reverse relation.
470
+
471
+ ```ts
472
+ // WRONG -- missing through/sourceField/targetField
473
+ services: f.relation("services").manyToMany({});
474
+
475
+ // CORRECT
476
+ services: f.relation("services").manyToMany({
477
+ through: "barberServices",
478
+ sourceField: "barber",
479
+ targetField: "service",
480
+ });
481
+ ```
482
+
483
+ ### MEDIUM: Forgetting `.localized()`
484
+
485
+ If content should vary by locale but the field does not chain `.localized()`, queries with different locales will return the same value.
486
+
487
+ ### MEDIUM: Object fields -- function vs plain object
488
+
489
+ Both `f.object({ fields: {...} })` and `f.object({ fields: () => ({...}) })` are valid. Use the function form when reusing helpers or referencing `f`.