@palbase/backend 25.1.0 → 27.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 +2401 -1039
- package/dist/bin/palbase-backend.cjs.map +1 -1
- package/dist/bin/palbase-backend.js +87 -51
- package/dist/bin/palbase-backend.js.map +1 -1
- package/dist/chunk-CGNN2PUH.js +213 -0
- package/dist/chunk-CGNN2PUH.js.map +1 -0
- package/dist/chunk-DRZFQRJI.js +370 -0
- package/dist/chunk-DRZFQRJI.js.map +1 -0
- package/dist/chunk-GYK6QYS4.js +708 -0
- package/dist/chunk-GYK6QYS4.js.map +1 -0
- package/dist/chunk-I3C4PFIW.js +930 -0
- package/dist/chunk-I3C4PFIW.js.map +1 -0
- package/dist/{chunk-VDF2T4AS.js → chunk-OO7R25AI.js} +1213 -591
- package/dist/chunk-OO7R25AI.js.map +1 -0
- package/dist/chunk-TS4U7NBD.js +885 -0
- package/dist/chunk-TS4U7NBD.js.map +1 -0
- package/dist/{chunk-YOY5DFQS.js → chunk-TWX6JTGJ.js} +76 -34
- package/dist/{chunk-YOY5DFQS.js.map → chunk-TWX6JTGJ.js.map} +1 -1
- package/dist/{chunk-35PNTIRN.js → chunk-VVMJEVQP.js} +63 -162
- package/dist/chunk-VVMJEVQP.js.map +1 -0
- package/dist/{chunk-7D4SUZUM.js → chunk-VXPNPVAG.js} +3 -1
- package/dist/db/env.cjs.map +1 -1
- package/dist/db/env.d.cts +2 -2
- package/dist/db/env.d.ts +2 -2
- package/dist/db/index.cjs +754 -344
- 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 +7 -4
- package/dist/engine/index.cjs +2339 -1006
- package/dist/engine/index.cjs.map +1 -1
- package/dist/engine/index.d.cts +6 -6
- package/dist/engine/index.d.ts +6 -6
- package/dist/engine/index.js +7 -6
- package/dist/index-BrvvxSpn.d.ts +4844 -0
- package/dist/index-Bve7BBTL.d.cts +4844 -0
- package/dist/{index-CUomTA3e.d.ts → index-NuzRCuxe.d.ts} +171 -296
- package/dist/{index-ClpDeSos.d.cts → index-VtToZmUm.d.cts} +171 -296
- package/dist/index.cjs +2694 -1169
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +165 -19
- package/dist/index.d.ts +165 -19
- package/dist/index.js +738 -477
- package/dist/index.js.map +1 -1
- package/dist/module-Dl1KFVtc.d.cts +54 -0
- package/dist/module-Dl1KFVtc.d.ts +54 -0
- package/dist/openapi/index.cjs +1330 -484
- package/dist/openapi/index.cjs.map +1 -1
- package/dist/openapi/index.d.cts +4 -2
- package/dist/openapi/index.d.ts +4 -2
- package/dist/openapi/index.js +1264 -474
- package/dist/openapi/index.js.map +1 -1
- package/dist/{registry-dZZ5JKYg.d.ts → registry-B0eyOF9x.d.ts} +1 -1
- package/dist/{registry-CC0WBQq6.d.cts → registry-Bk9_rbNd.d.cts} +1 -1
- package/dist/stack.cjs.map +1 -1
- package/dist/test/index.cjs +705 -141
- package/dist/test/index.cjs.map +1 -1
- package/dist/test/index.d.cts +30 -4
- package/dist/test/index.d.ts +30 -4
- package/dist/test/index.js +472 -124
- package/dist/test/index.js.map +1 -1
- package/docs/README.md +33 -18
- package/docs/auth.md +1 -1
- package/docs/background.md +2 -2
- package/docs/database.md +221 -50
- package/docs/endpoints.md +3 -4
- package/docs/events.md +3 -3
- package/docs/getting-started.md +1 -1
- package/docs/llms-full.txt +401 -117
- package/docs/migrations.md +2 -2
- package/docs/schema.md +19 -10
- package/docs/services.md +116 -26
- package/package.json +8 -4
- package/stager/generics.js +205 -0
- package/stager/stage.js +39 -3
- package/template/AGENTS.md +110 -72
- package/template/db/public.ts +1 -1
- package/template/{controllers → modules/health}/health.controller.ts +1 -1
- package/template/modules/health/health.module.ts +24 -0
- package/template/modules/notes/note.service.test.ts +49 -0
- package/template/modules/notes/note.service.ts +108 -0
- package/template/{controllers → modules/notes}/notes.controller.ts +17 -11
- package/template/modules/notes/notes.module.ts +37 -0
- package/template/package.json +5 -3
- package/template/scripts/test.sh +33 -0
- package/template/tsconfig.json +29 -30
- package/dist/chunk-35PNTIRN.js.map +0 -1
- package/dist/chunk-CJSKYY76.js +0 -627
- package/dist/chunk-CJSKYY76.js.map +0 -1
- package/dist/chunk-CRQKCRGF.js +0 -276
- package/dist/chunk-CRQKCRGF.js.map +0 -1
- package/dist/chunk-G4R6BTLV.js +0 -662
- package/dist/chunk-G4R6BTLV.js.map +0 -1
- package/dist/chunk-VDF2T4AS.js.map +0 -1
- package/dist/chunk-XABBC7JP.js +0 -55
- package/dist/chunk-XABBC7JP.js.map +0 -1
- package/dist/endpoint-CTEHhb7A.d.ts +0 -2386
- package/dist/endpoint-DYHMo6cC.d.cts +0 -2386
- package/dist/index-CW21M9Z3.d.ts +0 -1222
- package/dist/index-CmBK76nx.d.cts +0 -1222
- package/template/services/note.service.test.ts +0 -45
- package/template/services/note.service.ts +0 -76
- /package/dist/{chunk-7D4SUZUM.js.map → chunk-VXPNPVAG.js.map} +0 -0
- /package/template/{models/notes → modules/notes/dto}/create.ts +0 -0
|
@@ -1,285 +1,6 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import { E as EnvTypedDatabase } from './index-CW21M9Z3.js';
|
|
5
|
-
import { R as RouteMeta } from './registry-dZZ5JKYg.js';
|
|
6
|
-
|
|
7
|
-
/**
|
|
8
|
-
* runtime.ts — request-scoped service singletons.
|
|
9
|
-
*
|
|
10
|
-
* The backend SDK no longer threads a `ctx` god-object through every handler.
|
|
11
|
-
* Instead, controller methods import PascalCase service singletons directly:
|
|
12
|
-
*
|
|
13
|
-
* import { Controller, Post, Body, Database } from "@palbase/backend";
|
|
14
|
-
*
|
|
15
|
-
* \@Controller("/todos")
|
|
16
|
-
* export default class TodosController {
|
|
17
|
-
* \@Post("") create(\@Body(CreateTodoBody) body: CreateTodoBody): unknown {
|
|
18
|
-
* return Database.insert("todos", { title: body.title });
|
|
19
|
-
* }
|
|
20
|
-
* }
|
|
21
|
-
*
|
|
22
|
-
* The singletons are thin Proxies. Every property access forwards to the live
|
|
23
|
-
* client for the CURRENT request scope, resolved through {@link __getRuntime}.
|
|
24
|
-
*
|
|
25
|
-
* # Request-scope resolution (persistent app-server)
|
|
26
|
-
*
|
|
27
|
-
* The runtime is a long-running Node process that serves many concurrent
|
|
28
|
-
* requests on one event loop (NOT a fresh subprocess per request). A single
|
|
29
|
-
* module-global slot would let one in-flight request's services bleed into
|
|
30
|
-
* another's. So the services are carried in an {@link AsyncLocalStorage} store
|
|
31
|
-
* ({@link __requestALS}) that the runtime sets per request with
|
|
32
|
-
* {@link __runWithRuntime}; every async continuation of that request reads its
|
|
33
|
-
* own store. `__getRuntime` reads the ALS store first; the module-global slot
|
|
34
|
-
* (set by {@link __setRuntime}) is only a fallback for callers that run OUTSIDE
|
|
35
|
-
* an ALS scope (dev-server, unit tests, the legacy single-shot path). Because
|
|
36
|
-
* each `br-<ref>` pod is single-tenant, there is no cross-tenant leakage; the
|
|
37
|
-
* ALS store is what prevents cross-REQUEST leakage within the shared process.
|
|
38
|
-
*
|
|
39
|
-
* The seam that makes `import { Database } from "@palbase/backend"` resolve to
|
|
40
|
-
* the runtime-injected client: `@palbase/backend` is marked esbuild-EXTERNAL
|
|
41
|
-
* when the tenant bundle is built, and the package is installed globally in the
|
|
42
|
-
* pod (NODE_PATH=/usr/local/lib/node_modules). So worker.js's
|
|
43
|
-
* `require('@palbase/backend')` and the bundle's `import` resolve to ONE shared
|
|
44
|
-
* module instance — the ALS store and `__setRuntime` slot on that instance are
|
|
45
|
-
* visible to the singletons the bundle imported.
|
|
46
|
-
*/
|
|
47
|
-
|
|
48
|
-
/** The set of live clients the runtime injects per request scope.
|
|
49
|
-
*
|
|
50
|
-
* Realtime is BROADCAST-ONLY here (a stateless handler can push an event but
|
|
51
|
-
* cannot hold a subscription socket — `subscribe()` lives on the client SDK).
|
|
52
|
-
*
|
|
53
|
-
* EXCLUDED on purpose: Functions, Links, Analytics, Auth. They are not
|
|
54
|
-
* exposed as backend handler singletons (auth lives on the client SDK; the rest
|
|
55
|
-
* are out of scope for backend endpoints). */
|
|
56
|
-
interface RuntimeServices {
|
|
57
|
-
Database: DBClient;
|
|
58
|
-
Secrets: SecretsService;
|
|
59
|
-
Documents: PalbaseDocsClient;
|
|
60
|
-
Storage: PalbaseStorageClient;
|
|
61
|
-
Cache: CacheClient;
|
|
62
|
-
Log: Logger;
|
|
63
|
-
Notifications: PalbaseNotificationsClient;
|
|
64
|
-
Flags: PalbaseFlagsClient;
|
|
65
|
-
Realtime: PalbaseRealtimeClient;
|
|
66
|
-
}
|
|
67
|
-
/**
|
|
68
|
-
* The per-request ALS box.
|
|
69
|
-
*
|
|
70
|
-
* `runtime` is the service bundle. `userId` is the request's authenticated user
|
|
71
|
-
* id, written by the runtime immediately after it assembles the request object
|
|
72
|
-
* (worker.js: `requestALS.getStore().userId = pbReq.user?.id || null`) and
|
|
73
|
-
* `null` on an anonymous request. It was already being written there for the
|
|
74
|
-
* Flags client's auto-bind; declaring it here makes the existing contract typed
|
|
75
|
-
* instead of implicit, which is what lets `currentSubjectId()` read a
|
|
76
|
-
* server-owned identity rather than trusting anything the caller sent.
|
|
77
|
-
*/
|
|
78
|
-
interface RequestStore {
|
|
79
|
-
runtime: RuntimeServices;
|
|
80
|
-
userId?: string | null;
|
|
81
|
-
/** This request's id (`req_…`), written by the runtime. Used as the spend
|
|
82
|
-
* idempotency scope when the caller supplied no `Idempotency-Key`. */
|
|
83
|
-
requestId?: string;
|
|
84
|
-
/** The caller's `Idempotency-Key` header, or `null` when absent. What makes a
|
|
85
|
-
* client's retry replay its first result instead of charging twice. */
|
|
86
|
-
idempotencyKey?: string | null;
|
|
87
|
-
}
|
|
88
|
-
/**
|
|
89
|
-
* Per-request store. The persistent runtime runs each request inside
|
|
90
|
-
* {@link __runWithRuntime}, so every async continuation of that request reads
|
|
91
|
-
* its OWN `runtime` (and any other request-scoped fields the runtime adds).
|
|
92
|
-
*
|
|
93
|
-
* Exported with a `__` prefix so the runtime (worker.js) shares the SAME ALS
|
|
94
|
-
* instance across the one module instance — two ALS instances would silently
|
|
95
|
-
* not see each other's stores. NOT part of the public author-facing API.
|
|
96
|
-
*/
|
|
97
|
-
declare const __requestALS: AsyncLocalStorage<RequestStore>;
|
|
98
|
-
/** Install the live clients in the process-global fallback slot.
|
|
99
|
-
*
|
|
100
|
-
* Persistent-server requests should use {@link __runWithRuntime} instead; this
|
|
101
|
-
* remains for dev-server / tests / the legacy single-shot path that run without
|
|
102
|
-
* an ALS scope. NOT part of the public author-facing API. */
|
|
103
|
-
declare function __setRuntime(services: RuntimeServices): void;
|
|
104
|
-
/** Run `fn` with `services` bound as the request-scoped runtime.
|
|
105
|
-
*
|
|
106
|
-
* The persistent worker calls this once per request so concurrent requests
|
|
107
|
-
* never share a services slot. NOT part of the public author-facing API. */
|
|
108
|
-
declare function __runWithRuntime<T>(services: RuntimeServices, fn: () => T): T;
|
|
109
|
-
/** Read the live clients, throwing if accessed outside a request scope.
|
|
110
|
-
*
|
|
111
|
-
* Resolves the ALS store first (persistent server, per-request), then the
|
|
112
|
-
* process-global fallback (dev-server / tests). NOT part of the public
|
|
113
|
-
* author-facing API — used by the runtime and the singleton Proxies. */
|
|
114
|
-
declare function __getRuntime(): RuntimeServices;
|
|
115
|
-
/** A lifecycle hook. Sync or async; the runtime awaits what it returns. */
|
|
116
|
-
type LifecycleHook = () => void | Promise<void>;
|
|
117
|
-
/** Runs one release's shutdown hooks. Handed back by {@link __runStartHooks}
|
|
118
|
-
* and called by the engine's `app.shutdown()`. Idempotent. */
|
|
119
|
-
type ShutdownRunner = () => Promise<void>;
|
|
120
|
-
/**
|
|
121
|
-
* Run `hook` ONCE while the application comes up, before it serves anything.
|
|
122
|
-
*
|
|
123
|
-
* Call it at MODULE SCOPE in a file the application imports — the same rule
|
|
124
|
-
* `defineDefaultAuth` and `@Controller` follow, and for the same reason: the
|
|
125
|
-
* declaration is claimed when the app boots, which is after module loading and
|
|
126
|
-
* before the first request. `name` is not decoration: a hook that throws is
|
|
127
|
-
* reported by that name and the boot is REFUSED, so it is what tells an
|
|
128
|
-
* operator which resource did not come up.
|
|
129
|
-
*
|
|
130
|
-
* There is no request scope yet, so the `Database`/`Secrets`/… singletons are
|
|
131
|
-
* NOT available inside a start hook. A secret is read from `process.env` here
|
|
132
|
-
* (the runtime mirrors the vault into it at boot).
|
|
133
|
-
*
|
|
134
|
-
* @example
|
|
135
|
-
* // resources/graph.ts
|
|
136
|
-
* import neo4j from "neo4j-driver";
|
|
137
|
-
* import { onStart, onShutdown } from "@palbase/backend";
|
|
138
|
-
*
|
|
139
|
-
* export let graph: Driver;
|
|
140
|
-
* onStart("graph", () => {
|
|
141
|
-
* graph = neo4j.driver(process.env.NEO4J_URL!, neo4j.auth.basic("neo4j", process.env.NEO4J_PASSWORD!));
|
|
142
|
-
* });
|
|
143
|
-
* onShutdown("graph", () => graph.close());
|
|
144
|
-
*/
|
|
145
|
-
declare function onStart(name: string, hook: LifecycleHook): void;
|
|
146
|
-
/**
|
|
147
|
-
* Run `hook` while the application shuts down — the place a pool opened in
|
|
148
|
-
* {@link onStart} is closed.
|
|
149
|
-
*
|
|
150
|
-
* Shutdown is BEST-EFFORT by design: a hook that throws is reported by name and
|
|
151
|
-
* the rest still run. A drain that abandoned the remaining hooks on the first
|
|
152
|
-
* failure would leak exactly what this exists to release, and the process is
|
|
153
|
-
* leaving anyway.
|
|
154
|
-
*
|
|
155
|
-
* Hooks run in REVERSE declaration order, so a resource is released before what
|
|
156
|
-
* it was built on.
|
|
157
|
-
*/
|
|
158
|
-
declare function onShutdown(name: string, hook: LifecycleHook): void;
|
|
159
|
-
/**
|
|
160
|
-
* CLAIM what has been declared, run the start hooks, and hand back the runner
|
|
161
|
-
* for this release's shutdown hooks. Called by the engine's `createApp`; the
|
|
162
|
-
* `App.shutdown()` it builds calls what comes back. NOT part of the public
|
|
163
|
-
* author-facing API.
|
|
164
|
-
*
|
|
165
|
-
* IT CLAIMS RATHER THAN READS, which is what makes it correct in this runtime:
|
|
166
|
-
* a candidate release is loaded BESIDE the live one in one process
|
|
167
|
-
* (`v2/runtime/src/registry-scope.ts`), and both bundles append to the one
|
|
168
|
-
* shared slot above. If each app read the whole list, the live app's shutdown
|
|
169
|
-
* would close the candidate's pool and the candidate's would close the live
|
|
170
|
-
* app's. Taking the declarations leaves each app holding exactly its own.
|
|
171
|
-
*
|
|
172
|
-
* A start hook that throws REFUSES THE BOOT — with the hook's name in the
|
|
173
|
-
* message — after releasing whatever the earlier hooks already opened. Serving
|
|
174
|
-
* from a half-initialised app is the silence this whole surface replaces, and a
|
|
175
|
-
* boot that dies holding an open pool is the leak it replaces.
|
|
176
|
-
*/
|
|
177
|
-
declare function __runStartHooks(): Promise<ShutdownRunner>;
|
|
178
|
-
/** Drop every declaration. For tests, which declare repeatedly in one process.
|
|
179
|
-
* NOT part of the public author-facing API. */
|
|
180
|
-
declare function __resetLifecycleHooks(): void;
|
|
181
|
-
/**
|
|
182
|
-
* The project's own Postgres (pgx, schema `env_<envId>`).
|
|
183
|
-
*
|
|
184
|
-
* Typed by default: `Database.tables.<name>.insert({...})` is typed against
|
|
185
|
-
* the project's generated `palbase-env.d.ts` with NO import and NO generic.
|
|
186
|
-
* The raw string ops (`query`/`insert`/`update`/`delete`/`findById`/`findMany`)
|
|
187
|
-
* are also available for dynamic table names and read-only SQL.
|
|
188
|
-
*
|
|
189
|
-
* RLS is enforced by default (the runtime runs each op as `authenticated` with
|
|
190
|
-
* the verified user's claims). To bypass RLS, call `Database.asService()` —
|
|
191
|
-
* explicit and greppable — which runs as the `service_role` (BYPASSRLS).
|
|
192
|
-
*
|
|
193
|
-
* @example
|
|
194
|
-
* import { Database } from "@palbase/backend";
|
|
195
|
-
*
|
|
196
|
-
* const todo = await Database.tables.todos.insert({ title: req.input.title });
|
|
197
|
-
* todo.id; // string ✓
|
|
198
|
-
* const rows = await Database.query("SELECT id FROM todos WHERE done = $1", [false]);
|
|
199
|
-
* const all = await Database.asService().tables.todos.findMany({}); // RLS bypass
|
|
200
|
-
*/
|
|
201
|
-
declare const Database: EnvTypedDatabase;
|
|
202
|
-
/** Firestore-like document client (PalDocs). */
|
|
203
|
-
declare const Documents: PalbaseDocsClient;
|
|
204
|
-
/**
|
|
205
|
-
* `buckets.<name>` — the storage twin of `Database.tables.<name>`, and the same
|
|
206
|
-
* mechanism: `config/storage.ts` generates a `Buckets` augmentation into
|
|
207
|
-
* `palbase-env.d.ts`, so a bucket name is a property with no import and no
|
|
208
|
-
* generic, and a typo is a compile error.
|
|
209
|
-
*
|
|
210
|
-
* The intermediate `.buckets` is there for the reason `.tables` is: bucket
|
|
211
|
-
* names must not share a namespace with the client's own methods, or the day
|
|
212
|
-
* somebody declares a bucket called `bucket` the surface breaks.
|
|
213
|
-
*/
|
|
214
|
-
type EnvBuckets = {
|
|
215
|
-
[K in keyof Buckets]: TypedBucketClient<Buckets[K] extends BucketTypes ? Buckets[K] : BucketTypes>;
|
|
216
|
-
};
|
|
217
|
-
/** One bucket, with its declared variant names in the type. */
|
|
218
|
-
interface TypedBucketClient<B extends BucketTypes> extends Omit<PalbaseBucketClient, "getPublicUrl"> {
|
|
219
|
-
/** The public URL for the object, or for one of THIS bucket's declared
|
|
220
|
-
* renditions. A name the bucket does not declare is a compile error. */
|
|
221
|
-
getPublicUrl(path: string, options?: {
|
|
222
|
-
variant?: B["variants"];
|
|
223
|
-
}): string;
|
|
224
|
-
}
|
|
225
|
-
/**
|
|
226
|
-
* Object storage: buckets, objects, renditions and signed URLs.
|
|
227
|
-
*
|
|
228
|
-
* `Storage.buckets.posts.upload(...)` is the typed path. `Storage.bucket(name)`
|
|
229
|
-
* remains for a name computed at runtime — rare, and it gives up the typing,
|
|
230
|
-
* which is the honest trade rather than a second blessed way to do it.
|
|
231
|
-
*/
|
|
232
|
-
declare const Storage: PalbaseStorageClient & {
|
|
233
|
-
buckets: EnvBuckets;
|
|
234
|
-
};
|
|
235
|
-
/** JSON-typed cache (get/set/incr/getOrSet). */
|
|
236
|
-
declare const Cache: CacheClient;
|
|
237
|
-
/**
|
|
238
|
-
* This tenant's secrets — API keys, provider credentials, signing material the
|
|
239
|
-
* BACKEND owns.
|
|
240
|
-
*
|
|
241
|
-
* `await Secrets.get("STRIPE_KEY")` in a handler. There is no `.env` to read
|
|
242
|
-
* and no file to mount: a secret is written through the vault's authenticated
|
|
243
|
-
* API (or the CLI/MCP that calls it), and this is how the deployed code reads
|
|
244
|
-
* it back. A name this tenant has not set answers null — as does every name
|
|
245
|
-
* the STACK holds, because no route returns a platform secret's value at all.
|
|
246
|
-
*/
|
|
247
|
-
declare const Secrets: SecretsService;
|
|
248
|
-
/** Structured logger. */
|
|
249
|
-
declare const Log: Logger;
|
|
250
|
-
/** Push / email / SMS / in-app notifications. */
|
|
251
|
-
declare const Notifications: PalbaseNotificationsClient;
|
|
252
|
-
/**
|
|
253
|
-
* Feature flags.
|
|
254
|
-
*
|
|
255
|
-
* Mirrors the `Database` / `Database.asService()` model. The default surface is
|
|
256
|
-
* RLS-equivalent for flags: reads resolve against the CURRENT request user and
|
|
257
|
-
* `Flags.setOverride(key, value)` writes an override for that same signed-in
|
|
258
|
-
* user (no userId argument, no admin power). Cross-user admin writes
|
|
259
|
-
* (`setOverrideForUser`, …) live behind `Flags.asService()` — explicit and
|
|
260
|
-
* greppable, just like `Database.asService()`.
|
|
261
|
-
*
|
|
262
|
-
* @example
|
|
263
|
-
* import { Flags } from "@palbase/backend";
|
|
264
|
-
*
|
|
265
|
-
* if (await Flags.isEnabled("new_checkout")) { ... } // current user
|
|
266
|
-
* await Flags.setOverride("new_checkout", true); // current user
|
|
267
|
-
* await Flags.asService().setOverrideForUser("u_9", "x", true); // cross-user
|
|
268
|
-
*/
|
|
269
|
-
declare const Flags: PalbaseFlagsClient;
|
|
270
|
-
/**
|
|
271
|
-
* The Realtime broadcast singleton for the current request scope. Backend-side
|
|
272
|
-
* Realtime is BROADCAST-ONLY (a stateless handler can push but not subscribe —
|
|
273
|
-
* `subscribe()` lives on the client SDK's `pb.realtime`). Fire-and-forget:
|
|
274
|
-
* `broadcast` resolves once accepted (or with an `error`), never blocking the
|
|
275
|
-
* handler on subscribers.
|
|
276
|
-
*
|
|
277
|
-
* @example
|
|
278
|
-
* import { Realtime } from "@palbase/backend";
|
|
279
|
-
*
|
|
280
|
-
* await Realtime.broadcast("room:42", "message", { text, from: user.id });
|
|
281
|
-
*/
|
|
282
|
-
declare const Realtime: PalbaseRealtimeClient;
|
|
1
|
+
import { D as DBClient, a as DBOps, T as TxPlanBody, b as TxPlanResponse, A as AuthSpec, C as CacheClient, c as RuntimeServices, _ as __runWithRuntime, d as __requestALS } from './index-BrvvxSpn.js';
|
|
2
|
+
import { T as Token } from './module-Dl1KFVtc.js';
|
|
3
|
+
import { R as RouteMeta } from './registry-B0eyOF9x.js';
|
|
283
4
|
|
|
284
5
|
/**
|
|
285
6
|
* engine/config.ts — settings from the environment, and the gate that refuses
|
|
@@ -334,7 +55,7 @@ interface EngineConfig {
|
|
|
334
55
|
* against it, so changing it changes who the database thinks is asking. */
|
|
335
56
|
dbRole: string;
|
|
336
57
|
/**
|
|
337
|
-
* The Postgres role `Database
|
|
58
|
+
* The Postgres role `Database.$asService()` is bound to. It is the one that
|
|
338
59
|
* carries BYPASSRLS, which is the whole of what "as service" means — a name
|
|
339
60
|
* pointing at a role without it does not fail, it returns fewer rows.
|
|
340
61
|
*
|
|
@@ -431,10 +152,20 @@ type TxLike = SqlTx | LazyTransaction;
|
|
|
431
152
|
* page offset. All three used to require dropping to raw SQL, and the docs said
|
|
432
153
|
* so — which is how a tenant's controllers filled up with hand-written SELECTs. */
|
|
433
154
|
interface FindManyOptions {
|
|
155
|
+
/** Sıralama: tek obje ya da SIRALI liste (FR-008). `nulls` NULL'ların yerini
|
|
156
|
+
* sabitler — Postgres varsayılanı yöne göre değişir (ASC'de son, DESC'te ilk). */
|
|
434
157
|
orderBy?: {
|
|
435
158
|
column: string;
|
|
436
159
|
direction?: "asc" | "desc";
|
|
437
|
-
|
|
160
|
+
nulls?: "first" | "last";
|
|
161
|
+
} | {
|
|
162
|
+
column: string;
|
|
163
|
+
direction?: "asc" | "desc";
|
|
164
|
+
nulls?: "first" | "last";
|
|
165
|
+
}[];
|
|
166
|
+
/** Projeksiyon (FR-009): verilmezse `SELECT *`. Kolon adları ŞEMAYA karşı
|
|
167
|
+
* doğrulanır — bilinmeyen ad SQL'e ulaşmadan, adıyla reddedilir. */
|
|
168
|
+
select?: string[];
|
|
438
169
|
limit?: number;
|
|
439
170
|
/** Rows to skip before the page starts. Only meaningful with `limit`, and
|
|
440
171
|
* refused without it — see `offsetClause`. */
|
|
@@ -442,8 +173,62 @@ interface FindManyOptions {
|
|
|
442
173
|
}
|
|
443
174
|
/** The six string-keyed operations, plus an interactive `transaction`. */
|
|
444
175
|
declare function createOps(tx: TxLike): {
|
|
445
|
-
query(sql: string, params?: unknown[]): Promise<
|
|
176
|
+
query<T = unknown>(sql: string, params?: unknown[]): Promise<T[]>;
|
|
446
177
|
insert(table: string, data: Row): Promise<Row>;
|
|
178
|
+
/**
|
|
179
|
+
* Bir ADI kilitle — satırı değil (FR-054).
|
|
180
|
+
*
|
|
181
|
+
* "Aynı anda tek bir fatura kapanışı koşsun" gibi, kilitlenecek satırı
|
|
182
|
+
* OLMAYAN işler için. Anahtar bir metin; Postgres 64-bit tamsayı istediği
|
|
183
|
+
* için `hashtextextended` ile hash'leniyor ve metin SQL'e hiç GİRMİYOR.
|
|
184
|
+
*
|
|
185
|
+
* YALNIZ transaction kapsamlı aile (`pg_advisory_xact_lock`). Oturum
|
|
186
|
+
* kapsamlı sürüm bırakıldığı yerde KALIR: bağlantı havuza döner, kilit
|
|
187
|
+
* üstünde kalır ve bir sonraki isteği — hiç bitmeyecek şekilde — bekletir.
|
|
188
|
+
* Transaction kapsamlısı commit/rollback'te Postgres tarafından bırakılır,
|
|
189
|
+
* yani unutulması imkânsız.
|
|
190
|
+
*/
|
|
191
|
+
advisoryXactLock(key: string): Promise<void>;
|
|
192
|
+
/**
|
|
193
|
+
* Bu satırları KİLİTLE, ve sırayı KATMAN koysun (FR-035).
|
|
194
|
+
*
|
|
195
|
+
* D-017: katman sıralamayı değil, **sıralamayı mümkün kılan primitifi**
|
|
196
|
+
* verir. Uygulama dokunacağı kümeyi önden bildirir; sırayı burası koyar.
|
|
197
|
+
* Çağıranın verdiği sıraya güvenmek, iki çağıranın aynı iki satıra ters
|
|
198
|
+
* sırayla girmesi demektir — ve deadlock'un bedeli ucuz değil:
|
|
199
|
+
* `deadlock_timeout` varsayılanı **1 saniye**, yani kaybeden işlem bir
|
|
200
|
+
* saniye bekledikten sonra hata alır.
|
|
201
|
+
*
|
|
202
|
+
* `FOR NO KEY UPDATE`, `updateMany`'nin CTE'siyle AYNI kilit modu: iki
|
|
203
|
+
* yazma yolunun tek bir sırası ve tek bir modu olmak zorunda, yoksa biri
|
|
204
|
+
* diğerini bekletir.
|
|
205
|
+
*/
|
|
206
|
+
lockRows(table: string, ids: readonly string[]): Promise<void>;
|
|
207
|
+
/**
|
|
208
|
+
* Bir idempotency anahtarını SAHİPLEN (FR-033).
|
|
209
|
+
*
|
|
210
|
+
* `{ inserted: true, row }` → bu çağrı yazdı.
|
|
211
|
+
* `{ inserted: false, row }` → başkası önce yazdı; dönen satır ONUN satırı.
|
|
212
|
+
*
|
|
213
|
+
* ÖLÇÜLDÜ (pg16, spec Research Appendix) — üç dalın üçü de farklı davranıyor:
|
|
214
|
+
*
|
|
215
|
+
* plain INSERT ikinci oturum 2121 ms BLOKLANIR, sonra 23505 alır;
|
|
216
|
+
* birincinin satırı korunur ← istenen
|
|
217
|
+
* ON CONFLICT DO UPDATE ikincinin verisi birincininkini EZER
|
|
218
|
+
* ON CONFLICT DO NOTHING `INSERT 0 0` — sessiz; çağıran ne olduğunu bilemez
|
|
219
|
+
*
|
|
220
|
+
* Bu yüzden `ON CONFLICT` KULLANILMIYOR. Ödeme yolunda "ikinci çağrı
|
|
221
|
+
* birincinin sonucunu görsün" idempotency'nin tanımıdır; ezmek ya da
|
|
222
|
+
* susmak ikisi de yanlış cevap.
|
|
223
|
+
*
|
|
224
|
+
* INSERT bir SAVEPOINT içinde koşuyor: başarısız bir INSERT transaction'ı
|
|
225
|
+
* ABORT eder (25P02) ve savepoint olmadan yakalama işe yaramaz — hata
|
|
226
|
+
* yakalanır ama sonraki SELECT de "current transaction is aborted" alır.
|
|
227
|
+
*/
|
|
228
|
+
claim(table: string, unique: Row, extra?: Row): Promise<{
|
|
229
|
+
inserted: boolean;
|
|
230
|
+
row: Row;
|
|
231
|
+
}>;
|
|
447
232
|
/**
|
|
448
233
|
* INSERT the row, or UPDATE it when it collides on `onConflict`.
|
|
449
234
|
*
|
|
@@ -458,7 +243,7 @@ declare function createOps(tx: TxLike): {
|
|
|
458
243
|
* The conflict columns are excluded from the SET list: they are what MATCHED,
|
|
459
244
|
* so writing them back is at best a no-op and at worst a surprise.
|
|
460
245
|
*/
|
|
461
|
-
|
|
246
|
+
put(table: string, data: Row, opts: {
|
|
462
247
|
onConflict: readonly string[];
|
|
463
248
|
}): Promise<Row>;
|
|
464
249
|
update(table: string, id: string, data: Row): Promise<Row | null>;
|
|
@@ -523,9 +308,6 @@ declare function createOps(tx: TxLike): {
|
|
|
523
308
|
};
|
|
524
309
|
/** Chunk-modunda satır başına dönen en iyi blok sayısı (1..10, vars. 3; FR-015). */
|
|
525
310
|
blocksPerRow?: number;
|
|
526
|
-
/** Filtrelenmiş küme üzerinde kolon başına top-20 değer sayacı —
|
|
527
|
-
* dönüş dizisinin `_facets` özelliği (FR-027). */
|
|
528
|
-
facets?: string[];
|
|
529
311
|
/** Satır-modunda FTS eşleşme vurgusu: ts_headline ile `_highlight`
|
|
530
312
|
* alanı; chunk-modda no-op — bloklar zaten eşleşen kesittir (FR-025). */
|
|
531
313
|
highlight?: boolean;
|
|
@@ -627,10 +409,10 @@ declare function createOps(tx: TxLike): {
|
|
|
627
409
|
* rolls back only what that handle wrote and the request can keep writing.
|
|
628
410
|
*
|
|
629
411
|
* WHY THE HANDLE IS AN ARGUMENT. The obvious shape — `attempt(async () => {
|
|
630
|
-
* ... Database
|
|
412
|
+
* ... Database.$insert(...) ... })`, with no parameter — would have to point
|
|
631
413
|
* the ambient `Database` at the savepoint for the duration, and a request is
|
|
632
|
-
* concurrent with itself: `Promise.all([Database
|
|
633
|
-
* Database
|
|
414
|
+
* concurrent with itself: `Promise.all([Database.$insert(a),
|
|
415
|
+
* Database.$attempt(...)])` would put `a` inside the savepoint and roll it
|
|
634
416
|
* back with it. Silent data loss, and the same interleaving this file already
|
|
635
417
|
* refuses for `asService()`. Passing the handle makes the boundary something
|
|
636
418
|
* you can see in the code that crosses it.
|
|
@@ -641,7 +423,7 @@ declare function createOps(tx: TxLike): {
|
|
|
641
423
|
*/
|
|
642
424
|
attempt<T>(fn: (tx: DBOps) => Promise<T>): Promise<T>;
|
|
643
425
|
/**
|
|
644
|
-
* Execute a whole transaction plan — what `Database
|
|
426
|
+
* Execute a whole transaction plan — what `Database.$transaction(fn)` builds.
|
|
645
427
|
*
|
|
646
428
|
* WHY IT RUNS HERE. The platform used to carry a complete implementation
|
|
647
429
|
* of this at `/internal-api/db/tx`, for tenant code that ran in an isolate
|
|
@@ -703,7 +485,7 @@ interface RequestDatabase {
|
|
|
703
485
|
*
|
|
704
486
|
* The obvious repair — re-issue `set_config('role', …)` around each service op
|
|
705
487
|
* — is worse than the bug. Two statements are not one: `Promise.all([
|
|
706
|
-
* Database
|
|
488
|
+
* Database.$query(…), Database.$asService().$query(…) ])` interleaves them on the
|
|
707
489
|
* single connection, and the user's query can execute between the service's
|
|
708
490
|
* set-role and its own statement. That is RLS silently OFF on the DEFAULT path,
|
|
709
491
|
* which is precisely the direction a security seam must never fail.
|
|
@@ -745,6 +527,91 @@ declare function createRequestDatabase(sql: SqlDriver, identity: {
|
|
|
745
527
|
claimsJson: string;
|
|
746
528
|
}): RequestDatabase;
|
|
747
529
|
|
|
530
|
+
/**
|
|
531
|
+
* How a container refuses.
|
|
532
|
+
*
|
|
533
|
+
* Every refusal is one of these, and every one names the class involved. There
|
|
534
|
+
* is deliberately no "unknown" member: a failure this list cannot classify is a
|
|
535
|
+
* failure the error surface has not been taught to explain, and that is a defect
|
|
536
|
+
* rather than a category.
|
|
537
|
+
*/
|
|
538
|
+
type DiKind = "unresolvable dependency" | "private dependency" | "missing import" | "unowned class" | "dependency cycle" | "metadata missing" | "generic dependency" | "duplicate ownership" | "unknown export" | "unknown import" | "undeclared provider";
|
|
539
|
+
/**
|
|
540
|
+
* A refusal carries four parts: what kind, the full resolution path, where
|
|
541
|
+
* exactly, and what to do about it.
|
|
542
|
+
*
|
|
543
|
+
* The shape is borrowed on purpose — Angular's path, Awilix's failure kind, and
|
|
544
|
+
* Nest's list of potential solutions — because each of the three answers a
|
|
545
|
+
* question the other two leave open: what broke, where in the graph, and what
|
|
546
|
+
* the author should type next.
|
|
547
|
+
*/
|
|
548
|
+
declare class DiError extends Error {
|
|
549
|
+
readonly kind: DiKind;
|
|
550
|
+
readonly path: string[];
|
|
551
|
+
readonly at: string;
|
|
552
|
+
readonly fixes: string[];
|
|
553
|
+
constructor(kind: DiKind, path: string[], at: string, detail: string, fixes: string[]);
|
|
554
|
+
}
|
|
555
|
+
/** How often a module appears in the OTHERS' `imports`. */
|
|
556
|
+
interface ModulePressure {
|
|
557
|
+
module: string;
|
|
558
|
+
pct: number;
|
|
559
|
+
/**
|
|
560
|
+
* How many OTHER modules there were — the denominator.
|
|
561
|
+
*
|
|
562
|
+
* Without it the percentage cannot be read. Measured 2026-09-02: a project
|
|
563
|
+
* with two modules, one importing the other, reports 100% — which is true and
|
|
564
|
+
* says nothing, because "all of the others" is one module. A reader deciding
|
|
565
|
+
* whether a module has become ambient needs to know whether 80% was four
|
|
566
|
+
* modules or one.
|
|
567
|
+
*/
|
|
568
|
+
of: number;
|
|
569
|
+
}
|
|
570
|
+
interface Container {
|
|
571
|
+
get<T>(t: Token<T>): T;
|
|
572
|
+
/** Every class a module claimed — the set an entry point must be in. */
|
|
573
|
+
readonly owned: ReadonlySet<Token>;
|
|
574
|
+
/**
|
|
575
|
+
* Where `@Global()` pressure is accumulating.
|
|
576
|
+
*
|
|
577
|
+
* A module that appears in more than ~80% of the others' `imports` is one the
|
|
578
|
+
* design is asking to be ambient. That is a JUDGEMENT — how much sharing is
|
|
579
|
+
* too much depends on the domain — so this is reported as a number and never
|
|
580
|
+
* enforced as a gate. Lives on the container rather than in a module-level
|
|
581
|
+
* variable: the runtime builds a candidate's container beside the live app's,
|
|
582
|
+
* and a shared variable would have one overwrite the other's report.
|
|
583
|
+
*/
|
|
584
|
+
readonly pressure: readonly ModulePressure[];
|
|
585
|
+
}
|
|
586
|
+
/**
|
|
587
|
+
* Validates the declared modules and returns a container over them.
|
|
588
|
+
*
|
|
589
|
+
* Validation runs in a fixed order, and the order is the point: each stage may
|
|
590
|
+
* assume the previous one held, so a message never has to hedge. Ownership
|
|
591
|
+
* before exports, exports before dependencies, dependencies before cycles,
|
|
592
|
+
* cycles before construction.
|
|
593
|
+
*
|
|
594
|
+
* Stages 3-5 (dependencies and visibility, cycles, resolution) are added by the
|
|
595
|
+
* tasks that follow; this file grows, it is not replaced.
|
|
596
|
+
*/
|
|
597
|
+
declare function buildContainer(): Container;
|
|
598
|
+
|
|
599
|
+
/**
|
|
600
|
+
* Refuses an entry point that no module lists (FR-035).
|
|
601
|
+
*
|
|
602
|
+
* A decorated class registers itself — `@Controller` pushes into a globalThis
|
|
603
|
+
* slot the moment its file is imported — so before this check a class listed in
|
|
604
|
+
* no module still reached the route table, the dispatcher and the OpenAPI
|
|
605
|
+
* document. It worked, which is the problem: nothing said the module system had
|
|
606
|
+
* been bypassed.
|
|
607
|
+
*
|
|
608
|
+
* After this, a successful boot means the two sets are EQUAL: what decorated
|
|
609
|
+
* itself and what a module claimed. That equality is what lets `src/openapi/`
|
|
610
|
+
* stay untouched — it renders the list it is handed, and the list is now the
|
|
611
|
+
* module's.
|
|
612
|
+
*/
|
|
613
|
+
declare function assertNoOrphanEntryPoints(registered: readonly unknown[], owned: ReadonlySet<Token>): void;
|
|
614
|
+
|
|
748
615
|
interface RouteEntry {
|
|
749
616
|
method: string;
|
|
750
617
|
/** Path segments; a parameter segment is stored as `:name`. */
|
|
@@ -770,7 +637,7 @@ interface RouteEntry {
|
|
|
770
637
|
* silence one level down (FR-010): nothing here has an argument to pass, so
|
|
771
638
|
* the field would simply be `undefined` in production.
|
|
772
639
|
*/
|
|
773
|
-
declare function buildRouteTable(controllers: readonly unknown[]): RouteEntry[];
|
|
640
|
+
declare function buildRouteTable(controllers: readonly unknown[], container: Container): RouteEntry[];
|
|
774
641
|
interface RouteMatch {
|
|
775
642
|
entry: RouteEntry;
|
|
776
643
|
params: Record<string, string>;
|
|
@@ -1045,6 +912,14 @@ interface App {
|
|
|
1045
912
|
runInServiceScope: <T>(fn: () => T | Promise<T>) => Promise<T>;
|
|
1046
913
|
/** Close the pool and release resources. */
|
|
1047
914
|
shutdown: () => Promise<void>;
|
|
915
|
+
/**
|
|
916
|
+
* The container this app resolved its classes from.
|
|
917
|
+
*
|
|
918
|
+
* Exposed because entry points the ENGINE does not own are built elsewhere:
|
|
919
|
+
* the runtime constructs rooms, jobs and hooks, and they must come from THIS
|
|
920
|
+
* app's graph rather than a second one built beside it (FR-037, FR-042).
|
|
921
|
+
*/
|
|
922
|
+
readonly container: Container;
|
|
1048
923
|
}
|
|
1049
924
|
/**
|
|
1050
925
|
* Build the app. Fails fast: the database is reached here, at boot, rather than
|
|
@@ -1052,4 +927,4 @@ interface App {
|
|
|
1052
927
|
*/
|
|
1053
928
|
declare function createApp(opts: CreateAppOptions): Promise<App>;
|
|
1054
929
|
|
|
1055
|
-
export { type App as A, BootRefused as B,
|
|
930
|
+
export { type App as A, BootRefused as B, type Container as C, DiError as D, type EgressPolicy as E, type ModuleClients as M, RateLimiter as R, type ScrubResult as S, type DiKind as a, type ModulePressure as b, assertNoOrphanEntryPoints as c, buildContainer as d, AuthVerifier as e, type CreateAppOptions as f, type EngineConfig as g, type RequestDatabase as h, type RouteEntry as i, type RuntimeHooks as j, type SqlDriver as k, type SqlTx as l, buildRouteTable as m, createApp as n, createLazyTransaction as o, createOps as p, createRequestDatabase as q, effectiveAuth as r, hostAllowed as s, installEgressFence as t, loadConfig as u, makeMemoryCache as v, matchRoute as w, quoteIdent as x, scrubSecrets as y, withTables as z };
|