@palbase/backend 10.3.0 → 12.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.
@@ -687,7 +687,7 @@ available:
687
687
  | `Database.findById(table, id)` | the row or `null` |
688
688
  | `Database.findMany(table, query?)` | matching rows (array) |
689
689
  | `Database.query(sql, params?)` | rows from a read-only SQL query (runs in a READ ONLY transaction) |
690
- | `Database.transaction(fn)` | runs `fn(tx)` in a transaction |
690
+ | `Database.transaction(fn)` | runs a whole transaction plan in one request |
691
691
 
692
692
  `findMany`'s `query` is an equality filter: keys are ANDed together. For
693
693
  anything richer (ranges, ordering, joins) use `Database.query`.
@@ -704,19 +704,108 @@ const rows = await Database.query(
704
704
 
705
705
  ## Transactions
706
706
 
707
- `transaction(fn)` gives you a `tx` with the same DB ops (no nested
708
- transaction). Returning commits; throwing rolls back.
707
+ A transaction is a **plan**, not a conversation. The callback DESCRIBES the
708
+ operations; the whole description travels in one request, and the broker runs it
709
+ inside a single transaction — committing when it finishes, rolling back on any
710
+ failure. That is unchanged. What changed is that no round trip happens in the
711
+ middle, so nothing holds a database connection open while your code thinks.
709
712
 
710
713
  ```ts
711
- await Database.transaction(async (tx) => {
712
- const order = await tx.tables.orders.insert({ amount: 1000, status: "pending" });
713
- await tx.tables.order_items.insert({ order_id: order.id, sku: "ABC" });
714
- // throw here → both inserts roll back
714
+ import { Database, NotFound } from "@palbase/backend";
715
+
716
+ const { orderId } = await Database.transaction((tx) => {
717
+ const order = tx.tables.orders
718
+ .insert({ amount: 1000, status: "pending" })
719
+ .expectOne(new NotFound("order could not be created"));
720
+
721
+ tx.tables.order_items.insertMany(
722
+ cart.map((line) => ({ order_id: order.id, sku: line.sku })),
723
+ );
724
+
725
+ return { orderId: order.id };
715
726
  });
716
727
  ```
717
728
 
718
- The `tx` carries the same typed `tx.tables.<name>` API as `Database.tables`
719
- (no nested transaction). See [schema.md](./schema.md) for the full surface.
729
+ Four things follow from "it is a plan", and the compiler enforces all four.
730
+
731
+ **The callback is synchronous.** Nothing has run when it returns, so there is
732
+ nothing to await. `async` on the callback and `await` inside it are compile
733
+ errors.
734
+
735
+ **An operation returns a result handle, not a row.** `insert()` gives you a
736
+ `TxRows`; to read a column you first say what you expect:
737
+
738
+ | Expectation | Holds when | Gives you |
739
+ |---|---|---|
740
+ | `.expectOne(err)` | exactly 1 row | the row — the only way to read columns |
741
+ | `.expectNone(err)` | 0 rows | nothing |
742
+ | `.expectAtLeast(n, err)` | ≥ n rows | nothing |
743
+ | `.expectAtMost(n, err)` | ≤ n rows | nothing |
744
+
745
+ If an expectation does not hold, the whole transaction rolls back and **your**
746
+ `err` is thrown — the error object stays on this side, the server only reports
747
+ which expectation failed.
748
+
749
+ **A column you read is a `Ref`, not a value.** It is a promise of what the
750
+ server will produce. Write it into a later operation, or return it and read the
751
+ real value after `transaction()` resolves. You cannot print it, concatenate it,
752
+ do arithmetic on it, or `JSON.stringify` it — all of those throw.
753
+
754
+ > ⚠️ **`if (ref)` is always true.** JavaScript does not let a Ref refuse a
755
+ > truthiness test, so branching on one silently takes the wrong path and commits
756
+ > the wrong data. Never branch on a value the transaction has not produced yet:
757
+ > read it before the transaction, or express the condition as a filter plus an
758
+ > expectation.
759
+
760
+ **Reads you do not write move outside.** A lookup whose result the transaction
761
+ does not write is not part of the transaction:
762
+
763
+ ```ts
764
+ // Before the transaction: an ordinary value you can branch on.
765
+ const overrides = await Database.tables.category_overrides.findMany({ household_id });
766
+
767
+ const stmt = await Database.transaction((tx) => { /* … */ });
768
+ ```
769
+
770
+ ### Writing conditions as filters
771
+
772
+ `if (row.accepted_at) throw new Conflict(…)` needs a real value, so it becomes a
773
+ filter plus an expectation — which is also stronger, because the check and the
774
+ write are now the same statement and nothing can slip between them:
775
+
776
+ ```ts
777
+ tx.tables.invites
778
+ .updateWhere({ token, accepted_at: null }, { accepted_at: now() })
779
+ .expectOne(new Conflict("invite already used", "invite_used"));
780
+ ```
781
+
782
+ ### The table surface inside a transaction
783
+
784
+ | Operation | Notes |
785
+ |---|---|
786
+ | `insert(values)` | one row |
787
+ | `insertMany(rows)` | one statement; every row must set the same columns. An empty list writes nothing |
788
+ | `updateWhere(where, set)` | **filter first**. A filterless update is refused |
789
+ | `deleteWhere(where)` | a filterless delete is refused |
790
+ | `select(where?, { limit, lock })` | `lock: "update"` takes a real `FOR UPDATE` row lock |
791
+
792
+ A `where` is equality-only and ANDed; `null` means `IS NULL`.
793
+
794
+ Three expressions may appear in the values you write:
795
+
796
+ | Expression | Where | Meaning |
797
+ |---|---|---|
798
+ | `now()` | anywhere | the server's clock |
799
+ | `inc(n)` | `updateWhere`'s `set` | `column = column + n`, atomically |
800
+ | `dec(n)` | `updateWhere`'s `set` | `column = column - n`, atomically |
801
+
802
+ `inc`/`dec` read the column's current value, which an inserted row does not
803
+ have — using them in an `insert` is a compile error.
804
+
805
+ ### Limits
806
+
807
+ A plan may carry at most 1000 operations, 5000 rows in one `insertMany`, and
808
+ 8 MiB of JSON. Exceeding any of them is reported before the request is sent.
720
809
 
721
810
  ## Bypassing RLS — `Database.asService()`
722
811
 
@@ -741,9 +830,10 @@ const mine = await Database.tables.todos.findMany({});
741
830
  const all = await Database.asService().tables.todos.findMany({});
742
831
  const rows = await Database.asService().query("SELECT count(*) FROM todos");
743
832
 
744
- // A service-role transaction (the role is fixed for the whole tx):
745
- await Database.asService().transaction(async (tx) => {
746
- await tx.tables.todos.update(id, { done: true });
833
+ // A service-role transaction (the role is fixed for the whole plan):
834
+ await Database.asService().transaction((tx) => {
835
+ tx.tables.todos.updateWhere({ id }, { done: true });
836
+ return null;
747
837
  });
748
838
  ```
749
839
 
@@ -753,10 +843,10 @@ Guidelines:
753
843
  only where you genuinely need cross-user access. It is intentionally easy to
754
844
  grep for in review.
755
845
  - **No double-bypass / no nesting.** The sibling does not re-expose
756
- `asService()`, and `tx` never exposes it — a transaction's role is fixed when
757
- it begins. Use `Database.transaction(...)` for an authenticated tx and
758
- `Database.asService().transaction(...)` for a service-role tx; you cannot mix
759
- enforced and bypassed ops inside one interactive transaction.
846
+ `asService()`, and `tx` never exposes it — a plan's role is fixed for the whole
847
+ transaction. Use `Database.transaction(...)` for an authenticated one and
848
+ `Database.asService().transaction(...)` for a service-role one; you cannot mix
849
+ enforced and bypassed operations inside a single plan.
760
850
 
761
851
 
762
852
 
@@ -855,10 +945,12 @@ export default class RoomsController {
855
945
  ```
856
946
 
857
947
  `Database.tables.<name>` exposes `insert`, `update(id, data)`, `delete(id)`,
858
- `findById(id)`, `findMany(query?)`, and `Database.transaction(fn)` yields a `tx`
859
- with the same typed tables. The raw string-keyed ops
860
- (`Database.insert("rooms", …)`, `Database.query(…)`) are still available for
861
- dynamic table names and read-only SQL.
948
+ `findById(id)`, `findMany(query?)`. `Database.transaction(fn)` yields a `tx`
949
+ whose `tx.tables.<name>` is typed from the same schema, but carries plan
950
+ operations (`insert`/`insertMany`/`updateWhere`/`deleteWhere`/`select`) rather
951
+ than awaited calls — see [database.md](./database.md#transactions). The raw
952
+ string-keyed ops (`Database.insert("rooms", …)`, `Database.query(…)`) are still
953
+ available for dynamic table names and read-only SQL.
862
954
 
863
955
  If you want a row type explicitly, import it from the generated env module:
864
956
 
@@ -1505,22 +1597,21 @@ await Queue.push("process-order", { orderId: "ord_1", amount: 1000 });
1505
1597
 
1506
1598
  ## Jobs (cron-scheduled)
1507
1599
 
1508
- A job runs on a cron schedule. File lives under `jobs/`.
1600
+ A job runs on a cron schedule. File lives under `jobs/` — the job's name is
1601
+ the file name, there is no `name` option.
1509
1602
 
1510
1603
  ```ts
1511
1604
  // jobs/cleanup.ts
1512
- import { defineJob, Database, Log } from "@palbase/backend";
1605
+ import { Database, Job, Log, type JobMeta } from "@palbase/backend";
1513
1606
 
1514
- export default defineJob({
1515
- name: "cleanup-expired",
1516
- schedule: "0 3 * * *", // standard cron
1517
- timeout: 120, // optional, seconds
1518
- handler: async (meta) => {
1607
+ @Job({ schedule: "0 3 * * *", timeout: 120 }) // schedule: standard cron; timeout: optional, seconds
1608
+ export default class CleanupJob {
1609
+ async run(meta: JobMeta) {
1519
1610
  const expired = await Database.findMany("sessions", { expired: true });
1520
1611
  for (const s of expired) await Database.delete("sessions", s.id as string);
1521
1612
  Log.info(`cleaned ${expired.length} sessions in ${meta.environmentId}`);
1522
- },
1523
- });
1613
+ }
1614
+ }
1524
1615
  ```
1525
1616
 
1526
1617
  `meta` shape: `{ env, environmentId }`. No `user` (jobs are
@@ -1571,29 +1662,32 @@ Available hook builders: `auth.onUserCreated`, `auth.onSignIn`, `auth.onSignOut`
1571
1662
  ## Webhooks (inbound provider events)
1572
1663
 
1573
1664
  Receive and verify webhooks from third-party providers. Files live under
1574
- `webhooks/`.
1665
+ `webhooks/` — the URL is `POST /webhooks/<file-name>` (e.g. `webhooks/stripe.ts`
1666
+ → `POST /webhooks/stripe`); there is no `path` option.
1575
1667
 
1576
1668
  ```ts
1577
1669
  // webhooks/stripe.ts
1578
- import { defineWebhook, Database, Log } from "@palbase/backend";
1579
-
1580
- export default defineWebhook({
1581
- provider: "stripe",
1582
- secret: { env: "STRIPE_WEBHOOK_SECRET" }, // signing secret resolved from env
1583
- events: {
1584
- "checkout.session.completed": async (event, meta) => {
1585
- await Database.insert("orders", { status: "paid", data: event });
1586
- },
1587
- "payment_intent.payment_failed": async (event, meta) => {
1588
- Log.error("payment failed");
1589
- await Database.insert("payment_failures", { data: event });
1590
- },
1591
- },
1592
- });
1670
+ import { Database, Log, On, Webhook, type WebhookMeta } from "@palbase/backend";
1671
+
1672
+ @Webhook({ provider: "stripe", secret: { env: "STRIPE_WEBHOOK_SECRET" } }) // signing secret resolved from env
1673
+ export default class StripeWebhook {
1674
+ @On("checkout.session.completed")
1675
+ async checkoutCompleted(event: unknown, meta: WebhookMeta) {
1676
+ await Database.insert("orders", { status: "paid", data: event });
1677
+ }
1678
+
1679
+ @On("payment_intent.payment_failed")
1680
+ async paymentFailed(event: unknown, meta: WebhookMeta) {
1681
+ Log.error("payment failed");
1682
+ await Database.insert("payment_failures", { data: event });
1683
+ }
1684
+ }
1593
1685
  ```
1594
1686
 
1595
- The signing secret is resolved by the runtime from `secret: { env: "NAME" }`;
1596
- your handlers access Environment variables via `meta.env`. The runtime verifies the
1687
+ `provider` selects a preset signature scheme; a service with no preset spells
1688
+ one out with `signature` instead — one of the two is required. The signing
1689
+ secret is resolved by the runtime from `secret: { env: "NAME" }`; your
1690
+ handlers access Environment variables via `meta.env`. The runtime verifies the
1597
1691
  signature before dispatching to your event handlers.
1598
1692
 
1599
1693
  `meta` shape: `{ env, requestId, environmentId }`.
package/docs/schema.md CHANGED
@@ -91,10 +91,12 @@ export default class RoomsController {
91
91
  ```
92
92
 
93
93
  `Database.tables.<name>` exposes `insert`, `update(id, data)`, `delete(id)`,
94
- `findById(id)`, `findMany(query?)`, and `Database.transaction(fn)` yields a `tx`
95
- with the same typed tables. The raw string-keyed ops
96
- (`Database.insert("rooms", …)`, `Database.query(…)`) are still available for
97
- dynamic table names and read-only SQL.
94
+ `findById(id)`, `findMany(query?)`. `Database.transaction(fn)` yields a `tx`
95
+ whose `tx.tables.<name>` is typed from the same schema, but carries plan
96
+ operations (`insert`/`insertMany`/`updateWhere`/`deleteWhere`/`select`) rather
97
+ than awaited calls — see [database.md](./database.md#transactions). The raw
98
+ string-keyed ops (`Database.insert("rooms", …)`, `Database.query(…)`) are still
99
+ available for dynamic table names and read-only SQL.
98
100
 
99
101
  If you want a row type explicitly, import it from the generated env module:
100
102
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@palbase/backend",
3
- "version": "10.3.0",
3
+ "version": "12.0.0",
4
4
  "description": "Palbase Backend SDK — class controllers (@Controller/@Get/@Post + @Body/@QueryParams/@Param), error classes, schema DSL",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/runtime.ts"],"sourcesContent":["/**\n * runtime.ts — request-scoped service singletons.\n *\n * The backend SDK no longer threads a `ctx` god-object through every handler.\n * Instead, controller methods import PascalCase service singletons directly:\n *\n * import { Controller, Post, Body, Database } from \"@palbase/backend\";\n *\n * \\@Controller(\"/todos\")\n * export default class TodosController {\n * \\@Post(\"\") create(\\@Body(CreateTodoBody) body: CreateTodoBody): unknown {\n * return Database.insert(\"todos\", { title: body.title });\n * }\n * }\n *\n * The singletons are thin Proxies. Every property access forwards to the live\n * client for the CURRENT request scope, resolved through {@link __getRuntime}.\n *\n * # Request-scope resolution (persistent app-server)\n *\n * The runtime is a long-running Node process that serves many concurrent\n * requests on one event loop (NOT a fresh subprocess per request). A single\n * module-global slot would let one in-flight request's services bleed into\n * another's. So the services are carried in an {@link AsyncLocalStorage} store\n * ({@link __requestALS}) that the runtime sets per request with\n * {@link __runWithRuntime}; every async continuation of that request reads its\n * own store. `__getRuntime` reads the ALS store first; the module-global slot\n * (set by {@link __setRuntime}) is only a fallback for callers that run OUTSIDE\n * an ALS scope (dev-server, unit tests, the legacy single-shot path). Because\n * each `br-<ref>` pod is single-tenant, there is no cross-tenant leakage; the\n * ALS store is what prevents cross-REQUEST leakage within the shared process.\n *\n * The seam that makes `import { Database } from \"@palbase/backend\"` resolve to\n * the runtime-injected client: `@palbase/backend` is marked esbuild-EXTERNAL\n * when the tenant bundle is built, and the package is installed globally in the\n * pod (NODE_PATH=/usr/local/lib/node_modules). So worker.js's\n * `require('@palbase/backend')` and the bundle's `import` resolve to ONE shared\n * module instance — the ALS store and `__setRuntime` slot on that instance are\n * visible to the singletons the bundle imported.\n */\n\nimport { AsyncLocalStorage } from \"node:async_hooks\";\n\nimport type {\n DBClient,\n DBOps,\n TxClient,\n CacheClient,\n QueueClient,\n Logger,\n PalbaseDocsClient,\n} from \"./endpoint.js\";\nimport type {\n PalbaseStorageClient,\n PalbaseNotificationsClient,\n PalbaseFlagsClient,\n PalbaseFlagsServiceClient,\n PalbaseFlagContext,\n PalbaseFlagVariant,\n PalbaseFlag,\n PalbaseFlagValue,\n PalbaseSetOverrideResult,\n PalbaseRealtimeClient,\n} from \"./clients.js\";\nimport type { PalbaseResult } from \"./endpoint.js\";\nimport type {\n EnvTypedDatabase,\n EnvServiceDatabase,\n EnvTypedTx,\n EnvTables,\n} from \"./db/typed-db.js\";\nimport type { PurchasesService } from \"./purchases/service.js\";\n\n/** The set of live clients the runtime injects per request scope.\n *\n * Realtime is BROADCAST-ONLY here (a stateless handler can push an event but\n * cannot hold a subscription socket — `subscribe()` lives on the client SDK).\n *\n * EXCLUDED on purpose: Functions, Links, Analytics, Auth. They are not\n * exposed as backend handler singletons (auth lives on the client SDK; the rest\n * are out of scope for backend endpoints). */\nexport interface RuntimeServices {\n Database: DBClient;\n Documents: PalbaseDocsClient;\n Storage: PalbaseStorageClient;\n Cache: CacheClient;\n Queue: QueueClient;\n Log: Logger;\n Notifications: PalbaseNotificationsClient;\n Flags: PalbaseFlagsClient;\n Realtime: PalbaseRealtimeClient;\n Purchases: PurchasesService;\n}\n\n/**\n * The per-request ALS box.\n *\n * `runtime` is the service bundle. `userId` is the request's authenticated user\n * id, written by the runtime immediately after it assembles the request object\n * (worker.js: `requestALS.getStore().userId = pbReq.user?.id || null`) and\n * `null` on an anonymous request. It was already being written there for the\n * Flags client's auto-bind; declaring it here makes the existing contract typed\n * instead of implicit, which is what lets `currentSubjectId()` read a\n * server-owned identity rather than trusting anything the caller sent.\n */\nexport interface RequestStore {\n runtime: RuntimeServices;\n userId?: string | null;\n /** This request's id (`req_…`), written by the runtime. Used as the spend\n * idempotency scope when the caller supplied no `Idempotency-Key`. */\n requestId?: string;\n /** The caller's `Idempotency-Key` header, or `null` when absent. What makes a\n * client's retry replay its first result instead of charging twice. */\n idempotencyKey?: string | null;\n}\n\n/**\n * Per-request store. The persistent runtime runs each request inside\n * {@link __runWithRuntime}, so every async continuation of that request reads\n * its OWN `runtime` (and any other request-scoped fields the runtime adds).\n *\n * Exported with a `__` prefix so the runtime (worker.js) shares the SAME ALS\n * instance across the one module instance — two ALS instances would silently\n * not see each other's stores. NOT part of the public author-facing API.\n */\nexport const __requestALS = new AsyncLocalStorage<RequestStore>();\n\n/** Process-global fallback slot. Used only OUTSIDE an ALS scope (dev-server,\n * unit tests, legacy single-shot worker). Inside the persistent server every\n * request runs in {@link __requestALS}, which takes precedence. */\nlet runtime: RuntimeServices | null = null;\n\n/** Install the live clients in the process-global fallback slot.\n *\n * Persistent-server requests should use {@link __runWithRuntime} instead; this\n * remains for dev-server / tests / the legacy single-shot path that run without\n * an ALS scope. NOT part of the public author-facing API. */\nexport function __setRuntime(services: RuntimeServices): void {\n runtime = services;\n}\n\n/** Run `fn` with `services` bound as the request-scoped runtime.\n *\n * The persistent worker calls this once per request so concurrent requests\n * never share a services slot. NOT part of the public author-facing API. */\nexport function __runWithRuntime<T>(services: RuntimeServices, fn: () => T): T {\n return __requestALS.run({ runtime: services }, fn);\n}\n\n/** Read the live clients, throwing if accessed outside a request scope.\n *\n * Resolves the ALS store first (persistent server, per-request), then the\n * process-global fallback (dev-server / tests). NOT part of the public\n * author-facing API — used by the runtime and the singleton Proxies. */\nexport function __getRuntime(): RuntimeServices {\n const scoped = __requestALS.getStore();\n if (scoped) return scoped.runtime;\n if (runtime === null) {\n throw new Error(\n \"Palbase services accessed outside a request scope. The Database/Documents/… \" +\n \"singletons are only available inside an endpoint handler (or after the \" +\n \"runtime has called __runWithRuntime / __setRuntime).\",\n );\n }\n return runtime;\n}\n\n/**\n * Build a Proxy singleton that forwards every property access to the live\n * client named `key` on the current runtime.\n *\n * The single `as RuntimeServices[K]` is the only contained cast in the surface:\n * `Reflect.get` on a typed object returns `unknown` for a `string | symbol`\n * key, but `prop` is constrained to keys of the client interface at the call\n * sites (the exported singletons are typed below), so the forward is sound.\n */\nfunction makeServiceProxy<K extends keyof RuntimeServices>(key: K): RuntimeServices[K] {\n const handler: ProxyHandler<RuntimeServices[K]> = {\n get(_target, prop, receiver) {\n const client = __getRuntime()[key];\n const value = Reflect.get(client as object, prop, receiver) as unknown;\n // Bind methods to their owning client so `this` stays correct when the\n // author destructures or calls `Database.query(...)`.\n return typeof value === \"function\" ? value.bind(client) : value;\n },\n };\n // The Proxy target is irrelevant (all access goes through `get`); the cast\n // names the surface type the singleton presents to authors.\n return new Proxy({} as RuntimeServices[K], handler);\n}\n\n/**\n * Build the `.tables` accessor for an op-bearing client (the top-level\n * `Database` or a transaction-scoped `tx`). Each `tables.<name>` access\n * returns a small object that forwards the five CRUD ops to the underlying\n * client using `name` as the string table identifier. The shapes are typed\n * against the generated `palbase-env.d.ts` (`EnvTables`); at runtime they are\n * plain string-keyed calls, so no schema value is needed here.\n *\n * Returns `EnvTables` — TS cannot infer the mapped type through the Proxy, so\n * a single structural narrowing names the surface (the proxy returns a\n * correctly-shaped accessor for whatever string member is read).\n */\nfunction makeTablesAccessor(ops: () => TxClient): EnvTables {\n const tablesProxy = new Proxy(\n {},\n {\n get(_t, prop: string | symbol) {\n if (typeof prop !== \"string\") return undefined;\n const name = prop;\n return {\n insert: (data: Record<string, unknown>) => ops().insert(name, data),\n update: (id: string, data: Record<string, unknown>) => ops().update(name, id, data),\n delete: (id: string) => ops().delete(name, id),\n findById: (id: string) => ops().findById(name, id),\n findMany: (query?: Record<string, unknown>) => ops().findMany(name, query),\n };\n },\n },\n );\n return tablesProxy as EnvTables;\n}\n\n/** The raw string-keyed `DBClient` for the current request scope. */\nconst rawDatabase: DBClient = makeServiceProxy(\"Database\");\n\n/**\n * Wrap a raw `DBClient` into the typed `{ ...ops, tables, transaction }`\n * surface. The five string ops forward straight through; `tables` is the\n * env-typed accessor; `transaction` yields typed tables. Reused for both the\n * default (RLS-enforced) `Database` and the `asService()` sibling — each is\n * fed its own raw client (the default proxy vs `rawDatabase.asService()`).\n *\n * The `satisfies` pins the op surface so a missing/renamed op is a compile\n * error; the assembled object carries `tables`/`transaction` alongside.\n */\nfunction makeTypedSurface(raw: Omit<DBClient, \"asService\">): EnvServiceDatabase {\n const ops = {\n query: (sql: string, params?: unknown[]) => raw.query(sql, params),\n insert: (table: string, data: Record<string, unknown>) => raw.insert(table, data),\n update: (table: string, id: string, data: Record<string, unknown>) =>\n raw.update(table, id, data),\n delete: (table: string, id: string) => raw.delete(table, id),\n findById: (table: string, id: string) => raw.findById(table, id),\n findMany: (table: string, query?: Record<string, unknown>) => raw.findMany(table, query),\n } satisfies DBOps;\n return Object.assign(ops, {\n tables: makeTablesAccessor(() => raw),\n transaction<T>(fn: (tx: EnvTypedTx) => Promise<T>): Promise<T> {\n return raw.transaction((rawTx) => fn({ tables: makeTablesAccessor(() => rawTx) }));\n },\n });\n}\n\n/**\n * The project's own Postgres (pgx, schema `env_<envId>`).\n *\n * Typed by default: `Database.tables.<name>.insert({...})` is typed against\n * the project's generated `palbase-env.d.ts` with NO import and NO generic.\n * The raw string ops (`query`/`insert`/`update`/`delete`/`findById`/`findMany`)\n * are also available for dynamic table names and read-only SQL.\n *\n * RLS is enforced by default (the runtime runs each op as `authenticated` with\n * the verified user's claims). To bypass RLS, call `Database.asService()` —\n * explicit and greppable — which runs as the `service_role` (BYPASSRLS).\n *\n * @example\n * import { Database } from \"@palbase/backend\";\n *\n * const todo = await Database.tables.todos.insert({ title: req.input.title });\n * todo.id; // string ✓\n * const rows = await Database.query(\"SELECT id FROM todos WHERE done = $1\", [false]);\n * const all = await Database.asService().tables.todos.findMany({}); // RLS bypass\n */\nexport const Database: EnvTypedDatabase = Object.assign(makeTypedSurface(rawDatabase), {\n /**\n * Lazily resolve the runtime's service-role sibling on each call. We do NOT\n * cache it: `rawDatabase.asService()` reads the CURRENT request scope through\n * the runtime proxy, and the per-request runtime injects a service client\n * bound to that request's identity headers — caching would leak one request's\n * sibling into another concurrent request.\n */\n asService(): EnvServiceDatabase {\n return makeTypedSurface(rawDatabase.asService());\n },\n});\n\n/** Firestore-like document client (PalDocs). */\nexport const Documents: PalbaseDocsClient = makeServiceProxy(\"Documents\");\n\n/** Object storage client (buckets, signed URLs). */\nexport const Storage: PalbaseStorageClient = makeServiceProxy(\"Storage\");\n\n/** JSON-typed cache (get/set/incr/getOrSet). */\nexport const Cache: CacheClient = makeServiceProxy(\"Cache\");\n\n/** Background job queue. */\nexport const Queue: QueueClient = makeServiceProxy(\"Queue\");\n\n/** Structured logger. */\nexport const Log: Logger = makeServiceProxy(\"Log\");\n\n/** Push / email / SMS / in-app notifications. */\nexport const Notifications: PalbaseNotificationsClient = makeServiceProxy(\"Notifications\");\n\n/**\n * Palstore purchases (entitlements + quota/credit spend).\n *\n * Reached by handlers through the `@RequireEntitlement` / `@Spend` decorators\n * rather than called directly in the common case; exposed as a singleton for\n * the cases the decorators deliberately do not cover (a dynamic spend count,\n * which must run BEFORE the billable side-effect).\n */\nexport const Purchases: PurchasesService = makeServiceProxy(\"Purchases\");\n\n/**\n * The raw runtime Flags client for the current request scope. Carries the\n * default-surface reads + `setOverride` AND the runtime's `asService()` sibling\n * (the br-pod's `buildFlagsClient` returns both). The default `Flags` singleton\n * below forwards reads + `setOverride` through here; `Flags.asService()`\n * forwards to this client's own `asService()`.\n */\nconst rawFlags: PalbaseFlagsClient = makeServiceProxy(\"Flags\");\n\n/**\n * Feature flags.\n *\n * Mirrors the `Database` / `Database.asService()` model. The default surface is\n * RLS-equivalent for flags: reads resolve against the CURRENT request user and\n * `Flags.setOverride(key, value)` writes an override for that same signed-in\n * user (no userId argument, no admin power). Cross-user admin writes\n * (`setOverrideForUser`, …) live behind `Flags.asService()` — explicit and\n * greppable, just like `Database.asService()`.\n *\n * @example\n * import { Flags } from \"@palbase/backend\";\n *\n * if (await Flags.isEnabled(\"new_checkout\")) { ... } // current user\n * await Flags.setOverride(\"new_checkout\", true); // current user\n * await Flags.asService().setOverrideForUser(\"u_9\", \"x\", true); // cross-user\n */\nexport const Flags: PalbaseFlagsClient = Object.assign(\n {\n isEnabled(\n flagName: string,\n context?: PalbaseFlagContext,\n ): Promise<PalbaseResult<boolean>> {\n return rawFlags.isEnabled(flagName, context);\n },\n getVariant(\n flagName: string,\n context?: PalbaseFlagContext,\n ): Promise<PalbaseResult<PalbaseFlagVariant>> {\n return rawFlags.getVariant(flagName, context);\n },\n getAll(context?: PalbaseFlagContext): Promise<PalbaseResult<PalbaseFlag[]>> {\n return rawFlags.getAll(context);\n },\n setOverride(\n key: string,\n value: PalbaseFlagValue,\n ): Promise<PalbaseResult<PalbaseSetOverrideResult>> {\n return rawFlags.setOverride(key, value);\n },\n },\n {\n /**\n * Lazily resolve the runtime's cross-user sibling on each call. We do NOT\n * cache it: `rawFlags.asService()` reads the CURRENT request scope through\n * the runtime proxy, so caching would leak one request's sibling into\n * another concurrent request. Mirrors `Database.asService()`.\n */\n asService(): PalbaseFlagsServiceClient {\n return rawFlags.asService();\n },\n },\n);\n\n/**\n * The Realtime broadcast singleton for the current request scope. Backend-side\n * Realtime is BROADCAST-ONLY (a stateless handler can push but not subscribe —\n * `subscribe()` lives on the client SDK's `pb.realtime`). Fire-and-forget:\n * `broadcast` resolves once accepted (or with an `error`), never blocking the\n * handler on subscribers.\n *\n * @example\n * import { Realtime } from \"@palbase/backend\";\n *\n * await Realtime.broadcast(\"room:42\", \"message\", { text, from: user.id });\n */\nexport const Realtime: PalbaseRealtimeClient = makeServiceProxy(\"Realtime\");\n"],"mappings":"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","names":[]}