@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.
- package/dist/chunk-7LAXRLPG.js +418 -0
- package/dist/chunk-7LAXRLPG.js.map +1 -0
- package/dist/{chunk-4WOQWFUP.js → chunk-LUV36KQU.js} +26 -11
- package/dist/{chunk-4WOQWFUP.js.map → chunk-LUV36KQU.js.map} +1 -1
- package/dist/{chunk-2PCNZBNZ.js → chunk-XATG7BRC.js} +20 -2
- package/dist/chunk-XATG7BRC.js.map +1 -0
- package/dist/db/index.cjs +438 -10
- 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 +13 -1
- package/dist/{endpoint-92kVepng.d.cts → endpoint-Ck4hER_7.d.cts} +343 -11
- package/dist/{endpoint-92kVepng.d.ts → endpoint-Ck4hER_7.d.ts} +343 -11
- package/dist/{index-BaHs33jr.d.cts → index-BJAf1uPC.d.cts} +64 -34
- package/dist/{index-DfryIw9U.d.ts → index-l7DhBDtn.d.ts} +64 -34
- package/dist/index.cjs +589 -124
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +60 -162
- package/dist/index.d.ts +60 -162
- package/dist/index.js +139 -108
- package/dist/index.js.map +1 -1
- package/dist/test/index.cjs +545 -12
- package/dist/test/index.cjs.map +1 -1
- package/dist/test/index.d.cts +1 -1
- package/dist/test/index.d.ts +1 -1
- package/dist/test/index.js +153 -12
- package/dist/test/index.js.map +1 -1
- package/docs/background.md +8 -9
- package/docs/database.md +106 -16
- package/docs/events.md +20 -17
- package/docs/llms-full.txt +141 -47
- package/docs/schema.md +6 -4
- package/package.json +1 -1
- package/dist/chunk-2PCNZBNZ.js.map +0 -1
package/docs/llms-full.txt
CHANGED
|
@@ -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
|
|
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
|
-
|
|
708
|
-
|
|
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
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
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
|
-
|
|
719
|
-
|
|
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
|
|
745
|
-
await Database.asService().transaction(
|
|
746
|
-
|
|
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
|
|
757
|
-
|
|
758
|
-
`Database.asService().transaction(...)` for a service-role
|
|
759
|
-
enforced and bypassed
|
|
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?)
|
|
859
|
-
|
|
860
|
-
(`
|
|
861
|
-
|
|
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 {
|
|
1605
|
+
import { Database, Job, Log, type JobMeta } from "@palbase/backend";
|
|
1513
1606
|
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
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 {
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
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
|
-
|
|
1596
|
-
|
|
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?)
|
|
95
|
-
|
|
96
|
-
(`
|
|
97
|
-
|
|
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 +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":[]}
|