@palbase/backend 23.1.0 → 24.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.
- package/dist/bin/palbase-backend.cjs +400 -47
- package/dist/bin/palbase-backend.cjs.map +1 -1
- package/dist/bin/palbase-backend.js +4 -5
- package/dist/bin/palbase-backend.js.map +1 -1
- package/dist/{chunk-OHALWEOG.js → chunk-7Z6MGMXQ.js} +57 -2
- package/dist/chunk-7Z6MGMXQ.js.map +1 -0
- package/dist/{chunk-RCLNBJCM.js → chunk-H3JAISUY.js} +136 -1
- package/dist/chunk-H3JAISUY.js.map +1 -0
- package/dist/{chunk-PY7YJDCT.js → chunk-LCL7TUAI.js} +32 -3
- package/dist/chunk-LCL7TUAI.js.map +1 -0
- package/dist/{chunk-NS5V43YQ.js → chunk-P2Q27SGP.js} +19 -3
- package/dist/chunk-P2Q27SGP.js.map +1 -0
- package/dist/{chunk-R3KN6RHD.js → chunk-T5IOSOE5.js} +7 -2
- package/dist/chunk-T5IOSOE5.js.map +1 -0
- package/dist/{chunk-M5MCBWJI.js → chunk-YSQBC2VL.js} +275 -31
- package/dist/chunk-YSQBC2VL.js.map +1 -0
- package/dist/db/index.cjs +48 -3
- 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 +2 -2
- package/dist/{endpoint-CVWXh6oG.d.ts → endpoint-0_DGBajf.d.ts} +100 -3
- package/dist/{endpoint-c9h5jriX.d.cts → endpoint-CcQ1a36a.d.cts} +100 -3
- package/dist/engine/index.cjs +389 -34
- package/dist/engine/index.cjs.map +1 -1
- package/dist/engine/index.d.cts +4 -4
- package/dist/engine/index.d.ts +4 -4
- package/dist/engine/index.js +4 -4
- package/dist/{index-CwAJ7HEe.d.ts → index-BOS_rFBO.d.ts} +125 -21
- package/dist/{index-CxeQSfJP.d.cts → index-C84bLgeO.d.cts} +134 -9
- package/dist/{index-By8Dle5U.d.cts → index-MoQ31B6M.d.cts} +125 -21
- package/dist/{index-BZrJXnVh.d.ts → index-dJNhDZ7j.d.ts} +134 -9
- package/dist/index.cjs +166 -5
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +54 -12
- package/dist/index.d.ts +54 -12
- package/dist/index.js +28 -11
- package/dist/index.js.map +1 -1
- package/dist/openapi/index.cjs +16 -1
- package/dist/openapi/index.cjs.map +1 -1
- package/dist/openapi/index.d.cts +6 -4
- package/dist/openapi/index.d.ts +6 -4
- package/dist/openapi/index.js +6 -7
- package/dist/openapi/index.js.map +1 -1
- package/dist/{registry-CqPK2Qby.d.cts → registry-1X-skBNu.d.cts} +1 -1
- package/dist/{registry-B3niOVYp.d.ts → registry-CEod_5sz.d.ts} +1 -1
- package/dist/test/index.cjs +27 -0
- 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 +27 -0
- package/dist/test/index.js.map +1 -1
- package/docs/README.md +4 -4
- package/docs/database.md +115 -11
- package/docs/getting-started.md +5 -4
- package/docs/llms-full.txt +385 -89
- package/docs/migrations.md +81 -59
- package/docs/schema.md +82 -2
- package/docs/services.md +98 -9
- package/package.json +2 -2
- package/template/AGENTS.md +121 -41
- package/template/controllers/notes.controller.ts +64 -0
- package/template/package.json +1 -1
- package/template/services/note.service.ts +74 -0
- package/template/tsconfig.json +11 -1
- package/dist/chunk-HQRJDARQ.js +0 -90
- package/dist/chunk-HQRJDARQ.js.map +0 -1
- package/dist/chunk-M5MCBWJI.js.map +0 -1
- package/dist/chunk-NS5V43YQ.js.map +0 -1
- package/dist/chunk-OHALWEOG.js.map +0 -1
- package/dist/chunk-PY7YJDCT.js.map +0 -1
- package/dist/chunk-R3KN6RHD.js.map +0 -1
- package/dist/chunk-RCLNBJCM.js.map +0 -1
package/docs/migrations.md
CHANGED
|
@@ -1,88 +1,109 @@
|
|
|
1
1
|
# Migrations
|
|
2
2
|
|
|
3
3
|
`db/schema.ts` is the single source of truth for your Postgres schema. You change
|
|
4
|
-
the schema by editing that file
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
4
|
+
the schema by editing that file — and **nothing generates a migration from it**.
|
|
5
|
+
There is no diff-to-file step: the change is computed against the database *as it
|
|
6
|
+
is right now*, by the stack that owns it, which is what lets it cover type
|
|
7
|
+
changes, policies and constraints that a text diff cannot see.
|
|
8
|
+
|
|
9
|
+
Two paths lead out of an edit, for two different moments:
|
|
10
|
+
|
|
11
|
+
- **On your machine** — `palbase db plan` and `palbase db apply` act on the local
|
|
12
|
+
stack (`palbase start`). Try the change, look at it, keep going.
|
|
13
|
+
- **Anywhere real** — `palbase push` carries the code AND the schema to the
|
|
14
|
+
selected Environment together, because a deploy that lands a handler against a
|
|
15
|
+
column that does not exist yet fails at the first request, in production.
|
|
16
|
+
|
|
17
|
+
`db/migrations/*.sql` still exists, and still runs on deploy, for the objects the
|
|
18
|
+
declaration cannot express — see [Hand-written migrations](#hand-written-migrations).
|
|
19
|
+
It is not where an ordinary column change goes any more.
|
|
8
20
|
|
|
9
21
|
## The workflow
|
|
10
22
|
|
|
11
23
|
```bash
|
|
12
24
|
# 1. Edit db/schema.ts (add a column, a table, a policy, …)
|
|
13
25
|
|
|
14
|
-
# 2.
|
|
15
|
-
palbase db
|
|
16
|
-
#
|
|
26
|
+
# 2. See what it would take. THIS APPLIES NOTHING.
|
|
27
|
+
palbase db plan
|
|
28
|
+
# ALTER TABLE todos ADD COLUMN priority text
|
|
29
|
+
# ⚠ drop todos.notes — 41908 value(s) in 41908 row(s)
|
|
30
|
+
#
|
|
31
|
+
# the ⚠ changes take data away — `palbase db apply --approve` runs them too
|
|
17
32
|
|
|
18
|
-
# 3.
|
|
19
|
-
|
|
20
|
-
|
|
33
|
+
# 3. Do it — one transaction, against the LOCAL database.
|
|
34
|
+
palbase db apply
|
|
35
|
+
|
|
36
|
+
# 4. Ship it. Code and schema travel together.
|
|
37
|
+
palbase push
|
|
21
38
|
```
|
|
22
39
|
|
|
23
|
-
`palbase db
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
40
|
+
`palbase db plan` hands `db/schema.ts` to the local stack, which computes the plan
|
|
41
|
+
against its own database and answers with what it would change. When there is
|
|
42
|
+
nothing to do it says `✓ the database matches db/schema.ts`. It writes nothing,
|
|
43
|
+
ever — neither a file nor a row — so it is safe to run at any moment, including
|
|
44
|
+
from a script (`--detailed-exitcode` exits 2 when the plan would change
|
|
45
|
+
something, 0 when it is in sync).
|
|
27
46
|
|
|
28
47
|
## Additive vs destructive
|
|
29
48
|
|
|
30
|
-
The
|
|
31
|
-
is
|
|
32
|
-
|
|
33
|
-
```sql
|
|
34
|
-
-- palbase db diff: add_priority
|
|
35
|
-
-- generated 20260605T142233
|
|
49
|
+
The plan separates the two, and a **destructive** change (dropping a column or a
|
|
50
|
+
table) is REFUSED unless you say so again:
|
|
36
51
|
|
|
37
|
-
ALTER TABLE todos ADD COLUMN IF NOT EXISTS priority text;
|
|
38
52
|
```
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
before committing:
|
|
43
|
-
|
|
44
|
-
```sql
|
|
45
|
-
-- DESTRUCTIVE: dropping todos.notes loses its data
|
|
46
|
-
ALTER TABLE todos DROP COLUMN notes;
|
|
53
|
+
$ palbase db apply
|
|
54
|
+
refused: this would take data away — run it again with --approve if that is
|
|
55
|
+
what you mean
|
|
47
56
|
```
|
|
48
57
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
58
|
+
Destructive lines carry their **row counts**, and that is the whole design: "this
|
|
59
|
+
drops a column" is a shrug, "this drops a column with 41,908 values in it" is a
|
|
60
|
+
decision. `palbase db apply --approve` runs them; `palbase push --approve` is the
|
|
61
|
+
same consent for an Environment.
|
|
62
|
+
|
|
63
|
+
A **column type change** is not applied by this rail at all. The plan lists it
|
|
64
|
+
under `not applied by this rail — change these yourself:` and you write the real
|
|
65
|
+
`ALTER ... TYPE` as a hand-written migration, with whatever `USING` cast and
|
|
66
|
+
backfill your data needs:
|
|
52
67
|
|
|
53
68
|
```sql
|
|
54
|
-
--
|
|
55
|
-
|
|
69
|
+
-- todos.priority text -> integer
|
|
70
|
+
ALTER TABLE todos ALTER COLUMN priority TYPE integer USING priority::integer;
|
|
56
71
|
```
|
|
57
72
|
|
|
58
73
|
## The drift gate
|
|
59
74
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
- **`palbase db
|
|
63
|
-
database
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
75
|
+
Code and schema move at different speeds, so three things keep them together:
|
|
76
|
+
|
|
77
|
+
- **`palbase db plan --detailed-exitcode`** exits 2 when the declaration and the
|
|
78
|
+
database disagree and 0 when they match — the form to put in CI, because it
|
|
79
|
+
answers with a status code and still writes nothing.
|
|
80
|
+
- The CLI installs a **git pre-push hook** that runs `palbase build`, the same
|
|
81
|
+
validation the deploy runs, so a push that would produce a failed deploy is
|
|
82
|
+
caught on your machine first. (Bypass with `git push --no-verify` — the server
|
|
83
|
+
still gates it.)
|
|
84
|
+
- On deploy, the migrations run and Palbase then asserts `db/schema.ts` matches
|
|
85
|
+
the live database. Unresolved drift **fails the deploy** and keeps the previous
|
|
69
86
|
version live — a broken schema never goes out silently.
|
|
70
87
|
|
|
71
|
-
## Your schema change is not live
|
|
88
|
+
## Your schema change is not live anywhere you have not applied it
|
|
89
|
+
|
|
90
|
+
The local stack and every Environment hold their own database, and editing
|
|
91
|
+
`db/schema.ts` changes none of them: `palbase db apply` is what moves the local
|
|
92
|
+
one, `palbase push` is what moves an Environment's. Until then the declaration is
|
|
93
|
+
ahead of the tables.
|
|
72
94
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
95
|
+
The TYPES move separately again. `palbase build` regenerates `palbase-env.d.ts`
|
|
96
|
+
from `db/schema.ts`, which is what types `Database.tables.<name>` in your
|
|
97
|
+
services — so after a schema edit, run it. (There is no second command for this:
|
|
98
|
+
`build` regenerates everything derived, because one verb to remember is one verb
|
|
99
|
+
to forget.) Typed and applied are independent, and knowing which one you are
|
|
100
|
+
missing is usually the whole debugging step.
|
|
79
101
|
|
|
80
102
|
## Hand-written migrations
|
|
81
103
|
|
|
82
104
|
`db/migrations/*.sql` is plain SQL applied in filename order and tracked in
|
|
83
|
-
`schema_migrations` (idempotent — a migration runs once).
|
|
84
|
-
|
|
85
|
-
can't express: a data backfill, a complex type change with a `USING` cast, a
|
|
105
|
+
`schema_migrations` (idempotent — a migration runs once). Nothing generates
|
|
106
|
+
these; you write one for anything the declaration cannot express: a data backfill, a complex type change with a `USING` cast, a
|
|
86
107
|
trigger, a `CHECK` constraint, an `EXCLUDE` constraint, a composite/multi-column
|
|
87
108
|
`UNIQUE`, an index (`CREATE INDEX`), or `CREATE EXTENSION` for an extension that
|
|
88
109
|
isn't in the DSL allowlist (e.g. `btree_gist`). The DSL column builders cover
|
|
@@ -126,12 +147,12 @@ Two patterns follow from this:
|
|
|
126
147
|
- **Extra constraint/index on a DSL-expressible table** — declare the table in
|
|
127
148
|
`db/schema.ts` and add **only** the constraint/index via the migration. The
|
|
128
149
|
base table stays modeled, the gate ignores the constraint, and `palbase db
|
|
129
|
-
|
|
150
|
+
plan` stays clean.
|
|
130
151
|
- **A whole object the DSL can't model** (like `instructor_sessions` above — its
|
|
131
152
|
`tstzrange` column has no DSL builder) — the table lives entirely in raw SQL,
|
|
132
|
-
not in `db/schema.ts`. The deploy still succeeds, but `palbase db
|
|
133
|
-
|
|
134
|
-
|
|
153
|
+
not in `db/schema.ts`. The deploy still succeeds, but `palbase db plan` will
|
|
154
|
+
list it as a ⚠ would-be-drop for the local database; that is expected, and the
|
|
155
|
+
deploy-time gate does not block on it. Never approve that one.
|
|
135
156
|
|
|
136
157
|
## Adding a NOT NULL column to a table with rows
|
|
137
158
|
|
|
@@ -158,8 +179,9 @@ RLS on and no applicable policy, Postgres denies everything: empty reads, refuse
|
|
|
158
179
|
writes, no error anywhere that says why.
|
|
159
180
|
|
|
160
181
|
**Guard the CREATE.** Postgres has no `CREATE POLICY IF NOT EXISTS`, so a
|
|
161
|
-
migration that replays — a fresh Environment,
|
|
162
|
-
fails on the second
|
|
182
|
+
migration that replays — a fresh Environment, or `palbase start --reset`, which
|
|
183
|
+
throws the local database away and brings it back empty — fails on the second
|
|
184
|
+
run. Drop first:
|
|
163
185
|
|
|
164
186
|
```sql
|
|
165
187
|
DROP POLICY IF EXISTS owner_all ON notes;
|
package/docs/schema.md
CHANGED
|
@@ -8,8 +8,9 @@ everywhere — by default, with no import and no generic.
|
|
|
8
8
|
## Defining a schema
|
|
9
9
|
|
|
10
10
|
The table NAME comes from the object key under `tables`. Each table value is an
|
|
11
|
-
object
|
|
12
|
-
|
|
11
|
+
object whose only required field is `columns`; `rls` and `policies` enable
|
|
12
|
+
[Row-Level Security](#row-level-security-rls), and `indexes` declares plain
|
|
13
|
+
btree [indexes](#indexes).
|
|
13
14
|
|
|
14
15
|
```ts
|
|
15
16
|
import {
|
|
@@ -65,6 +66,85 @@ Chainable modifiers: `.primaryKey()`, `.notNull()` (default), `.nullable()`,
|
|
|
65
66
|
`.defaultNow()` (timestamp → `now()`), `.references(table, column)`,
|
|
66
67
|
`.onDelete("cascade" | "set null" | "restrict" | "no action")`.
|
|
67
68
|
|
|
69
|
+
## Indexes
|
|
70
|
+
|
|
71
|
+
`indexes` declares plain (non-unique) btree indexes over an ordered column list:
|
|
72
|
+
|
|
73
|
+
```ts
|
|
74
|
+
export default defineSchema({
|
|
75
|
+
tables: {
|
|
76
|
+
sessions: {
|
|
77
|
+
columns: {
|
|
78
|
+
id: uuid().primaryKey().defaultRandom(),
|
|
79
|
+
room_id: uuid().notNull().references("rooms", "id"),
|
|
80
|
+
started_at: timestamp().defaultNow(),
|
|
81
|
+
},
|
|
82
|
+
indexes: [
|
|
83
|
+
{ name: "sessions_room_started_idx", columns: ["room_id", "started_at"] },
|
|
84
|
+
],
|
|
85
|
+
},
|
|
86
|
+
},
|
|
87
|
+
});
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
On deploy each entry becomes a statement of its own — `CREATE INDEX IF NOT
|
|
91
|
+
EXISTS <name> ON <table> (<columns>)` — not a clause of the `CREATE TABLE`. The
|
|
92
|
+
name and every column are identifier-validated before any SQL is built.
|
|
93
|
+
|
|
94
|
+
**The diff key is the NAME, and the diff is additive.** Three consequences, worth
|
|
95
|
+
knowing before you name an index:
|
|
96
|
+
|
|
97
|
+
- An index that exists in the database but is not in `indexes` is never dropped.
|
|
98
|
+
`db/schema.ts` does not own the database's indexes; it only adds the ones it
|
|
99
|
+
names.
|
|
100
|
+
- Removing an entry from `indexes` therefore does **not** drop the index. Drop it
|
|
101
|
+
in an explicit [migration](./migrations.md).
|
|
102
|
+
- Changing `columns` under a name that is already live does **nothing** — the
|
|
103
|
+
deploy matches the name, sees it, and skips. Give the new column list a NEW
|
|
104
|
+
name (and drop the old index in a migration if you want it gone).
|
|
105
|
+
|
|
106
|
+
### Scope: plain btree over columns, nothing else
|
|
107
|
+
|
|
108
|
+
`indexes` models exactly one kind of index. It cannot express:
|
|
109
|
+
|
|
110
|
+
| what you want | declare it with |
|
|
111
|
+
|---------------|-----------------|
|
|
112
|
+
| a UNIQUE index | `unique: [{ name, columns }]` on the table |
|
|
113
|
+
| a partial index (`WHERE …`) | `raw()` |
|
|
114
|
+
| an expression index (`lower(email)`, …) | `raw()` |
|
|
115
|
+
| any other method (`gin`, `gist`, `hnsw`, …) | `raw()` |
|
|
116
|
+
|
|
117
|
+
The limit is deliberate, not an oversight. Comparing a partial predicate or an
|
|
118
|
+
expression against the live database needs the same round-trip through Postgres'
|
|
119
|
+
own deparse that `checks` uses; without it an edited predicate would silently do
|
|
120
|
+
nothing. Rather than ship a half-working partial-index path, the typed field
|
|
121
|
+
stays columns-only and `raw()` carries the rest:
|
|
122
|
+
|
|
123
|
+
```ts
|
|
124
|
+
import { defineSchema, raw, uuid, text, timestamp } from "@palbase/backend";
|
|
125
|
+
|
|
126
|
+
// …
|
|
127
|
+
orders: {
|
|
128
|
+
columns: {
|
|
129
|
+
id: uuid().primaryKey().defaultRandom(),
|
|
130
|
+
status: text().notNull(),
|
|
131
|
+
created_at: timestamp().defaultNow(),
|
|
132
|
+
},
|
|
133
|
+
raw: [
|
|
134
|
+
raw(
|
|
135
|
+
"orders_pending_idx",
|
|
136
|
+
"CREATE INDEX IF NOT EXISTS orders_pending_idx ON orders (created_at) WHERE status = 'pending'",
|
|
137
|
+
{ down: "DROP INDEX IF EXISTS orders_pending_idx" },
|
|
138
|
+
),
|
|
139
|
+
],
|
|
140
|
+
},
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
`raw()`'s `up` is emitted verbatim on the privileged DDL connection and, like
|
|
144
|
+
`indexes`, is tracked by NAME only — a changed body needs a new name. Pass
|
|
145
|
+
`down` yourself for an index: the default drop statement drops a *constraint*,
|
|
146
|
+
and an index is not one.
|
|
147
|
+
|
|
68
148
|
## Typed DB access — by default
|
|
69
149
|
|
|
70
150
|
You do **not** wire anything per endpoint. Saving `db/schema.ts` regenerates
|
package/docs/services.md
CHANGED
|
@@ -1,12 +1,27 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Service singletons — `Database`, `Storage`, `Cache`, …
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
**middleware** uses a `ctx` argument (`ctx.db`, `ctx.log`, etc.).
|
|
3
|
+
**Two different things are called "services" here, and this page is the second
|
|
4
|
+
one.** Read the first paragraph before searching this page for a layout rule.
|
|
6
5
|
|
|
7
|
-
|
|
6
|
+
- **YOUR service layer** — `services/<name>.service.ts`, the plain classes your
|
|
7
|
+
own business logic lives in. The scaffold ships one, and the contract is at
|
|
8
|
+
the bottom of this page: [Your own service layer](#your-own-service-layer).
|
|
9
|
+
- **THE service singletons** — the platform objects you import from
|
|
10
|
+
`@palbase/backend` (`Database`, `Cache`, `Storage`, …). Everything between
|
|
11
|
+
here and that last section is about these.
|
|
12
|
+
|
|
13
|
+
Import them in every handler type — endpoints, workers, jobs, hooks, and
|
|
14
|
+
webhooks all use the same imports. Only **middleware** uses a `ctx` argument
|
|
15
|
+
(`ctx.db`, `ctx.log`, etc.).
|
|
16
|
+
|
|
17
|
+
Available singletons: `Database`, `Documents`, `Storage`, `Cache`,
|
|
8
18
|
`Log`, `Notifications`, `Flags`, `Realtime`.
|
|
9
19
|
|
|
20
|
+
There is **no `Queue`**. It was listed here for months and has never been
|
|
21
|
+
exported: background work is a cron `@Job` under `jobs/` (see
|
|
22
|
+
[background.md](./background.md)). Importing it does not fail at a queue that
|
|
23
|
+
turns out to be empty — it fails at the import.
|
|
24
|
+
|
|
10
25
|
`Realtime` is **broadcast-only** on the backend — a handler can push an event to
|
|
11
26
|
a channel, but cannot subscribe (a stateless request can't hold a socket).
|
|
12
27
|
Subscription lives on the client SDK (`pb.realtime`).
|
|
@@ -76,9 +91,10 @@ const { data: doc } = await Documents.doc("rooms/abc").get();
|
|
|
76
91
|
## Notifications
|
|
77
92
|
|
|
78
93
|
**No provider setup is required.** Every project starts with a managed sender, so
|
|
79
|
-
email and SMS deliver on a brand-new project with no provider
|
|
80
|
-
|
|
81
|
-
|
|
94
|
+
email and SMS deliver on a brand-new project with no provider and no API key. You
|
|
95
|
+
configure your own provider only to send from your own domain, and that is a CLI
|
|
96
|
+
act against the stack, not a file in this repository:
|
|
97
|
+
`palbase notifications providers`.
|
|
82
98
|
|
|
83
99
|
```ts
|
|
84
100
|
import { Notifications } from "@palbase/backend";
|
|
@@ -91,7 +107,7 @@ await Notifications.email.send({
|
|
|
91
107
|
});
|
|
92
108
|
await Notifications.email.send({
|
|
93
109
|
to: user.email,
|
|
94
|
-
templateSlug: "receipt", //
|
|
110
|
+
templateSlug: "receipt", // created with `palbase notifications templates`
|
|
95
111
|
locale: "tr", // without this the extra locale rows are unreachable
|
|
96
112
|
variables: { total: "₺240" },
|
|
97
113
|
});
|
|
@@ -198,3 +214,76 @@ and never blocks the handler waiting on subscribers.
|
|
|
198
214
|
|
|
199
215
|
Pair it with the client SDK's `pb.realtime.channel("room:42").on("message", …)`
|
|
200
216
|
to drive live chat, presence, dashboards, and other push features.
|
|
217
|
+
|
|
218
|
+
## Your own service layer
|
|
219
|
+
|
|
220
|
+
The singletons above are what a service CALLS. This section is the layer that
|
|
221
|
+
calls them — `services/<name>.service.ts`, which the scaffold ships a working
|
|
222
|
+
example of (`services/note.service.ts` and the controller that uses it).
|
|
223
|
+
|
|
224
|
+
The contract is three rules, and the scaffold's own test enforces all three:
|
|
225
|
+
|
|
226
|
+
**1. The logic lives in a service; the controller does not touch the database.**
|
|
227
|
+
A controller's job is HTTP — validate a body through a named schema, name the
|
|
228
|
+
200 shape as the return type, turn a missing row into a status. Which rows,
|
|
229
|
+
whose, in what order is the service's. A controller that reaches for `Database`
|
|
230
|
+
has moved the logic into the layer that is hardest to test.
|
|
231
|
+
|
|
232
|
+
**2. The dependency arrives through the CONSTRUCTOR, and the module exports one
|
|
233
|
+
wired instance.** There is no DI container and no decorator — the last line of
|
|
234
|
+
the file is the wiring:
|
|
235
|
+
|
|
236
|
+
```ts
|
|
237
|
+
// services/note.service.ts
|
|
238
|
+
import { Database } from "@palbase/backend";
|
|
239
|
+
|
|
240
|
+
type NotesTable = typeof Database.tables.notes;
|
|
241
|
+
|
|
242
|
+
export class NoteService {
|
|
243
|
+
private readonly notes: NotesTable;
|
|
244
|
+
|
|
245
|
+
// Assigned in the BODY. A parameter property (`constructor(private notes: …)`)
|
|
246
|
+
// is refused by Node's type-stripping test runner, for the whole file.
|
|
247
|
+
constructor(notes: NotesTable) {
|
|
248
|
+
this.notes = notes;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
list(userId: string) {
|
|
252
|
+
return this.notes.findMany({ user_id: userId });
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
export const noteService = new NoteService(Database.tables.notes);
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
```ts
|
|
260
|
+
// controllers/notes.controller.ts
|
|
261
|
+
import { noteService } from "../services/note.service";
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
**3. That module-level singleton is the only way to hold a dependency.**
|
|
265
|
+
Controllers, hooks, jobs and webhooks are all constructed by the runtime with
|
|
266
|
+
**no arguments**. A class that declares a constructor parameter is refused at
|
|
267
|
+
build with the class named — there is no injector to fill it, so the field would
|
|
268
|
+
otherwise be `undefined` in production. Import the singleton instead.
|
|
269
|
+
|
|
270
|
+
Rule 2 is also what makes rule 1 pay off: because the service is handed its
|
|
271
|
+
table rather than reaching for the singleton, a test constructs it with a
|
|
272
|
+
stand-in and never needs a database.
|
|
273
|
+
|
|
274
|
+
```ts
|
|
275
|
+
// services/note.service.test.ts — `npm test`, no database
|
|
276
|
+
import assert from "node:assert/strict";
|
|
277
|
+
import { test } from "node:test";
|
|
278
|
+
import { NoteService } from "./note.service.ts";
|
|
279
|
+
|
|
280
|
+
test("list filters by owner", async () => {
|
|
281
|
+
const seen: unknown[] = [];
|
|
282
|
+
const fake = { findMany: async (q: unknown) => (seen.push(q), []) };
|
|
283
|
+
await new NoteService(fake as never).list("u_1");
|
|
284
|
+
assert.deepEqual(seen, [{ user_id: "u_1" }]);
|
|
285
|
+
});
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
Node's ESM resolver wants the extension on a relative import inside a test
|
|
289
|
+
(`./note.service.ts`); the scaffold's `tsconfig.json` allows it.
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@palbase/backend",
|
|
3
|
-
"version": "
|
|
4
|
-
"description": "Palbase Backend SDK
|
|
3
|
+
"version": "24.0.1",
|
|
4
|
+
"description": "Palbase Backend SDK — class controllers (@Controller/@Get/@Post + @Body/@QueryParams/@Param), error classes, schema DSL",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
7
7
|
"type": "git",
|