@palbase/backend 4.0.0 → 5.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/{chunk-EG7TTYHY.js → chunk-2N4YNN6F.js} +1 -1
- package/dist/{chunk-EG7TTYHY.js.map → chunk-2N4YNN6F.js.map} +1 -1
- package/dist/db/index.cjs.map +1 -1
- package/dist/db/index.d.cts +2 -2
- package/dist/db/index.d.ts +2 -2
- package/dist/db/index.js +1 -1
- package/dist/{endpoint-2d_DpASt.d.cts → endpoint-BEHjfvFH.d.cts} +7 -1
- package/dist/{endpoint-2d_DpASt.d.ts → endpoint-BEHjfvFH.d.ts} +7 -1
- package/dist/{index-DZW9CjiY.d.ts → index-BTVdhfsb.d.ts} +9 -3
- package/dist/{index-DzRFS3Tl.d.cts → index-mr3Co63T.d.cts} +9 -3
- package/dist/index.cjs +304 -22
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +502 -17
- package/dist/index.d.ts +502 -17
- package/dist/index.js +292 -22
- package/dist/index.js.map +1 -1
- package/dist/test/index.d.cts +1 -1
- package/dist/test/index.d.ts +1 -1
- package/docs/README.md +201 -3
- package/docs/config.md +100 -0
- package/docs/endpoints.md +20 -8
- package/docs/getting-started.md +45 -11
- package/docs/llms-full.txt +349 -103
- package/docs/llms.txt +1 -1
- package/docs/migrations.md +75 -73
- package/docs/routing.md +6 -6
- package/docs/schema.md +3 -2
- package/docs/services.md +2 -3
- package/package.json +1 -1
package/docs/README.md
CHANGED
|
@@ -4,10 +4,202 @@ Write your backend as TypeScript files. Palbase discovers them by path, runs
|
|
|
4
4
|
them inside the managed backend runtime, and exposes them as a typed HTTP API.
|
|
5
5
|
|
|
6
6
|
This is **not** Express, Fastify, or a Supabase Edge Function. There is no
|
|
7
|
-
`app.get(...)`, no manual route registration, no `import express`. You
|
|
8
|
-
|
|
7
|
+
`app.get(...)`, no manual route registration, no `import express`. You author
|
|
8
|
+
**class controllers** with method + parameter decorators (NestJS-style); the
|
|
9
|
+
runtime wires them up.
|
|
9
10
|
|
|
10
|
-
##
|
|
11
|
+
## AGENTS / AI codegen guide (read this first)
|
|
12
|
+
|
|
13
|
+
If you are an AI generating a Palbase backend, this is the contract. Get these
|
|
14
|
+
seven rules right and the deploy succeeds; get them wrong and it fails the
|
|
15
|
+
deploy gate or the TypeScript compile.
|
|
16
|
+
|
|
17
|
+
### Folder layout
|
|
18
|
+
|
|
19
|
+
```
|
|
20
|
+
controllers/<name>.controller.ts # @Controller class + @Get/@Post/… route methods (the API surface)
|
|
21
|
+
models/<controller>/<endpoint>.ts # zod schemas — one folder per controller, one file per endpoint (+ shared.ts)
|
|
22
|
+
services/<name>.service.ts # plain class + singleton — the real logic (controllers stay thin)
|
|
23
|
+
db/schema.ts # config-as-code Postgres schema (tables, columns, RLS) — auto-migrated on deploy
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
The four folders above are the daily surface. These also exist (own docs, linked
|
|
27
|
+
below): `resources/` (external connections — [resources.md](./resources.md)),
|
|
28
|
+
`seeds/` (seed data), `jobs/` + `workers/` (background — [background.md](./background.md)),
|
|
29
|
+
`webhooks/` + `hooks/` (events — [events.md](./events.md)), `middleware/`.
|
|
30
|
+
|
|
31
|
+
### The 7 rules (checklist)
|
|
32
|
+
|
|
33
|
+
1. **`export default <ControllerClass>` is REQUIRED.** Every `controllers/*.controller.ts`
|
|
34
|
+
must default-export its `@Controller` class, or the deploy FAILS with a
|
|
35
|
+
"not a @Controller / non-controller export" error. Either `export default class X {}`
|
|
36
|
+
or a trailing `export default X;` — both work.
|
|
37
|
+
2. **Methods that call a service are `async` and return `Promise<T>`.** Services
|
|
38
|
+
`await Database`, so they return promises; a sync return type on an async body
|
|
39
|
+
is a tsc error. Annotate `: Promise<TodoSchema>`, not `: TodoSchema`, whenever
|
|
40
|
+
the body awaits.
|
|
41
|
+
3. **`@User()` (value) vs `UserT` (type) are two different imports.**
|
|
42
|
+
`import { User } from "@palbase/backend"` (the decorator) +
|
|
43
|
+
`import type { UserT } from "@palbase/backend"` (the user type). Write
|
|
44
|
+
`@User() user: UserT`. They are NOT the same symbol.
|
|
45
|
+
4. **The method's RETURN TYPE names the response schema.** Write
|
|
46
|
+
`: Promise<TodoSchema>` (or `: Promise<z.infer<typeof TodoSchema>>`) — codegen
|
|
47
|
+
+ the runtime read that named type to bind the 200 response zod schema. There
|
|
48
|
+
is no `@Returns` decorator. A body route with no named return type is a build
|
|
49
|
+
error; a method with no response body annotates `: Promise<void>` / `: void`.
|
|
50
|
+
5. **Schemas live in `models/<controller>/<endpoint>.ts`,** exported as a zod
|
|
51
|
+
VALUE and a same-named `z.infer` TYPE. The controller imports them and writes
|
|
52
|
+
`@Body(CreateTodoBody) body: CreateTodoBody` (value in the decorator, same name
|
|
53
|
+
as the type annotation).
|
|
54
|
+
6. **Errors are global throw classes.** `throw new Conflict("…")` / `NotFound` /
|
|
55
|
+
`BadRequest` / `Unauthorized` / `Forbidden` / `TooManyRequests` /
|
|
56
|
+
`PalError(status, code, msg)` — from a controller OR a service, with no `req`.
|
|
57
|
+
There is no per-route errors map and no `req.errors`.
|
|
58
|
+
7. **Auth is secure-by-default.** Every route needs a signed-in user unless it
|
|
59
|
+
opts out: `@Controller("/x", { auth: false })` (whole controller) or
|
|
60
|
+
`@Get("", { auth: false })` (one route). Cascade: route > controller >
|
|
61
|
+
default(required). On a public route inject `@OptionalUser() user: UserT | null`.
|
|
62
|
+
|
|
63
|
+
> **Never** emit `defineController`, `defineHandler`, `defineEndpoint`, `route.get(...)`,
|
|
64
|
+
> `req.input`, `req.params`, or `req.errors` — those are the removed legacy model
|
|
65
|
+
> and will not compile against `@palbase/backend` 4.
|
|
66
|
+
|
|
67
|
+
### Complete CRUD example (copy-pasteable, compiles)
|
|
68
|
+
|
|
69
|
+
```ts
|
|
70
|
+
// models/todos/shared.ts — the response shape, reused across endpoints.
|
|
71
|
+
import { z } from "@palbase/backend";
|
|
72
|
+
export const TodoSchema = z.object({
|
|
73
|
+
id: z.string(),
|
|
74
|
+
title: z.string(),
|
|
75
|
+
completed: z.boolean(),
|
|
76
|
+
});
|
|
77
|
+
export type TodoSchema = z.infer<typeof TodoSchema>;
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
```ts
|
|
81
|
+
// models/todos/create.ts — the POST body.
|
|
82
|
+
import { z } from "@palbase/backend";
|
|
83
|
+
export const CreateTodoBody = z.object({ title: z.string().min(1) });
|
|
84
|
+
export type CreateTodoBody = z.infer<typeof CreateTodoBody>;
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
```ts
|
|
88
|
+
// services/todo.service.ts — plain class + singleton. The real work.
|
|
89
|
+
import { Database, NotFound } from "@palbase/backend";
|
|
90
|
+
import type { TodoSchema } from "../models/todos/shared.js";
|
|
91
|
+
|
|
92
|
+
export class TodoService {
|
|
93
|
+
list(userId: string): Promise<TodoSchema[]> {
|
|
94
|
+
return Database.tables.todos.findMany({ user_id: userId });
|
|
95
|
+
}
|
|
96
|
+
create(userId: string, title: string): Promise<TodoSchema> {
|
|
97
|
+
return Database.tables.todos.insert({ user_id: userId, title });
|
|
98
|
+
}
|
|
99
|
+
async get(userId: string, id: string): Promise<TodoSchema> {
|
|
100
|
+
const t = await Database.tables.todos.findById(id);
|
|
101
|
+
if (!t || t.user_id !== userId) throw new NotFound("No todo with that id");
|
|
102
|
+
return t;
|
|
103
|
+
}
|
|
104
|
+
async remove(userId: string, id: string): Promise<void> {
|
|
105
|
+
const t = await Database.tables.todos.findById(id);
|
|
106
|
+
if (!t || t.user_id !== userId) throw new NotFound("No todo with that id");
|
|
107
|
+
await Database.tables.todos.delete(id);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
export const todoService = new TodoService();
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
```ts
|
|
114
|
+
// controllers/todos.controller.ts — class controller. Thin: delegates to the service.
|
|
115
|
+
import { Controller, Get, Post, Delete, Body, Param, User } from "@palbase/backend";
|
|
116
|
+
import type { UserT } from "@palbase/backend"; // the user TYPE (@User is the value)
|
|
117
|
+
import { todoService } from "../services/todo.service.js";
|
|
118
|
+
import { TodoSchema } from "../models/todos/shared.js";
|
|
119
|
+
import { CreateTodoBody } from "../models/todos/create.js";
|
|
120
|
+
|
|
121
|
+
@Controller("/todos") // secure-by-default; { auth: false } opts the whole controller out
|
|
122
|
+
export class TodosController {
|
|
123
|
+
private todos = todoService;
|
|
124
|
+
|
|
125
|
+
@Get("") // GET /todos → operationId getTodos
|
|
126
|
+
async list(@User() user: UserT): Promise<TodoSchema[]> { // return type → 200 response schema
|
|
127
|
+
return this.todos.list(user.id);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
@Post("") // POST /todos → postTodos
|
|
131
|
+
async create(@Body(CreateTodoBody) body: CreateTodoBody, @User() user: UserT): Promise<TodoSchema> {
|
|
132
|
+
return this.todos.create(user.id, body.title);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
@Get("/{id}") // GET /todos/{id} → getTodosById
|
|
136
|
+
async get(@Param("id") id: string, @User() user: UserT): Promise<TodoSchema> {
|
|
137
|
+
return this.todos.get(user.id, id);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
@Delete("/{id}") // DELETE /todos/{id} → deleteTodosById; no body → : Promise<void>
|
|
141
|
+
async remove(@Param("id") id: string, @User() user: UserT): Promise<void> {
|
|
142
|
+
await this.todos.remove(user.id, id);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
export default TodosController; // REQUIRED — the runtime loads the default export
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
```ts
|
|
150
|
+
// db/schema.ts — config-as-code; the deploy auto-migrates additive changes.
|
|
151
|
+
import { defineSchema, uuid, text, boolean, timestamp, policy } from "@palbase/backend";
|
|
152
|
+
export default defineSchema({
|
|
153
|
+
tables: {
|
|
154
|
+
todos: {
|
|
155
|
+
columns: {
|
|
156
|
+
id: uuid().primaryKey().defaultRandom(),
|
|
157
|
+
user_id: text().notNull(), // palauth user id (TEXT, e.g. "usr_…")
|
|
158
|
+
title: text().notNull(),
|
|
159
|
+
completed: boolean().default(false),
|
|
160
|
+
created_at: timestamp().defaultNow(),
|
|
161
|
+
},
|
|
162
|
+
rls: true, // every Database.* query runs as the request user
|
|
163
|
+
policies: [
|
|
164
|
+
policy("pb_owner_all")
|
|
165
|
+
.for("all")
|
|
166
|
+
.to("authenticated")
|
|
167
|
+
.using("user_id = (select auth.uid())")
|
|
168
|
+
.withCheck("user_id = (select auth.uid())"),
|
|
169
|
+
],
|
|
170
|
+
},
|
|
171
|
+
},
|
|
172
|
+
});
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
### operationId — what the generated clients call
|
|
176
|
+
|
|
177
|
+
The operationId is derived FLAT from the verb + full path (NOT the method name):
|
|
178
|
+
`GET /todos` → `getTodos`, `POST /todos` → `postTodos`, `GET /todos/{id}` →
|
|
179
|
+
`getTodosById`. The generated iOS/TS clients expose these as `pb.getTodos()`,
|
|
180
|
+
`pb.postTodos(...)`, `pb.getTodosById(id:)`. Rename the method freely; the
|
|
181
|
+
operationId is unaffected. Change the verb and the operationId changes — no file
|
|
182
|
+
rename needed.
|
|
183
|
+
|
|
184
|
+
### CLI workflow
|
|
185
|
+
|
|
186
|
+
- `palbase serve` — run `controllers/` locally with hot reload (proxies
|
|
187
|
+
`Database`/services to the deployed branch; runs `gen-types` on startup).
|
|
188
|
+
- `palbase gen-types` — regenerate `palbase-env.d.ts` from `db/schema.ts` so
|
|
189
|
+
`Database.tables.*` is typed (no import, no generic). Standalone version of what
|
|
190
|
+
`serve` runs.
|
|
191
|
+
- **Deploy is GitHub-native** — `git push` triggers the deploy (there is no
|
|
192
|
+
`palbase push`). Push a branch to deploy that branch.
|
|
193
|
+
- `palbase secret set NAME=value` / `palbase secret list` — branch-scoped
|
|
194
|
+
secrets (encrypted at rest), read in code via `process.env.NAME`.
|
|
195
|
+
- `palbase secret pull` / `palbase secret push` — sync the branch's env vars with
|
|
196
|
+
a local `.env.local` (gitignored). `pull` writes decrypted values for local dev
|
|
197
|
+
(merging local-only keys); `push` uploads changed keys (new keys default to
|
|
198
|
+
SECRET and need `--secret`/`--plain` classification, existing secrets are left
|
|
199
|
+
untouched unless `--force-secrets`).
|
|
200
|
+
- `tsconfig.json` needs `"experimentalDecorators": true` (the scaffold sets it).
|
|
201
|
+
|
|
202
|
+
## Mental model
|
|
11
203
|
|
|
12
204
|
Every handler type imports service singletons the same way:
|
|
13
205
|
|
|
@@ -35,6 +227,7 @@ the imported singletons — not from `ctx` or any argument.
|
|
|
35
227
|
```
|
|
36
228
|
my-backend/
|
|
37
229
|
├── package.json # depends on @palbase/backend
|
|
230
|
+
├── tsconfig.json # experimentalDecorators: true (required for the decorators)
|
|
38
231
|
├── controllers/ # @Controller classes: @Get/@Post methods (mount the API)
|
|
39
232
|
│ └── hello.controller.ts # @Controller("/hello") + @Get example
|
|
40
233
|
├── models/<ctrl>/<ep>.ts # zod schemas, folder per controller, file per endpoint
|
|
@@ -42,6 +235,8 @@ my-backend/
|
|
|
42
235
|
├── services/ # plain classes/singletons your controllers call
|
|
43
236
|
├── db/schema.ts # table definitions (optional, enables typed DB)
|
|
44
237
|
├── db/migrations/ # explicit SQL migrations for type changes (optional)
|
|
238
|
+
├── resources/ # external connections, set up once at boot (optional)
|
|
239
|
+
├── seeds/ # seed data (optional)
|
|
45
240
|
├── workers/ # background job handlers (optional)
|
|
46
241
|
├── jobs/ # cron-scheduled jobs (optional)
|
|
47
242
|
├── hooks/ # auth/storage/document event hooks (optional)
|
|
@@ -64,9 +259,12 @@ HTTP endpoints are **not** file-path routed. You author a class controller
|
|
|
64
259
|
| Schema & typed DB | [schema.md](./schema.md) |
|
|
65
260
|
| Migrations (additive vs type-change + drift-gate) | [migrations.md](./migrations.md) |
|
|
66
261
|
| Services (Cache, Queue, Storage, …) | [services.md](./services.md) |
|
|
262
|
+
| Resources (external connections) | [resources.md](./resources.md) |
|
|
67
263
|
| Errors | [errors.md](./errors.md) |
|
|
68
264
|
| Workers & Jobs | [background.md](./background.md) |
|
|
69
265
|
| Hooks & Webhooks | [events.md](./events.md) |
|
|
70
266
|
|
|
71
267
|
For AI coding tools: a single concatenated corpus is generated at
|
|
72
268
|
[`llms-full.txt`](./llms-full.txt) (and an index at [`llms.txt`](./llms.txt)).
|
|
269
|
+
</content>
|
|
270
|
+
</invoke>
|
package/docs/config.md
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
# Module Config (config-as-code)
|
|
2
|
+
|
|
3
|
+
Beyond `db/schema.ts`, three more module surfaces are git-authoritative: storage
|
|
4
|
+
buckets, notification providers, and feature-flag definitions. You declare them
|
|
5
|
+
in `config/*.ts` files (typed, imported from `@palbase/backend`) and on `git
|
|
6
|
+
push` the deploy creates/updates them. Secrets (certs, keys, API tokens) NEVER
|
|
7
|
+
go in git — they live in a reserved encrypted env namespace, uploaded by the
|
|
8
|
+
guided CLI.
|
|
9
|
+
|
|
10
|
+
You normally author these with `palbase <module> add …` (the CLI writes the
|
|
11
|
+
config file + uploads any secret); the files below are what it generates.
|
|
12
|
+
|
|
13
|
+
## Storage buckets — `config/storage.ts`
|
|
14
|
+
|
|
15
|
+
```ts
|
|
16
|
+
import { defineStorage, bucket } from "@palbase/backend";
|
|
17
|
+
|
|
18
|
+
export default defineStorage({
|
|
19
|
+
buckets: {
|
|
20
|
+
avatars: bucket({
|
|
21
|
+
public: true, // served without a signed URL
|
|
22
|
+
fileSizeLimit: "5MB", // "5MB"/"20MB"/"1GB" or a byte number
|
|
23
|
+
allowedMimeTypes: ["image/png", "image/jpeg", "image/webp"],
|
|
24
|
+
}),
|
|
25
|
+
invoices: bucket({ public: false, fileSizeLimit: "20MB", allowedMimeTypes: ["application/pdf"] }),
|
|
26
|
+
},
|
|
27
|
+
});
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
Author it: `palbase storage buckets add avatars --public --max-size 5MB --mime image/png,image/jpeg`.
|
|
31
|
+
On deploy, the buckets are created/updated. A bucket REMOVED from the file is
|
|
32
|
+
**never auto-deleted** (its files would be lost) — drop it explicitly in Studio.
|
|
33
|
+
The files inside a bucket are runtime state, not config.
|
|
34
|
+
|
|
35
|
+
## Notification providers — `config/notifications.ts`
|
|
36
|
+
|
|
37
|
+
Providers carry secrets (APNs `.p8`, FCM service-account JSON, Twilio token).
|
|
38
|
+
The config file is **structural** — it names the enabled providers + their
|
|
39
|
+
non-secret fields; the secret is bound by convention to a reserved env key and
|
|
40
|
+
NEVER appears in git.
|
|
41
|
+
|
|
42
|
+
```ts
|
|
43
|
+
import { defineNotifications } from "@palbase/backend";
|
|
44
|
+
|
|
45
|
+
export default defineNotifications({
|
|
46
|
+
push: {
|
|
47
|
+
apns: { enabled: true, teamId: "A1B2C3D4E5", keyId: "XYZ123", bundleId: "net.example.app" },
|
|
48
|
+
// no p8 key here — it's in the reserved secret PB_NOTIFICATIONS_APNS_P8
|
|
49
|
+
},
|
|
50
|
+
sms: {
|
|
51
|
+
twilio: { enabled: true, accountSid: "AC...", messagingServiceSid: "MG..." },
|
|
52
|
+
},
|
|
53
|
+
});
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
Author it with the guided CLI — it knows each provider's fields and uploads the
|
|
57
|
+
secret for you, so you never type a secret-name string:
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
palbase notifications providers # list the catalog + what's configured
|
|
61
|
+
palbase notifications add apns \
|
|
62
|
+
--team-id A1B2C3D4E5 --key-id XYZ123 --bundle-id net.example.app \
|
|
63
|
+
--p8-file ./AuthKey_XYZ123.p8 # → uploads PB_NOTIFICATIONS_APNS_P8 (encrypted)
|
|
64
|
+
palbase notifications add twilio --account-sid AC... --messaging-sid MG...
|
|
65
|
+
# prompts for the auth token (hidden)
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
The reserved secret env keys (`PB_NOTIFICATIONS_*`) are managed by these
|
|
69
|
+
commands — `palbase secret set PB_*` is refused. Your own custom env
|
|
70
|
+
(`MY_API_KEY` etc.) is unaffected and still flows via `.env.local`. On deploy,
|
|
71
|
+
each enabled provider's reserved secret is resolved and the provider is
|
|
72
|
+
configured; a provider whose secret is missing is skipped (warned, not fatal).
|
|
73
|
+
|
|
74
|
+
## Feature flags — `config/flags.ts`
|
|
75
|
+
|
|
76
|
+
Flag DEFINITIONS (key, type, default) are config; the value set for a specific
|
|
77
|
+
user / an A/B assignment is runtime (set via the SDK/Studio, not git).
|
|
78
|
+
|
|
79
|
+
```ts
|
|
80
|
+
import { defineFlags, flag } from "@palbase/backend";
|
|
81
|
+
|
|
82
|
+
export default defineFlags({
|
|
83
|
+
flags: {
|
|
84
|
+
new_checkout: flag({ type: "boolean", default: false, description: "Gate the redesigned checkout" }),
|
|
85
|
+
upload_limit: flag({ type: "number", default: 10 }),
|
|
86
|
+
theme: flag({ type: "string", default: "system", variants: ["light", "dark", "system"] }),
|
|
87
|
+
},
|
|
88
|
+
});
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
Author it: `palbase flags add new_checkout --type boolean --default false`. On
|
|
92
|
+
deploy, the definitions are upserted to the flags service (idempotent). A flag
|
|
93
|
+
removed from the file is **not auto-deleted** (orphan definitions are harmless).
|
|
94
|
+
|
|
95
|
+
## How it's applied
|
|
96
|
+
|
|
97
|
+
All three are evaluated + applied **in your backend pod on deploy**, the same
|
|
98
|
+
place `db/schema.ts` migrations run — the pod reaches each module through your
|
|
99
|
+
project's gateway with a service-role key. The apply is fail-soft: a config
|
|
100
|
+
error logs a warning but never aborts the deploy of your code.
|
package/docs/endpoints.md
CHANGED
|
@@ -12,22 +12,20 @@ imported singletons (see [services.md](./services.md)).
|
|
|
12
12
|
|
|
13
13
|
```ts
|
|
14
14
|
// controllers/rooms.controller.ts
|
|
15
|
-
import { Controller, Get, Post,
|
|
15
|
+
import { Controller, Get, Post, Body, Param, User, NotFound, Database } from "@palbase/backend";
|
|
16
16
|
import type { UserT } from "@palbase/backend";
|
|
17
17
|
import { CreateRoomBody } from "../models/rooms/create.js";
|
|
18
|
-
import { RoomSchema } from "../models/rooms/shared.js";
|
|
18
|
+
import type { RoomSchema } from "../models/rooms/shared.js"; // the return TYPE names the 200 schema
|
|
19
19
|
|
|
20
20
|
@Controller("/rooms")
|
|
21
21
|
export default class RoomsController {
|
|
22
22
|
@Post("")
|
|
23
|
-
@Returns(RoomSchema)
|
|
24
23
|
async create(@Body(CreateRoomBody) body: CreateRoomBody, @User() user: UserT): Promise<RoomSchema> {
|
|
25
24
|
const room = await Database.insert("rooms", { name: body.name, capacity: body.capacity ?? null });
|
|
26
25
|
return { id: room.id as string, name: room.name as string, capacity: (room.capacity as number) ?? null };
|
|
27
26
|
}
|
|
28
27
|
|
|
29
28
|
@Get("/{id}")
|
|
30
|
-
@Returns(RoomSchema)
|
|
31
29
|
async getOne(@Param("id") id: string): Promise<RoomSchema> {
|
|
32
30
|
const room = await Database.findById("rooms", id);
|
|
33
31
|
if (!room) throw new NotFound("Room does not exist", "room_not_found");
|
|
@@ -36,6 +34,19 @@ export default class RoomsController {
|
|
|
36
34
|
}
|
|
37
35
|
```
|
|
38
36
|
|
|
37
|
+
**Two non-negotiables** (the most common codegen mistakes):
|
|
38
|
+
|
|
39
|
+
1. **`export default <Controller>` is REQUIRED.** Above it is
|
|
40
|
+
`export default class RoomsController`; the trailing form `export class
|
|
41
|
+
RoomsController {…}` + `export default RoomsController;` is equivalent. Without
|
|
42
|
+
a default export the deploy aborts with a "not a @Controller / non-controller
|
|
43
|
+
export" error — the runtime loads the file's default export.
|
|
44
|
+
2. **A method that awaits a service is `async` + `Promise<T>`.** `Database`
|
|
45
|
+
returns promises, so a body that `await`s it cannot have a sync return type
|
|
46
|
+
(`: RoomSchema` on an `async` body is a `tsc` error). Both methods above are
|
|
47
|
+
`async` and return `Promise<RoomSchema>`. (A pure method that returns a literal
|
|
48
|
+
with no `await` may stay synchronous.)
|
|
49
|
+
|
|
39
50
|
## Method decorators
|
|
40
51
|
|
|
41
52
|
`@Get`/`@Post`/`@Put`/`@Patch`/`@Delete` take `(subpath, options?)`:
|
|
@@ -73,10 +84,11 @@ name `User` is the `@User()` decorator.
|
|
|
73
84
|
|
|
74
85
|
## Output = return type
|
|
75
86
|
|
|
76
|
-
The success response is the method's RETURN TYPE
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
87
|
+
The success response is the method's RETURN TYPE — there is no `@Returns`
|
|
88
|
+
decorator. Annotate `: Promise<RoomSchema>` (or `: Promise<z.infer<typeof
|
|
89
|
+
RoomSchema>>`); codegen + the runtime read that named type to bind the OpenAPI
|
|
90
|
+
200 response zod schema. A body route with no named return type is a build
|
|
91
|
+
error. A method that returns nothing annotates `: void` (or `: Promise<void>`).
|
|
80
92
|
|
|
81
93
|
## Auth — controller default + route override cascade
|
|
82
94
|
|
package/docs/getting-started.md
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
There is no CLI init command. A starter project is created for you when your
|
|
4
4
|
Palbase project is provisioned. You then edit the files locally and deploy them.
|
|
5
5
|
|
|
6
|
-
## package.json
|
|
6
|
+
## package.json + tsconfig.json
|
|
7
7
|
|
|
8
8
|
Your project depends on the SDK and uses the Palbase CLI for the dev loop:
|
|
9
9
|
|
|
@@ -20,13 +20,33 @@ Your project depends on the SDK and uses the Palbase CLI for the dev loop:
|
|
|
20
20
|
}
|
|
21
21
|
```
|
|
22
22
|
|
|
23
|
+
The controllers use **decorators**, so the `tsconfig.json` must set
|
|
24
|
+
`experimentalDecorators: true` (the scaffold ships it):
|
|
25
|
+
|
|
26
|
+
```json
|
|
27
|
+
{
|
|
28
|
+
"compilerOptions": {
|
|
29
|
+
"target": "ES2022",
|
|
30
|
+
"module": "ESNext",
|
|
31
|
+
"moduleResolution": "Bundler",
|
|
32
|
+
"strict": true,
|
|
33
|
+
"experimentalDecorators": true,
|
|
34
|
+
"noEmit": true
|
|
35
|
+
},
|
|
36
|
+
"include": ["controllers/**/*.ts", "models/**/*.ts", "services/**/*.ts", "db/**/*.ts", "*.d.ts"]
|
|
37
|
+
}
|
|
38
|
+
```
|
|
39
|
+
|
|
23
40
|
## Local dev loop
|
|
24
41
|
|
|
25
42
|
- `palbase serve` — run your backend locally with hot reload. It runs your
|
|
26
|
-
`controllers/` locally but proxies `Database
|
|
27
|
-
branch, so the branch must already be deployed (serve tells
|
|
28
|
-
if it isn't).
|
|
29
|
-
side of this.
|
|
43
|
+
`controllers/` locally but proxies `Database` and the service singletons to
|
|
44
|
+
the **deployed** branch, so the branch must already be deployed (serve tells
|
|
45
|
+
you to push first if it isn't). On startup it also runs `gen-types`. See
|
|
46
|
+
[migrations.md](./migrations.md) for the schema/migration side of this.
|
|
47
|
+
- `palbase gen-types` — regenerate `palbase-env.d.ts` from `db/schema.ts` so
|
|
48
|
+
`Database.tables.*` is typed (no import, no generic). Run it standalone after
|
|
49
|
+
editing the schema, or rely on `palbase serve` running it for you.
|
|
30
50
|
- **Deploy is GitHub-native** — there is no `palbase push`. Commit and
|
|
31
51
|
`git push` to your project's repo; the push triggers a deploy of the backend
|
|
32
52
|
runtime. Push a **branch** to deploy that branch instead of `main`.
|
|
@@ -49,14 +69,14 @@ export type HelloResponse = z.infer<typeof HelloResponse>;
|
|
|
49
69
|
Then write the controller in `controllers/hello.controller.ts`:
|
|
50
70
|
|
|
51
71
|
```ts
|
|
52
|
-
import { Controller, Get,
|
|
72
|
+
import { Controller, Get, Query, OptionalUser } from "@palbase/backend";
|
|
53
73
|
import type { UserT } from "@palbase/backend";
|
|
54
|
-
import { GreetQuery
|
|
74
|
+
import { GreetQuery } from "../models/hello/greet.js";
|
|
75
|
+
import type { HelloResponse } from "../models/hello/greet.js"; // the return TYPE names the 200 schema
|
|
55
76
|
|
|
56
77
|
@Controller("/hello", { auth: false })
|
|
57
78
|
export default class HelloController {
|
|
58
79
|
@Get("")
|
|
59
|
-
@Returns(HelloResponse)
|
|
60
80
|
greet(@Query(GreetQuery) q: GreetQuery, @OptionalUser() user: UserT | null): HelloResponse {
|
|
61
81
|
return { message: `hello, ${q.name ?? "world"}!`, user: user?.id ?? null };
|
|
62
82
|
}
|
|
@@ -64,6 +84,20 @@ export default class HelloController {
|
|
|
64
84
|
```
|
|
65
85
|
|
|
66
86
|
This is served at `GET /hello`. The `@Query` schema validates the query string;
|
|
67
|
-
the method's
|
|
68
|
-
|
|
69
|
-
|
|
87
|
+
the method's RETURN TYPE (`: HelloResponse`) names the response schema — codegen
|
|
88
|
+
+ the runtime read it to validate and describe the 200 response. There is no
|
|
89
|
+
`@Returns` decorator; a body route with no named return type is a build error.
|
|
90
|
+
|
|
91
|
+
Two things every controller file MUST have:
|
|
92
|
+
|
|
93
|
+
- **A default export of the `@Controller` class.** Above it is `export default class
|
|
94
|
+
HelloController` (inline); the equivalent trailing form is `export class
|
|
95
|
+
HelloController {…}` then `export default HelloController;`. Without a default
|
|
96
|
+
export the deploy fails ("not a @Controller").
|
|
97
|
+
- **`async` + `Promise<T>` once the method awaits a service.** `greet` above is
|
|
98
|
+
synchronous (pure), so it returns `HelloResponse` directly. The moment a method
|
|
99
|
+
calls a service that awaits `Database`, make it `async` and return
|
|
100
|
+
`Promise<HelloResponse>`.
|
|
101
|
+
|
|
102
|
+
See [routing.md](./routing.md) and [endpoints.md](./endpoints.md) for the full
|
|
103
|
+
class-controller model.
|