create-questpie 1.0.0 → 2.0.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/README.md +20 -12
- package/dist/index.mjs +9 -2
- package/package.json +21 -21
- package/templates/tanstack-start/AGENTS.md +366 -318
- package/templates/tanstack-start/CLAUDE.md +84 -52
- package/templates/tanstack-start/README.md +59 -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/admin.ts +8 -4
- package/templates/tanstack-start/src/questpie/server/.generated/factories.ts +318 -0
- package/templates/tanstack-start/src/questpie/server/.generated/index.ts +153 -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/
|
|
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/app.ts`** — App config (locale, access, hooks, context) via `appConfig()` factory.
|
|
119
|
+
- **`src/questpie/server/config/admin.ts`** — Admin config (sidebar, dashboard, branding, locale) via `adminConfig()` factory.
|
|
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,170 @@ 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";
|
|
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
|
+
.label("Category")
|
|
159
|
+
.options(["news", "blog", "tutorial"]),
|
|
160
|
+
author: f.relation().label("Author").to("users"),
|
|
161
|
+
image: f.upload().label("Cover Image"),
|
|
162
|
+
}))
|
|
163
|
+
.title(({ f }) => f.title)
|
|
164
|
+
.admin(({ c }) => ({
|
|
165
|
+
label: "Posts",
|
|
166
|
+
icon: c.icon("ph:article"),
|
|
167
|
+
}))
|
|
168
|
+
.access({
|
|
169
|
+
read: true,
|
|
170
|
+
create: ({ session }) => !!session,
|
|
171
|
+
update: ({ session }) => !!session,
|
|
172
|
+
delete: ({ session }) => session?.user?.role === "admin",
|
|
173
|
+
})
|
|
174
|
+
.hooks({
|
|
175
|
+
beforeCreate: [
|
|
176
|
+
async ({ data, ctx }) => {
|
|
177
|
+
/* ... */ return data;
|
|
178
|
+
},
|
|
179
|
+
],
|
|
180
|
+
})
|
|
181
|
+
.list(({ v }) => v.collectionTable({}))
|
|
182
|
+
.form(({ v, f }) =>
|
|
183
|
+
v.collectionForm({
|
|
184
|
+
sidebar: { position: "right", fields: [f.slug, f.published, f.category] },
|
|
185
|
+
fields: [f.title, f.content, f.author, f.image],
|
|
186
|
+
}),
|
|
187
|
+
);
|
|
158
188
|
```
|
|
159
189
|
|
|
160
|
-
Then
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
190
|
+
Then (preferred):
|
|
191
|
+
|
|
192
|
+
1. Use `bun questpie add collection <name>` to scaffold files
|
|
193
|
+
2. Codegen runs automatically
|
|
194
|
+
3. Run `bun questpie migrate:create` to generate migration
|
|
195
|
+
|
|
196
|
+
Manual workflow (if you create files yourself):
|
|
197
|
+
|
|
198
|
+
1. Run `bunx questpie generate` to regenerate `.generated/index.ts`
|
|
199
|
+
2. Run `bun questpie migrate:create` to generate migration
|
|
200
|
+
|
|
201
|
+
Collections are auto-discovered by codegen — no manual registration needed.
|
|
165
202
|
|
|
166
203
|
### Available Field Types
|
|
167
204
|
|
|
168
|
-
`text`, `number`, `boolean`, `date`, `
|
|
205
|
+
**Core:** `text`, `number`, `boolean`, `date`, `datetime`, `time`, `select`, `relation`, `upload`, `object`, `json`, `from`, `email`, `url`, `textarea`.
|
|
206
|
+
**Admin module:** `richText`, `blocks` (provided by `@questpie/admin`)
|
|
169
207
|
|
|
170
208
|
### Creating a Global
|
|
171
209
|
|
|
172
210
|
```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
|
-
|
|
211
|
+
// src/questpie/server/globals/site-settings.ts
|
|
212
|
+
import { global } from "questpie";
|
|
213
|
+
|
|
214
|
+
export const siteSettings = global("site_settings")
|
|
215
|
+
.fields(({ f }) => ({
|
|
216
|
+
siteName: f.text(255).label("Site Name").required(),
|
|
217
|
+
description: f.textarea().label("Description"),
|
|
218
|
+
logo: f.upload().label("Logo"),
|
|
219
|
+
maintenanceMode: f.boolean().label("Maintenance Mode").default(false),
|
|
220
|
+
}))
|
|
221
|
+
.admin(({ c }) => ({ label: "Site Settings", icon: c.icon("ph:gear") }))
|
|
222
|
+
.form(({ v, f }) =>
|
|
223
|
+
v.globalForm({
|
|
224
|
+
fields: [f.siteName, f.description, f.logo, f.maintenanceMode],
|
|
225
|
+
}),
|
|
226
|
+
);
|
|
188
227
|
```
|
|
189
228
|
|
|
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`
|
|
229
|
+
Then (preferred):
|
|
195
230
|
|
|
196
|
-
|
|
231
|
+
1. Use `bun questpie add global <name>` to scaffold files
|
|
232
|
+
2. Codegen runs automatically
|
|
233
|
+
3. Run `bun questpie migrate:create`
|
|
197
234
|
|
|
198
|
-
|
|
235
|
+
Manual workflow (if you create files yourself):
|
|
199
236
|
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
// rpc.ts — standalone RPC builder
|
|
203
|
-
import { rpc } from "questpie";
|
|
204
|
-
export const r = rpc();
|
|
205
|
-
```
|
|
237
|
+
1. Run `bunx questpie generate` to regenerate `.generated/index.ts`
|
|
238
|
+
2. Run `bun questpie migrate:create`
|
|
206
239
|
|
|
207
|
-
|
|
208
|
-
// app.ts — imports r (runtime), exports cms and appRpc separately
|
|
209
|
-
import { r } from "./rpc.js";
|
|
240
|
+
Globals are auto-discovered by codegen — no manual registration needed.
|
|
210
241
|
|
|
211
|
-
|
|
212
|
-
export const appRpc = r.router({ ...adminRpc, myFn });
|
|
242
|
+
### Creating a Server Route (End-to-End Type-Safe)
|
|
213
243
|
|
|
214
|
-
|
|
215
|
-
export type AppRpc = typeof appRpc;
|
|
216
|
-
```
|
|
244
|
+
Routes are defined as standalone files in `routes/` and auto-discovered by codegen.
|
|
217
245
|
|
|
218
|
-
**Step 1 — Define a
|
|
246
|
+
**Step 1 — Define a route:**
|
|
219
247
|
|
|
220
248
|
```ts
|
|
221
|
-
// src/questpie/server/
|
|
222
|
-
import {
|
|
249
|
+
// src/questpie/server/routes/get-stats.ts
|
|
250
|
+
import { route } from "questpie";
|
|
223
251
|
import { z } from "zod";
|
|
224
252
|
|
|
225
|
-
export
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
}
|
|
253
|
+
export default route()
|
|
254
|
+
.post()
|
|
255
|
+
.schema(
|
|
256
|
+
z.object({
|
|
257
|
+
period: z.enum(["day", "week", "month"]),
|
|
258
|
+
}),
|
|
259
|
+
)
|
|
260
|
+
.handler(async ({ input, collections }) => {
|
|
261
|
+
// input: typed from Zod schema; collections, db, session, etc. from AppContext
|
|
262
|
+
const count = await collections.posts.count({});
|
|
263
|
+
return { totalPosts: count, period: input.period };
|
|
264
|
+
});
|
|
236
265
|
```
|
|
237
266
|
|
|
238
|
-
**Step 2 —
|
|
239
|
-
|
|
240
|
-
```ts
|
|
241
|
-
import { getStats } from "./functions/get-stats.function.js";
|
|
267
|
+
**Step 2 — Run codegen:**
|
|
242
268
|
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
getStats,
|
|
246
|
-
});
|
|
269
|
+
```bash
|
|
270
|
+
bunx questpie generate
|
|
247
271
|
```
|
|
248
272
|
|
|
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
|
-
```
|
|
273
|
+
The route is auto-discovered and available at `/api/get-stats`.
|
|
257
274
|
|
|
258
275
|
**With access control:**
|
|
259
276
|
|
|
260
277
|
```ts
|
|
261
|
-
export
|
|
262
|
-
|
|
263
|
-
schema
|
|
264
|
-
handler
|
|
265
|
-
|
|
278
|
+
export default route()
|
|
279
|
+
.post()
|
|
280
|
+
.schema(z.object({ ... }))
|
|
281
|
+
.handler(async ({ input, session }) => {
|
|
282
|
+
if (session?.user?.role !== "admin") throw new Error("Forbidden");
|
|
283
|
+
return { ok: true };
|
|
284
|
+
});
|
|
266
285
|
```
|
|
267
286
|
|
|
268
287
|
**With TanStack Query:**
|
|
269
288
|
|
|
270
289
|
```ts
|
|
271
|
-
import { useQuery
|
|
290
|
+
import { useQuery } from "@tanstack/react-query";
|
|
272
291
|
|
|
273
292
|
const { data } = useQuery({
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
});
|
|
277
|
-
|
|
278
|
-
const mutation = useMutation({
|
|
279
|
-
mutationFn: (input) => client.rpc.createSomething(input),
|
|
293
|
+
queryKey: ["stats", period],
|
|
294
|
+
queryFn: () => client.routes.getStats({ period }),
|
|
280
295
|
});
|
|
281
296
|
```
|
|
282
297
|
|
|
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
298
|
### Blocks (Page Builder)
|
|
299
299
|
|
|
300
300
|
Blocks are content building units for page builders and rich content areas.
|
|
@@ -302,128 +302,80 @@ Blocks are content building units for page builders and rich content areas.
|
|
|
302
302
|
**Simple block (no data fetching):**
|
|
303
303
|
|
|
304
304
|
```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 };
|
|
305
|
+
// src/questpie/server/blocks/hero.ts
|
|
306
|
+
import { block } from "#questpie/factories";
|
|
307
|
+
|
|
308
|
+
export const heroBlock = block("hero")
|
|
309
|
+
.admin(({ c }) => ({
|
|
310
|
+
label: "Hero Section",
|
|
311
|
+
icon: c.icon("ph:image"),
|
|
312
|
+
category: { label: "Sections", icon: c.icon("ph:layout"), order: 1 },
|
|
313
|
+
}))
|
|
314
|
+
.fields(({ f }) => ({
|
|
315
|
+
title: f.text(255).label("Title").required(),
|
|
316
|
+
subtitle: f.textarea().label("Subtitle"),
|
|
317
|
+
backgroundImage: f.upload().label("Background Image"),
|
|
318
|
+
ctaText: f.text(255).label("CTA Text"),
|
|
319
|
+
ctaLink: f.text(255).label("CTA Link"),
|
|
320
|
+
}))
|
|
321
|
+
.prefetch({ with: { backgroundImage: true } }); // expand upload to full URL
|
|
325
322
|
```
|
|
326
323
|
|
|
327
324
|
**Block with dynamic data fetching (prefetch):**
|
|
328
325
|
|
|
329
326
|
```ts
|
|
330
|
-
const teamBlock =
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
});
|
|
327
|
+
const teamBlock = block("team")
|
|
328
|
+
.admin(({ c }) => ({
|
|
329
|
+
label: "Team",
|
|
330
|
+
icon: c.icon("ph:users"),
|
|
331
|
+
category: { label: "Sections", icon: c.icon("ph:layout"), order: 1 },
|
|
332
|
+
}))
|
|
333
|
+
.fields(({ f }) => ({
|
|
334
|
+
title: f.text(255).label("Title"),
|
|
335
|
+
limit: f.number().label("Number to Show").default(4),
|
|
336
|
+
}))
|
|
337
|
+
.prefetch(async ({ values, ctx }) => {
|
|
338
|
+
const res = await ctx.collections.members.find({
|
|
339
|
+
limit: values.limit || 4,
|
|
340
|
+
where: { isActive: true },
|
|
341
|
+
with: { avatar: true },
|
|
342
|
+
});
|
|
343
|
+
return { members: res.docs };
|
|
344
|
+
});
|
|
349
345
|
```
|
|
350
346
|
|
|
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
|
-
```
|
|
347
|
+
Blocks in `blocks/` are auto-discovered by codegen. No manual registration needed.
|
|
361
348
|
|
|
362
349
|
**Use blocks in a collection's richText field:**
|
|
363
350
|
|
|
364
351
|
```ts
|
|
365
|
-
content: f.richText(
|
|
366
|
-
label: "Content",
|
|
367
|
-
blocks: [heroBlock, teamBlock],
|
|
368
|
-
})
|
|
352
|
+
content: f.richText().label("Content").blocks([heroBlock, teamBlock]);
|
|
369
353
|
```
|
|
370
354
|
|
|
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!**
|
|
355
|
+
#### Blocks & Circular Dependencies
|
|
378
356
|
|
|
379
|
-
|
|
357
|
+
Block prefetch handlers receive `ctx` with fully typed `collections` and `globals` via `AppContext` augmentation. Use `ctx.collections.*` directly — no app import needed:
|
|
380
358
|
|
|
381
359
|
```ts
|
|
382
|
-
//
|
|
383
|
-
import {
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
export type AppCMS = typeof cms;
|
|
360
|
+
// blocks/latest-posts.ts
|
|
361
|
+
import { block } from "#questpie/factories";
|
|
362
|
+
|
|
363
|
+
export const latestPostsBlock = block("latest-posts")
|
|
364
|
+
.fields(({ f }) => ({
|
|
365
|
+
count: f.number().label("Number of Posts").default(3),
|
|
366
|
+
}))
|
|
367
|
+
.prefetch(async ({ values, ctx }) => {
|
|
368
|
+
const res = await ctx.collections.posts.find({
|
|
369
|
+
limit: values.count || 3,
|
|
370
|
+
where: { published: true },
|
|
371
|
+
});
|
|
372
|
+
return { posts: res.docs };
|
|
373
|
+
});
|
|
398
374
|
```
|
|
399
375
|
|
|
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
|
-
```
|
|
376
|
+
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
377
|
|
|
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.*`
|
|
378
|
+
If your blocks only use declarative prefetch (`{ with: { field: true } }`), you don't need a function at all.
|
|
427
379
|
|
|
428
380
|
### Reactive Fields
|
|
429
381
|
|
|
@@ -437,75 +389,81 @@ Fields support reactive behaviors in `meta.admin`:
|
|
|
437
389
|
All reactive handlers run **server-side** with access to `ctx.db`, `ctx.user`, `ctx.req`.
|
|
438
390
|
|
|
439
391
|
```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
|
-
})
|
|
392
|
+
fields: ({ f }) => ({
|
|
393
|
+
country: f.relation().to("countries").label("Country"),
|
|
394
|
+
city: f
|
|
395
|
+
.relation()
|
|
396
|
+
.to("cities")
|
|
397
|
+
.label("City")
|
|
398
|
+
.admin({
|
|
399
|
+
options: {
|
|
400
|
+
handler: async ({ data, search, ctx }) => {
|
|
401
|
+
const cities = await ctx.db.query.cities.findMany({
|
|
402
|
+
where: { countryId: data.country },
|
|
403
|
+
});
|
|
404
|
+
return {
|
|
405
|
+
options: cities.map((c) => ({ value: c.id, label: c.name })),
|
|
406
|
+
};
|
|
407
|
+
},
|
|
408
|
+
deps: ({ data }) => [data.country],
|
|
409
|
+
},
|
|
410
|
+
}),
|
|
411
|
+
status: f
|
|
412
|
+
.select()
|
|
413
|
+
.label("Status")
|
|
414
|
+
.options(["draft", "published", "archived"]),
|
|
415
|
+
publishedAt: f
|
|
416
|
+
.datetime()
|
|
417
|
+
.label("Published At")
|
|
418
|
+
.admin({
|
|
419
|
+
hidden: ({ data }) => data.status !== "published",
|
|
420
|
+
}),
|
|
421
|
+
});
|
|
468
422
|
```
|
|
469
423
|
|
|
470
424
|
### Admin Configuration (Client-Side)
|
|
471
425
|
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
import { adminModule, qa } from "@questpie/admin/client";
|
|
475
|
-
import type { AppCMS } from "@/questpie/server/cms";
|
|
426
|
+
The admin client config is auto-generated by codegen into `admin/.generated/client.ts`.
|
|
427
|
+
No manual builder setup is needed. Run `bunx questpie generate` to regenerate.
|
|
476
428
|
|
|
477
|
-
|
|
429
|
+
```ts
|
|
430
|
+
// src/questpie/admin/admin.ts (re-export for convenience)
|
|
431
|
+
export { default as admin } from "./.generated/client";
|
|
478
432
|
```
|
|
479
433
|
|
|
480
434
|
```ts
|
|
481
435
|
// src/questpie/admin/hooks.ts
|
|
482
436
|
import { createTypedHooks } from "@questpie/admin/client";
|
|
483
|
-
import type {
|
|
437
|
+
import type { App } from "#questpie";
|
|
484
438
|
|
|
485
439
|
export const {
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
440
|
+
useCollectionList,
|
|
441
|
+
useCollectionCount,
|
|
442
|
+
useCollectionItem,
|
|
443
|
+
useCollectionCreate,
|
|
444
|
+
useCollectionUpdate,
|
|
445
|
+
useCollectionDelete,
|
|
446
|
+
useGlobal,
|
|
447
|
+
useGlobalUpdate,
|
|
448
|
+
} = createTypedHooks<App>();
|
|
490
449
|
```
|
|
491
450
|
|
|
492
|
-
###
|
|
451
|
+
### QUESTPIE Route Handler
|
|
493
452
|
|
|
494
453
|
```ts
|
|
495
|
-
// src/routes/api
|
|
454
|
+
// src/routes/api/$.ts
|
|
496
455
|
import { createFetchHandler } from "questpie";
|
|
497
|
-
import {
|
|
498
|
-
import { appRpc, cms } from "~/questpie/server/cms";
|
|
456
|
+
import { app } from "#questpie";
|
|
499
457
|
|
|
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
|
-
);
|
|
458
|
+
const handler = createFetchHandler(app, { basePath: "/api" });
|
|
504
459
|
```
|
|
505
460
|
|
|
461
|
+
OpenAPI is registered as a module in `src/questpie/server/modules.ts` via `openApiModule()` — no wrapper needed in the route handler.
|
|
462
|
+
|
|
506
463
|
### Icons
|
|
507
464
|
|
|
508
465
|
Use `@iconify/react` with Phosphor icon set:
|
|
466
|
+
|
|
509
467
|
- Prefix: `ph:` (e.g., `ph:house`, `ph:article`, `ph:gear`)
|
|
510
468
|
- Weight variants: `-bold`, `-fill`, `-duotone`, `-light`, `-thin`
|
|
511
469
|
- Regular weight = no suffix (default)
|
|
@@ -515,13 +473,16 @@ Use `@iconify/react` with Phosphor icon set:
|
|
|
515
473
|
## Environment Variables
|
|
516
474
|
|
|
517
475
|
Type-safe via `@t3-oss/env-core` in `src/lib/env.ts`. All env vars must be:
|
|
476
|
+
|
|
518
477
|
1. Declared with Zod schema in `env.ts`
|
|
519
478
|
2. Accessed via `env.VAR_NAME` (not `process.env.VAR_NAME`)
|
|
520
479
|
|
|
521
480
|
Required:
|
|
481
|
+
|
|
522
482
|
- `DATABASE_URL` — PostgreSQL connection string
|
|
523
483
|
|
|
524
484
|
Optional (with defaults):
|
|
485
|
+
|
|
525
486
|
- `APP_URL` — Application URL (default: `http://localhost:3000`)
|
|
526
487
|
- `BETTER_AUTH_SECRET` — Auth secret key
|
|
527
488
|
- `MAIL_ADAPTER` — `console` or `smtp`
|
|
@@ -553,11 +514,98 @@ Always use these exact versions — check `package.json` before upgrading:
|
|
|
553
514
|
|
|
554
515
|
- **Schema rules in client code** — Validation, access control, and hooks belong on the server.
|
|
555
516
|
- **Splitting a collection across files** — Keep the full `.collection().fields().admin().list().form()` chain in one file.
|
|
556
|
-
- **Business logic in route handlers** —
|
|
517
|
+
- **Business logic in route handlers** — Mounting files should only mount handlers. Business logic goes in server routes, hooks, or jobs.
|
|
557
518
|
- **Hardcoding view components** — Use the registry pattern for custom views.
|
|
558
519
|
- **Using `process.env` directly** — Use the `env` object from `src/lib/env.ts`.
|
|
559
520
|
- **Using Zod v3 API** — This project uses Zod v4. Use `z.object()` etc. from `zod` (v4).
|
|
560
521
|
- **Using `asChild` prop** — This project uses `@base-ui/react`, not Radix. Use `render` prop instead.
|
|
561
522
|
- **Using Radix UI or Lucide icons** — Use `@base-ui/react` and `@iconify/react` with `ph:` prefix.
|
|
562
523
|
- **Adding UI config to database schema** — Admin UI config is UI-only, defined in builder chain.
|
|
563
|
-
- **Importing `
|
|
524
|
+
- **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.
|
|
525
|
+
|
|
526
|
+
## Live Preview
|
|
527
|
+
|
|
528
|
+
QUESTPIE supports live preview with a split-screen editor. The preview architecture uses a direct `postMessage` patch bus for instant feedback — NOT save-driven iframe reloads.
|
|
529
|
+
|
|
530
|
+
### Key Principles
|
|
531
|
+
|
|
532
|
+
- **Same-tab preview** = direct patch bus via `postMessage` (editor iframe and preview share the same browser tab)
|
|
533
|
+
- **Save/autosave** = persistence only, NOT the live transport mechanism
|
|
534
|
+
- **Realtime** = extension for detached/shared preview only (e.g., another browser tab or collaborator)
|
|
535
|
+
- **Hybrid model**: local draft patches give instant response; server reconciles derived data (slugs, relations, computed fields)
|
|
536
|
+
- **Preview wrappers must prevent accidental navigation** inside the iframe
|
|
537
|
+
|
|
538
|
+
### Server Config
|
|
539
|
+
|
|
540
|
+
Add `.preview()` to a collection to enable the split-screen editor:
|
|
541
|
+
|
|
542
|
+
```ts
|
|
543
|
+
// src/questpie/server/collections/pages.ts
|
|
544
|
+
import { collection } from "questpie";
|
|
545
|
+
|
|
546
|
+
export const pages = collection("pages")
|
|
547
|
+
.fields(({ f }) => ({
|
|
548
|
+
title: f.text(255).label("Title").required(),
|
|
549
|
+
slug: f.text(255).label("Slug").required(),
|
|
550
|
+
content: f.blocks().label("Content"),
|
|
551
|
+
}))
|
|
552
|
+
.preview({
|
|
553
|
+
url: ({ record }) => {
|
|
554
|
+
const slug = record.slug as string;
|
|
555
|
+
return slug === "home" ? "/?preview=true" : `/${slug}?preview=true`;
|
|
556
|
+
},
|
|
557
|
+
watch: ["title", "slug", "content"],
|
|
558
|
+
strategy: "hybrid", // "instant" | "server" | "hybrid"
|
|
559
|
+
});
|
|
560
|
+
```
|
|
561
|
+
|
|
562
|
+
| Strategy | Behavior |
|
|
563
|
+
| ----------- | ------------------------------------------------------------------------ |
|
|
564
|
+
| `"instant"` | Patches applied locally only — no server round-trip |
|
|
565
|
+
| `"server"` | Every change round-trips through the server before preview updates |
|
|
566
|
+
| `"hybrid"` | Local patches for instant response + server reconcile for derived fields |
|
|
567
|
+
|
|
568
|
+
### Frontend Integration
|
|
569
|
+
|
|
570
|
+
Use `useQuestpiePreview` in your frontend page components to receive live patches:
|
|
571
|
+
|
|
572
|
+
```tsx
|
|
573
|
+
// src/routes/[slug].tsx
|
|
574
|
+
import { useQuestpiePreview } from "@questpie/admin/client";
|
|
575
|
+
|
|
576
|
+
function PageComponent({ initialData }) {
|
|
577
|
+
const router = useRouter();
|
|
578
|
+
const { data } = useQuestpiePreview({
|
|
579
|
+
initialData,
|
|
580
|
+
reconcile: () => router.invalidate(), // called on COMMIT — refetch server data
|
|
581
|
+
});
|
|
582
|
+
|
|
583
|
+
return (
|
|
584
|
+
<PreviewRoot>
|
|
585
|
+
<h1>
|
|
586
|
+
<PreviewField path="title">{data.title}</PreviewField>
|
|
587
|
+
</h1>
|
|
588
|
+
<PreviewBlock id="content">{/* block renderers */}</PreviewBlock>
|
|
589
|
+
</PreviewRoot>
|
|
590
|
+
);
|
|
591
|
+
}
|
|
592
|
+
```
|
|
593
|
+
|
|
594
|
+
#### Protocol
|
|
595
|
+
|
|
596
|
+
Each `postMessage` carries a structured envelope:
|
|
597
|
+
|
|
598
|
+
| Field | Description |
|
|
599
|
+
| ----------------- | -------------------------------------- |
|
|
600
|
+
| `sessionId` | Unique preview session identifier |
|
|
601
|
+
| `seq` | Monotonic sequence number |
|
|
602
|
+
| `timestamp` | `Date.now()` at send time (number) |
|
|
603
|
+
| `protocolVersion` | Protocol version for forward compat |
|
|
604
|
+
| `patches` | Array of field path + value patch ops |
|
|
605
|
+
|
|
606
|
+
### Anti-Patterns (Preview)
|
|
607
|
+
|
|
608
|
+
- **Using `router.invalidate()` as the core live-preview mechanism** — this causes full data refetches and visible flicker. Use the patch bus instead.
|
|
609
|
+
- **Tying preview freshness to save/autosave** — save is for persistence. Preview updates flow through `postMessage` patches.
|
|
610
|
+
- **Using realtime transport for same-tab preview** — realtime (SSE/WebSocket) is for detached or shared preview sessions, not the default same-tab flow.
|
|
611
|
+
- **Allowing navigation inside the preview iframe** — preview wrappers must intercept link clicks and route changes to prevent the iframe from navigating away.
|