create-questpie 2.0.2 → 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 +310 -103
- package/skills/questpie/SKILL.md +196 -84
- 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 +26 -6
- package/skills/questpie/references/extend.md +98 -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 +31 -18
- package/skills/questpie/references/rules.md +140 -13
- 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 +141 -68
- package/skills/questpie-admin/SKILL.md +96 -63
- package/skills/questpie-admin/references/blocks.md +28 -4
- package/skills/questpie-admin/references/custom-ui.md +1 -1
- package/skills/questpie-admin/references/views.md +21 -5
- 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/lib/query-client.ts +10 -1
- 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/admin/$.tsx +12 -1
- package/templates/tanstack-start/src/routes/admin/index.tsx +12 -5
- package/templates/tanstack-start/src/routes/api/$.ts +1 -2
- package/templates/tanstack-start/vite.config.ts +2 -2
|
@@ -8,8 +8,7 @@ Auth is configured via `config/auth.ts` using the `authConfig()` factory:
|
|
|
8
8
|
|
|
9
9
|
```ts
|
|
10
10
|
// src/questpie/server/config/auth.ts
|
|
11
|
-
import { authConfig } from "questpie";
|
|
12
|
-
|
|
11
|
+
import { authConfig } from "questpie/app";
|
|
13
12
|
export default authConfig({
|
|
14
13
|
emailAndPassword: {
|
|
15
14
|
enabled: true,
|
|
@@ -38,7 +37,7 @@ Codegen discovers this file automatically. No manual registration needed.
|
|
|
38
37
|
### In Routes
|
|
39
38
|
|
|
40
39
|
```ts
|
|
41
|
-
import { route } from "questpie";
|
|
40
|
+
import { route } from "questpie/services";
|
|
42
41
|
import z from "zod";
|
|
43
42
|
|
|
44
43
|
export default route()
|
|
@@ -95,16 +94,132 @@ export default route()
|
|
|
95
94
|
|
|
96
95
|
## User Collection
|
|
97
96
|
|
|
98
|
-
The `adminModule` provides
|
|
97
|
+
The `adminModule` includes the starter auth model and provides the canonical Better Auth `user` collection. It stores:
|
|
99
98
|
|
|
100
99
|
- `id` -- unique identifier
|
|
101
100
|
- `email` -- email address
|
|
102
101
|
- `name` -- display name
|
|
103
102
|
- `image` -- avatar URL
|
|
104
103
|
- `emailVerified` -- verification status
|
|
104
|
+
- `role` -- admin access role (`admin` or `user`)
|
|
105
|
+
- `avatar`, `banned`, `banReason`, `banExpires` -- admin-managed profile and access fields
|
|
105
106
|
|
|
106
107
|
This collection is automatically created when you add the admin module to your config.
|
|
107
108
|
|
|
109
|
+
Critical: the built-in admin setup route and admin `AuthGuard` depend on `user.role`. Setup checks whether any user has `role = "admin"`, and the admin UI expects `session.user.role === "admin"`. Do not replace `collection("user")` from scratch in an app that uses `adminModule`; merge `starterModule.collections.user` and extend it if custom user fields or admin layout are needed.
|
|
110
|
+
|
|
111
|
+
```ts
|
|
112
|
+
import { starterModule } from "questpie/app";
|
|
113
|
+
import { collection } from "#questpie/factories";
|
|
114
|
+
|
|
115
|
+
export default collection("user")
|
|
116
|
+
.merge(starterModule.collections.user)
|
|
117
|
+
.fields(({ f }) => ({
|
|
118
|
+
internalNotes: f.textarea(),
|
|
119
|
+
}));
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
`.fields()` is cumulative -- it adds to the merged starter fields and overrides them by key, never wipes them, so this recipe keeps the full starter user model.
|
|
123
|
+
|
|
124
|
+
### Anonymous Users (Better Auth plugin)
|
|
125
|
+
|
|
126
|
+
Better Auth plugins that extend the user model follow the same recipe. For the anonymous plugin, register it in `auth.ts` (merged after the built-in plugins) and extend the starter user with the `isAnonymous` field the plugin expects:
|
|
127
|
+
|
|
128
|
+
```ts
|
|
129
|
+
// auth.ts
|
|
130
|
+
import { anonymous } from "better-auth/plugins";
|
|
131
|
+
import type { AuthConfig } from "questpie/app";
|
|
132
|
+
|
|
133
|
+
export default {
|
|
134
|
+
plugins: [anonymous()],
|
|
135
|
+
} satisfies AuthConfig;
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
```ts
|
|
139
|
+
// collections/user.ts
|
|
140
|
+
import { starterModule } from "questpie/app";
|
|
141
|
+
import { collection } from "#questpie/factories";
|
|
142
|
+
|
|
143
|
+
export default collection("user")
|
|
144
|
+
.merge(starterModule.collections.user)
|
|
145
|
+
.fields(({ f }) => ({
|
|
146
|
+
isAnonymous: f.boolean().default(false),
|
|
147
|
+
}));
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
Run `questpie generate` and apply migrations to add the column. Anonymous sign-in (`authClient.signIn.anonymous()` on the client) creates throwaway users that Better Auth can later link to real accounts.
|
|
151
|
+
|
|
152
|
+
## Reaching the App from Better Auth Callbacks
|
|
153
|
+
|
|
154
|
+
The `/auth/*` catch-all is a plain **raw route**, and raw routes execute their handler inside `runWithContext()` (the request's AsyncLocalStorage scope). That means every Better Auth callback — `onLinkAccount`, `databaseHooks`, `sendMagicLink`, plugin hooks — already runs inside the request scope, and `getContext<App>()` returns the live app, session, db, and locale.
|
|
155
|
+
|
|
156
|
+
**Never build a module-level app singleton or a hand-rolled context bridge for auth callbacks.** The `App` import stays type-only, so there is no circular import:
|
|
157
|
+
|
|
158
|
+
```ts
|
|
159
|
+
// config/auth.ts
|
|
160
|
+
import { anonymous } from "better-auth/plugins";
|
|
161
|
+
import { getContext } from "questpie";
|
|
162
|
+
import { authConfig } from "questpie/app";
|
|
163
|
+
import type { App } from "#questpie"; // type-only — no runtime cycle
|
|
164
|
+
|
|
165
|
+
export default authConfig({
|
|
166
|
+
plugins: [
|
|
167
|
+
anonymous({
|
|
168
|
+
// Fires when an anonymous user signs in with a real account —
|
|
169
|
+
// re-point the guest's rows onto the new user before the plugin
|
|
170
|
+
// deletes the anonymous user.
|
|
171
|
+
onLinkAccount: async ({ anonymousUser, newUser }) => {
|
|
172
|
+
const { app } = getContext<App>();
|
|
173
|
+
// Bare { accessMode: "system" } elevates ONLY the mode —
|
|
174
|
+
// session, db, and locale inherit from the request scope (ALS).
|
|
175
|
+
await app.collections.memberships.updateMany(
|
|
176
|
+
{
|
|
177
|
+
where: { user: anonymousUser.user.id },
|
|
178
|
+
data: { user: newUser.user.id },
|
|
179
|
+
},
|
|
180
|
+
{ accessMode: "system" },
|
|
181
|
+
);
|
|
182
|
+
},
|
|
183
|
+
}),
|
|
184
|
+
],
|
|
185
|
+
});
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
### Partial Context Overrides
|
|
189
|
+
|
|
190
|
+
CRUD context normalization merges what you pass with the ambient request scope — priority: explicit param → ALS scope → defaults (`accessMode: "system"`, `locale: "en"`). Passing only `{ accessMode: "system" }` elevates the mode while the request's session/db/locale ride along. The inverse also holds: `{ accessMode: "user" }` inside system-scoped code re-enables access rules against the inherited session without re-threading it:
|
|
191
|
+
|
|
192
|
+
```ts
|
|
193
|
+
// Inside any handler — session comes from the request ALS scope
|
|
194
|
+
await app.collections.posts.find({}, { accessMode: "user" }); // rules enforced for the current user
|
|
195
|
+
await app.collections.posts.find({}, { accessMode: "system" }); // rules bypassed, same session/locale
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
## Client-Side Auth (authClient)
|
|
199
|
+
|
|
200
|
+
For session state and sign-in/out on the frontend, create a typed Better Auth client. In admin-equipped apps use the typed wrapper (session includes your merged user fields):
|
|
201
|
+
|
|
202
|
+
```ts
|
|
203
|
+
// src/lib/auth-client.ts
|
|
204
|
+
import { createAdminAuthClient } from "@questpie/admin/client";
|
|
205
|
+
import type { AppConfig } from "#questpie";
|
|
206
|
+
import { env } from "#questpie/env.client.vite"; // generated from env.client.ts
|
|
207
|
+
|
|
208
|
+
export const authClient = createAdminAuthClient<AppConfig>({
|
|
209
|
+
baseURL: typeof window !== "undefined" ? window.location.origin : env.APP_URL,
|
|
210
|
+
basePath: "/api/auth",
|
|
211
|
+
});
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
```tsx
|
|
215
|
+
const { data: session, isPending } = authClient.useSession();
|
|
216
|
+
await authClient.signIn.email({ email, password });
|
|
217
|
+
await authClient.signIn.anonymous(); // with the anonymous plugin
|
|
218
|
+
await authClient.signOut();
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
Apps without `@questpie/admin` use Better Auth's own `createAuthClient` from `better-auth/react` pointed at `${APP_URL}/api/auth` — same call surface, without the app-inferred session typing.
|
|
222
|
+
|
|
108
223
|
## Environment Variables
|
|
109
224
|
|
|
110
225
|
| Variable | Required | Description |
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: questpie-core-business-logic
|
|
3
|
-
description:
|
|
3
|
+
description:
|
|
4
|
+
QUESTPIE routes jobs services emails route job service email background queue scheduling Zod input validation server-side logic reusable services email templates
|
|
4
5
|
- questpie-core
|
|
5
6
|
---
|
|
6
7
|
|
|
@@ -16,7 +17,7 @@ JSON routes are typed server-side endpoints. Define an input schema with Zod, wr
|
|
|
16
17
|
|
|
17
18
|
```ts
|
|
18
19
|
// routes/get-active-barbers.ts
|
|
19
|
-
import { route } from "questpie";
|
|
20
|
+
import { route } from "questpie/services";
|
|
20
21
|
import z from "zod";
|
|
21
22
|
|
|
22
23
|
export default route()
|
|
@@ -37,7 +38,7 @@ JSON routes validate input with Zod automatically:
|
|
|
37
38
|
|
|
38
39
|
```ts
|
|
39
40
|
// routes/create-booking.ts
|
|
40
|
-
import { route } from "questpie";
|
|
41
|
+
import { route } from "questpie/services";
|
|
41
42
|
import z from "zod";
|
|
42
43
|
|
|
43
44
|
export default route()
|
|
@@ -77,15 +78,18 @@ export default route()
|
|
|
77
78
|
|
|
78
79
|
Route handlers receive the full `AppContext`:
|
|
79
80
|
|
|
80
|
-
| Property | Description
|
|
81
|
-
| ------------- |
|
|
82
|
-
| `input` | Validated data matching the Zod schema
|
|
83
|
-
| `collections` | Typed collection API
|
|
84
|
-
| `queue` | Publish background jobs
|
|
85
|
-
| `email` | Send emails
|
|
86
|
-
| `db` | Raw database access
|
|
87
|
-
| `session` | Current auth session
|
|
88
|
-
| `services` | Custom services from `services/`
|
|
81
|
+
| Property | Description |
|
|
82
|
+
| ------------- | ---------------------------------------------------------- |
|
|
83
|
+
| `input` | Validated data matching the Zod schema |
|
|
84
|
+
| `collections` | Typed collection API |
|
|
85
|
+
| `queue` | Publish background jobs |
|
|
86
|
+
| `email` | Send emails |
|
|
87
|
+
| `db` | Raw database access |
|
|
88
|
+
| `session` | Current auth session |
|
|
89
|
+
| `services` | Custom services from `services/` |
|
|
90
|
+
| _extensions_ | `appConfig({ context })` result, flat (e.g. `workspaceId`) |
|
|
91
|
+
|
|
92
|
+
Derived request context (from `appConfig({ context })`) reaches route access rules and handlers alike — destructure the keys directly. Inside any nested code, `getContext<App>()` exposes the same keys (see `references/multi-tenancy.md`).
|
|
89
93
|
|
|
90
94
|
### Calling Routes
|
|
91
95
|
|
|
@@ -125,7 +129,7 @@ Jobs are background tasks that run outside the request lifecycle. Ideal for send
|
|
|
125
129
|
|
|
126
130
|
```ts
|
|
127
131
|
// jobs/send-appointment-confirmation.ts
|
|
128
|
-
import { job } from "questpie";
|
|
132
|
+
import { job } from "questpie/services";
|
|
129
133
|
import z from "zod";
|
|
130
134
|
|
|
131
135
|
export default job({
|
|
@@ -182,6 +186,31 @@ Publish from hooks, routes, or other jobs via the typed `queue` context:
|
|
|
182
186
|
|
|
183
187
|
The `queue` object provides full autocompletion for all jobs and their payloads.
|
|
184
188
|
|
|
189
|
+
### Recurring Jobs (Cron)
|
|
190
|
+
|
|
191
|
+
Jobs accept a job-level cron expression in `options.cron`. Schedules are registered automatically when the queue worker starts (`app.queue.listen()` calls `registerSchedules()`):
|
|
192
|
+
|
|
193
|
+
```ts
|
|
194
|
+
// jobs/cleanup-expired.ts
|
|
195
|
+
import { job } from "questpie/services";
|
|
196
|
+
import z from "zod";
|
|
197
|
+
|
|
198
|
+
export default job({
|
|
199
|
+
name: "cleanupExpired",
|
|
200
|
+
schema: z.object({}),
|
|
201
|
+
options: { cron: "0 3 * * *" }, // every day at 03:00
|
|
202
|
+
handler: async ({ collections }) => {
|
|
203
|
+
await collections.sessions.deleteMany({
|
|
204
|
+
where: { expiresAt: { lt: new Date() } },
|
|
205
|
+
});
|
|
206
|
+
},
|
|
207
|
+
});
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
Programmatic scheduling from any handler: `queue.cleanupExpired.schedule({}, "0 3 * * *")` and `queue.cleanupExpired.unschedule()`.
|
|
211
|
+
|
|
212
|
+
Use job-level cron for simple recurring tasks (cleanup, digests, syncs). Reach for **workflow-level cron** (`references/workflows.md`) only when the recurring process needs steps, durable waits, or replay — a workflow is the heavier primitive.
|
|
213
|
+
|
|
185
214
|
### Job Handler Context
|
|
186
215
|
|
|
187
216
|
| Property | Description |
|
|
@@ -198,7 +227,8 @@ Configure the queue adapter in your runtime config:
|
|
|
198
227
|
|
|
199
228
|
```ts
|
|
200
229
|
// questpie.config.ts
|
|
201
|
-
import {
|
|
230
|
+
import { runtimeConfig } from "questpie/app";
|
|
231
|
+
import { pgBossAdapter } from "questpie/adapters/pg-boss";
|
|
202
232
|
|
|
203
233
|
export default runtimeConfig({
|
|
204
234
|
queue: {
|
|
@@ -209,26 +239,28 @@ export default runtimeConfig({
|
|
|
209
239
|
});
|
|
210
240
|
```
|
|
211
241
|
|
|
212
|
-
## Routes
|
|
242
|
+
## Raw Routes
|
|
213
243
|
|
|
214
|
-
|
|
244
|
+
Raw routes (`route().raw()`) give raw HTTP request/response handling for webhooks, OAuth callbacks, health checks, file downloads, and streaming. The handler receives the standard `Request` and must return a `Response`.
|
|
215
245
|
|
|
216
|
-
### Defining a Route
|
|
246
|
+
### Defining a Raw Route
|
|
217
247
|
|
|
218
248
|
```ts
|
|
219
249
|
// routes/health.ts
|
|
220
|
-
import {
|
|
250
|
+
import { sql } from "questpie/drizzle";
|
|
251
|
+
import { route } from "questpie/services";
|
|
221
252
|
|
|
222
|
-
export default route(
|
|
223
|
-
|
|
224
|
-
|
|
253
|
+
export default route()
|
|
254
|
+
.get()
|
|
255
|
+
.raw()
|
|
256
|
+
.access(true)
|
|
257
|
+
.handler(async ({ db }) => {
|
|
225
258
|
const healthy = await db
|
|
226
259
|
.execute(sql`SELECT 1`)
|
|
227
260
|
.then(() => true)
|
|
228
261
|
.catch(() => false);
|
|
229
262
|
return Response.json({ status: healthy ? "ok" : "degraded" });
|
|
230
|
-
}
|
|
231
|
-
});
|
|
263
|
+
});
|
|
232
264
|
```
|
|
233
265
|
|
|
234
266
|
Place files in `routes/`. The file path maps to a flat URL under your `basePath` (`/api` by default):
|
|
@@ -243,27 +275,29 @@ routes/
|
|
|
243
275
|
|
|
244
276
|
### Route Methods
|
|
245
277
|
|
|
278
|
+
Chain HTTP method calls on the builder (multiple methods = multiple calls):
|
|
279
|
+
|
|
246
280
|
```ts
|
|
247
|
-
route(
|
|
248
|
-
route(
|
|
249
|
-
route({ handler: ... }) // All methods (default)
|
|
281
|
+
route().post().raw().handler(...) // POST only
|
|
282
|
+
route().get().post().raw().handler(...) // GET + POST
|
|
250
283
|
```
|
|
251
284
|
|
|
252
|
-
Supported:
|
|
285
|
+
Supported: `.get()`, `.post()`, `.put()`, `.delete()`, `.patch()`. The built-in `/auth/*` catch-all is itself a raw route (`route().get().post().raw()` delegating to Better Auth) — raw handlers run inside `runWithContext`, so the full request context is live in any code they call.
|
|
253
286
|
|
|
254
287
|
### Route Handler Context
|
|
255
288
|
|
|
256
|
-
| Property | Type | Description
|
|
257
|
-
| ------------- | ------------------------ |
|
|
258
|
-
| `request` | `Request` | Standard Web API Request
|
|
259
|
-
| `params` | `Record<string, string>` | URL path parameters
|
|
260
|
-
| `locale` | `string` | Current locale
|
|
261
|
-
| `db` | `Database` | Database instance
|
|
262
|
-
| `session` | `Session \| null` | Current auth session
|
|
263
|
-
| `collections` | `CollectionsAPI` | Typed collection API
|
|
264
|
-
| `queue` | `QueueClient` | Queue client
|
|
265
|
-
| `email` | `MailerService` | Email service
|
|
266
|
-
| `services` | | User-defined services
|
|
289
|
+
| Property | Type | Description |
|
|
290
|
+
| ------------- | ------------------------ | -------------------------------------------- |
|
|
291
|
+
| `request` | `Request` | Standard Web API Request |
|
|
292
|
+
| `params` | `Record<string, string>` | URL path parameters |
|
|
293
|
+
| `locale` | `string` | Current locale |
|
|
294
|
+
| `db` | `Database` | Database instance |
|
|
295
|
+
| `session` | `Session \| null` | Current auth session |
|
|
296
|
+
| `collections` | `CollectionsAPI` | Typed collection API |
|
|
297
|
+
| `queue` | `QueueClient` | Queue client |
|
|
298
|
+
| `email` | `MailerService` | Email service |
|
|
299
|
+
| `services` | | User-defined services |
|
|
300
|
+
| _extensions_ | | `appConfig({ context })` result, flat |
|
|
267
301
|
|
|
268
302
|
Route handlers must return a `Response` object.
|
|
269
303
|
|
|
@@ -279,27 +313,66 @@ Route handlers must return a `Response` object.
|
|
|
279
313
|
|
|
280
314
|
**Rule of thumb**: Use JSON routes for typed input/output with automatic validation. Use raw routes for HTTP-level control (custom headers, binary data, streams, signature verification).
|
|
281
315
|
|
|
282
|
-
### Webhook Example
|
|
316
|
+
### Webhook Example (Signature Verification)
|
|
317
|
+
|
|
318
|
+
Webhooks need the raw body for signature verification — exactly what `.raw()` is for:
|
|
283
319
|
|
|
284
320
|
```ts
|
|
285
321
|
// routes/webhooks/stripe.ts
|
|
286
|
-
import { route } from "questpie";
|
|
322
|
+
import { route } from "questpie/services";
|
|
287
323
|
|
|
288
|
-
export default route(
|
|
289
|
-
|
|
290
|
-
|
|
324
|
+
export default route()
|
|
325
|
+
.post()
|
|
326
|
+
.raw()
|
|
327
|
+
.access(true) // signature IS the auth — verify it yourself below
|
|
328
|
+
.handler(async ({ request, collections, queue }) => {
|
|
291
329
|
const body = await request.text();
|
|
292
330
|
const signature = request.headers.get("stripe-signature");
|
|
293
|
-
const event = verifyStripeWebhook(body, signature);
|
|
331
|
+
const event = verifyStripeWebhook(body, signature); // throws on bad signature
|
|
332
|
+
if (!event) return new Response("Invalid signature", { status: 401 });
|
|
294
333
|
|
|
295
|
-
await
|
|
334
|
+
await collections.webhook_events.create({
|
|
296
335
|
type: event.type,
|
|
297
336
|
payload: body,
|
|
298
337
|
});
|
|
338
|
+
await queue.processStripeEvent.publish({ eventId: event.id });
|
|
299
339
|
|
|
300
340
|
return new Response("OK", { status: 200 });
|
|
301
|
-
}
|
|
302
|
-
|
|
341
|
+
});
|
|
342
|
+
```
|
|
343
|
+
|
|
344
|
+
### Streamed Response Example
|
|
345
|
+
|
|
346
|
+
Raw routes can return any `Response`, including streams — CSV exports, server-sent progress, large file proxies:
|
|
347
|
+
|
|
348
|
+
```ts
|
|
349
|
+
// routes/export.ts
|
|
350
|
+
import { route } from "questpie/services";
|
|
351
|
+
|
|
352
|
+
export default route()
|
|
353
|
+
.get()
|
|
354
|
+
.raw()
|
|
355
|
+
.access(({ session }) => !!session)
|
|
356
|
+
.handler(async ({ collections }) => {
|
|
357
|
+
const { docs } = await collections.orders.find({ limit: 10_000 });
|
|
358
|
+
|
|
359
|
+
const stream = new ReadableStream({
|
|
360
|
+
start(controller) {
|
|
361
|
+
controller.enqueue("id,total,createdAt\n");
|
|
362
|
+
for (const order of docs) {
|
|
363
|
+
controller.enqueue(`${order.id},${order.total},${order.createdAt.toISOString()}\n`);
|
|
364
|
+
}
|
|
365
|
+
controller.close();
|
|
366
|
+
},
|
|
367
|
+
});
|
|
368
|
+
|
|
369
|
+
return new Response(stream, {
|
|
370
|
+
headers: {
|
|
371
|
+
"Content-Type": "text/csv; charset=utf-8",
|
|
372
|
+
"Content-Disposition": 'attachment; filename="orders.csv"',
|
|
373
|
+
},
|
|
374
|
+
});
|
|
375
|
+
});
|
|
303
376
|
```
|
|
304
377
|
|
|
305
378
|
## Services
|
|
@@ -310,8 +383,7 @@ Services are reusable units of logic injected into `AppContext` under the `servi
|
|
|
310
383
|
|
|
311
384
|
```ts
|
|
312
385
|
// services/blog.ts
|
|
313
|
-
import { service } from "questpie";
|
|
314
|
-
|
|
386
|
+
import { service } from "questpie/services";
|
|
315
387
|
const WORDS_PER_MINUTE = 200;
|
|
316
388
|
|
|
317
389
|
function stripHtml(html: string): string {
|
|
@@ -372,7 +444,7 @@ Services are available via `services` destructuring in any handler:
|
|
|
372
444
|
|
|
373
445
|
```ts
|
|
374
446
|
// services/stripe.ts
|
|
375
|
-
import { service } from "questpie";
|
|
447
|
+
import { service } from "questpie/services";
|
|
376
448
|
import Stripe from "stripe";
|
|
377
449
|
|
|
378
450
|
export default service({
|
|
@@ -385,8 +457,7 @@ export default service({
|
|
|
385
457
|
|
|
386
458
|
```ts
|
|
387
459
|
// services/tenant-db.ts
|
|
388
|
-
import { service } from "questpie";
|
|
389
|
-
|
|
460
|
+
import { service } from "questpie/services";
|
|
390
461
|
export default service({
|
|
391
462
|
lifecycle: "request",
|
|
392
463
|
deps: ["db", "session"] as const,
|
|
@@ -403,8 +474,7 @@ Services can depend on other services and infrastructure via `deps`. Use `as con
|
|
|
403
474
|
|
|
404
475
|
```ts
|
|
405
476
|
// services/analytics.ts
|
|
406
|
-
import { service } from "questpie";
|
|
407
|
-
|
|
477
|
+
import { service } from "questpie/services";
|
|
408
478
|
export default service({
|
|
409
479
|
deps: ["db", "logger"] as const,
|
|
410
480
|
create: ({ db, logger }) => {
|
|
@@ -449,7 +519,7 @@ Email templates are defined in `emails/` and discovered by codegen. Each templat
|
|
|
449
519
|
|
|
450
520
|
```ts
|
|
451
521
|
// emails/appointment-confirmation.ts
|
|
452
|
-
import { email } from "questpie";
|
|
522
|
+
import { email } from "questpie/services";
|
|
453
523
|
import { z } from "zod";
|
|
454
524
|
|
|
455
525
|
export default email({
|
|
@@ -500,7 +570,7 @@ Email handlers receive the full `AppContext` for fetching data:
|
|
|
500
570
|
|
|
501
571
|
```ts
|
|
502
572
|
// emails/weekly-digest.ts
|
|
503
|
-
import { email } from "questpie";
|
|
573
|
+
import { email } from "questpie/services";
|
|
504
574
|
import { z } from "zod";
|
|
505
575
|
|
|
506
576
|
export default email({
|