@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,869 @@
1
+ /*
2
+ * A page against a schema: the reference path a section follows from the
3
+ * root, the filter that pins it to one root row, and the list and metric
4
+ * views a section runs. Pure; nothing fetches.
5
+ */
6
+ import { columnOrRollup, findTable, labelColumn, primaryKey } from './schema.js';
7
+ import { MAX_PATH_DEPTH, hopName, parseViewQuery, validateView } from './view.js';
8
+ import { quoteValue, saidAt } from './query.js';
9
+ import { DEFAULT_PAGE_LIST_LIMIT, GROUPS_DEFAULT } from './pageModel.js';
10
+ import { cardsColumns } from './appCards.js';
11
+ import { tryResolvePath } from './viewPaths.js';
12
+ /**
13
+ * Follow `a.b.c` from a table, hop by hop, and say where it lands.
14
+ *
15
+ * Each segment names a foreign key on the current table by its canonical
16
+ * column(s), as in view paths (`a+b` for a composite), or by its constraint
17
+ * name, which tells apart two references over the same columns; a dotted
18
+ * constraint name is not a forward path segment.
19
+ */
20
+ export function resolvePath(schema, from, path) {
21
+ const segments = path
22
+ .split('.')
23
+ .map((s) => s.trim())
24
+ .filter(Boolean);
25
+ if (!segments.length)
26
+ return { error: 'A path cannot be empty.' };
27
+ if (segments.length > MAX_PATH_DEPTH) {
28
+ return { error: `"${path}" follows more than ${MAX_PATH_DEPTH} references.` };
29
+ }
30
+ let current = from;
31
+ const hops = [];
32
+ for (const segment of segments) {
33
+ const table = findTable(schema, current);
34
+ if (!table)
35
+ return { error: `"${current}" is not a table on this connection.` };
36
+ const outgoing = schema.foreignKeys.filter((fk) => fk.from.table === table.id);
37
+ const matches = outgoing.filter((fk) => hopName(fk) === segment || fk.name === segment);
38
+ if (matches.length > 1)
39
+ return { error: `"${segment}" is ambiguous on ${table.name} — ${matches.length} foreign keys match.` };
40
+ const hop = matches[0];
41
+ if (!hop) {
42
+ /* The commonest cause is a key the database does not declare: a text
43
+ column holding another table's code. Said as that, since "no
44
+ reference" beside a column the author can see reads as a typo. */
45
+ const column = table.columns.some((c) => c.name === segment);
46
+ return {
47
+ error: `"${table.name}" has no reference called "${segment}".` +
48
+ (column ? ` "${segment}" is a column of ${table.name}, but not a foreign key: paths follow declared foreign keys only.` : '') +
49
+ (outgoing.length ? ` It points at: ${outgoing.map((fk) => hopName(fk)).join(', ')}.` : ' It points at nothing.') +
50
+ ' See `mistri docs relationships`.',
51
+ };
52
+ }
53
+ hops.push(hop);
54
+ current = hop.to.table;
55
+ }
56
+ return { path: { hops, endsAt: current } };
57
+ }
58
+ /** Prefer column-based paths, retaining explicit names when columns alone
59
+ * cannot identify the relationship. Never emit a dotted name as one hop. */
60
+ export function pageHop(schema, fk) {
61
+ return [...new Set([hopName(fk), fk.name])].find((name) => {
62
+ const resolved = resolvePath(schema, fk.from.table, name).path;
63
+ return resolved?.hops.length === 1 && resolved.hops[0] === fk;
64
+ });
65
+ }
66
+ /**
67
+ * The filter that ties a section to one root record.
68
+ *
69
+ * Built as a structured `ViewFilter` rather than as text, for the reason the
70
+ * whole tool is built that way: a key value that happens to contain a quote
71
+ * or the word `and` is a value, and text would make it grammar.
72
+ *
73
+ * The path is written into the column, so the compiler does the joining —
74
+ * `invoice_id.customer_id` is a path it already knows how to walk, and the
75
+ * one place that knows how to join is better than a second one here.
76
+ */
77
+ export function rootFilter(path, key, resolved) {
78
+ const last = resolved.hops[resolved.hops.length - 1];
79
+ // The path's segments reach the view compiler as written: a constraint
80
+ // name tells apart two references sharing the same columns, and replacing
81
+ // it with a canonical hop would lose the choice.
82
+ const prefix = path
83
+ .split('.')
84
+ .map((segment) => segment.trim())
85
+ .filter(Boolean)
86
+ .slice(0, -1)
87
+ .join('.');
88
+ /* Every column of the root's key, compared through the same path. A
89
+ composite key compared on one column matches every row that shares it,
90
+ which on `invoice_line` is most of the table. */
91
+ /* A declared reference (a soft key) meets the root at another unique
92
+ column than its key: `product_sku` holds a product's `sku`, and the
93
+ page knows the product by `id`. Compared through the path instead, so
94
+ the join the reference already is finds the root by its own key. */
95
+ if (last.to.columns.some((column) => key[column] === undefined)) {
96
+ const whole = path
97
+ .split('.')
98
+ .map((segment) => segment.trim())
99
+ .filter(Boolean)
100
+ .join('.');
101
+ return { groups: [Object.entries(key).map(([column, value]) => ({ path: `${whole}.${column}`, op: '=', value }))] };
102
+ }
103
+ const conditions = last.to.columns.map((rootColumn, i) => {
104
+ const childColumn = last.from.columns[i] ?? last.from.columns[0];
105
+ return {
106
+ path: prefix ? `${prefix}.${childColumn}` : childColumn,
107
+ op: '=',
108
+ value: key[rootColumn],
109
+ };
110
+ });
111
+ return { groups: [conditions] };
112
+ }
113
+ /**
114
+ * Resolve a section's `of` — the forward reference naming which record the
115
+ * section is about.
116
+ *
117
+ * One hop, deliberately. `of: 'site_id.address_id'` is expressible in the
118
+ * type and refused here: each extra hop is another row the planner must read
119
+ * before any section can run, and nobody has yet asked for a section about
120
+ * the address of the site of the record they are looking at. The error says
121
+ * the limit rather than pretending the path is malformed.
122
+ */
123
+ export function resolveOf(schema, baseId, of) {
124
+ const resolved = resolvePath(schema, baseId, of);
125
+ if (resolved.error)
126
+ return { error: resolved.error };
127
+ if (resolved.path.hops.length !== 1) {
128
+ return { error: `"of" reaches one reference from ${baseId}; "${of}" is ${resolved.path.hops.length} hops.` };
129
+ }
130
+ const fk = resolved.path.hops[0];
131
+ const table = findTable(schema, resolved.path.endsAt);
132
+ if (!table)
133
+ return { error: `"${of}" leads to a table this connection does not have.` };
134
+ return { fk, table };
135
+ }
136
+ /**
137
+ * The section's own narrowing, ANDed onto the root's.
138
+ *
139
+ * `filter` has been in the model and in the config parser since the start and
140
+ * was never applied: a page saying `"filter": "status = open"` listed every
141
+ * order, silently. Documented, accepted, ignored — the worst shape a bug can
142
+ * take, because the file says the behaviour is there.
143
+ *
144
+ * Written in the query language and parsed against the *child*, which is what
145
+ * "recent open orders" means: the condition is about the order, not about the
146
+ * customer the page is on. Errors come back rather than throwing, so a
147
+ * mistyped filter names its section instead of blanking the page.
148
+ */
149
+ function withSectionFilter(schema, from, root, filter, now) {
150
+ if (!filter)
151
+ return { filter: root };
152
+ const parsed = parseViewQuery(schema, from, filter, now);
153
+ if (parsed.errors.length)
154
+ return { error: parsed.errors[0].message };
155
+ const extra = parsed.filter.groups;
156
+ if (!extra.length)
157
+ return { filter: root };
158
+ /* ANDed into every root group rather than appended as another group: groups
159
+ are OR-ed, so appending would have widened the result instead of
160
+ narrowing it — a filter that added rows. With one root group, which is
161
+ what `rootFilter` always builds, this is the ordinary case of "and also".
162
+
163
+ A filter with its own OR becomes a product, which is what AND-of-ORs
164
+ means when both sides have alternatives. */
165
+ const groups = [];
166
+ for (const rootGroup of root.groups) {
167
+ for (const extraGroup of extra)
168
+ groups.push([...rootGroup, ...extraGroup]);
169
+ }
170
+ return { filter: { groups } };
171
+ }
172
+ /* The engines' number types, by the names each gives its columns. */
173
+ const NUMERIC_TYPE = /int|real|num|dec|float|double|money/i;
174
+ /**
175
+ * What is wrong with a list's match (`same`, `near`) against this schema, or
176
+ * undefined. Said at startup (validatePage) and again when the list is
177
+ * planned, in the same words, so a page that loads is a page whose matched
178
+ * list can run. Each column is one of the listed table and of the page's own,
179
+ * the record's value being read off the page's row.
180
+ */
181
+ export function similarProblem(schema, page, section) {
182
+ const similar = section.similar;
183
+ if (!similar)
184
+ return undefined;
185
+ const table = findTable(schema, section.from);
186
+ if (!table)
187
+ return `"${section.from}" is not a table on this connection.`;
188
+ const base = findTable(schema, page.base);
189
+ const near = Object.keys(similar.near ?? {});
190
+ for (const column of [...similar.same, ...near]) {
191
+ if (!table.columns.some((c) => c.name === column))
192
+ return `"same" names "${column}", which is not a column on ${table.name}.`;
193
+ if (!base?.columns.some((c) => c.name === column)) {
194
+ return `"same" names "${column}", which the page's own table, ${base?.name ?? page.base}, does not have: a row shares it with the record the page is on.`;
195
+ }
196
+ }
197
+ for (const column of near) {
198
+ const type = table.columns.find((c) => c.name === column)?.type ?? '';
199
+ if (!NUMERIC_TYPE.test(type)) {
200
+ return `"near" keeps "${column}" near the record's value, so it must be a number column; it is ${type || 'untyped'}.`;
201
+ }
202
+ }
203
+ if (table.id === page.base && !primaryKey(table).length)
204
+ return `A match leaves out the record the page is on, and ${table.name} has no primary key to know it by.`;
205
+ return undefined;
206
+ }
207
+ /** Why `column` cannot be summed over `table`'s rows: not a column, rollup or computed column, or not a number. */
208
+ function numberColumnProblem(table, column, option, verb) {
209
+ const computed = table.computed && Object.hasOwn(table.computed, column) ? table.computed[column] : undefined;
210
+ const type = table.columns.find((c) => c.name === column)?.type ??
211
+ (table.rollups && Object.hasOwn(table.rollups, column) ? table.rollups[column].type : undefined) ??
212
+ (computed ? (computed.type === 'number' ? 'NUMERIC' : computed.type) : undefined);
213
+ if (type === undefined)
214
+ return `"${option}" names "${column}", which is not a column, rollup or computed column on ${table.name}.`;
215
+ if (!NUMERIC_TYPE.test(type))
216
+ return `"${option}" ${verb} "${column}", so it must be a number; it is ${type || 'untyped'}.`;
217
+ return undefined;
218
+ }
219
+ /**
220
+ * What is wrong with a related list's footer of sums (`totals`) against this
221
+ * schema, or undefined — said at startup and again when the list is planned.
222
+ * A total is a number column of the rows, or one of their numeric rollups or
223
+ * computed columns (`refunded`, the refunds of each order), through any
224
+ * reference `whose` walks.
225
+ */
226
+ export function relatedMatchProblem(schema, page, section) {
227
+ const table = findTable(schema, section.from);
228
+ if (!table)
229
+ return `"${section.from}" is not a table on this connection.`;
230
+ void page;
231
+ for (const column of section.totals ?? []) {
232
+ const problem = numberColumnProblem(table, column, 'totals', 'sums');
233
+ if (problem)
234
+ return problem;
235
+ }
236
+ return undefined;
237
+ }
238
+ /** The grouped record's columns a header says, as paths from the listed rows: read with each group's rows, so a header costs no read. */
239
+ export function groupFactPaths(section) {
240
+ const group = section.group;
241
+ return group?.show?.length ? group.show.map((column) => `${group.by}.${column}`) : [];
242
+ }
243
+ /** The table a group's header is a record of, or undefined where the groups are plain values (or the path does not resolve). */
244
+ export function groupTable(schema, section) {
245
+ if (!section.group)
246
+ return undefined;
247
+ const { resolved } = tryResolvePath(schema, section.from, section.group.by);
248
+ if (!resolved)
249
+ return undefined;
250
+ const { table, column } = resolved;
251
+ /* The declared key the column is the whole of: a leg of a composite key names no one record. */
252
+ const fk = schema.foreignKeys.find((k) => k.from.table === table.id && k.from.columns.length === 1 && k.from.columns[0] === column.name);
253
+ return fk ? findTable(schema, fk.to.table) : undefined;
254
+ }
255
+ /**
256
+ * What is wrong with a grouped list against this schema, or undefined — said at startup and again when the list is
257
+ * planned. The header is the record the rows point at through `by`; where `by` ends at a plain column the groups are
258
+ * its values, and there is no record to take facts or an order from.
259
+ */
260
+ export function groupProblem(schema, page, section) {
261
+ const group = section.group;
262
+ if (!group)
263
+ return undefined;
264
+ const table = findTable(schema, section.from);
265
+ if (!table)
266
+ return `"${section.from}" is not a table on this connection.`;
267
+ const found = tryResolvePath(schema, table.id, group.by);
268
+ if (!found.resolved)
269
+ return `"group.by" names "${group.by}", which is not a column of ${table.name} or a path from it. ${found.error?.message ?? ''}`.trim();
270
+ for (const column of group.sum ?? []) {
271
+ const problem = numberColumnProblem(table, column, 'group.sum', 'sums');
272
+ if (problem)
273
+ return problem;
274
+ }
275
+ const target = groupTable(schema, section);
276
+ if (target && target.id === page.base && !section.of)
277
+ return `"group.by" names "${group.by}", which is the record this page is on, so every row would fall in one group. Group by a record between them, such as the middle of the chain.`;
278
+ if (!target) {
279
+ if (group.show?.length)
280
+ return `"group.show" says facts of the record a group is, and "${group.by}" is a plain column: its groups are its values. Leave "show" out, or group by a reference.`;
281
+ if (group.sort)
282
+ return `"group.sort" orders by a column of the record a group is, and "${group.by}" is a plain column: its groups are values, largest first. Leave "sort" out, or group by a reference.`;
283
+ return undefined;
284
+ }
285
+ for (const name of group.show ?? []) {
286
+ if (!target.columns.some((c) => c.name === name) && !(target.computed && Object.hasOwn(target.computed, name)))
287
+ return `"group.show" names "${name}", which is not a column of ${target.name}, the record each group is.`;
288
+ }
289
+ if (group.sort && !target.columns.some((c) => c.name === group.sort.column))
290
+ return `"group.sort" names "${group.sort.column}", which is not a column of ${target.name}, the record each group is.`;
291
+ return undefined;
292
+ }
293
+ /**
294
+ * The groups of a grouped list: one row for each record (or value) the listed rows point at through `by`, with how
295
+ * many rows it holds, its subtotals when the list has `totals`, and, when the groups are ordered by a column of the
296
+ * grouped record, the largest of that column — the sort column depends on the key, so the maximum is exact. It is a
297
+ * plain breakdown over the same rows the list reads, so it adds no SQL shape of its own and the reader's grants,
298
+ * row policy and tenant scope narrow it as they narrow every breakdown.
299
+ */
300
+ export function groupsView(schema, page, section, key, ofKeys, now) {
301
+ const group = section.group;
302
+ if (!group)
303
+ return {};
304
+ const problem = groupProblem(schema, page, section) ?? relatedMatchProblem(schema, page, section);
305
+ if (problem)
306
+ return { error: problem };
307
+ const root = sectionRoot(schema, page, section, key, ofKeys);
308
+ if (root.error)
309
+ return { error: root.error };
310
+ if (root.empty)
311
+ return { empty: true };
312
+ const resolved = resolvePath(schema, section.from, section.path);
313
+ if (resolved.error)
314
+ return { error: resolved.error };
315
+ const table = findTable(schema, section.from);
316
+ const narrowed = withSectionFilter(schema, table.id, rootFilter(section.path, root.key, resolved.path), section.filter, now);
317
+ if (narrowed.error)
318
+ return { error: narrowed.error };
319
+ const sort = group.sort;
320
+ return {
321
+ breakdown: {
322
+ base: table.id,
323
+ by: [{ path: group.by }],
324
+ measures: [
325
+ { fn: 'count', alias: 'group_rows' },
326
+ ...(sort ? [{ fn: 'max', path: `${group.by}.${sort.column}`, alias: 'group_order' }] : []),
327
+ ...(section.totals ?? []).map((column, i) => ({ fn: 'sum', path: column, alias: `total_${i}` })),
328
+ ...(group.sum ?? []).map((column, i) => ({ fn: 'sum', path: column, alias: `group_sum_${i}` })),
329
+ ],
330
+ filter: narrowed.filter,
331
+ /* The rows with no value are one group, and they come last whatever the order: the breakdown is paged, so only
332
+ the query can keep them last on every page (a client could place them on the page they happen to fall on). */
333
+ orderBy: [
334
+ { name: group.by, direction: 'asc', emptyLast: true },
335
+ sort ? { name: 'group_order', direction: sort.direction } : { name: 'group_rows', direction: 'desc' },
336
+ ],
337
+ limit: group.limit ?? GROUPS_DEFAULT,
338
+ },
339
+ };
340
+ }
341
+ /**
342
+ * The sums a related list's footer says, as a breakdown with no key over the
343
+ * rows the list's reference matches — one row, a `sum` a column, as a
344
+ * narrowed page metric is measured — so the footer counts every row, not the
345
+ * page of them the list has loaded, and a rollup of the rows sums as a column
346
+ * does.
347
+ */
348
+ export function totalsView(schema, page, section, key, ofKeys, rootValues) {
349
+ if (!section.totals?.length)
350
+ return {};
351
+ /* A matched list sums the rows it shows: the same filter, built from the record's own values. */
352
+ if (section.similar) {
353
+ const problem = relatedMatchProblem(schema, page, section);
354
+ if (problem)
355
+ return { error: problem };
356
+ const matched = similarView(schema, page, section, key, 0, undefined, rootValues);
357
+ if (!matched.view)
358
+ return matched.empty ? { empty: true } : { error: matched.error };
359
+ return {
360
+ breakdown: {
361
+ base: matched.view.base,
362
+ by: [],
363
+ measures: section.totals.map((column, i) => ({ fn: 'sum', path: column, alias: `total_${i}` })),
364
+ filter: matched.view.filter,
365
+ limit: 1,
366
+ },
367
+ };
368
+ }
369
+ const root = sectionRoot(schema, page, section, key, ofKeys);
370
+ if (root.error)
371
+ return { error: root.error };
372
+ if (root.empty)
373
+ return { empty: true };
374
+ const resolved = resolvePath(schema, section.from, section.path);
375
+ if (resolved.error)
376
+ return { error: resolved.error };
377
+ const problem = relatedMatchProblem(schema, page, section);
378
+ if (problem)
379
+ return { error: problem };
380
+ const table = findTable(schema, section.from);
381
+ return {
382
+ breakdown: {
383
+ base: table.id,
384
+ by: [],
385
+ measures: section.totals.map((column, i) => ({ fn: 'sum', path: column, alias: `total_${i}` })),
386
+ filter: rootFilter(section.path, root.key, resolved.path),
387
+ limit: 1,
388
+ },
389
+ };
390
+ }
391
+ /**
392
+ * A matched list: rows that share the record's values — equal on `same`,
393
+ * within a fraction of the record's value on `near` — and, where they are
394
+ * rows of the page's own table, the record itself left out; closest first on
395
+ * the first `near` column, then the section's own sort, then the key, so equal
396
+ * distances still come back in one order.
397
+ *
398
+ * Built from values the caller read from the root row on the server, never
399
+ * from anything the client sent, and returned as an ordinary view, so the
400
+ * reader's grants, row policy, the public read ceiling and tenant scope
401
+ * narrow it exactly as they narrow any list. A value the record lacks — an
402
+ * empty `same`, a `near` that is not a number — empties the section: a
403
+ * "similar" list of everything, because this record's body type is blank,
404
+ * would be a list that lies.
405
+ */
406
+ function similarView(schema, page, section, key, index, now, rootValues) {
407
+ const problem = similarProblem(schema, page, section);
408
+ if (problem)
409
+ return { error: problem };
410
+ if (!rootValues)
411
+ return { error: 'The record\'s values for "same" were not read before planning.' };
412
+ const table = findTable(schema, section.from);
413
+ const similar = section.similar;
414
+ const conditions = [];
415
+ for (const column of similar.same) {
416
+ const value = rootValues[column];
417
+ if (value === null || value === undefined || (typeof value === 'string' && !value.trim()))
418
+ return { empty: true };
419
+ if (typeof value !== 'string' && typeof value !== 'number' && typeof value !== 'boolean')
420
+ return { empty: true };
421
+ conditions.push({ path: column, op: '=', value });
422
+ }
423
+ const closest = [];
424
+ for (const [column, fraction] of Object.entries(similar.near ?? {})) {
425
+ const raw = rootValues[column];
426
+ const value = typeof raw === 'number' ? raw : typeof raw === 'string' && raw.trim() ? Number(raw) : Number.NaN;
427
+ if (!Number.isFinite(value) || Math.abs(value) > Number.MAX_SAFE_INTEGER)
428
+ return { empty: true };
429
+ let [low, high] = [value * (1 - fraction), value * (1 + fraction)].sort((a, b) => a - b);
430
+ /* A whole-number column keeps whole-number bounds — the same rows, and a
431
+ question the query bar can read back ("price >= 22497"). */
432
+ if (/int/i.test(table.columns.find((c) => c.name === column)?.type ?? ''))
433
+ [low, high] = [Math.ceil(low), Math.floor(high)];
434
+ conditions.push({ path: column, op: '>=', value: low }, { path: column, op: '<=', value: high });
435
+ if (!closest.length)
436
+ closest.push({ path: column, direction: 'asc', near: value });
437
+ }
438
+ /* The record itself, left out as NOT (every key column equal): one OR-ed
439
+ group per key column, so a composite key leaves out exactly one row. Rows of another table are not it. */
440
+ const keyColumns = primaryKey(table);
441
+ const own = table.id === page.base;
442
+ if (own && keyColumns.some((column) => key[column] === undefined))
443
+ return { error: `The record's key does not name ${table.name}'s ${keyColumns.join(', ')}.` };
444
+ const groups = own ? keyColumns.map((column) => [...conditions, { path: column, op: '!=', value: key[column] }]) : [conditions];
445
+ const narrowed = withSectionFilter(schema, table.id, { groups }, section.filter, now);
446
+ if (narrowed.error)
447
+ return { error: narrowed.error };
448
+ const cardColumns = typeof section.presentation === 'object'
449
+ ? section.presentation.kind === 'custom'
450
+ ? [...section.presentation.show]
451
+ : cardsColumns(section.presentation)
452
+ : [];
453
+ const columns = [...new Set([...(section.columns?.length ? section.columns : defaultColumns(table, '')), ...cardColumns])].map((p) => ({
454
+ path: p,
455
+ }));
456
+ const sorted = section.orderBy?.map((o) => ({ path: o.column, direction: o.direction })) ?? [];
457
+ const tieBreak = keyColumns.filter((c) => !sorted.some((o) => o.path === c)).map((c) => ({ path: c, direction: 'asc' }));
458
+ return {
459
+ view: {
460
+ id: `page:${page.id}:${index}`,
461
+ name: section.title ?? table.name,
462
+ base: table.id,
463
+ columns,
464
+ filter: narrowed.filter,
465
+ orderBy: [...closest, ...sorted, ...tieBreak],
466
+ limit: section.limit ?? DEFAULT_PAGE_LIST_LIMIT,
467
+ },
468
+ };
469
+ }
470
+ /**
471
+ * What a list section needs to run, as a view the existing compiler can take.
472
+ *
473
+ * A page adds no query machinery of its own. Every section is a view — that
474
+ * is the point of having built one — so a page is an arrangement of views
475
+ * around a shared root rather than a second way to ask a database questions.
476
+ */
477
+ export function listView(schema, page, section, key, index, now, ofKeys, rootValues) {
478
+ /* A similar list walks no path: it is matched on the record's own values. */
479
+ if (section.similar)
480
+ return similarView(schema, page, section, key, index, now, rootValues);
481
+ const root = sectionRoot(schema, page, section, key, ofKeys);
482
+ if (root.error)
483
+ return { error: root.error };
484
+ if (root.empty)
485
+ return { empty: true };
486
+ const resolved = resolvePath(schema, section.from, section.path);
487
+ if (resolved.error)
488
+ return { error: resolved.error };
489
+ if (resolved.path.endsAt !== root.table) {
490
+ return {
491
+ error: `"${section.path}" leads from ${section.from} to ` + `${resolved.path.endsAt}, not to ${root.table}.`,
492
+ };
493
+ }
494
+ const table = findTable(schema, section.from);
495
+ if (!table)
496
+ return { error: `"${section.from}" is not a table on this connection.` };
497
+ /* Cards read their own columns (title, figure, badge, lines, summary)
498
+ beside whatever the section shows. */
499
+ const cardColumns = typeof section.presentation === 'object'
500
+ ? section.presentation.kind === 'custom'
501
+ ? [...section.presentation.show]
502
+ : cardsColumns(section.presentation)
503
+ : [];
504
+ /* A current card is titled by its row's name, which its facts leave out. */
505
+ const titled = section.presentation === 'card' ? [labelColumn(table)].filter((name) => Boolean(name)) : [];
506
+ const columns = [
507
+ ...new Set([
508
+ ...titled,
509
+ ...(section.columns?.length ? section.columns : defaultColumns(table, section.path, resolved.path)),
510
+ ...cardColumns,
511
+ ...groupFactPaths(section),
512
+ ]),
513
+ ].map((p) => ({ path: p }));
514
+ const joined = rootFilter(section.path, root.key, resolved.path);
515
+ const narrowed = withSectionFilter(schema, table.id, joined, section.filter, now);
516
+ if (narrowed.error)
517
+ return { error: narrowed.error };
518
+ return {
519
+ view: {
520
+ id: `page:${page.id}:${index}`,
521
+ name: section.title ?? table.name,
522
+ base: table.id,
523
+ columns,
524
+ filter: narrowed.filter,
525
+ orderBy: section.orderBy?.map((o) => ({ path: o.column, direction: o.direction })),
526
+ limit: section.limit ?? DEFAULT_PAGE_LIST_LIMIT,
527
+ },
528
+ };
529
+ }
530
+ /**
531
+ * The record a section is actually about: the root, or — through `of` — a
532
+ * record the root points at, whose key the planner has already read.
533
+ */
534
+ function sectionRoot(schema, page, section, key, ofKeys) {
535
+ if (!section.of)
536
+ return { table: page.base, key };
537
+ const of = resolveOf(schema, page.base, section.of);
538
+ if (of.error)
539
+ return { error: of.error };
540
+ const referenced = ofKeys?.[section.of];
541
+ if (referenced === undefined) {
542
+ /* The planner read the root row before building sections, so a missing
543
+ entry is the caller's bug, not the page's. Said as what it is. */
544
+ return { error: `The key for "${section.of}" was not resolved before planning.` };
545
+ }
546
+ if (referenced === null)
547
+ return { empty: true };
548
+ return { table: of.table.id, key: referenced };
549
+ }
550
+ /**
551
+ * What a metric section needs to run.
552
+ *
553
+ * Expressed as a one-row view over the *root* table with a single aggregate,
554
+ * rather than as a count over the child: the aggregate machinery already
555
+ * guarantees that two counts over different children cannot inflate each
556
+ * other, and doing it any other way here would give up that guarantee for a
557
+ * number nobody would think to check.
558
+ */
559
+ /**
560
+ * How the query bar would say what a section shows — when it can say it.
561
+ *
562
+ * A count on a page is a number, and a number you cannot get behind is a
563
+ * number you have to take on trust. Clicking one should open the rows it
564
+ * counted, narrowed the same way — and for a long time it opened the child
565
+ * table with no filter at all, because the plan carried no query and the
566
+ * client fell back to the table's own name. The count said 3 and the grid
567
+ * said 20.
568
+ *
569
+ * The query language has no dotted paths: `invoice_line invoice_id.customer_id
570
+ * = 4` is not a thing it can parse, and a page two hops out produces exactly
571
+ * that. So this answers `undefined` rather than something that will not run —
572
+ * the caller opens the composer on the view itself in that case, which can
573
+ * say it. The distinction is deliberate: it is better to be told the language
574
+ * cannot express this than to be shown a filter that quietly is not the one.
575
+ *
576
+ * `is empty` for a null test, because that is the grammar's spelling; `in` is
577
+ * left out, since a list needs bracket syntax the parser writes differently
578
+ * from how it reads it.
579
+ */
580
+ const QUERY_OPS = {
581
+ '=': '=',
582
+ '!=': '!=',
583
+ '<': '<',
584
+ '<=': '<=',
585
+ '>': '>',
586
+ '>=': '>=',
587
+ contains: 'contains',
588
+ startsWith: 'startswith',
589
+ endsWith: 'endswith',
590
+ };
591
+ export function sectionQuery(view) {
592
+ if (!view?.base)
593
+ return undefined;
594
+ const groups = view.filter?.groups?.filter((g) => g.length) ?? [];
595
+ if (!groups.length)
596
+ return view.base;
597
+ const parts = [];
598
+ for (const group of groups) {
599
+ const conditions = [];
600
+ for (const c of group) {
601
+ /* A path, not a column: the language filters the table in front of it
602
+ and nothing further out. */
603
+ if (!c.path || c.path.includes('.'))
604
+ return undefined;
605
+ if (c.op === 'isNull' || c.op === 'isNotNull') {
606
+ conditions.push(`${c.path} is ${c.op === 'isNull' ? '' : 'not '}empty`);
607
+ continue;
608
+ }
609
+ const op = QUERY_OPS[c.op];
610
+ if (c.other !== undefined) {
611
+ if (!op || c.other.includes('.'))
612
+ return undefined;
613
+ conditions.push(`${c.path} ${op} ${c.other}`);
614
+ continue;
615
+ }
616
+ if (!op || c.value === undefined || c.value === null || Array.isArray(c.value))
617
+ return undefined;
618
+ conditions.push(`${c.path} ${op} ${quoteValue(c.value)}`);
619
+ }
620
+ parts.push(conditions.join(' and '));
621
+ }
622
+ return `${view.base} ${parts.join(' or ')}`;
623
+ }
624
+ export function metricView(schema, page, section, key, index, now, ofKeys) {
625
+ const root = sectionRoot(schema, page, section, key, ofKeys);
626
+ if (root.error)
627
+ return { error: root.error };
628
+ if (root.empty)
629
+ return { empty: true, counts: section.fn === 'count' };
630
+ const resolved = resolvePath(schema, section.from, section.path);
631
+ if (resolved.error)
632
+ return { error: resolved.error };
633
+ if (resolved.path.endsAt !== root.table) {
634
+ return {
635
+ error: `"${section.path}" leads from ${section.from} to ` + `${resolved.path.endsAt}, not to ${root.table}.`,
636
+ };
637
+ }
638
+ if (section.fn !== 'count' && !section.column) {
639
+ return { error: `"${section.fn}" needs a column to ${section.fn}.` };
640
+ }
641
+ const table = findTable(schema, section.from);
642
+ if (!table)
643
+ return { error: `"${section.from}" is not a table on this connection.` };
644
+ if (section.column && !columnOrRollup(table, section.column)) {
645
+ return { error: `"${table.name}" has no column called "${section.column}".` };
646
+ }
647
+ /* The record this section is about — the page's base, or the table `of`
648
+ reaches. The aggregate view below anchors on it, so a sum "of its site"
649
+ is a one-row view over site exactly as a sum over the root is one over
650
+ the root. */
651
+ const base = findTable(schema, root.table);
652
+ if (!base)
653
+ return { error: `"${root.table}" is not a table on this connection.` };
654
+ /**
655
+ * A count is a filtered count of the child, not an aggregate on the root.
656
+ *
657
+ * It used to be an aggregate, with `via` taken from the *first* hop — which
658
+ * is the right constraint only when there is one. For anything further out
659
+ * the first hop names a key that does not point at the root at all: on a
660
+ * customer page, `invoice_line via invoice_id.customer_id` handed the
661
+ * aggregate `line_invoice_fk`, which joins lines to invoices and knows
662
+ * nothing about customers. The number that came back was not the number
663
+ * asked for, and looked like one.
664
+ *
665
+ * Counting the child directly is correct at any depth, and it is what the
666
+ * list beside it already does — so a count and the list it summarises can
667
+ * no longer disagree. The view returns one row and its `total`, which is
668
+ * exact: a view only ever joins to-one, so counting it is counting the
669
+ * table it started from.
670
+ */
671
+ const narrowed = withSectionFilter(schema, table.id, rootFilter(section.path, root.key, resolved.path), section.filter, now);
672
+ if (narrowed.error)
673
+ return { error: narrowed.error };
674
+ /* The rows it measures, as the list beside it would show them: what a
675
+ count reads its total from, and what any metric reopens as. */
676
+ const rowsView = {
677
+ id: `page:${page.id}:${index}`,
678
+ name: section.title ?? table.name,
679
+ base: table.id,
680
+ columns: (primaryKey(table)[0] ? [primaryKey(table)[0]] : [table.columns[0].name]).map((path) => ({ path })),
681
+ filter: narrowed.filter,
682
+ limit: 1,
683
+ };
684
+ if (section.fn === 'count')
685
+ return { counts: true, view: rowsView };
686
+ /* A narrowed `sum`, `avg`, `min` or `max` — or one further out than one
687
+ hop — is measured the way a count is counted: over the child's own rows,
688
+ filtered by the path to the root and the section's query, as a
689
+ breakdown with no key, which answers one row. That is correct at any
690
+ depth for the same reason the count is (a path only joins to-one), and
691
+ it is the same filter the list beside it reads, so the figure and the
692
+ list cannot disagree. The aggregate compiler below reaches one hop and
693
+ takes no filter; it was refusing both, which `mistri check` could not
694
+ see coming. */
695
+ /* A rollup of the child (rollups.ts) is not a column the one-hop
696
+ aggregate below can read, so it is measured this way too. */
697
+ const rollup = Boolean(section.column && !table.columns.some((c) => c.name === section.column));
698
+ if (section.filter || resolved.path.hops.length > 1 || rollup) {
699
+ return {
700
+ view: rowsView,
701
+ breakdown: {
702
+ base: table.id,
703
+ by: [],
704
+ measures: [{ fn: section.fn, path: section.column, alias: 'value' }],
705
+ filter: narrowed.filter,
706
+ limit: 1,
707
+ },
708
+ };
709
+ }
710
+ return {
711
+ view: {
712
+ id: `page:${page.id}:${index}`,
713
+ name: section.title ?? `${section.fn} of ${table.name}`,
714
+ base: base.id,
715
+ columns: primaryKey(base).map((c) => ({ path: c })),
716
+ aggregates: [
717
+ {
718
+ fn: section.fn,
719
+ via: `${table.id}.${resolved.path.hops[0].from.columns.join('+')}`,
720
+ column: section.column,
721
+ alias: 'value',
722
+ },
723
+ ],
724
+ filter: { groups: [primaryKey(base).map((c) => ({ path: c, op: '=', value: root.key[c] }))] },
725
+ limit: 1,
726
+ },
727
+ };
728
+ }
729
+ /**
730
+ * Columns worth showing in a list nobody has configured.
731
+ *
732
+ * The path back to the root is dropped: every row in the section has the same
733
+ * value for it, so a column of forty identical numbers is the one column that
734
+ * cannot tell two rows apart. The key and the label come first, because they
735
+ * are what a row is called.
736
+ */
737
+ export function defaultColumns(table, path, resolved) {
738
+ /* The columns the first hop actually uses, not the segment that named it.
739
+
740
+ A saved path segment can be a constraint name, so splitting the
741
+ path for a column name found `address_country_fk`, matched nothing, and
742
+ left `country_code` in the table: a column reading NZ on every row of a
743
+ section that is *about* NZ. The one column that cannot tell two rows
744
+ apart, kept in the one place it is guaranteed useless. */
745
+ const pathHead = resolved ? resolved.hops[0].from.columns : [path.split('.')[0]];
746
+ const key = primaryKey(table);
747
+ const label = labelColumn(table);
748
+ const ordered = [
749
+ ...key,
750
+ ...(label && !key.includes(label) ? [label] : []),
751
+ ...table.columns.map((c) => c.name).filter((n) => !key.includes(n) && n !== label),
752
+ ];
753
+ return ordered.filter((name) => !pathHead.includes(name)).slice(0, 6);
754
+ }
755
+ /* ---------- a custom section's reads ---------- */
756
+ /**
757
+ * One declared read of a custom section, planned as the list it is: with
758
+ * `whose`, exactly a related list (listView), the same join back to the
759
+ * record; without, the resource's own rows narrowed by its query — the
760
+ * dashboard's rows section, on a page. Either way the result is a view the
761
+ * server compiles and authorizes on every run, like any section's.
762
+ */
763
+ export function customReadView(schema, page, name, read, key, index, now) {
764
+ if (read.path) {
765
+ const built = listView(schema, page, {
766
+ kind: 'list',
767
+ from: read.from,
768
+ path: read.path,
769
+ columns: read.columns,
770
+ filter: read.filter,
771
+ orderBy: read.orderBy,
772
+ limit: read.limit ?? DEFAULT_PAGE_LIST_LIMIT,
773
+ }, key, index, now);
774
+ return built.view ? { ...built, view: { ...built.view, id: `page:${page.id}:${index}:${name}` } } : built;
775
+ }
776
+ const table = findTable(schema, read.from);
777
+ if (!table)
778
+ return { error: `"${read.from}" is not a table on this connection.` };
779
+ let filter;
780
+ if (read.filter) {
781
+ const parsed = parseViewQuery(schema, table.id, read.filter, now);
782
+ if (parsed.errors.length)
783
+ return { error: parsed.errors[0].message };
784
+ if (parsed.filter.groups.length)
785
+ filter = parsed.filter;
786
+ }
787
+ return {
788
+ view: {
789
+ id: `page:${page.id}:${index}:${name}`,
790
+ name,
791
+ base: table.id,
792
+ columns: read.columns.map((path) => ({ path })),
793
+ ...(filter ? { filter } : {}),
794
+ orderBy: read.orderBy?.map((o) => ({ path: o.column, direction: o.direction })),
795
+ limit: read.limit ?? DEFAULT_PAGE_LIST_LIMIT,
796
+ },
797
+ };
798
+ }
799
+ /**
800
+ * What is wrong with a custom section against this schema, or undefined:
801
+ * the record's fields judged as a fields section's, each read as the list it
802
+ * is planned as — its `whose` leading home, its columns, order and query
803
+ * through the compiler that will run them. Said at startup, so a misspelled
804
+ * column is refused before a reader sees the section.
805
+ */
806
+ export function customProblem(schema, page, section, index) {
807
+ if (section.record?.length) {
808
+ const error = validateView(schema, {
809
+ id: `page:${page.id}:${index}:record`,
810
+ name: 'record',
811
+ base: page.base,
812
+ columns: section.record.map((path) => ({ path })),
813
+ });
814
+ if (error)
815
+ return `"record": ${error}`;
816
+ }
817
+ for (const [name, read] of Object.entries(section.reads ?? {})) {
818
+ const here = `rows "${name}"`;
819
+ const table = findTable(schema, read.from);
820
+ if (!table)
821
+ return `${here}: "${read.from}" is not a table on this connection.`;
822
+ if (read.path) {
823
+ const resolved = resolvePath(schema, read.from, read.path);
824
+ if (resolved.error)
825
+ return `${here}: ${resolved.error}`;
826
+ if (resolved.path.endsAt !== page.base)
827
+ return `${here}: "${read.path}" leads from ${read.from} to ${resolved.path.endsAt}, not to ${page.base}.`;
828
+ }
829
+ if (read.filter) {
830
+ const parsed = parseViewQuery(schema, table.id, read.filter);
831
+ if (parsed.errors.length)
832
+ return `${here}: ${saidAt(parsed.errors[0], read.filter.trim())}`;
833
+ }
834
+ const error = validateView(schema, {
835
+ id: `page:${page.id}:${index}:${name}`,
836
+ name,
837
+ base: table.id,
838
+ columns: read.columns.map((path) => ({ path })),
839
+ orderBy: read.orderBy?.map((o) => ({ path: o.column, direction: o.direction })),
840
+ });
841
+ if (error)
842
+ return `${here}: ${error}`;
843
+ }
844
+ return undefined;
845
+ }
846
+ /* ---------- captions ---------- */
847
+ /** The hop every path shares, where each is one field past it (`dealer_id.name`, `dealer_id.city` → `dealer_id`), or ''. */
848
+ export function sharedPrefix(paths) {
849
+ if (!paths?.length)
850
+ return '';
851
+ const first = paths[0];
852
+ if (!first.includes('.'))
853
+ return '';
854
+ const prefix = first.slice(0, first.lastIndexOf('.'));
855
+ return paths.every((p) => p.startsWith(`${prefix}.`) && !p.slice(prefix.length + 1).includes('.')) ? prefix : '';
856
+ }
857
+ /**
858
+ * The hop a card's facts are captioned without. A card's title is the record
859
+ * (`bike_id`, the reference itself) and its facts are that record's own fields
860
+ * (`bike_id.make`, `bike_id.serial_number`): read with the title, the paths
861
+ * share no hop, and a fact that is the parent's label column was captioned as
862
+ * the reference ("Bike" over a serial number). Its own name is what it is there.
863
+ */
864
+ export function cardStem(paths) {
865
+ const [title, ...facts] = paths ?? [];
866
+ const stem = sharedPrefix(facts);
867
+ /* Only where the title is that record: beside a title of its own, `dealer_id.name` is still "Dealer". */
868
+ return stem && (title === stem || title.startsWith(`${stem}.`)) ? stem : sharedPrefix(paths);
869
+ }