@palbase/backend 39.0.0 → 39.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/docs/database.md CHANGED
@@ -29,6 +29,56 @@ await Database.public.todos.delete(todo.id);
29
29
 
30
30
  See [schema.md](./schema.md) for the full typed-table surface.
31
31
 
32
+ ## Tenant-scoped CRUD — `defineRepository`
33
+
34
+ Most repository classes are the same two lines per method: add the tenant column
35
+ to the predicate, then unwrap the result. Writing that by hand is what makes it
36
+ forgettable, and one forgotten predicate shows one tenant's row to another.
37
+ `defineRepository` produces a BASE CLASS that writes the predicate once.
38
+
39
+ ```ts
40
+ import { defineRepository } from "@palbase/backend";
41
+
42
+ export class TodoRepository extends defineRepository(
43
+ Database.public.todos,
44
+ { tenant: "household_id" },
45
+ ) {}
46
+ ```
47
+
48
+ The subclass gets six typed methods, all scoped to the tenant you pass first:
49
+ `list(tenant)`, `find(tenant, id)`, `insert(tenant, values)`,
50
+ `update(tenant, id, patch)`, `updateScoped(tenant, id, patch)` and
51
+ `delete(tenant, id)`. Add your own methods in the subclass body as usual.
52
+
53
+ `insert` does NOT take the tenant column in its payload — the repository writes
54
+ it. `update` throws `NotFound` when nothing matches, so the caller does not need
55
+ a `null` branch; `updateScoped` runs the same predicate and returns `null`
56
+ instead, for when absence is a value.
57
+
58
+ The base class constructor takes no arguments, so the DI container resolves the
59
+ subclass by its own name with no extra metadata. `Database` stays ambient rather
60
+ than injected: it is request-scoped, so a reference captured in a constructor
61
+ would carry one request's client into another.
62
+
63
+ ### A row key that is not `id`
64
+
65
+ The row key defaults to `id`, but it is not fixed — `defineTable` does not
66
+ require an `id` column, and a primary key can sit on any column. Name it:
67
+
68
+ ```ts
69
+ export class DocRepository extends defineRepository(
70
+ Database.public.docs,
71
+ { tenant: "org_id", key: "slug" },
72
+ ) {}
73
+
74
+ await new DocRepository().find("org_1", "getting-started");
75
+ ```
76
+
77
+ The key parameter carries the column's OWN type, so a numeric key is typed
78
+ `number` rather than assumed to be a string. If the table has no `id` column and
79
+ you do not name a key, the surface refuses at compile time and the refusal says
80
+ what to do (`MissingRowKey`).
81
+
32
82
  ## Transaction boundaries and retries
33
83
 
34
84
  Ordinary Database writes commit when the request completes. `$transaction(fn)`
@@ -1163,6 +1163,56 @@ await Database.public.todos.delete(todo.id);
1163
1163
 
1164
1164
  See [schema.md](./schema.md) for the full typed-table surface.
1165
1165
 
1166
+ ## Tenant-scoped CRUD — `defineRepository`
1167
+
1168
+ Most repository classes are the same two lines per method: add the tenant column
1169
+ to the predicate, then unwrap the result. Writing that by hand is what makes it
1170
+ forgettable, and one forgotten predicate shows one tenant's row to another.
1171
+ `defineRepository` produces a BASE CLASS that writes the predicate once.
1172
+
1173
+ ```ts
1174
+ import { defineRepository } from "@palbase/backend";
1175
+
1176
+ export class TodoRepository extends defineRepository(
1177
+ Database.public.todos,
1178
+ { tenant: "household_id" },
1179
+ ) {}
1180
+ ```
1181
+
1182
+ The subclass gets six typed methods, all scoped to the tenant you pass first:
1183
+ `list(tenant)`, `find(tenant, id)`, `insert(tenant, values)`,
1184
+ `update(tenant, id, patch)`, `updateScoped(tenant, id, patch)` and
1185
+ `delete(tenant, id)`. Add your own methods in the subclass body as usual.
1186
+
1187
+ `insert` does NOT take the tenant column in its payload — the repository writes
1188
+ it. `update` throws `NotFound` when nothing matches, so the caller does not need
1189
+ a `null` branch; `updateScoped` runs the same predicate and returns `null`
1190
+ instead, for when absence is a value.
1191
+
1192
+ The base class constructor takes no arguments, so the DI container resolves the
1193
+ subclass by its own name with no extra metadata. `Database` stays ambient rather
1194
+ than injected: it is request-scoped, so a reference captured in a constructor
1195
+ would carry one request's client into another.
1196
+
1197
+ ### A row key that is not `id`
1198
+
1199
+ The row key defaults to `id`, but it is not fixed — `defineTable` does not
1200
+ require an `id` column, and a primary key can sit on any column. Name it:
1201
+
1202
+ ```ts
1203
+ export class DocRepository extends defineRepository(
1204
+ Database.public.docs,
1205
+ { tenant: "org_id", key: "slug" },
1206
+ ) {}
1207
+
1208
+ await new DocRepository().find("org_1", "getting-started");
1209
+ ```
1210
+
1211
+ The key parameter carries the column's OWN type, so a numeric key is typed
1212
+ `number` rather than assumed to be a string. If the table has no `id` column and
1213
+ you do not name a key, the surface refuses at compile time and the refusal says
1214
+ what to do (`MissingRowKey`).
1215
+
1166
1216
  ## Transaction boundaries and retries
1167
1217
 
1168
1218
  Ordinary Database writes commit when the request completes. `$transaction(fn)`
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@palbase/backend",
3
- "version": "39.0.0",
3
+ "version": "39.1.0",
4
4
  "description": "Palbase Backend SDK — class controllers (@Controller/@Get/@Post + @Body/@QueryParams/@Param), error classes, schema DSL",
5
5
  "license": "MIT",
6
6
  "repository": {