@palbase/backend 3.0.0 → 4.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. package/dist/{chunk-B7EUJP5W.js → chunk-EG7TTYHY.js} +113 -3
  2. package/dist/chunk-EG7TTYHY.js.map +1 -0
  3. package/dist/{chunk-PHAFZGHN.js → chunk-WUQO76NW.js} +26 -19
  4. package/dist/chunk-WUQO76NW.js.map +1 -0
  5. package/dist/db/index.cjs +117 -2
  6. package/dist/db/index.cjs.map +1 -1
  7. package/dist/db/index.d.cts +2 -2
  8. package/dist/db/index.d.ts +2 -2
  9. package/dist/db/index.js +11 -1
  10. package/dist/{endpoint-DJ98tQd6.d.cts → endpoint-2d_DpASt.d.cts} +92 -56
  11. package/dist/{endpoint-DJ98tQd6.d.ts → endpoint-2d_DpASt.d.ts} +92 -56
  12. package/dist/{index-CXUs9iTQ.d.ts → index-DZW9CjiY.d.ts} +210 -41
  13. package/dist/{index-CZAwpQE1.d.cts → index-DzRFS3Tl.d.cts} +210 -41
  14. package/dist/index.cjs +371 -42
  15. package/dist/index.cjs.map +1 -1
  16. package/dist/index.d.cts +88 -215
  17. package/dist/index.d.ts +88 -215
  18. package/dist/index.js +217 -21
  19. package/dist/index.js.map +1 -1
  20. package/dist/test/index.cjs +34 -19
  21. package/dist/test/index.cjs.map +1 -1
  22. package/dist/test/index.d.cts +1 -1
  23. package/dist/test/index.d.ts +1 -1
  24. package/dist/test/index.js +10 -2
  25. package/dist/test/index.js.map +1 -1
  26. package/docs/README.md +11 -11
  27. package/docs/database.md +40 -0
  28. package/docs/endpoints.md +98 -92
  29. package/docs/errors.md +37 -30
  30. package/docs/getting-started.md +24 -20
  31. package/docs/llms-full.txt +401 -235
  32. package/docs/routing.md +39 -45
  33. package/docs/schema.md +134 -23
  34. package/docs/services.md +14 -10
  35. package/package.json +2 -2
  36. package/dist/chunk-B7EUJP5W.js.map +0 -1
  37. package/dist/chunk-PHAFZGHN.js.map +0 -1
package/docs/endpoints.md CHANGED
@@ -1,105 +1,126 @@
1
1
  # Endpoints
2
2
 
3
- An endpoint is a **handler** `export default defineHandler({...})` in a file
4
- under `handlers/`. A handler has NO method or path; a **controller** maps
5
- method+path to it (see [routing.md](./routing.md)). The handler receives **one
6
- argument**, `req` (a `PBRequest`). Services are NOT on `req` — import them as
7
- singletons (see [services.md](./services.md)).
3
+ An endpoint is a **method on a class controller** a class decorated with
4
+ `@Controller(basePath)` whose methods are decorated with `@Get`/`@Post`/`@Put`/
5
+ `@Patch`/`@Delete`. Controller files live under `controllers/` and are mounted
6
+ automatically (see [routing.md](./routing.md)). Request input + context are
7
+ injected into the method via **parameter decorators** (`@Body`/`@Query`/`@Param`/
8
+ `@User`/…), each piece direct — no `req` god-object. Services are reached via the
9
+ imported singletons (see [services.md](./services.md)).
8
10
 
9
- ## `defineHandler` config
11
+ ## A controller
10
12
 
11
13
  ```ts
12
- defineHandler({
13
- auth: { required: true }, // see Auth below. Omitted AUTH REQUIRED (secure-by-default).
14
- rateLimit: { max: 100, window: 60 }, // optional: max requests per window seconds
15
- input: z.object({ ... }), // optional Zod schema → validates & types req.input
16
- output: z.object({ ... }), // optional Zod schema → validates the return value
17
- errors: { ... }, // optional declared errors (see errors.md)
18
- middleware: [ ... ], // optional Middleware[] (see below)
19
- handler: async (req) => { ... }, // required
20
- });
14
+ // controllers/rooms.controller.ts
15
+ import { Controller, Get, Post, Returns, Body, Param, User, NotFound, Database } from "@palbase/backend";
16
+ import type { UserT } from "@palbase/backend";
17
+ import { CreateRoomBody } from "../models/rooms/create.js";
18
+ import { RoomSchema } from "../models/rooms/shared.js";
19
+
20
+ @Controller("/rooms")
21
+ export default class RoomsController {
22
+ @Post("")
23
+ @Returns(RoomSchema)
24
+ async create(@Body(CreateRoomBody) body: CreateRoomBody, @User() user: UserT): Promise<RoomSchema> {
25
+ const room = await Database.insert("rooms", { name: body.name, capacity: body.capacity ?? null });
26
+ return { id: room.id as string, name: room.name as string, capacity: (room.capacity as number) ?? null };
27
+ }
28
+
29
+ @Get("/{id}")
30
+ @Returns(RoomSchema)
31
+ async getOne(@Param("id") id: string): Promise<RoomSchema> {
32
+ const room = await Database.findById("rooms", id);
33
+ if (!room) throw new NotFound("Room does not exist", "room_not_found");
34
+ return { id: room.id as string, name: room.name as string, capacity: (room.capacity as number) ?? null };
35
+ }
36
+ }
21
37
  ```
22
38
 
23
- There is no `method` field — the HTTP verb (and path) live in the controller
24
- that mounts the handler via `route.get|post|put|patch|delete`.
39
+ ## Method decorators
40
+
41
+ `@Get`/`@Post`/`@Put`/`@Patch`/`@Delete` take `(subpath, options?)`:
25
42
 
26
- ## `req` (PBRequest)
43
+ ```ts
44
+ interface RouteOptions {
45
+ auth?: boolean | { required?: boolean; role?: string }; // overrides controller default
46
+ rateLimit?: { max: number; window: number }; // optional, per route
47
+ }
48
+ ```
27
49
 
28
- | Field | Type | Notes |
29
- |-------|------|-------|
30
- | `req.input` | inferred from `input` schema | request body for POST/PUT/PATCH; `{}` otherwise |
31
- | `req.params` | `Record<string,string>` | route params, e.g. `req.params.id` |
32
- | `req.query` | `Record<string,string>` | parsed query string |
33
- | `req.headers` | `Record<string,string>` | lowercase keys |
34
- | `req.user` | `User` when authenticated; `User \| null` when `auth` is omitted or `required: false` | see Auth below |
35
- | `req.client` | `ClientInfo` | calling SDK/app/platform/os version (all nullable) |
36
- | `req.file` | `FileContext \| null` | uploaded file, if any |
37
- | `req.method` | `string` | the HTTP method |
38
- | `req.requestId` / `req.traceId` / `req.spanId` | `string` | correlation ids |
39
- | `req.errors` | typed throwers | present when `errors` is declared (see errors.md) |
50
+ Input and output are NOT method-decorator options: input comes from the
51
+ parameter decorators, output from the method's return type. The method decorator
52
+ only carries route concerns (path, auth, rateLimit).
40
53
 
41
- `User` is `{ id: string; email?: string; role: string; metadata: Record<string, unknown> }`.
54
+ ## Parameter decorators
42
55
 
43
- ## Auth
56
+ | Decorator | Injects | Typed by | OpenAPI |
57
+ |---|---|---|---|
58
+ | `@Body(schema)` | request body | `: T` (`z.infer<schema>`, same name) | requestBody |
59
+ | `@Query(schema)` | query params | `: T` | query parameters |
60
+ | `@Param("id")` | one path param | `: string` | path parameter |
61
+ | `@Headers(schema?)` | request headers | `Record<string,string>` (+ typed with schema) | header parameters |
62
+ | `@User()` | authenticated user | `: UserT` | — (from token) |
63
+ | `@OptionalUser()` | user, possibly anon | `: UserT \| null` | — |
64
+ | `@Client()` | parsed client info | `: ClientInfo` | — |
65
+ | `@RequestId()` / `@TraceId()` | correlation ids | `: string` | — |
66
+ | `@Req()` | raw request (escape hatch) | `: PBRequest` | — |
44
67
 
45
- **Secure by default:** a handler requires authentication UNLESS it explicitly
46
- opts out. Omitting `auth` means AUTH REQUIRED a forgotten `auth` fails safe
47
- (401), never silently public. Mark a route PUBLIC with `auth: { required: false }`.
68
+ `@Body`/`@Query`/`@Headers` take a zod schema (validation + codegen source); the
69
+ developer writes the matching type annotation (same name). `UserT` is the
70
+ authenticated-user type (`{ id: string; email?: string; role: string; metadata:
71
+ Record<string, unknown> }`) — exported under the alias `UserT` because the value
72
+ name `User` is the `@User()` decorator.
48
73
 
49
- ```ts
50
- // auth omitted entirely // AUTH REQUIRED → req.user is non-null User
51
- auth: { required: true } // same: require any authenticated user
52
- auth: { required: true, role: "admin" } // require a specific role
53
- auth: { required: false } // PUBLIC → req.user may be null (anon key only)
54
- ```
74
+ ## Output = return type
55
75
 
56
- Whether `req.user` is non-null is computed from the `auth` config at the type
57
- level (and matches the runtime exactly):
76
+ The success response is the method's RETURN TYPE. Pair it with `@Returns(schema)`
77
+ so the zod value drives the OpenAPI 200 response (the type annotation drives
78
+ compile-time checking; the zod value is lossless for codegen). A method that
79
+ returns nothing annotates `: void`.
58
80
 
59
- | `auth` value | `req.user` type |
60
- |--------------|-----------------|
61
- | omitted | `User` (secure-by-default) |
62
- | `true` | `User` |
63
- | `false` | `User \| null` (public) |
64
- | `{ required: true }` | `User` |
65
- | `{ required: false }` | `User \| null` (public) |
66
- | `{ role: "admin" }` (object, no `required`) | `User` |
81
+ ## Auth controller default + route override cascade
67
82
 
68
- To make a route PUBLIC, set `auth: { required: false }` (or `auth: false`). An
69
- object with a `role` but no `required` key is treated as authenticated. When
70
- `auth` is omitted, the endpoint is public and `req.user` may be null.
83
+ **Secure by default:** a route requires authentication UNLESS it explicitly opts
84
+ out. Resolution order (most specific wins):
71
85
 
72
- ## Typed input/output
86
+ 1. Route-level `@Post("", { auth })` — wins if present.
87
+ 2. Controller-level `@Controller("/x", { auth })` — applies to routes without
88
+ their own `auth`.
89
+ 3. Default `true` (secure-by-default).
73
90
 
74
91
  ```ts
75
- import { defineHandler, z, Database } from "@palbase/backend";
76
-
77
- export default defineHandler({
78
- auth: { required: true },
79
- input: z.object({ name: z.string().min(1).max(100), capacity: z.number().int().positive().optional() }),
80
- output: z.object({ id: z.string(), name: z.string(), capacity: z.number().nullable() }),
81
- handler: async (req) => {
82
- const room = await Database.insert("rooms", {
83
- name: req.input.name,
84
- capacity: req.input.capacity ?? null,
85
- });
86
- return { id: room.id as string, name: room.name as string, capacity: (room.capacity as number) ?? null };
87
- },
88
- });
92
+ @Controller("/public", { auth: false }) // all routes default public
93
+ export default class PublicController {
94
+ @Get("/open") open(): Info { ... } // inherits → public
95
+ @Get("/secret", { auth: true }) // OVERRIDES → authed
96
+ secret(@User() u: UserT): Secret { ... }
97
+ }
89
98
  ```
90
99
 
91
- A controller then mounts it with a method + path:
100
+ | effective `auth` | `@User()` type to annotate |
101
+ |------------------|----------------------------|
102
+ | required (omitted / `true` / `{ required: true }` / role-only) | `@User() u: UserT` |
103
+ | public (`false` / `{ required: false }`) | `@OptionalUser() u: UserT \| null` |
104
+
105
+ The runtime always applies the resolved effective auth; the static type you
106
+ annotate is best-effort (a parameter decorator cannot see the controller default
107
+ at the type level).
108
+
109
+ ## Errors — global throw classes
110
+
111
+ Throw an error class anywhere (controller OR service) — no `req`, no per-route
112
+ map. The runtime catches it and emits the standard envelope:
92
113
 
93
114
  ```ts
94
- // controllers/rooms.controller.ts
95
- import { defineController, route } from "@palbase/backend";
96
- import create from "../handlers/rooms/create.js";
115
+ import { Conflict, NotFound, BadRequest, Unauthorized, Forbidden, TooManyRequests, PalError } from "@palbase/backend";
97
116
 
98
- export default defineController("/rooms", {
99
- create: route.post("/", create),
100
- });
117
+ throw new Conflict("title taken"); // → 409
118
+ throw new NotFound(); // → 404 ("Not found")
119
+ throw new PalError(418, "teapot", "custom"); // → custom status/code
101
120
  ```
102
121
 
122
+ See [errors.md](./errors.md) for the full set + the wire envelope shape.
123
+
103
124
  ## Middleware
104
125
 
105
126
  A middleware wraps a request. Define one in `middleware/<name>.ts`:
@@ -116,19 +137,4 @@ export default defineMiddleware(async (ctx, next) => {
116
137
  ```
117
138
 
118
139
  The middleware handler receives `(ctx, next)` — call `await next()` to run the
119
- rest of the chain (other middleware, then the endpoint handler). Note this uses
120
- the `ctx` model, not `req`.
121
-
122
- To attach middleware to a specific handler, import it and list it in the
123
- handler's `middleware` array:
124
-
125
- ```ts
126
- import { defineHandler, z } from "@palbase/backend";
127
- import logger from "../../middleware/logger.js";
128
-
129
- export default defineHandler({
130
- middleware: [logger],
131
- output: z.object({ ok: z.boolean() }),
132
- handler: async (req) => ({ ok: true }),
133
- });
134
- ```
140
+ rest of the chain (other middleware, then the endpoint method).
package/docs/errors.md CHANGED
@@ -1,47 +1,54 @@
1
1
  # Errors
2
2
 
3
- Two ways to fail a request. Both serialize to the standard Palbase error
4
- envelope:
3
+ Throw an error class to fail a request. Every one serializes to the standard
4
+ Palbase error envelope:
5
5
 
6
6
  ```json
7
7
  { "error": "todo_not_found", "error_description": "No such todo", "status": 404, "request_id": "req_…" }
8
8
  ```
9
9
 
10
- ## 1. `HttpError` (ad-hoc)
10
+ Throw anywhere — in a controller method OR in a `services/` class. No `req`, no
11
+ per-route error map: the runtime catches any thrown error class and emits the
12
+ envelope.
13
+
14
+ ## Named status classes
11
15
 
12
16
  ```ts
13
- import { HttpError } from "@palbase/backend";
14
- throw new HttpError(404, "todo_not_found", "No such todo");
15
- // optional structured payload (4th arg) rides along under `data`:
16
- throw new HttpError(423, "todo_locked", "Locked", { retryAfter: 30 });
17
+ import { BadRequest, Unauthorized, Forbidden, NotFound, Conflict, TooManyRequests } from "@palbase/backend";
18
+
19
+ throw new Conflict("title taken"); // 409 ("conflict" code by default)
20
+ throw new NotFound(); // → 404 ("not_found", "Not found")
21
+ throw new BadRequest("missing field"); // → 400
22
+ throw new Unauthorized(); // → 401
23
+ throw new Forbidden(); // → 403
24
+ throw new TooManyRequests(); // → 429
17
25
  ```
18
26
 
19
- `new HttpError(status, code, description, data?)`.
27
+ Each class fixes its HTTP status. The constructor is
28
+ `new <Class>(message?, code?, data?)`:
20
29
 
21
- ## 2. Declared errors (typed)
30
+ - `message` overrides the human-readable `error_description` (defaults to a label
31
+ derived from the class name).
32
+ - `code` overrides the wire `error` code (defaults to the class's snake_case
33
+ code, e.g. `not_found`).
34
+ - `data` rides along under the envelope's `data` field for structured context.
22
35
 
23
- Declare them on the handler; throw via `req.errors.<name>(...)`. Declared
24
- errors are described in the endpoint's OpenAPI and codegen'd into a typed enum
25
- for iOS callers.
36
+ ```ts
37
+ throw new NotFound("Room does not exist", "room_not_found");
38
+ throw new Conflict("locked", "title_locked", { retryAfter: 30 });
39
+ ```
40
+
41
+ ## `PalError` / `HttpError` (custom status)
42
+
43
+ For a status/code not covered by a named class:
26
44
 
27
45
  ```ts
28
- import { defineHandler, z, Database } from "@palbase/backend";
29
-
30
- export default defineHandler({
31
- input: z.object({ id: z.string() }),
32
- output: z.object({ ok: z.boolean() }),
33
- errors: {
34
- notFound: { status: 404, code: "todo_not_found", description: "No such todo" },
35
- locked: { status: 423, code: "todo_locked", data: z.object({ retryAfter: z.number() }) },
36
- },
37
- handler: async (req) => {
38
- const todo = await Database.findById("todos", req.input.id);
39
- if (!todo) throw req.errors.notFound(); // no data → no args
40
- if (todo.locked) throw req.errors.locked({ retryAfter: 30 }); // data schema → required arg
41
- return { ok: true };
42
- },
43
- });
46
+ import { PalError } from "@palbase/backend";
47
+ throw new PalError(418, "teapot", "I'm a teapot");
48
+ // optional structured payload (4th arg) rides along under `data`:
49
+ throw new PalError(423, "todo_locked", "Locked", { retryAfter: 30 });
44
50
  ```
45
51
 
46
- A declared error with a `data` Zod schema requires that payload as an argument;
47
- one without `data` takes no arguments. This is enforced by the types.
52
+ `PalError` (and its base `HttpError`) take `(status, code, description, data?)`.
53
+ The named classes all extend `HttpError`, so `catch (e) { if (e instanceof
54
+ HttpError) … }` matches any of them.
@@ -33,33 +33,37 @@ Your project depends on the SDK and uses the Palbase CLI for the dev loop:
33
33
 
34
34
  ## Your first endpoint
35
35
 
36
- A handler is one endpoint unit; a controller maps method+path to it. Create
37
- `handlers/hello.ts`:
36
+ An endpoint is a method on a class controller. Declare the schemas in `models/`:
38
37
 
39
38
  ```ts
40
- import { defineHandler, z } from "@palbase/backend";
39
+ // models/hello/greet.ts
40
+ import { z } from "@palbase/backend";
41
41
 
42
- export default defineHandler({
43
- input: z.object({ name: z.string().optional() }),
44
- output: z.object({ message: z.string(), user: z.string().nullable() }),
45
- handler: async (req) => ({
46
- message: `hello, ${req.input.name ?? "world"}!`,
47
- user: req.user?.id ?? null,
48
- }),
49
- });
42
+ export const GreetQuery = z.object({ name: z.string().optional() });
43
+ export type GreetQuery = z.infer<typeof GreetQuery>;
44
+
45
+ export const HelloResponse = z.object({ message: z.string(), user: z.string().nullable() });
46
+ export type HelloResponse = z.infer<typeof HelloResponse>;
50
47
  ```
51
48
 
52
- Then mount it in `controllers/hello.controller.ts`:
49
+ Then write the controller in `controllers/hello.controller.ts`:
53
50
 
54
51
  ```ts
55
- import { defineController, route } from "@palbase/backend";
56
- import hello from "../handlers/hello.js";
52
+ import { Controller, Get, Returns, Query, OptionalUser } from "@palbase/backend";
53
+ import type { UserT } from "@palbase/backend";
54
+ import { GreetQuery, HelloResponse } from "../models/hello/greet.js";
57
55
 
58
- export default defineController("/hello", {
59
- hello: route.get("/", hello),
60
- });
56
+ @Controller("/hello", { auth: false })
57
+ export default class HelloController {
58
+ @Get("")
59
+ @Returns(HelloResponse)
60
+ greet(@Query(GreetQuery) q: GreetQuery, @OptionalUser() user: UserT | null): HelloResponse {
61
+ return { message: `hello, ${q.name ?? "world"}!`, user: user?.id ?? null };
62
+ }
63
+ }
61
64
  ```
62
65
 
63
- This is served at `GET /hello`. The Zod `input` schema validates the request and
64
- flows into `req.input`; the `output` schema validates your return value. See
65
- [routing.md](./routing.md) for the handler + controller model.
66
+ This is served at `GET /hello`. The `@Query` schema validates the query string;
67
+ the method's return type (paired with `@Returns(HelloResponse)`) validates and
68
+ describes the response. See [routing.md](./routing.md) and
69
+ [endpoints.md](./endpoints.md) for the full class-controller model.