create-questpie 1.0.0 → 2.0.1
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/README.md +20 -12
- package/dist/index.mjs +10 -3
- package/package.json +21 -21
- package/templates/tanstack-start/AGENTS.md +371 -318
- package/templates/tanstack-start/CLAUDE.md +84 -52
- package/templates/tanstack-start/README.md +65 -52
- package/templates/tanstack-start/components.json +20 -20
- package/templates/tanstack-start/package.json +6 -0
- package/templates/tanstack-start/questpie.config.ts +7 -7
- package/templates/tanstack-start/src/lib/auth-client.ts +3 -3
- package/templates/tanstack-start/src/lib/client.ts +13 -0
- package/templates/tanstack-start/src/lib/env.ts +19 -22
- package/templates/tanstack-start/src/lib/query-client.ts +5 -5
- package/templates/tanstack-start/src/questpie/admin/.generated/client.ts +13 -0
- package/templates/tanstack-start/src/questpie/admin/admin.ts +8 -4
- package/templates/tanstack-start/src/questpie/admin/modules.ts +1 -0
- package/templates/tanstack-start/src/questpie/server/.generated/factories.ts +194 -0
- package/templates/tanstack-start/src/questpie/server/.generated/index.ts +201 -0
- package/templates/tanstack-start/src/questpie/server/app.ts +10 -52
- package/templates/tanstack-start/src/questpie/server/collections/posts.collection.ts +39 -53
- package/templates/tanstack-start/src/questpie/server/config/admin.ts +83 -0
- package/templates/tanstack-start/src/questpie/server/config/auth.ts +8 -0
- package/templates/tanstack-start/src/questpie/server/config/openapi.ts +10 -0
- package/templates/tanstack-start/src/questpie/server/globals/site-settings.global.ts +9 -14
- package/templates/tanstack-start/src/questpie/server/modules.ts +10 -0
- package/templates/tanstack-start/src/questpie/server/questpie.config.ts +20 -0
- package/templates/tanstack-start/src/router.tsx +6 -5
- package/templates/tanstack-start/src/routes/__root.tsx +11 -9
- package/templates/tanstack-start/src/routes/admin/$.tsx +14 -13
- package/templates/tanstack-start/src/routes/admin/index.tsx +11 -10
- package/templates/tanstack-start/src/routes/admin/login.tsx +11 -10
- package/templates/tanstack-start/src/routes/admin.tsx +53 -52
- package/templates/tanstack-start/src/routes/api/{cms/$.ts → $.ts} +6 -20
- package/templates/tanstack-start/src/styles.css +109 -109
- package/templates/tanstack-start/tsconfig.json +27 -25
- package/templates/tanstack-start/vite.config.ts +5 -3
- package/templates/tanstack-start/src/lib/cms-client.ts +0 -12
- package/templates/tanstack-start/src/migrations/index.ts +0 -8
- package/templates/tanstack-start/src/questpie/admin/builder.ts +0 -4
- package/templates/tanstack-start/src/questpie/server/builder.ts +0 -4
- package/templates/tanstack-start/src/questpie/server/dashboard.ts +0 -68
- package/templates/tanstack-start/src/questpie/server/rpc.ts +0 -4
- package/templates/tanstack-start/src/questpie/server/sidebar.ts +0 -26
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
# AGENTS.md
|
|
2
2
|
|
|
3
|
-
Source-of-truth guidance for AI agents working in this QUESTPIE
|
|
3
|
+
Source-of-truth guidance for AI agents working in this QUESTPIE project.
|
|
4
4
|
|
|
5
5
|
> **Docs for LLMs**: https://questpie.com/llms.txt (sitemap), https://questpie.com/llms-full.txt (full content)
|
|
6
6
|
|
|
7
7
|
## Project Overview
|
|
8
8
|
|
|
9
9
|
- **Framework**: TanStack Start (React) + Vite + Nitro (Bun preset)
|
|
10
|
-
- **
|
|
10
|
+
- **QUESTPIE**: QUESTPIE — application framework with config-driven admin UI
|
|
11
11
|
- **Database**: PostgreSQL (via Drizzle ORM)
|
|
12
12
|
- **Styling**: Tailwind CSS v4 + shadcn/ui components
|
|
13
13
|
- **Auth**: Better Auth (email/password)
|
|
@@ -20,7 +20,7 @@ When you need more context about QUESTPIE APIs, consult these resources in order
|
|
|
20
20
|
|
|
21
21
|
1. **LLMs full docs**: https://questpie.com/llms-full.txt — complete documentation in a single LLM-optimized file
|
|
22
22
|
2. **Online docs**: https://questpie.com/docs — browsable documentation
|
|
23
|
-
3. **Local API docs**: http://localhost:3000/api/
|
|
23
|
+
3. **Local API docs**: http://localhost:3000/api/docs — Scalar UI (available when dev server is running)
|
|
24
24
|
|
|
25
25
|
Key documentation pages:
|
|
26
26
|
|
|
@@ -28,14 +28,14 @@ Key documentation pages:
|
|
|
28
28
|
| -------------------------- | ---------------------------------------------------------------- |
|
|
29
29
|
| Getting Started | https://questpie.com/docs/getting-started |
|
|
30
30
|
| Project Structure | https://questpie.com/docs/getting-started/project-structure |
|
|
31
|
-
| Your First
|
|
31
|
+
| Your First QUESTPIE | https://questpie.com/docs/getting-started/your-first-app |
|
|
32
32
|
| Architecture Principles | https://questpie.com/docs/mentality |
|
|
33
33
|
| Field Builder | https://questpie.com/docs/server/field-builder |
|
|
34
34
|
| Field Types Reference | https://questpie.com/docs/server/field-types |
|
|
35
35
|
| Collections | https://questpie.com/docs/server/collections |
|
|
36
36
|
| Globals | https://questpie.com/docs/server/globals |
|
|
37
37
|
| Relations | https://questpie.com/docs/server/relations |
|
|
38
|
-
|
|
|
38
|
+
| Routes | https://questpie.com/docs/backend/business-logic/routes |
|
|
39
39
|
| Hooks & Lifecycle | https://questpie.com/docs/server/hooks-and-lifecycle |
|
|
40
40
|
| Access Control | https://questpie.com/docs/server/access-control |
|
|
41
41
|
| Reactive Fields | https://questpie.com/docs/server/reactive-fields |
|
|
@@ -64,25 +64,31 @@ Key documentation pages:
|
|
|
64
64
|
src/
|
|
65
65
|
questpie/
|
|
66
66
|
server/ ← WHAT: data contracts and behavior
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
67
|
+
questpie.config.ts ← App config: runtimeConfig({ db, app, ... })
|
|
68
|
+
modules.ts ← Module dependencies (adminModule, openApiModule, etc.)
|
|
69
|
+
config/ ← Typed configuration files
|
|
70
|
+
auth.ts ← authConfig({...}) — Better Auth options
|
|
71
|
+
app.ts ← appConfig({ locale, access, hooks, context })
|
|
72
|
+
admin.ts ← adminConfig({ sidebar, dashboard, branding, locale })
|
|
73
|
+
openapi.ts ← openApiConfig({ info, scalar })
|
|
74
|
+
.generated/ ← Codegen output (app instance + App type)
|
|
75
|
+
index.ts
|
|
76
|
+
collections/ ← One file per collection (auto-discovered)
|
|
77
|
+
globals/ ← One file per global (auto-discovered)
|
|
78
|
+
routes/ ← Server routes via route() (auto-discovered)
|
|
79
|
+
jobs/ ← Background job definitions (auto-discovered)
|
|
80
|
+
blocks/ ← Block definitions (auto-discovered)
|
|
77
81
|
admin/ ← HOW: UI rendering concerns
|
|
78
|
-
|
|
79
|
-
hooks.ts ← Typed hooks via createTypedHooks<
|
|
82
|
+
admin.ts ← Re-exports generated admin config
|
|
83
|
+
hooks.ts ← Typed hooks via createTypedHooks<App>()
|
|
84
|
+
.generated/ ← Codegen output (admin client config)
|
|
85
|
+
client.ts
|
|
80
86
|
blocks/ ← Block renderers (if using blocks)
|
|
81
87
|
lib/
|
|
82
88
|
env.ts ← Type-safe env vars (@t3-oss/env-core + Zod)
|
|
83
|
-
|
|
89
|
+
client.ts ← client instance
|
|
84
90
|
routes/
|
|
85
|
-
api
|
|
91
|
+
api/$.ts ← QUESTPIE catch-all handler (REST + OpenAPI + auth)
|
|
86
92
|
migrations/ ← Database migrations (generated by CLI)
|
|
87
93
|
```
|
|
88
94
|
|
|
@@ -90,28 +96,33 @@ src/
|
|
|
90
96
|
|
|
91
97
|
### Server-First Split
|
|
92
98
|
|
|
93
|
-
| Directory | Responsibility | Defines
|
|
94
|
-
| ------------------ | --------------------------------- |
|
|
95
|
-
| `questpie/server/` | **WHAT** — contracts and behavior | Schema, access, hooks,
|
|
96
|
-
| `questpie/admin/` | **HOW** — rendering concerns | Branding, locale, custom renderers
|
|
97
|
-
| `routes/` | **Mounting** — HTTP wiring | Route handlers, no business logic
|
|
99
|
+
| Directory | Responsibility | Defines |
|
|
100
|
+
| ------------------ | --------------------------------- | ----------------------------------- |
|
|
101
|
+
| `questpie/server/` | **WHAT** — contracts and behavior | Schema, access, hooks, routes, jobs |
|
|
102
|
+
| `questpie/admin/` | **HOW** — rendering concerns | Branding, locale, custom renderers |
|
|
103
|
+
| `routes/` | **Mounting** — HTTP wiring | Route handlers, no business logic |
|
|
98
104
|
|
|
99
105
|
### File Naming Conventions
|
|
100
106
|
|
|
101
|
-
- Collections: `*.
|
|
102
|
-
- Globals: `*.
|
|
103
|
-
-
|
|
104
|
-
-
|
|
107
|
+
- Collections: `*.ts` in `collections/` (e.g., `posts.ts`) — named exports
|
|
108
|
+
- Globals: `*.ts` in `globals/` (e.g., `site-settings.ts`) — named exports
|
|
109
|
+
- Routes: `*.ts` in `routes/` (e.g., `get-stats.ts`) — default exports
|
|
110
|
+
- Jobs: `*.ts` in `jobs/` (e.g., `send-email.ts`) — default exports
|
|
111
|
+
- Blocks: `*.ts` in `blocks/` (e.g., `hero.ts`) — named exports
|
|
105
112
|
|
|
106
113
|
### Key Files
|
|
107
114
|
|
|
108
|
-
- **`src/questpie/server/
|
|
109
|
-
- **`src/questpie/server/
|
|
110
|
-
- **`src/questpie/server/
|
|
111
|
-
- **`src/questpie/admin
|
|
115
|
+
- **`src/questpie/server/questpie.config.ts`** — App config: `runtimeConfig({ db, app, ... })`.
|
|
116
|
+
- **`src/questpie/server/modules.ts`** — Module dependencies: `export default [adminModule, openApiModule] as const`.
|
|
117
|
+
- **`src/questpie/server/config/auth.ts`** — Auth config via `authConfig()` factory.
|
|
118
|
+
- **`src/questpie/server/config/admin.ts`** — Admin config (sidebar, dashboard, branding, locale) via `adminConfig()` factory.
|
|
119
|
+
- **`src/questpie/server/config/app.ts`** — *(optional, not scaffolded)* App config (locale, access, hooks, context) via `appConfig()`. Create when needed.
|
|
120
|
+
- **`src/questpie/server/config/openapi.ts`** — OpenAPI config via `openApiConfig()` factory.
|
|
121
|
+
- **`src/questpie/server/.generated/index.ts`** — Codegen output. Exports typed `app` instance and `App` type. Run `bunx questpie generate` to regenerate.
|
|
122
|
+
- **`src/questpie/admin/.generated/client.ts`** — Codegen output: pre-built admin client config. Run `bunx questpie generate` to regenerate.
|
|
112
123
|
- **`src/lib/env.ts`** — Type-safe env variables via `@t3-oss/env-core`. Add new env vars here with Zod schemas.
|
|
113
124
|
- **`questpie.config.ts`** — CLI config (migration directory, app reference).
|
|
114
|
-
- **`src/routes/api
|
|
125
|
+
- **`src/routes/api/$.ts`** — API catch-all handler. Serves REST + OpenAPI docs at `/api/docs`.
|
|
115
126
|
|
|
116
127
|
## How To Write Code
|
|
117
128
|
|
|
@@ -120,181 +131,173 @@ src/
|
|
|
120
131
|
Keep the entire builder chain in one file — single source of truth per entity:
|
|
121
132
|
|
|
122
133
|
```ts
|
|
123
|
-
// src/questpie/server/collections/posts.
|
|
124
|
-
import {
|
|
125
|
-
|
|
126
|
-
export const posts =
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
134
|
+
// src/questpie/server/collections/posts.ts
|
|
135
|
+
import { collection } from "#questpie/factories";
|
|
136
|
+
|
|
137
|
+
export const posts = collection("posts")
|
|
138
|
+
.fields(({ f }) => ({
|
|
139
|
+
title: f.text(255).label("Title").required(),
|
|
140
|
+
slug: f
|
|
141
|
+
.text(255)
|
|
142
|
+
.label("Slug")
|
|
143
|
+
.required()
|
|
144
|
+
.inputOptional()
|
|
145
|
+
.admin({
|
|
146
|
+
compute: {
|
|
147
|
+
handler: ({ data, prev }) => {
|
|
148
|
+
/* slugify title */
|
|
149
|
+
},
|
|
150
|
+
deps: ({ data }) => [data.title],
|
|
151
|
+
debounce: 300,
|
|
152
|
+
},
|
|
153
|
+
}),
|
|
154
|
+
content: f.richText().label("Content"),
|
|
155
|
+
published: f.boolean().label("Published").default(false),
|
|
156
|
+
category: f
|
|
157
|
+
.select([
|
|
158
|
+
{ value: "news", label: "News" },
|
|
159
|
+
{ value: "blog", label: "Blog" },
|
|
160
|
+
{ value: "tutorial", label: "Tutorial" },
|
|
161
|
+
])
|
|
162
|
+
.label("Category")
|
|
163
|
+
author: f.relation("users").label("Author"),
|
|
164
|
+
image: f.upload().label("Cover Image"),
|
|
165
|
+
}))
|
|
166
|
+
.title(({ f }) => f.title)
|
|
167
|
+
.admin(({ c }) => ({
|
|
168
|
+
label: "Posts",
|
|
169
|
+
icon: c.icon("ph:article"),
|
|
170
|
+
}))
|
|
171
|
+
.access({
|
|
172
|
+
read: true,
|
|
173
|
+
create: ({ session }) => !!session,
|
|
174
|
+
update: ({ session }) => !!session,
|
|
175
|
+
delete: ({ session }) => session?.user?.role === "admin",
|
|
176
|
+
})
|
|
177
|
+
.hooks({
|
|
178
|
+
beforeCreate: [
|
|
179
|
+
async ({ data, ctx }) => {
|
|
180
|
+
/* ... */ return data;
|
|
181
|
+
},
|
|
182
|
+
],
|
|
183
|
+
})
|
|
184
|
+
.list(({ v }) => v.collectionTable({}))
|
|
185
|
+
.form(({ v, f }) =>
|
|
186
|
+
v.collectionForm({
|
|
187
|
+
sidebar: { position: "right", fields: [f.slug, f.published, f.category] },
|
|
188
|
+
fields: [f.title, f.content, f.author, f.image],
|
|
189
|
+
}),
|
|
190
|
+
);
|
|
158
191
|
```
|
|
159
192
|
|
|
160
|
-
Then
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
193
|
+
Then (preferred):
|
|
194
|
+
|
|
195
|
+
1. Use `bun questpie add collection <name>` to scaffold files
|
|
196
|
+
2. Codegen runs automatically
|
|
197
|
+
3. Run `bun questpie migrate:create` to generate migration
|
|
198
|
+
|
|
199
|
+
Manual workflow (if you create files yourself):
|
|
200
|
+
|
|
201
|
+
1. Run `bunx questpie generate` to regenerate `.generated/index.ts`
|
|
202
|
+
2. Run `bun questpie migrate:create` to generate migration
|
|
203
|
+
|
|
204
|
+
Collections are auto-discovered by codegen — no manual registration needed.
|
|
165
205
|
|
|
166
206
|
### Available Field Types
|
|
167
207
|
|
|
168
|
-
`text`, `number`, `boolean`, `date`, `
|
|
208
|
+
**Core:** `text`, `number`, `boolean`, `date`, `datetime`, `time`, `select`, `relation`, `upload`, `object`, `json`, `from`, `email`, `url`, `textarea`.
|
|
209
|
+
**Admin module:** `richText`, `blocks` (provided by `@questpie/admin`)
|
|
169
210
|
|
|
170
211
|
### Creating a Global
|
|
171
212
|
|
|
172
213
|
```ts
|
|
173
|
-
// src/questpie/server/globals/site-settings.
|
|
174
|
-
import {
|
|
175
|
-
|
|
176
|
-
export const siteSettings =
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
214
|
+
// src/questpie/server/globals/site-settings.ts
|
|
215
|
+
import { global } from "#questpie/factories";
|
|
216
|
+
|
|
217
|
+
export const siteSettings = global("siteSettings")
|
|
218
|
+
.fields(({ f }) => ({
|
|
219
|
+
siteName: f.text(255).label("Site Name").required(),
|
|
220
|
+
description: f.textarea().label("Description"),
|
|
221
|
+
logo: f.upload().label("Logo"),
|
|
222
|
+
maintenanceMode: f.boolean().label("Maintenance Mode").default(false),
|
|
223
|
+
}))
|
|
224
|
+
.admin(({ c }) => ({ label: "Site Settings", icon: c.icon("ph:gear") }))
|
|
225
|
+
.form(({ v, f }) =>
|
|
226
|
+
v.globalForm({
|
|
227
|
+
fields: [f.siteName, f.description, f.logo, f.maintenanceMode],
|
|
228
|
+
}),
|
|
229
|
+
);
|
|
188
230
|
```
|
|
189
231
|
|
|
190
|
-
Then
|
|
191
|
-
1. Export from `src/questpie/server/globals/index.ts`
|
|
192
|
-
2. Add to `.globals({ ..., siteSettings })` in `app.ts`
|
|
193
|
-
3. Add to sidebar in `sidebar.ts`
|
|
194
|
-
4. Run `bun questpie migrate:create`
|
|
232
|
+
Then (preferred):
|
|
195
233
|
|
|
196
|
-
|
|
234
|
+
1. Use `bun questpie add global <name>` to scaffold files
|
|
235
|
+
2. Codegen runs automatically
|
|
236
|
+
3. Run `bun questpie migrate:create`
|
|
197
237
|
|
|
198
|
-
|
|
238
|
+
Manual workflow (if you create files yourself):
|
|
199
239
|
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
// rpc.ts — standalone RPC builder
|
|
203
|
-
import { rpc } from "questpie";
|
|
204
|
-
export const r = rpc();
|
|
205
|
-
```
|
|
240
|
+
1. Run `bunx questpie generate` to regenerate `.generated/index.ts`
|
|
241
|
+
2. Run `bun questpie migrate:create`
|
|
206
242
|
|
|
207
|
-
|
|
208
|
-
// app.ts — imports r (runtime), exports cms and appRpc separately
|
|
209
|
-
import { r } from "./rpc.js";
|
|
243
|
+
Globals are auto-discovered by codegen — no manual registration needed.
|
|
210
244
|
|
|
211
|
-
|
|
212
|
-
export const appRpc = r.router({ ...adminRpc, myFn });
|
|
245
|
+
### Creating a Server Route (End-to-End Type-Safe)
|
|
213
246
|
|
|
214
|
-
|
|
215
|
-
export type AppRpc = typeof appRpc;
|
|
216
|
-
```
|
|
247
|
+
Routes are defined as standalone files in `routes/` and auto-discovered by codegen.
|
|
217
248
|
|
|
218
|
-
**Step 1 — Define a
|
|
249
|
+
**Step 1 — Define a route:**
|
|
219
250
|
|
|
220
251
|
```ts
|
|
221
|
-
// src/questpie/server/
|
|
222
|
-
import {
|
|
252
|
+
// src/questpie/server/routes/get-stats.ts
|
|
253
|
+
import { route } from "questpie";
|
|
223
254
|
import { z } from "zod";
|
|
224
255
|
|
|
225
|
-
export
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
}
|
|
256
|
+
export default route()
|
|
257
|
+
.post()
|
|
258
|
+
.schema(
|
|
259
|
+
z.object({
|
|
260
|
+
period: z.enum(["day", "week", "month"]),
|
|
261
|
+
}),
|
|
262
|
+
)
|
|
263
|
+
.handler(async ({ input, collections }) => {
|
|
264
|
+
// input: typed from Zod schema; collections, db, session, etc. from AppContext
|
|
265
|
+
const count = await collections.posts.count({});
|
|
266
|
+
return { totalPosts: count, period: input.period };
|
|
267
|
+
});
|
|
236
268
|
```
|
|
237
269
|
|
|
238
|
-
**Step 2 —
|
|
239
|
-
|
|
240
|
-
```ts
|
|
241
|
-
import { getStats } from "./functions/get-stats.function.js";
|
|
270
|
+
**Step 2 — Run codegen:**
|
|
242
271
|
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
getStats,
|
|
246
|
-
});
|
|
272
|
+
```bash
|
|
273
|
+
bunx questpie generate
|
|
247
274
|
```
|
|
248
275
|
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
```ts
|
|
252
|
-
import { client } from "@/lib/cms-client";
|
|
253
|
-
|
|
254
|
-
const result = await client.rpc.getStats({ period: "week" });
|
|
255
|
-
// result: { totalPosts: number, period: string }
|
|
256
|
-
```
|
|
276
|
+
The route is auto-discovered and available at `/api/get-stats`.
|
|
257
277
|
|
|
258
278
|
**With access control:**
|
|
259
279
|
|
|
260
280
|
```ts
|
|
261
|
-
export
|
|
262
|
-
|
|
263
|
-
schema
|
|
264
|
-
handler
|
|
265
|
-
|
|
281
|
+
export default route()
|
|
282
|
+
.post()
|
|
283
|
+
.schema(z.object({ ... }))
|
|
284
|
+
.handler(async ({ input, session }) => {
|
|
285
|
+
if (session?.user?.role !== "admin") throw new Error("Forbidden");
|
|
286
|
+
return { ok: true };
|
|
287
|
+
});
|
|
266
288
|
```
|
|
267
289
|
|
|
268
290
|
**With TanStack Query:**
|
|
269
291
|
|
|
270
292
|
```ts
|
|
271
|
-
import { useQuery
|
|
293
|
+
import { useQuery } from "@tanstack/react-query";
|
|
272
294
|
|
|
273
295
|
const { data } = useQuery({
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
});
|
|
277
|
-
|
|
278
|
-
const mutation = useMutation({
|
|
279
|
-
mutationFn: (input) => client.rpc.createSomething(input),
|
|
296
|
+
queryKey: ["stats", period],
|
|
297
|
+
queryFn: () => client.routes.getStats({ period }),
|
|
280
298
|
});
|
|
281
299
|
```
|
|
282
300
|
|
|
283
|
-
**Type flow:**
|
|
284
|
-
|
|
285
|
-
```
|
|
286
|
-
rpc() → r.fn() handlers get typed `app`
|
|
287
|
-
↓
|
|
288
|
-
r.fn({ schema, handler }) → RpcProcedureDefinition<TInput, TOutput>
|
|
289
|
-
↓
|
|
290
|
-
r.router({ myFn }) → AppRpc type (preserves all function types)
|
|
291
|
-
↓
|
|
292
|
-
createClient<AppCMS, AppRpc>(...) → client.rpc is fully typed
|
|
293
|
-
↓
|
|
294
|
-
client.rpc.myFn(input) → Input: compile-time + runtime (Zod) validation
|
|
295
|
-
→ Output: inferred from handler return type
|
|
296
|
-
```
|
|
297
|
-
|
|
298
301
|
### Blocks (Page Builder)
|
|
299
302
|
|
|
300
303
|
Blocks are content building units for page builders and rich content areas.
|
|
@@ -302,128 +305,80 @@ Blocks are content building units for page builders and rich content areas.
|
|
|
302
305
|
**Simple block (no data fetching):**
|
|
303
306
|
|
|
304
307
|
```ts
|
|
305
|
-
// src/questpie/server/blocks.ts
|
|
306
|
-
import {
|
|
307
|
-
|
|
308
|
-
const heroBlock =
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
.prefetch({ with: { backgroundImage: true } }); // expand upload to full URL
|
|
323
|
-
|
|
324
|
-
export const blocks = { hero: heroBlock };
|
|
308
|
+
// src/questpie/server/blocks/hero.ts
|
|
309
|
+
import { block } from "#questpie/factories";
|
|
310
|
+
|
|
311
|
+
export const heroBlock = block("hero")
|
|
312
|
+
.admin(({ c }) => ({
|
|
313
|
+
label: "Hero Section",
|
|
314
|
+
icon: c.icon("ph:image"),
|
|
315
|
+
category: { label: "Sections", icon: c.icon("ph:layout"), order: 1 },
|
|
316
|
+
}))
|
|
317
|
+
.fields(({ f }) => ({
|
|
318
|
+
title: f.text(255).label("Title").required(),
|
|
319
|
+
subtitle: f.textarea().label("Subtitle"),
|
|
320
|
+
backgroundImage: f.upload().label("Background Image"),
|
|
321
|
+
ctaText: f.text(255).label("CTA Text"),
|
|
322
|
+
ctaLink: f.text(255).label("CTA Link"),
|
|
323
|
+
}))
|
|
324
|
+
.prefetch({ with: { backgroundImage: true } }); // expand upload to full URL
|
|
325
325
|
```
|
|
326
326
|
|
|
327
327
|
**Block with dynamic data fetching (prefetch):**
|
|
328
328
|
|
|
329
329
|
```ts
|
|
330
|
-
const teamBlock =
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
});
|
|
330
|
+
const teamBlock = block("team")
|
|
331
|
+
.admin(({ c }) => ({
|
|
332
|
+
label: "Team",
|
|
333
|
+
icon: c.icon("ph:users"),
|
|
334
|
+
category: { label: "Sections", icon: c.icon("ph:layout"), order: 1 },
|
|
335
|
+
}))
|
|
336
|
+
.fields(({ f }) => ({
|
|
337
|
+
title: f.text(255).label("Title"),
|
|
338
|
+
limit: f.number().label("Number to Show").default(4),
|
|
339
|
+
}))
|
|
340
|
+
.prefetch(async ({ values, ctx }) => {
|
|
341
|
+
const res = await ctx.collections.members.find({
|
|
342
|
+
limit: values.limit || 4,
|
|
343
|
+
where: { isActive: true },
|
|
344
|
+
with: { avatar: true },
|
|
345
|
+
});
|
|
346
|
+
return { members: res.docs };
|
|
347
|
+
});
|
|
349
348
|
```
|
|
350
349
|
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
```ts
|
|
354
|
-
import { blocks } from "./blocks";
|
|
355
|
-
|
|
356
|
-
export const cms = qb
|
|
357
|
-
.collections({ ... })
|
|
358
|
-
.blocks(blocks) // ← register blocks
|
|
359
|
-
.build({ ... });
|
|
360
|
-
```
|
|
350
|
+
Blocks in `blocks/` are auto-discovered by codegen. No manual registration needed.
|
|
361
351
|
|
|
362
352
|
**Use blocks in a collection's richText field:**
|
|
363
353
|
|
|
364
354
|
```ts
|
|
365
|
-
content: f.richText(
|
|
366
|
-
label: "Content",
|
|
367
|
-
blocks: [heroBlock, teamBlock],
|
|
368
|
-
})
|
|
355
|
+
content: f.richText().label("Content").blocks([heroBlock, teamBlock]);
|
|
369
356
|
```
|
|
370
357
|
|
|
371
|
-
#### Blocks & Circular Dependencies
|
|
372
|
-
|
|
373
|
-
When blocks use `.prefetch()` with functional handlers that need typed access to `ctx.app` (e.g., `ctx.app.api.collections.posts.find(...)`), you hit a circular dependency:
|
|
374
|
-
|
|
375
|
-
- `app.ts` imports `blocks.ts` (to register blocks)
|
|
376
|
-
- `blocks.ts` wants to import `AppCMS` from `app.ts` (for typed prefetch)
|
|
377
|
-
- **Circular!**
|
|
358
|
+
#### Blocks & Circular Dependencies
|
|
378
359
|
|
|
379
|
-
|
|
360
|
+
Block prefetch handlers receive `ctx` with fully typed `collections` and `globals` via `AppContext` augmentation. Use `ctx.collections.*` directly — no app import needed:
|
|
380
361
|
|
|
381
362
|
```ts
|
|
382
|
-
//
|
|
383
|
-
import {
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
export type AppCMS = typeof cms;
|
|
363
|
+
// blocks/latest-posts.ts
|
|
364
|
+
import { block } from "#questpie/factories";
|
|
365
|
+
|
|
366
|
+
export const latestPostsBlock = block("latest-posts")
|
|
367
|
+
.fields(({ f }) => ({
|
|
368
|
+
count: f.number().label("Number of Posts").default(3),
|
|
369
|
+
}))
|
|
370
|
+
.prefetch(async ({ values, ctx }) => {
|
|
371
|
+
const res = await ctx.collections.posts.find({
|
|
372
|
+
limit: values.count || 3,
|
|
373
|
+
where: { published: true },
|
|
374
|
+
});
|
|
375
|
+
return { posts: res.docs };
|
|
376
|
+
});
|
|
398
377
|
```
|
|
399
378
|
|
|
400
|
-
|
|
401
|
-
// blocks.ts — imports BaseCMS (not AppCMS) to avoid circular dependency
|
|
402
|
-
import { typedApp, type Where } from "questpie";
|
|
403
|
-
import type { BaseCMS } from "./app";
|
|
404
|
-
|
|
405
|
-
const latestPostsBlock = qb
|
|
406
|
-
.block("latest-posts")
|
|
407
|
-
.fields((f) => ({
|
|
408
|
-
count: f.number({ label: "Number of Posts", default: 3 }),
|
|
409
|
-
}))
|
|
410
|
-
.prefetch(async ({ values, ctx }) => {
|
|
411
|
-
// Use typedApp<BaseCMS> for typed access without circular import
|
|
412
|
-
const cms = typedApp<BaseCMS>(ctx.app);
|
|
413
|
-
const res = await cms.api.collections.posts.find({
|
|
414
|
-
limit: values.count || 3,
|
|
415
|
-
where: { published: true },
|
|
416
|
-
orderBy: { createdAt: "desc" },
|
|
417
|
-
});
|
|
418
|
-
return { posts: res.docs };
|
|
419
|
-
});
|
|
420
|
-
```
|
|
379
|
+
Do NOT import `app` from `#questpie` inside block files — these are imported BY `.generated/index.ts`, creating circular dependencies. Use the `ctx` parameters instead.
|
|
421
380
|
|
|
422
|
-
|
|
423
|
-
- `BaseCMS` has the same collections/globals as `AppCMS` — blocks just aren't part of the type yet
|
|
424
|
-
- `typedApp<BaseCMS>(ctx.app)` casts the untyped `ctx.app` to the typed CMS API
|
|
425
|
-
- This is a known limitation; we're working on a more ergonomic solution
|
|
426
|
-
- If your blocks only use declarative prefetch (`{ with: { field: true } }`), you don't need this pattern at all — it's only needed for functional prefetch that calls `ctx.app.api.*`
|
|
381
|
+
If your blocks only use declarative prefetch (`{ with: { field: true } }`), you don't need a function at all.
|
|
427
382
|
|
|
428
383
|
### Reactive Fields
|
|
429
384
|
|
|
@@ -437,75 +392,83 @@ Fields support reactive behaviors in `meta.admin`:
|
|
|
437
392
|
All reactive handlers run **server-side** with access to `ctx.db`, `ctx.user`, `ctx.req`.
|
|
438
393
|
|
|
439
394
|
```ts
|
|
440
|
-
fields: (f) => ({
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
395
|
+
fields: ({ f }) => ({
|
|
396
|
+
country: f.relation("countries").label("Country"),
|
|
397
|
+
city: f
|
|
398
|
+
.relation("cities")
|
|
399
|
+
.label("City")
|
|
400
|
+
.admin({
|
|
401
|
+
options: {
|
|
402
|
+
handler: async ({ data, search, ctx }) => {
|
|
403
|
+
const cities = await ctx.db.query.cities.findMany({
|
|
404
|
+
where: { countryId: data.country },
|
|
405
|
+
});
|
|
406
|
+
return {
|
|
407
|
+
options: cities.map((c) => ({ value: c.id, label: c.name })),
|
|
408
|
+
};
|
|
409
|
+
},
|
|
410
|
+
deps: ({ data }) => [data.country],
|
|
411
|
+
},
|
|
412
|
+
}),
|
|
413
|
+
status: f
|
|
414
|
+
.select([
|
|
415
|
+
{ value: "draft", label: "Draft" },
|
|
416
|
+
{ value: "published", label: "Published" },
|
|
417
|
+
{ value: "archived", label: "Archived" },
|
|
418
|
+
])
|
|
419
|
+
.label("Status")
|
|
420
|
+
publishedAt: f
|
|
421
|
+
.datetime()
|
|
422
|
+
.label("Published At")
|
|
423
|
+
.admin({
|
|
424
|
+
hidden: ({ data }) => data.status !== "published",
|
|
425
|
+
}),
|
|
426
|
+
});
|
|
468
427
|
```
|
|
469
428
|
|
|
470
429
|
### Admin Configuration (Client-Side)
|
|
471
430
|
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
import { adminModule, qa } from "@questpie/admin/client";
|
|
475
|
-
import type { AppCMS } from "@/questpie/server/cms";
|
|
431
|
+
The admin client config is auto-generated by codegen into `admin/.generated/client.ts`.
|
|
432
|
+
No manual builder setup is needed. Run `bunx questpie generate` to regenerate.
|
|
476
433
|
|
|
477
|
-
|
|
434
|
+
```ts
|
|
435
|
+
// src/questpie/admin/admin.ts (re-export for convenience)
|
|
436
|
+
export { default as admin } from "./.generated/client";
|
|
478
437
|
```
|
|
479
438
|
|
|
480
439
|
```ts
|
|
481
440
|
// src/questpie/admin/hooks.ts
|
|
482
441
|
import { createTypedHooks } from "@questpie/admin/client";
|
|
483
|
-
import type {
|
|
442
|
+
import type { App } from "#questpie";
|
|
484
443
|
|
|
485
444
|
export const {
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
445
|
+
useCollectionList,
|
|
446
|
+
useCollectionCount,
|
|
447
|
+
useCollectionItem,
|
|
448
|
+
useCollectionCreate,
|
|
449
|
+
useCollectionUpdate,
|
|
450
|
+
useCollectionDelete,
|
|
451
|
+
useGlobal,
|
|
452
|
+
useGlobalUpdate,
|
|
453
|
+
} = createTypedHooks<App>();
|
|
490
454
|
```
|
|
491
455
|
|
|
492
|
-
###
|
|
456
|
+
### QUESTPIE Route Handler
|
|
493
457
|
|
|
494
458
|
```ts
|
|
495
|
-
// src/routes/api
|
|
459
|
+
// src/routes/api/$.ts
|
|
496
460
|
import { createFetchHandler } from "questpie";
|
|
497
|
-
import {
|
|
498
|
-
import { appRpc, cms } from "~/questpie/server/cms";
|
|
461
|
+
import { app } from "#questpie";
|
|
499
462
|
|
|
500
|
-
const handler =
|
|
501
|
-
createFetchHandler(cms, { basePath: "/api/cms", rpc: appRpc }),
|
|
502
|
-
{ cms, rpc: appRpc, basePath: "/api/cms", info: { title: "My API", version: "1.0.0" } },
|
|
503
|
-
);
|
|
463
|
+
const handler = createFetchHandler(app, { basePath: "/api" });
|
|
504
464
|
```
|
|
505
465
|
|
|
466
|
+
OpenAPI is registered as a module in `src/questpie/server/modules.ts` via `openApiModule` — no wrapper needed in the route handler.
|
|
467
|
+
|
|
506
468
|
### Icons
|
|
507
469
|
|
|
508
470
|
Use `@iconify/react` with Phosphor icon set:
|
|
471
|
+
|
|
509
472
|
- Prefix: `ph:` (e.g., `ph:house`, `ph:article`, `ph:gear`)
|
|
510
473
|
- Weight variants: `-bold`, `-fill`, `-duotone`, `-light`, `-thin`
|
|
511
474
|
- Regular weight = no suffix (default)
|
|
@@ -515,13 +478,16 @@ Use `@iconify/react` with Phosphor icon set:
|
|
|
515
478
|
## Environment Variables
|
|
516
479
|
|
|
517
480
|
Type-safe via `@t3-oss/env-core` in `src/lib/env.ts`. All env vars must be:
|
|
481
|
+
|
|
518
482
|
1. Declared with Zod schema in `env.ts`
|
|
519
483
|
2. Accessed via `env.VAR_NAME` (not `process.env.VAR_NAME`)
|
|
520
484
|
|
|
521
485
|
Required:
|
|
486
|
+
|
|
522
487
|
- `DATABASE_URL` — PostgreSQL connection string
|
|
523
488
|
|
|
524
489
|
Optional (with defaults):
|
|
490
|
+
|
|
525
491
|
- `APP_URL` — Application URL (default: `http://localhost:3000`)
|
|
526
492
|
- `BETTER_AUTH_SECRET` — Auth secret key
|
|
527
493
|
- `MAIL_ADAPTER` — `console` or `smtp`
|
|
@@ -553,11 +519,98 @@ Always use these exact versions — check `package.json` before upgrading:
|
|
|
553
519
|
|
|
554
520
|
- **Schema rules in client code** — Validation, access control, and hooks belong on the server.
|
|
555
521
|
- **Splitting a collection across files** — Keep the full `.collection().fields().admin().list().form()` chain in one file.
|
|
556
|
-
- **Business logic in route handlers** —
|
|
522
|
+
- **Business logic in route handlers** — Mounting files should only mount handlers. Business logic goes in server routes, hooks, or jobs.
|
|
557
523
|
- **Hardcoding view components** — Use the registry pattern for custom views.
|
|
558
524
|
- **Using `process.env` directly** — Use the `env` object from `src/lib/env.ts`.
|
|
559
525
|
- **Using Zod v3 API** — This project uses Zod v4. Use `z.object()` etc. from `zod` (v4).
|
|
560
526
|
- **Using `asChild` prop** — This project uses `@base-ui/react`, not Radix. Use `render` prop instead.
|
|
561
527
|
- **Using Radix UI or Lucide icons** — Use `@base-ui/react` and `@iconify/react` with `ph:` prefix.
|
|
562
528
|
- **Adding UI config to database schema** — Admin UI config is UI-only, defined in builder chain.
|
|
563
|
-
- **Importing `
|
|
529
|
+
- **Importing `app` from `#questpie` in blocks/collections/hooks** — Files inside `collections/`, `globals/`, `routes/`, `hooks/`, `blocks/` are imported BY `.generated/index.ts`, so importing from it back creates circular dependencies. Use the `ctx` parameters instead.
|
|
530
|
+
|
|
531
|
+
## Live Preview
|
|
532
|
+
|
|
533
|
+
QUESTPIE supports live preview with a split-screen editor. The current implementation refreshes the preview iframe after save/autosave and uses `postMessage` for field/block focus sync.
|
|
534
|
+
|
|
535
|
+
### Key Principles
|
|
536
|
+
|
|
537
|
+
- **Same-tab preview** = iframe refresh after save/autosave plus `postMessage` focus events
|
|
538
|
+
- **Frontend hook** = `useCollectionPreview({ initialData, onRefresh })`
|
|
539
|
+
- **Field focus** = `PreviewProvider` + `PreviewField`
|
|
540
|
+
- **Block field paths** = `BlockScopeProvider` + `PreviewField`
|
|
541
|
+
|
|
542
|
+
### Server Config
|
|
543
|
+
|
|
544
|
+
Add `.preview()` to a collection to enable the split-screen editor:
|
|
545
|
+
|
|
546
|
+
```ts
|
|
547
|
+
// src/questpie/server/collections/pages.ts
|
|
548
|
+
import { collection } from "#questpie/factories";
|
|
549
|
+
|
|
550
|
+
export const pages = collection("pages")
|
|
551
|
+
.fields(({ f }) => ({
|
|
552
|
+
title: f.text(255).label("Title").required(),
|
|
553
|
+
slug: f.text(255).label("Slug").required(),
|
|
554
|
+
content: f.blocks().label("Content"),
|
|
555
|
+
}))
|
|
556
|
+
.preview({
|
|
557
|
+
enabled: true,
|
|
558
|
+
position: "right",
|
|
559
|
+
defaultWidth: 50,
|
|
560
|
+
url: ({ record }) => {
|
|
561
|
+
const slug = record.slug as string;
|
|
562
|
+
return slug === "home" ? "/?preview=true" : `/${slug}?preview=true`;
|
|
563
|
+
},
|
|
564
|
+
});
|
|
565
|
+
```
|
|
566
|
+
|
|
567
|
+
### Frontend Integration
|
|
568
|
+
|
|
569
|
+
Use `useCollectionPreview` in your frontend page components:
|
|
570
|
+
|
|
571
|
+
```tsx
|
|
572
|
+
// src/routes/[slug].tsx
|
|
573
|
+
import {
|
|
574
|
+
PreviewField,
|
|
575
|
+
PreviewProvider,
|
|
576
|
+
useCollectionPreview,
|
|
577
|
+
} from "@questpie/admin/client";
|
|
578
|
+
|
|
579
|
+
function PageComponent({ initialData }) {
|
|
580
|
+
const router = useRouter();
|
|
581
|
+
const preview = useCollectionPreview({
|
|
582
|
+
initialData,
|
|
583
|
+
onRefresh: () => router.invalidate(),
|
|
584
|
+
});
|
|
585
|
+
|
|
586
|
+
return (
|
|
587
|
+
<PreviewProvider
|
|
588
|
+
isPreviewMode={preview.isPreviewMode}
|
|
589
|
+
focusedField={preview.focusedField}
|
|
590
|
+
onFieldClick={preview.handleFieldClick}
|
|
591
|
+
>
|
|
592
|
+
<PreviewField field="title" as="h1">
|
|
593
|
+
{preview.data.title}
|
|
594
|
+
</PreviewField>
|
|
595
|
+
</PreviewProvider>
|
|
596
|
+
);
|
|
597
|
+
}
|
|
598
|
+
```
|
|
599
|
+
|
|
600
|
+
#### Protocol
|
|
601
|
+
|
|
602
|
+
The implemented preview messages are simple `postMessage` events:
|
|
603
|
+
|
|
604
|
+
| Field | Description |
|
|
605
|
+
| ----------------- | --------------------------------------- |
|
|
606
|
+
| `PREVIEW_READY` | Preview iframe tells admin it is ready |
|
|
607
|
+
| `PREVIEW_REFRESH` | Admin asks iframe to refresh data |
|
|
608
|
+
| `FIELD_CLICKED` | Preview asks admin to focus a field |
|
|
609
|
+
| `BLOCK_CLICKED` | Preview asks admin to select a block |
|
|
610
|
+
| `FOCUS_FIELD` | Admin asks preview to highlight a field |
|
|
611
|
+
| `SELECT_BLOCK` | Admin asks preview to highlight a block |
|
|
612
|
+
|
|
613
|
+
### Anti-Patterns (Preview)
|
|
614
|
+
|
|
615
|
+
- **Using V2-only APIs in this template** — `useQuestpiePreview`, `PreviewRoot`, and `PreviewBlock` are not exported yet.
|
|
616
|
+
- **Importing `app` inside previewed collection/block files** — use handler `ctx` values to avoid generated-app cycles.
|