@happyvertical/smrt-core 0.57.46 → 0.57.47

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 (189) hide show
  1. package/AGENTS.md +11 -3
  2. package/agents/browser-entry.md +16 -0
  3. package/agents/build-knowledge.md +58 -14
  4. package/agents/change-feed.md +20 -0
  5. package/agents/generators.md +69 -3
  6. package/agents/schema-paths.md +82 -0
  7. package/dist/_virtual/_rolldown/runtime.js +22 -1
  8. package/dist/browser.d.ts +2 -2
  9. package/dist/browser.d.ts.map +1 -1
  10. package/dist/browser.js +2 -2
  11. package/dist/cascade.d.ts +46 -0
  12. package/dist/cascade.d.ts.map +1 -1
  13. package/dist/cascade.js +96 -7
  14. package/dist/cascade.js.map +1 -1
  15. package/dist/change-feed.d.ts.map +1 -1
  16. package/dist/change-feed.js +7 -4
  17. package/dist/change-feed.js.map +1 -1
  18. package/dist/class.d.ts.map +1 -1
  19. package/dist/class.js +3 -3
  20. package/dist/class.js.map +1 -1
  21. package/dist/consumer-plugin/index.d.ts +13 -0
  22. package/dist/consumer-plugin/index.d.ts.map +1 -1
  23. package/dist/consumer-plugin/index.js +69 -13
  24. package/dist/consumer-plugin/index.js.map +1 -1
  25. package/dist/consumer-plugin.js +2 -2
  26. package/dist/database.d.ts.map +1 -1
  27. package/dist/database.js +3 -3
  28. package/dist/database.js.map +1 -1
  29. package/dist/dispatch/bus.d.ts.map +1 -1
  30. package/dist/dispatch/bus.js +1 -4
  31. package/dist/dispatch/bus.js.map +1 -1
  32. package/dist/dispatch/index.d.ts +1 -1
  33. package/dist/dispatch/index.d.ts.map +1 -1
  34. package/dist/dispatch/types.d.ts +22 -3
  35. package/dist/dispatch/types.d.ts.map +1 -1
  36. package/dist/generators/changes-route.d.ts.map +1 -1
  37. package/dist/generators/changes-route.js +2 -2
  38. package/dist/generators/changes-route.js.map +1 -1
  39. package/dist/generators/custom-action.d.ts +11 -0
  40. package/dist/generators/custom-action.d.ts.map +1 -1
  41. package/dist/generators/custom-action.js +23 -4
  42. package/dist/generators/custom-action.js.map +1 -1
  43. package/dist/index.d.ts +2 -2
  44. package/dist/index.d.ts.map +1 -1
  45. package/dist/index.js +3 -2
  46. package/dist/lazy-config.d.ts +120 -3
  47. package/dist/lazy-config.d.ts.map +1 -1
  48. package/dist/lazy-config.js +81 -10
  49. package/dist/lazy-config.js.map +1 -1
  50. package/dist/manifest/generator.d.ts +8 -2
  51. package/dist/manifest/generator.d.ts.map +1 -1
  52. package/dist/manifest/generator.js +6 -5
  53. package/dist/manifest/generator.js.map +1 -1
  54. package/dist/manifest/index.d.ts +1 -0
  55. package/dist/manifest/index.d.ts.map +1 -1
  56. package/dist/manifest/index.js +2 -1
  57. package/dist/manifest/index.js.map +1 -1
  58. package/dist/manifest/local-manifest-reader.d.ts +13 -2
  59. package/dist/manifest/local-manifest-reader.d.ts.map +1 -1
  60. package/dist/manifest/local-manifest-reader.js +38 -6
  61. package/dist/manifest/local-manifest-reader.js.map +1 -1
  62. package/dist/manifest/manifest-loader.d.ts +9 -8
  63. package/dist/manifest/manifest-loader.d.ts.map +1 -1
  64. package/dist/manifest/manifest-loader.js +67 -27
  65. package/dist/manifest/manifest-loader.js.map +1 -1
  66. package/dist/manifest/object-declarations.d.ts +116 -0
  67. package/dist/manifest/object-declarations.d.ts.map +1 -0
  68. package/dist/manifest/object-declarations.js +161 -0
  69. package/dist/manifest/object-declarations.js.map +1 -0
  70. package/dist/manifest/static-manifest.d.ts.map +1 -1
  71. package/dist/manifest/static-manifest.js +5 -3
  72. package/dist/manifest/static-manifest.js.map +1 -1
  73. package/dist/manifest/store.d.ts.map +1 -1
  74. package/dist/manifest/store.js +18 -2
  75. package/dist/manifest/store.js.map +1 -1
  76. package/dist/manifest/test-artifacts.d.ts +57 -0
  77. package/dist/manifest/test-artifacts.d.ts.map +1 -0
  78. package/dist/manifest/test-artifacts.js +111 -0
  79. package/dist/manifest/test-artifacts.js.map +1 -0
  80. package/dist/manifest.js +2 -1
  81. package/dist/manifest.json +5 -3
  82. package/dist/migrations/differ.d.ts.map +1 -1
  83. package/dist/migrations/differ.js +11 -3
  84. package/dist/migrations/differ.js.map +1 -1
  85. package/dist/migrations/index.d.ts +2 -0
  86. package/dist/migrations/index.d.ts.map +1 -1
  87. package/dist/migrations/index.js +3 -1
  88. package/dist/migrations/qualified-name-aliases.d.ts.map +1 -1
  89. package/dist/migrations/qualified-name-aliases.js +1 -1
  90. package/dist/migrations/qualified-name-aliases.js.map +1 -1
  91. package/dist/migrations.js +3 -1
  92. package/dist/object.d.ts.map +1 -1
  93. package/dist/object.js +2 -2
  94. package/dist/object.js.map +1 -1
  95. package/dist/postgres-permissions.d.ts.map +1 -1
  96. package/dist/postgres-permissions.js +4 -3
  97. package/dist/postgres-permissions.js.map +1 -1
  98. package/dist/postgres-timeouts.d.ts +30 -3
  99. package/dist/postgres-timeouts.d.ts.map +1 -1
  100. package/dist/postgres-timeouts.js +39 -2
  101. package/dist/postgres-timeouts.js.map +1 -1
  102. package/dist/prebuild/index.d.ts.map +1 -1
  103. package/dist/prebuild/index.js +14 -11
  104. package/dist/prebuild/index.js.map +1 -1
  105. package/dist/registry/class-registration.d.ts.map +1 -1
  106. package/dist/registry/class-registration.js +24 -2
  107. package/dist/registry/class-registration.js.map +1 -1
  108. package/dist/registry/collection-exposure.d.ts +27 -0
  109. package/dist/registry/collection-exposure.d.ts.map +1 -1
  110. package/dist/registry/collection-exposure.js +74 -1
  111. package/dist/registry/collection-exposure.js.map +1 -1
  112. package/dist/registry/collection-resolution.d.ts.map +1 -1
  113. package/dist/registry/collection-resolution.js +1 -0
  114. package/dist/registry/collection-resolution.js.map +1 -1
  115. package/dist/registry/framework-base-classes.d.ts.map +1 -1
  116. package/dist/registry/framework-base-classes.js +5 -0
  117. package/dist/registry/framework-base-classes.js.map +1 -1
  118. package/dist/registry/schema-builder.d.ts +8 -1
  119. package/dist/registry/schema-builder.d.ts.map +1 -1
  120. package/dist/registry/schema-builder.js +17 -4
  121. package/dist/registry/schema-builder.js.map +1 -1
  122. package/dist/registry/types.d.ts +12 -0
  123. package/dist/registry/types.d.ts.map +1 -1
  124. package/dist/registry.d.ts +2 -1
  125. package/dist/registry.d.ts.map +1 -1
  126. package/dist/registry.js +30 -10
  127. package/dist/registry.js.map +1 -1
  128. package/dist/scanner/manifest-generator.d.ts.map +1 -1
  129. package/dist/scanner/manifest-generator.js +10 -8
  130. package/dist/scanner/manifest-generator.js.map +1 -1
  131. package/dist/scanner/types.d.ts +29 -0
  132. package/dist/scanner/types.d.ts.map +1 -1
  133. package/dist/scanner/types.js.map +1 -1
  134. package/dist/schema/generator.d.ts +32 -0
  135. package/dist/schema/generator.d.ts.map +1 -1
  136. package/dist/schema/generator.js +59 -0
  137. package/dist/schema/generator.js.map +1 -1
  138. package/dist/schema/live-parity.d.ts.map +1 -1
  139. package/dist/schema/live-parity.js +4 -2
  140. package/dist/schema/live-parity.js.map +1 -1
  141. package/dist/scripts/browser-unbundled-dev.mjs +90 -0
  142. package/dist/scripts/generate-test-manifest.js +9 -49
  143. package/dist/smrt-knowledge.json +16 -16
  144. package/dist/system/compatibility.d.ts.map +1 -1
  145. package/dist/system/compatibility.js +12 -4
  146. package/dist/system/compatibility.js.map +1 -1
  147. package/dist/system/schema.d.ts +70 -8
  148. package/dist/system/schema.d.ts.map +1 -1
  149. package/dist/system/schema.js +231 -24
  150. package/dist/system/schema.js.map +1 -1
  151. package/dist/testing/database.d.ts.map +1 -1
  152. package/dist/testing/database.js +14 -2
  153. package/dist/testing/database.js.map +1 -1
  154. package/dist/tools/tool-generator.d.ts.map +1 -1
  155. package/dist/tools/tool-generator.js +2 -2
  156. package/dist/tools/tool-generator.js.map +1 -1
  157. package/dist/utils/import-workspace-module.d.ts.map +1 -1
  158. package/dist/utils/import-workspace-module.js +23 -0
  159. package/dist/utils/import-workspace-module.js.map +1 -1
  160. package/dist/utils/naming.js +5 -3
  161. package/dist/utils/naming.js.map +1 -1
  162. package/dist/vendor/pluralize/pluralize.js +421 -0
  163. package/dist/vendor/pluralize/pluralize.js.map +1 -0
  164. package/dist/vite-plugin/api-client-entries.d.ts +5 -1
  165. package/dist/vite-plugin/api-client-entries.d.ts.map +1 -1
  166. package/dist/vite-plugin/api-client-entries.js +11 -6
  167. package/dist/vite-plugin/api-client-entries.js.map +1 -1
  168. package/dist/vite-plugin/consumer-import.d.ts +28 -0
  169. package/dist/vite-plugin/consumer-import.d.ts.map +1 -0
  170. package/dist/vite-plugin/consumer-import.js +42 -0
  171. package/dist/vite-plugin/consumer-import.js.map +1 -0
  172. package/dist/vite-plugin/dev-plane-route.d.ts +6 -1
  173. package/dist/vite-plugin/dev-plane-route.d.ts.map +1 -1
  174. package/dist/vite-plugin/dev-plane-route.js +8 -9
  175. package/dist/vite-plugin/dev-plane-route.js.map +1 -1
  176. package/dist/vite-plugin/index.d.ts.map +1 -1
  177. package/dist/vite-plugin/index.js +12 -9
  178. package/dist/vite-plugin/index.js.map +1 -1
  179. package/dist/vite-plugin/resources-route.d.ts.map +1 -1
  180. package/dist/vite-plugin/resources-route.js +3 -8
  181. package/dist/vite-plugin/resources-route.js.map +1 -1
  182. package/dist/vite-plugin/sveltekit-generator.d.ts.map +1 -1
  183. package/dist/vite-plugin/sveltekit-generator.js +7 -11
  184. package/dist/vite-plugin/sveltekit-generator.js.map +1 -1
  185. package/dist/vite-plugin/sync-apply-route.d.ts +16 -0
  186. package/dist/vite-plugin/sync-apply-route.d.ts.map +1 -1
  187. package/dist/vite-plugin/sync-apply-route.js +48 -6
  188. package/dist/vite-plugin/sync-apply-route.js.map +1 -1
  189. package/package.json +12 -12
@@ -1 +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"}
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()`, which forwards it (with `max` and\n * `idleTimeoutMillis`) to the `pg.Pool` it builds.\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 // A caller that spelled the pool option itself (`connectionTimeoutMillis`)\n // configured it on purpose; it ranks below the SMRT `timeouts` key but above\n // the environment and the default, instead of being silently replaced by the\n // default (#3046). Re-applying this function to its own output is therefore\n // idempotent.\n const explicitConnectionTimeout = (\n rest as { connectionTimeoutMillis?: unknown }\n ).connectionTimeoutMillis;\n const timeouts = resolvePostgresTimeouts(\n configuredTimeouts?.connectionTimeout === undefined &&\n typeof explicitConnectionTimeout === 'number'\n ? { ...configuredTimeouts, connectionTimeout: explicitConnectionTimeout }\n : configuredTimeouts,\n env,\n );\n\n return {\n ...rest,\n ...(config.url\n ? { url: applyPostgresTimeoutsToUrl(config.url, timeouts) }\n : {}),\n connectionTimeoutMillis: timeouts.connectionTimeoutMs,\n };\n}\n\n/**\n * The pool cache identity (`dbid`) SMRT passes to `getDatabase()` for a\n * runtime configuration already bounded by\n * {@link applyPostgresRuntimeTimeouts} (#3046).\n *\n * `@happyvertical/sql` caches connections by `dbid` alone when one is given,\n * so a dbid derived from the URL only made the first resolver of a URL fix the\n * pool's `max` and timeouts for the whole process: a later caller asking for\n * `max: 5` silently shared a 20-client pool. The rules:\n *\n * - An explicit `dbid` on the configuration is caller-owned identity and is\n * returned as given.\n * - PostgreSQL gets **no** derived dbid. The SDK then derives a keyed digest\n * from the effective connection *and* pool options (`max`,\n * `connectionTimeoutMillis`, `idleTimeoutMillis`, credentials), so equal\n * configurations still share one pool and differing pool options never do.\n * - Every other engine keeps the historical `smrt:<url>` identity, which is\n * what lets file-backed SQLite/DuckDB callers share one connection; they\n * have no pool options to disagree about.\n *\n * Callers decide the in-memory case themselves (an in-memory database is\n * never shared) and only call this for a persistent target.\n *\n * @param bounded - Configuration returned by {@link applyPostgresRuntimeTimeouts}\n * @param fallbackUrl - URL to key on when the configuration carries none\n * @returns The dbid to pass, or `undefined` to let the SDK derive one\n */\nexport function resolveRuntimePoolDbid(\n bounded: PostgresTimeoutAwareConfig,\n fallbackUrl?: string,\n): string | undefined {\n if (typeof bounded.dbid === 'string' && bounded.dbid !== '') {\n return bounded.dbid;\n }\n if (isPostgresTarget(bounded.url, bounded.type)) {\n return undefined;\n }\n const url = bounded.url ?? fallbackUrl;\n return url ? `smrt:${url}` : undefined;\n}\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/prebuild/index.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAIH,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,qBAAkB,CAAC;AAW5D,MAAM,WAAW,eAAe;IAC9B,+CAA+C;IAC/C,QAAQ,EAAE,MAAM,GAAG,mBAAmB,CAAC;IACvC,2CAA2C;IAC3C,MAAM,EAAE,MAAM,CAAC;IACf,0CAA0C;IAC1C,qBAAqB,CAAC,EAAE,OAAO,CAAC;IAChC,sCAAsC;IACtC,kBAAkB,CAAC,EAAE,OAAO,CAAC;IAC7B,qDAAqD;IACrD,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED;;GAEG;AACH,wBAAsB,oBAAoB,CACxC,OAAO,EAAE,eAAe,GACvB,OAAO,CAAC,IAAI,CAAC,CAkCf;AAkaD;;GAEG;AACH,wBAAsB,2BAA2B,CAC/C,IAAI,EAAE,MAAM,EAAE,GACb,OAAO,CAAC,IAAI,CAAC,CAkBf"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/prebuild/index.ts"],"names":[],"mappings":"AAAA;;;GAGG;AASH,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,qBAAkB,CAAC;AAQ5D,MAAM,WAAW,eAAe;IAC9B,+CAA+C;IAC/C,QAAQ,EAAE,MAAM,GAAG,mBAAmB,CAAC;IACvC,2CAA2C;IAC3C,MAAM,EAAE,MAAM,CAAC;IACf,0CAA0C;IAC1C,qBAAqB,CAAC,EAAE,OAAO,CAAC;IAChC,sCAAsC;IACtC,kBAAkB,CAAC,EAAE,OAAO,CAAC;IAC7B,qDAAqD;IACrD,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED;;GAEG;AACH,wBAAsB,oBAAoB,CACxC,OAAO,EAAE,eAAe,GACvB,OAAO,CAAC,IAAI,CAAC,CAuCf;AAsaD;;GAEG;AACH,wBAAsB,2BAA2B,CAC/C,IAAI,EAAE,MAAM,EAAE,GACb,OAAO,CAAC,IAAI,CAAC,CAkBf"}
@@ -1,4 +1,5 @@
1
- import { orderedManifestObjectEntries, selectWebCollectionEntries } from "../vite-plugin/web-collections.js";
1
+ import { selectWebCollectionEntries } from "../vite-plugin/web-collections.js";
2
+ import { createObjectDeclarationIndex, warnObjectDeclarationCollisions } from "../manifest/object-declarations.js";
2
3
  import { renderApiClientCrudType, renderApiClientCustomMethodParameters, selectApiClientEntries } from "../vite-plugin/api-client-entries.js";
3
4
  import * as path$1 from "node:path";
4
5
  import * as fs$1 from "node:fs";
@@ -16,17 +17,18 @@ async function generateDeclarations(options) {
16
17
  const fullOutDir = path$1.isAbsolute(outDir) ? outDir : path$1.join(projectRoot, outDir);
17
18
  fs$1.mkdirSync(fullOutDir, { recursive: true });
18
19
  console.log(`[smrt:prebuild] Generating declarations to ${fullOutDir}`);
19
- if (includeObjectTypes) await generateObjectTypeDeclarations(manifest, fullOutDir);
20
- if (includeVirtualModules) await generateVirtualModuleDeclarations(manifest, fullOutDir);
20
+ const declarations = createObjectDeclarationIndex(manifest);
21
+ warnObjectDeclarationCollisions(declarations, console.warn);
22
+ if (includeObjectTypes) await generateObjectTypeDeclarations(declarations, fullOutDir);
23
+ if (includeVirtualModules) await generateVirtualModuleDeclarations(manifest, declarations, fullOutDir);
21
24
  console.log(`[smrt:prebuild] Generated declarations for ${Object.keys(manifest.objects).length} SMRT objects`);
22
25
  }
23
26
  /**
24
27
  * Generate TypeScript interfaces for SMRT objects
25
28
  */
26
- async function generateObjectTypeDeclarations(manifest, outDir) {
29
+ async function generateObjectTypeDeclarations(declarations, outDir) {
27
30
  const interfaces = [];
28
- const sortedObjectEntries = orderedManifestObjectEntries(manifest);
29
- for (const [_objectName, objectMeta] of sortedObjectEntries) {
31
+ for (const { interfaceName, obj: objectMeta } of declarations.declarations) {
30
32
  const fields = objectMeta.fields || {};
31
33
  const propertyLines = [];
32
34
  propertyLines.push(" id?: string;");
@@ -37,7 +39,7 @@ async function generateObjectTypeDeclarations(manifest, outDir) {
37
39
  const optional = !fieldDef.required ? "?" : "";
38
40
  propertyLines.push(` ${fieldName}${optional}: ${type};`);
39
41
  }
40
- const interfaceDef = `export interface ${objectMeta.className}Data {
42
+ const interfaceDef = `export interface ${interfaceName} {
41
43
  ${propertyLines.join("\n")}
42
44
  }`;
43
45
  interfaces.push(interfaceDef);
@@ -56,7 +58,7 @@ ${interfaces.join("\n\n")}
56
58
  /**
57
59
  * Generate virtual module declarations
58
60
  */
59
- async function generateVirtualModuleDeclarations(manifest, outDir) {
61
+ async function generateVirtualModuleDeclarations(manifest, declarations, outDir) {
60
62
  const manifestDeclaration = `/**
61
63
  * Auto-generated manifest module declaration
62
64
  */
@@ -139,7 +141,8 @@ declare module '@smrt/client' {
139
141
 
140
142
  export interface ApiClient {
141
143
  ${selectApiClientEntries(manifest).map(({ clientKey, dataInterfaceName, crudMethods, customMethods }) => {
142
- const crudType = renderApiClientCrudType(dataInterfaceName, crudMethods, customMethods.map(({ name }) => name));
144
+ const overriddenMethods = customMethods.map(({ name }) => name);
145
+ const crudType = renderApiClientCrudType(`import('./smrt-objects').${dataInterfaceName}`, crudMethods, overriddenMethods);
143
146
  const customSignatures = customMethods.map((method) => ` ${method.name}(${renderApiClientCustomMethodParameters(method, mapFieldTypeToTypeScript)}): Promise<any>;`).join("\n");
144
147
  if (customSignatures) {
145
148
  const intersection = crudType ? `${crudType} & ` : "";
@@ -192,7 +195,7 @@ declare module '@smrt/mcp' {
192
195
  * Auto-generated types module declaration
193
196
  */
194
197
  declare module '@smrt/types' {
195
- ${orderedManifestObjectEntries(manifest).map(([, obj]) => ` export type ${obj.className}Data = import('./smrt-objects').${obj.className}Data;`).join("\n")}
198
+ ${declarations.declarations.map(({ interfaceName }) => ` export type ${interfaceName} = import('./smrt-objects').${interfaceName};`).join("\n")}
196
199
 
197
200
  export interface Request {
198
201
  params: Record<string, string>;
@@ -309,7 +312,7 @@ declare module '@smrt/web' {
309
312
  }
310
313
 
311
314
  export interface SmrtWebCollectionDefinitions {
312
- ${selectWebCollectionEntries(manifest).map(({ collection, obj }) => ` ${JSON.stringify(collection)}: SmrtWebCollectionDefinition<import('./smrt-objects').${obj.className}Data>;`).join("\n")}
315
+ ${selectWebCollectionEntries(manifest).map(({ collection, obj }) => ` ${JSON.stringify(collection)}: SmrtWebCollectionDefinition<import('./smrt-objects').${declarations.interfaceNameFor(obj)}>;`).join("\n")}
313
316
  }
314
317
 
315
318
  export const collectionDefinitions: SmrtWebCollectionDefinitions;
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","names":[],"sources":["../../src/prebuild/index.ts"],"sourcesContent":["/**\n * Pre-build utilities for generating TypeScript declarations\n * Solves virtual module resolution by creating physical .d.ts files\n */\n\nimport * as fs from 'node:fs';\nimport * as path from 'node:path';\nimport type { SmartObjectManifest } from '../scanner/types';\nimport {\n renderApiClientCrudType,\n renderApiClientCustomMethodParameters,\n selectApiClientEntries,\n} from '../vite-plugin/api-client-entries.js';\nimport {\n orderedManifestObjectEntries,\n selectWebCollectionEntries,\n} from '../vite-plugin/web-collections.js';\n\nexport interface PrebuildOptions {\n /** Path to manifest file or manifest object */\n manifest: string | SmartObjectManifest;\n /** Output directory for generated types */\n outDir: string;\n /** Include virtual module declarations */\n includeVirtualModules?: boolean;\n /** Include object type definitions */\n includeObjectTypes?: boolean;\n /** Project root path for resolving relative paths */\n projectRoot?: string;\n}\n\n/**\n * Generate TypeScript declaration files from SMRT manifest\n */\nexport async function generateDeclarations(\n options: PrebuildOptions,\n): Promise<void> {\n const {\n manifest: manifestInput,\n outDir,\n includeVirtualModules = true,\n includeObjectTypes = true,\n projectRoot = process.cwd(),\n } = options;\n\n // Load manifest\n const manifest: SmartObjectManifest =\n typeof manifestInput === 'string'\n ? JSON.parse(fs.readFileSync(manifestInput, 'utf-8'))\n : manifestInput;\n\n // Ensure output directory exists\n const fullOutDir = path.isAbsolute(outDir)\n ? outDir\n : path.join(projectRoot, outDir);\n fs.mkdirSync(fullOutDir, { recursive: true });\n\n console.log(`[smrt:prebuild] Generating declarations to ${fullOutDir}`);\n\n if (includeObjectTypes) {\n await generateObjectTypeDeclarations(manifest, fullOutDir);\n }\n\n if (includeVirtualModules) {\n await generateVirtualModuleDeclarations(manifest, fullOutDir);\n }\n\n console.log(\n `[smrt:prebuild] Generated declarations for ${Object.keys(manifest.objects).length} SMRT objects`,\n );\n}\n\n/**\n * Generate TypeScript interfaces for SMRT objects\n */\nasync function generateObjectTypeDeclarations(\n manifest: SmartObjectManifest,\n outDir: string,\n): Promise<void> {\n const interfaces: string[] = [];\n\n // Generate interfaces for each discovered SMRT object. Manifest key order\n // follows scan/discovery order, which is not stable across runs (#2749),\n // so sort deterministically before emitting.\n const sortedObjectEntries = orderedManifestObjectEntries(manifest);\n for (const [_objectName, objectMeta] of sortedObjectEntries) {\n const fields = objectMeta.fields || {};\n const propertyLines: string[] = [];\n\n // Add standard SmrtObject properties\n propertyLines.push(' id?: string;');\n propertyLines.push(' created_at?: string;');\n propertyLines.push(' updated_at?: string;');\n\n // Add object-specific properties\n for (const [fieldName, fieldDef] of Object.entries(fields)) {\n const type = mapFieldTypeToTypeScript(fieldDef.type);\n const optional = !fieldDef.required ? '?' : '';\n propertyLines.push(` ${fieldName}${optional}: ${type};`);\n }\n\n const interfaceDef = `export interface ${objectMeta.className}Data {\n${propertyLines.join('\\n')}\n}`;\n interfaces.push(interfaceDef);\n }\n\n // Write object types file\n const objectTypesContent = `/**\n * Auto-generated TypeScript interfaces for SMRT objects\n * Generated at build time from @smrt() decorated classes\n *\n * DO NOT EDIT THIS FILE MANUALLY\n */\n\n${interfaces.join('\\n\\n')}\n`;\n\n fs.writeFileSync(path.join(outDir, 'smrt-objects.d.ts'), objectTypesContent);\n}\n\n/**\n * Generate virtual module declarations\n */\nasync function generateVirtualModuleDeclarations(\n manifest: SmartObjectManifest,\n outDir: string,\n): Promise<void> {\n // Generate manifest module declaration\n const manifestDeclaration = `/**\n * Auto-generated manifest module declaration\n */\ndeclare module '@smrt/manifest' {\n export interface SmrtObjectField {\n type: string;\n required?: boolean;\n default?: any;\n }\n\n export interface SmrtObjectMethod {\n name: string;\n parameters: Array<{\n name: string;\n type: string;\n optional?: boolean;\n default?: any;\n }>;\n returnType: string;\n async: boolean;\n isStatic: boolean;\n isPublic: boolean;\n }\n\n export interface SmrtObjectDefinition {\n name: string;\n className: string;\n collection: string;\n filePath: string;\n fields: Record<string, SmrtObjectField>;\n methods: Record<string, SmrtObjectMethod>;\n decoratorConfig: any;\n extends?: string;\n }\n\n export interface SmrtManifest {\n version: string;\n timestamp: number;\n objects: Record<string, SmrtObjectDefinition>;\n }\n\n export const manifest: SmrtManifest;\n export default manifest;\n}`;\n\n // Generate client module declaration\n const apiClientInterface = selectApiClientEntries(manifest)\n .map(({ clientKey, dataInterfaceName, crudMethods, customMethods }) => {\n const overriddenMethods = customMethods.map(({ name }) => name);\n const crudType = renderApiClientCrudType(\n dataInterfaceName,\n crudMethods,\n overriddenMethods,\n );\n const customSignatures = customMethods\n .map(\n (method) =>\n ` ${method.name}(${renderApiClientCustomMethodParameters(\n method,\n mapFieldTypeToTypeScript,\n )}): Promise<any>;`,\n )\n .join('\\n');\n\n if (customSignatures) {\n const intersection = crudType ? `${crudType} & ` : '';\n return ` ${JSON.stringify(clientKey)}: ${intersection}{\\n${customSignatures}\\n };`;\n }\n\n return ` ${JSON.stringify(clientKey)}: ${crudType ?? 'Record<string, never>'};`;\n })\n .join('\\n');\n\n // Wire-shape policy (#1797): the server returns BARE JSON — a bare array for\n // list/search, a bare object for get/create/update — with snake_case field\n // names (created_at, updated_at). These physical declarations share client\n // entry selection and method-signature rendering with the Vite declaration.\n // Fetchers reject on non-2xx with a SmrtClientError (#1796).\n const clientDeclaration = `/**\n * Auto-generated API client module declaration\n */\ndeclare module '@smrt/client' {\n /** Structured custom-action failure nested under an API error body's \\`error\\` key. */\n export interface SmrtClientFailure {\n ok: false;\n code: string;\n message: string;\n status?: number;\n }\n\n /** Shape of a JSON error body carried by a rejected request (SmrtClientError.body). */\n export interface ApiError {\n error?: string | SmrtClientFailure;\n message?: string;\n }\n\n /** Typed error thrown by every fetcher on a non-2xx response (#1796). */\n export interface SmrtClientError extends Error {\n name: 'SmrtClientError';\n status: number;\n /** Machine-readable code from a structured custom-action failure, when present. */\n code?: string;\n body?: ApiError | string;\n }\n\n export interface CrudOperations<T = any> {\n list(params?: Record<string, any>): Promise<T[]>;\n get(id: string): Promise<T>;\n create(data: Partial<T>): Promise<T>;\n update(id: string, data: Partial<T>): Promise<T>;\n delete(id: string): Promise<boolean>;\n search(query: string): Promise<T[]>;\n }\n\n export interface ApiClient {\n${apiClientInterface}\n }\n\n export function createClient(basePath?: string): ApiClient;\n export default createClient;\n}`;\n\n // Generate routes module declaration\n const routesDeclaration = `/**\n * Auto-generated routes module declaration\n */\ndeclare module '@smrt/routes' {\n export interface RouteApp {\n get(path: string, handler: (req: any, res: any) => void): void;\n post(path: string, handler: (req: any, res: any) => void): void;\n put(path: string, handler: (req: any, res: any) => void): void;\n delete(path: string, handler: (req: any, res: any) => void): void;\n }\n\n export function setupRoutes(app: RouteApp): void;\n export default setupRoutes;\n}`;\n\n // Generate MCP module declaration\n const mcpDeclaration = `/**\n * Auto-generated MCP module declaration\n */\ndeclare module '@smrt/mcp' {\n export interface McpTool {\n name: string;\n description: string;\n inputSchema: {\n type: string;\n properties: Record<string, any>;\n required?: string[];\n };\n }\n\n export const tools: McpTool[];\n export function createMCPServer(): {\n name: string;\n version: string;\n tools: McpTool[]\n };\n export default createMCPServer;\n}`;\n\n // Generate types module declaration with object imports. Sorted for the\n // same reason as generateObjectTypeDeclarations (#2749).\n const objectImports = orderedManifestObjectEntries(manifest)\n .map(\n ([, obj]) =>\n ` export type ${obj.className}Data = import('./smrt-objects').${obj.className}Data;`,\n )\n .join('\\n');\n\n const typesDeclaration = `/**\n * Auto-generated types module declaration\n */\ndeclare module '@smrt/types' {\n${objectImports}\n\n export interface Request {\n params: Record<string, string>;\n query: Record<string, any>;\n json(): Promise<any>;\n }\n\n export interface Response {\n json(data: any, init?: { status?: number }): Response;\n status(code: number): Response;\n }\n}`;\n\n // Generate web collection-definition module declaration (#1761). Selection\n // shares selectWebCollectionEntries with the vite-plugin runtime module and\n // its virt-web d.ts, so this `tsc`-only consumer declaration cannot drift\n // from the emitted values.\n const webCollectionEntries = selectWebCollectionEntries(manifest)\n .map(\n ({ collection, obj }) =>\n ` ${JSON.stringify(collection)}: SmrtWebCollectionDefinition<import('./smrt-objects').${obj.className}Data>;`,\n )\n .join('\\n');\n\n const webDeclaration = `/**\n * Auto-generated web collection-definition module declaration (#1761)\n */\ndeclare module '@smrt/web' {\n export type SmrtWebFieldType =\n | 'text'\n | 'decimal'\n | 'boolean'\n | 'integer'\n | 'datetime'\n | 'json'\n | 'foreignKey'\n | 'crossPackageRef';\n\n /** Static \\`@field({ ui })\\` hints (#2046) — the field-policy rail seed. */\n export interface SmrtWebFieldUIHints {\n basic?: boolean;\n group?: string;\n order?: number;\n locked?: boolean;\n widget?: 'textarea' | 'currency' | 'email' | 'url' | 'phone';\n format?: 'money' | 'date' | 'datetime' | 'percent';\n }\n\n export interface SmrtWebFieldDefinition {\n type: SmrtWebFieldType;\n required?: boolean;\n nullable?: boolean;\n default?: unknown;\n /** Developer-authored \\`@field({ description })\\` (#2046) — end-user help seed. */\n description?: string;\n /** Static \\`@field({ ui })\\` hints (#2046). */\n ui?: SmrtWebFieldUIHints;\n }\n\n export type SmrtWebRelationshipKind =\n | 'foreignKey'\n | 'crossPackageRef'\n | 'oneToMany'\n | 'manyToMany';\n\n /**\n * A manifest-derived edge to a sibling REST collection. Mutating this\n * collection invalidates the caches of the collections named by these edges\n * (#1761 relationship-derived invalidation).\n */\n export interface SmrtWebRelationship {\n field: string;\n kind: SmrtWebRelationshipKind;\n relatedCollection: string;\n role?: 'items';\n }\n\n export interface WebToolRouteDescriptor {\n method: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';\n scope: 'item' | 'collection';\n path: string[];\n parameterAliases?: Record<string, string>;\n optionsBag?: boolean;\n }\n\n /** A WebMCP/MCP tool descriptor for one collection action (#1812). */\n export interface WebToolDescriptor {\n action: string;\n name: string;\n description: string;\n inputSchema: Record<string, unknown>;\n readOnly: boolean;\n effect: 'read' | 'write' | 'destructive';\n idempotent: boolean;\n openWorld: boolean;\n route?: WebToolRouteDescriptor;\n }\n\n /** Canonical data-only definition for one generated browser tool. */\n export interface WebMcpToolDefinition extends WebToolDescriptor {\n collection: string;\n objectRef: string;\n className: string;\n endpoint: string;\n idField: string;\n idType: 'uuid' | 'text';\n route: WebToolRouteDescriptor;\n relationships: SmrtWebRelationship[];\n }\n\n export interface SmrtWebCollectionDefinition<TData = Record<string, unknown>> {\n name: string;\n /** Canonical qualified model identity (\\`@package/name:ClassName\\`) for policy APIs. */\n objectRef: string;\n className: string;\n endpoint: string;\n idField: string;\n actions: string[];\n fields: Record<string, SmrtWebFieldDefinition>;\n /** Manifest-derived relationship edges to sibling REST collections. */\n relationships: SmrtWebRelationship[];\n /** WebMCP/MCP tool descriptors for the exposed actions (#1812). */\n toolDescriptors: WebToolDescriptor[];\n /** Phantom row-type carrier for inference — never present at runtime. */\n _row?: TData;\n }\n\n export interface SmrtWebCollectionDefinitions {\n${webCollectionEntries}\n }\n\n export const collectionDefinitions: SmrtWebCollectionDefinitions;\n /** Every API-backed WebMCP tool, independent of list materialization. */\n export const webMcpToolDefinitions: readonly WebMcpToolDefinition[];\n export function getCollectionDefinition<\n K extends keyof SmrtWebCollectionDefinitions,\n >(name: K): SmrtWebCollectionDefinitions[K];\n /**\n * Build-time web-collection shape digest (#1764). A deterministic,\n * replica-stable hash of the emitted collection definitions; a change means\n * old persisted client rows may mis-hydrate. Consumers fold it into the\n * durable persistence namespace and the version-awareness updateAvailable\n * contract signal.\n */\n export const manifestHash: string;\n export default collectionDefinitions;\n}`;\n\n // Write all virtual module declarations\n fs.writeFileSync(\n path.join(outDir, 'smrt-manifest.d.ts'),\n manifestDeclaration,\n );\n fs.writeFileSync(path.join(outDir, 'smrt-client.d.ts'), clientDeclaration);\n fs.writeFileSync(path.join(outDir, 'smrt-routes.d.ts'), routesDeclaration);\n fs.writeFileSync(path.join(outDir, 'smrt-mcp.d.ts'), mcpDeclaration);\n fs.writeFileSync(path.join(outDir, 'smrt-types.d.ts'), typesDeclaration);\n fs.writeFileSync(path.join(outDir, 'smrt-web.d.ts'), webDeclaration);\n}\n\n/**\n * Map SMRT field types to TypeScript types\n */\nfunction mapFieldTypeToTypeScript(smrtType: string): string {\n switch (smrtType) {\n case 'text':\n return 'string';\n case 'decimal':\n case 'integer':\n return 'number';\n case 'boolean':\n return 'boolean';\n case 'datetime':\n return 'string | Date';\n case 'json':\n return 'any';\n case 'foreignKey':\n return 'string';\n default:\n return 'any';\n }\n}\n\n/**\n * CLI command for generating declarations\n */\nexport async function generateDeclarationsFromCLI(\n args: string[],\n): Promise<void> {\n const manifestPath = args[0];\n const outDir = args[1] || 'src/types/generated';\n\n if (!manifestPath) {\n console.error('Usage: generate-declarations <manifest-path> [output-dir]');\n process.exit(1);\n }\n\n if (!fs.existsSync(manifestPath)) {\n console.error(`Manifest file not found: ${manifestPath}`);\n process.exit(1);\n }\n\n await generateDeclarations({\n manifest: manifestPath,\n outDir,\n });\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"index.js","names":[],"sources":["../../src/prebuild/index.ts"],"sourcesContent":["/**\n * Pre-build utilities for generating TypeScript declarations\n * Solves virtual module resolution by creating physical .d.ts files\n */\n\nimport * as fs from 'node:fs';\nimport * as path from 'node:path';\nimport {\n createObjectDeclarationIndex,\n type ObjectDeclarationIndex,\n warnObjectDeclarationCollisions,\n} from '../manifest/object-declarations.js';\nimport type { SmartObjectManifest } from '../scanner/types';\nimport {\n renderApiClientCrudType,\n renderApiClientCustomMethodParameters,\n selectApiClientEntries,\n} from '../vite-plugin/api-client-entries.js';\nimport { selectWebCollectionEntries } from '../vite-plugin/web-collections.js';\n\nexport interface PrebuildOptions {\n /** Path to manifest file or manifest object */\n manifest: string | SmartObjectManifest;\n /** Output directory for generated types */\n outDir: string;\n /** Include virtual module declarations */\n includeVirtualModules?: boolean;\n /** Include object type definitions */\n includeObjectTypes?: boolean;\n /** Project root path for resolving relative paths */\n projectRoot?: string;\n}\n\n/**\n * Generate TypeScript declaration files from SMRT manifest\n */\nexport async function generateDeclarations(\n options: PrebuildOptions,\n): Promise<void> {\n const {\n manifest: manifestInput,\n outDir,\n includeVirtualModules = true,\n includeObjectTypes = true,\n projectRoot = process.cwd(),\n } = options;\n\n // Load manifest\n const manifest: SmartObjectManifest =\n typeof manifestInput === 'string'\n ? JSON.parse(fs.readFileSync(manifestInput, 'utf-8'))\n : manifestInput;\n\n // Ensure output directory exists\n const fullOutDir = path.isAbsolute(outDir)\n ? outDir\n : path.join(projectRoot, outDir);\n fs.mkdirSync(fullOutDir, { recursive: true });\n\n console.log(`[smrt:prebuild] Generating declarations to ${fullOutDir}`);\n\n // One declaration selection (and one set of names) for every file, so a\n // reference in smrt-client/smrt-web always names a declared interface.\n const declarations = createObjectDeclarationIndex(manifest);\n warnObjectDeclarationCollisions(declarations, console.warn);\n\n if (includeObjectTypes) {\n await generateObjectTypeDeclarations(declarations, fullOutDir);\n }\n\n if (includeVirtualModules) {\n await generateVirtualModuleDeclarations(manifest, declarations, fullOutDir);\n }\n\n console.log(\n `[smrt:prebuild] Generated declarations for ${Object.keys(manifest.objects).length} SMRT objects`,\n );\n}\n\n/**\n * Generate TypeScript interfaces for SMRT objects\n */\nasync function generateObjectTypeDeclarations(\n declarations: ObjectDeclarationIndex,\n outDir: string,\n): Promise<void> {\n const interfaces: string[] = [];\n\n // Generate one interface per discovered SMRT class. Manifest key order\n // follows scan/discovery order, which is not stable across runs (#2749),\n // and an aggregated manifest can list one class under several packages\n // (#3296); the shared selection sorts deterministically and collapses those\n // listings to one declaration.\n for (const { interfaceName, obj: objectMeta } of declarations.declarations) {\n const fields = objectMeta.fields || {};\n const propertyLines: string[] = [];\n\n // Add standard SmrtObject properties\n propertyLines.push(' id?: string;');\n propertyLines.push(' created_at?: string;');\n propertyLines.push(' updated_at?: string;');\n\n // Add object-specific properties\n for (const [fieldName, fieldDef] of Object.entries(fields)) {\n const type = mapFieldTypeToTypeScript(fieldDef.type);\n const optional = !fieldDef.required ? '?' : '';\n propertyLines.push(` ${fieldName}${optional}: ${type};`);\n }\n\n const interfaceDef = `export interface ${interfaceName} {\n${propertyLines.join('\\n')}\n}`;\n interfaces.push(interfaceDef);\n }\n\n // Write object types file\n const objectTypesContent = `/**\n * Auto-generated TypeScript interfaces for SMRT objects\n * Generated at build time from @smrt() decorated classes\n *\n * DO NOT EDIT THIS FILE MANUALLY\n */\n\n${interfaces.join('\\n\\n')}\n`;\n\n fs.writeFileSync(path.join(outDir, 'smrt-objects.d.ts'), objectTypesContent);\n}\n\n/**\n * Generate virtual module declarations\n */\nasync function generateVirtualModuleDeclarations(\n manifest: SmartObjectManifest,\n declarations: ObjectDeclarationIndex,\n outDir: string,\n): Promise<void> {\n // Generate manifest module declaration\n const manifestDeclaration = `/**\n * Auto-generated manifest module declaration\n */\ndeclare module '@smrt/manifest' {\n export interface SmrtObjectField {\n type: string;\n required?: boolean;\n default?: any;\n }\n\n export interface SmrtObjectMethod {\n name: string;\n parameters: Array<{\n name: string;\n type: string;\n optional?: boolean;\n default?: any;\n }>;\n returnType: string;\n async: boolean;\n isStatic: boolean;\n isPublic: boolean;\n }\n\n export interface SmrtObjectDefinition {\n name: string;\n className: string;\n collection: string;\n filePath: string;\n fields: Record<string, SmrtObjectField>;\n methods: Record<string, SmrtObjectMethod>;\n decoratorConfig: any;\n extends?: string;\n }\n\n export interface SmrtManifest {\n version: string;\n timestamp: number;\n objects: Record<string, SmrtObjectDefinition>;\n }\n\n export const manifest: SmrtManifest;\n export default manifest;\n}`;\n\n // Generate client module declaration\n const apiClientInterface = selectApiClientEntries(manifest)\n .map(({ clientKey, dataInterfaceName, crudMethods, customMethods }) => {\n const overriddenMethods = customMethods.map(({ name }) => name);\n // The row interfaces live in smrt-objects.d.ts; an ambient module must\n // import them, a bare name does not resolve here.\n const crudType = renderApiClientCrudType(\n `import('./smrt-objects').${dataInterfaceName}`,\n crudMethods,\n overriddenMethods,\n );\n const customSignatures = customMethods\n .map(\n (method) =>\n ` ${method.name}(${renderApiClientCustomMethodParameters(\n method,\n mapFieldTypeToTypeScript,\n )}): Promise<any>;`,\n )\n .join('\\n');\n\n if (customSignatures) {\n const intersection = crudType ? `${crudType} & ` : '';\n return ` ${JSON.stringify(clientKey)}: ${intersection}{\\n${customSignatures}\\n };`;\n }\n\n return ` ${JSON.stringify(clientKey)}: ${crudType ?? 'Record<string, never>'};`;\n })\n .join('\\n');\n\n // Wire-shape policy (#1797): the server returns BARE JSON — a bare array for\n // list/search, a bare object for get/create/update — with snake_case field\n // names (created_at, updated_at). These physical declarations share client\n // entry selection and method-signature rendering with the Vite declaration.\n // Fetchers reject on non-2xx with a SmrtClientError (#1796).\n const clientDeclaration = `/**\n * Auto-generated API client module declaration\n */\ndeclare module '@smrt/client' {\n /** Structured custom-action failure nested under an API error body's \\`error\\` key. */\n export interface SmrtClientFailure {\n ok: false;\n code: string;\n message: string;\n status?: number;\n }\n\n /** Shape of a JSON error body carried by a rejected request (SmrtClientError.body). */\n export interface ApiError {\n error?: string | SmrtClientFailure;\n message?: string;\n }\n\n /** Typed error thrown by every fetcher on a non-2xx response (#1796). */\n export interface SmrtClientError extends Error {\n name: 'SmrtClientError';\n status: number;\n /** Machine-readable code from a structured custom-action failure, when present. */\n code?: string;\n body?: ApiError | string;\n }\n\n export interface CrudOperations<T = any> {\n list(params?: Record<string, any>): Promise<T[]>;\n get(id: string): Promise<T>;\n create(data: Partial<T>): Promise<T>;\n update(id: string, data: Partial<T>): Promise<T>;\n delete(id: string): Promise<boolean>;\n search(query: string): Promise<T[]>;\n }\n\n export interface ApiClient {\n${apiClientInterface}\n }\n\n export function createClient(basePath?: string): ApiClient;\n export default createClient;\n}`;\n\n // Generate routes module declaration\n const routesDeclaration = `/**\n * Auto-generated routes module declaration\n */\ndeclare module '@smrt/routes' {\n export interface RouteApp {\n get(path: string, handler: (req: any, res: any) => void): void;\n post(path: string, handler: (req: any, res: any) => void): void;\n put(path: string, handler: (req: any, res: any) => void): void;\n delete(path: string, handler: (req: any, res: any) => void): void;\n }\n\n export function setupRoutes(app: RouteApp): void;\n export default setupRoutes;\n}`;\n\n // Generate MCP module declaration\n const mcpDeclaration = `/**\n * Auto-generated MCP module declaration\n */\ndeclare module '@smrt/mcp' {\n export interface McpTool {\n name: string;\n description: string;\n inputSchema: {\n type: string;\n properties: Record<string, any>;\n required?: string[];\n };\n }\n\n export const tools: McpTool[];\n export function createMCPServer(): {\n name: string;\n version: string;\n tools: McpTool[]\n };\n export default createMCPServer;\n}`;\n\n // Generate types module declaration with object imports. Same selection as\n // generateObjectTypeDeclarations (#2749, #3296): one alias per class.\n const objectImports = declarations.declarations\n .map(\n ({ interfaceName }) =>\n ` export type ${interfaceName} = import('./smrt-objects').${interfaceName};`,\n )\n .join('\\n');\n\n const typesDeclaration = `/**\n * Auto-generated types module declaration\n */\ndeclare module '@smrt/types' {\n${objectImports}\n\n export interface Request {\n params: Record<string, string>;\n query: Record<string, any>;\n json(): Promise<any>;\n }\n\n export interface Response {\n json(data: any, init?: { status?: number }): Response;\n status(code: number): Response;\n }\n}`;\n\n // Generate web collection-definition module declaration (#1761). Selection\n // shares selectWebCollectionEntries with the vite-plugin runtime module and\n // its virt-web d.ts, so this `tsc`-only consumer declaration cannot drift\n // from the emitted values.\n const webCollectionEntries = selectWebCollectionEntries(manifest)\n .map(\n ({ collection, obj }) =>\n ` ${JSON.stringify(collection)}: SmrtWebCollectionDefinition<import('./smrt-objects').${declarations.interfaceNameFor(obj)}>;`,\n )\n .join('\\n');\n\n const webDeclaration = `/**\n * Auto-generated web collection-definition module declaration (#1761)\n */\ndeclare module '@smrt/web' {\n export type SmrtWebFieldType =\n | 'text'\n | 'decimal'\n | 'boolean'\n | 'integer'\n | 'datetime'\n | 'json'\n | 'foreignKey'\n | 'crossPackageRef';\n\n /** Static \\`@field({ ui })\\` hints (#2046) — the field-policy rail seed. */\n export interface SmrtWebFieldUIHints {\n basic?: boolean;\n group?: string;\n order?: number;\n locked?: boolean;\n widget?: 'textarea' | 'currency' | 'email' | 'url' | 'phone';\n format?: 'money' | 'date' | 'datetime' | 'percent';\n }\n\n export interface SmrtWebFieldDefinition {\n type: SmrtWebFieldType;\n required?: boolean;\n nullable?: boolean;\n default?: unknown;\n /** Developer-authored \\`@field({ description })\\` (#2046) — end-user help seed. */\n description?: string;\n /** Static \\`@field({ ui })\\` hints (#2046). */\n ui?: SmrtWebFieldUIHints;\n }\n\n export type SmrtWebRelationshipKind =\n | 'foreignKey'\n | 'crossPackageRef'\n | 'oneToMany'\n | 'manyToMany';\n\n /**\n * A manifest-derived edge to a sibling REST collection. Mutating this\n * collection invalidates the caches of the collections named by these edges\n * (#1761 relationship-derived invalidation).\n */\n export interface SmrtWebRelationship {\n field: string;\n kind: SmrtWebRelationshipKind;\n relatedCollection: string;\n role?: 'items';\n }\n\n export interface WebToolRouteDescriptor {\n method: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';\n scope: 'item' | 'collection';\n path: string[];\n parameterAliases?: Record<string, string>;\n optionsBag?: boolean;\n }\n\n /** A WebMCP/MCP tool descriptor for one collection action (#1812). */\n export interface WebToolDescriptor {\n action: string;\n name: string;\n description: string;\n inputSchema: Record<string, unknown>;\n readOnly: boolean;\n effect: 'read' | 'write' | 'destructive';\n idempotent: boolean;\n openWorld: boolean;\n route?: WebToolRouteDescriptor;\n }\n\n /** Canonical data-only definition for one generated browser tool. */\n export interface WebMcpToolDefinition extends WebToolDescriptor {\n collection: string;\n objectRef: string;\n className: string;\n endpoint: string;\n idField: string;\n idType: 'uuid' | 'text';\n route: WebToolRouteDescriptor;\n relationships: SmrtWebRelationship[];\n }\n\n export interface SmrtWebCollectionDefinition<TData = Record<string, unknown>> {\n name: string;\n /** Canonical qualified model identity (\\`@package/name:ClassName\\`) for policy APIs. */\n objectRef: string;\n className: string;\n endpoint: string;\n idField: string;\n actions: string[];\n fields: Record<string, SmrtWebFieldDefinition>;\n /** Manifest-derived relationship edges to sibling REST collections. */\n relationships: SmrtWebRelationship[];\n /** WebMCP/MCP tool descriptors for the exposed actions (#1812). */\n toolDescriptors: WebToolDescriptor[];\n /** Phantom row-type carrier for inference — never present at runtime. */\n _row?: TData;\n }\n\n export interface SmrtWebCollectionDefinitions {\n${webCollectionEntries}\n }\n\n export const collectionDefinitions: SmrtWebCollectionDefinitions;\n /** Every API-backed WebMCP tool, independent of list materialization. */\n export const webMcpToolDefinitions: readonly WebMcpToolDefinition[];\n export function getCollectionDefinition<\n K extends keyof SmrtWebCollectionDefinitions,\n >(name: K): SmrtWebCollectionDefinitions[K];\n /**\n * Build-time web-collection shape digest (#1764). A deterministic,\n * replica-stable hash of the emitted collection definitions; a change means\n * old persisted client rows may mis-hydrate. Consumers fold it into the\n * durable persistence namespace and the version-awareness updateAvailable\n * contract signal.\n */\n export const manifestHash: string;\n export default collectionDefinitions;\n}`;\n\n // Write all virtual module declarations\n fs.writeFileSync(\n path.join(outDir, 'smrt-manifest.d.ts'),\n manifestDeclaration,\n );\n fs.writeFileSync(path.join(outDir, 'smrt-client.d.ts'), clientDeclaration);\n fs.writeFileSync(path.join(outDir, 'smrt-routes.d.ts'), routesDeclaration);\n fs.writeFileSync(path.join(outDir, 'smrt-mcp.d.ts'), mcpDeclaration);\n fs.writeFileSync(path.join(outDir, 'smrt-types.d.ts'), typesDeclaration);\n fs.writeFileSync(path.join(outDir, 'smrt-web.d.ts'), webDeclaration);\n}\n\n/**\n * Map SMRT field types to TypeScript types\n */\nfunction mapFieldTypeToTypeScript(smrtType: string): string {\n switch (smrtType) {\n case 'text':\n return 'string';\n case 'decimal':\n case 'integer':\n return 'number';\n case 'boolean':\n return 'boolean';\n case 'datetime':\n return 'string | Date';\n case 'json':\n return 'any';\n case 'foreignKey':\n return 'string';\n default:\n return 'any';\n }\n}\n\n/**\n * CLI command for generating declarations\n */\nexport async function generateDeclarationsFromCLI(\n args: string[],\n): Promise<void> {\n const manifestPath = args[0];\n const outDir = args[1] || 'src/types/generated';\n\n if (!manifestPath) {\n console.error('Usage: generate-declarations <manifest-path> [output-dir]');\n process.exit(1);\n }\n\n if (!fs.existsSync(manifestPath)) {\n console.error(`Manifest file not found: ${manifestPath}`);\n process.exit(1);\n }\n\n await generateDeclarations({\n manifest: manifestPath,\n outDir,\n });\n}\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"class-registration.d.ts","sourceRoot":"","sources":["../../src/registry/class-registration.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAwBH,OAAO,EAAE,UAAU,EAAE,MAAM,cAAW,CAAC;AACvC,OAAO,KAAK,EAKV,qBAAqB,EAGtB,MAAM,qBAAqB,CAAC;AAsD7B,OAAO,KAAK,EACV,eAAe,EACf,eAAe,EACf,iBAAiB,EAElB,MAAM,YAAY,CAAC;AAiGpB;;;;GAIG;AACH,wBAAgB,mBAAmB,CAAC,UAAU,EAAE,MAAM,GAAG,SAAS,GAAG,OAAO,CAS3E;AA4BD;;;;;;;GAOG;AACH,wBAAgB,gBAAgB,CAC9B,IAAI,EAAE,MAAM,GAAG,SAAS,EACxB,KAAK,EAAE,MAAM,GAAG,SAAS,GACxB,OAAO,CAeT;AAstBD;;;;GAIG;AACH,wBAAgB,QAAQ,CACtB,IAAI,EAAE,OAAO,UAAU,EACvB,MAAM,GAAE,iBAAsB,GAC7B,IAAI,CASN;AAioCD,wBAAgB,kBAAkB,CAChC,UAAU,EAAE,MAAM,EAMlB,qBAAqB,EAAE,WAAW,CAAC,eAAe,CAAC,uBAAuB,CAAC,CAAC,GAC3E,IAAI,CAeN;AA+BD,wBAAgB,uBAAuB,CACrC,MAAM,EAAE,GAAG,CAAC,MAAM,EAAE,eAAe,CAAC,EACpC,kBAAkB,EAAE,eAAe,CAAC,oBAAoB,CAAC,GAAG,SAAS,GACpE,IAAI,CAyCN;AAoHD;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAgB,4BAA4B,CAC1C,QAAQ,EAAE,eAAe,EACzB,gBAAgB,CAAC,EAAE;IACjB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB,GACA,IAAI,CA4CN;AAmMD,wBAAgB,oBAAoB,CAClC,IAAI,EAAE,MAAM,EACZ,SAAS,EAAE,qBAAqB,EAChC,mBAAmB,CAAC,EAAE,MAAM,GAC3B,IAAI,CAUN"}
1
+ {"version":3,"file":"class-registration.d.ts","sourceRoot":"","sources":["../../src/registry/class-registration.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAwBH,OAAO,EAAE,UAAU,EAAE,MAAM,cAAW,CAAC;AACvC,OAAO,KAAK,EAKV,qBAAqB,EAGtB,MAAM,qBAAqB,CAAC;AAsD7B,OAAO,KAAK,EACV,eAAe,EACf,eAAe,EACf,iBAAiB,EAElB,MAAM,YAAY,CAAC;AAiGpB;;;;GAIG;AACH,wBAAgB,mBAAmB,CAAC,UAAU,EAAE,MAAM,GAAG,SAAS,GAAG,OAAO,CAS3E;AA4BD;;;;;;;GAOG;AACH,wBAAgB,gBAAgB,CAC9B,IAAI,EAAE,MAAM,GAAG,SAAS,EACxB,KAAK,EAAE,MAAM,GAAG,SAAS,GACxB,OAAO,CAeT;AAstBD;;;;GAIG;AACH,wBAAgB,QAAQ,CACtB,IAAI,EAAE,OAAO,UAAU,EACvB,MAAM,GAAE,iBAAsB,GAC7B,IAAI,CASN;AAmoCD,wBAAgB,kBAAkB,CAChC,UAAU,EAAE,MAAM,EAMlB,qBAAqB,EAAE,WAAW,CAAC,eAAe,CAAC,uBAAuB,CAAC,CAAC,GAC3E,IAAI,CAeN;AA+BD,wBAAgB,uBAAuB,CACrC,MAAM,EAAE,GAAG,CAAC,MAAM,EAAE,eAAe,CAAC,EACpC,kBAAkB,EAAE,eAAe,CAAC,oBAAoB,CAAC,GAAG,SAAS,GACpE,IAAI,CAyCN;AAuID;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAgB,4BAA4B,CAC1C,QAAQ,EAAE,eAAe,EACzB,gBAAgB,CAAC,EAAE;IACjB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB,GACA,IAAI,CA4CN;AA4MD,wBAAgB,oBAAoB,CAClC,IAAI,EAAE,MAAM,EACZ,SAAS,EAAE,qBAAqB,EAChC,mBAAmB,CAAC,EAAE,MAAM,GAC3B,IAAI,CAUN"}
@@ -790,7 +790,7 @@ function registerUntracked(ctor, config = {}) {
790
790
  } else {
791
791
  const newMeta = decoratorOptions;
792
792
  const newField = {
793
- type: opts.type || "text",
793
+ type: opts.type || inferDecoratorOnlyFieldType(opts),
794
794
  _meta: newMeta
795
795
  };
796
796
  if (opts.transient !== void 0) newField.transient = opts.transient;
@@ -806,7 +806,7 @@ function registerUntracked(ctor, config = {}) {
806
806
  delete newField._meta?.related;
807
807
  }
808
808
  fields.set(fieldName, newField);
809
- verboseLog(`[registry] ✅ Added field ${fieldName} from decorator: type=${opts.type || "text"}`);
809
+ verboseLog(`[registry] ✅ Added field ${fieldName} from decorator: type=${newField.type}`);
810
810
  }
811
811
  }
812
812
  }
@@ -919,6 +919,8 @@ function registerUntracked(ctor, config = {}) {
919
919
  sourceFilePath,
920
920
  extends: extendsClass,
921
921
  extendsTypeArg: manifestEntry?.extendsTypeArg,
922
+ inheritedExposure: manifestEntry?.inheritedExposure,
923
+ collectionOf: manifestEntry?.collectionOf,
922
924
  tenantScopedConfig,
923
925
  tenantScopedConfigSource,
924
926
  visibility
@@ -1040,6 +1042,22 @@ function mergeIndexDefinitions(existingIndexes, manifestIndexes) {
1040
1042
  }
1041
1043
  return merged;
1042
1044
  }
1045
+ /**
1046
+ * Column type for a decorator-only field (no manifest entry) that declares no
1047
+ * explicit `type` (#4295).
1048
+ *
1049
+ * The runtime sees only the decorator options, never the property initializer
1050
+ * or annotation the static scanner reads, so the decorator `default` is the
1051
+ * only type evidence both paths share. A boolean default is unambiguous and
1052
+ * the scanner reads it the same way (`ManifestAdapter.inferFieldType`), so it
1053
+ * maps to `boolean`. A number is deliberately NOT inferred: `0` and `0.0` are
1054
+ * the same JavaScript value, so the source-level integer/decimal rule cannot
1055
+ * be applied here and guessing INTEGER would mis-type a decimal column.
1056
+ * Runtime-only numeric fields must declare `type: 'integer' | 'decimal'`.
1057
+ */
1058
+ function inferDecoratorOnlyFieldType(opts) {
1059
+ return typeof opts.default === "boolean" ? "boolean" : "text";
1060
+ }
1043
1061
  function getReferenceKindFromFieldOptions(fieldOptions) {
1044
1062
  if (fieldOptions?.__tenancy?.isTenantIdField || fieldOptions?._meta?.__tenancy?.isTenantIdField) return "tenantId";
1045
1063
  if (fieldOptions?.type === "foreignKey") return "foreignKey";
@@ -1203,6 +1221,8 @@ function mergeManifestIntoExistingRegistration(existing, objectDef, packageName)
1203
1221
  if (!(!isQualifiedName(qualifiedParent) && !!existing.extends && isQualifiedName(existing.extends) && parseQualifiedName(existing.extends).className === qualifiedParent)) existing.extends = qualifiedParent;
1204
1222
  }
1205
1223
  if (!existing.sourceFilePath && objectDef.filePath) existing.sourceFilePath = objectDef.filePath;
1224
+ if (objectDef.inheritedExposure) existing.inheritedExposure = objectDef.inheritedExposure;
1225
+ if (objectDef.collectionOf) existing.collectionOf = objectDef.collectionOf;
1206
1226
  if (objectDef.collection) existing.collection = existing.config.collection ?? objectDef.collection;
1207
1227
  existing.visibility = objectDef.visibility || manifestConfig.visibility || existing.visibility;
1208
1228
  invalidateInheritanceEntries(existing);
@@ -1334,6 +1354,8 @@ function registerFromManifestUntracked(name, objectDef, manifestPackageName) {
1334
1354
  replacedQualifiedNames: [...replacedQualifiedNames],
1335
1355
  extends: qualifiedExtends,
1336
1356
  extendsTypeArg: objectDef.extendsTypeArg,
1357
+ inheritedExposure: objectDef.inheritedExposure,
1358
+ collectionOf: objectDef.collectionOf,
1337
1359
  visibility
1338
1360
  };
1339
1361
  assertQualifiedNameAliasesAvailable(registrationKey, registration);