@happyvertical/smrt-core 0.40.69 → 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.
- package/AGENTS.md +29 -4
- package/README.md +20 -1
- package/agents/query-bounds.md +45 -0
- package/agents/schema-paths.md +328 -0
- package/dist/browser.d.ts +1 -0
- package/dist/browser.d.ts.map +1 -1
- package/dist/browser.js +2 -1
- package/dist/class.d.ts.map +1 -1
- package/dist/class.js +28 -4
- package/dist/class.js.map +1 -1
- package/dist/collection.d.ts +101 -2
- package/dist/collection.d.ts.map +1 -1
- package/dist/collection.js +224 -43
- package/dist/collection.js.map +1 -1
- package/dist/database.d.ts +8 -0
- package/dist/database.d.ts.map +1 -1
- package/dist/database.js +16 -8
- package/dist/database.js.map +1 -1
- package/dist/db-errors.d.ts +105 -0
- package/dist/db-errors.d.ts.map +1 -0
- package/dist/db-errors.js +382 -0
- package/dist/db-errors.js.map +1 -0
- package/dist/decorators/index.d.ts +17 -1
- package/dist/decorators/index.d.ts.map +1 -1
- package/dist/decorators/index.js +65 -7
- package/dist/decorators/index.js.map +1 -1
- package/dist/dispatch/bus.d.ts.map +1 -1
- package/dist/dispatch/bus.js +4 -3
- package/dist/dispatch/bus.js.map +1 -1
- package/dist/errors.d.ts +30 -2
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +34 -2
- package/dist/errors.js.map +1 -1
- package/dist/generators/changes-route.d.ts.map +1 -1
- package/dist/generators/changes-route.js +6 -3
- package/dist/generators/changes-route.js.map +1 -1
- package/dist/generators/mcp-runtime-template.d.ts +8 -0
- package/dist/generators/mcp-runtime-template.d.ts.map +1 -1
- package/dist/generators/mcp-runtime-template.js +38 -4
- package/dist/generators/mcp-runtime-template.js.map +1 -1
- package/dist/generators/mcp.d.ts +16 -0
- package/dist/generators/mcp.d.ts.map +1 -1
- package/dist/generators/mcp.js +41 -3
- package/dist/generators/mcp.js.map +1 -1
- package/dist/generators/rest.d.ts +22 -0
- package/dist/generators/rest.d.ts.map +1 -1
- package/dist/generators/rest.js +34 -3
- package/dist/generators/rest.js.map +1 -1
- package/dist/index.d.ts +6 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +8 -3
- package/dist/manifest/static-manifest.js +1 -1
- package/dist/manifest/static-manifest.js.map +1 -1
- package/dist/manifest/store.js +1 -1
- package/dist/manifest.json +1 -1
- package/dist/migrations/differ.d.ts +202 -8
- package/dist/migrations/differ.d.ts.map +1 -1
- package/dist/migrations/differ.js +604 -46
- package/dist/migrations/differ.js.map +1 -1
- package/dist/migrations/generator.d.ts +31 -4
- package/dist/migrations/generator.d.ts.map +1 -1
- package/dist/migrations/generator.js +49 -5
- package/dist/migrations/generator.js.map +1 -1
- package/dist/migrations/index.d.ts +3 -2
- package/dist/migrations/index.d.ts.map +1 -1
- package/dist/migrations/index.js +5 -3
- package/dist/migrations/orchestrate.js +35 -6
- package/dist/migrations/orchestrate.js.map +1 -1
- package/dist/migrations/sqlite-rebuild.d.ts +142 -0
- package/dist/migrations/sqlite-rebuild.d.ts.map +1 -0
- package/dist/migrations/sqlite-rebuild.js +514 -0
- package/dist/migrations/sqlite-rebuild.js.map +1 -0
- package/dist/migrations/tracker.d.ts +114 -1
- package/dist/migrations/tracker.d.ts.map +1 -1
- package/dist/migrations/tracker.js +331 -16
- package/dist/migrations/tracker.js.map +1 -1
- package/dist/migrations/types.d.ts +19 -4
- package/dist/migrations/types.d.ts.map +1 -1
- package/dist/migrations.js +5 -3
- package/dist/object.d.ts +45 -6
- package/dist/object.d.ts.map +1 -1
- package/dist/object.js +60 -13
- package/dist/object.js.map +1 -1
- package/dist/postgres-timeouts.d.ts +240 -0
- package/dist/postgres-timeouts.d.ts.map +1 -0
- package/dist/postgres-timeouts.js +204 -0
- package/dist/postgres-timeouts.js.map +1 -0
- package/dist/query-bounds.d.ts +97 -0
- package/dist/query-bounds.d.ts.map +1 -0
- package/dist/query-bounds.js +173 -0
- package/dist/query-bounds.js.map +1 -0
- package/dist/registry/types.d.ts +32 -1
- package/dist/registry/types.d.ts.map +1 -1
- package/dist/registry.js +1 -1
- package/dist/scanner/types.d.ts +5 -0
- package/dist/scanner/types.d.ts.map +1 -1
- package/dist/scanner/types.js.map +1 -1
- package/dist/schema/ddl/base-strategy.d.ts.map +1 -1
- package/dist/schema/ddl/base-strategy.js +2 -2
- package/dist/schema/ddl/base-strategy.js.map +1 -1
- package/dist/schema/generator.d.ts +70 -5
- package/dist/schema/generator.d.ts.map +1 -1
- package/dist/schema/generator.js +124 -6
- package/dist/schema/generator.js.map +1 -1
- package/dist/schema/index.d.ts +3 -0
- package/dist/schema/index.d.ts.map +1 -1
- package/dist/schema/index.js +4 -1
- package/dist/schema/live-parity.d.ts +90 -0
- package/dist/schema/live-parity.d.ts.map +1 -0
- package/dist/schema/live-parity.js +600 -0
- package/dist/schema/live-parity.js.map +1 -0
- package/dist/schema/manifest-schema.d.ts +121 -0
- package/dist/schema/manifest-schema.d.ts.map +1 -0
- package/dist/schema/manifest-schema.js +267 -0
- package/dist/schema/manifest-schema.js.map +1 -0
- package/dist/schema/schema-aggregator.d.ts +24 -10
- package/dist/schema/schema-aggregator.d.ts.map +1 -1
- package/dist/schema/schema-aggregator.js +35 -90
- package/dist/schema/schema-aggregator.js.map +1 -1
- package/dist/schema/system-table-shapes.d.ts +52 -0
- package/dist/schema/system-table-shapes.d.ts.map +1 -0
- package/dist/schema/system-table-shapes.js +174 -0
- package/dist/schema/system-table-shapes.js.map +1 -0
- package/dist/schema/types.d.ts +103 -4
- package/dist/schema/types.d.ts.map +1 -1
- package/dist/schema/utils.d.ts +1 -0
- package/dist/schema/utils.d.ts.map +1 -1
- package/dist/schema/utils.js +3 -1
- package/dist/schema/utils.js.map +1 -1
- package/dist/schema.js +4 -1
- package/dist/smrt-knowledge.json +18 -6
- package/dist/sync/apply.d.ts.map +1 -1
- package/dist/sync/apply.js +9 -16
- package/dist/sync/apply.js.map +1 -1
- package/dist/testing/database.d.ts.map +1 -1
- package/dist/testing/database.js +1 -0
- package/dist/testing/database.js.map +1 -1
- package/dist/vite-plugin/sveltekit-generator.d.ts.map +1 -1
- package/dist/vite-plugin/sveltekit-generator.js +71 -5
- package/dist/vite-plugin/sveltekit-generator.js.map +1 -1
- package/dist/vite-plugin/web-collections.d.ts.map +1 -1
- package/dist/vite-plugin/web-collections.js +6 -4
- package/dist/vite-plugin/web-collections.js.map +1 -1
- package/package.json +5 -5
|
@@ -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":";;;;;;;;;;;AA4GA,IAAa,4BAAsD;CACjE,qBAAqB;CACrB,oBAAoB;CACpB,mCAAmC;CACnC,eAAe;AACjB;;;;;;;;;AAUA,IAAa,4BAA4B;CACvC,mBAAmB;CACnB,kBAAkB;CAClB,iCAAiC;CACjC,aAAa;AACf;;;;;;;;;AAUA,IAAM,qBAAqB;CACzB,oBAAoB;CACpB,mCAAmC;CACnC,eAAe;AACjB;;;;;;;;;;;;;;;;;;;;;;AAuBA,SAAgB,uBACd,OACA,UACQ;CACR,IAAI,OAAO,UAAU,UACnB,OAAO,OAAO,SAAS,KAAK,KAAK,SAAS,IAAI,KAAK,MAAM,KAAK,IAAI;CAGpE,IAAI,OAAO,UAAU,YAAY,MAAM,KAAK,CAAC,CAAC,WAAW,GACvD,OAAO;CAGT,MAAM,QAAQ,6CAA6C,KAAK,KAAK;CACrE,IAAI,CAAC,OACH,OAAO;CAGT,MAAM,SAAS,OAAO,WAAW,MAAM,EAAE;CACzC,MAAM,QAAQ,MAAM,MAAM,KAAA,CAAM,YAAY;CAgB5C,MAAM,eAAe,UAdnB,SAAS,MACL,OACA,SAAS,SAAS,SAAS,MACzB,MACA,SAAS,MACP,MACA;CASV,OAAO,OAAO,SAAS,YAAY,IAAI,KAAK,MAAM,YAAY,IAAI;AACpE;;;;;;;;;;;;;;AAeA,SAAgB,wBACd,SAAgC,CAAC,GACjC,KAC0B;CAI1B,MAAM,cACJ,QAAQ,OAAO,YAAY,cAAc,QAAQ,MAAM,CAAC;CAC1D,MAAM,WACJ,KACA,aAEA,uBACE,OAAO,QAAQ,YAAY,0BAA0B,OACrD,QACF;CAEF,OAAO;EACL,qBAAqB,QACnB,qBACA,0BAA0B,mBAC5B;EACA,oBAAoB,QAClB,oBACA,0BAA0B,kBAC5B;EACA,mCAAmC,QACjC,mCACA,0BAA0B,iCAC5B;EACA,eAAe,QACb,eACA,0BAA0B,aAC5B;CACF;AACF;;;;;;;;;;AAWA,SAAgB,iBAAiB,KAAc,MAAwB;CACrE,MAAM,aAAa,OAAO,SAAS,WAAW,KAAK,KAAK,CAAC,CAAC,YAAY,IAAI;CAC1E,IAAI,YAAY;EACd,IACE,eAAe,cACf,eAAe,gBACf,eAAe,MAEf,OAAO;EAKT,OAAO;CACT;CAEA,MAAM,iBAAiB,OAAO,GAAA,CAAI,YAAY;CAC9C,OACE,cAAc,WAAW,aAAa,KACtC,cAAc,WAAW,eAAe;AAE5C;;;;;;;;;;;;;;;;;;;;;;;AAwBA,SAAgB,2BACd,KACA,UACQ;CACR,MAAM,gBAAgB,IAAI,QAAQ,GAAG;CACrC,MAAM,WAAW,kBAAkB,KAAK,KAAK,IAAI,MAAM,aAAa;CACpE,MAAM,kBACJ,kBAAkB,KAAK,MAAM,IAAI,MAAM,GAAG,aAAa;CACzD,MAAM,aAAa,gBAAgB,QAAQ,GAAG;CAC9C,MAAM,OACJ,eAAe,KAAK,kBAAkB,gBAAgB,MAAM,GAAG,UAAU;CAC3E,MAAM,QAAQ,eAAe,KAAK,KAAK,gBAAgB,MAAM,aAAa,CAAC;CAC3E,MAAM,UAAU,IAAI,gBAAgB,KAAK;CAEzC,MAAM,YAAsB,CAAC;CAC7B,KAAK,MAAM,CAAC,KAAK,UAAU,OAAO,QAAQ,kBAAkB,GAGvD;EACH,IAAI,QAAQ,IAAI,KAAK,GAAG;EACxB,UAAU,KAAK,GAAG,MAAM,GAAG,KAAK,IAAI,GAAG,KAAK,MAAM,SAAS,IAAI,CAAC,GAAG;CACrE;CAEA,IAAI,UAAU,WAAW,GACvB,OAAO;CAIT,OAAO,GAAG,KAAK,GAAG,QADA,MAAM,SAAS,IAAI,MAAM,KACL,UAAU,KAAK,GAAG,IAAI;AAC9D;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAuDA,SAAgB,6BACd,QACA,KAC4B;CAC5B,MAAM,EAAE,UAAU,oBAAoB,GAAG,SAAS;CAElD,IAAI,CAAC,iBAAiB,OAAO,KAAK,OAAO,IAAI,GAC3C,OAAO,uBAAuB,KAAA,IAAY,SAAS;CAGrD,MAAM,WAAW,wBAAwB,oBAAoB,GAAG;CAEhE,OAAO;EACL,GAAG;EACH,GAAI,OAAO,MACP,EAAE,KAAK,2BAA2B,OAAO,KAAK,QAAQ,EAAE,IACxD,CAAC;EACL,yBAAyB,SAAS;CACpC;AACF"}
|
|
@@ -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"}
|