@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/migrations.md
CHANGED
|
@@ -1,98 +1,100 @@
|
|
|
1
1
|
# Migrations
|
|
2
2
|
|
|
3
|
-
`db/schema.ts` is the single source of truth for your Postgres schema.
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
## Two kinds of change
|
|
9
|
-
|
|
10
|
-
### 1. Additive — auto-applied, no migration file
|
|
11
|
-
|
|
12
|
-
A new table, or a new **nullable** or **defaulted** column, is additive: the
|
|
13
|
-
deploy applies it automatically (`CREATE TABLE` / `ADD COLUMN`) with no manual
|
|
14
|
-
step and no backfill risk. Just edit `db/schema.ts` and deploy.
|
|
15
|
-
|
|
16
|
-
```ts
|
|
17
|
-
// before
|
|
18
|
-
todos: {
|
|
19
|
-
id: uuid().primaryKey().defaultRandom(),
|
|
20
|
-
title: text().notNull(),
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
// after — additive: `notes` (nullable) + `priority` (defaulted) auto-apply on deploy
|
|
24
|
-
todos: {
|
|
25
|
-
id: uuid().primaryKey().defaultRandom(),
|
|
26
|
-
title: text().notNull(),
|
|
27
|
-
notes: text().nullable(),
|
|
28
|
-
priority: text().nullable().default("normal"),
|
|
29
|
-
}
|
|
30
|
-
```
|
|
3
|
+
`db/schema.ts` is the single source of truth for your Postgres schema. You change
|
|
4
|
+
the schema by editing that file, then generating a **migration** from the diff
|
|
5
|
+
with `palbase db diff`. Every schema change — additive or destructive — flows
|
|
6
|
+
through a reviewable migration file committed to git. The deploy applies the
|
|
7
|
+
migrations in `db/migrations/`; nothing is auto-applied behind your back.
|
|
31
8
|
|
|
32
|
-
|
|
33
|
-
> that already has rows (there is nothing to put in the existing rows). Make it
|
|
34
|
-
> `.nullable()`, give it a `.default(...)`, or apply it as an explicit migration
|
|
35
|
-
> (add nullable → backfill → set NOT NULL).
|
|
9
|
+
## The workflow
|
|
36
10
|
|
|
37
|
-
|
|
11
|
+
```bash
|
|
12
|
+
# 1. Edit db/schema.ts (add a column, a table, a policy, …)
|
|
38
13
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
Write the SQL yourself in `db/migrations/`:
|
|
14
|
+
# 2. Generate the migration from the diff (declared schema vs the live branch)
|
|
15
|
+
palbase db diff -f add_priority
|
|
16
|
+
# → writes db/migrations/<timestamp>_add_priority.sql
|
|
43
17
|
|
|
18
|
+
# 3. Review the generated SQL (especially destructive changes — see below),
|
|
19
|
+
# then commit + push. git push deploys; the migration runs on deploy.
|
|
20
|
+
git add db/migrations && git commit -m "add priority column" && git push
|
|
44
21
|
```
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
22
|
+
|
|
23
|
+
`palbase db diff` introspects your **live branch database** (there is no local
|
|
24
|
+
database — every branch runs on the server), diffs it against `db/schema.ts`, and
|
|
25
|
+
writes one migration SQL file. If the schema is already in sync it writes nothing
|
|
26
|
+
and tells you so.
|
|
27
|
+
|
|
28
|
+
## Additive vs destructive
|
|
29
|
+
|
|
30
|
+
The generated SQL labels what it does. An additive change (new table, new column)
|
|
31
|
+
is plain DDL:
|
|
32
|
+
|
|
33
|
+
```sql
|
|
34
|
+
-- palbase db diff: add_priority
|
|
35
|
+
-- generated 20260605T142233
|
|
36
|
+
|
|
37
|
+
ALTER TABLE todos ADD COLUMN IF NOT EXISTS priority text;
|
|
48
38
|
```
|
|
49
39
|
|
|
40
|
+
A **destructive** change (dropping a column or table — losing data) is generated
|
|
41
|
+
with a clear warning comment, and `palbase db diff` prints a warning. Review it
|
|
42
|
+
before committing:
|
|
43
|
+
|
|
50
44
|
```sql
|
|
51
|
-
--
|
|
52
|
-
ALTER TABLE todos
|
|
45
|
+
-- DESTRUCTIVE: dropping todos.notes loses its data
|
|
46
|
+
ALTER TABLE todos DROP COLUMN notes;
|
|
53
47
|
```
|
|
54
48
|
|
|
49
|
+
A **column type change** is emitted as a commented stub — auto-migration never
|
|
50
|
+
alters types, so you write the real `ALTER ... TYPE` with whatever `USING` cast
|
|
51
|
+
and backfill your data needs:
|
|
52
|
+
|
|
55
53
|
```sql
|
|
56
|
-
--
|
|
57
|
-
ALTER TABLE todos ALTER COLUMN
|
|
54
|
+
-- TYPE CHANGE: todos.priority text -> integer (review; auto-migrate does not ALTER types)
|
|
55
|
+
-- ALTER TABLE todos ALTER COLUMN priority TYPE integer;
|
|
58
56
|
```
|
|
59
57
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
existing data gets there**. Keep the two in sync — after the migration lands,
|
|
64
|
-
`schema.ts` should already reflect the new column type.
|
|
58
|
+
## The drift gate
|
|
59
|
+
|
|
60
|
+
You can't push a schema change without its migration:
|
|
65
61
|
|
|
66
|
-
|
|
62
|
+
- **`palbase db check`** exits non-zero when `db/schema.ts` declares something the
|
|
63
|
+
database lacks (i.e. you edited the schema but didn't run `palbase db diff`).
|
|
64
|
+
- The scaffold installs a **git pre-push hook** that runs `palbase db check`, so a
|
|
65
|
+
plain `git push` is **blocked** until you generate + commit the migration.
|
|
66
|
+
(Bypass with `git push --no-verify` — but the deploy-time gate still rejects it.)
|
|
67
|
+
- On deploy, after migrations run, Palbase asserts `db/schema.ts` matches the live
|
|
68
|
+
database. Any unresolved drift **fails the deploy** and keeps the previous
|
|
69
|
+
version live — a broken schema never goes out silently.
|
|
67
70
|
|
|
68
|
-
|
|
71
|
+
## `palbase serve` uses the deployed database
|
|
69
72
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
73
|
+
`palbase serve` runs your controllers locally but proxies `Database` and `ctx.*`
|
|
74
|
+
to the **deployed** branch — it does not spin up a local Postgres. So a schema
|
|
75
|
+
change in `db/schema.ts` doesn't exist in the database until you generate the
|
|
76
|
+
migration and push. Run `palbase gen-types` (or just `palbase serve`, which
|
|
77
|
+
regenerates on change) after editing the schema to refresh `palbase-env.d.ts` so
|
|
78
|
+
`Database.tables.<name>` is fully typed in your services.
|
|
75
79
|
|
|
76
|
-
|
|
77
|
-
dropping production data. A blocked deploy is a prompt to write the migration,
|
|
78
|
-
not a failure to work around.
|
|
80
|
+
## Hand-written migrations
|
|
79
81
|
|
|
80
|
-
|
|
82
|
+
`db/migrations/*.sql` is plain SQL applied in filename order and tracked in
|
|
83
|
+
`schema_migrations` (idempotent — a migration runs once). `palbase db diff`
|
|
84
|
+
generates them for you, but you can also hand-write one for anything the diff
|
|
85
|
+
can't express (a data backfill, a complex type change with a `USING` cast, a
|
|
86
|
+
trigger). Keep `db/schema.ts` as the declared end-state so the drift gate passes.
|
|
81
87
|
|
|
82
|
-
|
|
83
|
-
`ctx.*` to the **deployed** branch — it does **not** spin up a local Postgres or
|
|
84
|
-
apply migrations locally. So when your local `db/schema.ts` or `db/migrations/`
|
|
85
|
-
is ahead of what's deployed, serve prints a note: new tables/columns won't exist
|
|
86
|
-
until you push. Deploy to apply them.
|
|
88
|
+
## Adding a NOT NULL column to a table with rows
|
|
87
89
|
|
|
88
|
-
|
|
90
|
+
There's nothing to put in existing rows, so do it in two migrations: first add it
|
|
91
|
+
nullable (or with a default) and backfill, then a follow-up `ALTER ... SET NOT
|
|
92
|
+
NULL`. `db/schema.ts` describes the end state; the migrations describe how
|
|
93
|
+
existing data gets there.
|
|
89
94
|
|
|
90
|
-
|
|
91
|
-
2. **Additive** change? → `git push`. It auto-migrates on deploy.
|
|
92
|
-
3. **Type change / rename / drop?** → add `db/migrations/NNN_*.up.sql` (+
|
|
93
|
-
`.down.sql`), then `git push`. The runner applies it; without it the
|
|
94
|
-
drift-gate blocks the deploy.
|
|
95
|
-
4. `palbase serve` warns locally until the change is deployed.
|
|
95
|
+
## Row-Level Security
|
|
96
96
|
|
|
97
|
-
|
|
97
|
+
Add `rls: true` + `policies: [policy(...)]` to a table in `db/schema.ts`; the
|
|
98
|
+
generated migration emits the `ENABLE ROW LEVEL SECURITY` + `CREATE POLICY` DDL.
|
|
99
|
+
See [schema.md](./schema.md) for the column builders, the policy DSL, and typed
|
|
98
100
|
`Database.tables.*` access.
|
package/docs/routing.md
CHANGED
|
@@ -19,22 +19,20 @@ method declares its verb + subpath; the real work lives in a `services/` class
|
|
|
19
19
|
|
|
20
20
|
```ts
|
|
21
21
|
// controllers/places.controller.ts
|
|
22
|
-
import { Controller, Get, Post,
|
|
22
|
+
import { Controller, Get, Post, Body, User } from "@palbase/backend";
|
|
23
23
|
import type { UserT } from "@palbase/backend";
|
|
24
24
|
import { placeService } from "../services/place.service.js";
|
|
25
25
|
import { ImportNearbyBody } from "../models/places/import.js";
|
|
26
|
-
import { PlaceSchema } from "../models/places/shared.js";
|
|
26
|
+
import type { PlaceSchema } from "../models/places/shared.js"; // the return TYPE names the 200 schema
|
|
27
27
|
|
|
28
28
|
@Controller("/places")
|
|
29
29
|
export default class PlacesController {
|
|
30
30
|
@Post("/import")
|
|
31
|
-
@Returns(PlaceSchema)
|
|
32
31
|
importNearby(@Body(ImportNearbyBody) body: ImportNearbyBody, @User() user: UserT): PlaceSchema {
|
|
33
32
|
return placeService.importNearby(body.lat, body.lng, user.id);
|
|
34
33
|
}
|
|
35
34
|
|
|
36
35
|
@Get("/favorites", { auth: false })
|
|
37
|
-
@Returns(z.array(PlaceSchema))
|
|
38
36
|
listFavorites(): PlaceSchema[] {
|
|
39
37
|
return placeService.listFavorites();
|
|
40
38
|
}
|
|
@@ -52,8 +50,10 @@ Rules:
|
|
|
52
50
|
- A `{segment}` in a path becomes a path param, injected via `@Param("segment")`.
|
|
53
51
|
- Input is declared with the parameter decorators — `@Body(schema)`,
|
|
54
52
|
`@Query(schema)`, `@Param("id")`, `@Headers(schema?)`. The success response is
|
|
55
|
-
the method's RETURN TYPE
|
|
56
|
-
|
|
53
|
+
the method's RETURN TYPE (`: PlaceSchema` or `: z.infer<typeof PlaceSchema>`):
|
|
54
|
+
codegen + the runtime read that named type to drive the OpenAPI 200 response.
|
|
55
|
+
A body route with no named return type is a build error — annotate `: void`
|
|
56
|
+
(or `: Promise<void>`) for no body.
|
|
57
57
|
- The operationId is derived FLAT from method + full path (`postPlacesImport`),
|
|
58
58
|
not from the method name. Change `@Post` → `@Put` — no file rename.
|
|
59
59
|
|
package/docs/schema.md
CHANGED
|
@@ -72,7 +72,7 @@ You do **not** wire anything per endpoint. Saving `db/schema.ts` regenerates
|
|
|
72
72
|
of the schema, no generic, no cast:
|
|
73
73
|
|
|
74
74
|
```ts
|
|
75
|
-
import { Controller, Post,
|
|
75
|
+
import { Controller, Post, Body, Database, z } from "@palbase/backend";
|
|
76
76
|
|
|
77
77
|
const CreateRoomBody = z.object({ name: z.string() });
|
|
78
78
|
const RoomOut = z.object({ id: z.string(), name: z.string() });
|
|
@@ -80,7 +80,8 @@ const RoomOut = z.object({ id: z.string(), name: z.string() });
|
|
|
80
80
|
@Controller("/rooms")
|
|
81
81
|
export default class RoomsController {
|
|
82
82
|
@Post("")
|
|
83
|
-
|
|
83
|
+
// The return type names the 200 schema — `z.infer<typeof RoomOut>` works
|
|
84
|
+
// inline, no separate `export type` needed.
|
|
84
85
|
async create(@Body(CreateRoomBody) body: z.infer<typeof CreateRoomBody>): Promise<z.infer<typeof RoomOut>> {
|
|
85
86
|
const room = await Database.tables.rooms.insert({ name: body.name });
|
|
86
87
|
return { id: room.id, name: room.name }; // room.id: string ✓
|
package/docs/services.md
CHANGED
|
@@ -90,7 +90,7 @@ await Notifications.sms.send({ /* PalbaseSmsSendParams */ });
|
|
|
90
90
|
## Flags
|
|
91
91
|
|
|
92
92
|
```ts
|
|
93
|
-
import { Controller, Get,
|
|
93
|
+
import { Controller, Get, User, Flags, z } from "@palbase/backend";
|
|
94
94
|
import type { UserT } from "@palbase/backend";
|
|
95
95
|
|
|
96
96
|
const FlagsOut = z.object({ enabled: z.boolean() });
|
|
@@ -98,8 +98,7 @@ const FlagsOut = z.object({ enabled: z.boolean() });
|
|
|
98
98
|
@Controller("/checkout")
|
|
99
99
|
export default class CheckoutController {
|
|
100
100
|
@Get("/flags") // auth omitted → required → user is non-null
|
|
101
|
-
@
|
|
102
|
-
async flags(@User() user: UserT): Promise<z.infer<typeof FlagsOut>> {
|
|
101
|
+
async flags(@User() user: UserT): Promise<z.infer<typeof FlagsOut>> { // return type names the 200 schema
|
|
103
102
|
const { data: enabled } = await Flags.isEnabled("new-checkout", { userId: user.id });
|
|
104
103
|
const { data: variant } = await Flags.getVariant("button-color", { userId: user.id });
|
|
105
104
|
return { enabled: enabled ?? false };
|
package/package.json
CHANGED