create-questpie 2.0.3 → 2.0.4
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 +244 -30
- package/package.json +1 -1
- package/skills/questpie/AGENTS.md +299 -98
- package/skills/questpie/SKILL.md +50 -17
- package/skills/questpie/coverage.json +213 -0
- package/skills/questpie/references/auth.md +119 -4
- package/skills/questpie/references/business-logic.md +126 -56
- package/skills/questpie/references/crud-api.md +231 -29
- package/skills/questpie/references/data-modeling.md +22 -6
- package/skills/questpie/references/extend.md +34 -7
- package/skills/questpie/references/field-types.md +14 -2
- package/skills/questpie/references/infrastructure-adapters.md +207 -32
- package/skills/questpie/references/mcp.md +147 -0
- package/skills/questpie/references/multi-tenancy.md +1 -2
- package/skills/questpie/references/production.md +218 -53
- package/skills/questpie/references/quickstart.md +6 -8
- package/skills/questpie/references/rules.md +86 -21
- package/skills/questpie/references/sandbox.md +110 -0
- package/skills/questpie/references/tanstack-query.md +34 -11
- package/skills/questpie/references/type-inference.md +167 -0
- package/skills/questpie/references/workflows.md +155 -0
- package/skills/questpie-admin/AGENTS.md +47 -40
- package/skills/questpie-admin/SKILL.md +46 -39
- package/skills/questpie-admin/references/custom-ui.md +1 -1
- package/templates/tanstack-start/AGENTS.md +15 -8
- package/templates/tanstack-start/CLAUDE.md +12 -5
- package/templates/tanstack-start/README.md +7 -6
- package/templates/tanstack-start/package.json +1 -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/config/auth.ts +1 -1
- package/templates/tanstack-start/src/questpie/server/modules.ts +4 -5
- package/templates/tanstack-start/src/questpie/server/questpie.config.ts +2 -1
- package/templates/tanstack-start/src/routes/api/$.ts +1 -2
- package/templates/tanstack-start/vite.config.ts +2 -2
|
@@ -62,6 +62,24 @@ export default collection("posts")
|
|
|
62
62
|
| `.options({...})` | Timestamps, versioning, soft delete |
|
|
63
63
|
| `.search({...})` | Search indexing |
|
|
64
64
|
| `.searchable(string[])` | Searchable fields |
|
|
65
|
+
| `.merge(other)` | Extend a same-name builder (see below) |
|
|
66
|
+
|
|
67
|
+
### Extending Collections — `.merge()`
|
|
68
|
+
|
|
69
|
+
To extend a collection a module already provides, merge its builder — never redefine the collection from scratch (same-key registration replaces the module's collection wholesale):
|
|
70
|
+
|
|
71
|
+
```ts
|
|
72
|
+
import { starterModule } from "questpie/app";
|
|
73
|
+
import { collection } from "#questpie/factories";
|
|
74
|
+
|
|
75
|
+
export default collection("user")
|
|
76
|
+
.merge(starterModule.collections.user)
|
|
77
|
+
.fields(({ f }) => ({
|
|
78
|
+
internalNotes: f.textarea(),
|
|
79
|
+
}));
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
Fields/options/extension keys combine by key (merged-in side wins), hooks concatenate, and `.fields()` after `.merge()` is cumulative — it never wipes merged fields. The result stays fully typed.
|
|
65
83
|
|
|
66
84
|
### Collection Options
|
|
67
85
|
|
|
@@ -242,8 +260,7 @@ Every field accepts:
|
|
|
242
260
|
### Virtual (Computed) Fields
|
|
243
261
|
|
|
244
262
|
```ts
|
|
245
|
-
import { sql } from "questpie";
|
|
246
|
-
|
|
263
|
+
import { sql } from "questpie/builders";
|
|
247
264
|
displayTitle: f.text().virtual(sql<string>`(
|
|
248
265
|
SELECT COALESCE(name, 'Unknown') || ' - ' ||
|
|
249
266
|
TO_CHAR("scheduledAt", 'YYYY-MM-DD HH24:MI')
|
|
@@ -260,7 +277,7 @@ All relations are defined via `f.relation()` inside `.fields()`.
|
|
|
260
277
|
### Belongs-To (Single)
|
|
261
278
|
|
|
262
279
|
```ts
|
|
263
|
-
author: f.relation("
|
|
280
|
+
author: f.relation("user").required(),
|
|
264
281
|
barber: f.relation("barbers").required().onDelete("cascade"),
|
|
265
282
|
```
|
|
266
283
|
|
|
@@ -319,8 +336,7 @@ const appointments = await collections.appointments.find({
|
|
|
319
336
|
### Locale Configuration
|
|
320
337
|
|
|
321
338
|
```ts title="config/app.ts"
|
|
322
|
-
import { appConfig } from "questpie";
|
|
323
|
-
|
|
339
|
+
import { appConfig } from "questpie/app";
|
|
324
340
|
export default appConfig({
|
|
325
341
|
locale: {
|
|
326
342
|
locales: [
|
|
@@ -450,7 +466,7 @@ collection("posts").relations({ author: belongsTo("users") });
|
|
|
450
466
|
|
|
451
467
|
// CORRECT -- use f.relation() inside .fields()
|
|
452
468
|
collection("posts").fields(({ f }) => ({
|
|
453
|
-
author: f.relation("
|
|
469
|
+
author: f.relation("user"),
|
|
454
470
|
}));
|
|
455
471
|
```
|
|
456
472
|
|
|
@@ -15,7 +15,7 @@ A plugin tells codegen what to discover and what types to generate. Plugins cont
|
|
|
15
15
|
### Plugin Structure
|
|
16
16
|
|
|
17
17
|
```ts
|
|
18
|
-
import type { CodegenPlugin } from "questpie";
|
|
18
|
+
import type { CodegenPlugin } from "questpie/codegen";
|
|
19
19
|
|
|
20
20
|
export function myPlugin(): CodegenPlugin {
|
|
21
21
|
return {
|
|
@@ -76,7 +76,7 @@ export function myPlugin(): CodegenPlugin {
|
|
|
76
76
|
### Register in Config
|
|
77
77
|
|
|
78
78
|
```ts title="questpie.config.ts"
|
|
79
|
-
import { runtimeConfig } from "questpie";
|
|
79
|
+
import { runtimeConfig } from "questpie/app";
|
|
80
80
|
import { myPlugin } from "my-plugin-package";
|
|
81
81
|
|
|
82
82
|
export default runtimeConfig({
|
|
@@ -167,7 +167,9 @@ The admin module contributes a codegen plugin to both `"server"` and `"admin-cli
|
|
|
167
167
|
A module is a reusable package that contributes entities to any QUESTPIE project.
|
|
168
168
|
|
|
169
169
|
```ts
|
|
170
|
-
import { module
|
|
170
|
+
import { module } from "questpie/app";
|
|
171
|
+
import { collection } from "questpie/builders";
|
|
172
|
+
import { job } from "questpie/services";
|
|
171
173
|
import { z } from "zod";
|
|
172
174
|
|
|
173
175
|
const notificationsCollection = collection("notifications")
|
|
@@ -235,10 +237,35 @@ export const notificationsModule = module({
|
|
|
235
237
|
| `seeds` | `Seed[]` | Seed data |
|
|
236
238
|
| `messages` | `Record` | i18n translations |
|
|
237
239
|
|
|
240
|
+
### How Module Contributions Merge
|
|
241
|
+
|
|
242
|
+
When several modules (and the app) contribute the same key, `createApp()` merges them deterministically — later modules win per entry:
|
|
243
|
+
|
|
244
|
+
| Key | Strategy |
|
|
245
|
+
| --- | --- |
|
|
246
|
+
| `collections`, `globals`, `jobs`, `routes`, `fields`, `services` | record spread-merge — same key: later wins |
|
|
247
|
+
| `messages` | deep-merge by locale — same message key: later wins |
|
|
248
|
+
| `migrations`, `seeds` | array concatenation |
|
|
249
|
+
| `config.*` (app, auth, admin, plugin config keys) | per-key strategies; `auth`/`admin` deep-merge; unknown keys: incoming replaces existing |
|
|
250
|
+
| anything else | auto-detect: object+object → spread, array+array → concat, otherwise incoming wins |
|
|
251
|
+
|
|
252
|
+
The merge helpers behind these strategies are exported from `questpie/app` for module authors combining config fragments of their own:
|
|
253
|
+
|
|
254
|
+
```ts
|
|
255
|
+
import { lastWins, mergeConcat, mergeDeepConcat, mergeRecord, type MergeFn } from "questpie/app";
|
|
256
|
+
|
|
257
|
+
mergeRecord(a, b); // { ...a, ...b }
|
|
258
|
+
mergeConcat(a, b); // [...a, ...b]
|
|
259
|
+
mergeDeepConcat(a, b); // spread objects, concat array-valued props
|
|
260
|
+
lastWins(a, b); // b
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
Use them (instead of hand-rolled spreads) when a module exposes its own "combine these contributions" surface — the semantics then match what the framework does for built-in keys.
|
|
264
|
+
|
|
238
265
|
### Using a Module
|
|
239
266
|
|
|
240
267
|
```ts title="modules.ts"
|
|
241
|
-
import { adminModule } from "@questpie/admin/
|
|
268
|
+
import { adminModule } from "@questpie/admin/modules/admin";
|
|
242
269
|
import { notificationsModule } from "my-notifications-package";
|
|
243
270
|
|
|
244
271
|
export default [adminModule, notificationsModule] as const;
|
|
@@ -281,7 +308,7 @@ A custom field defines:
|
|
|
281
308
|
The `Field` class is an immutable builder:
|
|
282
309
|
|
|
283
310
|
```ts
|
|
284
|
-
import { Field } from "questpie";
|
|
311
|
+
import { Field } from "questpie/builders";
|
|
285
312
|
|
|
286
313
|
// Each method returns a new Field with updated type state
|
|
287
314
|
f.text(255).required().label({ en: "Name" }).admin({ placeholder: "..." });
|
|
@@ -312,7 +339,7 @@ QUESTPIE ships with adapters for Hono, Elysia, and Next.js. For other frameworks
|
|
|
312
339
|
### Generic Fetch Handler
|
|
313
340
|
|
|
314
341
|
```ts
|
|
315
|
-
import { createFetchHandler } from "questpie";
|
|
342
|
+
import { createFetchHandler } from "questpie/http";
|
|
316
343
|
import { app } from "#questpie";
|
|
317
344
|
|
|
318
345
|
const handler = createFetchHandler(app, { basePath: "/api" });
|
|
@@ -360,7 +387,7 @@ export const { GET, POST, PATCH, DELETE } = questpieNextRouteHandlers(app, {
|
|
|
360
387
|
|
|
361
388
|
```ts title="src/routes/api/$.ts"
|
|
362
389
|
import { createAPIFileRoute } from "@tanstack/react-start/api";
|
|
363
|
-
import { createFetchHandler } from "questpie";
|
|
390
|
+
import { createFetchHandler } from "questpie/http";
|
|
364
391
|
import { app } from "#questpie";
|
|
365
392
|
|
|
366
393
|
const handler = createFetchHandler(app, { basePath: "/api" });
|
|
@@ -14,6 +14,9 @@ Complete configuration patterns for built-in QUESTPIE field types. Fields use fl
|
|
|
14
14
|
| `.inputOptional()` | Optional in API input but required in DB |
|
|
15
15
|
| `.admin(config)` | Admin UI rendering hints |
|
|
16
16
|
| `.virtual(sql)` | SQL expression for computed read-only field |
|
|
17
|
+
| `.zod(fn)` | Extend/replace Zod schema (output narrows value type) |
|
|
18
|
+
| `.drizzle(fn)` | Raw Drizzle column builder — constraints/SQL defaults land in DDL; `$type` narrows value type |
|
|
19
|
+
| `.$type<T>()` | Explicitly set TS value type (type-level; mainly json) |
|
|
17
20
|
|
|
18
21
|
## `f.text(options?)`
|
|
19
22
|
|
|
@@ -206,7 +209,7 @@ Reference to another collection.
|
|
|
206
209
|
Belongs-to (single):
|
|
207
210
|
|
|
208
211
|
```ts
|
|
209
|
-
author: f.relation("
|
|
212
|
+
author: f.relation("user").required(),
|
|
210
213
|
category: f.relation("categories").onDelete("set null"),
|
|
211
214
|
```
|
|
212
215
|
|
|
@@ -355,13 +358,22 @@ pageContent: f.blocks(),
|
|
|
355
358
|
|
|
356
359
|
## `f.json(options?)`
|
|
357
360
|
|
|
358
|
-
Raw JSON data. No schema validation
|
|
361
|
+
Raw JSON data. No schema validation by default; value types as loose `JsonValue`.
|
|
359
362
|
|
|
360
363
|
```ts
|
|
361
364
|
metadata: f.json(),
|
|
362
365
|
rawConfig: f.json().label("Configuration"),
|
|
363
366
|
```
|
|
364
367
|
|
|
368
|
+
Type it explicitly with `.$type<T>()` (type only) or `.zod()` (type + runtime validation) — the type flows into CRUD select/insert types:
|
|
369
|
+
|
|
370
|
+
```ts
|
|
371
|
+
type Layout = { rows: { id: string; span: number }[] };
|
|
372
|
+
|
|
373
|
+
layout: f.json().$type<Layout>(),
|
|
374
|
+
settings: f.json().zod(() => z.object({ theme: z.enum(["light", "dark"]) })),
|
|
375
|
+
```
|
|
376
|
+
|
|
365
377
|
## Admin Meta Options
|
|
366
378
|
|
|
367
379
|
The `meta.admin` object controls field rendering in the admin panel:
|
|
@@ -7,8 +7,7 @@ All adapter configurations for QUESTPIE production infrastructure.
|
|
|
7
7
|
PostgreSQL with Drizzle ORM. Configured in `questpie.config.ts`:
|
|
8
8
|
|
|
9
9
|
```ts
|
|
10
|
-
import { runtimeConfig } from "questpie";
|
|
11
|
-
|
|
10
|
+
import { runtimeConfig } from "questpie/app";
|
|
12
11
|
export default runtimeConfig({
|
|
13
12
|
db: {
|
|
14
13
|
url: process.env.DATABASE_URL || "postgres://localhost/myapp",
|
|
@@ -58,7 +57,7 @@ collection("posts")
|
|
|
58
57
|
|
|
59
58
|
## Storage
|
|
60
59
|
|
|
61
|
-
File storage via [
|
|
60
|
+
File storage via [Files SDK](https://files-sdk.dev/).
|
|
62
61
|
|
|
63
62
|
### Local (Development)
|
|
64
63
|
|
|
@@ -74,19 +73,42 @@ export default runtimeConfig({
|
|
|
74
73
|
|
|
75
74
|
### S3-Compatible (Production)
|
|
76
75
|
|
|
77
|
-
Works with AWS S3, MinIO, DigitalOcean Spaces,
|
|
76
|
+
Works with AWS S3, MinIO, DigitalOcean Spaces, and other S3-compatible
|
|
77
|
+
providers that do not have a dedicated Files SDK adapter:
|
|
78
78
|
|
|
79
79
|
```ts
|
|
80
|
-
import {
|
|
80
|
+
import { s3 } from "files-sdk/s3";
|
|
81
81
|
|
|
82
82
|
export default runtimeConfig({
|
|
83
83
|
storage: {
|
|
84
84
|
basePath: "/api",
|
|
85
|
-
|
|
85
|
+
adapter: s3({
|
|
86
86
|
bucket: process.env.S3_BUCKET,
|
|
87
87
|
region: process.env.S3_REGION,
|
|
88
|
-
|
|
89
|
-
|
|
88
|
+
credentials: {
|
|
89
|
+
accessKeyId: process.env.S3_ACCESS_KEY!,
|
|
90
|
+
secretAccessKey: process.env.S3_SECRET_KEY!,
|
|
91
|
+
},
|
|
92
|
+
}),
|
|
93
|
+
},
|
|
94
|
+
});
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
### Cloudflare R2 (Production)
|
|
98
|
+
|
|
99
|
+
Use Files SDK's dedicated R2 adapter:
|
|
100
|
+
|
|
101
|
+
```ts
|
|
102
|
+
import { r2 } from "files-sdk/r2";
|
|
103
|
+
|
|
104
|
+
export default runtimeConfig({
|
|
105
|
+
storage: {
|
|
106
|
+
basePath: "/api",
|
|
107
|
+
adapter: r2({
|
|
108
|
+
bucket: process.env.R2_BUCKET!,
|
|
109
|
+
accountId: process.env.R2_ACCOUNT_ID!,
|
|
110
|
+
accessKeyId: process.env.R2_ACCESS_KEY_ID!,
|
|
111
|
+
secretAccessKey: process.env.R2_SECRET_ACCESS_KEY!,
|
|
90
112
|
}),
|
|
91
113
|
},
|
|
92
114
|
});
|
|
@@ -114,14 +136,41 @@ const assets = await client.collections.assets.uploadMany(files, {
|
|
|
114
136
|
});
|
|
115
137
|
```
|
|
116
138
|
|
|
139
|
+
Failed uploads reject with `UploadError` (from `questpie/client`) carrying the HTTP status and server message.
|
|
140
|
+
|
|
141
|
+
### Signed URLs (Private Files)
|
|
142
|
+
|
|
143
|
+
Upload rows carry a `url` populated automatically on read: `visibility: "public"` files get a plain collection-scoped URL (`{basePath}/{collection}/files/{key}`); `"private"` files get an HMAC-signed token appended (`?token=...`), signed with `app.config.secret` and expiring after `storage.signedUrlExpiration` (default `3600` seconds):
|
|
144
|
+
|
|
145
|
+
```ts
|
|
146
|
+
export default runtimeConfig({
|
|
147
|
+
storage: {
|
|
148
|
+
basePath: "/api",
|
|
149
|
+
signedUrlExpiration: 900, // 15 minutes
|
|
150
|
+
},
|
|
151
|
+
});
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
Serving stays collection-scoped — the file route verifies the token **and** the collection's `serve` access rule. To mint URLs manually (custom emails, server-rendered pages):
|
|
155
|
+
|
|
156
|
+
```ts
|
|
157
|
+
import { buildStorageFileUrl, generateSignedUrlToken } from "questpie/storage";
|
|
158
|
+
|
|
159
|
+
const token = await generateSignedUrlToken(asset.key, app.config.secret!, 900, "assets");
|
|
160
|
+
const url = buildStorageFileUrl(app.config.app.url, "/api", "assets", asset.key, token);
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
(`verifySignedUrlToken` is the verification half the serve route runs — you rarely call it yourself.)
|
|
164
|
+
|
|
117
165
|
## Queue
|
|
118
166
|
|
|
119
|
-
Background jobs via [pg-boss](https://github.com/timgit/pg-boss)
|
|
167
|
+
Background jobs via [pg-boss](https://github.com/timgit/pg-boss), BullMQ, or a custom queue adapter.
|
|
120
168
|
|
|
121
169
|
### Configuration
|
|
122
170
|
|
|
123
171
|
```ts
|
|
124
|
-
import {
|
|
172
|
+
import { runtimeConfig } from "questpie/app";
|
|
173
|
+
import { pgBossAdapter } from "questpie/adapters/pg-boss";
|
|
125
174
|
|
|
126
175
|
export default runtimeConfig({
|
|
127
176
|
queue: {
|
|
@@ -132,6 +181,24 @@ export default runtimeConfig({
|
|
|
132
181
|
});
|
|
133
182
|
```
|
|
134
183
|
|
|
184
|
+
### BullMQ
|
|
185
|
+
|
|
186
|
+
```ts
|
|
187
|
+
import { runtimeConfig } from "questpie/app";
|
|
188
|
+
import { bullMQAdapter } from "questpie/adapters/bullmq";
|
|
189
|
+
|
|
190
|
+
export default runtimeConfig({
|
|
191
|
+
queue: {
|
|
192
|
+
adapter: bullMQAdapter({
|
|
193
|
+
connection: { url: process.env.REDIS_URL! },
|
|
194
|
+
queuePrefix: "my-app",
|
|
195
|
+
}),
|
|
196
|
+
},
|
|
197
|
+
});
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
The built-in BullMQ adapter targets open-source BullMQ and does not expose per-group FIFO behavior. Use a native grouped queue adapter if the workload needs one active job per group with cross-group parallelism.
|
|
201
|
+
|
|
135
202
|
### Publishing Jobs
|
|
136
203
|
|
|
137
204
|
The `queue` context object is fully typed:
|
|
@@ -154,7 +221,8 @@ SSE-based live updates.
|
|
|
154
221
|
Uses PostgreSQL `LISTEN/NOTIFY`. Best for single-server deployments:
|
|
155
222
|
|
|
156
223
|
```ts
|
|
157
|
-
import {
|
|
224
|
+
import { runtimeConfig } from "questpie/app";
|
|
225
|
+
import { pgNotifyAdapter } from "questpie/adapters/pg-notify";
|
|
158
226
|
|
|
159
227
|
export default runtimeConfig({
|
|
160
228
|
realtime: {
|
|
@@ -170,7 +238,8 @@ export default runtimeConfig({
|
|
|
170
238
|
Required for horizontal scaling across multiple server instances:
|
|
171
239
|
|
|
172
240
|
```ts
|
|
173
|
-
import {
|
|
241
|
+
import { runtimeConfig } from "questpie/app";
|
|
242
|
+
import { redisStreamsAdapter } from "questpie/adapters/redis-streams";
|
|
174
243
|
|
|
175
244
|
export default runtimeConfig({
|
|
176
245
|
realtime: {
|
|
@@ -188,6 +257,48 @@ export default runtimeConfig({
|
|
|
188
257
|
| `pgNotifyAdapter` | Single server, development, simple deployments |
|
|
189
258
|
| `redisStreamsAdapter` | Multiple servers, horizontal scaling, high throughput |
|
|
190
259
|
|
|
260
|
+
## Search
|
|
261
|
+
|
|
262
|
+
Full-text search via PostgreSQL (no extra service). The adapter goes directly on the `search` key:
|
|
263
|
+
|
|
264
|
+
```ts
|
|
265
|
+
import { runtimeConfig } from "questpie/app";
|
|
266
|
+
import { createPostgresSearchAdapter } from "questpie/adapters/postgres-search";
|
|
267
|
+
|
|
268
|
+
export default runtimeConfig({
|
|
269
|
+
search: createPostgresSearchAdapter(), // pg_trgm + tsvector FTS
|
|
270
|
+
});
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
### Semantic Search (pgvector + Embeddings)
|
|
274
|
+
|
|
275
|
+
`createPgVectorSearchAdapter` wraps the Postgres adapter and adds an `embedding` vector column + cosine-distance search. It needs the `pgvector` extension (`CREATE EXTENSION "vector";` — the adapter ships its own migrations) and an embedding provider:
|
|
276
|
+
|
|
277
|
+
```ts
|
|
278
|
+
import { runtimeConfig } from "questpie/app";
|
|
279
|
+
import {
|
|
280
|
+
createOpenAIEmbeddingProvider,
|
|
281
|
+
createPgVectorSearchAdapter,
|
|
282
|
+
} from "questpie/adapters/pgvector-search";
|
|
283
|
+
|
|
284
|
+
export default runtimeConfig({
|
|
285
|
+
search: createPgVectorSearchAdapter({
|
|
286
|
+
embeddingProvider: createOpenAIEmbeddingProvider({
|
|
287
|
+
apiKey: process.env.OPENAI_API_KEY!,
|
|
288
|
+
model: "text-embedding-3-small",
|
|
289
|
+
}),
|
|
290
|
+
// Hybrid scoring weights (defaults shown)
|
|
291
|
+
lexicalWeight: 0.4,
|
|
292
|
+
semanticWeight: 0.6,
|
|
293
|
+
indexType: "ivfflat", // or "hnsw"
|
|
294
|
+
}),
|
|
295
|
+
});
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
Search modes: `lexical` (FTS + trigram), `semantic` (pure vector similarity), `hybrid` (weighted combination). Embeddings are generated on `index()`; if generation fails, the row still indexes for lexical search.
|
|
299
|
+
|
|
300
|
+
For non-OpenAI providers, `createCustomEmbeddingProvider({ name, model, dimensions, generate })` wraps any embedding function.
|
|
301
|
+
|
|
191
302
|
## Email
|
|
192
303
|
|
|
193
304
|
Transactional email with typed templates.
|
|
@@ -195,7 +306,8 @@ Transactional email with typed templates.
|
|
|
195
306
|
### SMTP (Production)
|
|
196
307
|
|
|
197
308
|
```ts
|
|
198
|
-
import {
|
|
309
|
+
import { runtimeConfig } from "questpie/app";
|
|
310
|
+
import { SmtpAdapter } from "questpie/adapters/smtp";
|
|
199
311
|
|
|
200
312
|
export default runtimeConfig({
|
|
201
313
|
email: {
|
|
@@ -215,7 +327,8 @@ export default runtimeConfig({
|
|
|
215
327
|
Logs emails to console instead of sending:
|
|
216
328
|
|
|
217
329
|
```ts
|
|
218
|
-
import {
|
|
330
|
+
import { runtimeConfig } from "questpie/app";
|
|
331
|
+
import { ConsoleAdapter } from "questpie/adapters/console";
|
|
219
332
|
|
|
220
333
|
export default runtimeConfig({
|
|
221
334
|
email: {
|
|
@@ -224,6 +337,46 @@ export default runtimeConfig({
|
|
|
224
337
|
});
|
|
225
338
|
```
|
|
226
339
|
|
|
340
|
+
### Resend (HTTP API)
|
|
341
|
+
|
|
342
|
+
For [Resend](https://resend.com) and Resend-compatible providers — no SMTP credentials needed:
|
|
343
|
+
|
|
344
|
+
```ts
|
|
345
|
+
import { runtimeConfig } from "questpie/app";
|
|
346
|
+
import { resendAdapter } from "questpie/adapters/resend";
|
|
347
|
+
|
|
348
|
+
export default runtimeConfig({
|
|
349
|
+
email: {
|
|
350
|
+
adapter: resendAdapter({
|
|
351
|
+
apiKey: process.env.RESEND_API_KEY!,
|
|
352
|
+
// baseUrl: "https://api.resend.com", // override for compatible providers
|
|
353
|
+
}),
|
|
354
|
+
},
|
|
355
|
+
});
|
|
356
|
+
```
|
|
357
|
+
|
|
358
|
+
### Plunk (HTTP API)
|
|
359
|
+
|
|
360
|
+
For [Plunk](https://www.useplunk.com) transactional email (also self-hosted):
|
|
361
|
+
|
|
362
|
+
```ts
|
|
363
|
+
import { runtimeConfig } from "questpie/app";
|
|
364
|
+
import { plunkAdapter } from "questpie/adapters/plunk";
|
|
365
|
+
|
|
366
|
+
export default runtimeConfig({
|
|
367
|
+
email: {
|
|
368
|
+
adapter: plunkAdapter({
|
|
369
|
+
apiKey: process.env.PLUNK_API_KEY!, // secret key — public keys only track events
|
|
370
|
+
// baseUrl: "https://next-api.useplunk.com", // override for self-hosted
|
|
371
|
+
}),
|
|
372
|
+
},
|
|
373
|
+
});
|
|
374
|
+
```
|
|
375
|
+
|
|
376
|
+
### Custom Mail Adapter
|
|
377
|
+
|
|
378
|
+
For any other provider, extend the `MailAdapter` base class from `questpie/mailer` (implement `send(options)`) and pass an instance as `email.adapter`.
|
|
379
|
+
|
|
227
380
|
### Environment-Based Switching
|
|
228
381
|
|
|
229
382
|
```ts
|
|
@@ -247,7 +400,7 @@ Templates go in the `emails/` directory:
|
|
|
247
400
|
|
|
248
401
|
```ts
|
|
249
402
|
// emails/welcome.ts
|
|
250
|
-
import { email } from "questpie";
|
|
403
|
+
import { email } from "questpie/services";
|
|
251
404
|
import z from "zod";
|
|
252
405
|
|
|
253
406
|
export default email({
|
|
@@ -279,6 +432,26 @@ handler: async ({ email }) => {
|
|
|
279
432
|
|
|
280
433
|
Key-value storage for caching, rate limiting, ephemeral data.
|
|
281
434
|
|
|
435
|
+
### Redis
|
|
436
|
+
|
|
437
|
+
```ts
|
|
438
|
+
import { createClient } from "redis";
|
|
439
|
+
import { redisKVAdapter } from "questpie/adapters/redis-kv";
|
|
440
|
+
|
|
441
|
+
async function getRedis() {
|
|
442
|
+
const redis = createClient({ url: process.env.REDIS_URL! });
|
|
443
|
+
await redis.connect();
|
|
444
|
+
return redis;
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
export default runtimeConfig({
|
|
448
|
+
kv: {
|
|
449
|
+
adapter: redisKVAdapter({ client: getRedis, keyPrefix: "my-app:" }),
|
|
450
|
+
defaultTtl: 3600,
|
|
451
|
+
},
|
|
452
|
+
});
|
|
453
|
+
```
|
|
454
|
+
|
|
282
455
|
### Custom Adapter
|
|
283
456
|
|
|
284
457
|
```ts
|
|
@@ -305,7 +478,7 @@ export default runtimeConfig({
|
|
|
305
478
|
```ts
|
|
306
479
|
handler: async ({ kv }) => {
|
|
307
480
|
// Set with TTL (seconds)
|
|
308
|
-
await kv.set("session:abc", JSON.stringify(data),
|
|
481
|
+
await kv.set("session:abc", JSON.stringify(data), 3600);
|
|
309
482
|
|
|
310
483
|
// Get
|
|
311
484
|
const value = await kv.get("session:abc");
|
|
@@ -367,16 +540,18 @@ bun add @questpie/openapi
|
|
|
367
540
|
|
|
368
541
|
```ts
|
|
369
542
|
// src/questpie/server/modules.ts
|
|
370
|
-
import { adminModule } from "@questpie/admin/
|
|
371
|
-
import { openApiModule } from "@questpie/openapi";
|
|
543
|
+
import { adminModule } from "@questpie/admin/modules/admin";
|
|
544
|
+
import { openApiModule } from "@questpie/openapi/modules/openapi";
|
|
372
545
|
|
|
373
546
|
export default [adminModule, openApiModule] as const;
|
|
374
547
|
```
|
|
375
548
|
|
|
549
|
+
`openApiModule` carries its codegen plugin — do not also add `openApiPlugin()` to `questpie.config.ts` unless you deliberately omit the module.
|
|
550
|
+
|
|
376
551
|
Configure it in `config/openapi.ts`:
|
|
377
552
|
|
|
378
553
|
```ts
|
|
379
|
-
import { openApiConfig } from "@questpie/openapi";
|
|
554
|
+
import { openApiConfig } from "@questpie/openapi/server";
|
|
380
555
|
|
|
381
556
|
export default openApiConfig({
|
|
382
557
|
info: { title: "My API", version: "1.0.0" },
|
|
@@ -437,7 +612,7 @@ Instead of the module, create route files directly:
|
|
|
437
612
|
|
|
438
613
|
```ts
|
|
439
614
|
// routes/openapi.json.ts
|
|
440
|
-
import { openApiRoute } from "@questpie/openapi";
|
|
615
|
+
import { openApiRoute } from "@questpie/openapi/server";
|
|
441
616
|
|
|
442
617
|
export default openApiRoute({
|
|
443
618
|
info: { title: "My API", version: "1.0.0" },
|
|
@@ -446,7 +621,7 @@ export default openApiRoute({
|
|
|
446
621
|
|
|
447
622
|
```ts
|
|
448
623
|
// routes/docs.ts
|
|
449
|
-
import { docsRoute } from "@questpie/openapi";
|
|
624
|
+
import { docsRoute } from "@questpie/openapi/server";
|
|
450
625
|
|
|
451
626
|
export default docsRoute({
|
|
452
627
|
scalar: { theme: "purple" },
|
|
@@ -456,7 +631,7 @@ export default docsRoute({
|
|
|
456
631
|
### Programmatic Access
|
|
457
632
|
|
|
458
633
|
```ts
|
|
459
|
-
import { generateOpenApiSpec } from "@questpie/openapi";
|
|
634
|
+
import { generateOpenApiSpec } from "@questpie/openapi/server";
|
|
460
635
|
|
|
461
636
|
const spec = generateOpenApiSpec(app, {
|
|
462
637
|
info: { title: "My API", version: "1.0.0" },
|
|
@@ -478,13 +653,11 @@ const spec = generateOpenApiSpec(app, {
|
|
|
478
653
|
## Complete Production Config Example
|
|
479
654
|
|
|
480
655
|
```ts
|
|
481
|
-
import {
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
} from "questpie";
|
|
487
|
-
import { S3Driver } from "flydrive/drivers/s3";
|
|
656
|
+
import { runtimeConfig } from "questpie/app";
|
|
657
|
+
import { pgBossAdapter } from "questpie/adapters/pg-boss";
|
|
658
|
+
import { pgNotifyAdapter } from "questpie/adapters/pg-notify";
|
|
659
|
+
import { SmtpAdapter } from "questpie/adapters/smtp";
|
|
660
|
+
import { s3 } from "files-sdk/s3";
|
|
488
661
|
|
|
489
662
|
export default runtimeConfig({
|
|
490
663
|
db: {
|
|
@@ -492,11 +665,13 @@ export default runtimeConfig({
|
|
|
492
665
|
},
|
|
493
666
|
storage: {
|
|
494
667
|
basePath: "/api",
|
|
495
|
-
|
|
668
|
+
adapter: s3({
|
|
496
669
|
bucket: process.env.S3_BUCKET!,
|
|
497
670
|
region: process.env.S3_REGION!,
|
|
498
|
-
|
|
499
|
-
|
|
671
|
+
credentials: {
|
|
672
|
+
accessKeyId: process.env.S3_ACCESS_KEY!,
|
|
673
|
+
secretAccessKey: process.env.S3_SECRET_KEY!,
|
|
674
|
+
},
|
|
500
675
|
}),
|
|
501
676
|
},
|
|
502
677
|
queue: {
|