@mistriwork/engine 0.0.0-stage → 1.0.0-rc.10

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 (546) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +17 -2
  3. package/dist/adapters/adapter.d.ts +693 -0
  4. package/dist/adapters/adapter.js +444 -0
  5. package/dist/adapters/cache/cacheStore.d.ts +75 -0
  6. package/dist/adapters/cache/cacheStore.js +160 -0
  7. package/dist/adapters/cache/resp.d.ts +52 -0
  8. package/dist/adapters/cache/resp.js +237 -0
  9. package/dist/adapters/commands/canonicalJson.d.ts +19 -0
  10. package/dist/adapters/commands/canonicalJson.js +154 -0
  11. package/dist/adapters/connect.d.ts +29 -0
  12. package/dist/adapters/connect.js +40 -0
  13. package/dist/adapters/connectionBudget.d.ts +101 -0
  14. package/dist/adapters/connectionBudget.js +128 -0
  15. package/dist/adapters/dbLatency.d.ts +35 -0
  16. package/dist/adapters/dbLatency.js +79 -0
  17. package/dist/adapters/epochCache.d.ts +50 -0
  18. package/dist/adapters/epochCache.js +85 -0
  19. package/dist/adapters/http/egress.d.ts +48 -0
  20. package/dist/adapters/http/egress.js +175 -0
  21. package/dist/adapters/http/httpAdapter.d.ts +71 -0
  22. package/dist/adapters/http/httpAdapter.js +512 -0
  23. package/dist/adapters/http/httpDescription.d.ts +32 -0
  24. package/dist/adapters/http/httpDescription.js +246 -0
  25. package/dist/adapters/http/openapi.d.ts +98 -0
  26. package/dist/adapters/http/openapi.js +487 -0
  27. package/dist/adapters/layoutRefusal.d.ts +24 -0
  28. package/dist/adapters/layoutRefusal.js +35 -0
  29. package/dist/adapters/net/networkPolicy.d.ts +44 -0
  30. package/dist/adapters/net/networkPolicy.js +123 -0
  31. package/dist/adapters/net/untrustedDatabase.d.ts +18 -0
  32. package/dist/adapters/net/untrustedDatabase.js +120 -0
  33. package/dist/adapters/poolAdmission.d.ts +43 -0
  34. package/dist/adapters/poolAdmission.js +184 -0
  35. package/dist/adapters/poolMetrics.d.ts +121 -0
  36. package/dist/adapters/poolMetrics.js +185 -0
  37. package/dist/adapters/postgres.d.ts +436 -0
  38. package/dist/adapters/postgres.js +1890 -0
  39. package/dist/adapters/postgresCatalog.d.ts +96 -0
  40. package/dist/adapters/postgresCatalog.js +387 -0
  41. package/dist/adapters/postgresCatalogDigest.d.ts +30 -0
  42. package/dist/adapters/postgresCatalogDigest.js +87 -0
  43. package/dist/adapters/postgresCatalogRows.d.ts +62 -0
  44. package/dist/adapters/postgresCatalogRows.js +37 -0
  45. package/dist/adapters/postgresDates.d.ts +1 -0
  46. package/dist/adapters/postgresDates.js +39 -0
  47. package/dist/adapters/postgresMaintenance.d.ts +33 -0
  48. package/dist/adapters/postgresMaintenance.js +179 -0
  49. package/dist/adapters/postgresPool.d.ts +255 -0
  50. package/dist/adapters/postgresPool.js +838 -0
  51. package/dist/adapters/postgresPooler.d.ts +28 -0
  52. package/dist/adapters/postgresPooler.js +267 -0
  53. package/dist/adapters/postgresReadCatalog.d.ts +31 -0
  54. package/dist/adapters/postgresReadCatalog.js +226 -0
  55. package/dist/adapters/postgresReadSnapshot.d.ts +46 -0
  56. package/dist/adapters/postgresReadSnapshot.js +133 -0
  57. package/dist/adapters/postgresSchemaEpoch.d.ts +24 -0
  58. package/dist/adapters/postgresSchemaEpoch.js +149 -0
  59. package/dist/adapters/postgresTransactionHandle.d.ts +173 -0
  60. package/dist/adapters/postgresTransactionHandle.js +524 -0
  61. package/dist/adapters/postgresValues.d.ts +30 -0
  62. package/dist/adapters/postgresValues.js +75 -0
  63. package/dist/adapters/replicas.d.ts +70 -0
  64. package/dist/adapters/replicas.js +208 -0
  65. package/dist/adapters/reservedSql.d.ts +38 -0
  66. package/dist/adapters/reservedSql.js +264 -0
  67. package/dist/adapters/rolePrivileges.d.ts +17 -0
  68. package/dist/adapters/rolePrivileges.js +22 -0
  69. package/dist/adapters/scoped/keyRows.d.ts +65 -0
  70. package/dist/adapters/scoped/keyRows.js +173 -0
  71. package/dist/adapters/scoped/postgresScopeCatalog.d.ts +16 -0
  72. package/dist/adapters/scoped/postgresScopeCatalog.js +117 -0
  73. package/dist/adapters/scoped/scopedCommandOperations.d.ts +5 -0
  74. package/dist/adapters/scoped/scopedCommandOperations.js +48 -0
  75. package/dist/adapters/scoped/scopedReads.d.ts +20 -0
  76. package/dist/adapters/scoped/scopedReads.js +174 -0
  77. package/dist/adapters/scoped/scopedTransaction.d.ts +89 -0
  78. package/dist/adapters/scoped/scopedTransaction.js +303 -0
  79. package/dist/adapters/scoped/sqliteScopeCatalog.d.ts +16 -0
  80. package/dist/adapters/scoped/sqliteScopeCatalog.js +241 -0
  81. package/dist/adapters/scoped/updateGuard.d.ts +3 -0
  82. package/dist/adapters/scoped/updateGuard.js +40 -0
  83. package/dist/adapters/serverSettings.d.ts +45 -0
  84. package/dist/adapters/serverSettings.js +46 -0
  85. package/dist/adapters/sqlite.d.ts +262 -0
  86. package/dist/adapters/sqlite.js +1434 -0
  87. package/dist/adapters/sqliteCostlyReads.d.ts +33 -0
  88. package/dist/adapters/sqliteCostlyReads.js +49 -0
  89. package/dist/adapters/sqliteDecimals.d.ts +22 -0
  90. package/dist/adapters/sqliteDecimals.js +57 -0
  91. package/dist/adapters/sqliteHandles.d.ts +43 -0
  92. package/dist/adapters/sqliteHandles.js +143 -0
  93. package/dist/adapters/sqliteStatementProcess.d.ts +29 -0
  94. package/dist/adapters/sqliteStatementProcess.js +131 -0
  95. package/dist/adapters/statementControl.d.ts +34 -0
  96. package/dist/adapters/statementControl.js +90 -0
  97. package/dist/adapters/storeHost.d.ts +130 -0
  98. package/dist/adapters/storeHost.js +13 -0
  99. package/dist/adapters/telemetry/drivers.d.ts +21 -0
  100. package/dist/adapters/telemetry/drivers.js +321 -0
  101. package/dist/adapters/telemetry/metrics.d.ts +63 -0
  102. package/dist/adapters/telemetry/metrics.js +122 -0
  103. package/dist/adapters/telemetry/otlp.d.ts +12 -0
  104. package/dist/adapters/telemetry/otlp.js +320 -0
  105. package/dist/adapters/telemetry/otlpEncode.d.ts +48 -0
  106. package/dist/adapters/telemetry/otlpEncode.js +366 -0
  107. package/dist/adapters/telemetry/otlpEnv.d.ts +1 -0
  108. package/dist/adapters/telemetry/otlpEnv.js +12 -0
  109. package/dist/adapters/telemetry/serverTiming.d.ts +83 -0
  110. package/dist/adapters/telemetry/serverTiming.js +503 -0
  111. package/dist/adapters/telemetry/start.d.ts +10 -0
  112. package/dist/adapters/telemetry/start.js +25 -0
  113. package/dist/adapters/telemetry/statement.d.ts +16 -0
  114. package/dist/adapters/telemetry/statement.js +134 -0
  115. package/dist/adapters/telemetry/trace.d.ts +127 -0
  116. package/dist/adapters/telemetry/trace.js +295 -0
  117. package/dist/adapters/writeAhead.d.ts +27 -0
  118. package/dist/adapters/writeAhead.js +46 -0
  119. package/dist/adapters/writeAttribution.d.ts +18 -0
  120. package/dist/adapters/writeAttribution.js +23 -0
  121. package/dist/adapters/writeCapture.d.ts +73 -0
  122. package/dist/adapters/writeCapture.js +20 -0
  123. package/dist/adapters/writeLanes.d.ts +94 -0
  124. package/dist/adapters/writeLanes.js +232 -0
  125. package/dist/engine.d.ts +15 -0
  126. package/dist/engine.js +13 -0
  127. package/dist/server/auth/credentials.d.ts +61 -0
  128. package/dist/server/auth/credentials.js +260 -0
  129. package/dist/server/cacheService.d.ts +50 -0
  130. package/dist/server/cacheService.js +110 -0
  131. package/dist/server/cli/args.d.ts +176 -0
  132. package/dist/server/cli/args.js +342 -0
  133. package/dist/server/cli/bin.d.ts +5 -0
  134. package/dist/server/cli/bin.js +66 -0
  135. package/dist/server/cli/browser.d.ts +15 -0
  136. package/dist/server/cli/browser.js +37 -0
  137. package/dist/server/cli/config.d.ts +50 -0
  138. package/dist/server/cli/config.js +206 -0
  139. package/dist/server/cli/startup.d.ts +36 -0
  140. package/dist/server/cli/startup.js +128 -0
  141. package/dist/server/cli/startupOptions.d.ts +63 -0
  142. package/dist/server/cli/startupOptions.js +143 -0
  143. package/dist/server/cli/subcommand.d.ts +28 -0
  144. package/dist/server/cli/subcommand.js +32 -0
  145. package/dist/server/cli/usage.d.ts +13 -0
  146. package/dist/server/cli/usage.js +31 -0
  147. package/dist/server/clientHttp.d.ts +184 -0
  148. package/dist/server/clientHttp.js +700 -0
  149. package/dist/server/clientSurface.d.ts +128 -0
  150. package/dist/server/clientSurface.js +182 -0
  151. package/dist/server/columnGrants.d.ts +103 -0
  152. package/dist/server/columnGrants.js +730 -0
  153. package/dist/server/compileCache.d.ts +1 -0
  154. package/dist/server/compileCache.js +30 -0
  155. package/dist/server/config/locations.d.ts +1 -0
  156. package/dist/server/config/locations.js +15 -0
  157. package/dist/server/configPaths.d.ts +7 -0
  158. package/dist/server/configPaths.js +34 -0
  159. package/dist/server/configSections.d.ts +64 -0
  160. package/dist/server/configSections.js +72 -0
  161. package/dist/server/connectionRedaction.d.ts +30 -0
  162. package/dist/server/connectionRedaction.js +77 -0
  163. package/dist/server/connections.d.ts +353 -0
  164. package/dist/server/connections.js +978 -0
  165. package/dist/server/constraints.d.ts +82 -0
  166. package/dist/server/constraints.js +169 -0
  167. package/dist/server/demo.d.ts +9 -0
  168. package/dist/server/demo.js +554 -0
  169. package/dist/server/devclient.d.ts +108 -0
  170. package/dist/server/devclient.js +361 -0
  171. package/dist/server/evidence.d.ts +55 -0
  172. package/dist/server/evidence.js +323 -0
  173. package/dist/server/hiddenColumns.d.ts +32 -0
  174. package/dist/server/hiddenColumns.js +238 -0
  175. package/dist/server/http/baselineHeaders.d.ts +24 -0
  176. package/dist/server/http/baselineHeaders.js +12 -0
  177. package/dist/server/http/captured.d.ts +35 -0
  178. package/dist/server/http/captured.js +73 -0
  179. package/dist/server/http/clock.d.ts +1 -0
  180. package/dist/server/http/clock.js +14 -0
  181. package/dist/server/http/connectionSource.d.ts +4 -0
  182. package/dist/server/http/connectionSource.js +48 -0
  183. package/dist/server/http/context.d.ts +189 -0
  184. package/dist/server/http/context.js +11 -0
  185. package/dist/server/http/crawlerText.d.ts +15 -0
  186. package/dist/server/http/crawlerText.js +32 -0
  187. package/dist/server/http/dataRoutes.d.ts +88 -0
  188. package/dist/server/http/dataRoutes.js +137 -0
  189. package/dist/server/http/door.d.ts +57 -0
  190. package/dist/server/http/door.js +39 -0
  191. package/dist/server/http/exportRoute.d.ts +15 -0
  192. package/dist/server/http/exportRoute.js +320 -0
  193. package/dist/server/http/firstPaint.d.ts +108 -0
  194. package/dist/server/http/firstPaint.js +262 -0
  195. package/dist/server/http/httpOperationRoutes.d.ts +11 -0
  196. package/dist/server/http/httpOperationRoutes.js +50 -0
  197. package/dist/server/http/pageReads.d.ts +20 -0
  198. package/dist/server/http/pageReads.js +205 -0
  199. package/dist/server/http/pageRoutes.d.ts +39 -0
  200. package/dist/server/http/pageRoutes.js +291 -0
  201. package/dist/server/http/probes.d.ts +22 -0
  202. package/dist/server/http/probes.js +27 -0
  203. package/dist/server/http/rateLimit.d.ts +131 -0
  204. package/dist/server/http/rateLimit.js +351 -0
  205. package/dist/server/http/readRoutes.d.ts +9 -0
  206. package/dist/server/http/readRoutes.js +131 -0
  207. package/dist/server/http/readRouting.d.ts +20 -0
  208. package/dist/server/http/readRouting.js +80 -0
  209. package/dist/server/http/recordCompose.d.ts +67 -0
  210. package/dist/server/http/recordCompose.js +167 -0
  211. package/dist/server/http/recordWrites.d.ts +89 -0
  212. package/dist/server/http/recordWrites.js +298 -0
  213. package/dist/server/http/refusalReport.d.ts +38 -0
  214. package/dist/server/http/refusalReport.js +63 -0
  215. package/dist/server/http/reply.d.ts +130 -0
  216. package/dist/server/http/reply.js +394 -0
  217. package/dist/server/http/requestOrigin.d.ts +11 -0
  218. package/dist/server/http/requestOrigin.js +8 -0
  219. package/dist/server/http/router.d.ts +45 -0
  220. package/dist/server/http/router.js +22 -0
  221. package/dist/server/http/serverCore.d.ts +68 -0
  222. package/dist/server/http/serverCore.js +177 -0
  223. package/dist/server/http/traceRequest.d.ts +30 -0
  224. package/dist/server/http/traceRequest.js +224 -0
  225. package/dist/server/http/trust.d.ts +261 -0
  226. package/dist/server/http/trust.js +472 -0
  227. package/dist/server/http/writeRefusal.d.ts +33 -0
  228. package/dist/server/http/writeRefusal.js +24 -0
  229. package/dist/server/httpConnection.d.ts +49 -0
  230. package/dist/server/httpConnection.js +170 -0
  231. package/dist/server/impact.d.ts +55 -0
  232. package/dist/server/impact.js +47 -0
  233. package/dist/server/journal.d.ts +134 -0
  234. package/dist/server/journal.js +398 -0
  235. package/dist/server/keysetReadHttp.d.ts +52 -0
  236. package/dist/server/keysetReadHttp.js +85 -0
  237. package/dist/server/layouts.d.ts +49 -0
  238. package/dist/server/layouts.js +192 -0
  239. package/dist/server/migrate/cli.d.ts +55 -0
  240. package/dist/server/migrate/cli.js +517 -0
  241. package/dist/server/migrate/database.d.ts +37 -0
  242. package/dist/server/migrate/database.js +247 -0
  243. package/dist/server/migrate/desired.d.ts +76 -0
  244. package/dist/server/migrate/desired.js +292 -0
  245. package/dist/server/migrate/files.d.ts +34 -0
  246. package/dist/server/migrate/files.js +141 -0
  247. package/dist/server/migrate/flyway.d.ts +7 -0
  248. package/dist/server/migrate/flyway.js +69 -0
  249. package/dist/server/migrate/history.d.ts +70 -0
  250. package/dist/server/migrate/history.js +211 -0
  251. package/dist/server/migrate/runner.d.ts +100 -0
  252. package/dist/server/migrate/runner.js +409 -0
  253. package/dist/server/migrate/snapshot.d.ts +30 -0
  254. package/dist/server/migrate/snapshot.js +239 -0
  255. package/dist/server/migrate/target.d.ts +33 -0
  256. package/dist/server/migrate/target.js +82 -0
  257. package/dist/server/openapiDraft.d.ts +25 -0
  258. package/dist/server/openapiDraft.js +171 -0
  259. package/dist/server/operations/diagnostics.d.ts +101 -0
  260. package/dist/server/operations/diagnostics.js +284 -0
  261. package/dist/server/operations/requestMetrics.d.ts +33 -0
  262. package/dist/server/operations/requestMetrics.js +94 -0
  263. package/dist/server/packageVersion.d.ts +1 -0
  264. package/dist/server/packageVersion.js +11 -0
  265. package/dist/server/poolConfig.d.ts +34 -0
  266. package/dist/server/poolConfig.js +114 -0
  267. package/dist/server/product.d.ts +20 -0
  268. package/dist/server/product.js +12 -0
  269. package/dist/server/recordHistoryOperations.d.ts +39 -0
  270. package/dist/server/recordHistoryOperations.js +43 -0
  271. package/dist/server/recordWriteOperations.d.ts +111 -0
  272. package/dist/server/recordWriteOperations.js +329 -0
  273. package/dist/server/resourceReadHttp.d.ts +81 -0
  274. package/dist/server/resourceReadHttp.js +606 -0
  275. package/dist/server/resourceReadOperations.d.ts +147 -0
  276. package/dist/server/resourceReadOperations.js +452 -0
  277. package/dist/server/scaffoldMachine.d.ts +54 -0
  278. package/dist/server/scaffoldMachine.js +184 -0
  279. package/dist/server/seed/cli.d.ts +20 -0
  280. package/dist/server/seed/cli.js +164 -0
  281. package/dist/server/seed/lexicon.d.ts +26 -0
  282. package/dist/server/seed/lexicon.js +585 -0
  283. package/dist/server/seed/seed.d.ts +70 -0
  284. package/dist/server/seed/seed.js +779 -0
  285. package/dist/server/seed/words.d.ts +21 -0
  286. package/dist/server/seed/words.js +164 -0
  287. package/dist/server/serverRequests.d.ts +6 -0
  288. package/dist/server/serverRequests.js +33 -0
  289. package/dist/server/serverTimeouts.d.ts +33 -0
  290. package/dist/server/serverTimeouts.js +32 -0
  291. package/dist/server/shapeonly.d.ts +32 -0
  292. package/dist/server/shapeonly.js +110 -0
  293. package/dist/server/startupWarnings.d.ts +32 -0
  294. package/dist/server/startupWarnings.js +92 -0
  295. package/dist/shared/addressParams.d.ts +28 -0
  296. package/dist/shared/addressParams.js +47 -0
  297. package/dist/shared/appCards.d.ts +123 -0
  298. package/dist/shared/appCards.js +385 -0
  299. package/dist/shared/appComponents.d.ts +177 -0
  300. package/dist/shared/appComponents.js +422 -0
  301. package/dist/shared/appDocuments.d.ts +43 -0
  302. package/dist/shared/appDocuments.js +131 -0
  303. package/dist/shared/appEmbeds.d.ts +21 -0
  304. package/dist/shared/appEmbeds.js +24 -0
  305. package/dist/shared/appIcons.d.ts +28 -0
  306. package/dist/shared/appIcons.js +223 -0
  307. package/dist/shared/appLinks.d.ts +17 -0
  308. package/dist/shared/appLinks.js +124 -0
  309. package/dist/shared/appListColumns.d.ts +3 -0
  310. package/dist/shared/appListColumns.js +66 -0
  311. package/dist/shared/appLocale.d.ts +36 -0
  312. package/dist/shared/appLocale.js +86 -0
  313. package/dist/shared/appMessages.d.ts +44 -0
  314. package/dist/shared/appMessages.js +78 -0
  315. package/dist/shared/appNoun.d.ts +21 -0
  316. package/dist/shared/appNoun.js +32 -0
  317. package/dist/shared/autoBucket.d.ts +98 -0
  318. package/dist/shared/autoBucket.js +305 -0
  319. package/dist/shared/autoValues.d.ts +25 -0
  320. package/dist/shared/autoValues.js +25 -0
  321. package/dist/shared/breakdown.d.ts +228 -0
  322. package/dist/shared/breakdown.js +557 -0
  323. package/dist/shared/breakdowntext.d.ts +41 -0
  324. package/dist/shared/breakdowntext.js +400 -0
  325. package/dist/shared/builtInThemes.d.ts +2 -0
  326. package/dist/shared/builtInThemes.js +771 -0
  327. package/dist/shared/chartSeparation.d.ts +11 -0
  328. package/dist/shared/chartSeparation.js +58 -0
  329. package/dist/shared/clauses.d.ts +22 -0
  330. package/dist/shared/clauses.js +97 -0
  331. package/dist/shared/columnCaption.d.ts +16 -0
  332. package/dist/shared/columnCaption.js +90 -0
  333. package/dist/shared/commandFieldPath.d.ts +2 -0
  334. package/dist/shared/commandFieldPath.js +7 -0
  335. package/dist/shared/commandWire.d.ts +26 -0
  336. package/dist/shared/commandWire.js +41 -0
  337. package/dist/shared/computed.d.ts +161 -0
  338. package/dist/shared/computed.js +378 -0
  339. package/dist/shared/computedParse.d.ts +13 -0
  340. package/dist/shared/computedParse.js +278 -0
  341. package/dist/shared/computedValue.d.ts +47 -0
  342. package/dist/shared/computedValue.js +191 -0
  343. package/dist/shared/connection.d.ts +30 -0
  344. package/dist/shared/connection.js +1 -0
  345. package/dist/shared/csv.d.ts +20 -0
  346. package/dist/shared/csv.js +63 -0
  347. package/dist/shared/dashLayout.d.ts +30 -0
  348. package/dist/shared/dashLayout.js +132 -0
  349. package/dist/shared/dashboardAsks.d.ts +93 -0
  350. package/dist/shared/dashboardAsks.js +129 -0
  351. package/dist/shared/dashboardTrend.d.ts +20 -0
  352. package/dist/shared/dashboardTrend.js +50 -0
  353. package/dist/shared/declaredReferences.d.ts +50 -0
  354. package/dist/shared/declaredReferences.js +202 -0
  355. package/dist/shared/definedFields.d.ts +63 -0
  356. package/dist/shared/definedFields.js +103 -0
  357. package/dist/shared/definedValues.d.ts +63 -0
  358. package/dist/shared/definedValues.js +184 -0
  359. package/dist/shared/displayDefaults.d.ts +135 -0
  360. package/dist/shared/displayDefaults.js +529 -0
  361. package/dist/shared/dueDate.d.ts +6 -0
  362. package/dist/shared/dueDate.js +6 -0
  363. package/dist/shared/duration.d.ts +4 -0
  364. package/dist/shared/duration.js +23 -0
  365. package/dist/shared/fileBounds.d.ts +9 -0
  366. package/dist/shared/fileBounds.js +9 -0
  367. package/dist/shared/filter.d.ts +217 -0
  368. package/dist/shared/filter.js +305 -0
  369. package/dist/shared/firstPaint.d.ts +24 -0
  370. package/dist/shared/firstPaint.js +33 -0
  371. package/dist/shared/form.d.ts +21 -0
  372. package/dist/shared/form.js +25 -0
  373. package/dist/shared/formValues.d.ts +104 -0
  374. package/dist/shared/formValues.js +352 -0
  375. package/dist/shared/funnel.d.ts +45 -0
  376. package/dist/shared/funnel.js +108 -0
  377. package/dist/shared/httpOperations.d.ts +48 -0
  378. package/dist/shared/httpOperations.js +9 -0
  379. package/dist/shared/iconGlyphs.d.ts +4 -0
  380. package/dist/shared/iconGlyphs.js +151 -0
  381. package/dist/shared/iconWeights.d.ts +3 -0
  382. package/dist/shared/iconWeights.js +162 -0
  383. package/dist/shared/imageWidths.d.ts +32 -0
  384. package/dist/shared/imageWidths.js +38 -0
  385. package/dist/shared/keysetPage.d.ts +64 -0
  386. package/dist/shared/keysetPage.js +161 -0
  387. package/dist/shared/like.d.ts +20 -0
  388. package/dist/shared/like.js +29 -0
  389. package/dist/shared/listEmpty.d.ts +12 -0
  390. package/dist/shared/listEmpty.js +36 -0
  391. package/dist/shared/listFilters.d.ts +28 -0
  392. package/dist/shared/listFilters.js +84 -0
  393. package/dist/shared/listPresentations.d.ts +199 -0
  394. package/dist/shared/listPresentations.js +7 -0
  395. package/dist/shared/listQuestion.d.ts +52 -0
  396. package/dist/shared/listQuestion.js +137 -0
  397. package/dist/shared/lookPalettes.d.ts +62 -0
  398. package/dist/shared/lookPalettes.js +306 -0
  399. package/dist/shared/maskQuoted.d.ts +24 -0
  400. package/dist/shared/maskQuoted.js +48 -0
  401. package/dist/shared/mentionLabel.d.ts +19 -0
  402. package/dist/shared/mentionLabel.js +38 -0
  403. package/dist/shared/mentionLookup.d.ts +17 -0
  404. package/dist/shared/mentionLookup.js +64 -0
  405. package/dist/shared/menuCounts.d.ts +16 -0
  406. package/dist/shared/menuCounts.js +74 -0
  407. package/dist/shared/model.d.ts +79 -0
  408. package/dist/shared/model.js +153 -0
  409. package/dist/shared/money.d.ts +44 -0
  410. package/dist/shared/money.js +81 -0
  411. package/dist/shared/moneyEntry.d.ts +7 -0
  412. package/dist/shared/moneyEntry.js +27 -0
  413. package/dist/shared/mount.d.ts +118 -0
  414. package/dist/shared/mount.js +248 -0
  415. package/dist/shared/niceTicks.d.ts +26 -0
  416. package/dist/shared/niceTicks.js +50 -0
  417. package/dist/shared/order.d.ts +59 -0
  418. package/dist/shared/order.js +133 -0
  419. package/dist/shared/page.d.ts +135 -0
  420. package/dist/shared/page.js +1187 -0
  421. package/dist/shared/pageAnatomy.d.ts +99 -0
  422. package/dist/shared/pageAnatomy.js +370 -0
  423. package/dist/shared/pageConfig.d.ts +25 -0
  424. package/dist/shared/pageConfig.js +1308 -0
  425. package/dist/shared/pageEmptyActions.d.ts +12 -0
  426. package/dist/shared/pageEmptyActions.js +20 -0
  427. package/dist/shared/pageLayouts.d.ts +72 -0
  428. package/dist/shared/pageLayouts.js +205 -0
  429. package/dist/shared/pageModel.d.ts +785 -0
  430. package/dist/shared/pageModel.js +121 -0
  431. package/dist/shared/pagePaths.d.ts +180 -0
  432. package/dist/shared/pagePaths.js +869 -0
  433. package/dist/shared/pagePlans.d.ts +52 -0
  434. package/dist/shared/pagePlans.js +108 -0
  435. package/dist/shared/pageRoutes.d.ts +9 -0
  436. package/dist/shared/pageRoutes.js +9 -0
  437. package/dist/shared/query.d.ts +166 -0
  438. package/dist/shared/query.js +1302 -0
  439. package/dist/shared/queryDates.d.ts +12 -0
  440. package/dist/shared/queryDates.js +101 -0
  441. package/dist/shared/queryOperands.d.ts +99 -0
  442. package/dist/shared/queryOperands.js +131 -0
  443. package/dist/shared/reads.d.ts +103 -0
  444. package/dist/shared/reads.js +154 -0
  445. package/dist/shared/recordLabel.d.ts +51 -0
  446. package/dist/shared/recordLabel.js +123 -0
  447. package/dist/shared/recordReadKeys.d.ts +30 -0
  448. package/dist/shared/recordReadKeys.js +51 -0
  449. package/dist/shared/recordReads.d.ts +268 -0
  450. package/dist/shared/recordReads.js +368 -0
  451. package/dist/shared/recordview.d.ts +198 -0
  452. package/dist/shared/recordview.js +356 -0
  453. package/dist/shared/referenceModel.d.ts +58 -0
  454. package/dist/shared/referenceModel.js +194 -0
  455. package/dist/shared/refusal.d.ts +64 -0
  456. package/dist/shared/refusal.js +144 -0
  457. package/dist/shared/relatedCreate.d.ts +30 -0
  458. package/dist/shared/relatedCreate.js +162 -0
  459. package/dist/shared/resourceHttp.d.ts +464 -0
  460. package/dist/shared/resourceHttp.js +1 -0
  461. package/dist/shared/richText.d.ts +85 -0
  462. package/dist/shared/richText.js +305 -0
  463. package/dist/shared/rollups.d.ts +143 -0
  464. package/dist/shared/rollups.js +64 -0
  465. package/dist/shared/rowScope.d.ts +72 -0
  466. package/dist/shared/rowScope.js +376 -0
  467. package/dist/shared/runtimeTables.d.ts +44 -0
  468. package/dist/shared/runtimeTables.js +79 -0
  469. package/dist/shared/schema.d.ts +523 -0
  470. package/dist/shared/schema.js +627 -0
  471. package/dist/shared/schematext.d.ts +38 -0
  472. package/dist/shared/schematext.js +101 -0
  473. package/dist/shared/scopeCatalog.d.ts +29 -0
  474. package/dist/shared/scopeCatalog.js +468 -0
  475. package/dist/shared/servedApp.d.ts +681 -0
  476. package/dist/shared/servedApp.js +45 -0
  477. package/dist/shared/sessionLease.d.ts +34 -0
  478. package/dist/shared/sessionLease.js +3 -0
  479. package/dist/shared/shape.d.ts +55 -0
  480. package/dist/shared/shape.js +168 -0
  481. package/dist/shared/slug.d.ts +1 -0
  482. package/dist/shared/slug.js +12 -0
  483. package/dist/shared/sortText.d.ts +14 -0
  484. package/dist/shared/sortText.js +26 -0
  485. package/dist/shared/sqlRelation.d.ts +4 -0
  486. package/dist/shared/sqlRelation.js +8 -0
  487. package/dist/shared/sqlsaved.d.ts +44 -0
  488. package/dist/shared/sqlsaved.js +63 -0
  489. package/dist/shared/stitchedPaths.d.ts +27 -0
  490. package/dist/shared/stitchedPaths.js +38 -0
  491. package/dist/shared/structuredFilter.d.ts +22 -0
  492. package/dist/shared/structuredFilter.js +1 -0
  493. package/dist/shared/subtitlePath.d.ts +20 -0
  494. package/dist/shared/subtitlePath.js +24 -0
  495. package/dist/shared/suggest.d.ts +18 -0
  496. package/dist/shared/suggest.js +59 -0
  497. package/dist/shared/tableSizing.d.ts +4 -0
  498. package/dist/shared/tableSizing.js +9 -0
  499. package/dist/shared/tagLists.d.ts +57 -0
  500. package/dist/shared/tagLists.js +34 -0
  501. package/dist/shared/textRules.d.ts +54 -0
  502. package/dist/shared/textRules.js +133 -0
  503. package/dist/shared/theme.d.ts +94 -0
  504. package/dist/shared/theme.js +800 -0
  505. package/dist/shared/themeColor.d.ts +34 -0
  506. package/dist/shared/themeColor.js +109 -0
  507. package/dist/shared/themeFaces.d.ts +30 -0
  508. package/dist/shared/themeFaces.js +42 -0
  509. package/dist/shared/themeInk.d.ts +20 -0
  510. package/dist/shared/themeInk.js +52 -0
  511. package/dist/shared/themeLooks.d.ts +9 -0
  512. package/dist/shared/themeLooks.js +572 -0
  513. package/dist/shared/themeParts.d.ts +26 -0
  514. package/dist/shared/themeParts.js +71 -0
  515. package/dist/shared/themePartsCheck.d.ts +21 -0
  516. package/dist/shared/themePartsCheck.js +91 -0
  517. package/dist/shared/themePresets.d.ts +85 -0
  518. package/dist/shared/themePresets.js +139 -0
  519. package/dist/shared/themeRecipeWords.d.ts +156 -0
  520. package/dist/shared/themeRecipeWords.js +156 -0
  521. package/dist/shared/themeRecipes.d.ts +213 -0
  522. package/dist/shared/themeRecipes.js +234 -0
  523. package/dist/shared/themeResolve.d.ts +55 -0
  524. package/dist/shared/themeResolve.js +52 -0
  525. package/dist/shared/themeTokens.d.ts +107 -0
  526. package/dist/shared/themeTokens.js +116 -0
  527. package/dist/shared/themeType.d.ts +38 -0
  528. package/dist/shared/themeType.js +205 -0
  529. package/dist/shared/unitWord.d.ts +4 -0
  530. package/dist/shared/unitWord.js +37 -0
  531. package/dist/shared/view.d.ts +195 -0
  532. package/dist/shared/view.js +987 -0
  533. package/dist/shared/viewLabels.d.ts +47 -0
  534. package/dist/shared/viewLabels.js +459 -0
  535. package/dist/shared/viewPaths.d.ts +248 -0
  536. package/dist/shared/viewPaths.js +294 -0
  537. package/dist/shared/viewtext.d.ts +57 -0
  538. package/dist/shared/viewtext.js +282 -0
  539. package/dist/shared/vocabulary.d.ts +71 -0
  540. package/dist/shared/vocabulary.js +249 -0
  541. package/dist/shared/weekStart.d.ts +7 -0
  542. package/dist/shared/weekStart.js +36 -0
  543. package/mark.svg +1 -0
  544. package/package.json +261 -4
  545. package/schema/model.d.ts +298 -0
  546. package/schema/theme.d.ts +587 -0
@@ -0,0 +1,978 @@
1
+ /**
2
+ * The connection registry.
3
+ *
4
+ * The server used to hold exactly one adapter. It now holds several, opened
5
+ * lazily: naming a connection costs nothing until you look at it, which
6
+ * matters when a config file lists eight databases and you only wanted one.
7
+ *
8
+ * Two rules shape everything here.
9
+ *
10
+ * A connection string is a credential. It is read from disk or the command
11
+ * line, kept in this module, and never sent to the browser — the client sees
12
+ * an id, a name and a dialect. Anything that reaches the page has been
13
+ * through `publicView`, and there is a test asserting a password does not
14
+ * survive the trip.
15
+ *
16
+ * Introspection is per connection and cached, because it is the expensive
17
+ * part of connecting and the schema does not change while you browse. It is
18
+ * refreshable on request, since sometimes it does.
19
+ */
20
+ import { readFile, writeFile, mkdir } from 'node:fs/promises';
21
+ import { dirname, resolve } from 'node:path';
22
+ import { connect } from '../adapters/connect.js';
23
+ import { UNTRUSTED_NETWORK } from '../adapters/net/untrustedDatabase.js';
24
+ import { configLocations } from './config/locations.js';
25
+ import { redact, scrubSecrets } from './connectionRedaction.js';
26
+ import { hideColumns, parseHiddenColumns, withoutHiddenColumns } from './hiddenColumns.js';
27
+ export { redact, scrubSecrets } from './connectionRedaction.js';
28
+ import { shapeOnly } from './shapeonly.js';
29
+ import { resolveConnection, expandEnv, keychainAvailable, keychainSet, writeCredential } from './auth/credentials.js';
30
+ import { httpConnectionOf, openHttp } from './httpConnection.js';
31
+ import { checkServices, isServiceKind, isUrlless, keptUnread, parseConfigSections, serviceConnectionOf, serviceEntry, } from './configSections.js';
32
+ import { CACHE_SECTIONS, openCache } from './cacheService.js';
33
+ import { MemoryCacheStore, scopedCache } from '../adapters/cache/cacheStore.js';
34
+ import { listenOf, poolOf, replicaEngine, replicasOf } from './poolConfig.js';
35
+ import { slug } from '../shared/slug.js';
36
+ import { sqliteTargetFrom } from './configPaths.js';
37
+ import { product } from './product.js';
38
+ export { sqliteTargetFrom } from './configPaths.js';
39
+ export class Registry {
40
+ /** The environment this registry's references resolve in: an App's own in a suite, else the process's (undefined). */
41
+ environment;
42
+ /** A suite captures each App's references once; standalone registries retain
43
+ * the existing process-environment lookup when a connection opens. */
44
+ constructor(options = {}) {
45
+ this.environment = options.environment === undefined ? undefined : Object.freeze({ ...options.environment });
46
+ }
47
+ entries = new Map();
48
+ activeId = null;
49
+ invalidateAdapter(entry) {
50
+ entry.adapterEpoch = {};
51
+ entry.adapterLifetime = undefined;
52
+ entry.closingAdapters = (entry.closingAdapters ?? 0) + 1;
53
+ return () => {
54
+ entry.closingAdapters -= 1;
55
+ };
56
+ }
57
+ /** Private serving-owner fence, not an authorization proof or a lease.
58
+ * Refuses synchronously once closure starts, even while drain/close awaits. */
59
+ captureAdapterLifetime(id, expectedAdapter) {
60
+ const entry = this.entries.get(id);
61
+ const lifetime = entry?.adapterLifetime;
62
+ const current = () => {
63
+ if (!entry ||
64
+ !lifetime ||
65
+ this.entries.get(id) !== entry ||
66
+ entry.closingAdapters ||
67
+ entry.adapterLifetime !== lifetime ||
68
+ entry.adapter !== expectedAdapter ||
69
+ !entry.schema) {
70
+ throw new Error('The connection adapter is no longer current.');
71
+ }
72
+ };
73
+ current();
74
+ return current;
75
+ }
76
+ /** Told of each adapter a connection dials, before anything can write through it. */
77
+ dialedHooks = [];
78
+ /** Record history binds its tables here, so a reconnect records exactly as the adapter it replaced did (recordHistory.ts). */
79
+ onAdapterDialed(hook) {
80
+ this.dialedHooks.push(hook);
81
+ }
82
+ /**
83
+ * The adapter a connection has open now, as dialed: the object the dial
84
+ * hooks were told of and a write capture is keyed by, or undefined while
85
+ * none is open. Nothing is dialed. Trusted server code only (live sync's
86
+ * binding): it is beneath the hidden-column and shape-only wrappers, so it
87
+ * is never handed to a reader.
88
+ */
89
+ dialedAdapter(id) {
90
+ const entry = this.entries.get(id);
91
+ return entry?.adapter && entry.schema && !entry.closingAdapters ? entry.dialed : undefined;
92
+ }
93
+ /** Where a connection's one-time notices go: stderr, where startup speaks. Tests replace it. */
94
+ onWarning = (message) => console.error(`${product()}: ${message}`);
95
+ /** The services the config names beside its databases: not databases, never listed or browsed. The cache is the engine's; the App framework registers the rest (configSections.ts). */
96
+ services = [];
97
+ /** The config sections this registry's services were read under: what saves them back and what makes a kind a singleton. */
98
+ sections = CACHE_SECTIONS;
99
+ cacheStore;
100
+ cacheWarnedAt = 0;
101
+ /* State a module keeps per registry (a plugged-in search provider, a mail transport), made on first use. */
102
+ moduleState = new Map();
103
+ /* A wider role, said once per connection per run. A schema refresh (the
104
+ UI's, or the MCP `refresh` tool) dials again, and each dial used to say
105
+ it again: the same paragraph every time an agent re-read the schema. Keyed by what was said, so a
106
+ role that gained a grant since is still news. */
107
+ rolesSaid = new Set();
108
+ /** A service from the config. Names are unique among services; a kind that is a singleton (the cache, the email service) is configured once. */
109
+ addService(service) {
110
+ if (this.services.some((s) => s.name === service.name))
111
+ throw new Error(`A service named "${service.name}" is already configured.`);
112
+ const singleton = this.sections.kinds[service.kind]?.singleton;
113
+ if (singleton && this.services.some((s) => s.kind === service.kind)) {
114
+ throw new Error(`"${service.name}" would be a second ${singleton}; a deployment has one.`);
115
+ }
116
+ this.services.push(service);
117
+ }
118
+ /** The entries of kinds this binary does not read (`others: 'keep'`), as written, for a save to put back. */
119
+ kept = [];
120
+ keep(entry) {
121
+ this.kept.push(entry);
122
+ }
123
+ keptEntries() {
124
+ return [...this.kept];
125
+ }
126
+ /** The services as written — references intact — for saving the config back. */
127
+ serviceConfigs() {
128
+ return [...this.services];
129
+ }
130
+ /** The services of one kind, as written. */
131
+ servicesOfKind(kind) {
132
+ return this.services.filter((s) => s.kind === kind);
133
+ }
134
+ /** State a module keeps for this registry, made on first use; `resetOnClose` state is forgotten by `closeServices`. */
135
+ serviceState(key, make, options = {}) {
136
+ let held = this.moduleState.get(key);
137
+ if (!held) {
138
+ held = { value: make(), resetOnClose: options.resetOnClose === true };
139
+ this.moduleState.set(key, held);
140
+ }
141
+ return held.value;
142
+ }
143
+ /**
144
+ * The deployment's cache: the Valkey a cache connection names, dialed on
145
+ * first use, or this process's own. Its outages are said at most once a
146
+ * minute; meanwhile every read is a miss.
147
+ */
148
+ cache() {
149
+ if (this.cacheStore)
150
+ return this.cacheStore;
151
+ const configured = this.servicesOfKind('cache')[0];
152
+ this.cacheStore = configured
153
+ ? openCache(configured, (message) => {
154
+ if (Date.now() - this.cacheWarnedAt < 60_000)
155
+ return;
156
+ this.cacheWarnedAt = Date.now();
157
+ this.onWarning(`${message} Reading it as misses until it answers.`);
158
+ })
159
+ : new MemoryCacheStore();
160
+ return this.cacheStore;
161
+ }
162
+ async closeServices() {
163
+ const store = this.cacheStore;
164
+ this.cacheStore = undefined;
165
+ for (const [key, held] of this.moduleState)
166
+ if (held.resetOnClose)
167
+ this.moduleState.delete(key);
168
+ await store?.close();
169
+ }
170
+ add(config, options = {}) {
171
+ const id = config.id ?? slug(config.name, 'connection');
172
+ /* Ids have to be unique because the client addresses connections by them;
173
+ a duplicate name gets a suffix rather than silently replacing. */
174
+ let unique = id;
175
+ let n = 2;
176
+ while (this.entries.has(unique))
177
+ unique = `${id}-${n++}`;
178
+ const full = { ...config, id: unique };
179
+ this.entries.set(unique, {
180
+ config: full,
181
+ untrustedUrl: options.untrusted === true || options.resolved === true,
182
+ sealed: options.untrusted === true,
183
+ busy: 0,
184
+ idle: [],
185
+ });
186
+ if (!this.activeId)
187
+ this.activeId = unique;
188
+ return full;
189
+ }
190
+ /** The file a SQLite connection opens, absolute, where its URL is written out (no `${VAR}`): what a refusal names. */
191
+ sqliteFile(id) {
192
+ const entry = this.entries.get(id);
193
+ if (!entry || entry.config.http || entry.config.url.includes('$'))
194
+ return undefined;
195
+ const url = entry.config.url;
196
+ if (/^[a-z][\w+.-]*:\/\//i.test(url) || url === ':memory:')
197
+ return undefined;
198
+ return resolve(url.replace(/^sqlite:(\/\/)?/i, ''));
199
+ }
200
+ has(id) {
201
+ return this.entries.has(id);
202
+ }
203
+ /** The stored configuration, for the manager to edit and to write back. */
204
+ configOf(id) {
205
+ return this.entries.get(id)?.config;
206
+ }
207
+ /** Every stored configuration, in order, including its secrets as written. */
208
+ configs() {
209
+ return [...this.entries.values()].map((e) => e.config);
210
+ }
211
+ /**
212
+ * Each database connection's URL as opening it would read it: `${VAR}` references resolved through this
213
+ * registry's captured environment (an untrusted URL as written), an API's left out. Read in one pass with no
214
+ * await, so an update between reads cannot retarget a connection already visited.
215
+ */
216
+ openableUrls() {
217
+ const urls = [];
218
+ for (const entry of this.entries.values()) {
219
+ if (entry.config.http)
220
+ continue;
221
+ urls.push(entry.untrustedUrl ? entry.config.url : expandEnv(entry.config.url, this.environment));
222
+ }
223
+ return urls;
224
+ }
225
+ /**
226
+ * Change a connection's name, target or schema list.
227
+ *
228
+ * The open adapter is discarded whenever the target changes, because an
229
+ * entry whose URL says one database while its schema describes another is
230
+ * the worst of both: it looks edited and behaves as it did before. The name
231
+ * alone can change without reconnecting.
232
+ */
233
+ async update(id, patch, options = {}) {
234
+ const entry = this.entries.get(id);
235
+ if (!entry)
236
+ throw new Error(`No connection called "${id}".`);
237
+ const retarget = (patch.url !== undefined && patch.url !== entry.config.url) ||
238
+ (patch.schemas !== undefined && JSON.stringify(patch.schemas) !== JSON.stringify(entry.config.schemas));
239
+ const moved = patch.url !== undefined && patch.url !== entry.config.url;
240
+ entry.config = { ...entry.config, ...patch, id };
241
+ // A new url is the one typed now; the file's spelling of the old one is not saved over it.
242
+ if (moved)
243
+ delete entry.config.writtenUrl;
244
+ if (retarget) {
245
+ /* The writable flag does not follow a URL to a new database.
246
+
247
+ `writable` is the config's statement about *one* database: this file
248
+ may be edited, that server may not. An entry that kept the flag while
249
+ its URL changed carried "may be written" to whatever the new URL
250
+ named — any SQLite file the server user can read, any Postgres it
251
+ can reach — and `/api/sql` with `write: true` would then run there.
252
+ Retargeting makes the entry read-only; a connection that should be
253
+ writable says so in the config, where the decision is reviewable.
254
+ `rows: false` is the opposite kind of flag, a restriction, and it
255
+ stays: a shape-only connection pointed elsewhere is still shape-only.
256
+
257
+ A URL handed over by a client the route does not trust is marked, and
258
+ `dial` resolves it without secrets from then on. */
259
+ entry.config.writable = false;
260
+ if (patch.url !== undefined)
261
+ entry.untrustedUrl = entry.sealed = options.untrusted === true;
262
+ const finish = this.invalidateAdapter(entry);
263
+ try {
264
+ await this.drain(id);
265
+ await entry.adapter?.close().catch(() => { });
266
+ entry.adapter = entry.dialed = undefined;
267
+ entry.schema = undefined;
268
+ entry.error = undefined;
269
+ }
270
+ finally {
271
+ finish();
272
+ }
273
+ }
274
+ return entry.config;
275
+ }
276
+ /**
277
+ * Replace a connection's stored URL with one that names the same database.
278
+ *
279
+ * This is how a password moves out of the URL and into the keychain when
280
+ * the connections are saved: the target has not changed, only how the
281
+ * secret is remembered, so nothing that `update` does for a retarget —
282
+ * closing the adapter, dropping the writable flag, marking the URL as an
283
+ * untrusted client's — applies. The same-target rule is checked, not assumed: with
284
+ * the password blanked on both sides the two URLs must be identical, or
285
+ * this refuses rather than quietly becoming a retarget with the guards off.
286
+ */
287
+ rewriteUrl(id, url) {
288
+ const entry = this.entries.get(id);
289
+ if (!entry)
290
+ throw new Error(`No connection called "${id}".`);
291
+ if (sameTarget(entry.config.url, url) === false) {
292
+ throw new Error(`"${entry.config.name}": the rewritten URL names a different database.`);
293
+ }
294
+ entry.config = { ...entry.config, url };
295
+ delete entry.config.writtenUrl;
296
+ /* The URL is now the operator's, whatever it was before: saving is the
297
+ explicit act of making a session connection permanent, and the secret
298
+ the next open resolves by name is one this server stored. */
299
+ entry.untrustedUrl = entry.sealed = false;
300
+ return entry.config;
301
+ }
302
+ /**
303
+ * Forget a connection.
304
+ *
305
+ * This used to refuse the last one, and the reason it gave was honest: a
306
+ * browser with no database to browse was a page that could not do anything,
307
+ * and the way out of that state was not obvious. Both halves of that have
308
+ * stopped being true — the server starts with nothing now, and the page it
309
+ * serves in that state exists to do exactly one thing, which is get you out
310
+ * of it. A rule that guards against a state the product supports is just a
311
+ * connection you cannot remove.
312
+ *
313
+ * The active connection moves to whatever remains, or to nothing.
314
+ */
315
+ async remove(id) {
316
+ const entry = this.entries.get(id);
317
+ if (!entry)
318
+ throw new Error(`No connection called "${id}".`);
319
+ const finish = this.invalidateAdapter(entry);
320
+ try {
321
+ await this.drain(id);
322
+ await entry.adapter?.close().catch(() => { });
323
+ this.entries.delete(id);
324
+ if (this.activeId === id)
325
+ this.activeId = [...this.entries.keys()][0] ?? null;
326
+ }
327
+ finally {
328
+ finish();
329
+ }
330
+ }
331
+ get active() {
332
+ return this.activeId;
333
+ }
334
+ /**
335
+ * Choose the default connection, or `null` for none.
336
+ *
337
+ * Null is reachable now that the server starts with nothing, and it is what
338
+ * an add has to fall back to when the connection it just made turns out not
339
+ * to open: `add` makes the first one active before anything has tried to
340
+ * dial it, so a failed first add left the whole application pointed at a
341
+ * connection that had never worked.
342
+ */
343
+ setActive(id) {
344
+ if (id !== null && !this.entries.has(id))
345
+ throw new Error(`No connection called "${id}".`);
346
+ this.activeId = id;
347
+ }
348
+ list() {
349
+ return [...this.entries.values()].map((e) => publicView(e));
350
+ }
351
+ /**
352
+ * Open a connection if it is not already open, and return it with its
353
+ * schema. A failure is recorded on the entry rather than thrown away, so
354
+ * the sidebar can show which connection is broken and why.
355
+ */
356
+ async open(id) {
357
+ const entry = this.entries.get(id);
358
+ if (!entry)
359
+ throw new Error(`No connection called "${id}".`);
360
+ if (entry.adapter && entry.schema)
361
+ return { adapter: entry.adapter, schema: entry.schema };
362
+ /* Already dialling: wait for that one rather than starting a second.
363
+ Two tabs selecting the same unopened connection at the same moment used
364
+ to open two real connections and leak whichever finished first. */
365
+ if (entry.opening)
366
+ return entry.opening;
367
+ entry.opening = this.dial(entry);
368
+ try {
369
+ return await entry.opening;
370
+ }
371
+ finally {
372
+ entry.opening = undefined;
373
+ }
374
+ }
375
+ /* Every read runs in a read-only transaction, which holds for everything
376
+ tablewalk sends but not for a superuser: the role is the boundary that
377
+ always holds. Never fatal: a catalog that will not say is not a reason to
378
+ warn, or to keep the connection closed. */
379
+ async warnWiderRole(name, adapter) {
380
+ try {
381
+ const role = await adapter.privileges();
382
+ const wider = [...(role.superuser ? ['superuser'] : []), ...role.writes, ...(role.create ? ['CREATE'] : [])];
383
+ const said = `${name}\u0000${wider.join('\u0000')}`;
384
+ if (wider.length && !this.rolesSaid.has(said)) {
385
+ this.rolesSaid.add(said);
386
+ this.onWarning(`"${name}" is read-only here, but its database role can do more (${wider.join('; ')}). ` +
387
+ 'Reads still run in read-only transactions; connect as a role that can only read and the database enforces it too.');
388
+ }
389
+ }
390
+ catch {
391
+ /* Nothing to say. */
392
+ }
393
+ }
394
+ /* Each replica resolved as the primary's URL was, under the same name, so a
395
+ password kept for the connection is the replica's too — a physical
396
+ replica has the primary's roles. A reference is only known now, so the
397
+ engine is checked again here (replicasOf checked what was literal). */
398
+ async resolveReplicas(entry, primary) {
399
+ const engine = replicaEngine(primary);
400
+ if (!engine)
401
+ throw new Error(`"${entry.config.name}" has "replicas", but SQLite has no replicas. Remove "replicas".`);
402
+ return Promise.all((entry.config.replicas ?? []).map(async (url) => {
403
+ const resolved = await resolveConnection(entry.config.name, url, { secrets: !entry.untrustedUrl, environment: this.environment });
404
+ if (replicaEngine(resolved.url) !== engine)
405
+ throw new Error(`"${entry.config.name}" has a replica that is not a ${engine} URL; a replica is the same engine as its primary.`);
406
+ return resolved.url;
407
+ }));
408
+ }
409
+ async dial(entry) {
410
+ const epoch = entry.adapterEpoch;
411
+ /* Declared out here so the catch can sweep it. It holds the secret in its
412
+ expanded form, which is exactly what a driver quotes back on failure. */
413
+ let resolved;
414
+ let adapter;
415
+ let published = false;
416
+ try {
417
+ /* The stored URL may be a reference rather than a secret: an env var,
418
+ a keychain entry, or a name in the credentials file. It is resolved
419
+ here, at the last possible moment, and the resolved form is handed
420
+ straight to the adapter without being stored anywhere. */
421
+ if (entry.config.http) {
422
+ /* A URL that arrived from an untrusted client opens with no
423
+ credential, as a database URL does: the header is bound to the
424
+ host the operator wrote, never to one a browser named. */
425
+ /* The deployment's cache under this connection's own prefix: an answer
426
+ kept for one API is never read back as another's. openHttp uses it
427
+ only where the connection opted in (`"cache": { "ttl": … }`). */
428
+ const opened = await openHttp(entry.config.name, entry.config.url, entry.config.http, {
429
+ secrets: !entry.untrustedUrl,
430
+ environment: this.environment,
431
+ ...(entry.config.http.cache ? { cache: scopedCache(this.cache(), `tw:http:${entry.config.id}:`) } : {}),
432
+ });
433
+ entry.secret = opened.secret;
434
+ entry.warning = opened.warning;
435
+ adapter = opened.adapter;
436
+ }
437
+ else {
438
+ resolved = await resolveConnection(entry.config.name, entry.config.url, {
439
+ secrets: !entry.untrustedUrl,
440
+ environment: this.environment,
441
+ });
442
+ entry.secret = resolved.source;
443
+ entry.warning = resolved.warning;
444
+ /* A literal URL was checked against its "pool" when the file was
445
+ read; one that was a reference is only known now. Said rather than
446
+ refused: the connection still works, on the default pool. */
447
+ if (entry.config.pool && !/^postgres(ql)?:\/\//i.test(resolved.url)) {
448
+ this.onWarning(`"${entry.config.name}" has "pool", which sizes PostgreSQL connections; it names another engine, so "pool" has no effect.`);
449
+ }
450
+ const replicas = entry.config.replicas?.length ? await this.resolveReplicas(entry, resolved.url) : undefined;
451
+ adapter = connect(resolved.url, {
452
+ ...(replicas ? { replicas } : {}),
453
+ schemas: entry.config.schemas ?? [],
454
+ label: entry.config.name,
455
+ // An address a browser beyond this machine typed reaches the public network only, with nothing filled in.
456
+ ...(entry.sealed ? { network: UNTRUSTED_NETWORK } : {}),
457
+ ...(entry.config.pool
458
+ ? {
459
+ postgres: {
460
+ ...(entry.config.pool.read !== undefined ? { readers: entry.config.pool.read } : {}),
461
+ ...(entry.config.pool.write !== undefined ? { writers: entry.config.pool.write } : {}),
462
+ },
463
+ }
464
+ : {}),
465
+ });
466
+ }
467
+ const read = await adapter.introspect();
468
+ /* The catalog as served: a hidden column is not in it, for any caller. */
469
+ const schema = entry.config.hidden ? withoutHiddenColumns(read, entry.config.hidden, entry.config.name) : read;
470
+ for (const hook of this.dialedHooks)
471
+ hook(entry.config.id, adapter);
472
+ /* Wrapped before it is stored, so nothing can reach the unwrapped one:
473
+ the boundary is a property of the connection, not a rule each caller
474
+ remembers. Introspection happens first because the catalog is the
475
+ part a shape-only connection exists to serve. */
476
+ /* Hidden columns are cut first, so a shape-only connection's catalog
477
+ and counts never name them either. */
478
+ const hidden = entry.config.hidden ? hideColumns(adapter, entry.config.name, entry.config.hidden, read) : adapter;
479
+ entry.adapter = entry.config.rows === false ? shapeOnly(hidden, entry.config.name) : hidden;
480
+ entry.dialed = adapter;
481
+ published = true;
482
+ entry.schema = schema;
483
+ if (entry.adapterEpoch === epoch && !entry.closingAdapters)
484
+ entry.adapterLifetime = {};
485
+ entry.error = undefined;
486
+ /* Read-only here, over a role that could do more: said once, on the
487
+ first open, before anything else uses the handle. */
488
+ if (!entry.config.writable && adapter.privileges)
489
+ await this.warnWiderRole(entry.config.name, adapter);
490
+ /* The stored one, which is the wrapped one where it matters. Returning
491
+ the raw adapter here would hand the first caller a way past the
492
+ boundary the line above just put up. */
493
+ return { adapter: entry.adapter, schema };
494
+ }
495
+ catch (err) {
496
+ // Failed introspection/wrapping never reaches closeAll's ownership map.
497
+ // Once published, the registry owns closure (including wrapped adapters).
498
+ if (adapter && !published) {
499
+ try {
500
+ await adapter.close();
501
+ }
502
+ catch {
503
+ /* Preserve the original, scrubbed acquisition error. */
504
+ }
505
+ }
506
+ /* Scrubbed here, at the one place a failure becomes a stored string.
507
+
508
+ `resolved` is the URL with the secret expanded into it, and it is the
509
+ thing a driver quotes back. It may not exist yet — `resolveConnection`
510
+ can be what failed — so the configured URL is swept as well, since an
511
+ inline password lives in that one. */
512
+ entry.error = scrubSecrets(err.message, resolved?.url, entry.config.url);
513
+ /* A missing password, said as one.
514
+
515
+ Moving a secret to the keychain gives this tool a failure mode it did
516
+ not have before: the entry can be removed, or the keychain locked, or
517
+ the file copied to a machine that has neither. What the driver says
518
+ then is its own internal complaint — Postgres answers "SASL:
519
+ SCRAM-SERVER-FIRST-MESSAGE: client password must be a string", which
520
+ names nothing a reader can act on and does not even contain the word
521
+ "password" in a useful position.
522
+
523
+ So when the URL names a user, no password was found for it, and the
524
+ connection failed, the reason is stated where the reader is already
525
+ looking. Only then: guessing at any other failure would be worse than
526
+ the driver's own words, which are usually right. And only when a
527
+ lookup actually ran — a URL from an untrusted client resolves nothing,
528
+ so "looked in the keychain" would be a lie about it. */
529
+ if (resolved?.source === 'none' && !entry.untrustedUrl && namesUserWithoutPassword(resolved.url)) {
530
+ entry.error =
531
+ `no password found for "${entry.config.name}". ` +
532
+ 'Looked in the environment, the keychain, and the credentials file. ' +
533
+ 'If it was moved to the keychain and has since been removed, add the connection again ' +
534
+ `with its password. (The database said: ${entry.error})`;
535
+ }
536
+ // Rethrown with the connection named, because "ECONNREFUSED" on its own
537
+ // does not say which of eight databases is down.
538
+ throw new Error(`${entry.config.name}: ${entry.error}`);
539
+ }
540
+ }
541
+ /**
542
+ * Hold a connection open for the length of one request.
543
+ *
544
+ * Closing an adapter is not an operation the thing using it finds out
545
+ * about. A slow `/api/run` in one tab and a `refresh` in another meant the
546
+ * pg client was ended mid-query — the read came back as a driver-level
547
+ * "connection closed" 500, and on SQLite as a use-after-close. Nothing
548
+ * counted who was using what.
549
+ *
550
+ * The lease is taken when a request resolves its connection and released
551
+ * when the response ends, whichever way it ends.
552
+ */
553
+ lease(id) {
554
+ const entry = this.entries.get(id);
555
+ if (!entry)
556
+ return () => { };
557
+ entry.busy += 1;
558
+ let released = false;
559
+ return () => {
560
+ if (released)
561
+ return;
562
+ released = true;
563
+ entry.busy -= 1;
564
+ if (entry.busy <= 0) {
565
+ entry.busy = 0;
566
+ for (const wake of entry.idle.splice(0))
567
+ wake();
568
+ }
569
+ };
570
+ }
571
+ /**
572
+ * Wait for the requests using a connection to finish.
573
+ *
574
+ * Bounded, because a query can be stuck for as long as the statement
575
+ * timeout allows and "the close hangs" is a worse failure than "the close
576
+ * was slightly rude". After the wait it closes anyway — which is the old
577
+ * behaviour, now as a last resort rather than as the only one.
578
+ */
579
+ async drain(id, ms = 5_000) {
580
+ const entry = this.entries.get(id);
581
+ if (!entry || entry.busy <= 0)
582
+ return;
583
+ await new Promise((resolve) => {
584
+ const timer = setTimeout(resolve, ms);
585
+ entry.idle.push(() => {
586
+ clearTimeout(timer);
587
+ resolve();
588
+ });
589
+ });
590
+ }
591
+ /** Drop the cached schema so the next open re-reads the catalog. */
592
+ async refresh(id) {
593
+ const entry = this.entries.get(id);
594
+ if (!entry)
595
+ throw new Error(`No connection called "${id}".`);
596
+ const finish = this.invalidateAdapter(entry);
597
+ try {
598
+ await this.drain(id);
599
+ if (entry.adapter)
600
+ await entry.adapter.close();
601
+ entry.adapter = entry.dialed = undefined;
602
+ entry.schema = undefined;
603
+ }
604
+ finally {
605
+ finish();
606
+ }
607
+ return this.open(id);
608
+ }
609
+ async closeAll() {
610
+ const entries = [...this.entries.values()];
611
+ const finish = entries.map((entry) => this.invalidateAdapter(entry));
612
+ try {
613
+ await Promise.allSettled(entries.map((entry) => entry.adapter?.close()));
614
+ }
615
+ finally {
616
+ for (const done of finish)
617
+ done();
618
+ }
619
+ }
620
+ /**
621
+ * Every open connection's pool meters, for the operator surface. A
622
+ * connection is named by its position among the configured ones — the
623
+ * order of tablewalk.json, then the command line — and never by its name
624
+ * or URL, which that surface must not carry. A connection not yet opened
625
+ * has no pool to report.
626
+ */
627
+ poolStats() {
628
+ return [...this.entries.values()].flatMap((entry, index) => {
629
+ const adapter = entry.adapter;
630
+ if (!adapter?.poolStats)
631
+ return [];
632
+ try {
633
+ return adapter.poolStats().map((stats) => ({ ...stats, position: index + 1, dialect: adapter.dialect }));
634
+ }
635
+ catch {
636
+ // Instrumentation must not become a new failure path for the operator read.
637
+ return [];
638
+ }
639
+ });
640
+ }
641
+ }
642
+ /**
643
+ * Whether two connection strings name the same database, ignoring the
644
+ * password. `null` when either is not a URL (a SQLite path), in which case
645
+ * the answer is plain string equality, decided by the caller.
646
+ */
647
+ function sameTarget(a, b) {
648
+ if (a === b)
649
+ return true;
650
+ try {
651
+ const x = new URL(a);
652
+ const y = new URL(b);
653
+ x.password = '';
654
+ y.password = '';
655
+ return x.toString() === y.toString();
656
+ }
657
+ catch {
658
+ return null;
659
+ }
660
+ }
661
+ function publicView(entry) {
662
+ return {
663
+ id: entry.config.id,
664
+ name: entry.config.name,
665
+ dialect: entry.config.http ? 'http' : dialectOf(entry.config.url),
666
+ // As the file wrote it, as it always read in the sidebar.
667
+ detail: redact(entry.config.writtenUrl ?? entry.config.url),
668
+ source: entry.config.source,
669
+ connected: Boolean(entry.schema),
670
+ tables: entry.schema?.tables.length,
671
+ error: entry.error,
672
+ secret: entry.secret,
673
+ warning: entry.warning,
674
+ /* Safe to send, unlike the URL: a schema list is a set of names the user
675
+ chose to browse, and the manager needs them to prefill its editor. */
676
+ schemas: entry.config.schemas,
677
+ /* Sent so the interface can be honest about it: a write toggle offered on
678
+ a connection the server will refuse to write is a button that lies. */
679
+ writable: Boolean(entry.config.writable),
680
+ /* Same reason as `writable`: an interface that offers a query bar on a
681
+ connection the server will not read rows from is an interface that
682
+ lies. */
683
+ ...(entry.config.rows === false ? { rows: false } : {}),
684
+ // Same reason again: a SQL box on a connection that refuses SQL lies.
685
+ ...(entry.config.hidden ? { hidesColumns: true } : {}),
686
+ };
687
+ }
688
+ /* Read off the scheme, with SQLite as the fallback because a SQLite target is
689
+ a bare file path and has no scheme to read.
690
+
691
+ An unlisted scheme was once labelled "sqlite" in the picker and the
692
+ connection manager — a wrong answer to the one question those labels
693
+ exist to answer. Adding an adapter means adding its scheme here;
694
+ `connect.ts` is the other half of the same list. */
695
+ function dialectOf(url) {
696
+ if (/^postgres(ql)?:\/\//i.test(url))
697
+ return 'postgres';
698
+ return 'sqlite';
699
+ }
700
+ /**
701
+ * Read the first config file that exists.
702
+ *
703
+ * A missing file is not an error — most people will never write one. A file
704
+ * that exists but is malformed *is* an error, and is reported rather than
705
+ * skipped: silently ignoring a config someone wrote is worse than refusing.
706
+ *
707
+ * A SQLite path written relative to the file is located here, once, against
708
+ * the file's folder (`sqliteTargetFrom`), and `writtenUrl` keeps the spelling
709
+ * for a save. Every tool reads its connections through here — a start, check,
710
+ * build, migrate, seed, a suite, the token and test commands — and each used
711
+ * to apply the rule itself or not at all: a migrate run from an App's folder
712
+ * looked for `./shop.db` there while the server beside it opened the file
713
+ * next to the config, and the command journal a start puts beside the
714
+ * database was made in the working directory. A value from the environment
715
+ * (`"${DATABASE_URL}"`) is not the file's: it is read as given, from where
716
+ * tablewalk runs, as the same variable is without a config.
717
+ */
718
+ export async function loadConfig(explicit, sections) {
719
+ for (const path of configLocations(explicit)) {
720
+ let text;
721
+ try {
722
+ text = await readFile(path, 'utf8');
723
+ }
724
+ catch {
725
+ if (explicit)
726
+ throw new Error(`No config file at ${path}`);
727
+ continue;
728
+ }
729
+ let parsed;
730
+ try {
731
+ parsed = JSON.parse(text);
732
+ }
733
+ catch (err) {
734
+ throw new Error(`${path} is not valid JSON: ${err.message}`);
735
+ }
736
+ const connections = parsed?.connections;
737
+ if (!Array.isArray(connections)) {
738
+ throw new Error(`${path} needs a "connections" array.`);
739
+ }
740
+ const services = [];
741
+ const kept = [];
742
+ const loaded = connections.flatMap((c, i) => {
743
+ if (keptUnread(sections, c?.kind)) {
744
+ kept.push(c);
745
+ return [];
746
+ }
747
+ /* An identity provider, and a host that signs people in, are found by their issuer, which is their address;
748
+ the OAuth authorization server is this server itself, and has none of its own. */
749
+ if (!c || ((typeof c.url !== 'string' || !c.url) && !isUrlless(sections, c.kind))) {
750
+ throw new Error(`${path}: connection ${i + 1} has no "url".`);
751
+ }
752
+ if (typeof c.name !== 'string' || !c.name) {
753
+ throw new Error(`${path}: connection ${i + 1} has no "name".`);
754
+ }
755
+ /* A cache or a webhook is not a database: checked by its own
756
+ rules, kept apart, and never offered as a connection to browse. */
757
+ if (isServiceKind(sections, c.kind)) {
758
+ services.push(serviceConnectionOf(sections, c, path));
759
+ return [];
760
+ }
761
+ /* Refused rather than coerced. `"writable": "false"` is a string, and
762
+ every non-empty string is truthy — quietly reading that as permission
763
+ to write to production is the one mistake in this file worth being
764
+ loud about. */
765
+ if (c.writable !== undefined && typeof c.writable !== 'boolean') {
766
+ throw new Error(`${path}: connection "${c.name}" has "writable": ${JSON.stringify(c.writable)}. ` + `It must be true or false, without quotes.`);
767
+ }
768
+ /* Same reasoning one boundary further out: `"rows": "false"` is a
769
+ truthy string, and reading it as permission would open exactly the
770
+ connection someone wrote it to close. */
771
+ if (c.rows !== undefined && typeof c.rows !== 'boolean') {
772
+ throw new Error(`${path}: connection "${c.name}" has "rows": ${JSON.stringify(c.rows)}. ` + `It must be true or false, without quotes.`);
773
+ }
774
+ if (c.rows === false && c.writable) {
775
+ /* Writing is how rows get in, and an insert answers with the stored
776
+ row. Allowing both would be a boundary with a door in it. */
777
+ throw new Error(`${path}: connection "${c.name}" is both "writable" and "rows": false. ` +
778
+ `A shape-only connection cannot be written to — drop one of them.`);
779
+ }
780
+ if (c.hidden !== undefined)
781
+ parseHiddenColumns(c.hidden, `${path}: connection "${c.name}"`);
782
+ const migrations = c.migrations;
783
+ if (migrations !== undefined &&
784
+ (typeof migrations !== 'object' ||
785
+ migrations === null ||
786
+ typeof migrations.dir !== 'string' ||
787
+ Object.keys(migrations).some((key) => key !== 'dir' && key !== 'table') ||
788
+ !['undefined', 'string'].includes(typeof migrations.table))) {
789
+ throw new Error(`${path}: connection "${c.name}" has "migrations": ${JSON.stringify(migrations)}. It is { "dir": "<directory>" }, and "table" when the history table is not ${product()}'s own.`);
790
+ }
791
+ const http = httpConnectionOf(c, path);
792
+ const pool = poolOf(c, http !== undefined, path);
793
+ const replicas = replicasOf(c, http !== undefined, path);
794
+ const listen = listenOf(c, http !== undefined, path);
795
+ const { pool: _written, replicas: _listed, listen: _direct, ...entry } = c;
796
+ const located = http ? c.url : sqliteTargetFrom(c.url, dirname(resolve(path)));
797
+ return [
798
+ {
799
+ ...entry,
800
+ ...(located !== c.url ? { url: located, writtenUrl: c.url } : {}),
801
+ ...(http ? { http } : {}),
802
+ ...(pool ? { pool } : {}),
803
+ ...(replicas ? { replicas } : {}),
804
+ ...(listen ? { listen } : {}),
805
+ },
806
+ ];
807
+ });
808
+ checkServices(sections, services, path);
809
+ return { path, connections: loaded, services, ...(kept.length ? { kept } : {}), ...parseConfigSections(sections, parsed, path) };
810
+ }
811
+ return undefined;
812
+ }
813
+ /**
814
+ * Split a connection string into a URL worth persisting and a secret worth
815
+ * protecting, then store the secret where the platform can protect it.
816
+ *
817
+ * The URL that gets written to disk never contains the password. That is the
818
+ * property worth having: a config file that leaks is embarrassing, not a
819
+ * breach, and a connection list can be shared or committed without thinking
820
+ * about it.
821
+ */
822
+ /**
823
+ * A URL that names a user and carries no password.
824
+ *
825
+ * The shape that means "a password was expected from somewhere else" — which
826
+ * is exactly the shape `storeSecret` leaves behind, and so the shape whose
827
+ * failure deserves an explanation rather than the driver's own words.
828
+ */
829
+ function namesUserWithoutPassword(url) {
830
+ try {
831
+ const parsed = new URL(url);
832
+ return Boolean(parsed.username) && !parsed.password;
833
+ }
834
+ catch {
835
+ return false;
836
+ }
837
+ }
838
+ export async function storeSecret(name, url) {
839
+ let parsed;
840
+ try {
841
+ parsed = new URL(url);
842
+ }
843
+ catch {
844
+ return undefined; // A file path has no secret to store.
845
+ }
846
+ if (!parsed.password)
847
+ return undefined;
848
+ const secret = decodeURIComponent(parsed.password);
849
+ /* An environment reference is not a secret to move — it is the *absence* of
850
+ one, and the arrangement this file recommends first.
851
+ `postgres://app:${PROD_PASSWORD}@host/db` stores nothing anywhere; the
852
+ value arrives from the environment at open time. Moving it would put the
853
+ literal characters `${PROD_PASSWORD}` into the keychain and strip the
854
+ reference out of the URL, breaking the workflow in the course of trying
855
+ to secure it.
856
+
857
+ Checked on the raw password rather than with `hasInlinePassword`, which
858
+ is only asked *after* expansion — by then a reference has already become
859
+ the value it names, and the distinction this needs is gone. */
860
+ if (/\$\{?[A-Za-z_]/.test(secret))
861
+ return undefined;
862
+ parsed.password = '';
863
+ const withoutSecret = parsed.toString();
864
+ if (keychainAvailable() && (await keychainSet(name, secret))) {
865
+ return { stored: 'keychain', url: withoutSecret };
866
+ }
867
+ await writeCredential(name, secret);
868
+ return { stored: 'file', url: withoutSecret };
869
+ }
870
+ /**
871
+ * Write connections back to a config file.
872
+ *
873
+ * The rule this respects: **a page never edits a file on disk without being
874
+ * told to.** Adding or editing a connection changes the running session only;
875
+ * this is the separate, explicit act of making that permanent, and the UI has
876
+ * to name the file it is about to write.
877
+ *
878
+ * URLs are stored exactly as they were typed, which is what makes
879
+ * `postgres://app:${PROD_PASSWORD}@db/app` survive a round trip as a
880
+ * reference rather than being resolved into the secret it points at. The
881
+ * corollary is that a password someone typed inline gets written inline —
882
+ * true of any config file, and the reason the UI warns about it before this
883
+ * is ever called.
884
+ *
885
+ * Only the `connections` key is rewritten. The registry knows about
886
+ * connections and nothing else, so building the file out of it alone deleted
887
+ * the `views` and `queries` a team had committed — and reported success. A
888
+ * file with a section this function does not understand is a file it has no
889
+ * business rewriting from scratch.
890
+ */
891
+ /* A url the file did not write — a connection added in the session — is the working directory's when it is a
892
+ relative SQLite path, and the file is read from its own folder: so it is saved where it is. */
893
+ function savedUrl(url) {
894
+ return url && sqliteTargetFrom(url, process.cwd());
895
+ }
896
+ export async function saveConfig(path, connections, services = [], sections = CACHE_SECTIONS, kept = []) {
897
+ const body = {
898
+ ...(await existingConfig(path)),
899
+ connections: [
900
+ ...connections
901
+ // Session connections are included: making them permanent is the whole
902
+ // point of pressing the button.
903
+ .map((c) => ({
904
+ name: c.name,
905
+ url: c.writtenUrl ?? savedUrl(c.url),
906
+ ...(c.schemas?.length ? { schemas: c.schemas } : {}),
907
+ // Written only when true. Absent means read-only, and a file full of
908
+ // `"writable": false` would suggest the flag is doing something.
909
+ ...(c.writable ? { writable: true } : {}),
910
+ // And the same rule inverted: only written when it is doing something.
911
+ ...(c.rows === false ? { rows: false } : {}),
912
+ // Kept as written: a save from the connection manager must not unhide a column.
913
+ ...(c.hidden ? { hidden: c.hidden } : {}),
914
+ // A pool size is the file's capacity plan; a save that dropped it would change it.
915
+ ...(c.pool ? { pool: { ...c.pool } } : {}),
916
+ // And so are its replicas, as written, references and all.
917
+ ...(c.replicas?.length ? { replicas: [...c.replicas] } : {}),
918
+ // Where its migrations live is the file's, too.
919
+ ...(c.migrations ? { migrations: c.migrations } : {}),
920
+ /* An API keeps its description as it was written, relative to the
921
+ file, and its auth with the `${VAR}` references still in it. */
922
+ ...(c.http
923
+ ? {
924
+ kind: 'http',
925
+ description: c.http.description,
926
+ ...(c.http.auth ? { auth: c.http.auth } : {}),
927
+ ...(c.http.insecure ? { insecure: true } : {}),
928
+ ...(c.http.allowLinkLocal ? { allowLinkLocal: true } : {}),
929
+ ...(c.http.cache ? { cache: c.http.cache } : {}),
930
+ }
931
+ : {}),
932
+ })),
933
+ /* The cache and the other services, as they were read: the registry does
934
+ not list them as connections, and a save built from the list alone
935
+ deleted them from the file. */
936
+ ...services.map((service) => serviceEntry(sections, service)),
937
+ // And the entries this binary keeps unread: another reader of the file's.
938
+ ...kept,
939
+ ],
940
+ };
941
+ await mkdir(dirname(path), { recursive: true });
942
+ await writeFile(path, `${JSON.stringify(body, null, 2)}\n`);
943
+ }
944
+ /**
945
+ * What is already in the file, so that writing connections back keeps it.
946
+ *
947
+ * A file that does not exist yet is simply an empty object — the first save
948
+ * writes a file with one key in it. A file that exists and cannot be parsed
949
+ * is the case worth stopping on: overwriting it would replace something
950
+ * hand-edited with a machine-generated stub, and the person who typed the
951
+ * stray comma would find their views gone rather than their typo reported.
952
+ */
953
+ async function existingConfig(path) {
954
+ let text;
955
+ try {
956
+ text = await readFile(path, 'utf8');
957
+ }
958
+ catch {
959
+ return {};
960
+ }
961
+ if (!text.trim())
962
+ return {};
963
+ let parsed;
964
+ try {
965
+ parsed = JSON.parse(text);
966
+ }
967
+ catch (err) {
968
+ throw new Error(`${path} is not valid JSON (${err.message}), so its other settings cannot be ` +
969
+ 'preserved. Fix the file, or save to a different path.');
970
+ }
971
+ if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed))
972
+ return {};
973
+ /* `connections` is dropped because it is about to be written from the
974
+ registry; everything else — views, queries, comments, keys a later
975
+ version adds — is carried through untouched. */
976
+ const { connections: _drop, ...rest } = parsed;
977
+ return rest;
978
+ }