@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,1187 @@
1
+ /**
2
+ * Pages: a record layout with a front door.
3
+ *
4
+ * A record template lays out a row you have already found. A page starts one
5
+ * step earlier — it asks *which* customer, and then shows everything that
6
+ * customer is: their own fields, how many orders they have placed, what those
7
+ * orders were, and the lines inside those orders. One screen assembled from
8
+ * one root, instead of a walk you have to remember the way back through.
9
+ *
10
+ * The content model is deliberately the record template's, extended rather
11
+ * than replaced. Two systems that both lay out sections keyed by foreign
12
+ * key would drift within a month, and the second one would be the one that
13
+ * forgot composite keys. What pages add is:
14
+ *
15
+ * - **metric** sections — one number, from an aggregate over a relationship
16
+ * - **lists that reach further than one hop**, via a path rather than a
17
+ * single constraint, so "the lines of this customer's orders" is a
18
+ * section rather than two clicks
19
+ * - **a filter on a list**, so "recent open orders" is a section and not
20
+ * the whole of them
21
+ * - **a width**, so three counts sit in a row instead of stacked
22
+ *
23
+ * Everything here is pure: parsing, validating, and turning a page plus a
24
+ * root key into the requests its sections need. Nothing fetches. That is what
25
+ * lets the hard part — path resolution against a real schema, which is where
26
+ * the mistakes live — be tested without a browser or a database.
27
+ */
28
+ import { ANYONE, saidAt } from './query.js';
29
+ import { listEmptyConfig } from './listEmpty.js';
30
+ import { withDefaultAside } from './pageAnatomy.js';
31
+ import { columnKind, columnOrRollup, findTable, primaryKey, looksLikeDate, rowName, referencesTo, tableNamed, } from './schema.js';
32
+ import { tryResolvePath } from './viewPaths.js';
33
+ import { hopName, parseViewQuery, validateView } from './view.js';
34
+ import { DATE_UNITS } from './breakdown.js';
35
+ import { authoredSpelling } from './autoValues.js';
36
+ import { createSaysMore, relatedCreatePlan } from './relatedCreate.js';
37
+ import { groupsAsked } from './appComponents.js';
38
+ import { DEFAULT_PAGE_LIST_LIMIT, timelineMarkColumns, timelineSide, sectionReading, } from './pageModel.js';
39
+ import { resolvePath, pageHop, resolveOf, listView, defaultColumns, similarProblem, groupProblem, groupFactPaths, relatedMatchProblem, customProblem, } from './pagePaths.js';
40
+ export { WIDTHS, OFFSETS, SECTION_REGIONS, FIELD_COLUMNS, timelineMarkColumns, DEFAULT_PAGE_LIST_LIMIT, MAX_PAGE_SECTIONS, } from './pageModel.js';
41
+ export { resolvePath, rootFilter, resolveOf, listView, sectionQuery, metricView, defaultColumns, customReadView, } from './pagePaths.js';
42
+ export { parsePagesConfig, PAGE_KEYS, PAGE_SECTION_KEYS, STREAM_KEYS, parsePageBody } from './pageConfig.js';
43
+ /* How a suggested page divides the relationships pointing at its root: the
44
+ first few as lists, the next few as counts, the rest not shown. A page that
45
+ listed all twelve things pointing at `customer` would be a report. */
46
+ const LISTED = 2;
47
+ const COUNTED = 3;
48
+ /**
49
+ * Check a page against a real schema, and say what is wrong with it.
50
+ *
51
+ * Returns the page with an `error` rather than throwing, because a page that
52
+ * names a table this connection does not have is still a page — it belongs
53
+ * in the list, greyed, with its reason. A config file shared by a team will
54
+ * always describe some connection someone is not on.
55
+ */
56
+ export function validatePage(schema, page) {
57
+ return withDefaultAside(schema, validated(schema, page));
58
+ }
59
+ function validated(schema, page) {
60
+ /* Names resolve the nav's way first: a manifest writes "account" and
61
+ Postgres calls it "public.account", and matching ids alone made every
62
+ app page "missing" on the product's primary target — found the first
63
+ time the app layer ever ran against live Postgres. The resolved copy
64
+ is what this returns, so everything downstream — the planner, the
65
+ client, the caller's own reads — keys on real ids. */
66
+ const resolvedBase = tableNamed(schema, page.base);
67
+ page = {
68
+ ...page,
69
+ base: resolvedBase?.id ?? page.base,
70
+ sections: page.sections.map((s) => {
71
+ /* Only a section that reads rows names a table to resolve. */
72
+ if (!sectionReading(s, 'rows'))
73
+ return s;
74
+ if (s.kind === 'custom') {
75
+ const named = (each) => each &&
76
+ Object.fromEntries(Object.entries(each).map(([name, read]) => {
77
+ const from = tableNamed(schema, read.from);
78
+ return [name, from && from.id !== read.from ? { ...read, from: from.id } : read];
79
+ }));
80
+ return s.reads || s.aggregates
81
+ ? {
82
+ ...s,
83
+ ...(s.reads ? { reads: named(s.reads) } : {}),
84
+ ...(s.aggregates ? { aggregates: named(s.aggregates) } : {}),
85
+ }
86
+ : s;
87
+ }
88
+ if (s.kind === 'timeline') {
89
+ return {
90
+ ...s,
91
+ streams: s.streams.map((stream) => {
92
+ const from = tableNamed(schema, stream.from);
93
+ return from && from.id !== stream.from ? { ...stream, from: from.id } : stream;
94
+ }),
95
+ };
96
+ }
97
+ const from = tableNamed(schema, s.from);
98
+ return from && from.id !== s.from ? { ...s, from: from.id } : s;
99
+ }),
100
+ };
101
+ const base = findTable(schema, page.base);
102
+ if (!base)
103
+ return { ...page, error: `"${page.base}" is not a table on this connection.` };
104
+ /* A section's `when` is a condition on the root, judged now rather than as a dead section later, by the parser
105
+ that judges it at run time (/api/run reads it as a view): a reference path and a computed column read there as
106
+ they do in a list's filter. */
107
+ for (const [i, section] of page.sections.entries()) {
108
+ if (!section.when)
109
+ continue;
110
+ // Judged before any request: `me` reads as anyone's, and /api/run reads it as the reader.
111
+ const parsed = parseViewQuery(schema, base.id, section.when, undefined, { me: ANYONE });
112
+ const problem = parsed.errors.length
113
+ ? saidAt(parsed.errors[0], section.when)
114
+ : parsed.columns.length || parsed.orderBy.length
115
+ ? 'is a condition, with no "show" or "sort".'
116
+ : undefined;
117
+ if (problem)
118
+ return { ...page, error: `section ${i + 1}: "when": ${problem}` };
119
+ }
120
+ if (!primaryKey(base).length) {
121
+ return {
122
+ ...page,
123
+ error: `"${base.name}" has no primary key, so a single record cannot be identified.`,
124
+ };
125
+ }
126
+ /* A document's number is a field of its record, read as a fields section reads one. */
127
+ if (page.document?.number !== undefined) {
128
+ const error = validateView(schema, {
129
+ id: `page:${page.id}:document`,
130
+ name: 'document',
131
+ base: page.base,
132
+ columns: [{ path: page.document.number }],
133
+ });
134
+ if (error)
135
+ return { ...page, error: `document: "number": ${error}` };
136
+ }
137
+ for (const [i, section] of page.sections.entries()) {
138
+ const at = section.title ? `"${section.title}"` : `section ${i + 1}`;
139
+ /* A note reads nothing. Nor does a history (the record's own writes), a flows section (the runs are the record's
140
+ own, and the base resource declaring them is judged with the App) or a diagram (its steps are read as the
141
+ reader's rows, and the base being a flows table is judged with the App): nothing to resolve. */
142
+ if (sectionReading(section, 'none'))
143
+ continue;
144
+ /* An embed's table is its view's, copied in when the App loads; one that
145
+ arrives without it was never resolved against the App's views. */
146
+ if (section.kind === 'list' && section.view !== undefined && !section.from) {
147
+ return { ...page, error: `${at}: embeds the view "${section.view}", which is not one of this App's list views.` };
148
+ }
149
+ if (section.kind === 'list' && section.create) {
150
+ const creation = relatedCreatePlan(schema, page.base, section.from, section.path, section.create, {
151
+ filter: section.filter,
152
+ });
153
+ if (creation.error)
154
+ return { ...page, error: `${at}: ${creation.error}` };
155
+ }
156
+ /* A composer adds to the first stream the way a related list adds to
157
+ itself, so it is judged the same way; a stream narrowed by a query
158
+ could hide the row it just added. */
159
+ if (section.kind === 'timeline' && section.create) {
160
+ const first = section.streams[0];
161
+ if (first.filter)
162
+ return { ...page, error: `${at}: a timeline composer cannot add to a stream narrowed by a query.` };
163
+ if (section.create.prefill)
164
+ return { ...page, error: `${at}: a timeline composer takes no "prefill"; a related list's create fills from the record.` };
165
+ const creation = relatedCreatePlan(schema, page.base, first.from, first.path, section.create);
166
+ if (creation.error)
167
+ return { ...page, error: `${at}: ${creation.error}` };
168
+ }
169
+ if (section.kind === 'fields') {
170
+ const error = validateView(schema, {
171
+ id: `page:${page.id}:${i}`,
172
+ name: at,
173
+ base: page.base,
174
+ columns: (section.paths ?? []).map((path) => ({ path })),
175
+ });
176
+ if (error)
177
+ return { ...page, error: `${at}: ${error}` };
178
+ /* A was-price is a figure beside the figure: a number the reader can compare, or it says nothing. */
179
+ if (section.was !== undefined) {
180
+ const was = tryResolvePath(schema, page.base, section.was);
181
+ if (!was.resolved)
182
+ return { ...page, error: `${at}: "was" names "${section.was}", ${was.error?.message ?? 'which does not resolve'}` };
183
+ if (columnKind(was.resolved.column.type) !== 'number')
184
+ return {
185
+ ...page,
186
+ error: `${at}: "was" is a number or money column beside the figure; "${section.was}" is ${was.resolved.column.type || 'untyped'}.`,
187
+ };
188
+ }
189
+ if (section.status !== undefined && !columnOrRollup(base, section.status)) {
190
+ return { ...page, error: `${at}: the header's status "${section.status}" is not a column on ${base.name}.` };
191
+ }
192
+ if (section.workflow) {
193
+ const column = base.columns.find((c) => c.name === section.workflow.field);
194
+ if (!column || !/text|char|enum/i.test(column.type)) {
195
+ return { ...page, error: `${at}: workflow field must name a text or enum column on ${base.name}.` };
196
+ }
197
+ }
198
+ continue;
199
+ }
200
+ if (section.kind === 'timeline') {
201
+ /* Each stream judged the way a list is: the path leads home, the
202
+ date column is a date, the shown paths resolve. */
203
+ for (const stream of section.streams) {
204
+ const here = `${at}, stream "${stream.from}"`;
205
+ let home = page.base;
206
+ if (stream.of) {
207
+ const of = resolveOf(schema, page.base, stream.of);
208
+ if (of.error)
209
+ return { ...page, error: `${here}: ${of.error}` };
210
+ home = of.table.id;
211
+ }
212
+ const resolved = resolvePath(schema, stream.from, stream.path);
213
+ if (resolved.error)
214
+ return { ...page, error: `${here}: ${resolved.error}` };
215
+ if (resolved.path.endsAt !== home) {
216
+ return {
217
+ ...page,
218
+ error: `${here}: "${stream.path}" leads from ${stream.from} to ` + `${resolved.path.endsAt}, not to ${home}.`,
219
+ };
220
+ }
221
+ const from = findTable(schema, stream.from);
222
+ const atCol = from?.columns.find((c) => c.name === stream.at);
223
+ if (!atCol)
224
+ return { ...page, error: `${here}: "${stream.at}" is not a column on ${stream.from}.` };
225
+ if (!/date|time/i.test(atCol.type ?? '')) {
226
+ return {
227
+ ...page,
228
+ error: `${here}: "${stream.at}" is ${atCol.type || 'untyped'} — a timeline orders by a date column. See \`mistri docs dates-and-types\`.`,
229
+ };
230
+ }
231
+ for (const path of [stream.show, stream.detail, stream.by].filter((p) => Boolean(p))) {
232
+ /* A bare name is a column of `from`; a dotted path walks references
233
+ to a column of the table it reaches, and is judged the way a
234
+ list column is — `author_id.name`, not a chain of references. */
235
+ if (path.includes('.')) {
236
+ const error = validateView(schema, { id: `page:${page.id}:${i}:stream`, name: here, base: stream.from, columns: [{ path }] });
237
+ if (error)
238
+ return { ...page, error: `${here}: ${error}` };
239
+ }
240
+ else if (!from?.columns.some((c) => c.name === path)) {
241
+ return { ...page, error: `${here}: "${path}" is not a column on ${stream.from}.` };
242
+ }
243
+ }
244
+ }
245
+ /* A tone and a compact rule read their column on each stream that has it and says none of its own, as a side
246
+ does; a rule no stream reads is a slip, named with the streams it missed. */
247
+ for (const column of timelineMarkColumns(section)) {
248
+ const reads = section.streams.some((stream) => timelineMarkColumns(section, stream, findTable(schema, stream.from)).includes(column));
249
+ if (!reads)
250
+ return {
251
+ ...page,
252
+ error: `${at}: the "tone" or "compact" column "${column}" is read by no stream: it is not a column on ${section.streams.map((stream) => stream.from).join(' or ')}, or each says its own.`,
253
+ };
254
+ }
255
+ /* The other party's rows: the condition each stream's rows are asked, the record reached by the stream's way
256
+ back to it, judged by the real parser now rather than as a read that fails on the page. */
257
+ const sides = timelineSides(schema, section);
258
+ if (sides.error)
259
+ return { ...page, error: `${at}${sides.error}` };
260
+ continue;
261
+ }
262
+ /* A custom section's reads are judged as the lists they are planned
263
+ as, and the record's fields as a fields section's (customProblem). */
264
+ if (section.kind === 'custom') {
265
+ const problem = customProblem(schema, page, section, i) ?? customTotalsProblem(schema, page, section, i);
266
+ if (problem)
267
+ return { ...page, error: `${at}: ${problem}` };
268
+ continue;
269
+ }
270
+ /* A similar list walks no path, so it is judged on its match and on the
271
+ columns it shows — in the words the planner would use for them. */
272
+ if (section.kind === 'list' && section.similar) {
273
+ const problem = similarProblem(schema, page, section);
274
+ if (problem)
275
+ return { ...page, error: `${at}: ${problem}` };
276
+ const table = findTable(schema, section.from);
277
+ const error = validateView(schema, {
278
+ id: `page:${page.id}:${i}`,
279
+ name: at,
280
+ base: table.id,
281
+ columns: (section.columns?.length ? section.columns : defaultColumns(table, '')).map((path) => ({ path })),
282
+ orderBy: section.orderBy?.map((order) => ({ path: order.column, direction: order.direction })),
283
+ });
284
+ if (error)
285
+ return { ...page, error: `${at}: ${error}` };
286
+ continue;
287
+ }
288
+ let rootTable = page.base;
289
+ if (section.of) {
290
+ const of = resolveOf(schema, page.base, section.of);
291
+ if (of.error)
292
+ return { ...page, error: `${at}: ${of.error}` };
293
+ rootTable = of.table.id;
294
+ }
295
+ const resolved = resolvePath(schema, section.from, section.path);
296
+ if (resolved.error)
297
+ return { ...page, error: `${at}: ${resolved.error}` };
298
+ if (resolved.path.endsAt !== rootTable) {
299
+ return {
300
+ ...page,
301
+ error: `${at}: "${section.path}" leads from ${section.from} to ` + `${resolved.path.endsAt}, not to ${rootTable}.`,
302
+ };
303
+ }
304
+ if (section.kind === 'list') {
305
+ /* Validate shown and ordered paths with the same compiler that will
306
+ execute the list. In particular, projected/private columns must not
307
+ survive startup just because the relationship itself is valid. */
308
+ const table = findTable(schema, section.from);
309
+ const columns = [
310
+ ...(section.columns?.length ? section.columns : defaultColumns(table, section.path, resolved.path)),
311
+ ...groupFactPaths(section),
312
+ ];
313
+ const grouped = groupProblem(schema, page, section);
314
+ if (grouped)
315
+ return { ...page, error: `${at}: ${grouped}` };
316
+ const error = validateView(schema, {
317
+ id: `page:${page.id}:${i}`,
318
+ name: at,
319
+ base: table.id,
320
+ columns: columns.map((path) => ({ path })),
321
+ orderBy: section.orderBy?.map((order) => ({ path: order.column, direction: order.direction })),
322
+ });
323
+ if (error)
324
+ return { ...page, error: `${at}: ${error}` };
325
+ const problem = relatedMatchProblem(schema, page, section);
326
+ if (problem)
327
+ return { ...page, error: `${at}: ${problem}` };
328
+ }
329
+ if (section.kind === 'metric' && section.fn !== 'count' && !section.column) {
330
+ return { ...page, error: `${at}: "${section.fn}" needs a column to ${section.fn}.` };
331
+ }
332
+ /* A dotted `by` is the view compiler's to judge at run time; a plain one
333
+ is checkable right here, and "the column does not exist" is a better
334
+ sentence before the page is opened than after. */
335
+ if (section.kind === 'chart') {
336
+ const keys = chartKeys(section.by);
337
+ if ('error' in keys)
338
+ return { ...page, error: `${at}: ${keys.error}` };
339
+ const from = findTable(schema, section.from);
340
+ const error = validateView(schema, {
341
+ id: `page:${page.id}:${i}:chart`,
342
+ name: at,
343
+ base: from.id,
344
+ columns: keys.keys.map((key) => ({ path: key.path })),
345
+ });
346
+ if (error)
347
+ return { ...page, error: `${at}: ${error}` };
348
+ for (const key of keys.keys) {
349
+ if (!key.bucket || key.path.includes('.'))
350
+ continue;
351
+ const column = from.columns.find((c) => c.name === key.path);
352
+ if (column && !/date|time/i.test(column.type ?? '')) {
353
+ return {
354
+ ...page,
355
+ error: `${at}: "${key.bucket} ${key.path}" cuts ${key.path} into ${key.bucket}s, and it is ${column.type || 'untyped'} — a date column is.`,
356
+ };
357
+ }
358
+ }
359
+ if (section.column && !columnOrRollup(from, section.column)) {
360
+ return { ...page, error: `${at}: "${section.column}" is not a column on ${section.from}.` };
361
+ }
362
+ const ordering = chartSortColumn(section);
363
+ if (ordering && !from.columns.some((c) => c.name === ordering.column)) {
364
+ return { ...page, error: `${at}: "sort" names "${ordering.column}", which is not a column on ${section.from}.` };
365
+ }
366
+ }
367
+ if (section.kind === 'breakdown') {
368
+ const from = findTable(schema, section.from);
369
+ if (from && !section.by.includes('.') && !columnOrRollup(from, section.by)) {
370
+ return { ...page, error: `${at}: "${section.by}" is not a column on ${section.from}.` };
371
+ }
372
+ if (from && section.fn !== undefined && section.fn !== 'count' && !section.column) {
373
+ return { ...page, error: `${at}: "${section.fn}" needs a column to ${section.fn}.` };
374
+ }
375
+ if (from && section.column && !columnOrRollup(from, section.column)) {
376
+ return { ...page, error: `${at}: "${section.column}" is not a column on ${section.from}.` };
377
+ }
378
+ }
379
+ }
380
+ return { ...page, error: undefined };
381
+ }
382
+ /**
383
+ * Which rows of each timeline stream are the other party's: the condition each stream is asked, with `record.` reached
384
+ * by the stream's way back to the record, or undefined for a stream that has no far side. A stream's own `side` is its
385
+ * condition and must read over its table. The section's `side` is every other stream's, where it reads there: a stream
386
+ * whose table lacks a column it names (a time entry beside the replies, with no author) is never on the far side. A
387
+ * section's `side` no stream can read is refused, so a misspelt column is still named. `error` follows the section's
388
+ * own place, as `, stream "reply": "side": …`.
389
+ */
390
+ export function timelineSides(schema, section) {
391
+ const read = (condition, stream) => {
392
+ const side = timelineSide(condition, stream);
393
+ if (side === undefined)
394
+ return {
395
+ problem: `"side" names the record (record.…), and this stream is about "${stream.of}": its rows lead to that row, not back to the record.`,
396
+ };
397
+ const parsed = parseViewQuery(schema, stream.from, side);
398
+ const problem = parsed.errors[0]?.message.split(`${stream.path}.`).join('record.') ??
399
+ (parsed.columns.length || parsed.orderBy.length ? 'it is a condition, with no "show" or "sort".' : undefined);
400
+ return problem ? { problem: `"side": ${problem}` } : { side };
401
+ };
402
+ const sides = [];
403
+ let unread;
404
+ let shared = false;
405
+ for (const stream of section.streams) {
406
+ if (stream.side) {
407
+ const own = read(stream.side, stream);
408
+ if (own.problem)
409
+ return { sides, error: `, stream "${stream.from}": ${own.problem}` };
410
+ sides.push(own.side);
411
+ continue;
412
+ }
413
+ if (!section.side) {
414
+ sides.push(undefined);
415
+ continue;
416
+ }
417
+ const each = read(section.side, stream);
418
+ if (each.side !== undefined)
419
+ shared = true;
420
+ else
421
+ unread ??= `, stream "${stream.from}": ${each.problem}`;
422
+ sides.push(each.side);
423
+ }
424
+ if (section.side && !shared)
425
+ return { sides, error: unread ?? ': "side" is read by no stream: each stream says its own. Leave the section\'s out.' };
426
+ return { sides };
427
+ }
428
+ /**
429
+ * What a breakdown section needs to run, as a structured BreakdownDef.
430
+ *
431
+ * It borrows the list machinery for everything except the grouping: the
432
+ * `of` resolution, the join back to the record's key, the extra filter
433
+ * text — a breakdown asks the same question a list does, about the same
434
+ * rows, and answers with groups instead of rows. Building the filter twice
435
+ * would be two implementations of "these rows belong to that record" to
436
+ * keep honest.
437
+ */
438
+ export function breakdownPlan(schema, page, section, key, index, now = new Date(), ofKeys) {
439
+ const asList = {
440
+ kind: 'list',
441
+ from: section.from,
442
+ path: section.path,
443
+ of: section.of,
444
+ filter: section.filter,
445
+ title: section.title,
446
+ };
447
+ const built = listView(schema, page, asList, key, index, now, ofKeys);
448
+ if (built.error)
449
+ return { error: built.error };
450
+ if (!built.view)
451
+ return { empty: built.empty };
452
+ const measured = section.fn !== undefined && !(section.fn === 'count' && section.column === undefined);
453
+ return {
454
+ empty: built.empty,
455
+ def: {
456
+ base: built.view.base,
457
+ by: [{ path: section.by }],
458
+ // A count, or the measure it names ("sum open_value"), called `value` as a chart's is.
459
+ measures: [
460
+ measured ? { fn: section.fn, ...(section.column === undefined ? {} : { path: section.column }), alias: 'value' } : { fn: 'count' },
461
+ ],
462
+ filter: built.view.filter,
463
+ // Biggest first: a breakdown on a page answers "mostly what?".
464
+ orderBy: [{ name: measured ? 'value' : 'count', direction: 'desc' }],
465
+ limit: section.limit ?? 8,
466
+ },
467
+ };
468
+ }
469
+ /**
470
+ * A chart's `by`, in the breakdown grammar's words: one or two keys, each a
471
+ * path and optionally a period to cut a date into — "month issued_on",
472
+ * "status", "month issued_on, status".
473
+ */
474
+ export function chartKeys(by) {
475
+ const parts = by.split(',').map((part) => part.trim());
476
+ if (!parts.length || parts.length > 2 || parts.some((part) => !part)) {
477
+ return { error: `"${by}" is not one or two keys, like "status" or "month issued_on, status".` };
478
+ }
479
+ const keys = [];
480
+ for (const part of parts) {
481
+ const m = /^(?:([a-z]+)\s+)?([A-Za-z_$][\w$+]*(?:\.[A-Za-z_$][\w$+]*)*)$/i.exec(part);
482
+ const unit = m?.[1]?.toLowerCase();
483
+ if (unit === 'auto') {
484
+ return {
485
+ error: `"${part}" cuts the date by the window, which only a dashboard chart has: a record page's chart names its unit, like "month issued_on".`,
486
+ };
487
+ }
488
+ if (!m || (unit !== undefined && !DATE_UNITS.includes(unit))) {
489
+ return {
490
+ error: `"${part}" is not a key: write a column, a path, or a period and a date column — ${DATE_UNITS.map((u) => `"${u} …"`).join(', ')}.`,
491
+ };
492
+ }
493
+ keys.push({ path: m[2], ...(unit ? { bucket: unit } : {}) });
494
+ }
495
+ return { keys };
496
+ }
497
+ /** A chart's `sort` when it names a column of the rows ("starts_on", "ended_on desc"), not 'value' or 'category'. */
498
+ function chartSortColumn(section) {
499
+ if (!section.sort || section.sort === 'value' || section.sort === 'category')
500
+ return undefined;
501
+ const [column, direction] = section.sort.split(/\s+/);
502
+ return { column, direction: direction === 'desc' ? 'desc' : 'asc' };
503
+ }
504
+ /**
505
+ * The order a bar chart's or donut's categories are asked in, where the category has one of its own; undefined leaves
506
+ * them biggest first, which the client then arranges by the key's vocabulary or scale (ChartFigure, orderDeclared).
507
+ *
508
+ * - `sort: 'value'` is biggest first, whatever the key is; `'category'` is the key itself, ascending (a column with a
509
+ * vocabulary keeps the client's, which knows its order); a column of the rows orders by that column's extreme in
510
+ * each category.
511
+ * - Unsaid: a date column, or a number that is a step, runs from the first; and a chart whose category is the rows' own label, a bar for each row of
512
+ * the table (a sprint's name), runs in the order the rows were made, the primary key ascending. The engine's schema
513
+ * carries no resource sort, so the key stands for it.
514
+ *
515
+ * Asked of the query and not left to the client: a chart's categories are cut at its limit, and the cut must keep
516
+ * the first ones in this order, not the largest.
517
+ */
518
+ function chartOrder(schema, section, keys, keyAlias) {
519
+ const first = keys[0];
520
+ if (!first || !keyAlias || section.sort === 'value')
521
+ return undefined;
522
+ const table = findTable(schema, section.from);
523
+ const asked = chartSortColumn(section);
524
+ if (asked) {
525
+ const fn = asked.direction === 'desc' ? 'max' : 'min';
526
+ return { order: { name: 'order_1', direction: asked.direction }, measure: { fn, path: asked.column, alias: 'order_1' } };
527
+ }
528
+ if (first.bucket || !table || first.path.includes('.'))
529
+ return section.sort === 'category' ? { order: { name: keyAlias, direction: 'asc' } } : undefined;
530
+ const column = table.columns.find((c) => c.name === first.path);
531
+ if (!column || column.allowed?.length)
532
+ return undefined;
533
+ /* A number that is a step (a priority, an age in days) runs in its own order; an id, or a reference to a record, is no step. */
534
+ const reference = schema.foreignKeys.some((fk) => fk.from.table === table.id && fk.from.columns.includes(column.name));
535
+ const step = columnKind(column.type) === 'number' && !column.primaryKey && !reference;
536
+ if (section.sort === 'category' || looksLikeDate(column) || step)
537
+ return { order: { name: keyAlias, direction: 'asc' } };
538
+ const key = primaryKey(table);
539
+ if (key.length && rowName(table) === column.name) {
540
+ return { order: { name: 'order_1', direction: 'asc' }, measure: { fn: 'min', path: key[0], alias: 'order_1' } };
541
+ }
542
+ return undefined;
543
+ }
544
+ /**
545
+ * What a chart section needs to run: the breakdown section's plan — the same
546
+ * rows, anchored on the record the same way — with the chart's keys, its
547
+ * measure, and the order its shape reads in. A line reads along time, so it
548
+ * is ordered by its key; a donut and stacked bars are ordered on the client
549
+ * (dashboardModel.ts, orderGroups), as a dashboard's are.
550
+ */
551
+ export function chartPlan(schema, page, section, key, index, now = new Date(), ofKeys) {
552
+ const keys = chartKeys(section.by);
553
+ if ('error' in keys)
554
+ return { error: keys.error };
555
+ const asList = {
556
+ kind: 'list',
557
+ from: section.from,
558
+ path: section.path,
559
+ of: section.of,
560
+ filter: section.filter,
561
+ title: section.title,
562
+ };
563
+ const built = listView(schema, page, asList, key, index, now, ofKeys);
564
+ if (built.error)
565
+ return { error: built.error };
566
+ if (!built.view)
567
+ return { empty: built.empty };
568
+ const measure = section.fn === 'count' ? { fn: 'count', alias: 'value' } : { fn: section.fn, path: section.column, alias: 'value' };
569
+ const line = (section.shape ?? 'line') === 'line';
570
+ /* Named here, so the order below and the reader's columns do not depend on how the compiler would name them. */
571
+ const by = keys.keys.map((k, i) => ({ ...k, alias: `key_${i + 1}` }));
572
+ const ordered = line ? undefined : chartOrder(schema, section, keys.keys, by[0]?.alias);
573
+ return {
574
+ empty: built.empty,
575
+ def: {
576
+ base: built.view.base,
577
+ by,
578
+ /* The column a category order reads rides after the value, and is dropped by the drawing (ChartSection). */
579
+ measures: ordered?.measure ? [measure, ordered.measure] : [measure],
580
+ filter: built.view.filter,
581
+ orderBy: line
582
+ ? by.map((k) => ({ name: k.alias, direction: 'asc' }))
583
+ : ordered
584
+ ? [ordered.order]
585
+ : [{ name: 'value', direction: 'desc' }],
586
+ // A line keeps every period; the others are cut to their limit on the client, after ordering.
587
+ limit: line ? 500 : 400,
588
+ },
589
+ };
590
+ }
591
+ /**
592
+ * What one aggregate read of a custom section needs to run: a breakdown, built from the same pieces a metric and a
593
+ * chart on a page are — `whose` anchors it on the record as a related list is, a query narrows it as a list's does —
594
+ * so the figure counts every row the reader may see, not the page of rows a component happened to load, and the
595
+ * reader's grants, row policy and tenant scope narrow it as they narrow every breakdown. With a `by`, one row for
596
+ * each group (`key_1`, `value`); without, one row (`value`).
597
+ */
598
+ export function customTotalPlan(schema, page, name, total, key, index, now = new Date()) {
599
+ const keys = total.by === undefined ? { keys: [] } : chartKeys(total.by);
600
+ if ('error' in keys)
601
+ return { error: keys.error };
602
+ if (keys.keys.length > 1)
603
+ return { error: `"by" is one key here: "${total.by}" names ${keys.keys.length}.` };
604
+ let filter;
605
+ let base;
606
+ if (total.path) {
607
+ const built = listView(schema, page, { kind: 'list', from: total.from, path: total.path, filter: total.filter, title: name }, key, index, now);
608
+ if (built.error)
609
+ return { error: built.error };
610
+ if (!built.view)
611
+ return { empty: built.empty };
612
+ ({ base, filter } = built.view);
613
+ }
614
+ else {
615
+ const table = findTable(schema, total.from);
616
+ if (!table)
617
+ return { error: `"${total.from}" is not a table on this connection.` };
618
+ base = table.id;
619
+ if (total.filter) {
620
+ const parsed = parseViewQuery(schema, table.id, total.filter, now);
621
+ if (parsed.errors.length)
622
+ return { error: parsed.errors[0].message };
623
+ if (parsed.filter.groups.length)
624
+ filter = parsed.filter;
625
+ }
626
+ }
627
+ const by = keys.keys.map((k) => ({ ...k, alias: 'key_1' }));
628
+ return {
629
+ def: {
630
+ base,
631
+ by,
632
+ measures: [total.fn === 'count' ? { fn: 'count', alias: 'value' } : { fn: total.fn, path: total.column, alias: 'value' }],
633
+ ...(filter ? { filter } : {}),
634
+ // Biggest first, like a breakdown on a page: "mostly what?".
635
+ ...(by.length ? { orderBy: [{ name: 'value', direction: 'desc' }] } : {}),
636
+ limit: by.length ? groupsAsked(total.limit) : 1,
637
+ },
638
+ };
639
+ }
640
+ /**
641
+ * What is wrong with a custom section's aggregate reads against this schema, or undefined: the resource, the column
642
+ * measured, the key it groups by and the `whose` path leading home, judged before a reader sees the section.
643
+ */
644
+ function customTotalsProblem(schema, page, section, index) {
645
+ for (const [name, total] of Object.entries(section.aggregates ?? {})) {
646
+ const here = `figures "${name}"`;
647
+ const table = findTable(schema, total.from);
648
+ if (!table)
649
+ return `${here}: "${total.from}" is not a table on this connection.`;
650
+ if (total.column && !columnOrRollup(table, total.column))
651
+ return `${here}: "${total.column}" is not a column on ${total.from}.`;
652
+ const built = customTotalPlan(schema, page, name, total, {}, index);
653
+ if (built.error)
654
+ return `${here}: ${built.error}`;
655
+ for (const key of total.by === undefined ? [] : chartKeys(total.by).keys) {
656
+ const error = validateView(schema, { id: `page:${page.id}:${index}:${name}`, name, base: table.id, columns: [{ path: key.path }] });
657
+ if (error)
658
+ return `${here}: ${error}`;
659
+ }
660
+ if (total.path) {
661
+ const resolved = resolvePath(schema, total.from, total.path);
662
+ if (resolved.error)
663
+ return `${here}: ${resolved.error}`;
664
+ if (resolved.path.endsAt !== page.base)
665
+ return `${here}: "${total.path}" leads from ${total.from} to ${resolved.path.endsAt}, not to ${page.base}.`;
666
+ }
667
+ }
668
+ return undefined;
669
+ }
670
+ /**
671
+ * A compiled section, spelled back in the authored words — whose/about/
672
+ * show/sort — because an emitter speaking the internal field names writes
673
+ * exactly the config parseSection refuses. Both the scaffold and browser
674
+ * editor use this conversion; new section kinds must be handled here.
675
+ */
676
+ /* A New form's defaults as they are written: a filled-in word as its constant, `{ auto: 'today' }`. */
677
+ function createConfig(create) {
678
+ return create.defaults
679
+ ? {
680
+ ...create,
681
+ defaults: Object.fromEntries(Object.entries(create.defaults).map(([column, value]) => [column, authoredSpelling(value)])),
682
+ }
683
+ : create;
684
+ }
685
+ export function sectionConfig(s) {
686
+ const common = {
687
+ ...(s.title !== undefined ? { title: s.title } : {}),
688
+ ...(s.region !== undefined ? { region: s.region } : {}),
689
+ ...(s.tab !== undefined ? { tab: s.tab } : {}),
690
+ ...(s.width && s.width !== 'full' ? { width: s.width } : {}),
691
+ ...(s.offsetLeft ? { offsetLeft: s.offsetLeft } : {}),
692
+ ...(s.offsetRight ? { offsetRight: s.offsetRight } : {}),
693
+ ...(s.hidden ? { hidden: true } : {}),
694
+ ...(s.when !== undefined ? { when: s.when } : {}),
695
+ };
696
+ switch (s.kind) {
697
+ case 'fields':
698
+ if (s.presentation === 'header')
699
+ return {
700
+ kind: 'header',
701
+ ...(s.hidden ? { hidden: true } : {}),
702
+ ...(s.status !== undefined ? { status: s.status } : {}),
703
+ ...(s.paths?.length
704
+ ? {
705
+ facts: s.paths.map((path) => {
706
+ const said = s.factLabels?.[path];
707
+ return said
708
+ ? {
709
+ column: path,
710
+ ...(said.position === 'before'
711
+ ? { before: said.one }
712
+ : { after: said.many === undefined ? said.one : { one: said.one, many: said.many } }),
713
+ }
714
+ : path;
715
+ }),
716
+ }
717
+ : {}),
718
+ ...(s.primary !== undefined ? { primary: s.primary } : {}),
719
+ ...(s.destructive?.length ? { destructive: s.destructive } : {}),
720
+ ...(s.headerActions?.length ? { actions: s.headerActions } : {}),
721
+ };
722
+ /* A card and a headline are written as the sections they are. */
723
+ if (s.presentation === 'card')
724
+ return { ...common, kind: 'card', show: s.paths ?? [] };
725
+ if (s.presentation === 'headline')
726
+ return {
727
+ ...common,
728
+ kind: 'headline',
729
+ show: s.paths ?? [],
730
+ ...(s.actions ? { links: s.actions.map((a) => ('view' in a ? (({ view, ...rest }) => ({ ...rest, to: view }))(a) : a)) } : {}),
731
+ ...(s.subfigure !== undefined ? { subfigure: s.subfigure } : {}),
732
+ ...(s.was !== undefined ? { was: s.was } : {}),
733
+ ...(s.note !== undefined ? { note: s.note } : {}),
734
+ };
735
+ return {
736
+ ...common,
737
+ kind: s.kind,
738
+ show: s.paths,
739
+ columns: s.columns,
740
+ ...(s.names === false ? { names: false } : {}),
741
+ ...(s.presentation ? { presentation: s.presentation } : {}),
742
+ ...(s.edit ? { editable: true } : {}),
743
+ };
744
+ case 'timeline':
745
+ return {
746
+ ...common,
747
+ kind: s.kind,
748
+ limit: s.limit,
749
+ streams: s.streams.map((stream) => ({
750
+ from: stream.from,
751
+ whose: stream.path,
752
+ at: stream.at,
753
+ show: stream.show,
754
+ detail: stream.detail,
755
+ author: stream.by,
756
+ query: stream.filter,
757
+ ...(stream.of ? { about: stream.of } : {}),
758
+ ...(stream.side ? { side: stream.side } : {}),
759
+ ...(stream.tone ? { tone: stream.tone } : {}),
760
+ ...(stream.compact ? { compact: stream.compact } : {}),
761
+ })),
762
+ ...(s.create ? { create: createSaysMore(s.create) ? createConfig(s.create) : s.create.label } : {}),
763
+ ...(s.newest ? { newest: s.newest } : {}),
764
+ ...(s.composer ? { composer: s.composer } : {}),
765
+ ...(s.tone ? { tone: s.tone } : {}),
766
+ ...(s.compact ? { compact: s.compact } : {}),
767
+ ...(s.side ? { side: s.side } : {}),
768
+ };
769
+ case 'metric':
770
+ return {
771
+ ...common,
772
+ kind: s.kind,
773
+ from: s.from,
774
+ whose: s.path,
775
+ about: s.of,
776
+ // A count is what a metric says when it says nothing.
777
+ ...(s.fn === 'count' && s.column === undefined ? {} : { measure: [s.fn, s.column].filter(Boolean).join(' ') }),
778
+ query: s.filter,
779
+ ...(s.format !== undefined ? { format: s.format } : {}),
780
+ ...(s.unit !== undefined ? { unit: s.unit } : {}),
781
+ };
782
+ /* An embed is said the way it was written: the view, and whose rows. */
783
+ case 'list':
784
+ if (s.view !== undefined)
785
+ return {
786
+ ...common,
787
+ kind: 'view',
788
+ view: s.view,
789
+ whose: s.path,
790
+ ...(s.of !== undefined ? { about: s.of } : {}),
791
+ ...(s.limit !== undefined ? { limit: s.limit } : {}),
792
+ };
793
+ if (s.presentation === 'card')
794
+ return {
795
+ /* The current card takes no offset and no hiding: its placement is the rest of `common`. */
796
+ ...(({ offsetLeft: _left, offsetRight: _right, hidden: _hidden, ...placed }) => placed)(common),
797
+ kind: 'current',
798
+ from: s.from,
799
+ whose: s.path,
800
+ show: s.labels ? s.columns?.map((column) => (s.labels[column] ? { column, label: s.labels[column] } : column)) : s.columns,
801
+ query: s.filter,
802
+ ...(s.empty ? { empty: listEmptyConfig(s.empty) } : {}),
803
+ sort: s.orderBy?.map((o) => (o.direction === 'desc' ? `${o.column} desc` : o.column)).join(', '),
804
+ };
805
+ return {
806
+ ...common,
807
+ kind: s.kind,
808
+ from: s.from,
809
+ // A matched list is found by the record's own values: it has no path to say.
810
+ ...(s.similar ? { same: s.similar.same, ...(s.similar.near ? { near: s.similar.near } : {}) } : { whose: s.path }),
811
+ ...(s.sizing !== undefined ? { sizing: s.sizing } : {}),
812
+ ...(s.presentation === 'gallery' ? { presentation: s.presentation } : {}),
813
+ show: s.labels ? s.columns?.map((column) => (s.labels[column] ? { column, label: s.labels[column] } : column)) : s.columns,
814
+ query: s.filter,
815
+ limit: s.limit,
816
+ ...(s.create
817
+ ? { create: createSaysMore(s.create) ? createConfig(s.create) : s.create.label }
818
+ : s.noCreate
819
+ ? { create: false }
820
+ : {}),
821
+ ...(s.emptyMode ? { empty: s.emptyMode } : s.empty ? { empty: listEmptyConfig(s.empty) } : {}),
822
+ ...(s.group
823
+ ? {
824
+ group: {
825
+ by: s.group.by,
826
+ ...(s.group.show ? { show: s.group.show } : {}),
827
+ ...(s.group.sort ? { sort: s.group.sort.direction === 'desc' ? `${s.group.sort.column} desc` : s.group.sort.column } : {}),
828
+ ...(s.group.limit === undefined ? {} : { limit: s.group.limit }),
829
+ ...(s.group.empty === undefined ? {} : { empty: s.group.empty }),
830
+ ...(s.group.sum ? { sum: s.group.sum } : {}),
831
+ },
832
+ }
833
+ : {}),
834
+ ...(s.totals ? { totals: s.totals } : {}),
835
+ ...(s.rowActions ? { rowActions: true } : {}),
836
+ ...(s.listActions ? { actions: s.listActions } : {}),
837
+ sort: s.orderBy?.map((o) => (o.direction === 'desc' ? `${o.column} desc` : o.column)).join(', '),
838
+ };
839
+ case 'breakdown':
840
+ return {
841
+ ...common,
842
+ kind: s.kind,
843
+ from: s.from,
844
+ whose: s.path,
845
+ about: s.of,
846
+ by: s.by,
847
+ ...(s.fn === undefined || (s.fn === 'count' && s.column === undefined)
848
+ ? {}
849
+ : { measure: [s.fn, s.column].filter(Boolean).join(' ') }),
850
+ query: s.filter,
851
+ limit: s.limit,
852
+ ...(s.format !== undefined ? { format: s.format } : {}),
853
+ ...(s.unit !== undefined ? { unit: s.unit } : {}),
854
+ };
855
+ case 'history':
856
+ return { ...common, kind: s.kind, ...(s.limit !== undefined ? { limit: s.limit } : {}) };
857
+ case 'flows':
858
+ case 'diagram':
859
+ return { ...common, kind: s.kind };
860
+ case 'chart':
861
+ return {
862
+ ...common,
863
+ kind: s.kind,
864
+ from: s.from,
865
+ whose: s.path,
866
+ about: s.of,
867
+ by: s.by,
868
+ ...(s.fn === 'count' && s.column === undefined ? {} : { measure: [s.fn, s.column].filter(Boolean).join(' ') }),
869
+ ...(s.shape ? { shape: s.shape } : {}),
870
+ ...(s.sort ? { sort: s.sort } : {}),
871
+ query: s.filter,
872
+ limit: s.limit,
873
+ ...(s.height !== undefined ? { height: s.height } : {}),
874
+ ...(s.target !== undefined ? { target: s.target } : {}),
875
+ ...(s.format !== undefined ? { format: s.format } : {}),
876
+ ...(s.unit !== undefined ? { unit: s.unit } : {}),
877
+ };
878
+ case 'note':
879
+ return {
880
+ ...common,
881
+ kind: s.kind,
882
+ text: s.text,
883
+ ...(s.variant ? { variant: s.variant } : {}),
884
+ ...(s.icon ? { icon: s.icon } : {}),
885
+ ...(s.action ? { action: 'view' in s.action ? { label: s.action.label, to: s.action.view } : s.action } : {}),
886
+ };
887
+ case 'custom':
888
+ return {
889
+ ...common,
890
+ kind: s.kind,
891
+ component: s.component,
892
+ ...(s.record ? { record: s.record } : {}),
893
+ ...(s.reads
894
+ ? {
895
+ rows: Object.fromEntries(Object.entries(s.reads).map(([name, read]) => [
896
+ name,
897
+ {
898
+ from: read.from,
899
+ ...(read.path ? { whose: read.path } : {}),
900
+ ...(read.filter ? { query: read.filter } : {}),
901
+ show: read.columns,
902
+ ...(read.sort ? { sort: read.sort } : {}),
903
+ ...(read.limit ? { limit: read.limit } : {}),
904
+ },
905
+ ])),
906
+ }
907
+ : {}),
908
+ ...(s.aggregates
909
+ ? {
910
+ figures: Object.fromEntries(Object.entries(s.aggregates).map(([name, total]) => [
911
+ name,
912
+ {
913
+ from: total.from,
914
+ measure: (total.column === undefined ? total.fn : `${total.fn} ${total.column}`),
915
+ ...(total.by ? { by: total.by } : {}),
916
+ ...(total.path ? { whose: total.path } : {}),
917
+ ...(total.filter ? { query: total.filter } : {}),
918
+ ...(total.limit ? { limit: total.limit } : {}),
919
+ },
920
+ ])),
921
+ }
922
+ : {}),
923
+ ...(s.props && Object.keys(s.props).length ? { props: s.props } : {}),
924
+ };
925
+ default: {
926
+ const unsupported = s;
927
+ throw new Error(`Cannot export an unsupported page section: ${JSON.stringify(unsupported)}`);
928
+ }
929
+ }
930
+ }
931
+ /** A whole page in the authored spelling, ready to write to a pages/ file. */
932
+ export function pageConfig(p) {
933
+ return {
934
+ label: p.name,
935
+ base: p.base,
936
+ sections: p.sections.map(sectionConfig),
937
+ ...(p.connection !== undefined ? { connection: p.connection } : {}),
938
+ ...(typeof p.listTabs === 'boolean' ? { listTabs: p.listTabs } : {}),
939
+ ...(p.empty ? { empty: p.empty } : {}),
940
+ };
941
+ }
942
+ /**
943
+ * A page for a table nobody has written one for: the record, a count of each
944
+ * thing that points at it, and the two biggest of those as lists.
945
+ */
946
+ export function suggestPage(schema, baseId) {
947
+ const base = findTable(schema, baseId);
948
+ if (!base || !primaryKey(base).length)
949
+ return null;
950
+ const incoming = referencesTo(schema, base.id).filter((fk) => pageHop(schema, fk) !== undefined);
951
+ const sections = [{ kind: 'fields', width: 'full' }];
952
+ /* Lists for the first few, counts for the rest — never both for the same
953
+ relationship.
954
+
955
+ A count above a list of the rows it counted says nothing the list does
956
+ not: `ROOM 2` over a table of two rooms is a number and its own proof,
957
+ taking a card to do it. The count belongs *on* the list, where it says
958
+ how many there are beyond the ones shown; a card earns its place only for
959
+ a relationship you cannot see, which is what the ones past the listed
960
+ few are. Show the big ones, count the rest. */
961
+ const listed = incoming.slice(0, LISTED);
962
+ const counted = incoming.slice(LISTED, LISTED + COUNTED);
963
+ for (const fk of counted) {
964
+ const child = findTable(schema, fk.from.table);
965
+ const path = pageHop(schema, fk);
966
+ if (!child || path === undefined)
967
+ continue;
968
+ sections.push({
969
+ kind: 'metric',
970
+ fn: 'count',
971
+ from: child.id,
972
+ path,
973
+ title: metricTitle(counted, fk, child.name, base.id),
974
+ width: 'third',
975
+ });
976
+ }
977
+ for (const fk of listed) {
978
+ const child = findTable(schema, fk.from.table);
979
+ const path = pageHop(schema, fk);
980
+ if (!child || path === undefined)
981
+ continue;
982
+ sections.push({
983
+ kind: 'list',
984
+ from: child.id,
985
+ path,
986
+ title: metricTitle(listed, fk, child.name, base.id),
987
+ limit: DEFAULT_PAGE_LIST_LIMIT,
988
+ width: listed.length > 1 ? 'half' : 'full',
989
+ });
990
+ }
991
+ return {
992
+ id: `suggested:${base.id}`,
993
+ name: base.name,
994
+ base: base.id,
995
+ sections,
996
+ source: 'suggested',
997
+ };
998
+ }
999
+ /**
1000
+ * What to call a count.
1001
+ *
1002
+ * The child's name, which is what someone reading a summary expects — until
1003
+ * that is not enough to tell two counts apart. A table pointing at the same
1004
+ * parent twice gives two counts both labelled `work_order`, and a
1005
+ * self-reference gives a count labelled with the table you are already on:
1006
+ * `employee` on an employee page, which is the count of their reports and
1007
+ * says nothing of the sort. Both cases are named by their foreign key
1008
+ * instead, which is the thing that actually differs.
1009
+ */
1010
+ function metricTitle(chosen, fk, childName, baseId) {
1011
+ const ambiguous = chosen.filter((other) => other.from.table === fk.from.table).length > 1;
1012
+ const selfReference = fk.from.table === baseId;
1013
+ return ambiguous || selfReference ? `${childName} via ${fk.from.columns.join(', ')}` : childName;
1014
+ }
1015
+ /**
1016
+ * Every way a table could reach this root, within a couple of hops.
1017
+ *
1018
+ * The one field nobody guesses when writing a page by hand is the path —
1019
+ * `invoice_id.customer_id` is obvious only once you already know it. So it is
1020
+ * not typed: the schema is asked what actually points here, directly and one
1021
+ * table further out, and each answer comes back with the path already
1022
+ * written.
1023
+ *
1024
+ * Two hops rather than the full depth limit. Three is a real relationship and
1025
+ * a list nobody can read: on a schema of this size it runs to hundreds of
1026
+ * entries, most of them routes no one would choose, and a menu that long is a
1027
+ * menu people close.
1028
+ */
1029
+ export function relationsTo(schema, baseId, maxHops = 2) {
1030
+ const base = findTable(schema, baseId);
1031
+ if (!base)
1032
+ return [];
1033
+ const found = [];
1034
+ const seen = new Set();
1035
+ /** Every spelling of each path already built, so deeper hops can extend them. */
1036
+ const aliasesOf = new Map([['', ['']]]);
1037
+ const walk = (targetId, path, via, hops) => {
1038
+ if (hops > maxHops)
1039
+ return;
1040
+ for (const fk of schema.foreignKeys) {
1041
+ if (fk.to.table !== targetId)
1042
+ continue;
1043
+ const child = findTable(schema, fk.from.table);
1044
+ if (!child)
1045
+ continue;
1046
+ // Public logical constraint names may contain dots. Forward paths use
1047
+ // column-based hops, including all columns of a composite reference.
1048
+ const preferred = pageHop(schema, fk);
1049
+ if (preferred === undefined)
1050
+ continue;
1051
+ const next = [preferred, ...path];
1052
+ const route = next.join('.');
1053
+ const id = `${child.id}|${route}`;
1054
+ if (seen.has(id))
1055
+ continue;
1056
+ seen.add(id);
1057
+ const target = findTable(schema, targetId);
1058
+ // Advertise only aliases that actually resolve to this exact hop.
1059
+ const hopNames = [...new Set([hopName(fk), fk.name])].filter((name) => {
1060
+ const resolved = resolvePath(schema, child.id, name).path;
1061
+ return resolved?.hops.length === 1 && resolved.hops[0] === fk;
1062
+ });
1063
+ const tailAliases = aliasesOf.get(path.join('.')) ?? [path.join('.')];
1064
+ const aliases = hopNames.flatMap((name) => tailAliases.map((tail) => (tail ? `${name}.${tail}` : name)));
1065
+ aliasesOf.set(route, aliases);
1066
+ found.push({
1067
+ from: child.id,
1068
+ path: route,
1069
+ aliases,
1070
+ label: child.name,
1071
+ hops: hops + 1,
1072
+ /* Table names and joining columns, innermost first so the description
1073
+ reads outward from the root. */
1074
+ via: [`${child.name}.${fk.from.columns.join(', ')} \u2192 ${target?.name ?? targetId}`, ...via],
1075
+ });
1076
+ /* Not through the root, and not through itself. A self-referencing key
1077
+ is a real relationship at one hop and an infinite regress past it. */
1078
+ if (child.id !== baseId && child.id !== targetId) {
1079
+ walk(child.id, next, [`${child.name}.${fk.from.columns.join(', ')} \u2192 ${findTable(schema, targetId)?.name ?? targetId}`, ...via], hops + 1);
1080
+ }
1081
+ }
1082
+ };
1083
+ walk(base.id, [], [], 0);
1084
+ /* Disambiguated after the fact rather than while walking, because whether
1085
+ `invoice` needs qualifying depends on whether a *second* invoice route
1086
+ turned up later.
1087
+
1088
+ Qualified by the columns, not by the path: `invoice via sold_by` is a
1089
+ sentence someone can check against the schema in their head, where
1090
+ `invoice via invoice_fk_2` is a string only the database has ever seen. */
1091
+ /* Qualified only as far as it takes, and as far as it takes.
1092
+
1093
+ One pass was not enough. `work_log through ticket` was three entries on
1094
+ the employee page — work_log reaches employee through ticket by
1095
+ `assigned_to`, by `raised_by` and by `closed_by` — and all three were
1096
+ rendered with the same words and the same detail line underneath. Three
1097
+ identical rows in a menu is a menu you cannot use: whichever is picked,
1098
+ nobody can tell which was picked.
1099
+
1100
+ So each name is refined until it is unique, one step at a time, and only
1101
+ the ones that collide take the next step. `invoice` stays `invoice` where
1102
+ nothing else is called that. */
1103
+ const steps = new Map(found.map((r) => [r, 0]));
1104
+ for (let pass = 0; pass < 4; pass += 1) {
1105
+ const groups = new Map();
1106
+ for (const r of found) {
1107
+ const label = qualify(r, steps.get(r) ?? 0);
1108
+ const group = groups.get(label);
1109
+ if (group)
1110
+ group.push(r);
1111
+ else
1112
+ groups.set(label, [r]);
1113
+ }
1114
+ let refined = false;
1115
+ for (const group of groups.values()) {
1116
+ if (group.length < 2)
1117
+ continue;
1118
+ for (const r of group) {
1119
+ steps.set(r, (steps.get(r) ?? 0) + 1);
1120
+ refined = true;
1121
+ }
1122
+ }
1123
+ if (!refined)
1124
+ break;
1125
+ }
1126
+ for (const r of found)
1127
+ r.label = qualify(r, steps.get(r) ?? 0);
1128
+ return found.sort((a, b) => a.hops - b.hops || a.label.localeCompare(b.label));
1129
+ }
1130
+ /**
1131
+ * The columns of one hop, out of the sentence it is written as.
1132
+ *
1133
+ * A `via` entry reads `work_log.ticket_id \u2192 ticket`, which is the form the
1134
+ * screen wants; this is the one place that takes it apart again.
1135
+ */
1136
+ const hopColumns = (via) => via?.split(' \u2192 ')[0]?.split('.').slice(1).join('.') ?? '';
1137
+ const hopTarget = (via) => via?.split('\u2192 ')[1] ?? '\u2026';
1138
+ /**
1139
+ * A relationship's name at a given degree of qualification.
1140
+ *
1141
+ * Step 0 is the child table's name, which is right whenever nothing else on
1142
+ * the page is called that. Each step after it adds the next thing that could
1143
+ * tell two of them apart, in the order a reader would ask: which column, then
1144
+ * which table in the middle, then which column *that* one reaches the root
1145
+ * by. The last step falls back to the path — unreadable, and unique by
1146
+ * construction, which is what a last step is for.
1147
+ */
1148
+ function qualify(r, step) {
1149
+ const name = r.label.split(' via ')[0].split(' through ')[0];
1150
+ if (step <= 0)
1151
+ return name;
1152
+ if (r.hops === 1) {
1153
+ return step === 1 ? `${name} via ${hopColumns(r.via[0])}` : `${name} (${r.path})`;
1154
+ }
1155
+ /* Two hops are told apart by the table in the middle — `contract_line
1156
+ through contract` and `contract_line through asset` are two genuinely
1157
+ different lists. Naming the root instead would label both of them
1158
+ "through customer", which is the thing they have in common. */
1159
+ if (step === 1)
1160
+ return `${name} through ${hopTarget(r.via[0])}`;
1161
+ /* And where the middle table is the same, by how *it* reaches the root:
1162
+ `work_log through ticket.assigned_to` against `\u2026 through
1163
+ ticket.raised_by`. `via` runs innermost first, so the hop that lands on
1164
+ the root is the last one. */
1165
+ if (step === 2 && r.hops === 2) {
1166
+ /* Only at exactly two hops, where the table in the middle is also the one
1167
+ holding the column that reaches the root. Three hops out they are
1168
+ different tables, and `work_log through ticket.lead_employee_id` would
1169
+ name a column that is on `team` — a sentence that reads as fact and is
1170
+ not one. Those go straight to the full spelling below. */
1171
+ return `${name} through ${hopTarget(r.via[0])}.${hopColumns(r.via[r.via.length - 1])}`;
1172
+ }
1173
+ if (step === 2 || step === 3) {
1174
+ return `${name} through ${r.via.map((v) => hopColumns(v)).join(' \u2192 ')}`;
1175
+ }
1176
+ return `${name} (${r.path})`;
1177
+ }
1178
+ /** Columns of a table that a metric could summarise. `count` needs none. */
1179
+ export function metricColumns(schema, tableId) {
1180
+ const table = findTable(schema, tableId);
1181
+ if (!table)
1182
+ return [];
1183
+ /* Only the numeric ones. `sum` of a name is not a question, and offering it
1184
+ produces a page that fails at run time for a reason the builder knew at
1185
+ build time. */
1186
+ return table.columns.filter((c) => /int|numeric|decimal|real|double|float|money|serial/i.test(c.type)).map((c) => c.name);
1187
+ }