@palbase/backend 5.2.0 → 7.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/{chunk-WUQO76NW.js → chunk-SWT2QR5F.js} +32 -3
- package/dist/chunk-SWT2QR5F.js.map +1 -0
- package/dist/db/index.d.cts +2 -2
- package/dist/db/index.d.ts +2 -2
- package/dist/{endpoint-B3uVK6OL.d.cts → endpoint-BFgsOTiL.d.cts} +103 -60
- package/dist/{endpoint-B3uVK6OL.d.ts → endpoint-BFgsOTiL.d.ts} +103 -60
- package/dist/{index-BAEWl60b.d.cts → index-CE31P7Dt.d.cts} +1 -1
- package/dist/{index-D9uLjEhB.d.ts → index-E7CMoir3.d.ts} +1 -1
- package/dist/index.cjs +149 -3
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +100 -10
- package/dist/index.d.ts +100 -10
- package/dist/index.js +122 -3
- package/dist/index.js.map +1 -1
- package/dist/test/index.cjs +40 -10
- 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 +12 -10
- package/dist/test/index.js.map +1 -1
- package/docs/README.md +11 -10
- package/docs/llms-full.txt +93 -19
- package/docs/routing.md +9 -5
- package/docs/services.md +73 -4
- package/package.json +5 -2
- package/dist/chunk-WUQO76NW.js.map +0 -1
package/docs/llms-full.txt
CHANGED
|
@@ -130,22 +130,22 @@ import { CreateTodoBody } from "../models/todos/create.js";
|
|
|
130
130
|
export class TodosController {
|
|
131
131
|
private todos = todoService;
|
|
132
132
|
|
|
133
|
-
@Get("") // GET /todos → operationId
|
|
133
|
+
@Get("") // GET /todos → operationId todos.list
|
|
134
134
|
async list(@User() user: UserT): Promise<TodoSchema[]> { // return type → 200 response schema
|
|
135
135
|
return this.todos.list(user.id);
|
|
136
136
|
}
|
|
137
137
|
|
|
138
|
-
@Post("") // POST /todos →
|
|
138
|
+
@Post("") // POST /todos → todos.create
|
|
139
139
|
async create(@Body(CreateTodoBody) body: CreateTodoBody, @User() user: UserT): Promise<TodoSchema> {
|
|
140
140
|
return this.todos.create(user.id, body.title);
|
|
141
141
|
}
|
|
142
142
|
|
|
143
|
-
@Get("/{id}") // GET /todos/{id} →
|
|
143
|
+
@Get("/{id}") // GET /todos/{id} → todos.get
|
|
144
144
|
async get(@Param("id") id: string, @User() user: UserT): Promise<TodoSchema> {
|
|
145
145
|
return this.todos.get(user.id, id);
|
|
146
146
|
}
|
|
147
147
|
|
|
148
|
-
@Delete("/{id}") // DELETE /todos/{id} →
|
|
148
|
+
@Delete("/{id}") // DELETE /todos/{id} → todos.remove; no body → : Promise<void>
|
|
149
149
|
async remove(@Param("id") id: string, @User() user: UserT): Promise<void> {
|
|
150
150
|
await this.todos.remove(user.id, id);
|
|
151
151
|
}
|
|
@@ -182,12 +182,13 @@ export default defineSchema({
|
|
|
182
182
|
|
|
183
183
|
### operationId — what the generated clients call
|
|
184
184
|
|
|
185
|
-
The operationId is
|
|
186
|
-
|
|
187
|
-
`
|
|
188
|
-
`
|
|
189
|
-
|
|
190
|
-
|
|
185
|
+
The operationId is DOTTED: `<controllerName>.<methodName>`. The namespace is the
|
|
186
|
+
controller class name with its trailing `Controller` suffix stripped and the
|
|
187
|
+
first letter lower-cased (`TodosController` → `todos`); the method name is used
|
|
188
|
+
as-is: `todos.list`, `todos.create`, `todos.get`. The generated iOS/TS clients
|
|
189
|
+
group these into namespaced calls — `pb.todos.list()`, `pb.todos.create(...)`,
|
|
190
|
+
`pb.todos.get(id:)`. Rename the class or the method and the operationId (and the
|
|
191
|
+
generated client surface) changes; the verb/path do not affect it.
|
|
191
192
|
|
|
192
193
|
### CLI workflow
|
|
193
194
|
|
|
@@ -430,10 +431,10 @@ export default class PlacesController {
|
|
|
430
431
|
}
|
|
431
432
|
```
|
|
432
433
|
|
|
433
|
-
| Method name
|
|
434
|
+
| Method name | Verb | Full path | operationId (dotted) |
|
|
434
435
|
|---|---|---|---|
|
|
435
|
-
| `importNearby` | POST | `/places/import` | `
|
|
436
|
-
| `listFavorites` | GET | `/places/favorites` | `
|
|
436
|
+
| `importNearby` | POST | `/places/import` | `places.importNearby` |
|
|
437
|
+
| `listFavorites` | GET | `/places/favorites` | `places.listFavorites` |
|
|
437
438
|
|
|
438
439
|
Rules:
|
|
439
440
|
|
|
@@ -445,8 +446,12 @@ Rules:
|
|
|
445
446
|
codegen + the runtime read that named type to drive the OpenAPI 200 response.
|
|
446
447
|
A body route with no named return type is a build error — annotate `: void`
|
|
447
448
|
(or `: Promise<void>`) for no body.
|
|
448
|
-
- The operationId is
|
|
449
|
-
|
|
449
|
+
- The operationId is DOTTED: `<controllerName>.<methodName>`. The namespace is
|
|
450
|
+
the controller class name with its trailing `Controller` suffix stripped and
|
|
451
|
+
the first letter lower-cased (`PlacesController` → `places`); the method name
|
|
452
|
+
is used as-is (`places.importNearby`). The generated clients group routes by
|
|
453
|
+
that namespace (`pb.places.importNearby(...)`). Rename the class or the
|
|
454
|
+
method and the operationId changes; the verb/path do not affect it.
|
|
450
455
|
|
|
451
456
|
See [endpoints.md](./endpoints.md) for the full decorator reference (`@Controller`
|
|
452
457
|
options, the parameter decorators, auth cascade, and error classes).
|
|
@@ -1051,11 +1056,15 @@ endpoints, workers, jobs, hooks, and webhooks all use the same imports. Only
|
|
|
1051
1056
|
**middleware** uses a `ctx` argument (`ctx.db`, `ctx.log`, etc.).
|
|
1052
1057
|
|
|
1053
1058
|
Available singletons: `Database`, `Documents`, `Storage`, `Cache`, `Queue`,
|
|
1054
|
-
`Log`, `Notifications`, `Flags`.
|
|
1059
|
+
`Log`, `Notifications`, `Flags`, `Realtime`.
|
|
1055
1060
|
|
|
1056
|
-
**
|
|
1057
|
-
|
|
1058
|
-
|
|
1061
|
+
`Realtime` is **broadcast-only** on the backend — a handler can push an event to
|
|
1062
|
+
a channel, but cannot subscribe (a stateless request can't hold a socket).
|
|
1063
|
+
Subscription lives on the client SDK (`pb.realtime`).
|
|
1064
|
+
|
|
1065
|
+
**Not available to backend handlers** (do not import them here): Functions, CMS,
|
|
1066
|
+
Links, Analytics, and Auth. Auth runs on the client SDK; the others are out of
|
|
1067
|
+
scope for backend endpoints.
|
|
1059
1068
|
|
|
1060
1069
|
## Cache
|
|
1061
1070
|
|
|
@@ -1152,6 +1161,71 @@ export default class CheckoutController {
|
|
|
1152
1161
|
}
|
|
1153
1162
|
```
|
|
1154
1163
|
|
|
1164
|
+
### Overriding a flag — current user vs cross-user
|
|
1165
|
+
|
|
1166
|
+
Writes mirror the `Database` / `Database.asService()` model:
|
|
1167
|
+
|
|
1168
|
+
- `Flags.setOverride(key, value)` (default) writes an override for the **current
|
|
1169
|
+
request user** — no `userId` argument, no admin power. It errors on an
|
|
1170
|
+
anonymous request (no signed-in user).
|
|
1171
|
+
- `Flags.asService()` returns the cross-user admin surface
|
|
1172
|
+
(`setOverrideForUser`, `setOverridesForUser`, `clearOverrideForUser`,
|
|
1173
|
+
`clearAllOverridesForUser`, `batchSetOverrides`) for writing overrides for an
|
|
1174
|
+
**arbitrary** user. Explicit and greppable, just like `Database.asService()`.
|
|
1175
|
+
|
|
1176
|
+
```ts
|
|
1177
|
+
// Current request user — no userId needed:
|
|
1178
|
+
await Flags.setOverride("new-checkout", true);
|
|
1179
|
+
|
|
1180
|
+
// Cross-user admin write — explicit target, via asService():
|
|
1181
|
+
await Flags.asService().setOverrideForUser("user_123", "new-checkout", true);
|
|
1182
|
+
```
|
|
1183
|
+
|
|
1184
|
+
## Realtime
|
|
1185
|
+
|
|
1186
|
+
`Realtime` is **broadcast-only** on the backend: a handler pushes an event to a
|
|
1187
|
+
named channel and every client subscribed to that channel (via the client SDK's
|
|
1188
|
+
`pb.realtime.channel(...).on(...)`) receives it. There is no `subscribe()` on
|
|
1189
|
+
the backend — a stateless request handler can't hold a socket, so it fires an
|
|
1190
|
+
HTTP broadcast and returns.
|
|
1191
|
+
|
|
1192
|
+
```ts
|
|
1193
|
+
import { Controller, Post, Body, User, Realtime, z } from "@palbase/backend";
|
|
1194
|
+
|
|
1195
|
+
const Msg = z.object({ text: z.string() });
|
|
1196
|
+
|
|
1197
|
+
@Controller("/rooms")
|
|
1198
|
+
class RoomController {
|
|
1199
|
+
@Post("/:roomId/messages")
|
|
1200
|
+
async send(
|
|
1201
|
+
@Param("roomId") roomId: string,
|
|
1202
|
+
@Body(Msg) body: z.infer<typeof Msg>,
|
|
1203
|
+
@User() user: UserT,
|
|
1204
|
+
) {
|
|
1205
|
+
// ... persist the message ...
|
|
1206
|
+
// Notify everyone subscribed to this room:
|
|
1207
|
+
await Realtime.broadcast(`room:${roomId}`, "message", {
|
|
1208
|
+
text: body.text,
|
|
1209
|
+
from: user.id,
|
|
1210
|
+
});
|
|
1211
|
+
return { ok: true };
|
|
1212
|
+
}
|
|
1213
|
+
}
|
|
1214
|
+
```
|
|
1215
|
+
|
|
1216
|
+
`broadcast(channel, event, payload?)` is **fire-and-forget**: it resolves
|
|
1217
|
+
`{ data: undefined, error: null }` once accepted, or `{ data: null, error }`
|
|
1218
|
+
when it could not be sent (e.g. realtime is not provisioned). It never throws
|
|
1219
|
+
and never blocks the handler waiting on subscribers.
|
|
1220
|
+
|
|
1221
|
+
- **Channel** names are app-defined and arbitrary (`"room:42"`, `"orders"`). Do
|
|
1222
|
+
NOT prefix with `"realtime:"` — that prefix is internal to the transport.
|
|
1223
|
+
- **Event** is the name subscribers filter on (`"message"`, `"status_changed"`).
|
|
1224
|
+
- **Payload** is any JSON-serializable object.
|
|
1225
|
+
|
|
1226
|
+
Pair it with the client SDK's `pb.realtime.channel("room:42").on("message", …)`
|
|
1227
|
+
to drive live chat, presence, dashboards, and other push features.
|
|
1228
|
+
|
|
1155
1229
|
|
|
1156
1230
|
|
|
1157
1231
|
<!-- ===== resources.md ===== -->
|
package/docs/routing.md
CHANGED
|
@@ -39,10 +39,10 @@ export default class PlacesController {
|
|
|
39
39
|
}
|
|
40
40
|
```
|
|
41
41
|
|
|
42
|
-
| Method name
|
|
42
|
+
| Method name | Verb | Full path | operationId (dotted) |
|
|
43
43
|
|---|---|---|---|
|
|
44
|
-
| `importNearby` | POST | `/places/import` | `
|
|
45
|
-
| `listFavorites` | GET | `/places/favorites` | `
|
|
44
|
+
| `importNearby` | POST | `/places/import` | `places.importNearby` |
|
|
45
|
+
| `listFavorites` | GET | `/places/favorites` | `places.listFavorites` |
|
|
46
46
|
|
|
47
47
|
Rules:
|
|
48
48
|
|
|
@@ -54,8 +54,12 @@ Rules:
|
|
|
54
54
|
codegen + the runtime read that named type to drive the OpenAPI 200 response.
|
|
55
55
|
A body route with no named return type is a build error — annotate `: void`
|
|
56
56
|
(or `: Promise<void>`) for no body.
|
|
57
|
-
- The operationId is
|
|
58
|
-
|
|
57
|
+
- The operationId is DOTTED: `<controllerName>.<methodName>`. The namespace is
|
|
58
|
+
the controller class name with its trailing `Controller` suffix stripped and
|
|
59
|
+
the first letter lower-cased (`PlacesController` → `places`); the method name
|
|
60
|
+
is used as-is (`places.importNearby`). The generated clients group routes by
|
|
61
|
+
that namespace (`pb.places.importNearby(...)`). Rename the class or the
|
|
62
|
+
method and the operationId changes; the verb/path do not affect it.
|
|
59
63
|
|
|
60
64
|
See [endpoints.md](./endpoints.md) for the full decorator reference (`@Controller`
|
|
61
65
|
options, the parameter decorators, auth cascade, and error classes).
|
package/docs/services.md
CHANGED
|
@@ -5,11 +5,15 @@ endpoints, workers, jobs, hooks, and webhooks all use the same imports. Only
|
|
|
5
5
|
**middleware** uses a `ctx` argument (`ctx.db`, `ctx.log`, etc.).
|
|
6
6
|
|
|
7
7
|
Available singletons: `Database`, `Documents`, `Storage`, `Cache`, `Queue`,
|
|
8
|
-
`Log`, `Notifications`, `Flags`.
|
|
8
|
+
`Log`, `Notifications`, `Flags`, `Realtime`.
|
|
9
9
|
|
|
10
|
-
**
|
|
11
|
-
|
|
12
|
-
|
|
10
|
+
`Realtime` is **broadcast-only** on the backend — a handler can push an event to
|
|
11
|
+
a channel, but cannot subscribe (a stateless request can't hold a socket).
|
|
12
|
+
Subscription lives on the client SDK (`pb.realtime`).
|
|
13
|
+
|
|
14
|
+
**Not available to backend handlers** (do not import them here): Functions, CMS,
|
|
15
|
+
Links, Analytics, and Auth. Auth runs on the client SDK; the others are out of
|
|
16
|
+
scope for backend endpoints.
|
|
13
17
|
|
|
14
18
|
## Cache
|
|
15
19
|
|
|
@@ -105,3 +109,68 @@ export default class CheckoutController {
|
|
|
105
109
|
}
|
|
106
110
|
}
|
|
107
111
|
```
|
|
112
|
+
|
|
113
|
+
### Overriding a flag — current user vs cross-user
|
|
114
|
+
|
|
115
|
+
Writes mirror the `Database` / `Database.asService()` model:
|
|
116
|
+
|
|
117
|
+
- `Flags.setOverride(key, value)` (default) writes an override for the **current
|
|
118
|
+
request user** — no `userId` argument, no admin power. It errors on an
|
|
119
|
+
anonymous request (no signed-in user).
|
|
120
|
+
- `Flags.asService()` returns the cross-user admin surface
|
|
121
|
+
(`setOverrideForUser`, `setOverridesForUser`, `clearOverrideForUser`,
|
|
122
|
+
`clearAllOverridesForUser`, `batchSetOverrides`) for writing overrides for an
|
|
123
|
+
**arbitrary** user. Explicit and greppable, just like `Database.asService()`.
|
|
124
|
+
|
|
125
|
+
```ts
|
|
126
|
+
// Current request user — no userId needed:
|
|
127
|
+
await Flags.setOverride("new-checkout", true);
|
|
128
|
+
|
|
129
|
+
// Cross-user admin write — explicit target, via asService():
|
|
130
|
+
await Flags.asService().setOverrideForUser("user_123", "new-checkout", true);
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
## Realtime
|
|
134
|
+
|
|
135
|
+
`Realtime` is **broadcast-only** on the backend: a handler pushes an event to a
|
|
136
|
+
named channel and every client subscribed to that channel (via the client SDK's
|
|
137
|
+
`pb.realtime.channel(...).on(...)`) receives it. There is no `subscribe()` on
|
|
138
|
+
the backend — a stateless request handler can't hold a socket, so it fires an
|
|
139
|
+
HTTP broadcast and returns.
|
|
140
|
+
|
|
141
|
+
```ts
|
|
142
|
+
import { Controller, Post, Body, User, Realtime, z } from "@palbase/backend";
|
|
143
|
+
|
|
144
|
+
const Msg = z.object({ text: z.string() });
|
|
145
|
+
|
|
146
|
+
@Controller("/rooms")
|
|
147
|
+
class RoomController {
|
|
148
|
+
@Post("/:roomId/messages")
|
|
149
|
+
async send(
|
|
150
|
+
@Param("roomId") roomId: string,
|
|
151
|
+
@Body(Msg) body: z.infer<typeof Msg>,
|
|
152
|
+
@User() user: UserT,
|
|
153
|
+
) {
|
|
154
|
+
// ... persist the message ...
|
|
155
|
+
// Notify everyone subscribed to this room:
|
|
156
|
+
await Realtime.broadcast(`room:${roomId}`, "message", {
|
|
157
|
+
text: body.text,
|
|
158
|
+
from: user.id,
|
|
159
|
+
});
|
|
160
|
+
return { ok: true };
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
`broadcast(channel, event, payload?)` is **fire-and-forget**: it resolves
|
|
166
|
+
`{ data: undefined, error: null }` once accepted, or `{ data: null, error }`
|
|
167
|
+
when it could not be sent (e.g. realtime is not provisioned). It never throws
|
|
168
|
+
and never blocks the handler waiting on subscribers.
|
|
169
|
+
|
|
170
|
+
- **Channel** names are app-defined and arbitrary (`"room:42"`, `"orders"`). Do
|
|
171
|
+
NOT prefix with `"realtime:"` — that prefix is internal to the transport.
|
|
172
|
+
- **Event** is the name subscribers filter on (`"message"`, `"status_changed"`).
|
|
173
|
+
- **Payload** is any JSON-serializable object.
|
|
174
|
+
|
|
175
|
+
Pair it with the client SDK's `pb.realtime.channel("room:42").on("message", …)`
|
|
176
|
+
to drive live chat, presence, dashboards, and other push features.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@palbase/backend",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "7.0.0",
|
|
4
4
|
"description": "Palbase Backend SDK — class controllers (@Controller/@Get/@Post + @Body/@Query/@Param), error classes, schema DSL",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -65,7 +65,9 @@
|
|
|
65
65
|
"devDependencies": {
|
|
66
66
|
"@types/node": "^25.5.2",
|
|
67
67
|
"fast-glob": "^3.3.3",
|
|
68
|
+
"openapi3-ts": "^4.5.0",
|
|
68
69
|
"tsup": "^8.0.0",
|
|
70
|
+
"tsx": "^4.22.4",
|
|
69
71
|
"typescript": "^5.7.0",
|
|
70
72
|
"vitest": "^3.0.0"
|
|
71
73
|
},
|
|
@@ -77,6 +79,7 @@
|
|
|
77
79
|
"test": "vitest run",
|
|
78
80
|
"test:watch": "vitest",
|
|
79
81
|
"typecheck": "tsc --noEmit",
|
|
80
|
-
"docs:build": "node scripts/build-llms.mjs"
|
|
82
|
+
"docs:build": "node scripts/build-llms.mjs",
|
|
83
|
+
"emit:openapi-fixture": "tsx scripts/emit-openapi-fixture.mjs"
|
|
81
84
|
}
|
|
82
85
|
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/runtime.ts"],"sourcesContent":["/**\n * runtime.ts — request-scoped service singletons.\n *\n * The backend SDK no longer threads a `ctx` god-object through every handler.\n * Instead, controller methods import PascalCase service singletons directly:\n *\n * import { Controller, Post, Body, Database } from \"@palbase/backend\";\n *\n * \\@Controller(\"/todos\")\n * export default class TodosController {\n * \\@Post(\"\") create(\\@Body(CreateTodoBody) body: CreateTodoBody): unknown {\n * return Database.insert(\"todos\", { title: body.title });\n * }\n * }\n *\n * The singletons are thin Proxies. Every property access forwards to the live\n * client for the CURRENT request scope, resolved through {@link __getRuntime}.\n *\n * # Request-scope resolution (persistent app-server)\n *\n * The runtime is a long-running Node process that serves many concurrent\n * requests on one event loop (NOT a fresh subprocess per request). A single\n * module-global slot would let one in-flight request's services bleed into\n * another's. So the services are carried in an {@link AsyncLocalStorage} store\n * ({@link __requestALS}) that the runtime sets per request with\n * {@link __runWithRuntime}; every async continuation of that request reads its\n * own store. `__getRuntime` reads the ALS store first; the module-global slot\n * (set by {@link __setRuntime}) is only a fallback for callers that run OUTSIDE\n * an ALS scope (dev-server, unit tests, the legacy single-shot path). Because\n * each `br-<ref>` pod is single-tenant, there is no cross-tenant leakage; the\n * ALS store is what prevents cross-REQUEST leakage within the shared process.\n *\n * The seam that makes `import { Database } from \"@palbase/backend\"` resolve to\n * the runtime-injected client: `@palbase/backend` is marked esbuild-EXTERNAL\n * when the tenant bundle is built, and the package is installed globally in the\n * pod (NODE_PATH=/usr/local/lib/node_modules). So worker.js's\n * `require('@palbase/backend')` and the bundle's `import` resolve to ONE shared\n * module instance — the ALS store and `__setRuntime` slot on that instance are\n * visible to the singletons the bundle imported.\n */\n\nimport { AsyncLocalStorage } from \"node:async_hooks\";\n\nimport type {\n DBClient,\n DBOps,\n TxClient,\n CacheClient,\n QueueClient,\n Logger,\n PalbaseDocsClient,\n} from \"./endpoint.js\";\nimport type {\n PalbaseStorageClient,\n PalbaseNotificationsClient,\n PalbaseFlagsClient,\n} from \"./clients.js\";\nimport type {\n EnvTypedDatabase,\n EnvServiceDatabase,\n EnvTypedTx,\n EnvTables,\n} from \"./db/typed-db.js\";\n\n/** The set of live clients the runtime injects per request scope.\n *\n * EXCLUDED on purpose: Realtime, Functions, CMS, Links, Analytics, Auth. They\n * are not exposed as backend handler singletons (auth lives on the client SDK;\n * the rest are out of scope for backend endpoints). */\nexport interface RuntimeServices {\n Database: DBClient;\n Documents: PalbaseDocsClient;\n Storage: PalbaseStorageClient;\n Cache: CacheClient;\n Queue: QueueClient;\n Log: Logger;\n Notifications: PalbaseNotificationsClient;\n Flags: PalbaseFlagsClient;\n}\n\n/**\n * Per-request store. The persistent runtime runs each request inside\n * {@link __runWithRuntime}, so every async continuation of that request reads\n * its OWN `runtime` (and any other request-scoped fields the runtime adds).\n *\n * Exported with a `__` prefix so the runtime (worker.js) shares the SAME ALS\n * instance across the one module instance — two ALS instances would silently\n * not see each other's stores. NOT part of the public author-facing API.\n */\nexport const __requestALS = new AsyncLocalStorage<{ runtime: RuntimeServices }>();\n\n/** Process-global fallback slot. Used only OUTSIDE an ALS scope (dev-server,\n * unit tests, legacy single-shot worker). Inside the persistent server every\n * request runs in {@link __requestALS}, which takes precedence. */\nlet runtime: RuntimeServices | null = null;\n\n/** Install the live clients in the process-global fallback slot.\n *\n * Persistent-server requests should use {@link __runWithRuntime} instead; this\n * remains for dev-server / tests / the legacy single-shot path that run without\n * an ALS scope. NOT part of the public author-facing API. */\nexport function __setRuntime(services: RuntimeServices): void {\n runtime = services;\n}\n\n/** Run `fn` with `services` bound as the request-scoped runtime.\n *\n * The persistent worker calls this once per request so concurrent requests\n * never share a services slot. NOT part of the public author-facing API. */\nexport function __runWithRuntime<T>(services: RuntimeServices, fn: () => T): T {\n return __requestALS.run({ runtime: services }, fn);\n}\n\n/** Read the live clients, throwing if accessed outside a request scope.\n *\n * Resolves the ALS store first (persistent server, per-request), then the\n * process-global fallback (dev-server / tests). NOT part of the public\n * author-facing API — used by the runtime and the singleton Proxies. */\nexport function __getRuntime(): RuntimeServices {\n const scoped = __requestALS.getStore();\n if (scoped) return scoped.runtime;\n if (runtime === null) {\n throw new Error(\n \"Palbase services accessed outside a request scope. The Database/Documents/… \" +\n \"singletons are only available inside an endpoint handler (or after the \" +\n \"runtime has called __runWithRuntime / __setRuntime).\",\n );\n }\n return runtime;\n}\n\n/**\n * Build a Proxy singleton that forwards every property access to the live\n * client named `key` on the current runtime.\n *\n * The single `as RuntimeServices[K]` is the only contained cast in the surface:\n * `Reflect.get` on a typed object returns `unknown` for a `string | symbol`\n * key, but `prop` is constrained to keys of the client interface at the call\n * sites (the exported singletons are typed below), so the forward is sound.\n */\nfunction makeServiceProxy<K extends keyof RuntimeServices>(key: K): RuntimeServices[K] {\n const handler: ProxyHandler<RuntimeServices[K]> = {\n get(_target, prop, receiver) {\n const client = __getRuntime()[key];\n const value = Reflect.get(client as object, prop, receiver) as unknown;\n // Bind methods to their owning client so `this` stays correct when the\n // author destructures or calls `Database.query(...)`.\n return typeof value === \"function\" ? value.bind(client) : value;\n },\n };\n // The Proxy target is irrelevant (all access goes through `get`); the cast\n // names the surface type the singleton presents to authors.\n return new Proxy({} as RuntimeServices[K], handler);\n}\n\n/**\n * Build the `.tables` accessor for an op-bearing client (the top-level\n * `Database` or a transaction-scoped `tx`). Each `tables.<name>` access\n * returns a small object that forwards the five CRUD ops to the underlying\n * client using `name` as the string table identifier. The shapes are typed\n * against the generated `palbase-env.d.ts` (`EnvTables`); at runtime they are\n * plain string-keyed calls, so no schema value is needed here.\n *\n * Returns `EnvTables` — TS cannot infer the mapped type through the Proxy, so\n * a single structural narrowing names the surface (the proxy returns a\n * correctly-shaped accessor for whatever string member is read).\n */\nfunction makeTablesAccessor(ops: () => TxClient): EnvTables {\n const tablesProxy = new Proxy(\n {},\n {\n get(_t, prop: string | symbol) {\n if (typeof prop !== \"string\") return undefined;\n const name = prop;\n return {\n insert: (data: Record<string, unknown>) => ops().insert(name, data),\n update: (id: string, data: Record<string, unknown>) => ops().update(name, id, data),\n delete: (id: string) => ops().delete(name, id),\n findById: (id: string) => ops().findById(name, id),\n findMany: (query?: Record<string, unknown>) => ops().findMany(name, query),\n };\n },\n },\n );\n return tablesProxy as EnvTables;\n}\n\n/** The raw string-keyed `DBClient` for the current request scope. */\nconst rawDatabase: DBClient = makeServiceProxy(\"Database\");\n\n/**\n * Wrap a raw `DBClient` into the typed `{ ...ops, tables, transaction }`\n * surface. The five string ops forward straight through; `tables` is the\n * env-typed accessor; `transaction` yields typed tables. Reused for both the\n * default (RLS-enforced) `Database` and the `asService()` sibling — each is\n * fed its own raw client (the default proxy vs `rawDatabase.asService()`).\n *\n * The `satisfies` pins the op surface so a missing/renamed op is a compile\n * error; the assembled object carries `tables`/`transaction` alongside.\n */\nfunction makeTypedSurface(raw: Omit<DBClient, \"asService\">): EnvServiceDatabase {\n const ops = {\n query: (sql: string, params?: unknown[]) => raw.query(sql, params),\n insert: (table: string, data: Record<string, unknown>) => raw.insert(table, data),\n update: (table: string, id: string, data: Record<string, unknown>) =>\n raw.update(table, id, data),\n delete: (table: string, id: string) => raw.delete(table, id),\n findById: (table: string, id: string) => raw.findById(table, id),\n findMany: (table: string, query?: Record<string, unknown>) => raw.findMany(table, query),\n } satisfies DBOps;\n return Object.assign(ops, {\n tables: makeTablesAccessor(() => raw),\n transaction<T>(fn: (tx: EnvTypedTx) => Promise<T>): Promise<T> {\n return raw.transaction((rawTx) => fn({ tables: makeTablesAccessor(() => rawTx) }));\n },\n });\n}\n\n/**\n * The project's own Postgres (pgx, schema `env_<envId>`).\n *\n * Typed by default: `Database.tables.<name>.insert({...})` is typed against\n * the project's generated `palbase-env.d.ts` with NO import and NO generic.\n * The raw string ops (`query`/`insert`/`update`/`delete`/`findById`/`findMany`)\n * are also available for dynamic table names and read-only SQL.\n *\n * RLS is enforced by default (the runtime runs each op as `authenticated` with\n * the verified user's claims). To bypass RLS, call `Database.asService()` —\n * explicit and greppable — which runs as the `service_role` (BYPASSRLS).\n *\n * @example\n * import { Database } from \"@palbase/backend\";\n *\n * const todo = await Database.tables.todos.insert({ title: req.input.title });\n * todo.id; // string ✓\n * const rows = await Database.query(\"SELECT id FROM todos WHERE done = $1\", [false]);\n * const all = await Database.asService().tables.todos.findMany({}); // RLS bypass\n */\nexport const Database: EnvTypedDatabase = Object.assign(makeTypedSurface(rawDatabase), {\n /**\n * Lazily resolve the runtime's service-role sibling on each call. We do NOT\n * cache it: `rawDatabase.asService()` reads the CURRENT request scope through\n * the runtime proxy, and the per-request runtime injects a service client\n * bound to that request's identity headers — caching would leak one request's\n * sibling into another concurrent request.\n */\n asService(): EnvServiceDatabase {\n return makeTypedSurface(rawDatabase.asService());\n },\n});\n\n/** Firestore-like document client (PalDocs). */\nexport const Documents: PalbaseDocsClient = makeServiceProxy(\"Documents\");\n\n/** Object storage client (buckets, signed URLs). */\nexport const Storage: PalbaseStorageClient = makeServiceProxy(\"Storage\");\n\n/** JSON-typed cache (get/set/incr/getOrSet). */\nexport const Cache: CacheClient = makeServiceProxy(\"Cache\");\n\n/** Background job queue. */\nexport const Queue: QueueClient = makeServiceProxy(\"Queue\");\n\n/** Structured logger. */\nexport const Log: Logger = makeServiceProxy(\"Log\");\n\n/** Push / email / SMS / in-app notifications. */\nexport const Notifications: PalbaseNotificationsClient = makeServiceProxy(\"Notifications\");\n\n/** Feature flags. */\nexport const Flags: PalbaseFlagsClient = makeServiceProxy(\"Flags\");\n"],"mappings":"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","names":[]}
|