@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.
- package/LICENSE +21 -0
- package/README.md +17 -2
- package/dist/adapters/adapter.d.ts +693 -0
- package/dist/adapters/adapter.js +444 -0
- package/dist/adapters/cache/cacheStore.d.ts +75 -0
- package/dist/adapters/cache/cacheStore.js +160 -0
- package/dist/adapters/cache/resp.d.ts +52 -0
- package/dist/adapters/cache/resp.js +237 -0
- package/dist/adapters/commands/canonicalJson.d.ts +19 -0
- package/dist/adapters/commands/canonicalJson.js +154 -0
- package/dist/adapters/connect.d.ts +29 -0
- package/dist/adapters/connect.js +40 -0
- package/dist/adapters/connectionBudget.d.ts +101 -0
- package/dist/adapters/connectionBudget.js +128 -0
- package/dist/adapters/dbLatency.d.ts +35 -0
- package/dist/adapters/dbLatency.js +79 -0
- package/dist/adapters/epochCache.d.ts +50 -0
- package/dist/adapters/epochCache.js +85 -0
- package/dist/adapters/http/egress.d.ts +48 -0
- package/dist/adapters/http/egress.js +175 -0
- package/dist/adapters/http/httpAdapter.d.ts +71 -0
- package/dist/adapters/http/httpAdapter.js +512 -0
- package/dist/adapters/http/httpDescription.d.ts +32 -0
- package/dist/adapters/http/httpDescription.js +246 -0
- package/dist/adapters/http/openapi.d.ts +98 -0
- package/dist/adapters/http/openapi.js +487 -0
- package/dist/adapters/layoutRefusal.d.ts +24 -0
- package/dist/adapters/layoutRefusal.js +35 -0
- package/dist/adapters/net/networkPolicy.d.ts +44 -0
- package/dist/adapters/net/networkPolicy.js +123 -0
- package/dist/adapters/net/untrustedDatabase.d.ts +18 -0
- package/dist/adapters/net/untrustedDatabase.js +120 -0
- package/dist/adapters/poolAdmission.d.ts +43 -0
- package/dist/adapters/poolAdmission.js +184 -0
- package/dist/adapters/poolMetrics.d.ts +121 -0
- package/dist/adapters/poolMetrics.js +185 -0
- package/dist/adapters/postgres.d.ts +436 -0
- package/dist/adapters/postgres.js +1890 -0
- package/dist/adapters/postgresCatalog.d.ts +96 -0
- package/dist/adapters/postgresCatalog.js +387 -0
- package/dist/adapters/postgresCatalogDigest.d.ts +30 -0
- package/dist/adapters/postgresCatalogDigest.js +87 -0
- package/dist/adapters/postgresCatalogRows.d.ts +62 -0
- package/dist/adapters/postgresCatalogRows.js +37 -0
- package/dist/adapters/postgresDates.d.ts +1 -0
- package/dist/adapters/postgresDates.js +39 -0
- package/dist/adapters/postgresMaintenance.d.ts +33 -0
- package/dist/adapters/postgresMaintenance.js +179 -0
- package/dist/adapters/postgresPool.d.ts +255 -0
- package/dist/adapters/postgresPool.js +838 -0
- package/dist/adapters/postgresPooler.d.ts +28 -0
- package/dist/adapters/postgresPooler.js +267 -0
- package/dist/adapters/postgresReadCatalog.d.ts +31 -0
- package/dist/adapters/postgresReadCatalog.js +226 -0
- package/dist/adapters/postgresReadSnapshot.d.ts +46 -0
- package/dist/adapters/postgresReadSnapshot.js +133 -0
- package/dist/adapters/postgresSchemaEpoch.d.ts +24 -0
- package/dist/adapters/postgresSchemaEpoch.js +149 -0
- package/dist/adapters/postgresTransactionHandle.d.ts +173 -0
- package/dist/adapters/postgresTransactionHandle.js +524 -0
- package/dist/adapters/postgresValues.d.ts +30 -0
- package/dist/adapters/postgresValues.js +75 -0
- package/dist/adapters/replicas.d.ts +70 -0
- package/dist/adapters/replicas.js +208 -0
- package/dist/adapters/reservedSql.d.ts +38 -0
- package/dist/adapters/reservedSql.js +264 -0
- package/dist/adapters/rolePrivileges.d.ts +17 -0
- package/dist/adapters/rolePrivileges.js +22 -0
- package/dist/adapters/scoped/keyRows.d.ts +65 -0
- package/dist/adapters/scoped/keyRows.js +173 -0
- package/dist/adapters/scoped/postgresScopeCatalog.d.ts +16 -0
- package/dist/adapters/scoped/postgresScopeCatalog.js +117 -0
- package/dist/adapters/scoped/scopedCommandOperations.d.ts +5 -0
- package/dist/adapters/scoped/scopedCommandOperations.js +48 -0
- package/dist/adapters/scoped/scopedReads.d.ts +20 -0
- package/dist/adapters/scoped/scopedReads.js +174 -0
- package/dist/adapters/scoped/scopedTransaction.d.ts +89 -0
- package/dist/adapters/scoped/scopedTransaction.js +303 -0
- package/dist/adapters/scoped/sqliteScopeCatalog.d.ts +16 -0
- package/dist/adapters/scoped/sqliteScopeCatalog.js +241 -0
- package/dist/adapters/scoped/updateGuard.d.ts +3 -0
- package/dist/adapters/scoped/updateGuard.js +40 -0
- package/dist/adapters/serverSettings.d.ts +45 -0
- package/dist/adapters/serverSettings.js +46 -0
- package/dist/adapters/sqlite.d.ts +262 -0
- package/dist/adapters/sqlite.js +1434 -0
- package/dist/adapters/sqliteCostlyReads.d.ts +33 -0
- package/dist/adapters/sqliteCostlyReads.js +49 -0
- package/dist/adapters/sqliteDecimals.d.ts +22 -0
- package/dist/adapters/sqliteDecimals.js +57 -0
- package/dist/adapters/sqliteHandles.d.ts +43 -0
- package/dist/adapters/sqliteHandles.js +143 -0
- package/dist/adapters/sqliteStatementProcess.d.ts +29 -0
- package/dist/adapters/sqliteStatementProcess.js +131 -0
- package/dist/adapters/statementControl.d.ts +34 -0
- package/dist/adapters/statementControl.js +90 -0
- package/dist/adapters/storeHost.d.ts +130 -0
- package/dist/adapters/storeHost.js +13 -0
- package/dist/adapters/telemetry/drivers.d.ts +21 -0
- package/dist/adapters/telemetry/drivers.js +321 -0
- package/dist/adapters/telemetry/metrics.d.ts +63 -0
- package/dist/adapters/telemetry/metrics.js +122 -0
- package/dist/adapters/telemetry/otlp.d.ts +12 -0
- package/dist/adapters/telemetry/otlp.js +320 -0
- package/dist/adapters/telemetry/otlpEncode.d.ts +48 -0
- package/dist/adapters/telemetry/otlpEncode.js +366 -0
- package/dist/adapters/telemetry/otlpEnv.d.ts +1 -0
- package/dist/adapters/telemetry/otlpEnv.js +12 -0
- package/dist/adapters/telemetry/serverTiming.d.ts +83 -0
- package/dist/adapters/telemetry/serverTiming.js +503 -0
- package/dist/adapters/telemetry/start.d.ts +10 -0
- package/dist/adapters/telemetry/start.js +25 -0
- package/dist/adapters/telemetry/statement.d.ts +16 -0
- package/dist/adapters/telemetry/statement.js +134 -0
- package/dist/adapters/telemetry/trace.d.ts +127 -0
- package/dist/adapters/telemetry/trace.js +295 -0
- package/dist/adapters/writeAhead.d.ts +27 -0
- package/dist/adapters/writeAhead.js +46 -0
- package/dist/adapters/writeAttribution.d.ts +18 -0
- package/dist/adapters/writeAttribution.js +23 -0
- package/dist/adapters/writeCapture.d.ts +73 -0
- package/dist/adapters/writeCapture.js +20 -0
- package/dist/adapters/writeLanes.d.ts +94 -0
- package/dist/adapters/writeLanes.js +232 -0
- package/dist/engine.d.ts +15 -0
- package/dist/engine.js +13 -0
- package/dist/server/auth/credentials.d.ts +61 -0
- package/dist/server/auth/credentials.js +260 -0
- package/dist/server/cacheService.d.ts +50 -0
- package/dist/server/cacheService.js +110 -0
- package/dist/server/cli/args.d.ts +176 -0
- package/dist/server/cli/args.js +342 -0
- package/dist/server/cli/bin.d.ts +5 -0
- package/dist/server/cli/bin.js +66 -0
- package/dist/server/cli/browser.d.ts +15 -0
- package/dist/server/cli/browser.js +37 -0
- package/dist/server/cli/config.d.ts +50 -0
- package/dist/server/cli/config.js +206 -0
- package/dist/server/cli/startup.d.ts +36 -0
- package/dist/server/cli/startup.js +128 -0
- package/dist/server/cli/startupOptions.d.ts +63 -0
- package/dist/server/cli/startupOptions.js +143 -0
- package/dist/server/cli/subcommand.d.ts +28 -0
- package/dist/server/cli/subcommand.js +32 -0
- package/dist/server/cli/usage.d.ts +13 -0
- package/dist/server/cli/usage.js +31 -0
- package/dist/server/clientHttp.d.ts +184 -0
- package/dist/server/clientHttp.js +700 -0
- package/dist/server/clientSurface.d.ts +128 -0
- package/dist/server/clientSurface.js +182 -0
- package/dist/server/columnGrants.d.ts +103 -0
- package/dist/server/columnGrants.js +730 -0
- package/dist/server/compileCache.d.ts +1 -0
- package/dist/server/compileCache.js +30 -0
- package/dist/server/config/locations.d.ts +1 -0
- package/dist/server/config/locations.js +15 -0
- package/dist/server/configPaths.d.ts +7 -0
- package/dist/server/configPaths.js +34 -0
- package/dist/server/configSections.d.ts +64 -0
- package/dist/server/configSections.js +72 -0
- package/dist/server/connectionRedaction.d.ts +30 -0
- package/dist/server/connectionRedaction.js +77 -0
- package/dist/server/connections.d.ts +353 -0
- package/dist/server/connections.js +978 -0
- package/dist/server/constraints.d.ts +82 -0
- package/dist/server/constraints.js +169 -0
- package/dist/server/demo.d.ts +9 -0
- package/dist/server/demo.js +554 -0
- package/dist/server/devclient.d.ts +108 -0
- package/dist/server/devclient.js +361 -0
- package/dist/server/evidence.d.ts +55 -0
- package/dist/server/evidence.js +323 -0
- package/dist/server/hiddenColumns.d.ts +32 -0
- package/dist/server/hiddenColumns.js +238 -0
- package/dist/server/http/baselineHeaders.d.ts +24 -0
- package/dist/server/http/baselineHeaders.js +12 -0
- package/dist/server/http/captured.d.ts +35 -0
- package/dist/server/http/captured.js +73 -0
- package/dist/server/http/clock.d.ts +1 -0
- package/dist/server/http/clock.js +14 -0
- package/dist/server/http/connectionSource.d.ts +4 -0
- package/dist/server/http/connectionSource.js +48 -0
- package/dist/server/http/context.d.ts +189 -0
- package/dist/server/http/context.js +11 -0
- package/dist/server/http/crawlerText.d.ts +15 -0
- package/dist/server/http/crawlerText.js +32 -0
- package/dist/server/http/dataRoutes.d.ts +88 -0
- package/dist/server/http/dataRoutes.js +137 -0
- package/dist/server/http/door.d.ts +57 -0
- package/dist/server/http/door.js +39 -0
- package/dist/server/http/exportRoute.d.ts +15 -0
- package/dist/server/http/exportRoute.js +320 -0
- package/dist/server/http/firstPaint.d.ts +108 -0
- package/dist/server/http/firstPaint.js +262 -0
- package/dist/server/http/httpOperationRoutes.d.ts +11 -0
- package/dist/server/http/httpOperationRoutes.js +50 -0
- package/dist/server/http/pageReads.d.ts +20 -0
- package/dist/server/http/pageReads.js +205 -0
- package/dist/server/http/pageRoutes.d.ts +39 -0
- package/dist/server/http/pageRoutes.js +291 -0
- package/dist/server/http/probes.d.ts +22 -0
- package/dist/server/http/probes.js +27 -0
- package/dist/server/http/rateLimit.d.ts +131 -0
- package/dist/server/http/rateLimit.js +351 -0
- package/dist/server/http/readRoutes.d.ts +9 -0
- package/dist/server/http/readRoutes.js +131 -0
- package/dist/server/http/readRouting.d.ts +20 -0
- package/dist/server/http/readRouting.js +80 -0
- package/dist/server/http/recordCompose.d.ts +67 -0
- package/dist/server/http/recordCompose.js +167 -0
- package/dist/server/http/recordWrites.d.ts +89 -0
- package/dist/server/http/recordWrites.js +298 -0
- package/dist/server/http/refusalReport.d.ts +38 -0
- package/dist/server/http/refusalReport.js +63 -0
- package/dist/server/http/reply.d.ts +130 -0
- package/dist/server/http/reply.js +394 -0
- package/dist/server/http/requestOrigin.d.ts +11 -0
- package/dist/server/http/requestOrigin.js +8 -0
- package/dist/server/http/router.d.ts +45 -0
- package/dist/server/http/router.js +22 -0
- package/dist/server/http/serverCore.d.ts +68 -0
- package/dist/server/http/serverCore.js +177 -0
- package/dist/server/http/traceRequest.d.ts +30 -0
- package/dist/server/http/traceRequest.js +224 -0
- package/dist/server/http/trust.d.ts +261 -0
- package/dist/server/http/trust.js +472 -0
- package/dist/server/http/writeRefusal.d.ts +33 -0
- package/dist/server/http/writeRefusal.js +24 -0
- package/dist/server/httpConnection.d.ts +49 -0
- package/dist/server/httpConnection.js +170 -0
- package/dist/server/impact.d.ts +55 -0
- package/dist/server/impact.js +47 -0
- package/dist/server/journal.d.ts +134 -0
- package/dist/server/journal.js +398 -0
- package/dist/server/keysetReadHttp.d.ts +52 -0
- package/dist/server/keysetReadHttp.js +85 -0
- package/dist/server/layouts.d.ts +49 -0
- package/dist/server/layouts.js +192 -0
- package/dist/server/migrate/cli.d.ts +55 -0
- package/dist/server/migrate/cli.js +517 -0
- package/dist/server/migrate/database.d.ts +37 -0
- package/dist/server/migrate/database.js +247 -0
- package/dist/server/migrate/desired.d.ts +76 -0
- package/dist/server/migrate/desired.js +292 -0
- package/dist/server/migrate/files.d.ts +34 -0
- package/dist/server/migrate/files.js +141 -0
- package/dist/server/migrate/flyway.d.ts +7 -0
- package/dist/server/migrate/flyway.js +69 -0
- package/dist/server/migrate/history.d.ts +70 -0
- package/dist/server/migrate/history.js +211 -0
- package/dist/server/migrate/runner.d.ts +100 -0
- package/dist/server/migrate/runner.js +409 -0
- package/dist/server/migrate/snapshot.d.ts +30 -0
- package/dist/server/migrate/snapshot.js +239 -0
- package/dist/server/migrate/target.d.ts +33 -0
- package/dist/server/migrate/target.js +82 -0
- package/dist/server/openapiDraft.d.ts +25 -0
- package/dist/server/openapiDraft.js +171 -0
- package/dist/server/operations/diagnostics.d.ts +101 -0
- package/dist/server/operations/diagnostics.js +284 -0
- package/dist/server/operations/requestMetrics.d.ts +33 -0
- package/dist/server/operations/requestMetrics.js +94 -0
- package/dist/server/packageVersion.d.ts +1 -0
- package/dist/server/packageVersion.js +11 -0
- package/dist/server/poolConfig.d.ts +34 -0
- package/dist/server/poolConfig.js +114 -0
- package/dist/server/product.d.ts +20 -0
- package/dist/server/product.js +12 -0
- package/dist/server/recordHistoryOperations.d.ts +39 -0
- package/dist/server/recordHistoryOperations.js +43 -0
- package/dist/server/recordWriteOperations.d.ts +111 -0
- package/dist/server/recordWriteOperations.js +329 -0
- package/dist/server/resourceReadHttp.d.ts +81 -0
- package/dist/server/resourceReadHttp.js +606 -0
- package/dist/server/resourceReadOperations.d.ts +147 -0
- package/dist/server/resourceReadOperations.js +452 -0
- package/dist/server/scaffoldMachine.d.ts +54 -0
- package/dist/server/scaffoldMachine.js +184 -0
- package/dist/server/seed/cli.d.ts +20 -0
- package/dist/server/seed/cli.js +164 -0
- package/dist/server/seed/lexicon.d.ts +26 -0
- package/dist/server/seed/lexicon.js +585 -0
- package/dist/server/seed/seed.d.ts +70 -0
- package/dist/server/seed/seed.js +779 -0
- package/dist/server/seed/words.d.ts +21 -0
- package/dist/server/seed/words.js +164 -0
- package/dist/server/serverRequests.d.ts +6 -0
- package/dist/server/serverRequests.js +33 -0
- package/dist/server/serverTimeouts.d.ts +33 -0
- package/dist/server/serverTimeouts.js +32 -0
- package/dist/server/shapeonly.d.ts +32 -0
- package/dist/server/shapeonly.js +110 -0
- package/dist/server/startupWarnings.d.ts +32 -0
- package/dist/server/startupWarnings.js +92 -0
- package/dist/shared/addressParams.d.ts +28 -0
- package/dist/shared/addressParams.js +47 -0
- package/dist/shared/appCards.d.ts +123 -0
- package/dist/shared/appCards.js +385 -0
- package/dist/shared/appComponents.d.ts +177 -0
- package/dist/shared/appComponents.js +422 -0
- package/dist/shared/appDocuments.d.ts +43 -0
- package/dist/shared/appDocuments.js +131 -0
- package/dist/shared/appEmbeds.d.ts +21 -0
- package/dist/shared/appEmbeds.js +24 -0
- package/dist/shared/appIcons.d.ts +28 -0
- package/dist/shared/appIcons.js +223 -0
- package/dist/shared/appLinks.d.ts +17 -0
- package/dist/shared/appLinks.js +124 -0
- package/dist/shared/appListColumns.d.ts +3 -0
- package/dist/shared/appListColumns.js +66 -0
- package/dist/shared/appLocale.d.ts +36 -0
- package/dist/shared/appLocale.js +86 -0
- package/dist/shared/appMessages.d.ts +44 -0
- package/dist/shared/appMessages.js +78 -0
- package/dist/shared/appNoun.d.ts +21 -0
- package/dist/shared/appNoun.js +32 -0
- package/dist/shared/autoBucket.d.ts +98 -0
- package/dist/shared/autoBucket.js +305 -0
- package/dist/shared/autoValues.d.ts +25 -0
- package/dist/shared/autoValues.js +25 -0
- package/dist/shared/breakdown.d.ts +228 -0
- package/dist/shared/breakdown.js +557 -0
- package/dist/shared/breakdowntext.d.ts +41 -0
- package/dist/shared/breakdowntext.js +400 -0
- package/dist/shared/builtInThemes.d.ts +2 -0
- package/dist/shared/builtInThemes.js +771 -0
- package/dist/shared/chartSeparation.d.ts +11 -0
- package/dist/shared/chartSeparation.js +58 -0
- package/dist/shared/clauses.d.ts +22 -0
- package/dist/shared/clauses.js +97 -0
- package/dist/shared/columnCaption.d.ts +16 -0
- package/dist/shared/columnCaption.js +90 -0
- package/dist/shared/commandFieldPath.d.ts +2 -0
- package/dist/shared/commandFieldPath.js +7 -0
- package/dist/shared/commandWire.d.ts +26 -0
- package/dist/shared/commandWire.js +41 -0
- package/dist/shared/computed.d.ts +161 -0
- package/dist/shared/computed.js +378 -0
- package/dist/shared/computedParse.d.ts +13 -0
- package/dist/shared/computedParse.js +278 -0
- package/dist/shared/computedValue.d.ts +47 -0
- package/dist/shared/computedValue.js +191 -0
- package/dist/shared/connection.d.ts +30 -0
- package/dist/shared/connection.js +1 -0
- package/dist/shared/csv.d.ts +20 -0
- package/dist/shared/csv.js +63 -0
- package/dist/shared/dashLayout.d.ts +30 -0
- package/dist/shared/dashLayout.js +132 -0
- package/dist/shared/dashboardAsks.d.ts +93 -0
- package/dist/shared/dashboardAsks.js +129 -0
- package/dist/shared/dashboardTrend.d.ts +20 -0
- package/dist/shared/dashboardTrend.js +50 -0
- package/dist/shared/declaredReferences.d.ts +50 -0
- package/dist/shared/declaredReferences.js +202 -0
- package/dist/shared/definedFields.d.ts +63 -0
- package/dist/shared/definedFields.js +103 -0
- package/dist/shared/definedValues.d.ts +63 -0
- package/dist/shared/definedValues.js +184 -0
- package/dist/shared/displayDefaults.d.ts +135 -0
- package/dist/shared/displayDefaults.js +529 -0
- package/dist/shared/dueDate.d.ts +6 -0
- package/dist/shared/dueDate.js +6 -0
- package/dist/shared/duration.d.ts +4 -0
- package/dist/shared/duration.js +23 -0
- package/dist/shared/fileBounds.d.ts +9 -0
- package/dist/shared/fileBounds.js +9 -0
- package/dist/shared/filter.d.ts +217 -0
- package/dist/shared/filter.js +305 -0
- package/dist/shared/firstPaint.d.ts +24 -0
- package/dist/shared/firstPaint.js +33 -0
- package/dist/shared/form.d.ts +21 -0
- package/dist/shared/form.js +25 -0
- package/dist/shared/formValues.d.ts +104 -0
- package/dist/shared/formValues.js +352 -0
- package/dist/shared/funnel.d.ts +45 -0
- package/dist/shared/funnel.js +108 -0
- package/dist/shared/httpOperations.d.ts +48 -0
- package/dist/shared/httpOperations.js +9 -0
- package/dist/shared/iconGlyphs.d.ts +4 -0
- package/dist/shared/iconGlyphs.js +151 -0
- package/dist/shared/iconWeights.d.ts +3 -0
- package/dist/shared/iconWeights.js +162 -0
- package/dist/shared/imageWidths.d.ts +32 -0
- package/dist/shared/imageWidths.js +38 -0
- package/dist/shared/keysetPage.d.ts +64 -0
- package/dist/shared/keysetPage.js +161 -0
- package/dist/shared/like.d.ts +20 -0
- package/dist/shared/like.js +29 -0
- package/dist/shared/listEmpty.d.ts +12 -0
- package/dist/shared/listEmpty.js +36 -0
- package/dist/shared/listFilters.d.ts +28 -0
- package/dist/shared/listFilters.js +84 -0
- package/dist/shared/listPresentations.d.ts +199 -0
- package/dist/shared/listPresentations.js +7 -0
- package/dist/shared/listQuestion.d.ts +52 -0
- package/dist/shared/listQuestion.js +137 -0
- package/dist/shared/lookPalettes.d.ts +62 -0
- package/dist/shared/lookPalettes.js +306 -0
- package/dist/shared/maskQuoted.d.ts +24 -0
- package/dist/shared/maskQuoted.js +48 -0
- package/dist/shared/mentionLabel.d.ts +19 -0
- package/dist/shared/mentionLabel.js +38 -0
- package/dist/shared/mentionLookup.d.ts +17 -0
- package/dist/shared/mentionLookup.js +64 -0
- package/dist/shared/menuCounts.d.ts +16 -0
- package/dist/shared/menuCounts.js +74 -0
- package/dist/shared/model.d.ts +79 -0
- package/dist/shared/model.js +153 -0
- package/dist/shared/money.d.ts +44 -0
- package/dist/shared/money.js +81 -0
- package/dist/shared/moneyEntry.d.ts +7 -0
- package/dist/shared/moneyEntry.js +27 -0
- package/dist/shared/mount.d.ts +118 -0
- package/dist/shared/mount.js +248 -0
- package/dist/shared/niceTicks.d.ts +26 -0
- package/dist/shared/niceTicks.js +50 -0
- package/dist/shared/order.d.ts +59 -0
- package/dist/shared/order.js +133 -0
- package/dist/shared/page.d.ts +135 -0
- package/dist/shared/page.js +1187 -0
- package/dist/shared/pageAnatomy.d.ts +99 -0
- package/dist/shared/pageAnatomy.js +370 -0
- package/dist/shared/pageConfig.d.ts +25 -0
- package/dist/shared/pageConfig.js +1308 -0
- package/dist/shared/pageEmptyActions.d.ts +12 -0
- package/dist/shared/pageEmptyActions.js +20 -0
- package/dist/shared/pageLayouts.d.ts +72 -0
- package/dist/shared/pageLayouts.js +205 -0
- package/dist/shared/pageModel.d.ts +785 -0
- package/dist/shared/pageModel.js +121 -0
- package/dist/shared/pagePaths.d.ts +180 -0
- package/dist/shared/pagePaths.js +869 -0
- package/dist/shared/pagePlans.d.ts +52 -0
- package/dist/shared/pagePlans.js +108 -0
- package/dist/shared/pageRoutes.d.ts +9 -0
- package/dist/shared/pageRoutes.js +9 -0
- package/dist/shared/query.d.ts +166 -0
- package/dist/shared/query.js +1302 -0
- package/dist/shared/queryDates.d.ts +12 -0
- package/dist/shared/queryDates.js +101 -0
- package/dist/shared/queryOperands.d.ts +99 -0
- package/dist/shared/queryOperands.js +131 -0
- package/dist/shared/reads.d.ts +103 -0
- package/dist/shared/reads.js +154 -0
- package/dist/shared/recordLabel.d.ts +51 -0
- package/dist/shared/recordLabel.js +123 -0
- package/dist/shared/recordReadKeys.d.ts +30 -0
- package/dist/shared/recordReadKeys.js +51 -0
- package/dist/shared/recordReads.d.ts +268 -0
- package/dist/shared/recordReads.js +368 -0
- package/dist/shared/recordview.d.ts +198 -0
- package/dist/shared/recordview.js +356 -0
- package/dist/shared/referenceModel.d.ts +58 -0
- package/dist/shared/referenceModel.js +194 -0
- package/dist/shared/refusal.d.ts +64 -0
- package/dist/shared/refusal.js +144 -0
- package/dist/shared/relatedCreate.d.ts +30 -0
- package/dist/shared/relatedCreate.js +162 -0
- package/dist/shared/resourceHttp.d.ts +464 -0
- package/dist/shared/resourceHttp.js +1 -0
- package/dist/shared/richText.d.ts +85 -0
- package/dist/shared/richText.js +305 -0
- package/dist/shared/rollups.d.ts +143 -0
- package/dist/shared/rollups.js +64 -0
- package/dist/shared/rowScope.d.ts +72 -0
- package/dist/shared/rowScope.js +376 -0
- package/dist/shared/runtimeTables.d.ts +44 -0
- package/dist/shared/runtimeTables.js +79 -0
- package/dist/shared/schema.d.ts +523 -0
- package/dist/shared/schema.js +627 -0
- package/dist/shared/schematext.d.ts +38 -0
- package/dist/shared/schematext.js +101 -0
- package/dist/shared/scopeCatalog.d.ts +29 -0
- package/dist/shared/scopeCatalog.js +468 -0
- package/dist/shared/servedApp.d.ts +681 -0
- package/dist/shared/servedApp.js +45 -0
- package/dist/shared/sessionLease.d.ts +34 -0
- package/dist/shared/sessionLease.js +3 -0
- package/dist/shared/shape.d.ts +55 -0
- package/dist/shared/shape.js +168 -0
- package/dist/shared/slug.d.ts +1 -0
- package/dist/shared/slug.js +12 -0
- package/dist/shared/sortText.d.ts +14 -0
- package/dist/shared/sortText.js +26 -0
- package/dist/shared/sqlRelation.d.ts +4 -0
- package/dist/shared/sqlRelation.js +8 -0
- package/dist/shared/sqlsaved.d.ts +44 -0
- package/dist/shared/sqlsaved.js +63 -0
- package/dist/shared/stitchedPaths.d.ts +27 -0
- package/dist/shared/stitchedPaths.js +38 -0
- package/dist/shared/structuredFilter.d.ts +22 -0
- package/dist/shared/structuredFilter.js +1 -0
- package/dist/shared/subtitlePath.d.ts +20 -0
- package/dist/shared/subtitlePath.js +24 -0
- package/dist/shared/suggest.d.ts +18 -0
- package/dist/shared/suggest.js +59 -0
- package/dist/shared/tableSizing.d.ts +4 -0
- package/dist/shared/tableSizing.js +9 -0
- package/dist/shared/tagLists.d.ts +57 -0
- package/dist/shared/tagLists.js +34 -0
- package/dist/shared/textRules.d.ts +54 -0
- package/dist/shared/textRules.js +133 -0
- package/dist/shared/theme.d.ts +94 -0
- package/dist/shared/theme.js +800 -0
- package/dist/shared/themeColor.d.ts +34 -0
- package/dist/shared/themeColor.js +109 -0
- package/dist/shared/themeFaces.d.ts +30 -0
- package/dist/shared/themeFaces.js +42 -0
- package/dist/shared/themeInk.d.ts +20 -0
- package/dist/shared/themeInk.js +52 -0
- package/dist/shared/themeLooks.d.ts +9 -0
- package/dist/shared/themeLooks.js +572 -0
- package/dist/shared/themeParts.d.ts +26 -0
- package/dist/shared/themeParts.js +71 -0
- package/dist/shared/themePartsCheck.d.ts +21 -0
- package/dist/shared/themePartsCheck.js +91 -0
- package/dist/shared/themePresets.d.ts +85 -0
- package/dist/shared/themePresets.js +139 -0
- package/dist/shared/themeRecipeWords.d.ts +156 -0
- package/dist/shared/themeRecipeWords.js +156 -0
- package/dist/shared/themeRecipes.d.ts +213 -0
- package/dist/shared/themeRecipes.js +234 -0
- package/dist/shared/themeResolve.d.ts +55 -0
- package/dist/shared/themeResolve.js +52 -0
- package/dist/shared/themeTokens.d.ts +107 -0
- package/dist/shared/themeTokens.js +116 -0
- package/dist/shared/themeType.d.ts +38 -0
- package/dist/shared/themeType.js +205 -0
- package/dist/shared/unitWord.d.ts +4 -0
- package/dist/shared/unitWord.js +37 -0
- package/dist/shared/view.d.ts +195 -0
- package/dist/shared/view.js +987 -0
- package/dist/shared/viewLabels.d.ts +47 -0
- package/dist/shared/viewLabels.js +459 -0
- package/dist/shared/viewPaths.d.ts +248 -0
- package/dist/shared/viewPaths.js +294 -0
- package/dist/shared/viewtext.d.ts +57 -0
- package/dist/shared/viewtext.js +282 -0
- package/dist/shared/vocabulary.d.ts +71 -0
- package/dist/shared/vocabulary.js +249 -0
- package/dist/shared/weekStart.d.ts +7 -0
- package/dist/shared/weekStart.js +36 -0
- package/mark.svg +1 -0
- package/package.json +261 -4
- package/schema/model.d.ts +298 -0
- 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
|
+
}
|