@palbase/backend 24.3.0 → 25.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.
- package/dist/bin/palbase-backend.cjs +101 -60
- package/dist/bin/palbase-backend.cjs.map +1 -1
- package/dist/bin/palbase-backend.js +17 -13
- package/dist/bin/palbase-backend.js.map +1 -1
- package/dist/{chunk-EIXCY4SS.js → chunk-43A3KGWL.js} +80 -49
- package/dist/chunk-43A3KGWL.js.map +1 -0
- package/dist/{chunk-ERDL5VAE.js → chunk-5CMLOAEF.js} +2 -2
- package/dist/chunk-OEQBHE2Z.js +825 -0
- package/dist/chunk-OEQBHE2Z.js.map +1 -0
- package/dist/{chunk-7Z6MGMXQ.js → chunk-XJ2RSHEU.js} +11 -5
- package/dist/chunk-XJ2RSHEU.js.map +1 -0
- package/dist/{chunk-UWSYTUGM.js → chunk-ZQRWW37O.js} +44 -1
- package/dist/chunk-ZQRWW37O.js.map +1 -0
- package/dist/db/env.cjs.map +1 -1
- package/dist/db/env.d.cts +29 -13
- package/dist/db/env.d.ts +29 -13
- package/dist/db/index.cjs +212 -111
- 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 +11 -1
- package/dist/engine/index.cjs +87 -50
- package/dist/engine/index.cjs.map +1 -1
- package/dist/engine/index.d.cts +2 -2
- package/dist/engine/index.d.ts +2 -2
- package/dist/engine/index.js +3 -3
- package/dist/{index-DEneI8Mn.d.ts → index-BF1f0DfA.d.ts} +5 -2
- package/dist/{index-C-ALG22n.d.cts → index-CoaDN9dL.d.cts} +5 -2
- package/dist/{index-BTMYod_l.d.ts → index-Ct1iiB4N.d.ts} +203 -61
- package/dist/{index-BLAbr9ZH.d.cts → index-CwaWRhyc.d.cts} +203 -61
- package/dist/index.cjs +550 -297
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +122 -20
- package/dist/index.d.ts +122 -20
- package/dist/index.js +164 -217
- package/dist/index.js.map +1 -1
- package/dist/openapi/index.cjs +100 -36
- package/dist/openapi/index.cjs.map +1 -1
- package/dist/openapi/index.js +59 -2
- package/dist/openapi/index.js.map +1 -1
- package/docs/README.md +64 -31
- package/docs/endpoints.md +25 -28
- package/docs/llms-full.txt +399 -153
- package/docs/schema.md +272 -91
- package/docs/services.md +39 -4
- package/package.json +1 -1
- package/template/AGENTS.md +119 -314
- package/template/CLAUDE.md +13 -0
- package/template/controllers/notes.controller.ts +6 -13
- package/template/db/public.ts +38 -0
- package/template/models/notes/create.ts +38 -0
- package/template/package.json +6 -3
- package/template/services/note.service.test.ts +45 -0
- package/template/services/note.service.ts +2 -2
- package/dist/chunk-7Z6MGMXQ.js.map +0 -1
- package/dist/chunk-D5CQES25.js +0 -556
- package/dist/chunk-D5CQES25.js.map +0 -1
- package/dist/chunk-EIXCY4SS.js.map +0 -1
- package/dist/chunk-UWSYTUGM.js.map +0 -1
- package/template/db/schema.ts +0 -35
- /package/dist/{chunk-ERDL5VAE.js.map → chunk-5CMLOAEF.js.map} +0 -0
package/docs/README.md
CHANGED
|
@@ -23,17 +23,26 @@ services/<name>.service.ts # plain class + singleton — the real logic
|
|
|
23
23
|
db/schema.ts # config-as-code Postgres schema (tables, columns, RLS) — auto-migrated on deploy
|
|
24
24
|
```
|
|
25
25
|
|
|
26
|
-
The four folders above are the daily surface.
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
26
|
+
The four folders above are the daily surface. Three more are discovered by name,
|
|
27
|
+
one class per file, `export default` required: `jobs/` (background —
|
|
28
|
+
[background.md](./background.md)), `webhooks/` and `hooks/` (events —
|
|
29
|
+
[events.md](./events.md)).
|
|
30
|
+
|
|
31
|
+
There is no `resources/`, no `seeds/` and no working `middleware/`. `middleware/`
|
|
32
|
+
is discovered by nothing and the engine has no middleware pipeline: code written
|
|
33
|
+
against it deploys, never runs, and nothing says so. Put cross-cutting work in a
|
|
34
|
+
service the controllers call.
|
|
30
35
|
|
|
31
36
|
### The 7 rules (checklist)
|
|
32
37
|
|
|
33
|
-
1.
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
38
|
+
1. **A controller needs NO export at all.** `@Controller` records the class into a
|
|
39
|
+
`globalThis` registry as it decorates it, and the bundler imports the file for
|
|
40
|
+
that side effect alone — the shipped scaffold's own `HealthController` and
|
|
41
|
+
`NotesController` are not exported. Exporting is harmless and reads well, so
|
|
42
|
+
these examples do it; it is not a requirement. (`export default` **is**
|
|
43
|
+
required for `jobs/`, `webhooks/`, `hooks/` and `db/public.ts`, one class per
|
|
44
|
+
file.) What IS fatal is a `@Controller` class that collected zero routes —
|
|
45
|
+
usually `experimentalDecorators` missing from `tsconfig.json`.
|
|
37
46
|
2. **Methods that call a service are `async` and return `Promise<T>`.** Services
|
|
38
47
|
`await Database`, so they return promises; a sync return type on an async body
|
|
39
48
|
is a tsc error. Annotate `: Promise<TodoSchema>`, not `: TodoSchema`, whenever
|
|
@@ -62,7 +71,7 @@ formatter, anything shared), `seeds/` (seed data), `jobs/` (background — [back
|
|
|
62
71
|
|
|
63
72
|
> **Never** emit `defineController`, `defineHandler`, `defineEndpoint`, `route.get(...)`,
|
|
64
73
|
> `req.input`, `req.params`, or `req.errors` — those are the removed legacy model
|
|
65
|
-
> and will not compile against `@palbase/backend`
|
|
74
|
+
> and will not compile against `@palbase/backend` 25.
|
|
66
75
|
|
|
67
76
|
### Complete CRUD example (copy-pasteable, compiles)
|
|
68
77
|
|
|
@@ -89,25 +98,44 @@ export type CreateTodoBody = z.infer<typeof CreateTodoBody>;
|
|
|
89
98
|
import { Database, NotFound } from "@palbase/backend";
|
|
90
99
|
import type { TodoSchema } from "../models/todos/shared.js";
|
|
91
100
|
|
|
101
|
+
/** The typed surface of ONE table. Naming it keeps the seam one table wide:
|
|
102
|
+
* a test fake implements five methods, not the whole `Database`. */
|
|
103
|
+
type TodosTable = typeof Database.tables.todos;
|
|
104
|
+
|
|
92
105
|
export class TodoService {
|
|
106
|
+
private readonly todos: TodosTable;
|
|
107
|
+
|
|
108
|
+
// THE SEAM IS THE CONSTRUCTOR: the class is handed the table rather than
|
|
109
|
+
// reaching for the singleton, so a test constructs it with a stand-in and
|
|
110
|
+
// never needs a database. Assign in the BODY — a parameter property
|
|
111
|
+
// (`constructor(private todos: …)`) is refused by Node's type-stripping test
|
|
112
|
+
// runner, and refused for the whole FILE.
|
|
113
|
+
constructor(todos: TodosTable) {
|
|
114
|
+
this.todos = todos;
|
|
115
|
+
}
|
|
116
|
+
|
|
93
117
|
list(userId: string): Promise<TodoSchema[]> {
|
|
94
|
-
return
|
|
118
|
+
return this.todos.findMany({ user_id: userId });
|
|
95
119
|
}
|
|
96
120
|
create(userId: string, title: string): Promise<TodoSchema> {
|
|
97
|
-
return
|
|
121
|
+
return this.todos.insert({ user_id: userId, title });
|
|
98
122
|
}
|
|
99
123
|
async get(userId: string, id: string): Promise<TodoSchema> {
|
|
100
|
-
const t = await
|
|
124
|
+
const t = await this.todos.findById(id);
|
|
101
125
|
if (!t || t.user_id !== userId) throw new NotFound("No todo with that id");
|
|
102
126
|
return t;
|
|
103
127
|
}
|
|
104
128
|
async remove(userId: string, id: string): Promise<void> {
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
await Database.tables.todos.delete(id);
|
|
129
|
+
await this.get(userId, id);
|
|
130
|
+
await this.todos.delete(id);
|
|
108
131
|
}
|
|
109
132
|
}
|
|
110
|
-
|
|
133
|
+
|
|
134
|
+
/** The wired instance. Controllers import THIS, never the class. It is also the
|
|
135
|
+
* ONLY supported way to hold a dependency: a controller, job, hook or webhook is
|
|
136
|
+
* constructed with no arguments, and one that declares a constructor parameter
|
|
137
|
+
* is refused at build with the class named. */
|
|
138
|
+
export const todoService = new TodoService(Database.tables.todos);
|
|
111
139
|
```
|
|
112
140
|
|
|
113
141
|
```ts
|
|
@@ -119,31 +147,33 @@ import { TodoSchema } from "../models/todos/shared.js";
|
|
|
119
147
|
import { CreateTodoBody } from "../models/todos/create.js";
|
|
120
148
|
|
|
121
149
|
@Controller("/todos") // secure-by-default; { auth: false } opts the whole controller out
|
|
122
|
-
export class TodosController {
|
|
123
|
-
|
|
150
|
+
export default class TodosController {
|
|
151
|
+
// The service arrives as an IMPORTED SINGLETON, not a constructor parameter:
|
|
152
|
+
// the runtime constructs this class with no arguments, and one that declares a
|
|
153
|
+
// parameter is refused at build with the class named.
|
|
124
154
|
|
|
125
155
|
@Get("") // GET /todos → operationId todos.list
|
|
126
156
|
async list(@User() user: UserT): Promise<TodoSchema[]> { // return type → 200 response schema
|
|
127
|
-
return
|
|
157
|
+
return todoService.list(user.id);
|
|
128
158
|
}
|
|
129
159
|
|
|
130
160
|
@Post("") // POST /todos → todos.create
|
|
131
161
|
async create(@Body(CreateTodoBody) body: CreateTodoBody, @User() user: UserT): Promise<TodoSchema> {
|
|
132
|
-
return
|
|
162
|
+
return todoService.create(user.id, body.title);
|
|
133
163
|
}
|
|
134
164
|
|
|
135
165
|
@Get("/{id}") // GET /todos/{id} → todos.get
|
|
136
166
|
async get(@Param("id") id: string, @User() user: UserT): Promise<TodoSchema> {
|
|
137
|
-
return
|
|
167
|
+
return todoService.get(user.id, id);
|
|
138
168
|
}
|
|
139
169
|
|
|
140
170
|
@Delete("/{id}") // DELETE /todos/{id} → todos.remove; no body → : Promise<void>
|
|
141
171
|
async remove(@Param("id") id: string, @User() user: UserT): Promise<void> {
|
|
142
|
-
await
|
|
172
|
+
await todoService.remove(user.id, id);
|
|
143
173
|
}
|
|
144
174
|
}
|
|
145
|
-
|
|
146
|
-
export default
|
|
175
|
+
// No export is needed at all — @Controller registered the class as it decorated
|
|
176
|
+
// it. `export default` here is style, not a requirement.
|
|
147
177
|
```
|
|
148
178
|
|
|
149
179
|
```ts
|
|
@@ -230,7 +260,10 @@ The **only difference** is the trigger argument:
|
|
|
230
260
|
| **Jobs** (`jobs/**`) | `(meta)` | `JobMeta` |
|
|
231
261
|
| **Hooks** (`hooks/**`) | `(event, meta)` | typed event + `HookMeta` |
|
|
232
262
|
| **Webhooks** (`webhooks/**`) | `(event, meta)` | typed event + `WebhookMeta` |
|
|
233
|
-
|
|
263
|
+
|
|
264
|
+
`defineMiddleware` is still exported and takes `(ctx, next)`, but **no bundler
|
|
265
|
+
reads a `middleware/` directory and the engine never calls one** — there is no
|
|
266
|
+
`ctx` anywhere on a path that runs. Every handler above imports its services.
|
|
234
267
|
|
|
235
268
|
`meta` carries non-service data: `env` (Environment variables),
|
|
236
269
|
`environmentId`, and for webhooks `requestId`. Services always come from
|
|
@@ -247,16 +280,16 @@ my-backend/
|
|
|
247
280
|
├── models/<ctrl>/<ep>.ts # zod schemas, folder per controller, file per endpoint
|
|
248
281
|
│ └── hello/greet.ts # GreetQuery + HelloResponse (zod value + z.infer type)
|
|
249
282
|
├── services/ # plain classes/singletons your controllers call
|
|
250
|
-
├── db/
|
|
251
|
-
├── db/migrations/ # explicit SQL migrations for type changes (optional)
|
|
252
|
-
├── resources/ # external connections, set up once at boot (optional)
|
|
253
|
-
├── seeds/ # seed data (optional)
|
|
283
|
+
├── db/public.ts # the database itself: tables, columns, RLS policies
|
|
254
284
|
├── jobs/ # cron-scheduled jobs (optional)
|
|
255
285
|
├── hooks/ # auth/storage/document event hooks (optional)
|
|
256
|
-
|
|
257
|
-
└── middleware/ # cross-cutting request middleware (optional)
|
|
286
|
+
└── webhooks/ # inbound provider webhooks (optional)
|
|
258
287
|
```
|
|
259
288
|
|
|
289
|
+
There is **no `db/migrations/`**. Nothing generates a migration file, nothing
|
|
290
|
+
commits one and nothing replays one: `db/schema.ts` is diffed against the live
|
|
291
|
+
database and applied — see [migrations.md](./migrations.md).
|
|
292
|
+
|
|
260
293
|
HTTP endpoints are **not** file-path routed. You author a class controller
|
|
261
294
|
(`@Controller("/base")` with `@Get`/`@Post`/… methods); putting it under
|
|
262
295
|
`controllers/` mounts it. See [routing.md](./routing.md).
|
package/docs/endpoints.md
CHANGED
|
@@ -12,35 +12,38 @@ imported singletons (see [services.md](./services.md)).
|
|
|
12
12
|
|
|
13
13
|
```ts
|
|
14
14
|
// controllers/rooms.controller.ts
|
|
15
|
-
import { Controller, Get, Post, Body, Param, User
|
|
15
|
+
import { Controller, Get, Post, Body, Param, User } from "@palbase/backend";
|
|
16
16
|
import type { UserT } from "@palbase/backend";
|
|
17
|
+
import { roomService } from "../services/room.service.js";
|
|
17
18
|
import { CreateRoomBody } from "../models/rooms/create.js";
|
|
18
19
|
import type { RoomSchema } from "../models/rooms/shared.js"; // the return TYPE names the 200 schema
|
|
19
20
|
|
|
21
|
+
// A controller does not import `Database`. Everything here is HTTP: validate the
|
|
22
|
+
// body through a named schema, name the 200 shape as the return type, delegate.
|
|
23
|
+
// Which rows, whose, in what order is the service's job — and the service is the
|
|
24
|
+
// thing worth testing, because it is the thing that can be wrong.
|
|
20
25
|
@Controller("/rooms")
|
|
21
26
|
export default class RoomsController {
|
|
22
27
|
@Post("")
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
return { id: room.id as string, name: room.name as string, capacity: (room.capacity as number) ?? null };
|
|
28
|
+
create(@Body(CreateRoomBody) body: CreateRoomBody, @User() user: UserT): Promise<RoomSchema> {
|
|
29
|
+
return roomService.create(user.id, body);
|
|
26
30
|
}
|
|
27
31
|
|
|
28
32
|
@Get("/{id}")
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
if (!room) throw new NotFound("Room does not exist", "room_not_found");
|
|
32
|
-
return { id: room.id as string, name: room.name as string, capacity: (room.capacity as number) ?? null };
|
|
33
|
+
getOne(@Param("id") id: string): Promise<RoomSchema> {
|
|
34
|
+
return roomService.get(id);
|
|
33
35
|
}
|
|
34
36
|
}
|
|
35
37
|
```
|
|
36
38
|
|
|
37
39
|
**Two non-negotiables** (the most common codegen mistakes):
|
|
38
40
|
|
|
39
|
-
1.
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
41
|
+
1. **The controller does not touch the database.** It delegates to a service, as
|
|
42
|
+
above. A method here that reaches for `Database` has moved the logic into the
|
|
43
|
+
layer that is hardest to test. (An export is not required at all: `@Controller`
|
|
44
|
+
records the class as it decorates it, and importing the file IS the
|
|
45
|
+
registration — the shipped scaffold's own controllers are not exported.
|
|
46
|
+
`export default` here is style.)
|
|
44
47
|
2. **A method that awaits a service is `async` + `Promise<T>`.** `Database`
|
|
45
48
|
returns promises, so a body that `await`s it cannot have a sync return type
|
|
46
49
|
(`: RoomSchema` on an `async` body is a `tsc` error). Both methods above are
|
|
@@ -150,20 +153,14 @@ throw new PalError(418, "teapot", "custom"); // → custom status/code
|
|
|
150
153
|
|
|
151
154
|
See [errors.md](./errors.md) for the full set + the wire envelope shape.
|
|
152
155
|
|
|
153
|
-
##
|
|
156
|
+
## There is no middleware
|
|
154
157
|
|
|
155
|
-
|
|
158
|
+
`defineMiddleware(async (ctx, next) => { … })` is still exported and still
|
|
159
|
+
type-checks, but **nothing mounts a `middleware/` directory and the engine has no
|
|
160
|
+
middleware pipeline** — a handler written against it deploys, never runs, and
|
|
161
|
+
nothing reports it. There is no `ctx` object anywhere on a path that executes, so
|
|
162
|
+
treat the export as a leftover rather than a seam.
|
|
156
163
|
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
export default defineMiddleware(async (ctx, next) => {
|
|
162
|
-
ctx.log.info(`start ${ctx.requestId}`);
|
|
163
|
-
await next();
|
|
164
|
-
ctx.log.info(`done ${ctx.requestId}`);
|
|
165
|
-
});
|
|
166
|
-
```
|
|
167
|
-
|
|
168
|
-
The middleware handler receives `(ctx, next)` — call `await next()` to run the
|
|
169
|
-
rest of the chain (other middleware, then the endpoint method).
|
|
164
|
+
Cross-cutting work goes in a service the controllers call, and the route concerns
|
|
165
|
+
that used to live in a wrapper are route options instead: `auth` on `@Controller`
|
|
166
|
+
or the method decorator, and `rateLimit: { max, window }` per route.
|