@happyvertical/smrt-core 0.40.68 → 0.40.70

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 (144) hide show
  1. package/AGENTS.md +29 -4
  2. package/README.md +20 -1
  3. package/agents/query-bounds.md +45 -0
  4. package/agents/schema-paths.md +328 -0
  5. package/dist/browser.d.ts +1 -0
  6. package/dist/browser.d.ts.map +1 -1
  7. package/dist/browser.js +2 -1
  8. package/dist/class.d.ts.map +1 -1
  9. package/dist/class.js +28 -4
  10. package/dist/class.js.map +1 -1
  11. package/dist/collection.d.ts +101 -2
  12. package/dist/collection.d.ts.map +1 -1
  13. package/dist/collection.js +224 -43
  14. package/dist/collection.js.map +1 -1
  15. package/dist/database.d.ts +28 -0
  16. package/dist/database.d.ts.map +1 -1
  17. package/dist/database.js +16 -8
  18. package/dist/database.js.map +1 -1
  19. package/dist/db-errors.d.ts +105 -0
  20. package/dist/db-errors.d.ts.map +1 -0
  21. package/dist/db-errors.js +382 -0
  22. package/dist/db-errors.js.map +1 -0
  23. package/dist/decorators/index.d.ts +17 -1
  24. package/dist/decorators/index.d.ts.map +1 -1
  25. package/dist/decorators/index.js +65 -7
  26. package/dist/decorators/index.js.map +1 -1
  27. package/dist/dispatch/bus.d.ts.map +1 -1
  28. package/dist/dispatch/bus.js +4 -3
  29. package/dist/dispatch/bus.js.map +1 -1
  30. package/dist/errors.d.ts +30 -2
  31. package/dist/errors.d.ts.map +1 -1
  32. package/dist/errors.js +34 -2
  33. package/dist/errors.js.map +1 -1
  34. package/dist/generators/changes-route.d.ts.map +1 -1
  35. package/dist/generators/changes-route.js +6 -3
  36. package/dist/generators/changes-route.js.map +1 -1
  37. package/dist/generators/mcp-runtime-template.d.ts +8 -0
  38. package/dist/generators/mcp-runtime-template.d.ts.map +1 -1
  39. package/dist/generators/mcp-runtime-template.js +38 -4
  40. package/dist/generators/mcp-runtime-template.js.map +1 -1
  41. package/dist/generators/mcp.d.ts +16 -0
  42. package/dist/generators/mcp.d.ts.map +1 -1
  43. package/dist/generators/mcp.js +41 -3
  44. package/dist/generators/mcp.js.map +1 -1
  45. package/dist/generators/rest.d.ts +22 -0
  46. package/dist/generators/rest.d.ts.map +1 -1
  47. package/dist/generators/rest.js +34 -3
  48. package/dist/generators/rest.js.map +1 -1
  49. package/dist/index.d.ts +6 -1
  50. package/dist/index.d.ts.map +1 -1
  51. package/dist/index.js +8 -3
  52. package/dist/manifest/static-manifest.js +1 -1
  53. package/dist/manifest/static-manifest.js.map +1 -1
  54. package/dist/manifest/store.js +1 -1
  55. package/dist/manifest.json +1 -1
  56. package/dist/migrations/differ.d.ts +202 -8
  57. package/dist/migrations/differ.d.ts.map +1 -1
  58. package/dist/migrations/differ.js +604 -46
  59. package/dist/migrations/differ.js.map +1 -1
  60. package/dist/migrations/generator.d.ts +31 -4
  61. package/dist/migrations/generator.d.ts.map +1 -1
  62. package/dist/migrations/generator.js +49 -5
  63. package/dist/migrations/generator.js.map +1 -1
  64. package/dist/migrations/index.d.ts +3 -2
  65. package/dist/migrations/index.d.ts.map +1 -1
  66. package/dist/migrations/index.js +5 -3
  67. package/dist/migrations/orchestrate.js +35 -6
  68. package/dist/migrations/orchestrate.js.map +1 -1
  69. package/dist/migrations/sqlite-rebuild.d.ts +142 -0
  70. package/dist/migrations/sqlite-rebuild.d.ts.map +1 -0
  71. package/dist/migrations/sqlite-rebuild.js +514 -0
  72. package/dist/migrations/sqlite-rebuild.js.map +1 -0
  73. package/dist/migrations/tracker.d.ts +114 -1
  74. package/dist/migrations/tracker.d.ts.map +1 -1
  75. package/dist/migrations/tracker.js +331 -16
  76. package/dist/migrations/tracker.js.map +1 -1
  77. package/dist/migrations/types.d.ts +19 -4
  78. package/dist/migrations/types.d.ts.map +1 -1
  79. package/dist/migrations.js +5 -3
  80. package/dist/object.d.ts +45 -6
  81. package/dist/object.d.ts.map +1 -1
  82. package/dist/object.js +60 -13
  83. package/dist/object.js.map +1 -1
  84. package/dist/postgres-timeouts.d.ts +240 -0
  85. package/dist/postgres-timeouts.d.ts.map +1 -0
  86. package/dist/postgres-timeouts.js +204 -0
  87. package/dist/postgres-timeouts.js.map +1 -0
  88. package/dist/query-bounds.d.ts +97 -0
  89. package/dist/query-bounds.d.ts.map +1 -0
  90. package/dist/query-bounds.js +173 -0
  91. package/dist/query-bounds.js.map +1 -0
  92. package/dist/registry/types.d.ts +32 -1
  93. package/dist/registry/types.d.ts.map +1 -1
  94. package/dist/registry.js +1 -1
  95. package/dist/scanner/types.d.ts +5 -0
  96. package/dist/scanner/types.d.ts.map +1 -1
  97. package/dist/scanner/types.js.map +1 -1
  98. package/dist/schema/ddl/base-strategy.d.ts.map +1 -1
  99. package/dist/schema/ddl/base-strategy.js +2 -2
  100. package/dist/schema/ddl/base-strategy.js.map +1 -1
  101. package/dist/schema/generator.d.ts +70 -5
  102. package/dist/schema/generator.d.ts.map +1 -1
  103. package/dist/schema/generator.js +124 -6
  104. package/dist/schema/generator.js.map +1 -1
  105. package/dist/schema/index.d.ts +3 -0
  106. package/dist/schema/index.d.ts.map +1 -1
  107. package/dist/schema/index.js +4 -1
  108. package/dist/schema/live-parity.d.ts +90 -0
  109. package/dist/schema/live-parity.d.ts.map +1 -0
  110. package/dist/schema/live-parity.js +600 -0
  111. package/dist/schema/live-parity.js.map +1 -0
  112. package/dist/schema/manifest-schema.d.ts +121 -0
  113. package/dist/schema/manifest-schema.d.ts.map +1 -0
  114. package/dist/schema/manifest-schema.js +267 -0
  115. package/dist/schema/manifest-schema.js.map +1 -0
  116. package/dist/schema/schema-aggregator.d.ts +24 -10
  117. package/dist/schema/schema-aggregator.d.ts.map +1 -1
  118. package/dist/schema/schema-aggregator.js +35 -90
  119. package/dist/schema/schema-aggregator.js.map +1 -1
  120. package/dist/schema/system-table-shapes.d.ts +52 -0
  121. package/dist/schema/system-table-shapes.d.ts.map +1 -0
  122. package/dist/schema/system-table-shapes.js +174 -0
  123. package/dist/schema/system-table-shapes.js.map +1 -0
  124. package/dist/schema/types.d.ts +103 -4
  125. package/dist/schema/types.d.ts.map +1 -1
  126. package/dist/schema/utils.d.ts +1 -0
  127. package/dist/schema/utils.d.ts.map +1 -1
  128. package/dist/schema/utils.js +3 -1
  129. package/dist/schema/utils.js.map +1 -1
  130. package/dist/schema.js +4 -1
  131. package/dist/smrt-knowledge.json +18 -6
  132. package/dist/sync/apply.d.ts.map +1 -1
  133. package/dist/sync/apply.js +9 -16
  134. package/dist/sync/apply.js.map +1 -1
  135. package/dist/testing/database.d.ts.map +1 -1
  136. package/dist/testing/database.js +1 -0
  137. package/dist/testing/database.js.map +1 -1
  138. package/dist/vite-plugin/sveltekit-generator.d.ts.map +1 -1
  139. package/dist/vite-plugin/sveltekit-generator.js +97 -13
  140. package/dist/vite-plugin/sveltekit-generator.js.map +1 -1
  141. package/dist/vite-plugin/web-collections.d.ts.map +1 -1
  142. package/dist/vite-plugin/web-collections.js +6 -4
  143. package/dist/vite-plugin/web-collections.js.map +1 -1
  144. package/package.json +11 -11
@@ -0,0 +1,240 @@
1
+ /**
2
+ * Runtime PostgreSQL timeout defaults for every pool SMRT constructs.
3
+ *
4
+ * `@happyvertical/sql` builds its pool as `new Pool({ connectionString, max })`
5
+ * and nothing else, so an un-configured deployment inherits `pg`'s unbounded
6
+ * defaults: `pool.connect()` waits forever once the pool is exhausted, a
7
+ * runaway query holds its client indefinitely, and a request-scoped transaction
8
+ * that stalls mid-flight holds its locks until the process dies (#2377).
9
+ *
10
+ * This module resolves a bounded set of timeouts and applies them at pool
11
+ * construction. Three of them ride the connection URL: `pg` lifts
12
+ * `statement_timeout`, `lock_timeout`, and `idle_in_transaction_session_timeout`
13
+ * out of the connection string's query parameters and sends them in the startup
14
+ * packet, so they are session defaults on **every** client the pool opens,
15
+ * including ones opened long after startup. The fourth, the connection
16
+ * acquisition timeout, is a `pg` *pool* option with no connection-string
17
+ * spelling; it is emitted as `connectionTimeoutMillis` on the options object
18
+ * passed to `getDatabase()`, where `resolvePostgresConfig()` currently drops it.
19
+ * It becomes live with no change here once the SDK forwards pool options
20
+ * (happyvertical/sdk#1204).
21
+ *
22
+ * Scope: this is the *runtime* pool only. `smrt db:migrate` opens its own
23
+ * connection through `@happyvertical/sql` directly and bounds its statements
24
+ * with `SET LOCAL lock_timeout` / `statement_timeout` from
25
+ * `migrations.postgres.*` (#2362); a migration therefore keeps its own, larger
26
+ * budget even when it runs against a database whose runtime pool is bounded
27
+ * here. The two read the same way on purpose — same string units, same
28
+ * "unparseable falls back to the documented default" rule.
29
+ *
30
+ * @module
31
+ */
32
+ /**
33
+ * Runtime PostgreSQL timeouts, as accepted from configuration.
34
+ *
35
+ * Every value is either a number of milliseconds or a duration string with an
36
+ * `ms`, `s`, `min`/`m`, or `h` suffix — the spelling
37
+ * `migrations.postgres.lockTimeout` already uses (`'30s'`). `0` means
38
+ * *disabled*, which is PostgreSQL's own semantic for the three server-side
39
+ * timeouts and `pg`'s semantic for the connection timeout ("wait forever");
40
+ * it is emitted explicitly so it overrides a server-side default rather than
41
+ * silently inheriting one.
42
+ *
43
+ * @example
44
+ * ```typescript
45
+ * const db = await resolveDatabase({
46
+ * type: 'postgres',
47
+ * url: process.env.DATABASE_URL,
48
+ * timeouts: { statementTimeout: '10s', lockTimeout: '2s' },
49
+ * });
50
+ * ```
51
+ */
52
+ export interface PostgresTimeoutConfig {
53
+ /**
54
+ * Milliseconds to wait for a pool client before failing the acquisition.
55
+ *
56
+ * Bounds `pool.connect()`, whose `pg` default of `0` waits forever — the
57
+ * failure mode where one runaway query silently converts into a hung request
58
+ * queue. Emitted as `connectionTimeoutMillis`.
59
+ *
60
+ * @default '10s'
61
+ */
62
+ connectionTimeout?: string | number;
63
+ /**
64
+ * Session `statement_timeout`: the ceiling on a single statement.
65
+ *
66
+ * @default '30s'
67
+ */
68
+ statementTimeout?: string | number;
69
+ /**
70
+ * Session `idle_in_transaction_session_timeout`: the ceiling on an open
71
+ * transaction that is not currently executing a statement. This is the one
72
+ * that releases locks held by a request that died between statements.
73
+ *
74
+ * @default '60s'
75
+ */
76
+ idleInTransactionSessionTimeout?: string | number;
77
+ /**
78
+ * Session `lock_timeout`: the ceiling on waiting for a lock.
79
+ *
80
+ * @default '10s'
81
+ */
82
+ lockTimeout?: string | number;
83
+ }
84
+ /**
85
+ * Timeouts resolved to milliseconds, ready to emit.
86
+ */
87
+ export interface ResolvedPostgresTimeouts {
88
+ connectionTimeoutMs: number;
89
+ statementTimeoutMs: number;
90
+ idleInTransactionSessionTimeoutMs: number;
91
+ lockTimeoutMs: number;
92
+ }
93
+ /**
94
+ * Defaults applied to every runtime PostgreSQL pool.
95
+ *
96
+ * Chosen to bound the failure modes in #2377 without truncating legitimate
97
+ * request work: a web request that needs more than 30 s of server time or
98
+ * holds a transaction open for more than a minute is the pathology these
99
+ * bound, not the workload. Long-running maintenance does not run on this pool
100
+ * — `db:migrate` carries its own budget (#2362) — and any deployment that
101
+ * disagrees raises or disables the value per key.
102
+ */
103
+ export declare const DEFAULT_POSTGRES_TIMEOUTS: ResolvedPostgresTimeouts;
104
+ /**
105
+ * Environment variable read for each timeout when configuration does not set
106
+ * it.
107
+ *
108
+ * Deployments that only ever hand SMRT a `DATABASE_URL` (SvelteKit runtime
109
+ * config, container env, serverless) have no object to put a `timeouts` key on,
110
+ * so the environment is the configuration surface that actually reaches them.
111
+ */
112
+ export declare const POSTGRES_TIMEOUT_ENV_VARS: {
113
+ readonly connectionTimeout: "SMRT_PG_CONNECTION_TIMEOUT";
114
+ readonly statementTimeout: "SMRT_PG_STATEMENT_TIMEOUT";
115
+ readonly idleInTransactionSessionTimeout: "SMRT_PG_IDLE_IN_TRANSACTION_TIMEOUT";
116
+ readonly lockTimeout: "SMRT_PG_LOCK_TIMEOUT";
117
+ };
118
+ /**
119
+ * Parse a PostgreSQL timeout expressed as milliseconds or as the duration
120
+ * string used in `smrt.config.js` into milliseconds.
121
+ *
122
+ * Accepts a bare number (milliseconds) or a number with an `ms`, `s`, `min`,
123
+ * `m`, or `h` suffix. Returns `fallback` for `undefined`, empty, or
124
+ * unparseable input so a typo degrades to the documented default instead of
125
+ * silently disabling the timeout it was meant to impose.
126
+ *
127
+ * This is the single implementation behind both timeout surfaces: the
128
+ * migration tracker re-exports it as the public
129
+ * `@happyvertical/smrt-core/migrations` symbol (#2362), so a runtime `timeouts`
130
+ * config and `migrations.postgres.*` are parsed by the same function and cannot
131
+ * drift. This module deliberately imports nothing, which is what lets the
132
+ * migration path depend on it without dragging anything into the connection
133
+ * path.
134
+ *
135
+ * @param value - Millisecond count or duration string
136
+ * @param fallback - Value returned for missing or unparseable input
137
+ * @returns Timeout in milliseconds
138
+ */
139
+ export declare function parsePostgresTimeoutMs(value: string | number | undefined, fallback: number): number;
140
+ /**
141
+ * Resolve the effective runtime timeouts.
142
+ *
143
+ * Precedence, highest first: explicit configuration, then the matching
144
+ * `SMRT_PG_*` environment variable, then {@link DEFAULT_POSTGRES_TIMEOUTS}.
145
+ * A parameter already present in the connection URL beats all three, but that
146
+ * is enforced where the URL is written ({@link applyPostgresTimeoutsToUrl}) —
147
+ * an operator who spelled a timeout into the DSN keeps it.
148
+ *
149
+ * @param config - Timeouts from configuration
150
+ * @param env - Environment to read (defaults to `process.env`)
151
+ * @returns Timeouts in milliseconds
152
+ */
153
+ export declare function resolvePostgresTimeouts(config?: PostgresTimeoutConfig, env?: Record<string, string | undefined>): ResolvedPostgresTimeouts;
154
+ /**
155
+ * Does this configuration open a PostgreSQL connection?
156
+ *
157
+ * Deliberately narrower than `detectEngine()` from the DDL module: that helper
158
+ * answers "which dialect do I generate SQL in" and folds `json` into `duckdb`,
159
+ * while this one answers "may I rewrite this URL", which must be false for
160
+ * every non-PostgreSQL adapter and must not pull the DDL strategy singletons
161
+ * into the connection path.
162
+ */
163
+ export declare function isPostgresTarget(url?: string, type?: string): boolean;
164
+ /**
165
+ * Write the session timeout parameters into a PostgreSQL connection URL.
166
+ *
167
+ * Parameters already present are left exactly as they are — an operator who
168
+ * put `statement_timeout` in the DSN configured it on purpose, and the
169
+ * per-parameter granularity means overriding one does not surrender the other
170
+ * two to `pg`'s unbounded defaults.
171
+ *
172
+ * The DSN is edited by **appending**, never by re-serializing. Everything the
173
+ * caller wrote — scheme, userinfo, host, path, and the existing query substring
174
+ * byte for byte — is copied through, and only the missing parameters are added
175
+ * after it. `URLSearchParams` is used to *read* which parameters are already
176
+ * present; writing through it would hand `pg` a re-encoded DSN (`%20` becoming
177
+ * `+`, parameters reordered) that is merely equivalent rather than identical,
178
+ * which is not this function's business. The emitted values are bare digits, so
179
+ * appending needs no encoding of its own.
180
+ *
181
+ * @param url - PostgreSQL connection URL
182
+ * @param timeouts - Resolved timeouts
183
+ * @returns The URL with the missing timeout parameters appended (the same
184
+ * string when all three were already present)
185
+ */
186
+ export declare function applyPostgresTimeoutsToUrl(url: string, timeouts: ResolvedPostgresTimeouts): string;
187
+ /**
188
+ * Options object accepted and returned by
189
+ * {@link applyPostgresRuntimeTimeouts}.
190
+ */
191
+ export interface PostgresTimeoutAwareConfig {
192
+ url?: string;
193
+ type?: string;
194
+ /** Runtime timeout overrides for this connection. */
195
+ timeouts?: PostgresTimeoutConfig;
196
+ /**
197
+ * `pg` pool option emitted for PostgreSQL configurations. Present on the
198
+ * result, not on the input.
199
+ */
200
+ connectionTimeoutMillis?: number;
201
+ [key: string]: unknown;
202
+ }
203
+ /**
204
+ * Bound a runtime database configuration before it reaches `getDatabase()`.
205
+ *
206
+ * Non-PostgreSQL configurations pass through unchanged — the same reference,
207
+ * so SQLite/DuckDB/JSON callers pay nothing and no file path is ever rewritten
208
+ * — except that a stray `timeouts` key is stripped on every engine, because it
209
+ * is SMRT configuration and never an adapter option.
210
+ *
211
+ * For PostgreSQL the returned object carries a URL with the three session
212
+ * timeouts and a `connectionTimeoutMillis` pool option. Callers that derive a
213
+ * `dbid` from the URL must derive it from the **returned** URL: two configs
214
+ * that differ only in their timeouts are different pools, and every call site
215
+ * applying the same deterministic rewrite is what keeps them one pool when the
216
+ * timeouts match.
217
+ *
218
+ * A PostgreSQL configuration with no `url` — discrete `host`/`port`/`database`
219
+ * fields, or a bare `{ type: 'postgres' }` that lets the adapter read
220
+ * `HAVE_SQL_URL`/`SQLOO_URL` — has nowhere to carry the session parameters and
221
+ * receives only `connectionTimeoutMillis`. Reproducing the adapter's own
222
+ * environment precedence here to synthesize a URL would fork it; pass the URL
223
+ * through the config (or set the `SMRT_PG_*` variables on a config that does
224
+ * carry one) to get the session timeouts.
225
+ *
226
+ * The `timeouts` key is consumed here and not forwarded — `@happyvertical/sql`
227
+ * would ignore it, and leaving it in the options object would make it look
228
+ * like a supported adapter option.
229
+ *
230
+ * Rewriting the URL also changes the SDK's own pool cache key for call sites
231
+ * that pass no `dbid`, so a consumer that separately calls `getDatabase()` with
232
+ * the same raw URL no longer shares SMRT's pool. Consumers that want one pool
233
+ * should hand SMRT the `DatabaseInterface`, not the same URL.
234
+ *
235
+ * @param config - Database configuration bound for `getDatabase()`
236
+ * @param env - Environment to read (defaults to `process.env`)
237
+ * @returns Configuration with runtime timeouts applied
238
+ */
239
+ export declare function applyPostgresRuntimeTimeouts(config: PostgresTimeoutAwareConfig, env?: Record<string, string | undefined>): PostgresTimeoutAwareConfig;
240
+ //# sourceMappingURL=postgres-timeouts.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"postgres-timeouts.d.ts","sourceRoot":"","sources":["../src/postgres-timeouts.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AAEH;;;;;;;;;;;;;;;;;;;GAmBG;AACH,MAAM,WAAW,qBAAqB;IACpC;;;;;;;;OAQG;IACH,iBAAiB,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;IAEpC;;;;OAIG;IACH,gBAAgB,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;IAEnC;;;;;;OAMG;IACH,+BAA+B,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;IAElD;;;;OAIG;IACH,WAAW,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;CAC/B;AAED;;GAEG;AACH,MAAM,WAAW,wBAAwB;IACvC,mBAAmB,EAAE,MAAM,CAAC;IAC5B,kBAAkB,EAAE,MAAM,CAAC;IAC3B,iCAAiC,EAAE,MAAM,CAAC;IAC1C,aAAa,EAAE,MAAM,CAAC;CACvB;AAED;;;;;;;;;GASG;AACH,eAAO,MAAM,yBAAyB,EAAE,wBAKvC,CAAC;AAEF;;;;;;;GAOG;AACH,eAAO,MAAM,yBAAyB;;;;;CAK0B,CAAC;AAgBjE;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,wBAAgB,sBAAsB,CACpC,KAAK,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,EAClC,QAAQ,EAAE,MAAM,GACf,MAAM,CAiCR;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,uBAAuB,CACrC,MAAM,GAAE,qBAA0B,EAClC,GAAG,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,GACvC,wBAAwB,CAiC1B;AAED;;;;;;;;GAQG;AACH,wBAAgB,gBAAgB,CAAC,GAAG,CAAC,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,GAAG,OAAO,CAqBrE;AAED;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,wBAAgB,0BAA0B,CACxC,GAAG,EAAE,MAAM,EACX,QAAQ,EAAE,wBAAwB,GACjC,MAAM,CA0BR;AAED;;;GAGG;AACH,MAAM,WAAW,0BAA0B;IACzC,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,qDAAqD;IACrD,QAAQ,CAAC,EAAE,qBAAqB,CAAC;IACjC;;;OAGG;IACH,uBAAuB,CAAC,EAAE,MAAM,CAAC;IACjC,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;CACxB;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmCG;AACH,wBAAgB,4BAA4B,CAC1C,MAAM,EAAE,0BAA0B,EAClC,GAAG,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,GACvC,0BAA0B,CAgB5B"}
@@ -0,0 +1,204 @@
1
+ //#region src/postgres-timeouts.ts
2
+ /**
3
+ * Defaults applied to every runtime PostgreSQL pool.
4
+ *
5
+ * Chosen to bound the failure modes in #2377 without truncating legitimate
6
+ * request work: a web request that needs more than 30 s of server time or
7
+ * holds a transaction open for more than a minute is the pathology these
8
+ * bound, not the workload. Long-running maintenance does not run on this pool
9
+ * — `db:migrate` carries its own budget (#2362) — and any deployment that
10
+ * disagrees raises or disables the value per key.
11
+ */
12
+ var DEFAULT_POSTGRES_TIMEOUTS = {
13
+ connectionTimeoutMs: 1e4,
14
+ statementTimeoutMs: 3e4,
15
+ idleInTransactionSessionTimeoutMs: 6e4,
16
+ lockTimeoutMs: 1e4
17
+ };
18
+ /**
19
+ * Environment variable read for each timeout when configuration does not set
20
+ * it.
21
+ *
22
+ * Deployments that only ever hand SMRT a `DATABASE_URL` (SvelteKit runtime
23
+ * config, container env, serverless) have no object to put a `timeouts` key on,
24
+ * so the environment is the configuration surface that actually reaches them.
25
+ */
26
+ var POSTGRES_TIMEOUT_ENV_VARS = {
27
+ connectionTimeout: "SMRT_PG_CONNECTION_TIMEOUT",
28
+ statementTimeout: "SMRT_PG_STATEMENT_TIMEOUT",
29
+ idleInTransactionSessionTimeout: "SMRT_PG_IDLE_IN_TRANSACTION_TIMEOUT",
30
+ lockTimeout: "SMRT_PG_LOCK_TIMEOUT"
31
+ };
32
+ /**
33
+ * PostgreSQL connection-string parameters that carry a session timeout.
34
+ *
35
+ * `pg` reads exactly these three out of the connection string and puts them in
36
+ * the startup packet (`Client.getStartupConf`), which is why the URL is the
37
+ * delivery mechanism: the settings then apply to every client the pool opens,
38
+ * not just the first, with no per-checkout `SET` round trip.
39
+ */
40
+ var URL_TIMEOUT_PARAMS = {
41
+ statementTimeoutMs: "statement_timeout",
42
+ idleInTransactionSessionTimeoutMs: "idle_in_transaction_session_timeout",
43
+ lockTimeoutMs: "lock_timeout"
44
+ };
45
+ /**
46
+ * Parse a PostgreSQL timeout expressed as milliseconds or as the duration
47
+ * string used in `smrt.config.js` into milliseconds.
48
+ *
49
+ * Accepts a bare number (milliseconds) or a number with an `ms`, `s`, `min`,
50
+ * `m`, or `h` suffix. Returns `fallback` for `undefined`, empty, or
51
+ * unparseable input so a typo degrades to the documented default instead of
52
+ * silently disabling the timeout it was meant to impose.
53
+ *
54
+ * This is the single implementation behind both timeout surfaces: the
55
+ * migration tracker re-exports it as the public
56
+ * `@happyvertical/smrt-core/migrations` symbol (#2362), so a runtime `timeouts`
57
+ * config and `migrations.postgres.*` are parsed by the same function and cannot
58
+ * drift. This module deliberately imports nothing, which is what lets the
59
+ * migration path depend on it without dragging anything into the connection
60
+ * path.
61
+ *
62
+ * @param value - Millisecond count or duration string
63
+ * @param fallback - Value returned for missing or unparseable input
64
+ * @returns Timeout in milliseconds
65
+ */
66
+ function parsePostgresTimeoutMs(value, fallback) {
67
+ if (typeof value === "number") return Number.isFinite(value) && value >= 0 ? Math.trunc(value) : fallback;
68
+ if (typeof value !== "string" || value.trim().length === 0) return fallback;
69
+ const match = /^\s*(\d+(?:\.\d+)?)\s*(ms|s|min|m|h)?\s*$/i.exec(value);
70
+ if (!match) return fallback;
71
+ const amount = Number.parseFloat(match[1]);
72
+ const unit = (match[2] ?? "ms").toLowerCase();
73
+ const milliseconds = amount * (unit === "h" ? 36e5 : unit === "min" || unit === "m" ? 6e4 : unit === "s" ? 1e3 : 1);
74
+ return Number.isFinite(milliseconds) ? Math.trunc(milliseconds) : fallback;
75
+ }
76
+ /**
77
+ * Resolve the effective runtime timeouts.
78
+ *
79
+ * Precedence, highest first: explicit configuration, then the matching
80
+ * `SMRT_PG_*` environment variable, then {@link DEFAULT_POSTGRES_TIMEOUTS}.
81
+ * A parameter already present in the connection URL beats all three, but that
82
+ * is enforced where the URL is written ({@link applyPostgresTimeoutsToUrl}) —
83
+ * an operator who spelled a timeout into the DSN keeps it.
84
+ *
85
+ * @param config - Timeouts from configuration
86
+ * @param env - Environment to read (defaults to `process.env`)
87
+ * @returns Timeouts in milliseconds
88
+ */
89
+ function resolvePostgresTimeouts(config = {}, env) {
90
+ const environment = env ?? (typeof process !== "undefined" ? process.env : {});
91
+ const resolve = (key, fallback) => parsePostgresTimeoutMs(config[key] ?? environment[POSTGRES_TIMEOUT_ENV_VARS[key]], fallback);
92
+ return {
93
+ connectionTimeoutMs: resolve("connectionTimeout", DEFAULT_POSTGRES_TIMEOUTS.connectionTimeoutMs),
94
+ statementTimeoutMs: resolve("statementTimeout", DEFAULT_POSTGRES_TIMEOUTS.statementTimeoutMs),
95
+ idleInTransactionSessionTimeoutMs: resolve("idleInTransactionSessionTimeout", DEFAULT_POSTGRES_TIMEOUTS.idleInTransactionSessionTimeoutMs),
96
+ lockTimeoutMs: resolve("lockTimeout", DEFAULT_POSTGRES_TIMEOUTS.lockTimeoutMs)
97
+ };
98
+ }
99
+ /**
100
+ * Does this configuration open a PostgreSQL connection?
101
+ *
102
+ * Deliberately narrower than `detectEngine()` from the DDL module: that helper
103
+ * answers "which dialect do I generate SQL in" and folds `json` into `duckdb`,
104
+ * while this one answers "may I rewrite this URL", which must be false for
105
+ * every non-PostgreSQL adapter and must not pull the DDL strategy singletons
106
+ * into the connection path.
107
+ */
108
+ function isPostgresTarget(url, type) {
109
+ const normalized = typeof type === "string" ? type.trim().toLowerCase() : "";
110
+ if (normalized) {
111
+ if (normalized === "postgres" || normalized === "postgresql" || normalized === "pg") return true;
112
+ return false;
113
+ }
114
+ const normalizedUrl = (url ?? "").toLowerCase();
115
+ return normalizedUrl.startsWith("postgres://") || normalizedUrl.startsWith("postgresql://");
116
+ }
117
+ /**
118
+ * Write the session timeout parameters into a PostgreSQL connection URL.
119
+ *
120
+ * Parameters already present are left exactly as they are — an operator who
121
+ * put `statement_timeout` in the DSN configured it on purpose, and the
122
+ * per-parameter granularity means overriding one does not surrender the other
123
+ * two to `pg`'s unbounded defaults.
124
+ *
125
+ * The DSN is edited by **appending**, never by re-serializing. Everything the
126
+ * caller wrote — scheme, userinfo, host, path, and the existing query substring
127
+ * byte for byte — is copied through, and only the missing parameters are added
128
+ * after it. `URLSearchParams` is used to *read* which parameters are already
129
+ * present; writing through it would hand `pg` a re-encoded DSN (`%20` becoming
130
+ * `+`, parameters reordered) that is merely equivalent rather than identical,
131
+ * which is not this function's business. The emitted values are bare digits, so
132
+ * appending needs no encoding of its own.
133
+ *
134
+ * @param url - PostgreSQL connection URL
135
+ * @param timeouts - Resolved timeouts
136
+ * @returns The URL with the missing timeout parameters appended (the same
137
+ * string when all three were already present)
138
+ */
139
+ function applyPostgresTimeoutsToUrl(url, timeouts) {
140
+ const fragmentStart = url.indexOf("#");
141
+ const fragment = fragmentStart === -1 ? "" : url.slice(fragmentStart);
142
+ const withoutFragment = fragmentStart === -1 ? url : url.slice(0, fragmentStart);
143
+ const queryStart = withoutFragment.indexOf("?");
144
+ const base = queryStart === -1 ? withoutFragment : withoutFragment.slice(0, queryStart);
145
+ const query = queryStart === -1 ? "" : withoutFragment.slice(queryStart + 1);
146
+ const present = new URLSearchParams(query);
147
+ const additions = [];
148
+ for (const [key, param] of Object.entries(URL_TIMEOUT_PARAMS)) {
149
+ if (present.has(param)) continue;
150
+ additions.push(`${param}=${Math.max(0, Math.trunc(timeouts[key]))}`);
151
+ }
152
+ if (additions.length === 0) return url;
153
+ return `${base}?${query}${query.length > 0 ? "&" : ""}${additions.join("&")}${fragment}`;
154
+ }
155
+ /**
156
+ * Bound a runtime database configuration before it reaches `getDatabase()`.
157
+ *
158
+ * Non-PostgreSQL configurations pass through unchanged — the same reference,
159
+ * so SQLite/DuckDB/JSON callers pay nothing and no file path is ever rewritten
160
+ * — except that a stray `timeouts` key is stripped on every engine, because it
161
+ * is SMRT configuration and never an adapter option.
162
+ *
163
+ * For PostgreSQL the returned object carries a URL with the three session
164
+ * timeouts and a `connectionTimeoutMillis` pool option. Callers that derive a
165
+ * `dbid` from the URL must derive it from the **returned** URL: two configs
166
+ * that differ only in their timeouts are different pools, and every call site
167
+ * applying the same deterministic rewrite is what keeps them one pool when the
168
+ * timeouts match.
169
+ *
170
+ * A PostgreSQL configuration with no `url` — discrete `host`/`port`/`database`
171
+ * fields, or a bare `{ type: 'postgres' }` that lets the adapter read
172
+ * `HAVE_SQL_URL`/`SQLOO_URL` — has nowhere to carry the session parameters and
173
+ * receives only `connectionTimeoutMillis`. Reproducing the adapter's own
174
+ * environment precedence here to synthesize a URL would fork it; pass the URL
175
+ * through the config (or set the `SMRT_PG_*` variables on a config that does
176
+ * carry one) to get the session timeouts.
177
+ *
178
+ * The `timeouts` key is consumed here and not forwarded — `@happyvertical/sql`
179
+ * would ignore it, and leaving it in the options object would make it look
180
+ * like a supported adapter option.
181
+ *
182
+ * Rewriting the URL also changes the SDK's own pool cache key for call sites
183
+ * that pass no `dbid`, so a consumer that separately calls `getDatabase()` with
184
+ * the same raw URL no longer shares SMRT's pool. Consumers that want one pool
185
+ * should hand SMRT the `DatabaseInterface`, not the same URL.
186
+ *
187
+ * @param config - Database configuration bound for `getDatabase()`
188
+ * @param env - Environment to read (defaults to `process.env`)
189
+ * @returns Configuration with runtime timeouts applied
190
+ */
191
+ function applyPostgresRuntimeTimeouts(config, env) {
192
+ const { timeouts: configuredTimeouts, ...rest } = config;
193
+ if (!isPostgresTarget(config.url, config.type)) return configuredTimeouts === void 0 ? config : rest;
194
+ const timeouts = resolvePostgresTimeouts(configuredTimeouts, env);
195
+ return {
196
+ ...rest,
197
+ ...config.url ? { url: applyPostgresTimeoutsToUrl(config.url, timeouts) } : {},
198
+ connectionTimeoutMillis: timeouts.connectionTimeoutMs
199
+ };
200
+ }
201
+ //#endregion
202
+ export { DEFAULT_POSTGRES_TIMEOUTS, POSTGRES_TIMEOUT_ENV_VARS, applyPostgresRuntimeTimeouts, applyPostgresTimeoutsToUrl, isPostgresTarget, parsePostgresTimeoutMs, resolvePostgresTimeouts };
203
+
204
+ //# sourceMappingURL=postgres-timeouts.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"postgres-timeouts.js","names":[],"sources":["../src/postgres-timeouts.ts"],"sourcesContent":["/**\n * Runtime PostgreSQL timeout defaults for every pool SMRT constructs.\n *\n * `@happyvertical/sql` builds its pool as `new Pool({ connectionString, max })`\n * and nothing else, so an un-configured deployment inherits `pg`'s unbounded\n * defaults: `pool.connect()` waits forever once the pool is exhausted, a\n * runaway query holds its client indefinitely, and a request-scoped transaction\n * that stalls mid-flight holds its locks until the process dies (#2377).\n *\n * This module resolves a bounded set of timeouts and applies them at pool\n * construction. Three of them ride the connection URL: `pg` lifts\n * `statement_timeout`, `lock_timeout`, and `idle_in_transaction_session_timeout`\n * out of the connection string's query parameters and sends them in the startup\n * packet, so they are session defaults on **every** client the pool opens,\n * including ones opened long after startup. The fourth, the connection\n * acquisition timeout, is a `pg` *pool* option with no connection-string\n * spelling; it is emitted as `connectionTimeoutMillis` on the options object\n * passed to `getDatabase()`, where `resolvePostgresConfig()` currently drops it.\n * It becomes live with no change here once the SDK forwards pool options\n * (happyvertical/sdk#1204).\n *\n * Scope: this is the *runtime* pool only. `smrt db:migrate` opens its own\n * connection through `@happyvertical/sql` directly and bounds its statements\n * with `SET LOCAL lock_timeout` / `statement_timeout` from\n * `migrations.postgres.*` (#2362); a migration therefore keeps its own, larger\n * budget even when it runs against a database whose runtime pool is bounded\n * here. The two read the same way on purpose — same string units, same\n * \"unparseable falls back to the documented default\" rule.\n *\n * @module\n */\n\n/**\n * Runtime PostgreSQL timeouts, as accepted from configuration.\n *\n * Every value is either a number of milliseconds or a duration string with an\n * `ms`, `s`, `min`/`m`, or `h` suffix — the spelling\n * `migrations.postgres.lockTimeout` already uses (`'30s'`). `0` means\n * *disabled*, which is PostgreSQL's own semantic for the three server-side\n * timeouts and `pg`'s semantic for the connection timeout (\"wait forever\");\n * it is emitted explicitly so it overrides a server-side default rather than\n * silently inheriting one.\n *\n * @example\n * ```typescript\n * const db = await resolveDatabase({\n * type: 'postgres',\n * url: process.env.DATABASE_URL,\n * timeouts: { statementTimeout: '10s', lockTimeout: '2s' },\n * });\n * ```\n */\nexport interface PostgresTimeoutConfig {\n /**\n * Milliseconds to wait for a pool client before failing the acquisition.\n *\n * Bounds `pool.connect()`, whose `pg` default of `0` waits forever — the\n * failure mode where one runaway query silently converts into a hung request\n * queue. Emitted as `connectionTimeoutMillis`.\n *\n * @default '10s'\n */\n connectionTimeout?: string | number;\n\n /**\n * Session `statement_timeout`: the ceiling on a single statement.\n *\n * @default '30s'\n */\n statementTimeout?: string | number;\n\n /**\n * Session `idle_in_transaction_session_timeout`: the ceiling on an open\n * transaction that is not currently executing a statement. This is the one\n * that releases locks held by a request that died between statements.\n *\n * @default '60s'\n */\n idleInTransactionSessionTimeout?: string | number;\n\n /**\n * Session `lock_timeout`: the ceiling on waiting for a lock.\n *\n * @default '10s'\n */\n lockTimeout?: string | number;\n}\n\n/**\n * Timeouts resolved to milliseconds, ready to emit.\n */\nexport interface ResolvedPostgresTimeouts {\n connectionTimeoutMs: number;\n statementTimeoutMs: number;\n idleInTransactionSessionTimeoutMs: number;\n lockTimeoutMs: number;\n}\n\n/**\n * Defaults applied to every runtime PostgreSQL pool.\n *\n * Chosen to bound the failure modes in #2377 without truncating legitimate\n * request work: a web request that needs more than 30 s of server time or\n * holds a transaction open for more than a minute is the pathology these\n * bound, not the workload. Long-running maintenance does not run on this pool\n * — `db:migrate` carries its own budget (#2362) — and any deployment that\n * disagrees raises or disables the value per key.\n */\nexport const DEFAULT_POSTGRES_TIMEOUTS: ResolvedPostgresTimeouts = {\n connectionTimeoutMs: 10_000,\n statementTimeoutMs: 30_000,\n idleInTransactionSessionTimeoutMs: 60_000,\n lockTimeoutMs: 10_000,\n};\n\n/**\n * Environment variable read for each timeout when configuration does not set\n * it.\n *\n * Deployments that only ever hand SMRT a `DATABASE_URL` (SvelteKit runtime\n * config, container env, serverless) have no object to put a `timeouts` key on,\n * so the environment is the configuration surface that actually reaches them.\n */\nexport const POSTGRES_TIMEOUT_ENV_VARS = {\n connectionTimeout: 'SMRT_PG_CONNECTION_TIMEOUT',\n statementTimeout: 'SMRT_PG_STATEMENT_TIMEOUT',\n idleInTransactionSessionTimeout: 'SMRT_PG_IDLE_IN_TRANSACTION_TIMEOUT',\n lockTimeout: 'SMRT_PG_LOCK_TIMEOUT',\n} as const satisfies Record<keyof PostgresTimeoutConfig, string>;\n\n/**\n * PostgreSQL connection-string parameters that carry a session timeout.\n *\n * `pg` reads exactly these three out of the connection string and puts them in\n * the startup packet (`Client.getStartupConf`), which is why the URL is the\n * delivery mechanism: the settings then apply to every client the pool opens,\n * not just the first, with no per-checkout `SET` round trip.\n */\nconst URL_TIMEOUT_PARAMS = {\n statementTimeoutMs: 'statement_timeout',\n idleInTransactionSessionTimeoutMs: 'idle_in_transaction_session_timeout',\n lockTimeoutMs: 'lock_timeout',\n} as const;\n\n/**\n * Parse a PostgreSQL timeout expressed as milliseconds or as the duration\n * string used in `smrt.config.js` into milliseconds.\n *\n * Accepts a bare number (milliseconds) or a number with an `ms`, `s`, `min`,\n * `m`, or `h` suffix. Returns `fallback` for `undefined`, empty, or\n * unparseable input so a typo degrades to the documented default instead of\n * silently disabling the timeout it was meant to impose.\n *\n * This is the single implementation behind both timeout surfaces: the\n * migration tracker re-exports it as the public\n * `@happyvertical/smrt-core/migrations` symbol (#2362), so a runtime `timeouts`\n * config and `migrations.postgres.*` are parsed by the same function and cannot\n * drift. This module deliberately imports nothing, which is what lets the\n * migration path depend on it without dragging anything into the connection\n * path.\n *\n * @param value - Millisecond count or duration string\n * @param fallback - Value returned for missing or unparseable input\n * @returns Timeout in milliseconds\n */\nexport function parsePostgresTimeoutMs(\n value: string | number | undefined,\n fallback: number,\n): number {\n if (typeof value === 'number') {\n return Number.isFinite(value) && value >= 0 ? Math.trunc(value) : fallback;\n }\n\n if (typeof value !== 'string' || value.trim().length === 0) {\n return fallback;\n }\n\n const match = /^\\s*(\\d+(?:\\.\\d+)?)\\s*(ms|s|min|m|h)?\\s*$/i.exec(value);\n if (!match) {\n return fallback;\n }\n\n const amount = Number.parseFloat(match[1]);\n const unit = (match[2] ?? 'ms').toLowerCase();\n const multiplier =\n unit === 'h'\n ? 3_600_000\n : unit === 'min' || unit === 'm'\n ? 60_000\n : unit === 's'\n ? 1000\n : 1;\n\n // The regex admits an arbitrarily long digit run, so `parseFloat` — and the\n // unit multiplication after it — can overflow to `Infinity`. That is\n // unparseable input, not a timeout: emitted into a URL parameter or a pool\n // option it fails connection setup somewhere far from the typo that caused\n // it. The numeric branch above already refuses non-finite input; this keeps\n // the string branch honest to the same contract.\n const milliseconds = amount * multiplier;\n return Number.isFinite(milliseconds) ? Math.trunc(milliseconds) : fallback;\n}\n\n/**\n * Resolve the effective runtime timeouts.\n *\n * Precedence, highest first: explicit configuration, then the matching\n * `SMRT_PG_*` environment variable, then {@link DEFAULT_POSTGRES_TIMEOUTS}.\n * A parameter already present in the connection URL beats all three, but that\n * is enforced where the URL is written ({@link applyPostgresTimeoutsToUrl}) —\n * an operator who spelled a timeout into the DSN keeps it.\n *\n * @param config - Timeouts from configuration\n * @param env - Environment to read (defaults to `process.env`)\n * @returns Timeouts in milliseconds\n */\nexport function resolvePostgresTimeouts(\n config: PostgresTimeoutConfig = {},\n env?: Record<string, string | undefined>,\n): ResolvedPostgresTimeouts {\n // Read `process.env` lazily rather than through a default parameter, which\n // would dereference `process` on every call — including the SQLite ones that\n // never need it — in whatever environment this module is loaded.\n const environment =\n env ?? (typeof process !== 'undefined' ? process.env : {});\n const resolve = (\n key: keyof PostgresTimeoutConfig,\n fallback: number,\n ): number =>\n parsePostgresTimeoutMs(\n config[key] ?? environment[POSTGRES_TIMEOUT_ENV_VARS[key]],\n fallback,\n );\n\n return {\n connectionTimeoutMs: resolve(\n 'connectionTimeout',\n DEFAULT_POSTGRES_TIMEOUTS.connectionTimeoutMs,\n ),\n statementTimeoutMs: resolve(\n 'statementTimeout',\n DEFAULT_POSTGRES_TIMEOUTS.statementTimeoutMs,\n ),\n idleInTransactionSessionTimeoutMs: resolve(\n 'idleInTransactionSessionTimeout',\n DEFAULT_POSTGRES_TIMEOUTS.idleInTransactionSessionTimeoutMs,\n ),\n lockTimeoutMs: resolve(\n 'lockTimeout',\n DEFAULT_POSTGRES_TIMEOUTS.lockTimeoutMs,\n ),\n };\n}\n\n/**\n * Does this configuration open a PostgreSQL connection?\n *\n * Deliberately narrower than `detectEngine()` from the DDL module: that helper\n * answers \"which dialect do I generate SQL in\" and folds `json` into `duckdb`,\n * while this one answers \"may I rewrite this URL\", which must be false for\n * every non-PostgreSQL adapter and must not pull the DDL strategy singletons\n * into the connection path.\n */\nexport function isPostgresTarget(url?: string, type?: string): boolean {\n const normalized = typeof type === 'string' ? type.trim().toLowerCase() : '';\n if (normalized) {\n if (\n normalized === 'postgres' ||\n normalized === 'postgresql' ||\n normalized === 'pg'\n ) {\n return true;\n }\n // An explicit non-PostgreSQL type wins over a URL that merely looks like\n // one; the adapter that will actually run is the one named here. An empty\n // or whitespace `type` is not an answer, so it falls through to the URL.\n return false;\n }\n\n const normalizedUrl = (url ?? '').toLowerCase();\n return (\n normalizedUrl.startsWith('postgres://') ||\n normalizedUrl.startsWith('postgresql://')\n );\n}\n\n/**\n * Write the session timeout parameters into a PostgreSQL connection URL.\n *\n * Parameters already present are left exactly as they are — an operator who\n * put `statement_timeout` in the DSN configured it on purpose, and the\n * per-parameter granularity means overriding one does not surrender the other\n * two to `pg`'s unbounded defaults.\n *\n * The DSN is edited by **appending**, never by re-serializing. Everything the\n * caller wrote — scheme, userinfo, host, path, and the existing query substring\n * byte for byte — is copied through, and only the missing parameters are added\n * after it. `URLSearchParams` is used to *read* which parameters are already\n * present; writing through it would hand `pg` a re-encoded DSN (`%20` becoming\n * `+`, parameters reordered) that is merely equivalent rather than identical,\n * which is not this function's business. The emitted values are bare digits, so\n * appending needs no encoding of its own.\n *\n * @param url - PostgreSQL connection URL\n * @param timeouts - Resolved timeouts\n * @returns The URL with the missing timeout parameters appended (the same\n * string when all three were already present)\n */\nexport function applyPostgresTimeoutsToUrl(\n url: string,\n timeouts: ResolvedPostgresTimeouts,\n): string {\n const fragmentStart = url.indexOf('#');\n const fragment = fragmentStart === -1 ? '' : url.slice(fragmentStart);\n const withoutFragment =\n fragmentStart === -1 ? url : url.slice(0, fragmentStart);\n const queryStart = withoutFragment.indexOf('?');\n const base =\n queryStart === -1 ? withoutFragment : withoutFragment.slice(0, queryStart);\n const query = queryStart === -1 ? '' : withoutFragment.slice(queryStart + 1);\n const present = new URLSearchParams(query);\n\n const additions: string[] = [];\n for (const [key, param] of Object.entries(URL_TIMEOUT_PARAMS) as [\n keyof typeof URL_TIMEOUT_PARAMS,\n string,\n ][]) {\n if (present.has(param)) continue;\n additions.push(`${param}=${Math.max(0, Math.trunc(timeouts[key]))}`);\n }\n\n if (additions.length === 0) {\n return url;\n }\n\n const separator = query.length > 0 ? '&' : '';\n return `${base}?${query}${separator}${additions.join('&')}${fragment}`;\n}\n\n/**\n * Options object accepted and returned by\n * {@link applyPostgresRuntimeTimeouts}.\n */\nexport interface PostgresTimeoutAwareConfig {\n url?: string;\n type?: string;\n /** Runtime timeout overrides for this connection. */\n timeouts?: PostgresTimeoutConfig;\n /**\n * `pg` pool option emitted for PostgreSQL configurations. Present on the\n * result, not on the input.\n */\n connectionTimeoutMillis?: number;\n [key: string]: unknown;\n}\n\n/**\n * Bound a runtime database configuration before it reaches `getDatabase()`.\n *\n * Non-PostgreSQL configurations pass through unchanged — the same reference,\n * so SQLite/DuckDB/JSON callers pay nothing and no file path is ever rewritten\n * — except that a stray `timeouts` key is stripped on every engine, because it\n * is SMRT configuration and never an adapter option.\n *\n * For PostgreSQL the returned object carries a URL with the three session\n * timeouts and a `connectionTimeoutMillis` pool option. Callers that derive a\n * `dbid` from the URL must derive it from the **returned** URL: two configs\n * that differ only in their timeouts are different pools, and every call site\n * applying the same deterministic rewrite is what keeps them one pool when the\n * timeouts match.\n *\n * A PostgreSQL configuration with no `url` — discrete `host`/`port`/`database`\n * fields, or a bare `{ type: 'postgres' }` that lets the adapter read\n * `HAVE_SQL_URL`/`SQLOO_URL` — has nowhere to carry the session parameters and\n * receives only `connectionTimeoutMillis`. Reproducing the adapter's own\n * environment precedence here to synthesize a URL would fork it; pass the URL\n * through the config (or set the `SMRT_PG_*` variables on a config that does\n * carry one) to get the session timeouts.\n *\n * The `timeouts` key is consumed here and not forwarded — `@happyvertical/sql`\n * would ignore it, and leaving it in the options object would make it look\n * like a supported adapter option.\n *\n * Rewriting the URL also changes the SDK's own pool cache key for call sites\n * that pass no `dbid`, so a consumer that separately calls `getDatabase()` with\n * the same raw URL no longer shares SMRT's pool. Consumers that want one pool\n * should hand SMRT the `DatabaseInterface`, not the same URL.\n *\n * @param config - Database configuration bound for `getDatabase()`\n * @param env - Environment to read (defaults to `process.env`)\n * @returns Configuration with runtime timeouts applied\n */\nexport function applyPostgresRuntimeTimeouts(\n config: PostgresTimeoutAwareConfig,\n env?: Record<string, string | undefined>,\n): PostgresTimeoutAwareConfig {\n const { timeouts: configuredTimeouts, ...rest } = config;\n\n if (!isPostgresTarget(config.url, config.type)) {\n return configuredTimeouts === undefined ? config : rest;\n }\n\n const timeouts = resolvePostgresTimeouts(configuredTimeouts, env);\n\n return {\n ...rest,\n ...(config.url\n ? { url: applyPostgresTimeoutsToUrl(config.url, timeouts) }\n : {}),\n connectionTimeoutMillis: timeouts.connectionTimeoutMs,\n };\n}\n"],"mappings":"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"}
@@ -0,0 +1,97 @@
1
+ import { ValidationError } from './errors';
2
+ /**
3
+ * Page size a generated list surface uses when the caller supplies none.
4
+ *
5
+ * Matches the value the REST and SvelteKit generators already hard-coded, so
6
+ * adopting the shared parser is not a behavior change for callers that omit
7
+ * `limit`.
8
+ */
9
+ export declare const DEFAULT_LIST_LIMIT = 50;
10
+ /**
11
+ * Largest page a generated list surface will serve.
12
+ *
13
+ * Matches the cap `MCPGenerator` already applied in-process, now enforced on
14
+ * every generated surface rather than one of them.
15
+ */
16
+ export declare const MAX_LIST_LIMIT = 1000;
17
+ /**
18
+ * A `limit`/`offset` value that cannot be honored.
19
+ *
20
+ * Extends `ValidationError` so the retry classifier never retries it, and
21
+ * carries `status`/`publicMessage` so `normalizeTypedHttpError()` renders it as
22
+ * a structured 400 instead of letting a caller typo become a 500.
23
+ */
24
+ export declare class QueryBoundsError extends ValidationError {
25
+ readonly status = 400;
26
+ readonly publicMessage: string;
27
+ constructor(message: string, code?: string, details?: Record<string, unknown>);
28
+ }
29
+ /**
30
+ * An `orderBy` term that cannot be honored (#2367).
31
+ *
32
+ * `orderBy` reaches SQL in the identifier position, and until this rail existed
33
+ * it was checked only against `/^[a-zA-Z0-9_]+$/` — so an unknown column
34
+ * produced a driver error and a *sensitive* column produced a working ordering
35
+ * oracle: `?orderBy=api_secret&limit=1` reveals the extreme value of a secret
36
+ * one request at a time, even though the column itself is never serialized.
37
+ * `where` and `select` have rejected sensitive fields since #1540/#1902; this
38
+ * is the same rail for the third identifier-position input.
39
+ */
40
+ export declare class QueryOrderByError extends ValidationError {
41
+ readonly status = 400;
42
+ readonly publicMessage: string;
43
+ constructor(message: string, publicMessage: string, code?: string, details?: Record<string, unknown>);
44
+ }
45
+ /**
46
+ * Options for {@link resolveListLimit} / {@link resolveListOffset}.
47
+ */
48
+ export interface ListBoundOptions {
49
+ /** Value used when the input is absent (`null`, `undefined`, or `''`). */
50
+ defaultValue?: number;
51
+ /** Upper bound; larger values are clamped down to it. Omit for none. */
52
+ maxValue?: number;
53
+ /** Parameter name used in the rejection message. */
54
+ parameterName?: string;
55
+ }
56
+ /**
57
+ * Resolve a page size from untrusted input: default when absent, rejected when
58
+ * malformed, clamped when oversized.
59
+ *
60
+ * @throws {QueryBoundsError} when the input is not a non-negative integer
61
+ */
62
+ export declare function resolveListLimit(raw: unknown, options?: ListBoundOptions): number;
63
+ /**
64
+ * Resolve a page offset from untrusted input.
65
+ *
66
+ * Unbounded above by default: an offset past the end of the table returns an
67
+ * empty page, and capping it would silently serve the wrong page instead.
68
+ *
69
+ * @throws {QueryBoundsError} when the input is not a non-negative integer
70
+ */
71
+ export declare function resolveListOffset(raw: unknown, options?: ListBoundOptions): number;
72
+ /**
73
+ * Deterministic default ordering for a generated list page.
74
+ *
75
+ * `LIMIT`/`OFFSET` without `ORDER BY` is not pagination: PostgreSQL may return
76
+ * rows in any order and is free to choose a different one per query, so page 2
77
+ * can repeat or skip rows from page 1. `created_at DESC` alone is not enough
78
+ * either — rows sharing a timestamp still tie — so the primary key follows as a
79
+ * total-order tiebreak.
80
+ *
81
+ * The index that makes this ordering cheap is #2363.
82
+ */
83
+ export declare const DEFAULT_LIST_ORDER_BY: readonly string[];
84
+ /**
85
+ * Deterministic default ordering for a model whose primary key may not be `id`.
86
+ *
87
+ * A class declaring `@field({ primaryKey: true })` on its own column has no
88
+ * synthetic `id` column — schema generation omits `id`/`slug`/`context` for
89
+ * custom-primary-key classes — so `ORDER BY ... id ASC` would name a column
90
+ * that does not exist and fail the whole page. The tiebreak therefore follows
91
+ * the declared key.
92
+ *
93
+ * @param primaryKeyField - the model's primary-key field name in SMRT (not
94
+ * column) form; `collection.list()` maps it to the column
95
+ */
96
+ export declare function buildDefaultListOrderBy(primaryKeyField?: string): string[];
97
+ //# sourceMappingURL=query-bounds.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"query-bounds.d.ts","sourceRoot":"","sources":["../src/query-bounds.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AAEH,OAAO,EAAE,eAAe,EAAE,MAAM,UAAU,CAAC;AAE3C;;;;;;GAMG;AACH,eAAO,MAAM,kBAAkB,KAAK,CAAC;AAErC;;;;;GAKG;AACH,eAAO,MAAM,cAAc,OAAO,CAAC;AAEnC;;;;;;GAMG;AACH,qBAAa,gBAAiB,SAAQ,eAAe;IACnD,QAAQ,CAAC,MAAM,OAAO;IACtB,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAC;gBAG7B,OAAO,EAAE,MAAM,EACf,IAAI,SAAyB,EAC7B,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC;CAMpC;AAED;;;;;;;;;;GAUG;AACH,qBAAa,iBAAkB,SAAQ,eAAe;IACpD,QAAQ,CAAC,MAAM,OAAO;IACtB,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAC;gBAG7B,OAAO,EAAE,MAAM,EACf,aAAa,EAAE,MAAM,EACrB,IAAI,SAAqB,EACzB,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC;CAMpC;AAED;;GAEG;AACH,MAAM,WAAW,gBAAgB;IAC/B,0EAA0E;IAC1E,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,wEAAwE;IACxE,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,oDAAoD;IACpD,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB;AA2DD;;;;;GAKG;AACH,wBAAgB,gBAAgB,CAC9B,GAAG,EAAE,OAAO,EACZ,OAAO,GAAE,gBAAqB,GAC7B,MAAM,CAMR;AAED;;;;;;;GAOG;AACH,wBAAgB,iBAAiB,CAC/B,GAAG,EAAE,OAAO,EACZ,OAAO,GAAE,gBAAqB,GAC7B,MAAM,CAMR;AAED;;;;;;;;;;GAUG;AACH,eAAO,MAAM,qBAAqB,EAAE,SAAS,MAAM,EAGlD,CAAC;AAEF;;;;;;;;;;;GAWG;AACH,wBAAgB,uBAAuB,CAAC,eAAe,SAAO,GAAG,MAAM,EAAE,CAExE"}