@palbase/backend 33.0.0 → 33.0.2
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/bin/palbase-backend.cjs.map +1 -1
- package/dist/bin/palbase-backend.js +5 -5
- package/dist/{chunk-BRLJOXWS.js → chunk-AAT5G7KY.js} +2 -2
- package/dist/{chunk-BRLJOXWS.js.map → chunk-AAT5G7KY.js.map} +1 -1
- package/dist/{chunk-DCDHAKF3.js → chunk-C6COAB3E.js} +5 -5
- package/dist/{chunk-C4ZA5AT2.js → chunk-SI4KGEM3.js} +1 -1
- package/dist/{chunk-C4ZA5AT2.js.map → chunk-SI4KGEM3.js.map} +1 -1
- package/dist/{chunk-QMFOL2K6.js → chunk-TVCCR6SO.js} +2 -2
- package/dist/{chunk-N54QZER3.js → chunk-WWUG2QXF.js} +2 -2
- package/dist/{chunk-GOPZPM2A.js → chunk-YIQ4RS4F.js} +2 -2
- package/dist/db/index.cjs.map +1 -1
- package/dist/db/index.d.cts +1 -1
- package/dist/db/index.d.ts +1 -1
- package/dist/db/index.js +2 -2
- package/dist/engine/index.cjs.map +1 -1
- package/dist/engine/index.d.cts +3 -3
- package/dist/engine/index.d.ts +3 -3
- package/dist/engine/index.js +5 -5
- package/dist/{index-Bi74dcOu.d.ts → index-D-3duy8Y.d.ts} +2 -2
- package/dist/{index-BWgnGj68.d.cts → index-DB_nW-AV.d.cts} +19 -10
- package/dist/{index-g6iQyYci.d.cts → index-DLveQoOf.d.cts} +2 -2
- package/dist/{index-Dg10RnZO.d.ts → index-DMZG3kpo.d.ts} +19 -10
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +6 -6
- package/dist/index.d.ts +6 -6
- package/dist/index.js +5 -5
- package/dist/openapi/index.cjs +1 -1
- package/dist/openapi/index.cjs.map +1 -1
- package/dist/openapi/index.d.cts +2 -2
- package/dist/openapi/index.d.ts +2 -2
- package/dist/openapi/index.js +1 -1
- package/dist/{registry-6VT5RPeO.d.cts → registry-DSTThhKf.d.cts} +1 -1
- package/dist/{registry-B-sxJJN0.d.ts → registry-JjF5lcj4.d.ts} +1 -1
- package/dist/test/index.cjs +1120 -4
- package/dist/test/index.cjs.map +1 -1
- package/dist/test/index.d.cts +1 -1
- package/dist/test/index.d.ts +1 -1
- package/dist/test/index.js +1121 -5
- package/dist/test/index.js.map +1 -1
- package/docs/README.md +123 -79
- package/docs/auth.md +11 -7
- package/docs/background.md +16 -7
- package/docs/database.md +22 -18
- package/docs/endpoints.md +26 -20
- package/docs/errors.md +2 -3
- package/docs/events.md +63 -29
- package/docs/getting-started.md +8 -7
- package/docs/llms-full.txt +399 -232
- package/docs/migrations.md +6 -5
- package/docs/routing.md +34 -11
- package/docs/schema.md +61 -36
- package/docs/services.md +30 -13
- package/package.json +2 -1
- package/stack-images.json +24 -0
- package/template/AGENTS.md +7 -7
- package/template/modules/health/health.controller.ts +7 -4
- package/template/modules/notes/dto/create.ts +4 -3
- package/template/modules/notes/notes.controller.ts +8 -6
- /package/dist/{chunk-DCDHAKF3.js.map → chunk-C6COAB3E.js.map} +0 -0
- /package/dist/{chunk-QMFOL2K6.js.map → chunk-TVCCR6SO.js.map} +0 -0
- /package/dist/{chunk-N54QZER3.js.map → chunk-WWUG2QXF.js.map} +0 -0
- /package/dist/{chunk-GOPZPM2A.js.map → chunk-YIQ4RS4F.js.map} +0 -0
package/docs/endpoints.md
CHANGED
|
@@ -2,36 +2,41 @@
|
|
|
2
2
|
|
|
3
3
|
An endpoint is a **method on a class controller** — a class decorated with
|
|
4
4
|
`@Controller(basePath)` whose methods are decorated with `@Get`/`@Post`/`@Put`/
|
|
5
|
-
`@Patch`/`@Delete`/`@Query`.
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
5
|
+
`@Patch`/`@Delete`/`@Query`. A controller lives in its domain's folder
|
|
6
|
+
(`modules/<domain>/<name>.controller.ts`) and a `@Module` lists it in
|
|
7
|
+
`controllers` — that list is the registration, not the directory (see
|
|
8
|
+
[routing.md](./routing.md)). Request input + context are injected into the method
|
|
9
|
+
via **parameter decorators** (`@Body`/`@QueryParams`/`@Param`/`@User`/…), each
|
|
10
|
+
piece direct — no `req` god-object. Your own services arrive through the
|
|
11
|
+
CONSTRUCTOR and the container supplies them (see [services.md](./services.md)).
|
|
10
12
|
|
|
11
13
|
## A controller
|
|
12
14
|
|
|
13
15
|
```ts
|
|
14
|
-
//
|
|
16
|
+
// modules/rooms/rooms.controller.ts
|
|
15
17
|
import { Controller, Get, Post, Body, Param, User } from "@palbase/backend";
|
|
16
18
|
import type { UserT } from "@palbase/backend";
|
|
17
|
-
import {
|
|
18
|
-
import { CreateRoomBody } from "
|
|
19
|
-
import
|
|
19
|
+
import { RoomService } from "./room.service";
|
|
20
|
+
import { CreateRoomBody } from "./dto/create";
|
|
21
|
+
import { RoomSchema } from "./dto/shared"; // the return TYPE names the 200 schema
|
|
20
22
|
|
|
21
23
|
// A controller does not import `Database`. Everything here is HTTP: validate the
|
|
22
24
|
// body through a named schema, name the 200 shape as the return type, delegate.
|
|
23
25
|
// Which rows, whose, in what order is the service's job — and the service is the
|
|
24
26
|
// thing worth testing, because it is the thing that can be wrong.
|
|
25
27
|
@Controller("/rooms")
|
|
26
|
-
export
|
|
28
|
+
export class RoomsController {
|
|
29
|
+
// The service arrives through the constructor; `rooms.module.ts` lists both.
|
|
30
|
+
constructor(private readonly rooms: RoomService) {}
|
|
31
|
+
|
|
27
32
|
@Post("")
|
|
28
33
|
create(@Body(CreateRoomBody) body: CreateRoomBody, @User() user: UserT): Promise<RoomSchema> {
|
|
29
|
-
return
|
|
34
|
+
return this.rooms.create(user.id, body);
|
|
30
35
|
}
|
|
31
36
|
|
|
32
37
|
@Get("/{id}")
|
|
33
38
|
getOne(@Param("id") id: string): Promise<RoomSchema> {
|
|
34
|
-
return
|
|
39
|
+
return this.rooms.get(id);
|
|
35
40
|
}
|
|
36
41
|
}
|
|
37
42
|
```
|
|
@@ -41,13 +46,14 @@ export default class RoomsController {
|
|
|
41
46
|
1. **The controller does not touch the database.** It delegates to a service, as
|
|
42
47
|
above. A method here that reaches for `Database` has moved the logic into the
|
|
43
48
|
layer that is hardest to test. (Export the class by NAME so its module can
|
|
44
|
-
list it in `controllers`; a class no module lists is refused at
|
|
45
|
-
`unowned class`. Nothing here is default-exported.)
|
|
49
|
+
import it and list it in `controllers`; a class no module lists is refused at
|
|
50
|
+
build with `unowned class`. Nothing here is default-exported.)
|
|
46
51
|
2. **A method that awaits a service is `async` + `Promise<T>`.** `Database`
|
|
47
52
|
returns promises, so a body that `await`s it cannot have a sync return type
|
|
48
|
-
(`: RoomSchema` on an `async` body is a `tsc` error). Both methods above
|
|
49
|
-
|
|
50
|
-
|
|
53
|
+
(`: RoomSchema` on an `async` body is a `tsc` error). Both methods above hand
|
|
54
|
+
the service's promise straight back, so they need no `await` — but they still
|
|
55
|
+
return `Promise<RoomSchema>`. (A pure method that returns a literal and touches
|
|
56
|
+
no promise may stay synchronous.)
|
|
51
57
|
|
|
52
58
|
## Method decorators
|
|
53
59
|
|
|
@@ -73,8 +79,8 @@ a query string — complex search/filter endpoints that would otherwise be a
|
|
|
73
79
|
|
|
74
80
|
```ts
|
|
75
81
|
@Query("/search")
|
|
76
|
-
|
|
77
|
-
return
|
|
82
|
+
search(@Body(SearchTodosBody) body: SearchTodosBody): Promise<TodoSchema[]> {
|
|
83
|
+
return this.todos.search(body);
|
|
78
84
|
}
|
|
79
85
|
```
|
|
80
86
|
|
|
@@ -121,7 +127,7 @@ out. Resolution order (most specific wins):
|
|
|
121
127
|
|
|
122
128
|
```ts
|
|
123
129
|
@Controller("/public", { auth: false }) // all routes default public
|
|
124
|
-
export
|
|
130
|
+
export class PublicController {
|
|
125
131
|
@Get("/open") open(): Info { ... } // inherits → public
|
|
126
132
|
@Get("/secret", { auth: true }) // OVERRIDES → authed
|
|
127
133
|
secret(@User() u: UserT): Secret { ... }
|
package/docs/errors.md
CHANGED
|
@@ -7,9 +7,8 @@ Palbase error envelope:
|
|
|
7
7
|
{ "error": "todo_not_found", "error_description": "No such todo", "status": 404, "request_id": "req_…" }
|
|
8
8
|
```
|
|
9
9
|
|
|
10
|
-
Throw anywhere — in a controller method OR in a
|
|
11
|
-
|
|
12
|
-
envelope.
|
|
10
|
+
Throw anywhere — in a controller method OR in a service. No `req`, no per-route
|
|
11
|
+
error map: the runtime catches any thrown error class and emits the envelope.
|
|
13
12
|
|
|
14
13
|
The runtime refuses some requests before a handler runs, and those refusals use
|
|
15
14
|
the same envelope and the same codes as the classes below — so a client decodes
|
package/docs/events.md
CHANGED
|
@@ -1,52 +1,86 @@
|
|
|
1
1
|
# Hooks & Webhooks
|
|
2
2
|
|
|
3
|
-
Like jobs, hooks and webhooks
|
|
4
|
-
|
|
5
|
-
"@palbase/backend"`). They do
|
|
6
|
-
carries the non-service data
|
|
7
|
-
get `requestId`).
|
|
3
|
+
Like jobs, `hooks/` and `webhooks/` stay at the ROOT — neither is part of a
|
|
4
|
+
module — and both reach the platform services through the same import as
|
|
5
|
+
everything else (`import { Database, Log } from "@palbase/backend"`). They do
|
|
6
|
+
**not** receive a `req`. A second `meta` argument carries the non-service data
|
|
7
|
+
(`env`, `environmentId`; webhooks also get `requestId`).
|
|
8
8
|
|
|
9
9
|
## Hooks (platform events)
|
|
10
10
|
|
|
11
|
-
React to auth, storage,
|
|
12
|
-
|
|
11
|
+
React to events this stack raises — auth, storage, documents. Files live under
|
|
12
|
+
`hooks/`, one **default-exported class** per file, and the handlers are methods
|
|
13
|
+
carrying a decorator. There are no builder functions to import.
|
|
14
|
+
|
|
15
|
+
Two decorators, and the difference is whether the handler can stop the event:
|
|
16
|
+
|
|
17
|
+
| | Where the event comes from | Can it block? |
|
|
18
|
+
| --- | --- | --- |
|
|
19
|
+
| `@Hook("before.…")` | this stack | **Yes** — `throw` cancels the operation |
|
|
20
|
+
| `@On("after.…")` | this stack | No — a monitor; a throw reaches only the log |
|
|
21
|
+
| `@Webhook` + `@On` | an OUTSIDE service | No — see the next section |
|
|
13
22
|
|
|
14
23
|
```ts
|
|
15
24
|
// hooks/auth.ts
|
|
16
|
-
import {
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
25
|
+
import { Database, Deny, Hook, Log, On } from "@palbase/backend";
|
|
26
|
+
import type { AuthHookEvent, HookMeta } from "@palbase/backend";
|
|
27
|
+
|
|
28
|
+
export default class AuthHooks {
|
|
29
|
+
// The gate. A throw CANCELS the signup and the reason reaches the caller.
|
|
30
|
+
@Hook("before.user.create")
|
|
31
|
+
async gate(event: AuthHookEvent, _meta: HookMeta): Promise<void> {
|
|
32
|
+
if ((event.user?.email ?? "").endsWith("@blocked.test")) {
|
|
33
|
+
throw new Deny("this address is not accepted");
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// The monitor. It runs after the fact, so a throw here is logged and the
|
|
38
|
+
// session is not un-created.
|
|
39
|
+
@On("after.login.failed")
|
|
40
|
+
async record(event: AuthHookEvent, meta: HookMeta): Promise<void> {
|
|
41
|
+
Log.info(`failed login for ${event.user?.email ?? "(no email)"} req=${meta.requestId}`);
|
|
42
|
+
await Database.$insert("login_failures", { email: event.user?.email ?? null });
|
|
43
|
+
}
|
|
44
|
+
}
|
|
29
45
|
```
|
|
30
46
|
|
|
31
|
-
`
|
|
32
|
-
|
|
47
|
+
`Deny` is exported for exactly this: any throw denies, but `Deny` carries the
|
|
48
|
+
reason deliberately instead of surfacing whatever a stray `TypeError` said. The
|
|
49
|
+
engine is fail-closed — an unreachable hook denies — so keep a blocking handler
|
|
50
|
+
narrow.
|
|
33
51
|
|
|
34
|
-
|
|
35
|
-
`
|
|
36
|
-
|
|
37
|
-
|
|
52
|
+
`meta` shape: `{ env, environmentId, requestId? }`. Environment variables are in
|
|
53
|
+
`meta.env`; the platform services (`Database`, `Log`, …) are imported as
|
|
54
|
+
singletons, never passed in.
|
|
55
|
+
|
|
56
|
+
> **Warning:** `auth`, `storage` and `documents` builder objects —
|
|
57
|
+
> `auth.onUserCreated(handler)`, `storage.onFileUploaded(handler)` and friends —
|
|
58
|
+
> **were removed in 26.0.0** and this page taught them until 33.0.0. They were
|
|
59
|
+
> not merely renamed: nothing in the runtime or the bundler ever read the record
|
|
60
|
+
> they returned, so a project that wrote one got a handler that was **never
|
|
61
|
+
> called and no error said so**. The types went with them for the same reason —
|
|
62
|
+
> `UserCreatedEvent` promised `user.role`, `user.metadata` and `user.createdAt`
|
|
63
|
+
> and the auth engine sends none of the three. If you have such a file, the
|
|
64
|
+
> import now fails to build, which is the point.
|
|
38
65
|
|
|
39
66
|
## Webhooks (inbound provider events)
|
|
40
67
|
|
|
41
68
|
Receive and verify webhooks from third-party providers. Files live under
|
|
42
|
-
`webhooks
|
|
43
|
-
|
|
69
|
+
`webhooks/`, one class per file, `export default` required.
|
|
70
|
+
|
|
71
|
+
**`name` is required and DECLARED.** It is the path segment the webhook is served
|
|
72
|
+
at — `POST /webhooks/<name>` — so `name: "stripe"` answers at
|
|
73
|
+
`POST /webhooks/stripe`. It is not taken from the file name any more: a PUBLIC
|
|
74
|
+
URL, the one a sender is configured with, belongs beside the provider and the
|
|
75
|
+
secret rather than in the file system. Lowercase letters, digits and dashes.
|
|
44
76
|
|
|
45
77
|
```ts
|
|
46
78
|
// webhooks/stripe.ts
|
|
47
79
|
import { Database, Log, On, Webhook, type WebhookMeta } from "@palbase/backend";
|
|
48
80
|
|
|
49
|
-
|
|
81
|
+
// name: the path segment (POST /webhooks/stripe); secret: the signing secret,
|
|
82
|
+
// resolved by the runtime from the named env var.
|
|
83
|
+
@Webhook({ name: "stripe", provider: "stripe", secret: { env: "STRIPE_WEBHOOK_SECRET" } })
|
|
50
84
|
export default class StripeWebhook {
|
|
51
85
|
@On("checkout.session.completed")
|
|
52
86
|
async checkoutCompleted(event: unknown, meta: WebhookMeta) {
|
package/docs/getting-started.md
CHANGED
|
@@ -33,7 +33,8 @@ The controllers use **decorators**, so the `tsconfig.json` must set
|
|
|
33
33
|
"experimentalDecorators": true,
|
|
34
34
|
"noEmit": true
|
|
35
35
|
},
|
|
36
|
-
"include": ["
|
|
36
|
+
"include": ["**/*.ts"],
|
|
37
|
+
"exclude": ["node_modules", ".palbase"]
|
|
37
38
|
}
|
|
38
39
|
```
|
|
39
40
|
|
|
@@ -43,7 +44,7 @@ Your backend runs on Palbase, not on your laptop — there is no local runtime t
|
|
|
43
44
|
start. The loop is: edit, validate, push to a dev Environment.
|
|
44
45
|
|
|
45
46
|
- `palbase build` — validate the tree the way the deploy will. It stages and
|
|
46
|
-
bundles your `
|
|
47
|
+
bundles your `modules/` exactly as the deploy does and runs the deploy's
|
|
47
48
|
own metadata extractor over the result, so a bad decorator, an illegal return
|
|
48
49
|
type or an SDK major skew fails here rather than shipping a deploy that
|
|
49
50
|
serves zero endpoints. It is wired into a `pre-push` git hook for you. It also
|
|
@@ -57,10 +58,10 @@ start. The loop is: edit, validate, push to a dev Environment.
|
|
|
57
58
|
|
|
58
59
|
## Your first endpoint
|
|
59
60
|
|
|
60
|
-
An endpoint is a method on a class controller.
|
|
61
|
+
An endpoint is a method on a class controller. Everything for one domain lives in `modules/<domain>/`; declare the schemas in its `dto/`:
|
|
61
62
|
|
|
62
63
|
```ts
|
|
63
|
-
//
|
|
64
|
+
// modules/hello/dto/greet.ts
|
|
64
65
|
import { z } from "@palbase/backend";
|
|
65
66
|
|
|
66
67
|
export const GreetQuery = z.object({ name: z.string().optional() });
|
|
@@ -70,13 +71,13 @@ export const HelloResponse = z.object({ message: z.string(), user: z.string().nu
|
|
|
70
71
|
export type HelloResponse = z.infer<typeof HelloResponse>;
|
|
71
72
|
```
|
|
72
73
|
|
|
73
|
-
Then write the controller in `
|
|
74
|
+
Then write the controller in `modules/hello/hello.controller.ts`:
|
|
74
75
|
|
|
75
76
|
```ts
|
|
76
77
|
import { Controller, Get, QueryParams, OptionalUser } from "@palbase/backend";
|
|
77
78
|
import type { UserT } from "@palbase/backend";
|
|
78
|
-
import { GreetQuery } from "
|
|
79
|
-
import type { HelloResponse } from "
|
|
79
|
+
import { GreetQuery } from "./dto/greet.js";
|
|
80
|
+
import type { HelloResponse } from "./dto/greet.js"; // the return TYPE names the 200 schema
|
|
80
81
|
|
|
81
82
|
@Controller("/hello", { auth: false })
|
|
82
83
|
export default class HelloController {
|