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,2670 +0,0 @@
|
|
|
1
|
-
# QuestPie Framework 101
|
|
2
|
-
|
|
3
|
-
Everything a developer needs to understand and work with QuestPie CMS.
|
|
4
|
-
|
|
5
|
-
---
|
|
6
|
-
|
|
7
|
-
## Table of Contents
|
|
8
|
-
|
|
9
|
-
1. [Architecture Overview](#1-architecture-overview)
|
|
10
|
-
2. [Project Structure & File Conventions](#2-project-structure--file-conventions)
|
|
11
|
-
3. [App Bootstrap](#3-app-bootstrap)
|
|
12
|
-
4. [Modules](#4-modules)
|
|
13
|
-
5. [Collections](#5-collections)
|
|
14
|
-
6. [Globals](#6-globals)
|
|
15
|
-
7. [Fields](#7-fields)
|
|
16
|
-
8. [Relations](#8-relations)
|
|
17
|
-
9. [Hooks](#9-hooks)
|
|
18
|
-
10. [Access Control](#10-access-control)
|
|
19
|
-
11. [Routes](#11-routes)
|
|
20
|
-
12. [Services](#12-services)
|
|
21
|
-
13. [Jobs](#13-jobs)
|
|
22
|
-
14. [Email Templates](#14-email-templates)
|
|
23
|
-
15. [Migrations & Seeds](#15-migrations--seeds)
|
|
24
|
-
16. [Uploads & Storage](#16-uploads--storage)
|
|
25
|
-
17. [Versioning & Workflow](#17-versioning--workflow)
|
|
26
|
-
18. [Search](#18-search)
|
|
27
|
-
19. [Realtime](#19-realtime)
|
|
28
|
-
20. [i18n / Localization](#20-i18n--localization)
|
|
29
|
-
21. [AppContext — What's Available Everywhere](#21-appcontext--whats-available-everywhere)
|
|
30
|
-
22. [Server-Side CRUD API](#22-server-side-crud-api)
|
|
31
|
-
23. [Client SDK](#23-client-sdk)
|
|
32
|
-
24. [TanStack Query Integration](#24-tanstack-query-integration)
|
|
33
|
-
25. [Admin Panel](#25-admin-panel)
|
|
34
|
-
26. [Admin Builder Extensions](#26-admin-builder-extensions)
|
|
35
|
-
27. [Admin Fields, Views, Widgets, Blocks, Components](#27-admin-fields-views-widgets-blocks-components)
|
|
36
|
-
28. [Codegen & the .generated/ Directory](#28-codegen--the-generated-directory)
|
|
37
|
-
29. [Adapters & Infrastructure](#29-adapters--infrastructure)
|
|
38
|
-
30. [TypeScript Type Augmentation](#30-typescript-type-augmentation)
|
|
39
|
-
31. [CLI Reference](#31-cli-reference)
|
|
40
|
-
32. [How It All Connects — The Big Picture](#32-how-it-all-connects--the-big-picture)
|
|
41
|
-
|
|
42
|
-
---
|
|
43
|
-
|
|
44
|
-
## 1. Architecture Overview
|
|
45
|
-
|
|
46
|
-
QuestPie is a **headless CMS framework** for TypeScript. You define your content model declaratively (collections, globals, fields), and the framework gives you:
|
|
47
|
-
|
|
48
|
-
- A fully typed **REST API** (auto-generated from your schema)
|
|
49
|
-
- A pluggable **admin panel** (React, server-driven)
|
|
50
|
-
- **Typed client SDK** + TanStack Query hooks
|
|
51
|
-
- Background **jobs**, **email**, **storage**, **search**, **realtime** — all pluggable
|
|
52
|
-
|
|
53
|
-
### Tech Stack
|
|
54
|
-
|
|
55
|
-
| Layer | Technology |
|
|
56
|
-
| -------- | ------------------------------------------------- |
|
|
57
|
-
| Runtime | **Bun** |
|
|
58
|
-
| Database | **PostgreSQL** via **Drizzle ORM** |
|
|
59
|
-
| Auth | **Better Auth** |
|
|
60
|
-
| Storage | **FlyDrive** (local FS, S3, R2, GCS) |
|
|
61
|
-
| Admin UI | **React** + **TanStack Router** + **Tailwind** |
|
|
62
|
-
| HTTP | Custom trie-based router (no Express/Hono needed) |
|
|
63
|
-
| Build | **tsdown** (Rolldown-based) + **Turbo** monorepo |
|
|
64
|
-
|
|
65
|
-
### Monorepo Packages
|
|
66
|
-
|
|
67
|
-
```
|
|
68
|
-
packages/
|
|
69
|
-
questpie/ ← Core framework (server, client, CLI, shared)
|
|
70
|
-
admin/ ← Admin panel (server augmentation + React client)
|
|
71
|
-
elysia/ ← Elysia HTTP adapter
|
|
72
|
-
hono/ ← Hono HTTP adapter
|
|
73
|
-
next/ ← Next.js adapter
|
|
74
|
-
openapi/ ← OpenAPI/Scalar plugin
|
|
75
|
-
tanstack-query/ ← TanStack Query integration
|
|
76
|
-
create-questpie/ ← Project scaffolder (bunx create-questpie)
|
|
77
|
-
```
|
|
78
|
-
|
|
79
|
-
---
|
|
80
|
-
|
|
81
|
-
## 2. Project Structure & File Conventions
|
|
82
|
-
|
|
83
|
-
A QuestPie project follows a **convention-over-configuration** file layout. The codegen system scans these directories and auto-wires everything.
|
|
84
|
-
|
|
85
|
-
```
|
|
86
|
-
<project>/
|
|
87
|
-
questpie.config.ts ← CLI entry (wraps app + cli config)
|
|
88
|
-
src/
|
|
89
|
-
questpie/
|
|
90
|
-
server/ ← Server root (all data + behavior)
|
|
91
|
-
questpie.config.ts ← runtimeConfig({ db, app, storage, ... })
|
|
92
|
-
modules.ts ← export default [adminModule, ...] as const
|
|
93
|
-
app.ts ← re-export of .generated/index (stable import)
|
|
94
|
-
config/
|
|
95
|
-
auth.ts ← authConfig({...}) (from "questpie")
|
|
96
|
-
app.ts ← appConfig({...}) (from "questpie", optional)
|
|
97
|
-
admin.ts ← adminConfig({...}) (from "#questpie/factories")
|
|
98
|
-
collections/ ← One file per collection
|
|
99
|
-
globals/ ← One file per global
|
|
100
|
-
routes/ ← API routes (recursive, default export)
|
|
101
|
-
jobs/ ← Background jobs
|
|
102
|
-
services/ ← Custom services
|
|
103
|
-
emails/ ← Email templates (.tsx)
|
|
104
|
-
blocks/ ← Content blocks
|
|
105
|
-
fields/ ← Custom field types
|
|
106
|
-
migrations/ ← DB migrations
|
|
107
|
-
seeds/ ← DB seeds
|
|
108
|
-
messages/ ← i18n messages
|
|
109
|
-
features/ ← Feature-first alternative layout
|
|
110
|
-
<feature>/
|
|
111
|
-
collections/
|
|
112
|
-
routes/
|
|
113
|
-
jobs/
|
|
114
|
-
...
|
|
115
|
-
.generated/ ← DO NOT EDIT (codegen output)
|
|
116
|
-
index.ts
|
|
117
|
-
factories.ts
|
|
118
|
-
admin/ ← Admin client config
|
|
119
|
-
admin.ts
|
|
120
|
-
modules.ts
|
|
121
|
-
blocks/ ← Block renderer components
|
|
122
|
-
.generated/
|
|
123
|
-
client.ts
|
|
124
|
-
routes/
|
|
125
|
-
api/$.ts ← HTTP catch-all → createFetchHandler(app)
|
|
126
|
-
```
|
|
127
|
-
|
|
128
|
-
### Key Rules
|
|
129
|
-
|
|
130
|
-
- **Files starting with `_`** are private/utility — skipped by discovery.
|
|
131
|
-
- **`index.ts`** files are always ignored by the scanner (use them as barrel re-exports if needed).
|
|
132
|
-
- **File names become keys**: `site-settings.ts` → `siteSettings` (kebab → camelCase). Underscores are preserved (`my_table.ts` → `my_table`) for PostgreSQL naming.
|
|
133
|
-
- **`features/`** mirrors the same directory structure — entities from both flat and feature layouts are merged.
|
|
134
|
-
|
|
135
|
-
### Export Conventions Per Directory
|
|
136
|
-
|
|
137
|
-
| Directory | Export Style | Factory |
|
|
138
|
-
| -------------- | ------------------------- | -------------------- |
|
|
139
|
-
| `collections/` | **named** export | `collection("name")` |
|
|
140
|
-
| `globals/` | **named** export | `global("name")` |
|
|
141
|
-
| `routes/` | **default** export | `route()` |
|
|
142
|
-
| `jobs/` | **default** export | `job({...})` |
|
|
143
|
-
| `services/` | **named** export | `service()` |
|
|
144
|
-
| `emails/` | **default** export (.tsx) | `email({...})` |
|
|
145
|
-
| `blocks/` | **named** export | `block("name")` |
|
|
146
|
-
| `migrations/` | **default** export | `migration({...})` |
|
|
147
|
-
| `seeds/` | **default** export | `seed({...})` |
|
|
148
|
-
|
|
149
|
-
---
|
|
150
|
-
|
|
151
|
-
## 3. App Bootstrap
|
|
152
|
-
|
|
153
|
-
### How It Starts
|
|
154
|
-
|
|
155
|
-
```
|
|
156
|
-
questpie.config.ts → modules.ts → codegen → .generated/index.ts → createApp()
|
|
157
|
-
```
|
|
158
|
-
|
|
159
|
-
**Step 1** — `questpie.config.ts` declares infrastructure (DB, storage, email, etc.):
|
|
160
|
-
|
|
161
|
-
```ts
|
|
162
|
-
import { runtimeConfig } from "questpie";
|
|
163
|
-
|
|
164
|
-
export default runtimeConfig({
|
|
165
|
-
app: { url: "http://localhost:3000" },
|
|
166
|
-
db: { url: process.env.DATABASE_URL },
|
|
167
|
-
storage: { basePath: "/api" },
|
|
168
|
-
email: { adapter: new ConsoleAdapter() },
|
|
169
|
-
});
|
|
170
|
-
```
|
|
171
|
-
|
|
172
|
-
**Step 2** — `modules.ts` declares which module packages to use:
|
|
173
|
-
|
|
174
|
-
```ts
|
|
175
|
-
import { adminModule } from "@questpie/admin/server";
|
|
176
|
-
import { openApiModule } from "@questpie/openapi";
|
|
177
|
-
|
|
178
|
-
export default [adminModule, openApiModule] as const;
|
|
179
|
-
```
|
|
180
|
-
|
|
181
|
-
**Step 3** — `questpie generate` scans everything and writes `.generated/index.ts` which calls:
|
|
182
|
-
|
|
183
|
-
```ts
|
|
184
|
-
export const app = await createApp(
|
|
185
|
-
{ modules, collections, globals, routes, jobs, seeds, migrations, ... },
|
|
186
|
-
runtime
|
|
187
|
-
);
|
|
188
|
-
```
|
|
189
|
-
|
|
190
|
-
**Step 4** — Inside `createApp()`:
|
|
191
|
-
|
|
192
|
-
1. Auto-prepends `coreModule` (built-in routes, services, field types)
|
|
193
|
-
2. Flattens all modules **depth-first** (sub-modules first, parent last)
|
|
194
|
-
3. **Merges** contributions per key — later modules override earlier ones
|
|
195
|
-
4. Wraps user-level entities as `__user` module (appended **last** = user always wins)
|
|
196
|
-
5. Creates the `Questpie` instance with merged config
|
|
197
|
-
6. Initializes all services (`db`, `auth`, `storage`, `queue`, `email`, `kv`, `logger`, `search`, `realtime`)
|
|
198
|
-
|
|
199
|
-
**Step 5** — The HTTP handler connects it all:
|
|
200
|
-
|
|
201
|
-
```ts
|
|
202
|
-
// src/routes/api/$.ts (TanStack Start example)
|
|
203
|
-
import { createFetchHandler } from "questpie";
|
|
204
|
-
import { app } from "@/questpie/server/app";
|
|
205
|
-
import { createAPIFileRoute } from "@tanstack/react-start/api";
|
|
206
|
-
|
|
207
|
-
const handler = createFetchHandler(app, { basePath: "/api" });
|
|
208
|
-
|
|
209
|
-
export const APIRoute = createAPIFileRoute("/api/$")({
|
|
210
|
-
GET: ({ request }) => handler(request),
|
|
211
|
-
POST: ({ request }) => handler(request),
|
|
212
|
-
PUT: ({ request }) => handler(request),
|
|
213
|
-
DELETE: ({ request }) => handler(request),
|
|
214
|
-
PATCH: ({ request }) => handler(request),
|
|
215
|
-
});
|
|
216
|
-
|
|
217
|
-
// For standalone Bun.serve:
|
|
218
|
-
// Bun.serve({ fetch: createFetchHandler(app) });
|
|
219
|
-
```
|
|
220
|
-
|
|
221
|
-
This single handler serves all collection CRUD, auth, search, realtime, storage, and custom routes via a **trie-based dispatcher**. The exact wiring depends on your framework — TanStack Start uses `createAPIFileRoute`, Hono uses middleware, Next.js uses route handlers.
|
|
222
|
-
|
|
223
|
-
---
|
|
224
|
-
|
|
225
|
-
## 4. Modules
|
|
226
|
-
|
|
227
|
-
Modules are the **packaging unit** of the framework. They are plain static objects — no class instances, no runtime instantiation.
|
|
228
|
-
|
|
229
|
-
```ts
|
|
230
|
-
import { module } from "questpie";
|
|
231
|
-
|
|
232
|
-
export const billingModule = module({
|
|
233
|
-
name: "billing",
|
|
234
|
-
modules: [stripeModule], // sub-dependencies
|
|
235
|
-
collections: { invoices, subscriptions },
|
|
236
|
-
globals: { billingSettings },
|
|
237
|
-
routes: { "create-checkout": createCheckoutRoute },
|
|
238
|
-
jobs: { retryPayment },
|
|
239
|
-
services: { stripe: stripeService },
|
|
240
|
-
migrations: [billingMigration001],
|
|
241
|
-
seeds: [billingSeeds],
|
|
242
|
-
messages: { en: { "billing.title": "Billing" } },
|
|
243
|
-
config: { app: billingAppConfig },
|
|
244
|
-
});
|
|
245
|
-
```
|
|
246
|
-
|
|
247
|
-
### What Modules Can Contribute
|
|
248
|
-
|
|
249
|
-
| Key | Type | Merge Strategy |
|
|
250
|
-
| ------------- | ------------------------------------- | --------------------------- |
|
|
251
|
-
| `collections` | `Record<string, CollectionBuilder>` | spread (later wins per key) |
|
|
252
|
-
| `globals` | `Record<string, GlobalBuilder>` | spread |
|
|
253
|
-
| `routes` | `Record<string, RouteDefinition>` | spread |
|
|
254
|
-
| `jobs` | `Record<string, JobDefinition>` | spread |
|
|
255
|
-
| `services` | `Record<string, ServiceBuilder>` | spread |
|
|
256
|
-
| `fields` | `Record<string, FieldTypeDefinition>` | spread |
|
|
257
|
-
| `migrations` | `Migration[]` | array concat |
|
|
258
|
-
| `seeds` | `Seed[]` | array concat |
|
|
259
|
-
| `messages` | `Record<locale, Record<key, string>>` | deep merge per locale |
|
|
260
|
-
| `config` | `{ app?, auth?, admin?, ... }` | per-key strategy |
|
|
261
|
-
|
|
262
|
-
### Built-in Modules
|
|
263
|
-
|
|
264
|
-
| Module | Package | Provides |
|
|
265
|
-
| --------------- | --------------------------- | ------------------------------------------------------------------------------------------- |
|
|
266
|
-
| `coreModule` | `questpie` (auto-prepended) | All REST routes, all services, built-in field types |
|
|
267
|
-
| `starterModule` | `questpie` | Auth collections (user, session, account, verification, apikey, assets), Better Auth config |
|
|
268
|
-
| `adminModule` | `@questpie/admin` | Admin panel routes, views, components, admin-specific collections |
|
|
269
|
-
| `auditModule` | `@questpie/admin` | Audit log collection + cleanup job |
|
|
270
|
-
| `openApiModule` | `@questpie/openapi` | OpenAPI schema + Scalar docs UI |
|
|
271
|
-
|
|
272
|
-
### Module Resolution
|
|
273
|
-
|
|
274
|
-
Modules are resolved **depth-first**: sub-modules are processed before their parent. If two modules share the same `name`, the **last** occurrence wins (deduplication by name). This allows overriding.
|
|
275
|
-
|
|
276
|
-
---
|
|
277
|
-
|
|
278
|
-
## 5. Collections
|
|
279
|
-
|
|
280
|
-
Collections are the **primary data primitive** — each one maps to a PostgreSQL table with auto-generated CRUD, hooks, access control, and admin UI.
|
|
281
|
-
|
|
282
|
-
```ts
|
|
283
|
-
import { collection } from "#questpie/factories";
|
|
284
|
-
|
|
285
|
-
export const posts = collection("posts")
|
|
286
|
-
.options({
|
|
287
|
-
timestamps: true, // adds createdAt, updatedAt
|
|
288
|
-
softDelete: true, // adds deletedAt (records are "trashed", not deleted)
|
|
289
|
-
versioning: {
|
|
290
|
-
// creates a _versions table
|
|
291
|
-
workflow: true, // enables draft → published stages
|
|
292
|
-
},
|
|
293
|
-
})
|
|
294
|
-
.fields(({ f }) => ({
|
|
295
|
-
title: f.text(255).required().label("Title"),
|
|
296
|
-
slug: f.text(255).required(),
|
|
297
|
-
content: f.richText().localized(),
|
|
298
|
-
status: f.select(["internal", "featured"]).default("internal"),
|
|
299
|
-
author: f.relation("users"),
|
|
300
|
-
tags: f.relation("tags").manyToMany({ through: "post_tags" }),
|
|
301
|
-
cover: f.upload(),
|
|
302
|
-
}))
|
|
303
|
-
.title(({ f }) => f.title)
|
|
304
|
-
.indexes(({ table }) => [uniqueIndex("posts_slug_unique").on(table.slug)])
|
|
305
|
-
.access({
|
|
306
|
-
read: true, // public
|
|
307
|
-
create: ({ session }) => !!session, // authenticated only
|
|
308
|
-
update: ({ session, data }) => data.authorId === session?.user.id, // own records
|
|
309
|
-
delete: ({ session }) => session?.user.role === "admin",
|
|
310
|
-
})
|
|
311
|
-
.hooks({
|
|
312
|
-
beforeChange: async ({ data, operation }) => {
|
|
313
|
-
if (operation === "create") {
|
|
314
|
-
data.slug = slugify(data.title);
|
|
315
|
-
}
|
|
316
|
-
return data;
|
|
317
|
-
},
|
|
318
|
-
afterChange: async ({ data, queue }) => {
|
|
319
|
-
await queue.indexRecords.publish({ collection: "posts", ids: [data.id] });
|
|
320
|
-
},
|
|
321
|
-
})
|
|
322
|
-
.searchable({ content: (r) => r.title })
|
|
323
|
-
.admin(({ c }) => ({
|
|
324
|
-
label: "Blog Posts",
|
|
325
|
-
icon: c.icon({ name: "ph:article" }),
|
|
326
|
-
group: "content",
|
|
327
|
-
}))
|
|
328
|
-
.list(({ v, f, a }) =>
|
|
329
|
-
v.collectionTable({
|
|
330
|
-
columns: [f.title, f.status, f.author, f.createdAt],
|
|
331
|
-
defaultSort: { field: f.createdAt, direction: "desc" },
|
|
332
|
-
filterable: [f.status, f.author],
|
|
333
|
-
actions: {
|
|
334
|
-
header: { primary: [a.create()] },
|
|
335
|
-
row: [a.delete()],
|
|
336
|
-
bulk: [a.deleteMany()],
|
|
337
|
-
},
|
|
338
|
-
}),
|
|
339
|
-
)
|
|
340
|
-
.form(({ v, f }) =>
|
|
341
|
-
v.collectionForm({
|
|
342
|
-
fields: [
|
|
343
|
-
f.title,
|
|
344
|
-
f.slug,
|
|
345
|
-
{ type: "section", label: "Content", fields: [f.content] },
|
|
346
|
-
{
|
|
347
|
-
type: "tabs",
|
|
348
|
-
tabs: [
|
|
349
|
-
{ id: "media", label: "Media", fields: [f.cover] },
|
|
350
|
-
{
|
|
351
|
-
id: "seo",
|
|
352
|
-
label: "SEO",
|
|
353
|
-
fields: [f.metaTitle, f.metaDescription],
|
|
354
|
-
},
|
|
355
|
-
],
|
|
356
|
-
},
|
|
357
|
-
],
|
|
358
|
-
sidebar: {
|
|
359
|
-
fields: [f.status, f.author, f.tags],
|
|
360
|
-
},
|
|
361
|
-
}),
|
|
362
|
-
)
|
|
363
|
-
.preview({
|
|
364
|
-
enabled: true,
|
|
365
|
-
url: ({ record, locale }) => `/blog/${record.slug}?locale=${locale}`,
|
|
366
|
-
position: "right",
|
|
367
|
-
})
|
|
368
|
-
.actions(({ a, c }) => ({
|
|
369
|
-
custom: [
|
|
370
|
-
a.action({
|
|
371
|
-
id: "publish",
|
|
372
|
-
label: "Publish Now",
|
|
373
|
-
icon: c.icon({ name: "ph:rocket" }),
|
|
374
|
-
confirmation: {
|
|
375
|
-
title: "Publish this post?",
|
|
376
|
-
description: "It will become visible to all readers.",
|
|
377
|
-
},
|
|
378
|
-
handler: async ({ record, collections }) => {
|
|
379
|
-
await collections.posts.transitionStage({
|
|
380
|
-
id: record.id,
|
|
381
|
-
stage: "published",
|
|
382
|
-
});
|
|
383
|
-
return { type: "success", toast: { message: "Published!" } };
|
|
384
|
-
},
|
|
385
|
-
}),
|
|
386
|
-
],
|
|
387
|
-
}));
|
|
388
|
-
```
|
|
389
|
-
|
|
390
|
-
### CollectionBuilder Methods
|
|
391
|
-
|
|
392
|
-
| Method | Purpose |
|
|
393
|
-
| -------------------------------- | ----------------------------------------------------------------- |
|
|
394
|
-
| `.fields(({ f }) => {...})` | Define fields (see [Fields](#7-fields)) |
|
|
395
|
-
| `.options({...})` | timestamps, softDelete, versioning |
|
|
396
|
-
| `.title(({ f }) => f.name)` | Which field is the display title |
|
|
397
|
-
| `.indexes(({ table }) => [...])` | Drizzle indexes/constraints |
|
|
398
|
-
| `.access({...})` | Permission rules (see [Access Control](#10-access-control)) |
|
|
399
|
-
| `.hooks({...})` | Lifecycle hooks (see [Hooks](#9-hooks)) |
|
|
400
|
-
| `.searchable({...})` | Full-text search config |
|
|
401
|
-
| `.validation({...})` | Zod validation overrides |
|
|
402
|
-
| `.upload({...})` | Turn into upload collection (see [Uploads](#16-uploads--storage)) |
|
|
403
|
-
| `.set(key, value)` | Plugin extension point |
|
|
404
|
-
| `.merge(other)` | Combine two builders |
|
|
405
|
-
| `.admin({...})` | Admin panel metadata (label, icon, group) |
|
|
406
|
-
| `.list({...})` | List view config |
|
|
407
|
-
| `.form({...})` | Form view config |
|
|
408
|
-
| `.preview({...})` | Live preview config |
|
|
409
|
-
| `.actions({...})` | Custom server actions |
|
|
410
|
-
|
|
411
|
-
> `.admin()`, `.list()`, `.form()`, `.preview()`, `.actions()` are added by the admin plugin — they're not available without `@questpie/admin`.
|
|
412
|
-
|
|
413
|
-
---
|
|
414
|
-
|
|
415
|
-
## 6. Globals
|
|
416
|
-
|
|
417
|
-
Globals are **singleton documents** — one row per global (or one per tenant if `scoped`). Think "site settings", "homepage config", "footer links".
|
|
418
|
-
|
|
419
|
-
```ts
|
|
420
|
-
import { global } from "#questpie/factories";
|
|
421
|
-
|
|
422
|
-
export const siteSettings = global("site_settings")
|
|
423
|
-
.fields(({ f }) => ({
|
|
424
|
-
siteName: f.text(255).required().default("My Site"),
|
|
425
|
-
description: f.textarea(),
|
|
426
|
-
logo: f.upload(),
|
|
427
|
-
socialLinks: f.object({
|
|
428
|
-
twitter: f.url(),
|
|
429
|
-
github: f.url(),
|
|
430
|
-
linkedin: f.url(),
|
|
431
|
-
}),
|
|
432
|
-
}))
|
|
433
|
-
.options({
|
|
434
|
-
timestamps: true,
|
|
435
|
-
scoped: ({ session }) => session?.user.tenantId, // multi-tenant scoping
|
|
436
|
-
})
|
|
437
|
-
.access({
|
|
438
|
-
read: true,
|
|
439
|
-
update: ({ session }) => session?.user.role === "admin",
|
|
440
|
-
})
|
|
441
|
-
.admin(({ c }) => ({
|
|
442
|
-
label: "Site Settings",
|
|
443
|
-
icon: c.icon({ name: "ph:gear" }),
|
|
444
|
-
}))
|
|
445
|
-
.form(({ v, f }) =>
|
|
446
|
-
v.globalForm({
|
|
447
|
-
fields: [f.siteName, f.description, f.logo, f.socialLinks],
|
|
448
|
-
}),
|
|
449
|
-
);
|
|
450
|
-
```
|
|
451
|
-
|
|
452
|
-
### GlobalBuilder Methods
|
|
453
|
-
|
|
454
|
-
Same as CollectionBuilder but without `.list()`, `.preview()`, `.actions()`, `.indexes()`, `.searchable()`, `.upload()`. Globals have `get` + `update` instead of full CRUD.
|
|
455
|
-
|
|
456
|
-
---
|
|
457
|
-
|
|
458
|
-
## 7. Fields
|
|
459
|
-
|
|
460
|
-
Fields define the shape of your data. Every field starts from a **field type factory** and can be customized with chained methods.
|
|
461
|
-
|
|
462
|
-
### Built-in Field Types
|
|
463
|
-
|
|
464
|
-
All accessed via `f` in the `.fields()` callback:
|
|
465
|
-
|
|
466
|
-
| Factory | DB Column | JS Type | Key Options |
|
|
467
|
-
| ---------------------- | ------------------------------------ | -------------- | -------------------------------------------------------------------------------- |
|
|
468
|
-
| `f.text(maxLength?)` | `varchar(n)` / `text` | `string` | `.pattern(re)`, `.trim()`, `.lowercase()`, `.uppercase()`, `.min(n)`, `.max(n)` |
|
|
469
|
-
| `f.textarea()` | `text` | `string` | `.min(n)`, `.max(n)` |
|
|
470
|
-
| `f.email(maxLength?)` | `varchar(255)` | `string` | `.min(n)`, `.max(n)` |
|
|
471
|
-
| `f.url(maxLength?)` | `varchar(2048)` | `string` | `.min(n)`, `.max(n)` |
|
|
472
|
-
| `f.number(mode?)` | `integer` / `real` / `numeric` / ... | `number` | `.min(n)`, `.max(n)`, `.positive()`, `.int()`, `.step(n)` |
|
|
473
|
-
| `f.boolean()` | `boolean` | `boolean` | — |
|
|
474
|
-
| `f.date()` | `date` | `string` (ISO) | `.autoNow()`, `.autoNowUpdate()` |
|
|
475
|
-
| `f.datetime()` | `timestamp` | `Date` | `.autoNow()`, `.autoNowUpdate()` |
|
|
476
|
-
| `f.time()` | `time` | `string` | — |
|
|
477
|
-
| `f.select(options[])` | `varchar` | `string` | `.enum(name)` |
|
|
478
|
-
| `f.relation(target)` | `varchar(36)` FK | `string` | See [Relations](#8-relations) |
|
|
479
|
-
| `f.upload(config?)` | `varchar(36)` FK | `string` | `.multiple()` |
|
|
480
|
-
| `f.object(fields)` | `jsonb` | `{...}` | Nested field definitions |
|
|
481
|
-
| `f.json(config?)` | `jsonb` / `json` | `JsonValue` | `{ mode: "jsonb" \| "json" }` |
|
|
482
|
-
| `f.richText()` | `jsonb` | TipTap doc | _Admin plugin only_ |
|
|
483
|
-
| `f.blocks()` | `jsonb` | Block tree | _Admin plugin only_ |
|
|
484
|
-
| `f.from(column, zod?)` | custom | `unknown` | `.type(name)` — escape hatch (PostGIS, etc.). Internal type string is `"custom"` |
|
|
485
|
-
|
|
486
|
-
### Common Field Methods (Available on All Types)
|
|
487
|
-
|
|
488
|
-
```ts
|
|
489
|
-
f.text(255)
|
|
490
|
-
.required() // NOT NULL
|
|
491
|
-
.default("untitled") // default value (or function)
|
|
492
|
-
.label("Post Title") // display label (I18nText)
|
|
493
|
-
.description("The main title") // help text
|
|
494
|
-
.localized() // stored in i18n table, per-locale
|
|
495
|
-
.virtual(sql`...`) // computed column, no DB storage
|
|
496
|
-
.array() // wrap as JSONB array
|
|
497
|
-
.minItems(1)
|
|
498
|
-
.maxItems(10)
|
|
499
|
-
.inputFalse() // exclude from create/update input (read-only)
|
|
500
|
-
.inputOptional() // always optional in input
|
|
501
|
-
.inputTrue() // force into input even if virtual
|
|
502
|
-
.outputFalse() // exclude from output (write-only, e.g. password)
|
|
503
|
-
.hooks({
|
|
504
|
-
// field-level hooks
|
|
505
|
-
beforeChange: (value, ctx) => value.trim(),
|
|
506
|
-
afterRead: (value, ctx) => value,
|
|
507
|
-
validate: (value, ctx) => {
|
|
508
|
-
if (!value) throw new Error("Required");
|
|
509
|
-
},
|
|
510
|
-
})
|
|
511
|
-
.access({
|
|
512
|
-
// field-level access
|
|
513
|
-
read: true,
|
|
514
|
-
create: ({ session }) => !!session,
|
|
515
|
-
update: ({ session }) => session?.user.role === "admin",
|
|
516
|
-
})
|
|
517
|
-
.operators(ops) // override WHERE operator set for queries
|
|
518
|
-
.drizzle((col) => col) // escape hatch: modify Drizzle column
|
|
519
|
-
.zod((schema) => schema) // escape hatch: modify Zod schema
|
|
520
|
-
.fromDb((value) => value) // transform after reading from DB
|
|
521
|
-
.toDb((value) => value) // transform before writing to DB
|
|
522
|
-
.set(key, value) // plugin extension point (e.g. admin, form config)
|
|
523
|
-
.derive(extra); // derive additional runtime state
|
|
524
|
-
```
|
|
525
|
-
|
|
526
|
-
### Custom Field Types
|
|
527
|
-
|
|
528
|
-
Define reusable field types with `fieldType()`:
|
|
529
|
-
|
|
530
|
-
```ts
|
|
531
|
-
import { fieldType } from "questpie";
|
|
532
|
-
|
|
533
|
-
export const colorField = fieldType("color", {
|
|
534
|
-
create: () => ({
|
|
535
|
-
type: "color",
|
|
536
|
-
drizzleType: "varchar",
|
|
537
|
-
drizzleArgs: [7],
|
|
538
|
-
}),
|
|
539
|
-
methods: {
|
|
540
|
-
palette: (field, colors: string[]) => field.set("palette", colors),
|
|
541
|
-
},
|
|
542
|
-
});
|
|
543
|
-
|
|
544
|
-
// Usage: f.color().palette(["#ff0000", "#00ff00"])
|
|
545
|
-
```
|
|
546
|
-
|
|
547
|
-
---
|
|
548
|
-
|
|
549
|
-
## 8. Relations
|
|
550
|
-
|
|
551
|
-
All relationships are expressed via `f.relation(target)` with chain methods:
|
|
552
|
-
|
|
553
|
-
### Relationship Types
|
|
554
|
-
|
|
555
|
-
```ts
|
|
556
|
-
// belongsTo (default) — FK on this table
|
|
557
|
-
f.relation("users");
|
|
558
|
-
// Column: authorId varchar(36) → FK to users.id
|
|
559
|
-
|
|
560
|
-
// hasMany — virtual, FK lives on the target table
|
|
561
|
-
f.relation("comments").hasMany({
|
|
562
|
-
foreignKey: "postId",
|
|
563
|
-
onDelete: "cascade",
|
|
564
|
-
});
|
|
565
|
-
|
|
566
|
-
// manyToMany — virtual, uses a junction table
|
|
567
|
-
f.relation("tags").manyToMany({
|
|
568
|
-
through: "post_tags",
|
|
569
|
-
sourceField: "postId", // optional, inferred
|
|
570
|
-
targetField: "tagId", // optional, inferred
|
|
571
|
-
});
|
|
572
|
-
|
|
573
|
-
// multiple — stores array of IDs as JSONB
|
|
574
|
-
f.relation("assets").multiple();
|
|
575
|
-
// Column: imageIds jsonb (["uuid1", "uuid2", ...])
|
|
576
|
-
|
|
577
|
-
// polymorphic (morphTo) — two columns: type + id
|
|
578
|
-
f.relation({ users: "users", teams: "teams" });
|
|
579
|
-
// Columns: assigneeType varchar, assigneeId varchar
|
|
580
|
-
```
|
|
581
|
-
|
|
582
|
-
### Relation Targets
|
|
583
|
-
|
|
584
|
-
```ts
|
|
585
|
-
f.relation("users"); // string (collection name)
|
|
586
|
-
f.relation(() => users); // lazy reference (avoids circular imports)
|
|
587
|
-
f.relation({ users: "users", teams: "teams" }); // polymorphic map
|
|
588
|
-
```
|
|
589
|
-
|
|
590
|
-
### Additional Config
|
|
591
|
-
|
|
592
|
-
```ts
|
|
593
|
-
f.relation("users")
|
|
594
|
-
.onDelete("cascade" | "set null" | "restrict" | "no action")
|
|
595
|
-
.onUpdate("cascade" | ...)
|
|
596
|
-
.relationName("postAuthor") // disambiguate multiple relations to same target
|
|
597
|
-
```
|
|
598
|
-
|
|
599
|
-
---
|
|
600
|
-
|
|
601
|
-
## 9. Hooks
|
|
602
|
-
|
|
603
|
-
Hooks are lifecycle callbacks that run at specific points during CRUD operations.
|
|
604
|
-
|
|
605
|
-
### Execution Order
|
|
606
|
-
|
|
607
|
-
```
|
|
608
|
-
CREATE: beforeOperation → beforeValidate → beforeChange → [DB INSERT] → afterChange → afterRead
|
|
609
|
-
UPDATE: beforeOperation → beforeValidate → beforeChange → [DB UPDATE] → afterChange → afterRead
|
|
610
|
-
DELETE: beforeOperation → beforeDelete → [DB DELETE] → afterDelete → afterRead
|
|
611
|
-
READ: beforeOperation → beforeRead → [DB SELECT] → afterRead
|
|
612
|
-
```
|
|
613
|
-
|
|
614
|
-
### Collection Hooks
|
|
615
|
-
|
|
616
|
-
```ts
|
|
617
|
-
collection("posts").hooks({
|
|
618
|
-
// Runs before ANY operation — logging, rate limiting
|
|
619
|
-
beforeOperation: async (ctx) => { ... },
|
|
620
|
-
|
|
621
|
-
// Runs before validation on create/update — normalize input
|
|
622
|
-
beforeValidate: async (ctx) => {
|
|
623
|
-
ctx.data.email = ctx.data.email?.toLowerCase();
|
|
624
|
-
return ctx.data;
|
|
625
|
-
},
|
|
626
|
-
|
|
627
|
-
// Runs after validation, before DB write — business logic, derived fields
|
|
628
|
-
beforeChange: async (ctx) => {
|
|
629
|
-
if (ctx.operation === "create") {
|
|
630
|
-
ctx.data.slug = slugify(ctx.data.title);
|
|
631
|
-
}
|
|
632
|
-
return ctx.data;
|
|
633
|
-
},
|
|
634
|
-
|
|
635
|
-
// Runs after create/update — notifications, webhooks, side effects
|
|
636
|
-
afterChange: async (ctx) => {
|
|
637
|
-
// ctx.original is available on update (the record before changes)
|
|
638
|
-
ctx.onAfterCommit(async () => {
|
|
639
|
-
await ctx.email.send("post-updated", { to: ctx.data.authorEmail });
|
|
640
|
-
});
|
|
641
|
-
},
|
|
642
|
-
|
|
643
|
-
// Runs before DB SELECT — modify query
|
|
644
|
-
beforeRead: async (ctx) => { ... },
|
|
645
|
-
|
|
646
|
-
// Runs after any operation that returns data
|
|
647
|
-
afterRead: async (ctx) => {
|
|
648
|
-
ctx.data.displayName = `${ctx.data.firstName} ${ctx.data.lastName}`;
|
|
649
|
-
return ctx.data;
|
|
650
|
-
},
|
|
651
|
-
|
|
652
|
-
// Delete-specific
|
|
653
|
-
beforeDelete: async (ctx) => { ... },
|
|
654
|
-
afterDelete: async (ctx) => { ... },
|
|
655
|
-
|
|
656
|
-
// Workflow transitions
|
|
657
|
-
beforeTransition: async (ctx) => { ... },
|
|
658
|
-
afterTransition: async (ctx) => { ... },
|
|
659
|
-
})
|
|
660
|
-
```
|
|
661
|
-
|
|
662
|
-
### Hook Context
|
|
663
|
-
|
|
664
|
-
Every hook receives `HookContext` which includes:
|
|
665
|
-
|
|
666
|
-
```ts
|
|
667
|
-
{
|
|
668
|
-
// AppContext (always available)
|
|
669
|
-
db, session, collections, globals, queue, email, storage,
|
|
670
|
-
kv, logger, search, realtime, t,
|
|
671
|
-
|
|
672
|
-
// Hook-specific
|
|
673
|
-
data, // the record being processed
|
|
674
|
-
original, // previous version (update only)
|
|
675
|
-
operation, // "create" | "update" | "delete" | "read"
|
|
676
|
-
locale,
|
|
677
|
-
accessMode, // "system" | "user"
|
|
678
|
-
onAfterCommit(callback), // run after DB transaction commits
|
|
679
|
-
|
|
680
|
-
// Batch operations
|
|
681
|
-
isBatch, recordIds, records, count,
|
|
682
|
-
}
|
|
683
|
-
```
|
|
684
|
-
|
|
685
|
-
### Important Rules
|
|
686
|
-
|
|
687
|
-
- **`before*` hooks** — errors abort the operation (throw to reject)
|
|
688
|
-
- **`afterDelete`** (single) and **`afterChange`** (batch update) — errors are caught and logged (non-fatal)
|
|
689
|
-
- **`afterRead`** — errors **propagate** (fatal) — keep `afterRead` hooks safe
|
|
690
|
-
- **`afterChange`** (single create/update) — collection-level errors propagate; use `onAfterCommit` for risky side effects
|
|
691
|
-
- **Hooks can be arrays**: `.hooks({ afterChange: [hook1, hook2] })` — executed sequentially
|
|
692
|
-
- Use **`onAfterCommit`** for side effects (emails, jobs) that should only fire if the DB write succeeds
|
|
693
|
-
- **`deleteMany`** does NOT trigger `afterRead` — it returns `{ success, count }` directly
|
|
694
|
-
|
|
695
|
-
### Global Hooks
|
|
696
|
-
|
|
697
|
-
Apply hooks to **all** collections at once via `config/app.ts`. Available hook names for global hooks: `beforeChange`, `afterChange`, `beforeDelete`, `afterDelete`, `beforeTransition`, `afterTransition`.
|
|
698
|
-
|
|
699
|
-
```ts
|
|
700
|
-
appConfig({
|
|
701
|
-
hooks: {
|
|
702
|
-
collections: [
|
|
703
|
-
{
|
|
704
|
-
hook: "afterChange",
|
|
705
|
-
include: ["posts", "pages"], // optional filter
|
|
706
|
-
handler: async (ctx) => {
|
|
707
|
-
/* audit log */
|
|
708
|
-
},
|
|
709
|
-
},
|
|
710
|
-
],
|
|
711
|
-
},
|
|
712
|
-
});
|
|
713
|
-
```
|
|
714
|
-
|
|
715
|
-
---
|
|
716
|
-
|
|
717
|
-
## 10. Access Control
|
|
718
|
-
|
|
719
|
-
Access control determines **who can do what**. It's evaluated automatically on every API request.
|
|
720
|
-
|
|
721
|
-
### Collection-Level Access
|
|
722
|
-
|
|
723
|
-
```ts
|
|
724
|
-
collection("posts").access({
|
|
725
|
-
read: true, // public
|
|
726
|
-
create: ({ session }) => !!session, // any authenticated user
|
|
727
|
-
update: ({ session, data }) => ..., // function → boolean or filter
|
|
728
|
-
delete: false, // nobody
|
|
729
|
-
transition: ({ session }) => ..., // workflow transitions
|
|
730
|
-
})
|
|
731
|
-
```
|
|
732
|
-
|
|
733
|
-
### Access Rule Types
|
|
734
|
-
|
|
735
|
-
```ts
|
|
736
|
-
true // allow everyone
|
|
737
|
-
false // deny everyone
|
|
738
|
-
undefined // require authenticated session (DEFAULT)
|
|
739
|
-
(ctx) => boolean // function returning allow/deny
|
|
740
|
-
(ctx) => AccessWhere // function returning a WHERE filter
|
|
741
|
-
```
|
|
742
|
-
|
|
743
|
-
**`AccessWhere`** is a SQL-like filter that gets merged into the query automatically:
|
|
744
|
-
|
|
745
|
-
```ts
|
|
746
|
-
// "Users can only read their own posts"
|
|
747
|
-
access: {
|
|
748
|
-
read: ({ session }) => ({
|
|
749
|
-
authorId: session.user.id, // simple equality
|
|
750
|
-
}),
|
|
751
|
-
// Complex filters:
|
|
752
|
-
read: ({ session }) => ({
|
|
753
|
-
OR: [
|
|
754
|
-
{ status: "published" },
|
|
755
|
-
{ authorId: session.user.id },
|
|
756
|
-
],
|
|
757
|
-
}),
|
|
758
|
-
}
|
|
759
|
-
```
|
|
760
|
-
|
|
761
|
-
### Field-Level Access
|
|
762
|
-
|
|
763
|
-
Control individual fields:
|
|
764
|
-
|
|
765
|
-
```ts
|
|
766
|
-
f.text(255).access({
|
|
767
|
-
read: true,
|
|
768
|
-
create: ({ session }) => session?.user.role === "admin",
|
|
769
|
-
update: false, // immutable after creation
|
|
770
|
-
});
|
|
771
|
-
|
|
772
|
-
// Or at collection level:
|
|
773
|
-
collection("users").access({
|
|
774
|
-
fields: {
|
|
775
|
-
email: { update: ({ session }) => session?.user.role === "admin" },
|
|
776
|
-
password: { read: false },
|
|
777
|
-
},
|
|
778
|
-
});
|
|
779
|
-
```
|
|
780
|
-
|
|
781
|
-
### Default Behavior
|
|
782
|
-
|
|
783
|
-
When no access rule is set, the default is **`!!session`** — require an authenticated session. This is secure-by-default.
|
|
784
|
-
|
|
785
|
-
### System Mode
|
|
786
|
-
|
|
787
|
-
Server-side code (hooks, jobs, seeds) runs in **system mode** (`accessMode: "system"`) by default, which **bypasses all access control**. HTTP requests run in **user mode** (`accessMode: "user"`).
|
|
788
|
-
|
|
789
|
-
```ts
|
|
790
|
-
// Force user mode in server code:
|
|
791
|
-
await collections.posts.find({}, { accessMode: "user", session });
|
|
792
|
-
|
|
793
|
-
// Force system mode explicitly (the default for server code):
|
|
794
|
-
await collections.posts.find({}, { accessMode: "system" });
|
|
795
|
-
```
|
|
796
|
-
|
|
797
|
-
---
|
|
798
|
-
|
|
799
|
-
## 11. Routes
|
|
800
|
-
|
|
801
|
-
Custom HTTP routes for APIs that don't fit the collection CRUD pattern.
|
|
802
|
-
|
|
803
|
-
```ts
|
|
804
|
-
import { route } from "questpie";
|
|
805
|
-
|
|
806
|
-
// JSON route with schema validation
|
|
807
|
-
export default route()
|
|
808
|
-
.post()
|
|
809
|
-
.schema(
|
|
810
|
-
z.object({
|
|
811
|
-
name: z.string(),
|
|
812
|
-
email: z.string().email(),
|
|
813
|
-
}),
|
|
814
|
-
)
|
|
815
|
-
.outputSchema(
|
|
816
|
-
z.object({
|
|
817
|
-
success: z.boolean(),
|
|
818
|
-
id: z.string(),
|
|
819
|
-
}),
|
|
820
|
-
)
|
|
821
|
-
.access(({ session }) => !!session)
|
|
822
|
-
.handler(async ({ input, db, collections, session }) => {
|
|
823
|
-
const user = await collections.users.create({ ...input });
|
|
824
|
-
return { success: true, id: user.id };
|
|
825
|
-
});
|
|
826
|
-
```
|
|
827
|
-
|
|
828
|
-
### Route Methods
|
|
829
|
-
|
|
830
|
-
```ts
|
|
831
|
-
route()
|
|
832
|
-
.get() // HTTP method
|
|
833
|
-
.post()
|
|
834
|
-
.put()
|
|
835
|
-
.delete()
|
|
836
|
-
.patch()
|
|
837
|
-
.raw() // raw Request/Response (no JSON parsing)
|
|
838
|
-
.schema(zodSchema) // input validation
|
|
839
|
-
.outputSchema(zodSchema) // response type (for OpenAPI)
|
|
840
|
-
.access(rule) // access control
|
|
841
|
-
.handler(fn); // terminal — returns RouteDefinition
|
|
842
|
-
```
|
|
843
|
-
|
|
844
|
-
### File-Path Routing
|
|
845
|
-
|
|
846
|
-
Route file paths map to URL paths:
|
|
847
|
-
|
|
848
|
-
```
|
|
849
|
-
routes/get-stats.ts → GET /api/get-stats
|
|
850
|
-
routes/webhooks/stripe.ts → POST /api/webhooks/stripe
|
|
851
|
-
routes/[collection].ts → /api/:collection (parameterized)
|
|
852
|
-
routes/[...path].ts → /api/* (wildcard)
|
|
853
|
-
```
|
|
854
|
-
|
|
855
|
-
### Raw Routes
|
|
856
|
-
|
|
857
|
-
For webhooks, file downloads, or custom responses:
|
|
858
|
-
|
|
859
|
-
```ts
|
|
860
|
-
export default route()
|
|
861
|
-
.post()
|
|
862
|
-
.raw()
|
|
863
|
-
.handler(async ({ request }) => {
|
|
864
|
-
const body = await request.text();
|
|
865
|
-
// ... verify webhook signature
|
|
866
|
-
return new Response("OK", { status: 200 });
|
|
867
|
-
});
|
|
868
|
-
```
|
|
869
|
-
|
|
870
|
-
### Built-in Routes (Auto-registered by Core Module)
|
|
871
|
-
|
|
872
|
-
**Collection routes:**
|
|
873
|
-
|
|
874
|
-
| Path | Method | Purpose |
|
|
875
|
-
| ------------------------------ | ------ | --------------------------- |
|
|
876
|
-
| `[collection]` | GET | Find (list with pagination) |
|
|
877
|
-
| `[collection]` | POST | Create |
|
|
878
|
-
| `[collection]` | PATCH | Update many |
|
|
879
|
-
| `[collection]/[id]` | GET | Find one |
|
|
880
|
-
| `[collection]/[id]` | PATCH | Update |
|
|
881
|
-
| `[collection]/[id]` | DELETE | Delete |
|
|
882
|
-
| `[collection]/count` | GET | Count |
|
|
883
|
-
| `[collection]/delete-many` | POST | Delete many |
|
|
884
|
-
| `[collection]/[id]/versions` | GET | List versions |
|
|
885
|
-
| `[collection]/[id]/revert` | POST | Revert to version |
|
|
886
|
-
| `[collection]/[id]/transition` | POST | Workflow transition |
|
|
887
|
-
| `[collection]/[id]/restore` | POST | Restore soft-deleted |
|
|
888
|
-
| `[collection]/[id]/audit` | GET | Audit log |
|
|
889
|
-
| `[collection]/upload` | POST | File upload |
|
|
890
|
-
| `[collection]/files/[...key]` | GET | Serve file |
|
|
891
|
-
| `[collection]/schema` | GET | Introspected schema |
|
|
892
|
-
| `[collection]/meta` | GET | Collection metadata |
|
|
893
|
-
|
|
894
|
-
**Global routes:**
|
|
895
|
-
|
|
896
|
-
| Path | Method | Purpose |
|
|
897
|
-
| --------------------------- | ------ | ------------------- |
|
|
898
|
-
| `globals/[name]` | GET | Get global |
|
|
899
|
-
| `globals/[name]` | PATCH | Update global |
|
|
900
|
-
| `globals/[name]/versions` | GET | List versions |
|
|
901
|
-
| `globals/[name]/revert` | POST | Revert to version |
|
|
902
|
-
| `globals/[name]/transition` | POST | Workflow transition |
|
|
903
|
-
| `globals/[name]/schema` | GET | Introspected schema |
|
|
904
|
-
| `globals/[name]/meta` | GET | Global metadata |
|
|
905
|
-
| `globals/[name]/audit` | GET | Audit log |
|
|
906
|
-
|
|
907
|
-
**System routes:**
|
|
908
|
-
|
|
909
|
-
| Path | Method | Purpose |
|
|
910
|
-
| ----------------------------- | ------ | ------------------- |
|
|
911
|
-
| `auth/[...path]` | \* | Better Auth handler |
|
|
912
|
-
| `search` | POST | Full-text search |
|
|
913
|
-
| `search/reindex/[collection]` | POST | Reindex collection |
|
|
914
|
-
| `realtime` | POST | SSE subscriptions |
|
|
915
|
-
| `storage/files/[...key]` | GET | Legacy file serving |
|
|
916
|
-
| `health` | GET | Health check |
|
|
917
|
-
|
|
918
|
-
---
|
|
919
|
-
|
|
920
|
-
## 12. Services
|
|
921
|
-
|
|
922
|
-
Services are **injectable singletons or request-scoped factories** available in `AppContext`.
|
|
923
|
-
|
|
924
|
-
```ts
|
|
925
|
-
import { service } from "questpie";
|
|
926
|
-
|
|
927
|
-
export const analyticsService = service()
|
|
928
|
-
.lifecycle("singleton") // created once at startup
|
|
929
|
-
.create(({ app }) => {
|
|
930
|
-
return new AnalyticsClient(app.config.analytics);
|
|
931
|
-
})
|
|
932
|
-
.dispose((client) => {
|
|
933
|
-
client.close();
|
|
934
|
-
});
|
|
935
|
-
```
|
|
936
|
-
|
|
937
|
-
### Namespaces
|
|
938
|
-
|
|
939
|
-
```ts
|
|
940
|
-
// Top-level (ctx.analytics)
|
|
941
|
-
service().namespace(null).create(...)
|
|
942
|
-
|
|
943
|
-
// Nested (ctx.services.analytics)
|
|
944
|
-
service().create(...) // default namespace: "services"
|
|
945
|
-
|
|
946
|
-
// Custom namespace (ctx.myNamespace.analytics)
|
|
947
|
-
service().namespace("myNamespace").create(...)
|
|
948
|
-
```
|
|
949
|
-
|
|
950
|
-
### Lifecycle
|
|
951
|
-
|
|
952
|
-
| Lifecycle | When Created | When Disposed |
|
|
953
|
-
| ------------- | ------------------------- | ----------------------- |
|
|
954
|
-
| `"singleton"` | Once at app startup | At `app.destroy()` |
|
|
955
|
-
| `"request"` | Per incoming HTTP request | After request completes |
|
|
956
|
-
|
|
957
|
-
### Using Services in Hooks/Routes
|
|
958
|
-
|
|
959
|
-
Services in the default namespace are on `ctx.services.*`. Top-level services (e.g., `db`, `auth`, `storage`) are directly on `ctx`:
|
|
960
|
-
|
|
961
|
-
```ts
|
|
962
|
-
// In a hook or route handler:
|
|
963
|
-
async ({ db, session, services }) => {
|
|
964
|
-
await services.analytics.track("page_view", { userId: session.user.id });
|
|
965
|
-
};
|
|
966
|
-
```
|
|
967
|
-
|
|
968
|
-
---
|
|
969
|
-
|
|
970
|
-
## 13. Jobs
|
|
971
|
-
|
|
972
|
-
Background jobs for async processing — retries, scheduling, and queuing.
|
|
973
|
-
|
|
974
|
-
```ts
|
|
975
|
-
import { job } from "questpie";
|
|
976
|
-
|
|
977
|
-
export default job({
|
|
978
|
-
name: "sendWelcomeEmail",
|
|
979
|
-
schema: z.object({
|
|
980
|
-
userId: z.string(),
|
|
981
|
-
locale: z.string().optional(),
|
|
982
|
-
}),
|
|
983
|
-
options: {
|
|
984
|
-
retryLimit: 3,
|
|
985
|
-
retryDelay: 5, // seconds (not ms!)
|
|
986
|
-
retryBackoff: true, // exponential
|
|
987
|
-
},
|
|
988
|
-
handler: async ({ payload, email, collections }) => {
|
|
989
|
-
const user = await collections.users.findOne({
|
|
990
|
-
where: { id: payload.userId },
|
|
991
|
-
});
|
|
992
|
-
await email.send("welcome", { to: user.email, data: { name: user.name } });
|
|
993
|
-
},
|
|
994
|
-
});
|
|
995
|
-
```
|
|
996
|
-
|
|
997
|
-
> **Note:** Job handlers receive `payload` (not `input`). Email handlers receive `input`.
|
|
998
|
-
|
|
999
|
-
### Dispatching Jobs
|
|
1000
|
-
|
|
1001
|
-
```ts
|
|
1002
|
-
// In any hook, route, or service:
|
|
1003
|
-
await ctx.queue.sendWelcomeEmail.publish({
|
|
1004
|
-
userId: "abc-123",
|
|
1005
|
-
locale: "en",
|
|
1006
|
-
});
|
|
1007
|
-
```
|
|
1008
|
-
|
|
1009
|
-
The queue client exposes jobs as typed properties: `queue[jobName].publish(payload, options?)`. This gives you full type safety on the payload.
|
|
1010
|
-
|
|
1011
|
-
### Queue Adapters
|
|
1012
|
-
|
|
1013
|
-
| Adapter | Use Case |
|
|
1014
|
-
| ------------------------- | ------------------------------------------------ |
|
|
1015
|
-
| `pgBossAdapter()` | PostgreSQL-based (default, great for most cases) |
|
|
1016
|
-
| `CloudflareQueuesAdapter` | Cloudflare Workers |
|
|
1017
|
-
|
|
1018
|
-
---
|
|
1019
|
-
|
|
1020
|
-
## 14. Email Templates
|
|
1021
|
-
|
|
1022
|
-
Email templates define how emails look and what data they accept.
|
|
1023
|
-
|
|
1024
|
-
```tsx
|
|
1025
|
-
import { email } from "questpie";
|
|
1026
|
-
|
|
1027
|
-
export default email({
|
|
1028
|
-
name: "welcome",
|
|
1029
|
-
schema: z.object({
|
|
1030
|
-
name: z.string(),
|
|
1031
|
-
verifyUrl: z.string().url(),
|
|
1032
|
-
}),
|
|
1033
|
-
handler: ({ input }) => ({
|
|
1034
|
-
subject: `Welcome, ${input.name}!`,
|
|
1035
|
-
html: `
|
|
1036
|
-
<h1>Welcome to our platform</h1>
|
|
1037
|
-
<p>Hi ${input.name}, please verify your email:</p>
|
|
1038
|
-
<a href="${input.verifyUrl}">Verify Email</a>
|
|
1039
|
-
`,
|
|
1040
|
-
text: `Welcome ${input.name}! Verify: ${input.verifyUrl}`,
|
|
1041
|
-
}),
|
|
1042
|
-
});
|
|
1043
|
-
```
|
|
1044
|
-
|
|
1045
|
-
### Sending Emails
|
|
1046
|
-
|
|
1047
|
-
```ts
|
|
1048
|
-
await ctx.email.send("welcome", {
|
|
1049
|
-
to: user.email,
|
|
1050
|
-
data: { name: user.name, verifyUrl: "https://..." },
|
|
1051
|
-
});
|
|
1052
|
-
```
|
|
1053
|
-
|
|
1054
|
-
### Email Adapters
|
|
1055
|
-
|
|
1056
|
-
| Adapter | Description |
|
|
1057
|
-
| ----------------------------- | -------------------------------- |
|
|
1058
|
-
| `ConsoleAdapter` | Logs to console (dev) |
|
|
1059
|
-
| `SmtpAdapter` | SMTP via Nodemailer |
|
|
1060
|
-
| `createEtherealSmtpAdapter()` | Auto-generated test SMTP account |
|
|
1061
|
-
|
|
1062
|
-
---
|
|
1063
|
-
|
|
1064
|
-
## 15. Migrations & Seeds
|
|
1065
|
-
|
|
1066
|
-
### Migrations
|
|
1067
|
-
|
|
1068
|
-
```ts
|
|
1069
|
-
import { migration } from "questpie";
|
|
1070
|
-
|
|
1071
|
-
export default migration({
|
|
1072
|
-
id: "0001_create_categories_table",
|
|
1073
|
-
up: async ({ db }) => {
|
|
1074
|
-
await db.execute(sql`
|
|
1075
|
-
CREATE TABLE categories (
|
|
1076
|
-
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
1077
|
-
name varchar(255) NOT NULL
|
|
1078
|
-
)
|
|
1079
|
-
`);
|
|
1080
|
-
},
|
|
1081
|
-
down: async ({ db }) => {
|
|
1082
|
-
await db.execute(sql`DROP TABLE categories`);
|
|
1083
|
-
},
|
|
1084
|
-
});
|
|
1085
|
-
```
|
|
1086
|
-
|
|
1087
|
-
### Seeds
|
|
1088
|
-
|
|
1089
|
-
```ts
|
|
1090
|
-
import { seed } from "questpie";
|
|
1091
|
-
|
|
1092
|
-
export default seed({
|
|
1093
|
-
id: "seed-default-categories",
|
|
1094
|
-
category: "required", // "required" | "dev" | "test"
|
|
1095
|
-
run: async ({ collections, log }) => {
|
|
1096
|
-
log("Creating default categories...");
|
|
1097
|
-
await collections.categories.create({ name: "Uncategorized" });
|
|
1098
|
-
await collections.categories.create({ name: "News" });
|
|
1099
|
-
},
|
|
1100
|
-
undo: async ({ collections }) => {
|
|
1101
|
-
await collections.categories.delete({
|
|
1102
|
-
where: { name: { in: ["Uncategorized", "News"] } },
|
|
1103
|
-
});
|
|
1104
|
-
},
|
|
1105
|
-
});
|
|
1106
|
-
```
|
|
1107
|
-
|
|
1108
|
-
### CLI Commands
|
|
1109
|
-
|
|
1110
|
-
```bash
|
|
1111
|
-
bun questpie migrate # Run pending migrations
|
|
1112
|
-
bun questpie migrate:generate # Generate migration from schema diff
|
|
1113
|
-
bun questpie migrate:status # Show status
|
|
1114
|
-
bun questpie migrate:down # Rollback
|
|
1115
|
-
bun questpie migrate:reset # Rollback all
|
|
1116
|
-
bun questpie migrate:fresh # Reset + re-run all
|
|
1117
|
-
|
|
1118
|
-
bun questpie seed # Run pending seeds
|
|
1119
|
-
bun questpie seed:status # Show status
|
|
1120
|
-
bun questpie seed:undo # Undo seeds
|
|
1121
|
-
bun questpie seed:reset # Reset tracking
|
|
1122
|
-
|
|
1123
|
-
bun questpie push # Push schema directly (dev only, like drizzle-kit push)
|
|
1124
|
-
```
|
|
1125
|
-
|
|
1126
|
-
### Auto-Migration/Seed
|
|
1127
|
-
|
|
1128
|
-
In `questpie.config.ts`:
|
|
1129
|
-
|
|
1130
|
-
```ts
|
|
1131
|
-
runtimeConfig({
|
|
1132
|
-
autoMigrate: true, // run migrations on app startup
|
|
1133
|
-
autoSeed: true, // run seeds on startup
|
|
1134
|
-
// autoSeed: "required", // or only specific categories ("required" | "dev" | "test")
|
|
1135
|
-
});
|
|
1136
|
-
```
|
|
1137
|
-
|
|
1138
|
-
---
|
|
1139
|
-
|
|
1140
|
-
## 16. Uploads & Storage
|
|
1141
|
-
|
|
1142
|
-
### Upload Collections
|
|
1143
|
-
|
|
1144
|
-
Any collection can become a file storage collection:
|
|
1145
|
-
|
|
1146
|
-
```ts
|
|
1147
|
-
collection("assets")
|
|
1148
|
-
.upload({
|
|
1149
|
-
visibility: "public", // "public" | "private"
|
|
1150
|
-
maxSize: 10_000_000, // 10MB
|
|
1151
|
-
allowedTypes: ["image/*", "application/pdf"],
|
|
1152
|
-
})
|
|
1153
|
-
.fields(({ f }) => ({
|
|
1154
|
-
alt: f.text(255),
|
|
1155
|
-
caption: f.textarea(),
|
|
1156
|
-
}));
|
|
1157
|
-
```
|
|
1158
|
-
|
|
1159
|
-
`.upload()` automatically adds system fields: `key`, `filename`, `mimeType`, `size`, `visibility`, plus HTTP routes for upload and file serving.
|
|
1160
|
-
|
|
1161
|
-
### Upload Field
|
|
1162
|
-
|
|
1163
|
-
Reference files from other collections:
|
|
1164
|
-
|
|
1165
|
-
```ts
|
|
1166
|
-
f.upload(); // single file → FK to "assets"
|
|
1167
|
-
f.upload({ to: "documents" }); // custom upload collection
|
|
1168
|
-
f.upload().multiple(); // multiple files (JSONB array)
|
|
1169
|
-
f.upload({
|
|
1170
|
-
// many-to-many via junction
|
|
1171
|
-
through: "post_images",
|
|
1172
|
-
mimeTypes: ["image/*"],
|
|
1173
|
-
maxSize: 5_000_000,
|
|
1174
|
-
});
|
|
1175
|
-
```
|
|
1176
|
-
|
|
1177
|
-
### Storage Adapters
|
|
1178
|
-
|
|
1179
|
-
```ts
|
|
1180
|
-
// Local filesystem (default)
|
|
1181
|
-
runtimeConfig({
|
|
1182
|
-
storage: { location: "./uploads", basePath: "/api" },
|
|
1183
|
-
});
|
|
1184
|
-
|
|
1185
|
-
// S3-compatible (R2, Minio, etc.)
|
|
1186
|
-
runtimeConfig({
|
|
1187
|
-
storage: { driver: myS3Driver },
|
|
1188
|
-
});
|
|
1189
|
-
|
|
1190
|
-
// Auto-detected from env vars:
|
|
1191
|
-
// QUESTPIE_STORAGE_ENDPOINT, QUESTPIE_STORAGE_BUCKET, QUESTPIE_STORAGE_REGION,
|
|
1192
|
-
// QUESTPIE_STORAGE_ACCESS_KEY, QUESTPIE_STORAGE_SECRET_KEY
|
|
1193
|
-
```
|
|
1194
|
-
|
|
1195
|
-
### Programmatic Upload
|
|
1196
|
-
|
|
1197
|
-
```ts
|
|
1198
|
-
const file = await collections.assets.upload(
|
|
1199
|
-
{ stream: readableStream, filename: "photo.jpg", mimeType: "image/jpeg" },
|
|
1200
|
-
{ accessMode: "system" },
|
|
1201
|
-
{ alt: "A nice photo" }, // additional field data
|
|
1202
|
-
);
|
|
1203
|
-
```
|
|
1204
|
-
|
|
1205
|
-
---
|
|
1206
|
-
|
|
1207
|
-
## 17. Versioning & Workflow
|
|
1208
|
-
|
|
1209
|
-
### Enabling Versioning
|
|
1210
|
-
|
|
1211
|
-
```ts
|
|
1212
|
-
collection("pages").options({
|
|
1213
|
-
versioning: true, // basic versioning (max 50 versions)
|
|
1214
|
-
// or:
|
|
1215
|
-
versioning: {
|
|
1216
|
-
maxVersions: 100,
|
|
1217
|
-
workflow: true, // enables stage transitions
|
|
1218
|
-
},
|
|
1219
|
-
// or with custom stages:
|
|
1220
|
-
versioning: {
|
|
1221
|
-
workflow: {
|
|
1222
|
-
initialStage: "draft",
|
|
1223
|
-
stages: {
|
|
1224
|
-
draft: { transitions: ["review"] },
|
|
1225
|
-
review: { transitions: ["published", "draft"] },
|
|
1226
|
-
published: { transitions: ["draft"] },
|
|
1227
|
-
},
|
|
1228
|
-
},
|
|
1229
|
-
},
|
|
1230
|
-
});
|
|
1231
|
-
```
|
|
1232
|
-
|
|
1233
|
-
### What Versioning Gives You
|
|
1234
|
-
|
|
1235
|
-
- A `{collection}_versions` table stores every change
|
|
1236
|
-
- `findVersions({ id })` → list all versions of a record
|
|
1237
|
-
- `revertToVersion({ id, version: 3 })` → restore a previous version
|
|
1238
|
-
- `transitionStage({ id, stage: "published" })` → move between workflow stages
|
|
1239
|
-
- `beforeTransition` / `afterTransition` hooks
|
|
1240
|
-
|
|
1241
|
-
For publishable pages with workflow enabled, workflow stage is the publication source. Public reads must pass `stage: "published"`. If public client/HTTP access is enabled, anonymous read access should require `input?.stage === "published"` so callers cannot omit `stage` and fetch the working draft. Preview/draft-mode reads may omit `stage` to show the working stage to authorized editors. Do not add duplicate `isPublished` guidance when workflow already controls publishing.
|
|
1242
|
-
|
|
1243
|
-
### API Usage
|
|
1244
|
-
|
|
1245
|
-
```ts
|
|
1246
|
-
// Server-side
|
|
1247
|
-
const versions = await collections.pages.findVersions({ id: "abc" });
|
|
1248
|
-
await collections.pages.revertToVersion({ id: "abc", version: 5 });
|
|
1249
|
-
await collections.pages.transitionStage({ id: "abc", stage: "published" });
|
|
1250
|
-
|
|
1251
|
-
// Client-side
|
|
1252
|
-
const versions = await client.collections.pages.findVersions({ id: "abc" });
|
|
1253
|
-
await client.collections.pages.transitionStage({
|
|
1254
|
-
id: "abc",
|
|
1255
|
-
stage: "published",
|
|
1256
|
-
});
|
|
1257
|
-
```
|
|
1258
|
-
|
|
1259
|
-
---
|
|
1260
|
-
|
|
1261
|
-
## 18. Search
|
|
1262
|
-
|
|
1263
|
-
Full-text search across collections.
|
|
1264
|
-
|
|
1265
|
-
### Configuring Search
|
|
1266
|
-
|
|
1267
|
-
```ts
|
|
1268
|
-
collection("posts").searchable({
|
|
1269
|
-
content: (record) => record.title,
|
|
1270
|
-
// or multiple fields:
|
|
1271
|
-
content: (record) => `${record.title} ${record.excerpt}`,
|
|
1272
|
-
});
|
|
1273
|
-
```
|
|
1274
|
-
|
|
1275
|
-
### Search Adapters
|
|
1276
|
-
|
|
1277
|
-
| Adapter | Description |
|
|
1278
|
-
| ----------------------- | ------------------------------------ |
|
|
1279
|
-
| `PostgresSearchAdapter` | pg_trgm + FTS (default, zero-config) |
|
|
1280
|
-
| `PgVectorSearchAdapter` | Hybrid semantic search with pgvector |
|
|
1281
|
-
|
|
1282
|
-
### API Usage
|
|
1283
|
-
|
|
1284
|
-
```ts
|
|
1285
|
-
// Client-side
|
|
1286
|
-
const results = await client.search.search({
|
|
1287
|
-
query: "typescript guide",
|
|
1288
|
-
collections: ["posts", "pages"],
|
|
1289
|
-
limit: 10,
|
|
1290
|
-
});
|
|
1291
|
-
|
|
1292
|
-
// Server-side
|
|
1293
|
-
const results = await ctx.search.search({
|
|
1294
|
-
query: "typescript guide",
|
|
1295
|
-
collections: ["posts"],
|
|
1296
|
-
});
|
|
1297
|
-
```
|
|
1298
|
-
|
|
1299
|
-
### Reindexing
|
|
1300
|
-
|
|
1301
|
-
```ts
|
|
1302
|
-
// Via API
|
|
1303
|
-
await client.collections.posts.reindex();
|
|
1304
|
-
|
|
1305
|
-
// Via job
|
|
1306
|
-
await ctx.queue.indexRecords.publish({ collection: "posts" });
|
|
1307
|
-
```
|
|
1308
|
-
|
|
1309
|
-
---
|
|
1310
|
-
|
|
1311
|
-
## 19. Realtime
|
|
1312
|
-
|
|
1313
|
-
Server-sent events for live data updates.
|
|
1314
|
-
|
|
1315
|
-
### How It Works
|
|
1316
|
-
|
|
1317
|
-
1. Every create/update/delete writes a change event to `questpie_realtime_log` (outbox pattern)
|
|
1318
|
-
2. An adapter (pg_notify or Redis Streams) notifies connected clients
|
|
1319
|
-
3. Clients subscribe via SSE to specific resources
|
|
1320
|
-
|
|
1321
|
-
### Client-Side Usage
|
|
1322
|
-
|
|
1323
|
-
```ts
|
|
1324
|
-
// With TanStack Query (automatic)
|
|
1325
|
-
const { data } = useQuery(qp.collections.posts.find({}, { realtime: true }));
|
|
1326
|
-
|
|
1327
|
-
// Manual subscription
|
|
1328
|
-
const unsub = client.realtime.subscribe(
|
|
1329
|
-
{ resourceType: "collection", resource: "posts" },
|
|
1330
|
-
(event) => {
|
|
1331
|
-
console.log("Change:", event.operation, event.recordId);
|
|
1332
|
-
},
|
|
1333
|
-
);
|
|
1334
|
-
```
|
|
1335
|
-
|
|
1336
|
-
### Realtime Adapters
|
|
1337
|
-
|
|
1338
|
-
```ts
|
|
1339
|
-
import { pgNotifyAdapter } from "questpie";
|
|
1340
|
-
|
|
1341
|
-
runtimeConfig({
|
|
1342
|
-
realtime: {
|
|
1343
|
-
adapter: pgNotifyAdapter({ connectionString: process.env.DATABASE_URL }),
|
|
1344
|
-
},
|
|
1345
|
-
});
|
|
1346
|
-
```
|
|
1347
|
-
|
|
1348
|
-
| Adapter | Description |
|
|
1349
|
-
| ----------------------- | ---------------------------- |
|
|
1350
|
-
| _None_ (default) | Polling-based (every 2s) |
|
|
1351
|
-
| `pgNotifyAdapter()` | PostgreSQL LISTEN/NOTIFY |
|
|
1352
|
-
| `redisStreamsAdapter()` | Redis Streams consumer group |
|
|
1353
|
-
|
|
1354
|
-
---
|
|
1355
|
-
|
|
1356
|
-
## 20. i18n / Localization
|
|
1357
|
-
|
|
1358
|
-
### Content Localization
|
|
1359
|
-
|
|
1360
|
-
Mark fields as localized — stored per-locale in a separate i18n table:
|
|
1361
|
-
|
|
1362
|
-
```ts
|
|
1363
|
-
f.text(255).localized(); // stored in {collection}_i18n table
|
|
1364
|
-
```
|
|
1365
|
-
|
|
1366
|
-
### Configuring Locales
|
|
1367
|
-
|
|
1368
|
-
```ts
|
|
1369
|
-
// config/app.ts
|
|
1370
|
-
appConfig({
|
|
1371
|
-
locale: {
|
|
1372
|
-
locales: ["en", "sk", "de"],
|
|
1373
|
-
defaultLocale: "en",
|
|
1374
|
-
fallback: { sk: "en", de: "en" },
|
|
1375
|
-
},
|
|
1376
|
-
});
|
|
1377
|
-
```
|
|
1378
|
-
|
|
1379
|
-
### Querying Localized Content
|
|
1380
|
-
|
|
1381
|
-
```ts
|
|
1382
|
-
// Server
|
|
1383
|
-
const post = await collections.posts.findOne({
|
|
1384
|
-
where: { id: "abc" },
|
|
1385
|
-
locale: "sk",
|
|
1386
|
-
localeFallback: true, // fall back to "en" if "sk" not available
|
|
1387
|
-
});
|
|
1388
|
-
|
|
1389
|
-
// Client
|
|
1390
|
-
const post = await client.collections.posts.findOne({
|
|
1391
|
-
where: { id: "abc" },
|
|
1392
|
-
locale: "sk",
|
|
1393
|
-
});
|
|
1394
|
-
```
|
|
1395
|
-
|
|
1396
|
-
### Admin UI Translations
|
|
1397
|
-
|
|
1398
|
-
```ts
|
|
1399
|
-
// config/admin.ts
|
|
1400
|
-
adminConfig({
|
|
1401
|
-
locale: {
|
|
1402
|
-
locales: ["en", "sk"],
|
|
1403
|
-
defaultLocale: "en",
|
|
1404
|
-
},
|
|
1405
|
-
});
|
|
1406
|
-
|
|
1407
|
-
// messages/en.ts — admin UI string translations
|
|
1408
|
-
export default {
|
|
1409
|
-
"admin.posts.title": "Blog Posts",
|
|
1410
|
-
"admin.posts.description": "Manage blog posts",
|
|
1411
|
-
};
|
|
1412
|
-
```
|
|
1413
|
-
|
|
1414
|
-
### Translation Function
|
|
1415
|
-
|
|
1416
|
-
Available in all hooks, routes, and services as `ctx.t(key, params?, locale?)`:
|
|
1417
|
-
|
|
1418
|
-
```ts
|
|
1419
|
-
const msg = ctx.t("billing.invoice_sent", { amount: 100 }, "en");
|
|
1420
|
-
```
|
|
1421
|
-
|
|
1422
|
-
---
|
|
1423
|
-
|
|
1424
|
-
## 21. AppContext — What's Available Everywhere
|
|
1425
|
-
|
|
1426
|
-
Every hook, route handler, job handler, and service receives `AppContext`. This is the **core runtime interface** — learn it once, use it everywhere.
|
|
1427
|
-
|
|
1428
|
-
```ts
|
|
1429
|
-
interface AppContext {
|
|
1430
|
-
app: Questpie; // the app instance
|
|
1431
|
-
db: DrizzleClient; // Drizzle ORM (may be a transaction in hooks)
|
|
1432
|
-
session: { user; session } | null; // current auth session
|
|
1433
|
-
collections: { [name]: CollectionAPI }; // typed CRUD for all collections
|
|
1434
|
-
globals: { [name]: GlobalAPI }; // typed CRUD for all globals
|
|
1435
|
-
queue: QueueClient; // dispatch background jobs
|
|
1436
|
-
email: MailerService; // send emails
|
|
1437
|
-
storage: DriveManager; // file storage operations
|
|
1438
|
-
kv: KVService; // key-value store
|
|
1439
|
-
logger: LoggerService; // structured logging
|
|
1440
|
-
search: SearchService; // full-text search
|
|
1441
|
-
realtime: RealtimeService; // publish realtime events
|
|
1442
|
-
t: (key, params?, locale?) => string; // i18n translator
|
|
1443
|
-
services: Record<string, unknown>; // user-defined services
|
|
1444
|
-
}
|
|
1445
|
-
```
|
|
1446
|
-
|
|
1447
|
-
### Where AppContext Is Available
|
|
1448
|
-
|
|
1449
|
-
| Context | How to Access |
|
|
1450
|
-
| ---------------- | ---------------------------------------------------------------- |
|
|
1451
|
-
| Collection hooks | First argument: `async (ctx) => { ... }` |
|
|
1452
|
-
| Route handlers | Destructure: `async ({ db, session, collections }) => { ... }` |
|
|
1453
|
-
| Job handlers | Destructure: `async ({ payload, queue, email }) => { ... }` |
|
|
1454
|
-
| Email templates | Destructure: `async ({ input, collections }) => { ... }` |
|
|
1455
|
-
| Access rules | Destructure: `({ session, data }) => boolean` |
|
|
1456
|
-
| Seeds | `async ({ collections, log }) => { ... }` |
|
|
1457
|
-
| Services | `create: ({ app }) => ...` (app instance only, not full context) |
|
|
1458
|
-
|
|
1459
|
-
### Getting Context Programmatically
|
|
1460
|
-
|
|
1461
|
-
```ts
|
|
1462
|
-
import { getContext, tryGetContext } from "questpie";
|
|
1463
|
-
|
|
1464
|
-
const ctx = getContext(); // throws if outside a request scope
|
|
1465
|
-
const ctx = tryGetContext(); // returns null if outside scope
|
|
1466
|
-
|
|
1467
|
-
// Create a fresh context manually:
|
|
1468
|
-
const ctx = await app.createContext({
|
|
1469
|
-
session: null,
|
|
1470
|
-
locale: "en",
|
|
1471
|
-
accessMode: "system",
|
|
1472
|
-
});
|
|
1473
|
-
```
|
|
1474
|
-
|
|
1475
|
-
---
|
|
1476
|
-
|
|
1477
|
-
## 22. Server-Side CRUD API
|
|
1478
|
-
|
|
1479
|
-
Access collections and globals programmatically from anywhere in server code.
|
|
1480
|
-
|
|
1481
|
-
### Collections
|
|
1482
|
-
|
|
1483
|
-
```ts
|
|
1484
|
-
// via AppContext (in hooks, routes, jobs)
|
|
1485
|
-
const posts = await ctx.collections.posts.find({
|
|
1486
|
-
where: { status: "published" },
|
|
1487
|
-
orderBy: { createdAt: "desc" },
|
|
1488
|
-
limit: 10,
|
|
1489
|
-
with: { author: true, tags: true },
|
|
1490
|
-
});
|
|
1491
|
-
|
|
1492
|
-
// via app instance (in startup scripts, CLI)
|
|
1493
|
-
const post = await app.collections.posts.findOne({
|
|
1494
|
-
where: { slug: "hello-world" },
|
|
1495
|
-
columns: { title: true, content: true },
|
|
1496
|
-
});
|
|
1497
|
-
```
|
|
1498
|
-
|
|
1499
|
-
### Collection Methods
|
|
1500
|
-
|
|
1501
|
-
```ts
|
|
1502
|
-
// READ
|
|
1503
|
-
.find(options?) → PaginatedResult<T>
|
|
1504
|
-
.findOne(options?) → T | null
|
|
1505
|
-
.count(options?) → number
|
|
1506
|
-
|
|
1507
|
-
// WRITE
|
|
1508
|
-
.create(data) → T
|
|
1509
|
-
.updateById({ id, data }) → T
|
|
1510
|
-
.update({ where, data }) → T[] (batch)
|
|
1511
|
-
.deleteById({ id }) → { success }
|
|
1512
|
-
.delete({ where }) → { success, count } (batch)
|
|
1513
|
-
.restoreById({ id }) → T (soft-delete)
|
|
1514
|
-
|
|
1515
|
-
// VERSIONING
|
|
1516
|
-
.findVersions({ id }) → VersionRecord[]
|
|
1517
|
-
.revertToVersion({ id, version }) → T
|
|
1518
|
-
.transitionStage({ id, stage }) → T
|
|
1519
|
-
|
|
1520
|
-
// UPLOAD (when .upload() is configured)
|
|
1521
|
-
.upload(file, ctx?, additionalData?) → T
|
|
1522
|
-
.uploadMany(files, ctx?, additionalData?) → T[]
|
|
1523
|
-
```
|
|
1524
|
-
|
|
1525
|
-
### FindOptions
|
|
1526
|
-
|
|
1527
|
-
```ts
|
|
1528
|
-
{
|
|
1529
|
-
where: {
|
|
1530
|
-
status: "published", // equality
|
|
1531
|
-
title: { like: "%guide%" }, // LIKE
|
|
1532
|
-
createdAt: { gte: new Date() }, // comparison operators
|
|
1533
|
-
OR: [{ a: 1 }, { b: 2 }], // logical
|
|
1534
|
-
AND: [...],
|
|
1535
|
-
NOT: { ... },
|
|
1536
|
-
},
|
|
1537
|
-
columns: { title: true, content: true }, // select specific fields
|
|
1538
|
-
with: { // include relations
|
|
1539
|
-
author: true,
|
|
1540
|
-
tags: { columns: { name: true } },
|
|
1541
|
-
},
|
|
1542
|
-
orderBy: { createdAt: "desc" },
|
|
1543
|
-
limit: 10,
|
|
1544
|
-
offset: 0,
|
|
1545
|
-
search: "keyword", // full-text ILIKE on title
|
|
1546
|
-
locale: "en",
|
|
1547
|
-
localeFallback: true,
|
|
1548
|
-
includeDeleted: false, // include soft-deleted
|
|
1549
|
-
stage: "published", // versioning stage filter
|
|
1550
|
-
extras: { wordCount: sql`...` }, // computed columns
|
|
1551
|
-
}
|
|
1552
|
-
```
|
|
1553
|
-
|
|
1554
|
-
### PaginatedResult
|
|
1555
|
-
|
|
1556
|
-
```ts
|
|
1557
|
-
{
|
|
1558
|
-
docs: T[],
|
|
1559
|
-
totalDocs: number,
|
|
1560
|
-
limit: number,
|
|
1561
|
-
totalPages: number,
|
|
1562
|
-
page: number,
|
|
1563
|
-
pagingCounter: number,
|
|
1564
|
-
hasPrevPage: boolean,
|
|
1565
|
-
hasNextPage: boolean,
|
|
1566
|
-
prevPage: number | null,
|
|
1567
|
-
nextPage: number | null,
|
|
1568
|
-
}
|
|
1569
|
-
```
|
|
1570
|
-
|
|
1571
|
-
### Globals
|
|
1572
|
-
|
|
1573
|
-
```ts
|
|
1574
|
-
const settings = await ctx.globals.siteSettings.get({
|
|
1575
|
-
with: { logo: true },
|
|
1576
|
-
locale: "en",
|
|
1577
|
-
});
|
|
1578
|
-
|
|
1579
|
-
await ctx.globals.siteSettings.update({
|
|
1580
|
-
siteName: "New Name",
|
|
1581
|
-
description: "Updated description",
|
|
1582
|
-
});
|
|
1583
|
-
```
|
|
1584
|
-
|
|
1585
|
-
### Global Methods
|
|
1586
|
-
|
|
1587
|
-
```ts
|
|
1588
|
-
.get(options?) → T | null
|
|
1589
|
-
.update(data, ctx?, options?) → T
|
|
1590
|
-
.findVersions(options?) → VersionRecord[]
|
|
1591
|
-
.revertToVersion({ version }) → T
|
|
1592
|
-
.transitionStage({ stage }) → T
|
|
1593
|
-
```
|
|
1594
|
-
|
|
1595
|
-
### Request Context
|
|
1596
|
-
|
|
1597
|
-
All CRUD methods accept an optional second argument for context:
|
|
1598
|
-
|
|
1599
|
-
```ts
|
|
1600
|
-
await collections.posts.find(
|
|
1601
|
-
{ where: { status: "published" } },
|
|
1602
|
-
{
|
|
1603
|
-
session: mySession, // override session
|
|
1604
|
-
locale: "sk", // override locale
|
|
1605
|
-
accessMode: "user", // force access checks (default: "system")
|
|
1606
|
-
stage: "published", // version stage filter
|
|
1607
|
-
db: transactionClient, // run inside a transaction
|
|
1608
|
-
},
|
|
1609
|
-
);
|
|
1610
|
-
```
|
|
1611
|
-
|
|
1612
|
-
---
|
|
1613
|
-
|
|
1614
|
-
## 23. Client SDK
|
|
1615
|
-
|
|
1616
|
-
The typed client for calling the QuestPie API from frontend or external code.
|
|
1617
|
-
|
|
1618
|
-
### Setup
|
|
1619
|
-
|
|
1620
|
-
```ts
|
|
1621
|
-
import { createClient } from "questpie/client";
|
|
1622
|
-
import type { App } from "@/questpie/server/app";
|
|
1623
|
-
|
|
1624
|
-
export const client = createClient<App>({
|
|
1625
|
-
baseURL: "http://localhost:3000",
|
|
1626
|
-
basePath: "/api", // matches your catch-all route
|
|
1627
|
-
});
|
|
1628
|
-
```
|
|
1629
|
-
|
|
1630
|
-
### Collections
|
|
1631
|
-
|
|
1632
|
-
```ts
|
|
1633
|
-
// Read
|
|
1634
|
-
const posts = await client.collections.posts.find({
|
|
1635
|
-
where: { status: "published" },
|
|
1636
|
-
limit: 10,
|
|
1637
|
-
with: { author: true },
|
|
1638
|
-
});
|
|
1639
|
-
|
|
1640
|
-
const post = await client.collections.posts.findOne({
|
|
1641
|
-
where: { id: "abc" },
|
|
1642
|
-
});
|
|
1643
|
-
|
|
1644
|
-
const count = await client.collections.posts.count({
|
|
1645
|
-
where: { status: "draft" },
|
|
1646
|
-
});
|
|
1647
|
-
|
|
1648
|
-
// Write
|
|
1649
|
-
const newPost = await client.collections.posts.create({
|
|
1650
|
-
title: "Hello",
|
|
1651
|
-
content: "...",
|
|
1652
|
-
});
|
|
1653
|
-
|
|
1654
|
-
const updated = await client.collections.posts.update({
|
|
1655
|
-
id: "abc",
|
|
1656
|
-
data: { title: "Updated" },
|
|
1657
|
-
});
|
|
1658
|
-
|
|
1659
|
-
await client.collections.posts.delete({ id: "abc" });
|
|
1660
|
-
|
|
1661
|
-
// Upload
|
|
1662
|
-
const asset = await client.collections.assets.upload(fileObject);
|
|
1663
|
-
|
|
1664
|
-
// Versioning
|
|
1665
|
-
const versions = await client.collections.posts.findVersions({ id: "abc" });
|
|
1666
|
-
await client.collections.posts.revertToVersion({ id: "abc", version: 3 });
|
|
1667
|
-
await client.collections.posts.transitionStage({
|
|
1668
|
-
id: "abc",
|
|
1669
|
-
stage: "published",
|
|
1670
|
-
});
|
|
1671
|
-
|
|
1672
|
-
// Schema introspection
|
|
1673
|
-
const schema = await client.collections.posts.schema();
|
|
1674
|
-
```
|
|
1675
|
-
|
|
1676
|
-
### Globals
|
|
1677
|
-
|
|
1678
|
-
```ts
|
|
1679
|
-
const settings = await client.globals.siteSettings.get();
|
|
1680
|
-
await client.globals.siteSettings.update({ siteName: "New Name" });
|
|
1681
|
-
```
|
|
1682
|
-
|
|
1683
|
-
### Custom Routes
|
|
1684
|
-
|
|
1685
|
-
```ts
|
|
1686
|
-
// Route: routes/admin/stats.ts → key "admin/stats"
|
|
1687
|
-
const stats = await client.routes.admin.stats({ period: "week" });
|
|
1688
|
-
```
|
|
1689
|
-
|
|
1690
|
-
### Search
|
|
1691
|
-
|
|
1692
|
-
```ts
|
|
1693
|
-
const results = await client.search.search({
|
|
1694
|
-
query: "typescript",
|
|
1695
|
-
collections: ["posts"],
|
|
1696
|
-
limit: 10,
|
|
1697
|
-
});
|
|
1698
|
-
```
|
|
1699
|
-
|
|
1700
|
-
### Locale
|
|
1701
|
-
|
|
1702
|
-
```ts
|
|
1703
|
-
client.setLocale("sk"); // set for all subsequent requests
|
|
1704
|
-
const locale = client.getLocale();
|
|
1705
|
-
```
|
|
1706
|
-
|
|
1707
|
-
### Error Handling
|
|
1708
|
-
|
|
1709
|
-
```ts
|
|
1710
|
-
import { QuestpieClientError } from "questpie/client";
|
|
1711
|
-
|
|
1712
|
-
try {
|
|
1713
|
-
await client.collections.posts.create({ title: "" });
|
|
1714
|
-
} catch (err) {
|
|
1715
|
-
if (err instanceof QuestpieClientError) {
|
|
1716
|
-
err.status; // HTTP status
|
|
1717
|
-
err.code; // error code
|
|
1718
|
-
err.fieldErrors; // per-field validation errors
|
|
1719
|
-
err.getFieldError("title"); // specific field error
|
|
1720
|
-
err.isCode("VALIDATION_ERROR"); // check error type
|
|
1721
|
-
}
|
|
1722
|
-
}
|
|
1723
|
-
```
|
|
1724
|
-
|
|
1725
|
-
---
|
|
1726
|
-
|
|
1727
|
-
## 24. TanStack Query Integration
|
|
1728
|
-
|
|
1729
|
-
Pre-built query/mutation options for `@tanstack/react-query`.
|
|
1730
|
-
|
|
1731
|
-
### Setup
|
|
1732
|
-
|
|
1733
|
-
```ts
|
|
1734
|
-
import { createQuestpieQueryOptions } from "@questpie/tanstack-query";
|
|
1735
|
-
import { client } from "@/lib/client";
|
|
1736
|
-
import type { App } from "@/questpie/server/app";
|
|
1737
|
-
|
|
1738
|
-
export const qp = createQuestpieQueryOptions<App>(client, {
|
|
1739
|
-
keyPrefix: ["questpie"],
|
|
1740
|
-
});
|
|
1741
|
-
```
|
|
1742
|
-
|
|
1743
|
-
### Queries
|
|
1744
|
-
|
|
1745
|
-
```ts
|
|
1746
|
-
import { useQuery, useMutation } from "@tanstack/react-query";
|
|
1747
|
-
|
|
1748
|
-
// List
|
|
1749
|
-
const { data: posts } = useQuery(
|
|
1750
|
-
qp.collections.posts.find({
|
|
1751
|
-
where: { status: "published" },
|
|
1752
|
-
limit: 10,
|
|
1753
|
-
}),
|
|
1754
|
-
);
|
|
1755
|
-
|
|
1756
|
-
// Single
|
|
1757
|
-
const { data: post } = useQuery(
|
|
1758
|
-
qp.collections.posts.findOne({ where: { id: postId } }),
|
|
1759
|
-
);
|
|
1760
|
-
|
|
1761
|
-
// Count
|
|
1762
|
-
const { data: count } = useQuery(
|
|
1763
|
-
qp.collections.posts.count({ where: { status: "draft" } }),
|
|
1764
|
-
);
|
|
1765
|
-
|
|
1766
|
-
// Global
|
|
1767
|
-
const { data: settings } = useQuery(qp.globals.siteSettings.get());
|
|
1768
|
-
|
|
1769
|
-
// With realtime (auto-refetches on server changes)
|
|
1770
|
-
const { data: posts } = useQuery(
|
|
1771
|
-
qp.collections.posts.find({}, { realtime: true }),
|
|
1772
|
-
);
|
|
1773
|
-
```
|
|
1774
|
-
|
|
1775
|
-
### Mutations
|
|
1776
|
-
|
|
1777
|
-
```ts
|
|
1778
|
-
const createPost = useMutation(qp.collections.posts.create());
|
|
1779
|
-
const updatePost = useMutation(qp.collections.posts.update());
|
|
1780
|
-
const deletePost = useMutation(qp.collections.posts.delete());
|
|
1781
|
-
|
|
1782
|
-
// Usage
|
|
1783
|
-
createPost.mutate({ title: "New Post", content: "..." });
|
|
1784
|
-
updatePost.mutate({ id: "abc", data: { title: "Updated" } });
|
|
1785
|
-
deletePost.mutate({ id: "abc" });
|
|
1786
|
-
```
|
|
1787
|
-
|
|
1788
|
-
### Custom Routes
|
|
1789
|
-
|
|
1790
|
-
```ts
|
|
1791
|
-
// Query (GET routes)
|
|
1792
|
-
const { data } = useQuery(qp.routes.admin.stats.query({ period: "week" }));
|
|
1793
|
-
|
|
1794
|
-
// Mutation (POST/PATCH/DELETE routes)
|
|
1795
|
-
const action = useMutation(qp.routes.webhooks.stripe.mutation());
|
|
1796
|
-
```
|
|
1797
|
-
|
|
1798
|
-
### Query Keys
|
|
1799
|
-
|
|
1800
|
-
Consistent key structure for cache invalidation:
|
|
1801
|
-
|
|
1802
|
-
```ts
|
|
1803
|
-
["questpie", "collections", "posts", "find", locale, stage, options][
|
|
1804
|
-
("questpie", "globals", "siteSettings", "get", locale, stage, options)
|
|
1805
|
-
][("questpie", "routes", "admin", "stats", "query", locale, options)];
|
|
1806
|
-
```
|
|
1807
|
-
|
|
1808
|
-
---
|
|
1809
|
-
|
|
1810
|
-
## 25. Admin Panel
|
|
1811
|
-
|
|
1812
|
-
The admin panel is a **server-driven React SPA**. The server declares what should be shown (via `.admin()`, `.list()`, `.form()`), and the client renders it.
|
|
1813
|
-
|
|
1814
|
-
### Enabling the Admin
|
|
1815
|
-
|
|
1816
|
-
```ts
|
|
1817
|
-
// modules.ts
|
|
1818
|
-
import { adminModule } from "@questpie/admin/server";
|
|
1819
|
-
export default [adminModule] as const;
|
|
1820
|
-
|
|
1821
|
-
// config/admin.ts
|
|
1822
|
-
import { adminConfig } from "#questpie/factories";
|
|
1823
|
-
|
|
1824
|
-
export default adminConfig({
|
|
1825
|
-
branding: {
|
|
1826
|
-
name: "My CMS",
|
|
1827
|
-
},
|
|
1828
|
-
locale: {
|
|
1829
|
-
locales: ["en", "sk"],
|
|
1830
|
-
defaultLocale: "en",
|
|
1831
|
-
},
|
|
1832
|
-
sidebar: ({ s, c }) => ({
|
|
1833
|
-
sections: [
|
|
1834
|
-
s.section({
|
|
1835
|
-
id: "content",
|
|
1836
|
-
title: "Content",
|
|
1837
|
-
icon: c.icon({ name: "ph:article" }),
|
|
1838
|
-
}),
|
|
1839
|
-
s.section({
|
|
1840
|
-
id: "settings",
|
|
1841
|
-
title: "Settings",
|
|
1842
|
-
icon: c.icon({ name: "ph:gear" }),
|
|
1843
|
-
}),
|
|
1844
|
-
],
|
|
1845
|
-
items: [
|
|
1846
|
-
s.item({ sectionId: "content", type: "collection", collection: "posts" }),
|
|
1847
|
-
s.item({ sectionId: "content", type: "collection", collection: "pages" }),
|
|
1848
|
-
s.item({
|
|
1849
|
-
sectionId: "settings",
|
|
1850
|
-
type: "global",
|
|
1851
|
-
global: "site_settings",
|
|
1852
|
-
}),
|
|
1853
|
-
s.item({
|
|
1854
|
-
sectionId: "settings",
|
|
1855
|
-
type: "link",
|
|
1856
|
-
label: "API Docs",
|
|
1857
|
-
href: "/api/docs",
|
|
1858
|
-
external: true,
|
|
1859
|
-
}),
|
|
1860
|
-
s.item({ sectionId: "settings", type: "divider" }),
|
|
1861
|
-
],
|
|
1862
|
-
}),
|
|
1863
|
-
dashboard: ({ d, c, a }) => ({
|
|
1864
|
-
title: "Dashboard",
|
|
1865
|
-
sections: [
|
|
1866
|
-
d.section({ id: "overview", label: "Overview", columns: 4 }),
|
|
1867
|
-
d.section({ id: "recent", label: "Recent Activity", columns: 2 }),
|
|
1868
|
-
],
|
|
1869
|
-
items: [
|
|
1870
|
-
d.stats({
|
|
1871
|
-
sectionId: "overview",
|
|
1872
|
-
id: "total-posts",
|
|
1873
|
-
label: "Total Posts",
|
|
1874
|
-
collection: "posts",
|
|
1875
|
-
span: 1,
|
|
1876
|
-
}),
|
|
1877
|
-
d.recentItems({
|
|
1878
|
-
sectionId: "recent",
|
|
1879
|
-
id: "recent-posts",
|
|
1880
|
-
label: "Recent Posts",
|
|
1881
|
-
collection: "posts",
|
|
1882
|
-
dateField: "createdAt",
|
|
1883
|
-
limit: 5,
|
|
1884
|
-
span: 1,
|
|
1885
|
-
}),
|
|
1886
|
-
],
|
|
1887
|
-
actions: [a.create({ collection: "posts", label: "New Post" })],
|
|
1888
|
-
}),
|
|
1889
|
-
});
|
|
1890
|
-
```
|
|
1891
|
-
|
|
1892
|
-
### AdminState
|
|
1893
|
-
|
|
1894
|
-
The admin client receives a pre-built `AdminState` object containing all registered fields, views, pages, widgets, blocks, components, and translations. This is generated at `.generated/client.ts` by codegen.
|
|
1895
|
-
|
|
1896
|
-
### How the Admin Renders a Collection
|
|
1897
|
-
|
|
1898
|
-
1. Client navigates to `/admin/collections/posts`
|
|
1899
|
-
2. Fetches `GET /api/posts/schema` → `CollectionSchema` (fields, access, options, admin config)
|
|
1900
|
-
3. Looks up the configured **list view** (`collection-table`) from AdminState
|
|
1901
|
-
4. Renders the view component with schema-driven columns, filters, actions
|
|
1902
|
-
5. On row click → navigates to `/admin/collections/posts/{id}`
|
|
1903
|
-
6. Fetches `GET /api/posts/{id}` + schema
|
|
1904
|
-
7. Looks up the configured **form view** (`collection-form`)
|
|
1905
|
-
8. Renders form fields using the field registry (each field type maps to a React component)
|
|
1906
|
-
|
|
1907
|
-
---
|
|
1908
|
-
|
|
1909
|
-
## 26. Admin Builder Extensions
|
|
1910
|
-
|
|
1911
|
-
These methods are added to `CollectionBuilder`, `GlobalBuilder`, and `Field` by the admin plugin's codegen.
|
|
1912
|
-
|
|
1913
|
-
### `.admin()` — Collection/Global Metadata
|
|
1914
|
-
|
|
1915
|
-
```ts
|
|
1916
|
-
collection("posts").admin(({ c }) => ({
|
|
1917
|
-
label: "Blog Posts", // display name
|
|
1918
|
-
description: "Manage blog posts", // subtitle
|
|
1919
|
-
icon: c.icon({ name: "ph:article" }),
|
|
1920
|
-
hidden: false, // hide from sidebar
|
|
1921
|
-
group: "content", // sidebar group key
|
|
1922
|
-
order: 1, // order within group
|
|
1923
|
-
audit: true, // enable audit logging
|
|
1924
|
-
}));
|
|
1925
|
-
```
|
|
1926
|
-
|
|
1927
|
-
### `.list()` — List View Config
|
|
1928
|
-
|
|
1929
|
-
```ts
|
|
1930
|
-
collection("posts").list(({ v, f, a }) =>
|
|
1931
|
-
v.collectionTable({
|
|
1932
|
-
columns: [f.title, f.status, f.author, f.createdAt],
|
|
1933
|
-
defaultSort: { field: f.createdAt, direction: "desc" },
|
|
1934
|
-
searchable: [f.title, f.content],
|
|
1935
|
-
filterable: [f.status, f.author],
|
|
1936
|
-
actions: {
|
|
1937
|
-
header: { primary: [a.create()], secondary: [a.custom("export")] },
|
|
1938
|
-
row: [a.delete(), a.duplicate()],
|
|
1939
|
-
bulk: [a.deleteMany(), a.custom("bulkPublish")],
|
|
1940
|
-
},
|
|
1941
|
-
}),
|
|
1942
|
-
);
|
|
1943
|
-
```
|
|
1944
|
-
|
|
1945
|
-
### `.form()` — Form View Config
|
|
1946
|
-
|
|
1947
|
-
```ts
|
|
1948
|
-
collection("posts").form(({ v, f }) =>
|
|
1949
|
-
v.collectionForm({
|
|
1950
|
-
fields: [
|
|
1951
|
-
f.title,
|
|
1952
|
-
f.slug,
|
|
1953
|
-
{
|
|
1954
|
-
type: "section",
|
|
1955
|
-
label: "Content",
|
|
1956
|
-
layout: "stack",
|
|
1957
|
-
fields: [f.content, f.excerpt],
|
|
1958
|
-
},
|
|
1959
|
-
{
|
|
1960
|
-
type: "tabs",
|
|
1961
|
-
tabs: [
|
|
1962
|
-
{
|
|
1963
|
-
id: "media",
|
|
1964
|
-
label: "Media",
|
|
1965
|
-
icon: { type: "icon", props: { name: "ph:image" } },
|
|
1966
|
-
fields: [f.cover, f.gallery],
|
|
1967
|
-
},
|
|
1968
|
-
{ id: "seo", label: "SEO", fields: [f.metaTitle, f.metaDescription] },
|
|
1969
|
-
],
|
|
1970
|
-
},
|
|
1971
|
-
],
|
|
1972
|
-
sidebar: {
|
|
1973
|
-
position: "right",
|
|
1974
|
-
fields: [f.status, f.author, f.tags, f.publishedAt],
|
|
1975
|
-
},
|
|
1976
|
-
}),
|
|
1977
|
-
);
|
|
1978
|
-
```
|
|
1979
|
-
|
|
1980
|
-
#### Form Layout Primitives
|
|
1981
|
-
|
|
1982
|
-
| Type | Description |
|
|
1983
|
-
| ---------------------------------------------------------------- | ---------------------------------------- |
|
|
1984
|
-
| `string` (e.g. `f.title`) | Bare field — renders with default config |
|
|
1985
|
-
| `{ field, hidden?, readOnly?, disabled?, compute?, className? }` | Field with overrides |
|
|
1986
|
-
| `{ type: "section", label?, layout?, columns?, fields: [...] }` | Visual grouping |
|
|
1987
|
-
| `{ type: "tabs", tabs: [{ id, label, icon?, fields }] }` | Tabbed layout |
|
|
1988
|
-
|
|
1989
|
-
#### Reactive Field Config
|
|
1990
|
-
|
|
1991
|
-
Fields can be dynamically hidden, read-only, disabled, or computed based on other field values:
|
|
1992
|
-
|
|
1993
|
-
```ts
|
|
1994
|
-
{
|
|
1995
|
-
field: f.publishedAt,
|
|
1996
|
-
hidden: ({ data }) => data.status !== "published",
|
|
1997
|
-
readOnly: { deps: [f.status], handler: ({ data }) => data.status === "archived" },
|
|
1998
|
-
compute: {
|
|
1999
|
-
deps: [f.firstName, f.lastName],
|
|
2000
|
-
handler: ({ data }) => `${data.firstName} ${data.lastName}`,
|
|
2001
|
-
debounce: 300,
|
|
2002
|
-
},
|
|
2003
|
-
}
|
|
2004
|
-
```
|
|
2005
|
-
|
|
2006
|
-
### `.preview()` — Live Preview
|
|
2007
|
-
|
|
2008
|
-
```ts
|
|
2009
|
-
collection("posts").preview({
|
|
2010
|
-
enabled: true,
|
|
2011
|
-
url: ({ record, locale }) => `/blog/${record.slug}?locale=${locale}`,
|
|
2012
|
-
position: "right", // "left" | "right" | "bottom"
|
|
2013
|
-
defaultWidth: 50, // percentage
|
|
2014
|
-
});
|
|
2015
|
-
```
|
|
2016
|
-
|
|
2017
|
-
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. Preserve save, autosave, Cmd+S, history, workflow transitions, locks, and actions in the normal form lifecycle.
|
|
2018
|
-
|
|
2019
|
-
Frontend visual editing needs `useCollectionPreview`, `PreviewProvider`, `PreviewField`, and usually `BlockRenderer`; load the `questpie-admin` skill for the full frontend preparation checklist.
|
|
2020
|
-
|
|
2021
|
-
### `.actions()` — Server Actions
|
|
2022
|
-
|
|
2023
|
-
```ts
|
|
2024
|
-
collection("posts").actions(({ a, c, f }) => ({
|
|
2025
|
-
builtin: [a.create(), a.save(), a.delete()],
|
|
2026
|
-
custom: [
|
|
2027
|
-
a.action({
|
|
2028
|
-
id: "publish",
|
|
2029
|
-
label: "Publish",
|
|
2030
|
-
icon: c.icon({ name: "ph:rocket" }),
|
|
2031
|
-
scope: "single", // "single" | "bulk" | "header" | "row"
|
|
2032
|
-
confirmation: {
|
|
2033
|
-
title: "Publish this post?",
|
|
2034
|
-
destructive: false,
|
|
2035
|
-
},
|
|
2036
|
-
handler: async ({ record, collections }) => {
|
|
2037
|
-
await collections.posts.transitionStage({
|
|
2038
|
-
id: record.id,
|
|
2039
|
-
stage: "published",
|
|
2040
|
-
});
|
|
2041
|
-
return { type: "success", toast: { message: "Published!" } };
|
|
2042
|
-
},
|
|
2043
|
-
}),
|
|
2044
|
-
a.bulkAction({
|
|
2045
|
-
id: "bulkArchive",
|
|
2046
|
-
label: "Archive Selected",
|
|
2047
|
-
form: {
|
|
2048
|
-
title: "Archive Posts",
|
|
2049
|
-
fields: {
|
|
2050
|
-
reason: f.textarea().label("Reason").required(),
|
|
2051
|
-
},
|
|
2052
|
-
},
|
|
2053
|
-
handler: async ({ records, formData, collections }) => {
|
|
2054
|
-
for (const record of records) {
|
|
2055
|
-
await collections.posts.updateById({
|
|
2056
|
-
id: record.id,
|
|
2057
|
-
data: { status: "archived", archiveReason: formData.reason },
|
|
2058
|
-
});
|
|
2059
|
-
}
|
|
2060
|
-
return {
|
|
2061
|
-
type: "success",
|
|
2062
|
-
toast: { message: `Archived ${records.length} posts` },
|
|
2063
|
-
};
|
|
2064
|
-
},
|
|
2065
|
-
}),
|
|
2066
|
-
],
|
|
2067
|
-
}));
|
|
2068
|
-
```
|
|
2069
|
-
|
|
2070
|
-
#### Action Result Types
|
|
2071
|
-
|
|
2072
|
-
```ts
|
|
2073
|
-
{ type: "success", toast?: { message, title? }, effects?: { closeModal?, invalidate?, redirect? } }
|
|
2074
|
-
{ type: "error", toast?: { message, title? }, errors?: Record<string, string> }
|
|
2075
|
-
{ type: "redirect", url: string, external?: boolean }
|
|
2076
|
-
{ type: "download", file: { name, content, mimeType } }
|
|
2077
|
-
```
|
|
2078
|
-
|
|
2079
|
-
### Field-Level `.admin()` and `.form()`
|
|
2080
|
-
|
|
2081
|
-
```ts
|
|
2082
|
-
f.text().admin({
|
|
2083
|
-
type: "password", // override the input type
|
|
2084
|
-
autoComplete: "new-password",
|
|
2085
|
-
placeholder: "Enter password",
|
|
2086
|
-
})
|
|
2087
|
-
|
|
2088
|
-
f.object({ ... }).form(({ f }) => ({
|
|
2089
|
-
fields: [
|
|
2090
|
-
{ type: "section", label: "Address", layout: "grid", columns: 2,
|
|
2091
|
-
fields: [f.street, f.city, f.zip, f.country] },
|
|
2092
|
-
],
|
|
2093
|
-
}))
|
|
2094
|
-
```
|
|
2095
|
-
|
|
2096
|
-
---
|
|
2097
|
-
|
|
2098
|
-
## 27. Admin Fields, Views, Widgets, Blocks, Components
|
|
2099
|
-
|
|
2100
|
-
### Fields (Client-Side)
|
|
2101
|
-
|
|
2102
|
-
Each server field type maps to a React component in the admin:
|
|
2103
|
-
|
|
2104
|
-
| Field Type | Component | Cell (in tables) |
|
|
2105
|
-
| -------------- | ------------------- | ---------------- |
|
|
2106
|
-
| `text` | `TextField` | `TextCell` |
|
|
2107
|
-
| `textarea` | `TextareaField` | `TextCell` |
|
|
2108
|
-
| `number` | `NumberField` | primitive |
|
|
2109
|
-
| `email` | `EmailField` | primitive |
|
|
2110
|
-
| `url` | `UrlField` | primitive |
|
|
2111
|
-
| `boolean` | `BooleanField` | primitive |
|
|
2112
|
-
| `date` | `DateField` | primitive |
|
|
2113
|
-
| `datetime` | `DatetimeField` | primitive |
|
|
2114
|
-
| `time` | `TimeField` | primitive |
|
|
2115
|
-
| `select` | `SelectField` | primitive |
|
|
2116
|
-
| `relation` | `RelationField` | `RelationCell` |
|
|
2117
|
-
| `upload` | `UploadField` | `UploadCell` |
|
|
2118
|
-
| `richText` | `RichTextField` | — |
|
|
2119
|
-
| `json` | `JsonField` | — |
|
|
2120
|
-
| `object` | `ObjectField` | `ObjectCell` |
|
|
2121
|
-
| `array` | `ArrayField` | `ArrayCell` |
|
|
2122
|
-
| `blocks` | `BlocksField` | `BlocksCell` |
|
|
2123
|
-
| `assetPreview` | `AssetPreviewField` | — |
|
|
2124
|
-
|
|
2125
|
-
Register custom field types:
|
|
2126
|
-
|
|
2127
|
-
```ts
|
|
2128
|
-
import { field } from "@questpie/admin/client";
|
|
2129
|
-
|
|
2130
|
-
export const colorPicker = field("color", {
|
|
2131
|
-
component: lazy(() => import("./color-picker-field")),
|
|
2132
|
-
cell: lazy(() => import("./color-picker-cell")),
|
|
2133
|
-
});
|
|
2134
|
-
```
|
|
2135
|
-
|
|
2136
|
-
### Views
|
|
2137
|
-
|
|
2138
|
-
| View | Kind | Description |
|
|
2139
|
-
| ------------------ | ------ | ---------------------------------------- |
|
|
2140
|
-
| `collection-table` | `list` | Default table view for collection lists |
|
|
2141
|
-
| `collection-form` | `form` | Default form view for collection editing |
|
|
2142
|
-
| `global-form` | `form` | Default form view for globals |
|
|
2143
|
-
|
|
2144
|
-
Register custom views:
|
|
2145
|
-
|
|
2146
|
-
```ts
|
|
2147
|
-
import { view } from "@questpie/admin/client";
|
|
2148
|
-
|
|
2149
|
-
export const kanbanView = view("kanban-board", {
|
|
2150
|
-
kind: "list",
|
|
2151
|
-
component: lazy(() => import("./kanban-view")),
|
|
2152
|
-
});
|
|
2153
|
-
```
|
|
2154
|
-
|
|
2155
|
-
### Widgets (Dashboard)
|
|
2156
|
-
|
|
2157
|
-
8 built-in widget types (use via `d.stats()`, `d.chart()`, etc. in dashboard config):
|
|
2158
|
-
|
|
2159
|
-
| Widget Type | Dashboard Builder | Description |
|
|
2160
|
-
| -------------- | ----------------------- | ----------------------------------------------- |
|
|
2161
|
-
| `stats` | `d.stats({...})` | Single number with optional trend indicator |
|
|
2162
|
-
| `value` | `d.value({...})` | Custom formatted value with subtitle/footer |
|
|
2163
|
-
| `chart` | `d.chart({...})` | Line, bar, area, or pie chart |
|
|
2164
|
-
| `recentItems` | `d.recentItems({...})` | List of recent records from a collection |
|
|
2165
|
-
| `quickActions` | `d.quickActions({...})` | Grid of action buttons |
|
|
2166
|
-
| `table` | `d.table({...})` | Mini data table |
|
|
2167
|
-
| `timeline` | `d.timeline({...})` | Chronological event list |
|
|
2168
|
-
| `progress` | `d.progress({...})` | Progress bar toward a target |
|
|
2169
|
-
| `custom` | `d.custom({...})` | Fully custom widget component (user-registered) |
|
|
2170
|
-
|
|
2171
|
-
### Blocks (Content Builder)
|
|
2172
|
-
|
|
2173
|
-
Blocks are user-defined content components for the page builder (`f.blocks()` field). Defined on the server, rendered on both admin and frontend.
|
|
2174
|
-
|
|
2175
|
-
```ts
|
|
2176
|
-
// Server: blocks/hero.ts
|
|
2177
|
-
import { block } from "#questpie/factories";
|
|
2178
|
-
|
|
2179
|
-
export const hero = block("hero")
|
|
2180
|
-
.fields(({ f }) => ({
|
|
2181
|
-
heading: f.text(255).required(),
|
|
2182
|
-
subheading: f.textarea(),
|
|
2183
|
-
image: f.upload(),
|
|
2184
|
-
cta: f.object({
|
|
2185
|
-
label: f.text(100),
|
|
2186
|
-
url: f.url(),
|
|
2187
|
-
}),
|
|
2188
|
-
}))
|
|
2189
|
-
.admin(({ c }) => ({
|
|
2190
|
-
label: "Hero Banner",
|
|
2191
|
-
icon: c.icon({ name: "ph:image" }),
|
|
2192
|
-
category: { label: "Sections" },
|
|
2193
|
-
}))
|
|
2194
|
-
.form(({ f }) => ({
|
|
2195
|
-
fields: [f.heading, f.subheading, f.image, f.cta],
|
|
2196
|
-
}))
|
|
2197
|
-
.allowChildren(0)
|
|
2198
|
-
.prefetch(async ({ values, db }) => {
|
|
2199
|
-
// Pre-load data for frontend rendering
|
|
2200
|
-
return { imageUrl: values.image ? await getSignedUrl(values.image) : null };
|
|
2201
|
-
});
|
|
2202
|
-
```
|
|
2203
|
-
|
|
2204
|
-
```tsx
|
|
2205
|
-
// Client: admin/blocks/hero.tsx
|
|
2206
|
-
export default function HeroBlock({ values, data }: BlockRendererProps) {
|
|
2207
|
-
return (
|
|
2208
|
-
<section className="hero">
|
|
2209
|
-
<h1>{values.heading}</h1>
|
|
2210
|
-
<p>{values.subheading}</p>
|
|
2211
|
-
{data?.imageUrl && <img src={data.imageUrl} />}
|
|
2212
|
-
</section>
|
|
2213
|
-
);
|
|
2214
|
-
}
|
|
2215
|
-
```
|
|
2216
|
-
|
|
2217
|
-
Block content is stored as:
|
|
2218
|
-
|
|
2219
|
-
```ts
|
|
2220
|
-
{
|
|
2221
|
-
_tree: [{ id: "abc", type: "hero", children: [] }],
|
|
2222
|
-
_values: { abc: { heading: "Welcome", subheading: "..." } },
|
|
2223
|
-
_data: { abc: { imageUrl: "https://..." } }, // server-prefetched
|
|
2224
|
-
}
|
|
2225
|
-
```
|
|
2226
|
-
|
|
2227
|
-
### Components (Server-Driven)
|
|
2228
|
-
|
|
2229
|
-
Components bridge serializable server config to React rendering. Two built-in:
|
|
2230
|
-
|
|
2231
|
-
| Name | Description |
|
|
2232
|
-
| ------- | --------------------------------- |
|
|
2233
|
-
| `icon` | Renders Iconify icons (fast path) |
|
|
2234
|
-
| `badge` | Renders a styled badge |
|
|
2235
|
-
|
|
2236
|
-
Used everywhere in config via `ComponentReference`:
|
|
2237
|
-
|
|
2238
|
-
```ts
|
|
2239
|
-
// In .admin(), sidebar, dashboard, actions:
|
|
2240
|
-
{ type: "icon", props: { name: "ph:users" } }
|
|
2241
|
-
{ type: "badge", props: { label: "New", variant: "success" } }
|
|
2242
|
-
```
|
|
2243
|
-
|
|
2244
|
-
Or via the `c` proxy in callbacks:
|
|
2245
|
-
|
|
2246
|
-
```ts
|
|
2247
|
-
.admin(({ c }) => ({
|
|
2248
|
-
icon: c.icon({ name: "ph:users" }),
|
|
2249
|
-
}))
|
|
2250
|
-
```
|
|
2251
|
-
|
|
2252
|
-
Register custom components:
|
|
2253
|
-
|
|
2254
|
-
```ts
|
|
2255
|
-
import { component } from "@questpie/admin/client";
|
|
2256
|
-
|
|
2257
|
-
export const statusIndicator = component("statusIndicator", {
|
|
2258
|
-
component: lazy(() => import("./status-indicator")),
|
|
2259
|
-
});
|
|
2260
|
-
```
|
|
2261
|
-
|
|
2262
|
-
---
|
|
2263
|
-
|
|
2264
|
-
## 28. Codegen & the .generated/ Directory
|
|
2265
|
-
|
|
2266
|
-
Codegen is the bridge between your file conventions and the runtime. It runs via `questpie generate` (one-shot) or `questpie dev` (watch mode).
|
|
2267
|
-
|
|
2268
|
-
### What Gets Generated
|
|
2269
|
-
|
|
2270
|
-
| File | Contents |
|
|
2271
|
-
| -------------------------------- | ------------------------------------------------------------------------------------ |
|
|
2272
|
-
| `server/.generated/index.ts` | `createApp()` call, `App` type, `AppCollections`, `AppGlobals`, module augmentations |
|
|
2273
|
-
| `server/.generated/factories.ts` | Typed `collection()`, `global()`, `block()`, config factories with plugin extensions |
|
|
2274
|
-
| `admin/.generated/client.ts` | Admin client config object for `<AdminLayoutProvider>` |
|
|
2275
|
-
|
|
2276
|
-
### When to Regenerate
|
|
2277
|
-
|
|
2278
|
-
- **File added or removed** in a convention directory → regenerate
|
|
2279
|
-
- **File content changed** → no regeneration needed (codegen uses `typeof import(...)` which is stable)
|
|
2280
|
-
- `questpie dev` watches for add/remove only
|
|
2281
|
-
|
|
2282
|
-
### The Process
|
|
2283
|
-
|
|
2284
|
-
```
|
|
2285
|
-
1. Read questpie.config.ts → runtime config
|
|
2286
|
-
2. Read modules.ts → extract codegen plugins from each module
|
|
2287
|
-
3. Build target graph: core plugin + extracted plugins → merged category declarations
|
|
2288
|
-
4. Discover files: scan convention directories
|
|
2289
|
-
5. Generate templates:
|
|
2290
|
-
- index.ts (createApp + type augmentation)
|
|
2291
|
-
- factories.ts (typed builders with extensions)
|
|
2292
|
-
- client.ts (admin config)
|
|
2293
|
-
```
|
|
2294
|
-
|
|
2295
|
-
### For NPM Module Packages
|
|
2296
|
-
|
|
2297
|
-
Module packages use `packageConfig()` instead of `runtimeConfig()`:
|
|
2298
|
-
|
|
2299
|
-
```ts
|
|
2300
|
-
// questpie.config.ts in a module package
|
|
2301
|
-
import { packageConfig } from "questpie/cli";
|
|
2302
|
-
|
|
2303
|
-
export default packageConfig({
|
|
2304
|
-
modulesDir: "src/server/modules",
|
|
2305
|
-
modulePrefix: "questpie",
|
|
2306
|
-
plugins: [adminPlugin()],
|
|
2307
|
-
});
|
|
2308
|
-
```
|
|
2309
|
-
|
|
2310
|
-
This generates `.generated/module.ts` for each module subdirectory (no `createApp`, no runtime config — just a typed module object).
|
|
2311
|
-
|
|
2312
|
-
---
|
|
2313
|
-
|
|
2314
|
-
## 29. Adapters & Infrastructure
|
|
2315
|
-
|
|
2316
|
-
QuestPie uses pluggable adapters for all infrastructure concerns.
|
|
2317
|
-
|
|
2318
|
-
### Database
|
|
2319
|
-
|
|
2320
|
-
Always **PostgreSQL** via **Drizzle ORM**. Two client options:
|
|
2321
|
-
|
|
2322
|
-
- `Bun.SQL` (production — native Bun PostgreSQL driver)
|
|
2323
|
-
- `PGlite` (testing — in-process SQLite-compatible Postgres)
|
|
2324
|
-
|
|
2325
|
-
### HTTP Adapters
|
|
2326
|
-
|
|
2327
|
-
The core provides `createFetchHandler()` which works with any framework that supports the `fetch` API:
|
|
2328
|
-
|
|
2329
|
-
```ts
|
|
2330
|
-
// Standalone (Bun.serve)
|
|
2331
|
-
Bun.serve({ fetch: createFetchHandler(app) });
|
|
2332
|
-
|
|
2333
|
-
// TanStack Start / Vinxi
|
|
2334
|
-
export default createFetchHandler(app, { basePath: "/api" });
|
|
2335
|
-
|
|
2336
|
-
// Hono
|
|
2337
|
-
import { questpieMiddleware } from "@questpie/hono/server";
|
|
2338
|
-
app.use("/api/*", questpieMiddleware(questpieApp));
|
|
2339
|
-
|
|
2340
|
-
// Elysia
|
|
2341
|
-
import { questpieElysia } from "@questpie/elysia/server";
|
|
2342
|
-
app.use(questpieElysia(questpieApp));
|
|
2343
|
-
|
|
2344
|
-
// Next.js
|
|
2345
|
-
import { questpieNextRouteHandlers } from "@questpie/next";
|
|
2346
|
-
export const { GET, POST, PUT, PATCH, DELETE } =
|
|
2347
|
-
questpieNextRouteHandlers(questpieApp);
|
|
2348
|
-
```
|
|
2349
|
-
|
|
2350
|
-
### Storage Adapters
|
|
2351
|
-
|
|
2352
|
-
| Config | Driver |
|
|
2353
|
-
| ----------------------------- | ------------------------------ |
|
|
2354
|
-
| Default | `FSDriver` (local `./uploads`) |
|
|
2355
|
-
| `QUESTPIE_STORAGE_*` env vars | S3-compatible (auto-detected) |
|
|
2356
|
-
| `{ driver: customDriver }` | Any FlyDrive `DriverContract` |
|
|
2357
|
-
|
|
2358
|
-
### Queue Adapters
|
|
2359
|
-
|
|
2360
|
-
| Adapter | Description |
|
|
2361
|
-
| ------------------------- | ------------------------------------ |
|
|
2362
|
-
| `pgBossAdapter()` | PostgreSQL-based job queue (pg-boss) |
|
|
2363
|
-
| `CloudflareQueuesAdapter` | Cloudflare Workers Queues |
|
|
2364
|
-
|
|
2365
|
-
### Search Adapters
|
|
2366
|
-
|
|
2367
|
-
| Adapter | Description |
|
|
2368
|
-
| ----------------------- | --------------------------------- |
|
|
2369
|
-
| `PostgresSearchAdapter` | pg_trgm + full-text search |
|
|
2370
|
-
| `PgVectorSearchAdapter` | Hybrid semantic search (pgvector) |
|
|
2371
|
-
|
|
2372
|
-
### Realtime Adapters
|
|
2373
|
-
|
|
2374
|
-
| Adapter | Description |
|
|
2375
|
-
| ----------------------- | ------------------------ |
|
|
2376
|
-
| Default | Polling (2s interval) |
|
|
2377
|
-
| `pgNotifyAdapter()` | PostgreSQL LISTEN/NOTIFY |
|
|
2378
|
-
| `redisStreamsAdapter()` | Redis Streams |
|
|
2379
|
-
|
|
2380
|
-
### KV Adapters
|
|
2381
|
-
|
|
2382
|
-
| Adapter | Description |
|
|
2383
|
-
| ------------------ | -------------------- |
|
|
2384
|
-
| `MemoryKVAdapter` | In-process (default) |
|
|
2385
|
-
| `IORedisKVAdapter` | Redis-backed |
|
|
2386
|
-
|
|
2387
|
-
### Email Adapters
|
|
2388
|
-
|
|
2389
|
-
| Adapter | Description |
|
|
2390
|
-
| ----------------------------- | --------------------------- |
|
|
2391
|
-
| `ConsoleAdapter` | Logs to console |
|
|
2392
|
-
| `SmtpAdapter` | Nodemailer SMTP |
|
|
2393
|
-
| `createEtherealSmtpAdapter()` | Auto-generated test account |
|
|
2394
|
-
|
|
2395
|
-
### Logger
|
|
2396
|
-
|
|
2397
|
-
| Adapter | Description |
|
|
2398
|
-
| ------- | ----------------------------------- |
|
|
2399
|
-
| Default | Pino-based structured logging |
|
|
2400
|
-
| Custom | Implement `LoggerAdapter` interface |
|
|
2401
|
-
|
|
2402
|
-
---
|
|
2403
|
-
|
|
2404
|
-
## 30. TypeScript Type Augmentation
|
|
2405
|
-
|
|
2406
|
-
QuestPie uses `declare global { namespace Questpie { ... } }` for type extensions. Codegen fills these in automatically.
|
|
2407
|
-
|
|
2408
|
-
### Augmentation Points
|
|
2409
|
-
|
|
2410
|
-
```ts
|
|
2411
|
-
declare global {
|
|
2412
|
-
namespace Questpie {
|
|
2413
|
-
interface AppContext {} // Add custom context keys
|
|
2414
|
-
interface Registry {} // Type catalog (collections, globals, routes, etc.)
|
|
2415
|
-
interface ViewsRegistry {} // Admin view autocomplete
|
|
2416
|
-
interface ComponentsRegistry {} // Admin component autocomplete
|
|
2417
|
-
interface FieldTypesMap {} // Field factory autocomplete (f.*)
|
|
2418
|
-
}
|
|
2419
|
-
}
|
|
2420
|
-
```
|
|
2421
|
-
|
|
2422
|
-
### What Codegen Produces
|
|
2423
|
-
|
|
2424
|
-
The generated `index.ts` augments these interfaces so that:
|
|
2425
|
-
|
|
2426
|
-
- `ctx.collections.posts` is fully typed with the actual field shapes
|
|
2427
|
-
- `ctx.globals.siteSettings` is fully typed
|
|
2428
|
-
- `f.text()`, `f.relation()`, etc. have full autocomplete
|
|
2429
|
-
- `ctx.queue.jobName.publish(payload)` validates payload types
|
|
2430
|
-
- `client.collections.posts.find()` returns typed results
|
|
2431
|
-
|
|
2432
|
-
### Plugin Config Augmentation
|
|
2433
|
-
|
|
2434
|
-
Plugins extend the module config type via `declare module "questpie"`:
|
|
2435
|
-
|
|
2436
|
-
```ts
|
|
2437
|
-
declare module "questpie" {
|
|
2438
|
-
interface AppStateConfig {
|
|
2439
|
-
admin?: AdminConfigInput;
|
|
2440
|
-
openapi?: OpenApiConfig;
|
|
2441
|
-
}
|
|
2442
|
-
}
|
|
2443
|
-
```
|
|
2444
|
-
|
|
2445
|
-
---
|
|
2446
|
-
|
|
2447
|
-
## 31. CLI Reference
|
|
2448
|
-
|
|
2449
|
-
```bash
|
|
2450
|
-
bun questpie <command> [options]
|
|
2451
|
-
```
|
|
2452
|
-
|
|
2453
|
-
| Command | Description |
|
|
2454
|
-
| ------------------------ | --------------------------------------------------- |
|
|
2455
|
-
| `generate` | Run codegen (one-shot) |
|
|
2456
|
-
| `dev` | Watch mode codegen (re-runs on file add/remove) |
|
|
2457
|
-
| `push` | Push schema to DB (dev only, like drizzle-kit push) |
|
|
2458
|
-
| `migrate` / `migrate:up` | Run pending migrations |
|
|
2459
|
-
| `migrate:generate` | Generate migration from schema diff |
|
|
2460
|
-
| `migrate:down` | Rollback migrations |
|
|
2461
|
-
| `migrate:status` | Show migration status |
|
|
2462
|
-
| `migrate:reset` | Rollback all |
|
|
2463
|
-
| `migrate:fresh` | Reset + re-run all migrations |
|
|
2464
|
-
| `seed` | Run pending seeds |
|
|
2465
|
-
| `seed:undo` | Undo executed seeds |
|
|
2466
|
-
| `seed:status` | Show seed status |
|
|
2467
|
-
| `seed:reset` | Reset seed tracking |
|
|
2468
|
-
| `add <type> <name>` | Scaffold new entity file |
|
|
2469
|
-
| `add --list` | Show available entity types |
|
|
2470
|
-
|
|
2471
|
-
Global options: `-c, --config <path>` (default: `questpie.config.ts`)
|
|
2472
|
-
|
|
2473
|
-
### Scaffolding
|
|
2474
|
-
|
|
2475
|
-
```bash
|
|
2476
|
-
bun questpie add collection blog-posts
|
|
2477
|
-
bun questpie add global site-settings
|
|
2478
|
-
bun questpie add route get-stats
|
|
2479
|
-
bun questpie add job send-newsletter
|
|
2480
|
-
bun questpie add service analytics
|
|
2481
|
-
bun questpie add email welcome
|
|
2482
|
-
bun questpie add seed default-data
|
|
2483
|
-
bun questpie add migration add-categories
|
|
2484
|
-
bun questpie add block hero # with admin plugin
|
|
2485
|
-
```
|
|
2486
|
-
|
|
2487
|
-
---
|
|
2488
|
-
|
|
2489
|
-
## 32. How It All Connects — The Big Picture
|
|
2490
|
-
|
|
2491
|
-
### Data Flow: Server Startup
|
|
2492
|
-
|
|
2493
|
-
```
|
|
2494
|
-
questpie.config.ts "Infrastructure: here's my DB, storage, email..."
|
|
2495
|
-
↓
|
|
2496
|
-
modules.ts "Packages: use admin, openapi, audit..."
|
|
2497
|
-
↓
|
|
2498
|
-
codegen Scans collections/, globals/, routes/, etc.
|
|
2499
|
-
↓
|
|
2500
|
-
.generated/index.ts Bundles everything into createApp() call
|
|
2501
|
-
↓
|
|
2502
|
-
createApp()
|
|
2503
|
-
├─ resolveModules() Flatten module tree depth-first
|
|
2504
|
-
├─ mergeModuleIntoState() Merge all contributions (later wins)
|
|
2505
|
-
├─ new Questpie(config) Create the app instance
|
|
2506
|
-
└─ _initServices() Boot: db, auth, storage, queue, email, kv, logger, search, realtime
|
|
2507
|
-
↓
|
|
2508
|
-
createFetchHandler(app) Compile all routes into a trie-based dispatcher
|
|
2509
|
-
↓
|
|
2510
|
-
Ready to serve requests
|
|
2511
|
-
```
|
|
2512
|
-
|
|
2513
|
-
### Data Flow: HTTP Request
|
|
2514
|
-
|
|
2515
|
-
```
|
|
2516
|
-
HTTP Request
|
|
2517
|
-
↓
|
|
2518
|
-
createFetchHandler (trie match)
|
|
2519
|
-
↓
|
|
2520
|
-
Resolve session (Better Auth)
|
|
2521
|
-
↓
|
|
2522
|
-
Resolve locale (header / cookie)
|
|
2523
|
-
↓
|
|
2524
|
-
Build AppContext { db, session, collections, globals, queue, email, ... }
|
|
2525
|
-
↓
|
|
2526
|
-
Route handler / Collection CRUD
|
|
2527
|
-
↓
|
|
2528
|
-
Access control check
|
|
2529
|
-
↓
|
|
2530
|
-
Hooks: beforeOperation → beforeValidate → beforeChange
|
|
2531
|
-
↓
|
|
2532
|
-
DB operation (Drizzle)
|
|
2533
|
-
↓
|
|
2534
|
-
Hooks: afterChange → afterRead
|
|
2535
|
-
↓
|
|
2536
|
-
Realtime event (written to outbox)
|
|
2537
|
-
↓
|
|
2538
|
-
HTTP Response (JSON / SuperJSON)
|
|
2539
|
-
```
|
|
2540
|
-
|
|
2541
|
-
### Data Flow: Admin Panel
|
|
2542
|
-
|
|
2543
|
-
```
|
|
2544
|
-
Browser loads /admin
|
|
2545
|
-
↓
|
|
2546
|
-
AdminLayoutProvider receives pre-built AdminState
|
|
2547
|
-
(fields, views, pages, widgets, blocks, components, translations)
|
|
2548
|
-
↓
|
|
2549
|
-
Sidebar rendered from adminConfig.sidebar
|
|
2550
|
-
↓
|
|
2551
|
-
User clicks "Posts"
|
|
2552
|
-
↓
|
|
2553
|
-
Fetch GET /api/posts/schema → CollectionSchema (server-driven UI config)
|
|
2554
|
-
↓
|
|
2555
|
-
Look up "collection-table" view in AdminState.views
|
|
2556
|
-
↓
|
|
2557
|
-
Render table with schema-driven columns, filters, sort, actions
|
|
2558
|
-
↓
|
|
2559
|
-
User clicks a row
|
|
2560
|
-
↓
|
|
2561
|
-
Fetch GET /api/posts/{id}
|
|
2562
|
-
↓
|
|
2563
|
-
Look up "collection-form" view in AdminState.views
|
|
2564
|
-
↓
|
|
2565
|
-
Render form with schema-driven field layout, sections, tabs, sidebar
|
|
2566
|
-
↓
|
|
2567
|
-
Each field renders via AdminState.fields[fieldType].component
|
|
2568
|
-
↓
|
|
2569
|
-
Validation via buildZodFromIntrospection() (client-side Zod)
|
|
2570
|
-
↓
|
|
2571
|
-
Submit PATCH /api/posts/{id}
|
|
2572
|
-
↓
|
|
2573
|
-
Server hooks + access control + DB write + realtime event
|
|
2574
|
-
```
|
|
2575
|
-
|
|
2576
|
-
### Data Flow: Module Contribution
|
|
2577
|
-
|
|
2578
|
-
```
|
|
2579
|
-
Module author writes:
|
|
2580
|
-
collections/invoices.ts → collection("invoices").fields(...)
|
|
2581
|
-
routes/create-checkout.ts → route().post().handler(...)
|
|
2582
|
-
jobs/retry-payment.ts → job({ name: "retryPayment", ... })
|
|
2583
|
-
↓
|
|
2584
|
-
questpie generate --module → .generated/module.ts
|
|
2585
|
-
↓
|
|
2586
|
-
Published to npm as @my-org/billing-module
|
|
2587
|
-
↓
|
|
2588
|
-
User adds to modules.ts:
|
|
2589
|
-
import { billingModule } from "@my-org/billing-module";
|
|
2590
|
-
export default [adminModule, billingModule] as const;
|
|
2591
|
-
↓
|
|
2592
|
-
questpie generate → merges into .generated/index.ts
|
|
2593
|
-
↓
|
|
2594
|
-
At runtime: billingModule's collections, routes, jobs are part of the app
|
|
2595
|
-
```
|
|
2596
|
-
|
|
2597
|
-
### The Primitive Hierarchy
|
|
2598
|
-
|
|
2599
|
-
```
|
|
2600
|
-
runtimeConfig() Infrastructure (DB, storage, email, queue, ...)
|
|
2601
|
-
↓
|
|
2602
|
-
module() Packaging unit (groups related entities)
|
|
2603
|
-
├── collection() Data table with CRUD, hooks, access, versioning
|
|
2604
|
-
│ ├── f.text() Field definitions (each → DB column + validation + UI)
|
|
2605
|
-
│ ├── f.relation() Relationships between collections
|
|
2606
|
-
│ ├── .hooks() Lifecycle callbacks
|
|
2607
|
-
│ ├── .access() Permission rules
|
|
2608
|
-
│ ├── .admin() Admin panel metadata ← admin plugin
|
|
2609
|
-
│ ├── .list() List view configuration ← admin plugin
|
|
2610
|
-
│ ├── .form() Form view configuration ← admin plugin
|
|
2611
|
-
│ ├── .preview() Live preview ← admin plugin
|
|
2612
|
-
│ └── .actions() Server actions ← admin plugin
|
|
2613
|
-
├── global() Singleton document (settings, config)
|
|
2614
|
-
├── route() Custom HTTP endpoint
|
|
2615
|
-
├── job() Background task
|
|
2616
|
-
├── service() Injectable dependency
|
|
2617
|
-
├── email() Email template
|
|
2618
|
-
├── block() Content builder block ← admin plugin
|
|
2619
|
-
├── migration() DB schema change
|
|
2620
|
-
├── seed() DB seed data
|
|
2621
|
-
├── appConfig() App-level config (locale, access, hooks)
|
|
2622
|
-
├── authConfig() Auth config (Better Auth options)
|
|
2623
|
-
└── adminConfig() Admin config (sidebar, dashboard, branding) ← admin plugin
|
|
2624
|
-
```
|
|
2625
|
-
|
|
2626
|
-
### Environment Variables
|
|
2627
|
-
|
|
2628
|
-
| Variable | Purpose | Default |
|
|
2629
|
-
| ----------------------------- | --------------------- | ----------------------- |
|
|
2630
|
-
| `DATABASE_URL` | PostgreSQL connection | (required) |
|
|
2631
|
-
| `APP_URL` | Application URL | `http://localhost:3000` |
|
|
2632
|
-
| `BETTER_AUTH_SECRET` | Auth signing secret | — |
|
|
2633
|
-
| `QUESTPIE_DB` | Cloud DB override | — |
|
|
2634
|
-
| `QUESTPIE_APP_URL` | Cloud URL override | — |
|
|
2635
|
-
| `QUESTPIE_SECRET` | Cloud secret override | — |
|
|
2636
|
-
| `QUESTPIE_STORAGE_ENDPOINT` | S3 endpoint | — |
|
|
2637
|
-
| `QUESTPIE_STORAGE_BUCKET` | S3 bucket | — |
|
|
2638
|
-
| `QUESTPIE_STORAGE_REGION` | S3 region | — |
|
|
2639
|
-
| `QUESTPIE_STORAGE_ACCESS_KEY` | S3 access key | — |
|
|
2640
|
-
| `QUESTPIE_STORAGE_SECRET_KEY` | S3 secret key | — |
|
|
2641
|
-
|
|
2642
|
-
---
|
|
2643
|
-
|
|
2644
|
-
## Quick Reference: All Primitives
|
|
2645
|
-
|
|
2646
|
-
| Primitive | Factory | Import From | Purpose |
|
|
2647
|
-
| -------------- | ------------------------------------ | -------------------------- | ---------------------------- |
|
|
2648
|
-
| Collection | `collection(name)` | `#questpie/factories` | Data table + CRUD |
|
|
2649
|
-
| Global | `global(name)` | `#questpie/factories` | Singleton document |
|
|
2650
|
-
| Field | `f.text()`, `f.number()`, ... | `.fields()` callback | Column definition |
|
|
2651
|
-
| Field Type | `fieldType(name, config)` | `questpie` | Custom field type |
|
|
2652
|
-
| Route | `route()` | `questpie` | HTTP endpoint |
|
|
2653
|
-
| Service | `service()` | `questpie` | Injectable dependency |
|
|
2654
|
-
| Job | `job({...})` | `questpie` | Background task |
|
|
2655
|
-
| Email | `email({...})` | `questpie` | Email template |
|
|
2656
|
-
| Block | `block(name)` | `#questpie/factories` | Content builder block |
|
|
2657
|
-
| Migration | `migration({...})` | `questpie` | DB schema change |
|
|
2658
|
-
| Seed | `seed({...})` | `questpie` | DB seed data |
|
|
2659
|
-
| Module | `module({...})` | `questpie` | Packaging unit |
|
|
2660
|
-
| Runtime Config | `runtimeConfig({...})` | `questpie` | Infrastructure config |
|
|
2661
|
-
| App Config | `appConfig({...})` | `questpie` | Locale, access, hooks |
|
|
2662
|
-
| Auth Config | `authConfig({...})` | `questpie` | Better Auth options |
|
|
2663
|
-
| Admin Config | `adminConfig({...})` | `#questpie/factories` | Sidebar, dashboard, branding |
|
|
2664
|
-
| View | `view(name, {kind, component})` | `@questpie/admin/client` | Admin view component |
|
|
2665
|
-
| Widget | `widget(name, {component})` | `@questpie/admin/client` | Dashboard widget |
|
|
2666
|
-
| Component | `component(name, {component})` | `@questpie/admin/client` | Server-driven component |
|
|
2667
|
-
| Client | `createClient<App>(config)` | `questpie/client` | Frontend API client |
|
|
2668
|
-
| Query Options | `createQuestpieQueryOptions(client)` | `@questpie/tanstack-query` | TanStack Query integration |
|
|
2669
|
-
|
|
2670
|
-
> **Import rule of thumb:** Only `collection()`, `global()`, `block()`, and `adminConfig()` come from `#questpie/factories` (they need codegen-generated types). Everything else comes from `"questpie"` directly.
|