@palbase/backend 33.0.0 → 33.0.1

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 (53) hide show
  1. package/dist/bin/palbase-backend.cjs.map +1 -1
  2. package/dist/bin/palbase-backend.js +4 -4
  3. package/dist/{chunk-DCDHAKF3.js → chunk-IKDONZ5D.js} +4 -4
  4. package/dist/{chunk-C4ZA5AT2.js → chunk-SI4KGEM3.js} +1 -1
  5. package/dist/{chunk-C4ZA5AT2.js.map → chunk-SI4KGEM3.js.map} +1 -1
  6. package/dist/{chunk-QMFOL2K6.js → chunk-TVCCR6SO.js} +2 -2
  7. package/dist/{chunk-N54QZER3.js → chunk-WWUG2QXF.js} +2 -2
  8. package/dist/{chunk-GOPZPM2A.js → chunk-YIQ4RS4F.js} +2 -2
  9. package/dist/db/index.cjs.map +1 -1
  10. package/dist/db/index.d.cts +1 -1
  11. package/dist/db/index.d.ts +1 -1
  12. package/dist/db/index.js +2 -2
  13. package/dist/engine/index.cjs.map +1 -1
  14. package/dist/engine/index.d.cts +3 -3
  15. package/dist/engine/index.d.ts +3 -3
  16. package/dist/engine/index.js +4 -4
  17. package/dist/{index-Bi74dcOu.d.ts → index-D-3duy8Y.d.ts} +2 -2
  18. package/dist/{index-BWgnGj68.d.cts → index-DB_nW-AV.d.cts} +19 -10
  19. package/dist/{index-g6iQyYci.d.cts → index-DLveQoOf.d.cts} +2 -2
  20. package/dist/{index-Dg10RnZO.d.ts → index-DMZG3kpo.d.ts} +19 -10
  21. package/dist/index.cjs.map +1 -1
  22. package/dist/index.d.cts +6 -6
  23. package/dist/index.d.ts +6 -6
  24. package/dist/index.js +4 -4
  25. package/dist/openapi/index.d.cts +2 -2
  26. package/dist/openapi/index.d.ts +2 -2
  27. package/dist/{registry-6VT5RPeO.d.cts → registry-DSTThhKf.d.cts} +1 -1
  28. package/dist/{registry-B-sxJJN0.d.ts → registry-JjF5lcj4.d.ts} +1 -1
  29. package/dist/test/index.cjs +1120 -4
  30. package/dist/test/index.cjs.map +1 -1
  31. package/dist/test/index.d.cts +1 -1
  32. package/dist/test/index.d.ts +1 -1
  33. package/dist/test/index.js +1121 -5
  34. package/dist/test/index.js.map +1 -1
  35. package/docs/README.md +123 -79
  36. package/docs/auth.md +11 -7
  37. package/docs/background.md +16 -7
  38. package/docs/database.md +22 -18
  39. package/docs/endpoints.md +26 -20
  40. package/docs/errors.md +2 -3
  41. package/docs/events.md +63 -29
  42. package/docs/getting-started.md +8 -7
  43. package/docs/llms-full.txt +399 -232
  44. package/docs/migrations.md +6 -5
  45. package/docs/routing.md +34 -11
  46. package/docs/schema.md +61 -36
  47. package/docs/services.md +30 -13
  48. package/package.json +2 -1
  49. package/stack-images.json +24 -0
  50. /package/dist/{chunk-DCDHAKF3.js.map → chunk-IKDONZ5D.js.map} +0 -0
  51. /package/dist/{chunk-QMFOL2K6.js.map → chunk-TVCCR6SO.js.map} +0 -0
  52. /package/dist/{chunk-N54QZER3.js.map → chunk-WWUG2QXF.js.map} +0 -0
  53. /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`. Controller files live under `controllers/` and are
6
- mounted automatically (see [routing.md](./routing.md)). Request input + context
7
- are injected into the method via **parameter decorators** (`@Body`/`@QueryParams`/
8
- `@Param`/`@User`/…), each piece direct no `req` god-object. Services are reached via the
9
- imported singletons (see [services.md](./services.md)).
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
- // controllers/rooms.controller.ts
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 { roomService } from "../services/room.service.js";
18
- import { CreateRoomBody } from "../models/rooms/create.js";
19
- import type { RoomSchema } from "../models/rooms/shared.js"; // the return TYPE names the 200 schema
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 default class RoomsController {
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 roomService.create(user.id, body);
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 roomService.get(id);
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 build with
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 are
49
- `async` and return `Promise<RoomSchema>`. (A pure method that returns a literal
50
- with no `await` may stay synchronous.)
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
- async search(@Body(SearchTodosBody) body: SearchTodosBody): Promise<TodoSchema[]> {
77
- return todoService.search(body);
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 default class PublicController {
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 `services/` class. No `req`, no
11
- per-route error map: the runtime catches any thrown error class and emits the
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 use the **singleton model** the same
4
- imported service singletons as endpoints (`import { Database, Log } from
5
- "@palbase/backend"`). They do **not** receive a `req`. A second `meta` argument
6
- carries the non-service data (`env`, `environmentId`; webhooks also
7
- get `requestId`).
3
+ Like jobs, `hooks/` and `webhooks/` stay at the ROOTneither 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, and document events. Files live under `hooks/`. Builders
12
- are imported from `@palbase/backend`: `auth`, `storage`, `documents`.
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 { auth, Database, Log } from "@palbase/backend";
17
-
18
- export const onUserCreated = auth.onUserCreated(async (event, meta) => {
19
- Log.info(`new user: ${event.user.email}`);
20
- await Database.$insert("profiles", {
21
- user_id: event.user.id,
22
- email: event.user.email,
23
- });
24
- });
25
-
26
- export const onSignIn = auth.onSignIn(async (event, meta) => {
27
- Log.info(`sign in: ${event.user.email} via ${event.provider}`);
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
- `meta` shape: `{ env, environmentId }`. Environment variables are in
32
- `meta.env`; services come from the imported singletons.
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
- Available hook builders: `auth.onUserCreated`, `auth.onSignIn`, `auth.onSignOut`,
35
- `auth.onPasswordReset`, `storage.onFileUploaded`, `storage.onFileDeleted`,
36
- `documents.onDocumentCreated`, `documents.onDocumentUpdated`,
37
- `documents.onDocumentDeleted`.
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/` the URL is `POST /webhooks/<file-name>` (e.g. `webhooks/stripe.ts`
43
- → `POST /webhooks/stripe`); there is no `path` option.
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
- @Webhook({ provider: "stripe", secret: { env: "STRIPE_WEBHOOK_SECRET" } }) // signing secret resolved from env
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) {
@@ -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": ["controllers/**/*.ts", "models/**/*.ts", "services/**/*.ts", "db/**/*.ts", "*.d.ts"]
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 `controllers/` exactly as the deploy does and runs the deploy's
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. Declare the schemas in `models/`:
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
- // models/hello/greet.ts
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 `controllers/hello.controller.ts`:
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 "../models/hello/greet.js";
79
- import type { HelloResponse } from "../models/hello/greet.js"; // the return TYPE names the 200 schema
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 {