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