@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.
Files changed (104) hide show
  1. package/dist/bin/palbase-backend.cjs +2401 -1039
  2. package/dist/bin/palbase-backend.cjs.map +1 -1
  3. package/dist/bin/palbase-backend.js +87 -51
  4. package/dist/bin/palbase-backend.js.map +1 -1
  5. package/dist/chunk-CGNN2PUH.js +213 -0
  6. package/dist/chunk-CGNN2PUH.js.map +1 -0
  7. package/dist/chunk-DRZFQRJI.js +370 -0
  8. package/dist/chunk-DRZFQRJI.js.map +1 -0
  9. package/dist/chunk-GYK6QYS4.js +708 -0
  10. package/dist/chunk-GYK6QYS4.js.map +1 -0
  11. package/dist/chunk-I3C4PFIW.js +930 -0
  12. package/dist/chunk-I3C4PFIW.js.map +1 -0
  13. package/dist/{chunk-VDF2T4AS.js → chunk-OO7R25AI.js} +1213 -591
  14. package/dist/chunk-OO7R25AI.js.map +1 -0
  15. package/dist/chunk-TS4U7NBD.js +885 -0
  16. package/dist/chunk-TS4U7NBD.js.map +1 -0
  17. package/dist/{chunk-YOY5DFQS.js → chunk-TWX6JTGJ.js} +76 -34
  18. package/dist/{chunk-YOY5DFQS.js.map → chunk-TWX6JTGJ.js.map} +1 -1
  19. package/dist/{chunk-35PNTIRN.js → chunk-VVMJEVQP.js} +63 -162
  20. package/dist/chunk-VVMJEVQP.js.map +1 -0
  21. package/dist/{chunk-7D4SUZUM.js → chunk-VXPNPVAG.js} +3 -1
  22. package/dist/db/env.cjs.map +1 -1
  23. package/dist/db/env.d.cts +2 -2
  24. package/dist/db/env.d.ts +2 -2
  25. package/dist/db/index.cjs +754 -344
  26. package/dist/db/index.cjs.map +1 -1
  27. package/dist/db/index.d.cts +2 -2
  28. package/dist/db/index.d.ts +2 -2
  29. package/dist/db/index.js +7 -4
  30. package/dist/engine/index.cjs +2339 -1006
  31. package/dist/engine/index.cjs.map +1 -1
  32. package/dist/engine/index.d.cts +6 -6
  33. package/dist/engine/index.d.ts +6 -6
  34. package/dist/engine/index.js +7 -6
  35. package/dist/index-BrvvxSpn.d.ts +4844 -0
  36. package/dist/index-Bve7BBTL.d.cts +4844 -0
  37. package/dist/{index-CUomTA3e.d.ts → index-NuzRCuxe.d.ts} +171 -296
  38. package/dist/{index-ClpDeSos.d.cts → index-VtToZmUm.d.cts} +171 -296
  39. package/dist/index.cjs +2694 -1169
  40. package/dist/index.cjs.map +1 -1
  41. package/dist/index.d.cts +165 -19
  42. package/dist/index.d.ts +165 -19
  43. package/dist/index.js +738 -477
  44. package/dist/index.js.map +1 -1
  45. package/dist/module-Dl1KFVtc.d.cts +54 -0
  46. package/dist/module-Dl1KFVtc.d.ts +54 -0
  47. package/dist/openapi/index.cjs +1330 -484
  48. package/dist/openapi/index.cjs.map +1 -1
  49. package/dist/openapi/index.d.cts +4 -2
  50. package/dist/openapi/index.d.ts +4 -2
  51. package/dist/openapi/index.js +1264 -474
  52. package/dist/openapi/index.js.map +1 -1
  53. package/dist/{registry-dZZ5JKYg.d.ts → registry-B0eyOF9x.d.ts} +1 -1
  54. package/dist/{registry-CC0WBQq6.d.cts → registry-Bk9_rbNd.d.cts} +1 -1
  55. package/dist/stack.cjs.map +1 -1
  56. package/dist/test/index.cjs +705 -141
  57. package/dist/test/index.cjs.map +1 -1
  58. package/dist/test/index.d.cts +30 -4
  59. package/dist/test/index.d.ts +30 -4
  60. package/dist/test/index.js +472 -124
  61. package/dist/test/index.js.map +1 -1
  62. package/docs/README.md +33 -18
  63. package/docs/auth.md +1 -1
  64. package/docs/background.md +2 -2
  65. package/docs/database.md +221 -50
  66. package/docs/endpoints.md +3 -4
  67. package/docs/events.md +3 -3
  68. package/docs/getting-started.md +1 -1
  69. package/docs/llms-full.txt +401 -117
  70. package/docs/migrations.md +2 -2
  71. package/docs/schema.md +19 -10
  72. package/docs/services.md +116 -26
  73. package/package.json +8 -4
  74. package/stager/generics.js +205 -0
  75. package/stager/stage.js +39 -3
  76. package/template/AGENTS.md +110 -72
  77. package/template/db/public.ts +1 -1
  78. package/template/{controllers → modules/health}/health.controller.ts +1 -1
  79. package/template/modules/health/health.module.ts +24 -0
  80. package/template/modules/notes/note.service.test.ts +49 -0
  81. package/template/modules/notes/note.service.ts +108 -0
  82. package/template/{controllers → modules/notes}/notes.controller.ts +17 -11
  83. package/template/modules/notes/notes.module.ts +37 -0
  84. package/template/package.json +5 -3
  85. package/template/scripts/test.sh +33 -0
  86. package/template/tsconfig.json +29 -30
  87. package/dist/chunk-35PNTIRN.js.map +0 -1
  88. package/dist/chunk-CJSKYY76.js +0 -627
  89. package/dist/chunk-CJSKYY76.js.map +0 -1
  90. package/dist/chunk-CRQKCRGF.js +0 -276
  91. package/dist/chunk-CRQKCRGF.js.map +0 -1
  92. package/dist/chunk-G4R6BTLV.js +0 -662
  93. package/dist/chunk-G4R6BTLV.js.map +0 -1
  94. package/dist/chunk-VDF2T4AS.js.map +0 -1
  95. package/dist/chunk-XABBC7JP.js +0 -55
  96. package/dist/chunk-XABBC7JP.js.map +0 -1
  97. package/dist/endpoint-CTEHhb7A.d.ts +0 -2386
  98. package/dist/endpoint-DYHMo6cC.d.cts +0 -2386
  99. package/dist/index-CW21M9Z3.d.ts +0 -1222
  100. package/dist/index-CmBK76nx.d.cts +0 -1222
  101. package/template/services/note.service.test.ts +0 -45
  102. package/template/services/note.service.ts +0 -76
  103. /package/dist/{chunk-7D4SUZUM.js.map → chunk-VXPNPVAG.js.map} +0 -0
  104. /package/template/{models/notes → modules/notes/dto}/create.ts +0 -0
@@ -1,285 +1,6 @@
1
- import { Buckets, BucketTypes } from './stack.js';
2
- import { AsyncLocalStorage } from 'node:async_hooks';
3
- import { C as CacheClient, P as PalbaseDocsClient, a as PalbaseFlagsClient, L as Logger, b as PalbaseNotificationsClient, c as PalbaseRealtimeClient, D as DBClient, S as SecretsService, d as PalbaseStorageClient, e as PalbaseBucketClient, f as DBOps, T as TxPlanBody, g as TxPlanResponse, A as AuthSpec } from './endpoint-CTEHhb7A.js';
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.asService()` is bound to. It is the one that
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<Row[]>;
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
- upsert(table: string, data: Row, opts: {
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.insert(...) ... })`, with no parameter — would have to point
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.insert(a),
633
- * Database.attempt(...)])` would put `a` inside the savepoint and roll it
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.transaction(fn)` builds.
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.query(…), Database.asService().query(…) ])` interleaves them on the
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, Cache as C, Database as D, type EgressPolicy as E, Flags as F, createOps as G, createRequestDatabase as H, effectiveAuth as I, hostAllowed as J, installEgressFence as K, type LifecycleHook as L, type ModuleClients as M, Notifications as N, loadConfig as O, makeMemoryCache as P, matchRoute as Q, Realtime as R, Secrets as S, quoteIdent as T, scrubSecrets as U, withTables as V, __getRuntime as _, Documents as a, Log as b, type RequestStore as c, type RuntimeServices as d, type ShutdownRunner as e, Storage as f, __requestALS as g, __resetLifecycleHooks as h, __runStartHooks as i, __runWithRuntime as j, __setRuntime as k, onStart as l, AuthVerifier as m, type CreateAppOptions as n, onShutdown as o, type EngineConfig as p, RateLimiter as q, type RequestDatabase as r, type RouteEntry as s, type RuntimeHooks as t, type ScrubResult as u, type SqlDriver as v, type SqlTx as w, buildRouteTable as x, createApp as y, createLazyTransaction as z };
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 };