@palbase/backend 35.0.0 → 35.0.1

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/docs/README.md CHANGED
@@ -82,7 +82,7 @@ service the controllers call.
82
82
 
83
83
  > **Never** emit `defineController`, `defineHandler`, `defineEndpoint`, `route.get(...)`,
84
84
  > `req.input`, `req.params`, or `req.errors` — those are the removed legacy model
85
- > and will not compile against `@palbase/backend` 34.
85
+ > and will not compile against `@palbase/backend` 35.
86
86
 
87
87
  ### Complete CRUD example (copy-pasteable, compiles)
88
88
 
package/docs/database.md CHANGED
@@ -825,7 +825,56 @@ continue to return their rows. Custom SQL drivers must expose an integer `count`
825
825
  on a non-returning result. Empty input opens no connection. The batch remains one
826
826
  statement and participates in its enclosing transaction's rollback.
827
827
 
828
- ## Reusable queries and response contracts
828
+ ## Query results and response contracts
829
+
830
+ Use the ordinary CRUD expression. A controller without a return annotation gets
831
+ its response validator, OpenAPI response and generated client type from the
832
+ TypeScript return type, following calls through injected services:
833
+
834
+ ```ts
835
+ // Store/service
836
+ list(prefix: string) {
837
+ return Database.public.todos.findMany({
838
+ where: { title: { startsWith: prefix } },
839
+ select: ["id", "title"],
840
+ limit: 50,
841
+ });
842
+ }
843
+
844
+ // Controller
845
+ @Get("/")
846
+ list(@QueryParams(ListInput) input: ListInput) {
847
+ return this.todos.list(input.prefix);
848
+ }
849
+ ```
850
+
851
+ The generated contract contains only `id` and `title`. `findUnique` retains
852
+ nullability, `page` retains its rows/pageInfo shape, and `insertMany` with
853
+ `returning: false` returns a count. Use `updateMany` for one bulk UPDATE; a
854
+ command plan is useful when several dependent operations actually belong together.
855
+
856
+ Inference does not execute application code or capture a Database/transaction
857
+ handle. Reuse a normal service method; inside an atomic callback, use its `tx`
858
+ argument. A service-role handle still owns a separate transaction and identity.
859
+
860
+ The build refuses `any`, `unknown`, unresolved imports, recursive/class/tuple
861
+ responses and unsupported JSON shapes. Supply a named Zod response schema when
862
+ needed. Existing named Zod annotations remain supported. Inference describes
863
+ the static JSON shape; an explicit schema remains useful for semantic constraints
864
+ such as UUID formats or a domain-specific numeric range. Input validation still
865
+ uses the existing `@Body`/`@QueryParams` schema.
866
+
867
+ Invalid response values and serialization failures roll the request transaction
868
+ back before COMMIT. A committed independent `$atomic` call retains its existing
869
+ independent commit semantics; response validation cannot undo an earlier commit.
870
+ An explicit nullable response returns HTTP 200 with JSON `null`; a response with
871
+ no body remains 204. This fixes nullable client contracts previously receiving an
872
+ empty body.
873
+
874
+ ### Existing query definitions
875
+
876
+ `defineQuery` is deprecated for normal application work. Its existing `run`,
877
+ SQL inspection, manifests and validation remain supported for compatibility.
829
878
 
830
879
  `defineQuery` combines an explicit selection with a reusable input/filter. The
831
880
  result is also a Zod array schema: a controller can return
@@ -934,35 +983,60 @@ one invoker helper call with sequential statements inside PostgreSQL. The helper
934
983
  adds JSON/dynamic-SQL processing; measure the actual plan and network before
935
984
  selecting it for throughput. Fewer round trips alone do not guarantee a speedup.
936
985
 
937
- For contention on the same accounts/resources, declare a workload once:
986
+ Core owns admission for ordinary CRUD, raw queries, request transactions,
987
+ `$atomic`, service-role operations, upload authorization and scheduled database
988
+ work. Nested savepoints reuse their parent's admitted connection. Application
989
+ code does not declare concurrency or queue capacities:
938
990
 
939
991
  ```ts
940
- const transfers = defineWorkload("transfers", {
941
- concurrency: 16, queueLimit: 128,
942
- tenantConcurrency: 8, tenantQueueLimit: 32, queueTimeoutMs: 1000,
943
- });
944
-
945
992
  await Database.$atomic(async tx => {
946
993
  // Decision reads, idempotency claim, writes, ledger and outbox belong here.
947
994
  }, {
948
- workload: transfers,
949
- conflictKeys: [fromAccountId, toAccountId],
950
995
  timeoutMs: 3000, signal, retry: 2,
951
996
  });
952
997
  ```
953
998
 
954
- Overlapping resource keys are admitted in order. Independent keys and tenants
955
- can proceed while those requests wait. Queue size and per-tenant budgets are
956
- bounded; full/expired queues return 429 before opening a transaction. Tenant
957
- identity comes from the configured verified claim (`tenant_id`, then `sub`, then
958
- one anonymous bucket). Keys are shared within a workload; include the tenant in
959
- a key if resource IDs are only tenant-unique. Rejected/cancelled waiters leave the
960
- queue. The lease spans every retry and transaction settlement.
961
-
962
- These limits apply per engine/driver instance. Divide pool/admission budgets
963
- across replicas; PostgreSQL locks and constraints remain authoritative across
964
- instances. Admission is optional because independent work also pays its cost.
965
- Each driver supports at most 64 named workloads.
999
+ The core process shares one physical pool across live and candidate applications.
1000
+ Reloading or discarding an application does not create another pool. A retiring
1001
+ application drains its requests/jobs through transaction settlement before
1002
+ closing its own resources.
1003
+
1004
+ Before independent `$atomic` work, core releases an implicit transaction that
1005
+ contains only its authorization reads. This prevents requests holding the whole
1006
+ pool while each waits for a second connection. Role/permission decisions still
1007
+ come from verified identity and current database state, once per request. Once
1008
+ application SQL runs, its enclosing transaction is retained; atomic work never
1009
+ silently commits an application's earlier write or changes its identity.
1010
+
1011
+ Admission happens before BEGIN and its lease lasts through COMMIT/ROLLBACK.
1012
+ Full/expired queues return an explicit 429; cancellation removes queued work.
1013
+ The private runtime `/_internal/database` probe exposes current admission counts,
1014
+ rejection reasons and PostgreSQL connection groups. It is on the internal probe
1015
+ port, not the public edge.
1016
+
1017
+ These are resource protections, not product quotas or a transactions-per-second
1018
+ limit. `DB_POOL_MAX` remains a deployment setting, not a business-code parameter.
1019
+ It is a per-process maximum; PostgreSQL's server limit is shared by every
1020
+ process. Operators must account for every runtime application pool, control
1021
+ pool, palsvc pool, migration/admin connection and replication connection in
1022
+ their server resource plan. An instance-local queue is not a cluster-wide
1023
+ admission guarantee. Core contains no Free/Pro tier logic.
1024
+
1025
+ Optional deployment settings are `PALBASE_DB_QUEUE_LIMIT` (default: eight times
1026
+ the application pool size, bounded to 32–512), `PALBASE_DB_QUEUE_TIMEOUT_MS`
1027
+ (default: 1000), and `PALBASE_DB_TENANT_CLAIM`. These defaults bound transient
1028
+ memory/waiting; they are not a measured optimum for every installation. Fairness
1029
+ uses verified `sub` by default, with round robin under contention. One identity
1030
+ can use the entire available pool. A custom tenant claim must be signed and
1031
+ issuer-controlled; do not use editable user metadata. Missing metadata does not
1032
+ reduce an installation to a fraction of its pool.
1033
+
1034
+ Existing `defineWorkload` options retain their old behavior, including conflict
1035
+ ordering and any explicit tenant cap. They are deprecated, not silently ignored.
1036
+ Removing them removes that application-local scheduling policy; sorted database
1037
+ row locks and constraints remain the authority across all runtime instances.
1038
+ Do not place external service calls in a retryable callback. Use an idempotency
1039
+ claim and transactional outbox for effects that must survive retries.
966
1040
 
967
1041
  `timeoutMs` includes queueing, callback work and retries. The default driver uses
968
1042
  a separate control pool of at most two connections to cancel active PostgreSQL
@@ -1002,8 +1076,9 @@ disabled; the completion callback includes settlement timings. Only the first
1002
1076
 
1003
1077
  | Metric | Meaning |
1004
1078
  | --- | --- |
1005
- | `queueMs` | Workload admission wait, including refused waits |
1079
+ | `queueMs` | Core admission and legacy workload wait, including refused waits |
1006
1080
  | `poolMs` | Driver acquisition through transaction callback entry, including BEGIN |
1081
+ | `connectionMs` | Connection held from transaction callback entry through COMMIT/ROLLBACK settlement |
1007
1082
  | `setupMs` | Isolation, cancellation ticket and role/claims setup |
1008
1083
  | `sqlMs` | Awaited driver time, including execution, lock and network waits |
1009
1084
  | `commitMs` / `rollbackMs` | Observed settlement waits |
@@ -90,7 +90,7 @@ service the controllers call.
90
90
 
91
91
  > **Never** emit `defineController`, `defineHandler`, `defineEndpoint`, `route.get(...)`,
92
92
  > `req.input`, `req.params`, or `req.errors` — those are the removed legacy model
93
- > and will not compile against `@palbase/backend` 34.
93
+ > and will not compile against `@palbase/backend` 35.
94
94
 
95
95
  ### Complete CRUD example (copy-pasteable, compiles)
96
96
 
@@ -1929,7 +1929,56 @@ continue to return their rows. Custom SQL drivers must expose an integer `count`
1929
1929
  on a non-returning result. Empty input opens no connection. The batch remains one
1930
1930
  statement and participates in its enclosing transaction's rollback.
1931
1931
 
1932
- ## Reusable queries and response contracts
1932
+ ## Query results and response contracts
1933
+
1934
+ Use the ordinary CRUD expression. A controller without a return annotation gets
1935
+ its response validator, OpenAPI response and generated client type from the
1936
+ TypeScript return type, following calls through injected services:
1937
+
1938
+ ```ts
1939
+ // Store/service
1940
+ list(prefix: string) {
1941
+ return Database.public.todos.findMany({
1942
+ where: { title: { startsWith: prefix } },
1943
+ select: ["id", "title"],
1944
+ limit: 50,
1945
+ });
1946
+ }
1947
+
1948
+ // Controller
1949
+ @Get("/")
1950
+ list(@QueryParams(ListInput) input: ListInput) {
1951
+ return this.todos.list(input.prefix);
1952
+ }
1953
+ ```
1954
+
1955
+ The generated contract contains only `id` and `title`. `findUnique` retains
1956
+ nullability, `page` retains its rows/pageInfo shape, and `insertMany` with
1957
+ `returning: false` returns a count. Use `updateMany` for one bulk UPDATE; a
1958
+ command plan is useful when several dependent operations actually belong together.
1959
+
1960
+ Inference does not execute application code or capture a Database/transaction
1961
+ handle. Reuse a normal service method; inside an atomic callback, use its `tx`
1962
+ argument. A service-role handle still owns a separate transaction and identity.
1963
+
1964
+ The build refuses `any`, `unknown`, unresolved imports, recursive/class/tuple
1965
+ responses and unsupported JSON shapes. Supply a named Zod response schema when
1966
+ needed. Existing named Zod annotations remain supported. Inference describes
1967
+ the static JSON shape; an explicit schema remains useful for semantic constraints
1968
+ such as UUID formats or a domain-specific numeric range. Input validation still
1969
+ uses the existing `@Body`/`@QueryParams` schema.
1970
+
1971
+ Invalid response values and serialization failures roll the request transaction
1972
+ back before COMMIT. A committed independent `$atomic` call retains its existing
1973
+ independent commit semantics; response validation cannot undo an earlier commit.
1974
+ An explicit nullable response returns HTTP 200 with JSON `null`; a response with
1975
+ no body remains 204. This fixes nullable client contracts previously receiving an
1976
+ empty body.
1977
+
1978
+ ### Existing query definitions
1979
+
1980
+ `defineQuery` is deprecated for normal application work. Its existing `run`,
1981
+ SQL inspection, manifests and validation remain supported for compatibility.
1933
1982
 
1934
1983
  `defineQuery` combines an explicit selection with a reusable input/filter. The
1935
1984
  result is also a Zod array schema: a controller can return
@@ -2038,35 +2087,60 @@ one invoker helper call with sequential statements inside PostgreSQL. The helper
2038
2087
  adds JSON/dynamic-SQL processing; measure the actual plan and network before
2039
2088
  selecting it for throughput. Fewer round trips alone do not guarantee a speedup.
2040
2089
 
2041
- For contention on the same accounts/resources, declare a workload once:
2090
+ Core owns admission for ordinary CRUD, raw queries, request transactions,
2091
+ `$atomic`, service-role operations, upload authorization and scheduled database
2092
+ work. Nested savepoints reuse their parent's admitted connection. Application
2093
+ code does not declare concurrency or queue capacities:
2042
2094
 
2043
2095
  ```ts
2044
- const transfers = defineWorkload("transfers", {
2045
- concurrency: 16, queueLimit: 128,
2046
- tenantConcurrency: 8, tenantQueueLimit: 32, queueTimeoutMs: 1000,
2047
- });
2048
-
2049
2096
  await Database.$atomic(async tx => {
2050
2097
  // Decision reads, idempotency claim, writes, ledger and outbox belong here.
2051
2098
  }, {
2052
- workload: transfers,
2053
- conflictKeys: [fromAccountId, toAccountId],
2054
2099
  timeoutMs: 3000, signal, retry: 2,
2055
2100
  });
2056
2101
  ```
2057
2102
 
2058
- Overlapping resource keys are admitted in order. Independent keys and tenants
2059
- can proceed while those requests wait. Queue size and per-tenant budgets are
2060
- bounded; full/expired queues return 429 before opening a transaction. Tenant
2061
- identity comes from the configured verified claim (`tenant_id`, then `sub`, then
2062
- one anonymous bucket). Keys are shared within a workload; include the tenant in
2063
- a key if resource IDs are only tenant-unique. Rejected/cancelled waiters leave the
2064
- queue. The lease spans every retry and transaction settlement.
2065
-
2066
- These limits apply per engine/driver instance. Divide pool/admission budgets
2067
- across replicas; PostgreSQL locks and constraints remain authoritative across
2068
- instances. Admission is optional because independent work also pays its cost.
2069
- Each driver supports at most 64 named workloads.
2103
+ The core process shares one physical pool across live and candidate applications.
2104
+ Reloading or discarding an application does not create another pool. A retiring
2105
+ application drains its requests/jobs through transaction settlement before
2106
+ closing its own resources.
2107
+
2108
+ Before independent `$atomic` work, core releases an implicit transaction that
2109
+ contains only its authorization reads. This prevents requests holding the whole
2110
+ pool while each waits for a second connection. Role/permission decisions still
2111
+ come from verified identity and current database state, once per request. Once
2112
+ application SQL runs, its enclosing transaction is retained; atomic work never
2113
+ silently commits an application's earlier write or changes its identity.
2114
+
2115
+ Admission happens before BEGIN and its lease lasts through COMMIT/ROLLBACK.
2116
+ Full/expired queues return an explicit 429; cancellation removes queued work.
2117
+ The private runtime `/_internal/database` probe exposes current admission counts,
2118
+ rejection reasons and PostgreSQL connection groups. It is on the internal probe
2119
+ port, not the public edge.
2120
+
2121
+ These are resource protections, not product quotas or a transactions-per-second
2122
+ limit. `DB_POOL_MAX` remains a deployment setting, not a business-code parameter.
2123
+ It is a per-process maximum; PostgreSQL's server limit is shared by every
2124
+ process. Operators must account for every runtime application pool, control
2125
+ pool, palsvc pool, migration/admin connection and replication connection in
2126
+ their server resource plan. An instance-local queue is not a cluster-wide
2127
+ admission guarantee. Core contains no Free/Pro tier logic.
2128
+
2129
+ Optional deployment settings are `PALBASE_DB_QUEUE_LIMIT` (default: eight times
2130
+ the application pool size, bounded to 32–512), `PALBASE_DB_QUEUE_TIMEOUT_MS`
2131
+ (default: 1000), and `PALBASE_DB_TENANT_CLAIM`. These defaults bound transient
2132
+ memory/waiting; they are not a measured optimum for every installation. Fairness
2133
+ uses verified `sub` by default, with round robin under contention. One identity
2134
+ can use the entire available pool. A custom tenant claim must be signed and
2135
+ issuer-controlled; do not use editable user metadata. Missing metadata does not
2136
+ reduce an installation to a fraction of its pool.
2137
+
2138
+ Existing `defineWorkload` options retain their old behavior, including conflict
2139
+ ordering and any explicit tenant cap. They are deprecated, not silently ignored.
2140
+ Removing them removes that application-local scheduling policy; sorted database
2141
+ row locks and constraints remain the authority across all runtime instances.
2142
+ Do not place external service calls in a retryable callback. Use an idempotency
2143
+ claim and transactional outbox for effects that must survive retries.
2070
2144
 
2071
2145
  `timeoutMs` includes queueing, callback work and retries. The default driver uses
2072
2146
  a separate control pool of at most two connections to cancel active PostgreSQL
@@ -2106,8 +2180,9 @@ disabled; the completion callback includes settlement timings. Only the first
2106
2180
 
2107
2181
  | Metric | Meaning |
2108
2182
  | --- | --- |
2109
- | `queueMs` | Workload admission wait, including refused waits |
2183
+ | `queueMs` | Core admission and legacy workload wait, including refused waits |
2110
2184
  | `poolMs` | Driver acquisition through transaction callback entry, including BEGIN |
2185
+ | `connectionMs` | Connection held from transaction callback entry through COMMIT/ROLLBACK settlement |
2111
2186
  | `setupMs` | Isolation, cancellation ticket and role/claims setup |
2112
2187
  | `sqlMs` | Awaited driver time, including execution, lock and network waits |
2113
2188
  | `commitMs` / `rollbackMs` | Observed settlement waits |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@palbase/backend",
3
- "version": "35.0.0",
3
+ "version": "35.0.1",
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": {
@@ -3,13 +3,13 @@
3
3
  "version": "0.1.0",
4
4
  "private": true,
5
5
  "type": "module",
6
- "description": "A Palbase backend class controllers, a declared database, and the secrets it needs.",
6
+ "description": "A Palbase backend \u2014 class controllers, a declared database, and the secrets it needs.",
7
7
  "scripts": {
8
8
  "test": "./scripts/test.sh",
9
9
  "typecheck": "tsc --noEmit"
10
10
  },
11
11
  "dependencies": {
12
- "@palbase/backend": "^34.0.0",
12
+ "@palbase/backend": "^35.0.0",
13
13
  "reflect-metadata": "^0.2.2"
14
14
  },
15
15
  "engines": {