create-questpie 2.0.4 → 2.1.0

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