@palbase/backend 23.0.0 → 24.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 (74) hide show
  1. package/dist/bin/palbase-backend.cjs +695 -61
  2. package/dist/bin/palbase-backend.cjs.map +1 -1
  3. package/dist/bin/palbase-backend.js +4 -5
  4. package/dist/bin/palbase-backend.js.map +1 -1
  5. package/dist/{chunk-FSGSB42K.js → chunk-7Z6MGMXQ.js} +64 -4
  6. package/dist/chunk-7Z6MGMXQ.js.map +1 -0
  7. package/dist/{chunk-OMRTHM4X.js → chunk-H3JAISUY.js} +136 -1
  8. package/dist/chunk-H3JAISUY.js.map +1 -0
  9. package/dist/{chunk-REZU6UKT.js → chunk-NXDH6VQJ.js} +549 -42
  10. package/dist/chunk-NXDH6VQJ.js.map +1 -0
  11. package/dist/{chunk-W5ODXPY3.js → chunk-P2Q27SGP.js} +32 -3
  12. package/dist/chunk-P2Q27SGP.js.map +1 -0
  13. package/dist/{chunk-ZC6Q2BRD.js → chunk-T5IOSOE5.js} +7 -2
  14. package/dist/chunk-T5IOSOE5.js.map +1 -0
  15. package/dist/{chunk-HAF67F2H.js → chunk-ZUGY7RGS.js} +86 -3
  16. package/dist/chunk-ZUGY7RGS.js.map +1 -0
  17. package/dist/db/index.cjs +115 -3
  18. package/dist/db/index.cjs.map +1 -1
  19. package/dist/db/index.d.cts +2 -2
  20. package/dist/db/index.d.ts +2 -2
  21. package/dist/db/index.js +2 -2
  22. package/dist/{endpoint-BavvbW4P.d.ts → endpoint-0_DGBajf.d.ts} +168 -9
  23. package/dist/{endpoint-i8TTCohk.d.cts → endpoint-CcQ1a36a.d.cts} +168 -9
  24. package/dist/engine/index.cjs +684 -48
  25. package/dist/engine/index.cjs.map +1 -1
  26. package/dist/engine/index.d.cts +4 -4
  27. package/dist/engine/index.d.ts +4 -4
  28. package/dist/engine/index.js +4 -4
  29. package/dist/{index-B3jmmItD.d.ts → index-CJiJU9ux.d.ts} +209 -36
  30. package/dist/{index-Bmvx1EvJ.d.cts → index-D-4-PNuQ.d.cts} +209 -36
  31. package/dist/{index-B7YBEG5w.d.ts → index-D17r-MKb.d.ts} +177 -7
  32. package/dist/{index-E7OscPJT.d.cts → index-DRFxf07H.d.cts} +177 -7
  33. package/dist/index.cjs +269 -7
  34. package/dist/index.cjs.map +1 -1
  35. package/dist/index.d.cts +54 -12
  36. package/dist/index.d.ts +54 -12
  37. package/dist/index.js +54 -11
  38. package/dist/index.js.map +1 -1
  39. package/dist/openapi/index.cjs +45 -5
  40. package/dist/openapi/index.cjs.map +1 -1
  41. package/dist/openapi/index.d.cts +9 -4
  42. package/dist/openapi/index.d.ts +9 -4
  43. package/dist/openapi/index.js +35 -11
  44. package/dist/openapi/index.js.map +1 -1
  45. package/dist/{registry-C3H2uPeZ.d.cts → registry-1X-skBNu.d.cts} +101 -7
  46. package/dist/{registry-DY3d9l1k.d.ts → registry-CEod_5sz.d.ts} +101 -7
  47. package/dist/test/index.cjs +509 -9
  48. package/dist/test/index.cjs.map +1 -1
  49. package/dist/test/index.d.cts +35 -3
  50. package/dist/test/index.d.ts +35 -3
  51. package/dist/test/index.js +507 -8
  52. package/dist/test/index.js.map +1 -1
  53. package/docs/README.md +4 -4
  54. package/docs/database.md +115 -11
  55. package/docs/getting-started.md +5 -4
  56. package/docs/llms-full.txt +385 -89
  57. package/docs/migrations.md +81 -59
  58. package/docs/schema.md +82 -2
  59. package/docs/services.md +98 -9
  60. package/package.json +3 -2
  61. package/stager/return_types.js +23 -0
  62. package/template/AGENTS.md +121 -41
  63. package/template/controllers/notes.controller.ts +64 -0
  64. package/template/package.json +1 -1
  65. package/template/services/note.service.ts +74 -0
  66. package/template/tsconfig.json +11 -1
  67. package/dist/chunk-FSGSB42K.js.map +0 -1
  68. package/dist/chunk-HAF67F2H.js.map +0 -1
  69. package/dist/chunk-OMRTHM4X.js.map +0 -1
  70. package/dist/chunk-REZU6UKT.js.map +0 -1
  71. package/dist/chunk-W5ODXPY3.js.map +0 -1
  72. package/dist/chunk-Y5HXVUMP.js +0 -90
  73. package/dist/chunk-Y5HXVUMP.js.map +0 -1
  74. package/dist/chunk-ZC6Q2BRD.js.map +0 -1
@@ -1,8 +1,8 @@
1
1
  import { Buckets, BucketTypes } from './stack.cjs';
2
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, T as TxPlanBody, f as TxPlanResponse } from './endpoint-i8TTCohk.cjs';
4
- import { E as EnvTypedDatabase } from './index-Bmvx1EvJ.cjs';
5
- import { R as RouteMeta } from './registry-C3H2uPeZ.cjs';
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-CcQ1a36a.cjs';
4
+ import { E as EnvTypedDatabase } from './index-D-4-PNuQ.cjs';
5
+ import { R as RouteMeta } from './registry-1X-skBNu.cjs';
6
6
 
7
7
  /**
8
8
  * runtime.ts — request-scoped service singletons.
@@ -112,6 +112,72 @@ declare function __runWithRuntime<T>(services: RuntimeServices, fn: () => T): T;
112
112
  * process-global fallback (dev-server / tests). NOT part of the public
113
113
  * author-facing API — used by the runtime and the singleton Proxies. */
114
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;
115
181
  /**
116
182
  * The project's own Postgres (pgx, schema `env_<envId>`).
117
183
  *
@@ -361,14 +427,79 @@ declare function createLazyTransaction(sql: SqlDriver, role: string, claimsJson:
361
427
  type LazyTransaction = ReturnType<typeof createLazyTransaction>;
362
428
  /** Either a live driver transaction or the lazy holder above. */
363
429
  type TxLike = SqlTx | LazyTransaction;
430
+ /** What `findMany` accepts beside its filter: an ordering, a row ceiling and a
431
+ * page offset. All three used to require dropping to raw SQL, and the docs said
432
+ * so — which is how a tenant's controllers filled up with hand-written SELECTs. */
433
+ interface FindManyOptions {
434
+ orderBy?: {
435
+ column: string;
436
+ direction?: "asc" | "desc";
437
+ };
438
+ limit?: number;
439
+ /** Rows to skip before the page starts. Only meaningful with `limit`, and
440
+ * refused without it — see `offsetClause`. */
441
+ offset?: number;
442
+ }
364
443
  /** The six string-keyed operations, plus an interactive `transaction`. */
365
444
  declare function createOps(tx: TxLike): {
366
445
  query(sql: string, params?: unknown[]): Promise<Row[]>;
367
446
  insert(table: string, data: Row): Promise<Row>;
447
+ /**
448
+ * INSERT the row, or UPDATE it when it collides on `onConflict`.
449
+ *
450
+ * WHY IT IS AN OPERATION rather than a recipe. "Try the insert, catch the
451
+ * unique violation, update instead" does not work here: a request runs in ONE
452
+ * Postgres transaction, so the failed insert aborts it and every later
453
+ * statement answers `current transaction is aborted`. A tenant measured that
454
+ * as 7 of 8 concurrent requests returning 500, gave up on upsert, and had a
455
+ * trigger create the row instead — a workaround that needs a new trigger for
456
+ * every table with a unique row.
457
+ *
458
+ * The conflict columns are excluded from the SET list: they are what MATCHED,
459
+ * so writing them back is at best a no-op and at worst a surprise.
460
+ */
461
+ upsert(table: string, data: Row, opts: {
462
+ onConflict: readonly string[];
463
+ }): Promise<Row>;
368
464
  update(table: string, id: string, data: Row): Promise<Row | null>;
369
465
  delete(table: string, id: string): Promise<void>;
466
+ /**
467
+ * Update every row the filter matches, in ONE statement.
468
+ *
469
+ * The capability was already here and only reachable from inside a
470
+ * transaction plan (`tx.tables.x.updateWhere`, since 11.0.0). Outside it the
471
+ * only door was `update(id, …)`, so "mark every unapproved row in this
472
+ * household" was an N+1 loop or hand-written SQL — and hand-written SQL is
473
+ * where the typed surface and RLS both stop helping.
474
+ *
475
+ * The filter language is `findMany`'s, compiled by the same `compileWhere`:
476
+ * one filter language, or the two spellings drift.
477
+ *
478
+ * AN EMPTY FILTER IS REFUSED. `UPDATE … WHERE true` is a whole-table write,
479
+ * and the shape that produces it by accident — a filter object built from
480
+ * request input that happened to come back empty — is exactly the shape that
481
+ * should not silently succeed. Callers who mean every row say so with a
482
+ * predicate that is true for every row.
483
+ */
484
+ updateMany(table: string, where: Row, set: Row): Promise<Row[]>;
485
+ /**
486
+ * Delete every row the filter matches, in ONE statement; resolves to how
487
+ * many went. Same filter language, same empty-filter refusal as
488
+ * {@link updateMany} — and here the accident is worse.
489
+ */
490
+ deleteMany(table: string, where: Row): Promise<number>;
491
+ /**
492
+ * How many rows match — the half of pagination `limit`/`offset` cannot
493
+ * supply. Without it a page count is either a guess or "fetch everything and
494
+ * read .length", and the second one is the scale risk this surface exists to
495
+ * remove.
496
+ *
497
+ * An empty filter is legitimate HERE: counting a whole table is a read, and
498
+ * reads do not destroy anything.
499
+ */
500
+ count(table: string, where?: Row): Promise<number>;
370
501
  findById(table: string, id: string): Promise<Row | null>;
371
- findMany(table: string, query?: Row): Promise<Row[]>;
502
+ findMany(table: string, query?: Row, opts?: FindManyOptions): Promise<Row[]>;
372
503
  /**
373
504
  * Tek-SQL hibrit arama (FR-014): iki kol CTE + FULL OUTER JOIN + RRF
374
505
  * (1/(50+rank), CLAIM-N4). Operatör şema-nitelikli (C-10, M-1); GUC
@@ -417,6 +548,21 @@ declare function createOps(tx: TxLike): {
417
548
  * tek SQL'de "id yok" ile "0 komşu" ayrılamazdı; +1 küçük turla id-yokluğu
418
549
  * adlandırılmış hataya çevrilir. Sonuç şekli search ile aynı (FR-015).
419
550
  */
551
+ /** D-021 (FR-027 DX): sayaçlar BAĞIMSIZ op'la — search'ün dizi-üstü
552
+ * `_facets` özelliği JSON.stringify'da kaybolur (dizi özelliği), tenant
553
+ * yanıtına koyunca sessizce yok olurdu. Ayrı dönüş ciddi bir sözleşmedir;
554
+ * search'teki alan geriye-uyum için DURUR. where + validity default'u
555
+ * sayaçlara da uygulanır (sayaç, kullanıcının gördüğü kümeyi anlatır). */
556
+ facets(table: string, params: {
557
+ facets: string[];
558
+ where?: Record<string, unknown>;
559
+ validity?: "all" | {
560
+ asOf: string;
561
+ };
562
+ }): Promise<Record<string, {
563
+ value: string | null;
564
+ count: number;
565
+ }[]>>;
420
566
  similar(table: string, id: string, opts?: {
421
567
  where?: Record<string, unknown>;
422
568
  limit?: number;
@@ -476,6 +622,24 @@ declare function createOps(tx: TxLike): {
476
622
  supersede(table: string, id: string, row: Row): Promise<Row>;
477
623
  /** A real SAVEPOINT inside the request's transaction. */
478
624
  transaction<T>(cb: (t: unknown) => Promise<T>): Promise<T>;
625
+ /**
626
+ * Run `fn` against a handle bound to a SAVEPOINT, so a failure inside it
627
+ * rolls back only what that handle wrote and the request can keep writing.
628
+ *
629
+ * WHY THE HANDLE IS AN ARGUMENT. The obvious shape — `attempt(async () => {
630
+ * ... Database.insert(...) ... })`, with no parameter — would have to point
631
+ * 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
634
+ * back with it. Silent data loss, and the same interleaving this file already
635
+ * refuses for `asService()`. Passing the handle makes the boundary something
636
+ * you can see in the code that crosses it.
637
+ *
638
+ * Postgres, not us: the savepoint is released on success and rolled back on
639
+ * failure by the driver, so an aborted statement inside `fn` does not poison
640
+ * the surrounding transaction.
641
+ */
642
+ attempt<T>(fn: (tx: DBOps) => Promise<T>): Promise<T>;
479
643
  /**
480
644
  * Execute a whole transaction plan — what `Database.transaction(fn)` builds.
481
645
  *
@@ -587,8 +751,11 @@ interface RouteEntry {
587
751
  instance: Record<string, (...args: unknown[]) => unknown>;
588
752
  /** `GET /todos/{id}` — stable, human-readable, used as the rate-limit key. */
589
753
  id: string;
590
- /** The controller's `auth` default, if it declared one. */
591
- controllerAuth: unknown;
754
+ /** The auth spec that applies when the route itself declares none —
755
+ * controller default ?? application default ?? `true`, resolved once at boot
756
+ * by `resolveEffectiveAuth`. `engine/index.ts` reconciles the route's own
757
+ * spec against it per request. */
758
+ controllerAuth: AuthSpec;
592
759
  }
593
760
  /**
594
761
  * Build the table from controller classes.
@@ -596,6 +763,9 @@ interface RouteEntry {
596
763
  * @throws when a class carries no routes — a controller that collected zero
597
764
  * endpoints is the silent failure this whole runtime is built to refuse, and
598
765
  * it must be loud at boot rather than a 404 in production.
766
+ * @throws when a class declares constructor parameters — the same class of
767
+ * silence one level down (FR-010): nothing here has an argument to pass, so
768
+ * the field would simply be `undefined` in production.
599
769
  */
600
770
  declare function buildRouteTable(controllers: readonly unknown[]): RouteEntry[];
601
771
  interface RouteMatch {
@@ -879,4 +1049,4 @@ interface App {
879
1049
  */
880
1050
  declare function createApp(opts: CreateAppOptions): Promise<App>;
881
1051
 
882
- export { type App as A, BootRefused as B, Cache as C, Database as D, type EgressPolicy as E, Flags as F, makeMemoryCache as G, matchRoute as H, quoteIdent as I, scrubSecrets as J, withTables as K, Log as L, type ModuleClients as M, Notifications as N, Realtime as R, Secrets as S, __getRuntime as _, Documents as a, type RequestStore as b, type RuntimeServices as c, Storage as d, __requestALS as e, __runWithRuntime as f, __setRuntime as g, AuthVerifier as h, type CreateAppOptions as i, type EngineConfig as j, RateLimiter as k, type RequestDatabase as l, type RouteEntry as m, type RuntimeHooks as n, type ScrubResult as o, type SqlDriver as p, type SqlTx as q, buildRouteTable as r, createApp as s, createLazyTransaction as t, createOps as u, createRequestDatabase as v, effectiveAuth as w, hostAllowed as x, installEgressFence as y, loadConfig as z };
1052
+ 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 };