@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,987 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Multi-table views, built out of **reference paths**.
|
|
3
|
+
*
|
|
4
|
+
* The `Filter` in filter.ts addresses exactly one table and must keep doing
|
|
5
|
+
* so: it is the shape every read goes through, and widening it to carry joins
|
|
6
|
+
* would put join-building into every code path that filters anything. So the
|
|
7
|
+
* multi-table story is a separate idea rather than a bigger filter, and the
|
|
8
|
+
* idea is dot-walking:
|
|
9
|
+
*
|
|
10
|
+
* invoice customer_id.name contains Harbour
|
|
11
|
+
* work_order assigned_to.last_name = Petrova
|
|
12
|
+
* and customer_id.country_code.name = Australia
|
|
13
|
+
*
|
|
14
|
+
* A path is a chain of foreign keys starting at the base table, ending at a
|
|
15
|
+
* column. `customer_id.name` means "follow invoice.customer_id to customer,
|
|
16
|
+
* then take name". Nothing about it is guessed: the foreign key graph is
|
|
17
|
+
* already introspected, so every hop is derivable, checkable, and reversible
|
|
18
|
+
* into SQL — a path compiles to a LEFT JOIN chain and a qualified column.
|
|
19
|
+
*
|
|
20
|
+
* One mechanism answers three separate questions. Filtering across a long
|
|
21
|
+
* walk, showing a field from a related table, and the composer UI are all the
|
|
22
|
+
* same thing seen from three angles; the composer is a visual editor for the
|
|
23
|
+
* path set.
|
|
24
|
+
*
|
|
25
|
+
* Two properties are load-bearing and are pinned by tests:
|
|
26
|
+
*
|
|
27
|
+
* - **LEFT JOIN, never INNER.** A null foreign key must leave the base row
|
|
28
|
+
* in the result with nulls beside it. A composer that quietly drops rows
|
|
29
|
+
* is worse than no composer at all, because the number it shows you is
|
|
30
|
+
* wrong in a direction you cannot see.
|
|
31
|
+
* - **Every value is a bound parameter.** Only identifiers are ever quoted
|
|
32
|
+
* into the text, and they are quoted by the dialect's `SqlStyle`. The
|
|
33
|
+
* WHERE clause is still built by `buildWhere`, which now takes an
|
|
34
|
+
* identifier resolver so a path can become `"customer_1"."name"` without
|
|
35
|
+
* a second predicate builder existing to disagree with the first.
|
|
36
|
+
*/
|
|
37
|
+
import { ANSI_STYLE, buildWhere, collate, orderClauseSql, } from './filter.js';
|
|
38
|
+
import { clampLimit, clampOffset, countStatement } from './reads.js';
|
|
39
|
+
import { aggregateInput, declaredOrder, findTable, } from './schema.js';
|
|
40
|
+
import { ANY_CONDITION, anyConditions, expandSegments, parseQuery, splitClauseTail } from './query.js';
|
|
41
|
+
import { stitchedListProblem } from './stitchedPaths.js';
|
|
42
|
+
import { qualifiedTable } from './sqlRelation.js';
|
|
43
|
+
import { ScopedRelations } from './rowScope.js';
|
|
44
|
+
import { slug } from './slug.js';
|
|
45
|
+
import { maskQuoted } from './maskQuoted.js';
|
|
46
|
+
import { PathError, AGGREGATE_FNS, resolveAggregate, tryResolvePath, labelColumnMeta, backrefsTo, } from './viewPaths.js';
|
|
47
|
+
import { viewPaths, withViewComputed, withViewLabels } from './viewLabels.js';
|
|
48
|
+
import { rollupColumnMeta } from './rollups.js';
|
|
49
|
+
import { TAGS_COLUMN, tagsColumnMeta } from './tagLists.js';
|
|
50
|
+
import { computedColumnMeta } from './computed.js';
|
|
51
|
+
import { withViewReferences } from './declaredReferences.js';
|
|
52
|
+
export { withViewComputed, withViewLabels } from './viewLabels.js';
|
|
53
|
+
export { MAX_PATH_DEPTH, PathError, hopName, hopsFrom, resolvePath, backrefsTo, resolveAggregate, tableAtPrefix, tryResolvePath, } from './viewPaths.js';
|
|
54
|
+
/* ---------- compilation ---------- */
|
|
55
|
+
/* A view's filter as a column filter, for `buildWhere`. A condition's
|
|
56
|
+
collation travels with it: dropped here, a keyset comparison would fall
|
|
57
|
+
back to the column's collation while its ORDER BY kept the binary one. */
|
|
58
|
+
function viewConditions(view) {
|
|
59
|
+
return view.filter?.groups.length
|
|
60
|
+
? {
|
|
61
|
+
groups: view.filter.groups.map((group) => group.map((c) => ({
|
|
62
|
+
column: c.path,
|
|
63
|
+
op: c.op,
|
|
64
|
+
value: c.value,
|
|
65
|
+
...(c.collation === undefined ? {} : { collation: c.collation }),
|
|
66
|
+
...(c.other === undefined ? {} : { other: c.other }),
|
|
67
|
+
...(c.any === undefined ? {} : { any: c.any }),
|
|
68
|
+
}))),
|
|
69
|
+
}
|
|
70
|
+
: undefined;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Alias generation.
|
|
74
|
+
*
|
|
75
|
+
* Two requirements pull in opposite directions. Aliases must never collide —
|
|
76
|
+
* `work_order` in the demo has three separate references to `employee`, so a
|
|
77
|
+
* scheme keyed on the target table alone produces three joins all called
|
|
78
|
+
* `employee` and a statement that does not parse. And they must be stable, so
|
|
79
|
+
* the same view compiles to the same SQL every time and a diff of the explain
|
|
80
|
+
* panel means something.
|
|
81
|
+
*
|
|
82
|
+
* Both fall out of keying on the *path prefix* rather than the table: a prefix
|
|
83
|
+
* identifies a join uniquely, so the same path used twice reuses one join and
|
|
84
|
+
* two paths through the same table get two. The readable table name is kept as
|
|
85
|
+
* a prefix and made unique by an ordinal assigned in first-use order.
|
|
86
|
+
*/
|
|
87
|
+
export class JoinPlan {
|
|
88
|
+
schema;
|
|
89
|
+
style;
|
|
90
|
+
relation;
|
|
91
|
+
joins = [];
|
|
92
|
+
byPrefix = new Map();
|
|
93
|
+
n = 0;
|
|
94
|
+
constructor(schema, base, style, relation = (id) => qualifiedTable(schema, id, style)) {
|
|
95
|
+
this.schema = schema;
|
|
96
|
+
this.style = style;
|
|
97
|
+
this.relation = relation;
|
|
98
|
+
this.byPrefix.set('', alias(base.name, 0));
|
|
99
|
+
}
|
|
100
|
+
get baseAlias() {
|
|
101
|
+
return this.byPrefix.get('');
|
|
102
|
+
}
|
|
103
|
+
/** The alias for a path prefix, creating the join chain if it is new. */
|
|
104
|
+
aliasFor(hops, hopNames) {
|
|
105
|
+
let prefix = '';
|
|
106
|
+
for (let i = 0; i < hops.length; i++) {
|
|
107
|
+
prefix = prefix ? `${prefix}.${hopNames[i]}` : hopNames[i];
|
|
108
|
+
/* Already planned: the same path used twice must reuse one join, not
|
|
109
|
+
add a second copy of the same table under a different alias. */
|
|
110
|
+
if (this.byPrefix.has(prefix))
|
|
111
|
+
continue;
|
|
112
|
+
const target = findTable(this.schema, hops[i].to.table);
|
|
113
|
+
this.byPrefix.set(prefix, alias(target.name, ++this.n));
|
|
114
|
+
this.joins.push({ alias: this.byPrefix.get(prefix), table: target.id, path: prefix });
|
|
115
|
+
}
|
|
116
|
+
return this.byPrefix.get(prefix) ?? this.baseAlias;
|
|
117
|
+
}
|
|
118
|
+
/** The fully qualified, quoted SQL for a resolved path's final column. */
|
|
119
|
+
qualify(resolved) {
|
|
120
|
+
const owner = this.aliasFor(resolved.hops, resolved.hopNames);
|
|
121
|
+
return `${this.style.quote(owner)}.${this.style.quote(resolved.column.name)}`;
|
|
122
|
+
}
|
|
123
|
+
/** The relation a table is read as in this statement: its scoped CTE, or the table itself. */
|
|
124
|
+
relationOf(id) {
|
|
125
|
+
return this.relation(id);
|
|
126
|
+
}
|
|
127
|
+
/** The alias of a path prefix that has already been planned, or the base. */
|
|
128
|
+
aliasOfPrefix(prefix) {
|
|
129
|
+
return this.byPrefix.get(prefix) ?? this.baseAlias;
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* A fresh alias for something that is not a join — a subquery's table.
|
|
133
|
+
*
|
|
134
|
+
* It draws from the same counter as the joins, which is what guarantees a
|
|
135
|
+
* correlated subquery over `employee` cannot pick the alias a LEFT JOIN
|
|
136
|
+
* over `employee` is already using. Two `employee_3`s in one statement is
|
|
137
|
+
* a self-join nobody asked for, and the wrong answer rather than an error.
|
|
138
|
+
*/
|
|
139
|
+
subAlias(name) {
|
|
140
|
+
return alias(name, ++this.n);
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* The JOIN clauses, in the order the aliases were created.
|
|
144
|
+
*
|
|
145
|
+
* Order matters for more than tidiness: a join may only reference an alias
|
|
146
|
+
* introduced before it, and creation order is exactly path order, so a
|
|
147
|
+
* nested hop always follows the hop it hangs off.
|
|
148
|
+
*/
|
|
149
|
+
clauses() {
|
|
150
|
+
return this.joins.map((join) => {
|
|
151
|
+
const parts = join.path.split('.');
|
|
152
|
+
const parentPrefix = parts.slice(0, -1).join('.');
|
|
153
|
+
const parent = this.byPrefix.get(parentPrefix);
|
|
154
|
+
const fk = this.fkFor(join.path);
|
|
155
|
+
const target = findTable(this.schema, join.table);
|
|
156
|
+
/* Every column of the key, not just the first. A composite foreign key
|
|
157
|
+
joined on one column is a cross product wearing a join's clothes. */
|
|
158
|
+
const on = fk.to.columns
|
|
159
|
+
.map((toCol, i) => `${this.style.quote(join.alias)}.${this.style.quote(toCol)} = ` +
|
|
160
|
+
`${this.style.quote(parent)}.${this.style.quote(fk.from.columns[i] ?? fk.from.columns[0])}`)
|
|
161
|
+
.join(' AND ');
|
|
162
|
+
/* LEFT, always. An INNER JOIN here would drop every base row whose
|
|
163
|
+
foreign key is null — silently, and in a direction that looks like
|
|
164
|
+
the data simply is not there. */
|
|
165
|
+
return ` LEFT JOIN ${this.relation(target.id)} ${this.style.quote(join.alias)} ON ${on}`;
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
fkChain = new Map();
|
|
169
|
+
/** Remember which key produced each join, so `clauses` can rebuild the ON. */
|
|
170
|
+
record(path, fk) {
|
|
171
|
+
if (!this.fkChain.has(path))
|
|
172
|
+
this.fkChain.set(path, fk);
|
|
173
|
+
}
|
|
174
|
+
fkFor(path) {
|
|
175
|
+
return this.fkChain.get(path);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
/** Alias text: the table's own name, made unique by an ordinal. */
|
|
179
|
+
function alias(name, n) {
|
|
180
|
+
/* Truncated because Postgres silently cuts identifiers at 63 bytes, and a
|
|
181
|
+
silent cut is exactly how two long table names become one alias. */
|
|
182
|
+
const safe = name.replace(/[^A-Za-z0-9_]/g, '_').slice(0, 40) || 't';
|
|
183
|
+
return `${safe}_${n}`;
|
|
184
|
+
}
|
|
185
|
+
/**
|
|
186
|
+
* Compile a view to one parameterised SELECT.
|
|
187
|
+
*
|
|
188
|
+
* Order of work is deliberate and is what makes the output deterministic:
|
|
189
|
+
* columns, then the filter, then the sort. Aliases are handed out on first
|
|
190
|
+
* use, so the same view always produces the same statement.
|
|
191
|
+
*/
|
|
192
|
+
export function compileView(schema, view, style, offset = 0, scope) {
|
|
193
|
+
// The App's soft keys, where this catalog is the database's own (declaredReferences.ts).
|
|
194
|
+
schema = withViewReferences(schema, view.references);
|
|
195
|
+
const base = findTable(schema, view.base);
|
|
196
|
+
if (!base) {
|
|
197
|
+
throw new PathError(`No table called "${view.base}" in this database, so the view has nothing to start from.`, view.base, view.base, 0);
|
|
198
|
+
}
|
|
199
|
+
const relations = new ScopedRelations(schema, style, scope);
|
|
200
|
+
const baseRelation = relations.table(base.id);
|
|
201
|
+
const plan = new JoinPlan(schema, base, style, relations.table);
|
|
202
|
+
const { resolve, qualify, membership, exists } = viewPaths(schema, view, plan, style, relations);
|
|
203
|
+
/* An empty column list means the base table's own columns. Expanding it
|
|
204
|
+
here rather than emitting `SELECT *` keeps the result's column list
|
|
205
|
+
knowable without running the statement, which is what the composer's
|
|
206
|
+
field checklist reads. */
|
|
207
|
+
const wanted = view.columns?.length ? view.columns : base.columns.map((c) => ({ path: c.name }));
|
|
208
|
+
const seen = new Map();
|
|
209
|
+
const columns = [];
|
|
210
|
+
const select = [];
|
|
211
|
+
for (const col of wanted) {
|
|
212
|
+
const resolved = resolve(col.path);
|
|
213
|
+
const sql = qualify(col.path);
|
|
214
|
+
/* Two paths can legitimately end on the same name — `customer_id.name`
|
|
215
|
+
and `sold_by.name` are both "name" if aliased carelessly. The path is
|
|
216
|
+
already unique, so it is the default; an explicit collision gets a
|
|
217
|
+
numeric suffix rather than one column quietly overwriting the other. */
|
|
218
|
+
let name = col.alias?.trim() || resolved.path;
|
|
219
|
+
const count = seen.get(name) ?? 0;
|
|
220
|
+
seen.set(name, count + 1);
|
|
221
|
+
if (count)
|
|
222
|
+
name = `${name}_${count + 1}`;
|
|
223
|
+
columns.push({
|
|
224
|
+
path: resolved.path,
|
|
225
|
+
name,
|
|
226
|
+
table: resolved.table.id,
|
|
227
|
+
column: resolved.column.name,
|
|
228
|
+
type: resolved.column.type,
|
|
229
|
+
/* A column on a joined table is nullable in the result even when the
|
|
230
|
+
column itself is NOT NULL: the LEFT JOIN can produce no row at all.
|
|
231
|
+
Saying otherwise would have the UI render a real null as an error. */
|
|
232
|
+
nullable: resolved.column.nullable || resolved.hops.length > 0,
|
|
233
|
+
primaryKey: resolved.column.primaryKey,
|
|
234
|
+
hops: resolved.hops.length,
|
|
235
|
+
// A rollup reads like a column and is a summary: nothing edits it.
|
|
236
|
+
...(resolved.rollup && !resolved.rollup.through ? { aggregate: resolved.rollup.fn } : {}),
|
|
237
|
+
});
|
|
238
|
+
select.push(`${sql} AS ${style.quote(name)}`);
|
|
239
|
+
}
|
|
240
|
+
/* Aggregates come after the plain columns, and after them on purpose: a
|
|
241
|
+
summary hanging off a joined table needs that join's alias to exist, and
|
|
242
|
+
resolving columns first is what puts it there. */
|
|
243
|
+
for (const aggregate of view.aggregates ?? []) {
|
|
244
|
+
const resolved = resolveAggregate(schema, view.base, aggregate);
|
|
245
|
+
/* The parent side is a path prefix, so an aggregate can hang off a joined
|
|
246
|
+
table as easily as off the base — "invoice lines on the invoice this
|
|
247
|
+
shipment belongs to" is the same construction one hop along. */
|
|
248
|
+
if (resolved.on) {
|
|
249
|
+
/* Qualifying any column on the parent is what plans the joins leading
|
|
250
|
+
to it and records their keys. Doing it through the same `qualify`
|
|
251
|
+
the columns use is deliberate: two ways of planning a join is two
|
|
252
|
+
ways for the aliases to disagree. */
|
|
253
|
+
qualify(`${resolved.on}.${resolved.parent.columns[0].name}`);
|
|
254
|
+
}
|
|
255
|
+
const parentAlias = resolved.on ? plan.aliasOfPrefix(resolved.on) : plan.baseAlias;
|
|
256
|
+
const childAlias = plan.subAlias(resolved.child.name);
|
|
257
|
+
const on = resolved.fk.to.columns
|
|
258
|
+
.map((toCol, i) => `${style.quote(childAlias)}.${style.quote(resolved.fk.from.columns[i] ?? resolved.fk.from.columns[0])} = ` +
|
|
259
|
+
`${style.quote(parentAlias)}.${style.quote(toCol)}`)
|
|
260
|
+
.join(' AND ');
|
|
261
|
+
/* COUNT(*) rather than COUNT(column): a column count skips nulls, which
|
|
262
|
+
turns "how many parts" into "how many parts have a quantity" without
|
|
263
|
+
saying so. SUM over no rows is left as NULL rather than coerced to 0 —
|
|
264
|
+
a work order with no parts has no total, and 0 is a claim about money
|
|
265
|
+
that nobody made. */
|
|
266
|
+
const expression = resolved.aggregate.fn === 'count'
|
|
267
|
+
? 'COUNT(*)'
|
|
268
|
+
: `${resolved.aggregate.fn.toUpperCase()}(${aggregateInput(`${style.quote(childAlias)}.${style.quote(resolved.column.name)}`, resolved.column.type, style.dialect)})`;
|
|
269
|
+
let name = resolved.name;
|
|
270
|
+
const count = seen.get(name) ?? 0;
|
|
271
|
+
seen.set(name, count + 1);
|
|
272
|
+
if (count)
|
|
273
|
+
name = `${name}_${count + 1}`;
|
|
274
|
+
select.push(`(SELECT ${expression} FROM ${relations.table(resolved.child.id)} ${style.quote(childAlias)}` +
|
|
275
|
+
` WHERE ${on}) AS ${style.quote(name)}`);
|
|
276
|
+
columns.push({
|
|
277
|
+
path: resolved.name,
|
|
278
|
+
name,
|
|
279
|
+
table: resolved.child.id,
|
|
280
|
+
column: resolved.column?.name ?? '*',
|
|
281
|
+
type: resolved.aggregate.fn === 'count' ? 'integer' : (resolved.column?.type ?? 'integer'),
|
|
282
|
+
// COUNT is never null; every other summary is, over an empty child set.
|
|
283
|
+
nullable: resolved.aggregate.fn !== 'count',
|
|
284
|
+
primaryKey: false,
|
|
285
|
+
hops: resolved.on ? resolved.on.split('.').length : 0,
|
|
286
|
+
aggregate: resolved.aggregate.fn,
|
|
287
|
+
});
|
|
288
|
+
}
|
|
289
|
+
/* The filter is compiled by `buildWhere`, not beside it. One predicate
|
|
290
|
+
builder means one place where a value could stop being a parameter. */
|
|
291
|
+
const conditions = viewConditions(view);
|
|
292
|
+
// Preserve first-use alias ordering, but discover filter and sort joins
|
|
293
|
+
// before freezing the scope prefix and numbering ordinary parameters.
|
|
294
|
+
for (const group of conditions?.groups ?? []) {
|
|
295
|
+
for (const condition of group) {
|
|
296
|
+
if (condition.any) {
|
|
297
|
+
exists(condition.any);
|
|
298
|
+
continue;
|
|
299
|
+
}
|
|
300
|
+
qualify(condition.column);
|
|
301
|
+
if (condition.other !== undefined)
|
|
302
|
+
qualify(condition.other);
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
const aliasNames = new Set((view.aggregates ?? []).map((a) => a.alias?.trim()).filter(Boolean));
|
|
306
|
+
const order = (view.orderBy ?? []).flatMap((o) => {
|
|
307
|
+
if (o.nulls === undefined)
|
|
308
|
+
return [orderTerm(o)];
|
|
309
|
+
if (o.nulls !== 'last' || o.alias || o.near !== undefined) {
|
|
310
|
+
throw new PathError(`"${o.path}" puts nulls last only when it is a column.`, o.path, o.path, 0);
|
|
311
|
+
}
|
|
312
|
+
/* NULLS LAST in both directions, said the one way every dialect reads:
|
|
313
|
+
a null sorts after every value whichever way the column runs. */
|
|
314
|
+
return [`${qualify(o.path)} IS NULL ASC`, orderTerm(o)];
|
|
315
|
+
});
|
|
316
|
+
function orderTerm(o) {
|
|
317
|
+
if (o.collation !== undefined && (o.alias || o.near !== undefined)) {
|
|
318
|
+
throw new PathError(`"${o.path}" orders by a column's own text only when it is a column.`, o.path, o.path, 0);
|
|
319
|
+
}
|
|
320
|
+
if (o.alias) {
|
|
321
|
+
/* Refused by name rather than left to the engine: SQLite quietly
|
|
322
|
+
treats an unknown ORDER BY identifier as a new error anyway, but
|
|
323
|
+
"no such column: invoicez" would not say the fix is the alias two
|
|
324
|
+
words back. */
|
|
325
|
+
if (!aliasNames.has(o.path)) {
|
|
326
|
+
throw new PathError(`"${o.path}" is not an aggregate alias on this view.`, o.path, o.path, 0);
|
|
327
|
+
}
|
|
328
|
+
return `${style.quote(o.path)} ${o.direction === 'desc' ? 'DESC' : 'ASC'}`;
|
|
329
|
+
}
|
|
330
|
+
if (o.near !== undefined) {
|
|
331
|
+
/* Closest first, as a literal rather than a parameter: the ORDER BY is
|
|
332
|
+
written before the WHERE's parameters are numbered, and a finite,
|
|
333
|
+
safe number carries nothing but itself. Parenthesized, so a negative
|
|
334
|
+
value never meets the minus as "--", a comment in SQL. */
|
|
335
|
+
if (typeof o.near !== 'number' || !Number.isFinite(o.near) || Math.abs(o.near) > Number.MAX_SAFE_INTEGER) {
|
|
336
|
+
throw new PathError(`"near" on "${o.path}" must be a finite number.`, o.path, o.path, 0);
|
|
337
|
+
}
|
|
338
|
+
return `ABS(${qualify(o.path)} - (${String(o.near)})) ${o.direction === 'desc' ? 'DESC' : 'ASC'}`;
|
|
339
|
+
}
|
|
340
|
+
/* A vocabulary sorts as declared (schema.ts declaredOrder); an order by
|
|
341
|
+
bytes asked for the bytes, for a cursor that compares them. */
|
|
342
|
+
if (o.collation === undefined)
|
|
343
|
+
return { expression: qualify(o.path), direction: o.direction, declared: declaredOrder(resolve(o.path).column) };
|
|
344
|
+
return `${collate(qualify(o.path), o.collation, style)} ${o.direction === 'desc' ? 'DESC' : 'ASC'}`;
|
|
345
|
+
}
|
|
346
|
+
const limit = clampLimit(view.limit);
|
|
347
|
+
const skip = clampOffset(offset);
|
|
348
|
+
const joins = plan.clauses();
|
|
349
|
+
const prefix = relations.finish();
|
|
350
|
+
/* The other side of a column comparison is a path like any other: resolved
|
|
351
|
+
against this catalog, so a reader's pruned graph refuses one it may not
|
|
352
|
+
read exactly as it refuses the left. */
|
|
353
|
+
const where = buildWhere(conditions, style, prefix.params.length + 1, qualify, qualify, membership, exists);
|
|
354
|
+
/* Placeholder numbering continues across the whole statement, which is the
|
|
355
|
+
one thing Postgres needs and SQLite does not — and the exact place a join
|
|
356
|
+
builder gets it wrong, because the joins sit between the SELECT list and
|
|
357
|
+
the WHERE clause and are easy to count as if they carried parameters. */
|
|
358
|
+
const sorted = orderClauseSql(order, style, prefix.params.length + where.params.length + 1);
|
|
359
|
+
const n = prefix.params.length + where.params.length + sorted.params.length;
|
|
360
|
+
const lines = [`SELECT ${select.join(',\n ')}`, ` FROM ${baseRelation} ${style.quote(plan.baseAlias)}`, ...joins];
|
|
361
|
+
if (where.text)
|
|
362
|
+
lines.push(where.text.replace(/^ WHERE /, ' WHERE '));
|
|
363
|
+
if (sorted.text)
|
|
364
|
+
lines.push(sorted.text);
|
|
365
|
+
lines.push(` LIMIT ${style.placeholder(n + 1)} OFFSET ${style.placeholder(n + 2)}`);
|
|
366
|
+
return {
|
|
367
|
+
text: prefix.text + lines.join('\n'),
|
|
368
|
+
params: [...prefix.params, ...where.params, ...sorted.params, limit, skip],
|
|
369
|
+
columns,
|
|
370
|
+
joins: plan.joins,
|
|
371
|
+
limit,
|
|
372
|
+
};
|
|
373
|
+
}
|
|
374
|
+
/* ---------- the query language, over paths ----------
|
|
375
|
+
|
|
376
|
+
The composer's filter box takes the same language as the bar at the top,
|
|
377
|
+
because two languages for one idea is one too many. It is parsed by the
|
|
378
|
+
same parser, too: `parseQuery` already handles and/or, parentheses, quoting,
|
|
379
|
+
relative dates and per-column value coercion, and a second implementation
|
|
380
|
+
of those would be a second set of bugs.
|
|
381
|
+
|
|
382
|
+
The trick that makes it work is a *synthetic* table. `parseQuery` resolves
|
|
383
|
+
every field against a table's column list, so the base table is handed to it
|
|
384
|
+
with one extra column per path mentioned in the text, each carrying the
|
|
385
|
+
declared type of the column the path actually ends on. Coercion, date
|
|
386
|
+
handling and "did you mean" then work over paths for free.
|
|
387
|
+
|
|
388
|
+
Paths are resolved on demand rather than enumerated: `work_order.parent_id`
|
|
389
|
+
points back at `work_order`, so the set of paths four hops deep is tens of
|
|
390
|
+
thousands of entries and building it per keystroke would be absurd. */
|
|
391
|
+
/* One segment of a path: a column name, or — for a composite key — the
|
|
392
|
+
columns of it joined with `+`, exactly as `hopName` writes them.
|
|
393
|
+
|
|
394
|
+
`+` has to be admitted here as well as in `parseQuery`'s FIELD, or the two
|
|
395
|
+
disagree about where a token begins: the scanner reads only `line_no.sku`
|
|
396
|
+
out of `invoice_id+line_no.sku`, builds the synthetic column for a path that
|
|
397
|
+
does not exist, and a composite hop becomes something you can show but never
|
|
398
|
+
filter. It is legal only *between* identifier characters, never leading —
|
|
399
|
+
a leading `+` would let a signed number start a path. */
|
|
400
|
+
const SEGMENT = String.raw `[A-Za-z_$][\w$]*(?:\+[A-Za-z_$][\w$]*)*`;
|
|
401
|
+
/* A dotted token. `@` and `+` are excluded from the lookbehind on purpose:
|
|
402
|
+
without `@`, `email = ops@harbour.example` reads its own value as a path and
|
|
403
|
+
reports a confident error about a table that was never mentioned; without
|
|
404
|
+
`+`, the tail of a composite hop matches on its own and reports the same
|
|
405
|
+
kind of confident error about half a key. */
|
|
406
|
+
const DOTTED_TOKEN = new RegExp(String.raw `(?<![\w.$@+])(${SEGMENT}(?:\.${SEGMENT})+)`, 'g');
|
|
407
|
+
/** What makes a token a *field* rather than a value: an operator after it. */
|
|
408
|
+
const OPERATOR_AFTER = /^\s*(?:!=|<=|>=|=|<|>|\bcontains\b|\bstartswith\b|\bendswith\b|\blike\b|\bnot\s+in\b|\bin\b|\bis\b)/i;
|
|
409
|
+
/** …or a word before it that can only be followed by a field. */
|
|
410
|
+
const FIELD_BEFORE = /(?:^|[\s(,])(?:and|or|where|filter|show|select|sort|by)\s*$|(?:^|,)\s*$/i;
|
|
411
|
+
/**
|
|
412
|
+
* Parse the composer's filter/clause text against a base table.
|
|
413
|
+
*
|
|
414
|
+
* Returns errors rather than throwing, because this runs on every keystroke
|
|
415
|
+
* and a half-typed path is the normal state of the input, not an exception.
|
|
416
|
+
*/
|
|
417
|
+
export function parseViewQuery(schema, baseId, text, now, options = {}) {
|
|
418
|
+
const empty = { filter: { groups: [] }, columns: [], orderBy: [], errors: [] };
|
|
419
|
+
const base = findTable(schema, baseId);
|
|
420
|
+
if (!base) {
|
|
421
|
+
return { ...empty, errors: [{ message: `No table called "${baseId}" in this database.` }] };
|
|
422
|
+
}
|
|
423
|
+
const written = String(text ?? '').trim();
|
|
424
|
+
if (!written)
|
|
425
|
+
return empty;
|
|
426
|
+
const errors = [];
|
|
427
|
+
/* A segment's name is written out first (query.ts expandSegments), so the paths, rollups and computed
|
|
428
|
+
columns its conditions name are found below as if they had been typed; a position is said in the text
|
|
429
|
+
as written. */
|
|
430
|
+
let body = written;
|
|
431
|
+
let said = (at) => at;
|
|
432
|
+
if (base.segments) {
|
|
433
|
+
const { head } = splitClauseTail(written);
|
|
434
|
+
const start = head ? written.indexOf(head) : -1;
|
|
435
|
+
if (start >= 0) {
|
|
436
|
+
const expanded = expandSegments(head, base.segments);
|
|
437
|
+
if (expanded.error)
|
|
438
|
+
return { ...empty, errors: [{ message: expanded.error.message, at: start + (expanded.error.at ?? 0) }] };
|
|
439
|
+
if (expanded.text !== head) {
|
|
440
|
+
body = written.slice(0, start) + expanded.text + written.slice(start + head.length);
|
|
441
|
+
const end = start + expanded.text.length;
|
|
442
|
+
said = (at) => (at < start ? at : at < end ? start + expanded.at(at - start) : at - (expanded.text.length - head.length));
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
const extra = [];
|
|
447
|
+
/* Each `any`/`no` condition is read on its own, against the child, and stands in the text the parser reads as a
|
|
448
|
+
synthetic column asked `is (not) empty`; it becomes the condition it is when the parse comes back. */
|
|
449
|
+
const anys = new Map();
|
|
450
|
+
if (/\b(?:any|no)\s/i.test(body)) {
|
|
451
|
+
const read = anyRowsOf(schema, base, body, now, options, anys, extra);
|
|
452
|
+
if (read.errors.length)
|
|
453
|
+
return { ...empty, errors: read.errors.map((error) => ({ ...error, at: error.at === undefined ? undefined : said(error.at) })) };
|
|
454
|
+
if (read.text !== body) {
|
|
455
|
+
const outer = said;
|
|
456
|
+
body = read.text;
|
|
457
|
+
said = (at) => outer(read.at(at));
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
const added = new Set();
|
|
461
|
+
/* Paths already reported as broken. The synthetic table cannot contain
|
|
462
|
+
them, so the parser will reach them too and say "not a column on invoice"
|
|
463
|
+
— true, useless, and directly under a message that named the real
|
|
464
|
+
problem. Two errors for one mistake reads as two mistakes. */
|
|
465
|
+
const failed = new Set();
|
|
466
|
+
const scanned = maskQuoted(body);
|
|
467
|
+
for (const match of scanned.matchAll(DOTTED_TOKEN)) {
|
|
468
|
+
const path = match[1];
|
|
469
|
+
const start = match.index ?? 0;
|
|
470
|
+
/* Whether this token is being used as a field decides whether a failure
|
|
471
|
+
to resolve is worth reporting. In value position — `external_ref =
|
|
472
|
+
a.b` — a dotted word that is not a path is simply a value, and an error
|
|
473
|
+
about it would be noise on a query that is perfectly correct. */
|
|
474
|
+
/* …or a path on the right of a comparison whose first hop is a column
|
|
475
|
+
of the base: `on_hand < product_id.reorder_level` compares with it,
|
|
476
|
+
so a hop that goes nowhere is a mistake to name, not text. */
|
|
477
|
+
const isField = OPERATOR_AFTER.test(scanned.slice(start + path.length)) ||
|
|
478
|
+
FIELD_BEFORE.test(scanned.slice(0, start)) ||
|
|
479
|
+
(/(?:!=|<=|>=|=|<|>)\s*$/.test(scanned.slice(0, start)) && base.columns.some((c) => c.name === path.split('.')[0].split('+')[0]));
|
|
480
|
+
if (added.has(path))
|
|
481
|
+
continue;
|
|
482
|
+
added.add(path);
|
|
483
|
+
const { resolved, error } = tryResolvePath(schema, baseId, path);
|
|
484
|
+
if (error) {
|
|
485
|
+
/* Located at the token so the composer can underline the right word.
|
|
486
|
+
The offset is into the filter text, and the caller shifts it. */
|
|
487
|
+
if (isField) {
|
|
488
|
+
errors.push({ message: error.message, at: said(start) });
|
|
489
|
+
failed.add(path);
|
|
490
|
+
}
|
|
491
|
+
continue;
|
|
492
|
+
}
|
|
493
|
+
if (!resolved)
|
|
494
|
+
continue;
|
|
495
|
+
/* The synthetic column keeps the *declared* type of the real column, so
|
|
496
|
+
`signed_up = last 30 days` through a path behaves exactly as it does
|
|
497
|
+
on the base table. Nullability is widened because the join can miss. */
|
|
498
|
+
extra.push({
|
|
499
|
+
name: resolved.path,
|
|
500
|
+
type: resolved.column.type,
|
|
501
|
+
nullable: true,
|
|
502
|
+
primaryKey: false,
|
|
503
|
+
references: resolved.column.references,
|
|
504
|
+
});
|
|
505
|
+
}
|
|
506
|
+
/* The base's own record label (`show $label`, `$label contains 12`) is a
|
|
507
|
+
column of the synthetic table where the catalog serves a template. */
|
|
508
|
+
if (base.labelTemplate && /(?<![\w.$])\$label(?![\w$])/.test(scanned)) {
|
|
509
|
+
extra.push(labelColumnMeta());
|
|
510
|
+
}
|
|
511
|
+
/* The base's rollups (rollups.ts) are columns of the synthetic table where
|
|
512
|
+
the text names one, typed as the catalog serves them, so `sort
|
|
513
|
+
line_total desc` and `open_deals > 0` parse and coerce like any number.
|
|
514
|
+
A rollup through a reference is a dotted path, found above. */
|
|
515
|
+
for (const [name, rollup] of Object.entries(base.rollups ?? {})) {
|
|
516
|
+
if (added.has(name) || base.columns.some((c) => c.name === name))
|
|
517
|
+
continue;
|
|
518
|
+
if (new RegExp(`(?<![\\w.$])${name}(?![\\w$])`).test(scanned))
|
|
519
|
+
extra.push(rollupColumnMeta(name, rollup));
|
|
520
|
+
}
|
|
521
|
+
/* And its computed columns (computed.ts), typed as they come out: `is_overdue = true`, `sort days_open desc`. */
|
|
522
|
+
for (const [name, computed] of Object.entries(base.computed ?? {})) {
|
|
523
|
+
if (added.has(name) || base.columns.some((c) => c.name === name))
|
|
524
|
+
continue;
|
|
525
|
+
if (new RegExp(`(?<![\\w.$])${name}(?![\\w$])`).test(scanned))
|
|
526
|
+
extra.push(computedColumnMeta(name, computed));
|
|
527
|
+
}
|
|
528
|
+
/* And its tags (tagLists.ts), a column only `has` may ask: `tags has bug`, `show name, tags`. */
|
|
529
|
+
if (base.tags &&
|
|
530
|
+
!added.has(TAGS_COLUMN) &&
|
|
531
|
+
!base.columns.some((c) => c.name === TAGS_COLUMN) &&
|
|
532
|
+
new RegExp(`(?<![\\w.$])${TAGS_COLUMN}(?![\\w$])`).test(scanned))
|
|
533
|
+
extra.push(tagsColumnMeta());
|
|
534
|
+
/* Aggregate aliases join the synthetic table so `sort invoices desc`
|
|
535
|
+
parses — and only sort. A WHERE runs before the SELECT list exists, so
|
|
536
|
+
filtering by an alias is refused below with the reason, not with
|
|
537
|
+
"not a column", which is true and useless. */
|
|
538
|
+
const aliases = new Set(options.aggregateAliases ?? []);
|
|
539
|
+
for (const alias of aliases) {
|
|
540
|
+
if (!added.has(alias))
|
|
541
|
+
extra.push({ name: alias, type: 'NUMERIC', nullable: true, primaryKey: false });
|
|
542
|
+
}
|
|
543
|
+
const synthetic = {
|
|
544
|
+
...schema,
|
|
545
|
+
tables: schema.tables.map((t) => (t.id === base.id ? { ...t, columns: [...t.columns, ...extra] } : t)),
|
|
546
|
+
};
|
|
547
|
+
/* The parser wants `<table> <clauses>`, which is also what the composer's
|
|
548
|
+
hint text shows, so the base name is prepended rather than the grammar
|
|
549
|
+
being special-cased. */
|
|
550
|
+
const offset = base.id.length + 1;
|
|
551
|
+
const parsed = parseQuery(`${base.id} ${body}`, {
|
|
552
|
+
schema: synthetic,
|
|
553
|
+
now,
|
|
554
|
+
columnValues: options.columnValues !== false,
|
|
555
|
+
...(options.me !== undefined ? { me: options.me } : {}),
|
|
556
|
+
});
|
|
557
|
+
for (const err of parsed.errors) {
|
|
558
|
+
if ([...failed].some((path) => err.message.includes(`"${path}"`)))
|
|
559
|
+
continue;
|
|
560
|
+
errors.push({ message: err.message, at: err.at === undefined ? undefined : said(Math.max(0, err.at - offset)) });
|
|
561
|
+
}
|
|
562
|
+
const query = parsed.query;
|
|
563
|
+
if (!query)
|
|
564
|
+
return { ...empty, errors };
|
|
565
|
+
for (const group of query.filter.groups) {
|
|
566
|
+
for (const c of group) {
|
|
567
|
+
if (aliases.has(c.column)) {
|
|
568
|
+
errors.push({
|
|
569
|
+
message: `"${c.column}" is an aggregate result, and a filter runs before it exists. ` + 'Filter on the child table instead.',
|
|
570
|
+
});
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
for (const column of query.columns ?? []) {
|
|
575
|
+
if (aliases.has(column)) {
|
|
576
|
+
errors.push({ message: `"${column}" is already shown — the aggregate adds its own column.` });
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
return {
|
|
580
|
+
filter: {
|
|
581
|
+
groups: query.filter.groups.map((group) => group.map((c) => anys.get(c.column) ??
|
|
582
|
+
(c.other === undefined ? { path: c.column, op: c.op, value: c.value } : { path: c.column, op: c.op, other: c.other }))),
|
|
583
|
+
},
|
|
584
|
+
columns: query.columns,
|
|
585
|
+
orderBy: query.orderBy.map((o) => aliases.has(o.column) ? { path: o.column, direction: o.direction, alias: true } : { path: o.column, direction: o.direction }),
|
|
586
|
+
limit: query.limit,
|
|
587
|
+
errors,
|
|
588
|
+
};
|
|
589
|
+
}
|
|
590
|
+
/* The name an `any` condition stands under while the parser reads the rest: no column can be called it. */
|
|
591
|
+
const ANY_STANDIN = '$any';
|
|
592
|
+
/**
|
|
593
|
+
* The `any`/`no` conditions of a view's text, each read against its child
|
|
594
|
+
* table: the text with each written as a stand-in column asked `is (not)
|
|
595
|
+
* empty`, the stand-ins (in `extra`) and the conditions they become (in
|
|
596
|
+
* `anys`), and a position in the new text said in the old.
|
|
597
|
+
*
|
|
598
|
+
* The child is the table whose one reference points at the base, or the one
|
|
599
|
+
* `via` names; its conditions are its own columns and paths up from it, and
|
|
600
|
+
* its segments — what a row scope's narrowing reads (rowScope.ts `filtered`),
|
|
601
|
+
* which is how the EXISTS is written. A second `any` inside, a rollup, a
|
|
602
|
+
* computed column, tags or a record label are refused by name rather than
|
|
603
|
+
* left to fail as SQL.
|
|
604
|
+
*/
|
|
605
|
+
function anyRowsOf(schema, base, body, now, options, anys, extra) {
|
|
606
|
+
const same = { text: body, at: (position) => position, errors: [] };
|
|
607
|
+
const { head } = splitClauseTail(body);
|
|
608
|
+
const start = head ? body.indexOf(head) : -1;
|
|
609
|
+
if (start < 0)
|
|
610
|
+
return same;
|
|
611
|
+
const found = anyConditions(head);
|
|
612
|
+
if (!found.length)
|
|
613
|
+
return same;
|
|
614
|
+
const errors = [];
|
|
615
|
+
const splices = [];
|
|
616
|
+
let text = '';
|
|
617
|
+
let last = 0;
|
|
618
|
+
for (const condition of found) {
|
|
619
|
+
const at = start + condition.at;
|
|
620
|
+
const [, word, childName, via, rest] = ANY_CONDITION.exec(condition.text);
|
|
621
|
+
const fail = (message, offset = 0) => errors.push({ message, at: at + offset });
|
|
622
|
+
const named = backrefsTo(schema, base.id).filter((b) => {
|
|
623
|
+
const table = findTable(schema, b.child);
|
|
624
|
+
return table && (table.id === childName || table.name === childName);
|
|
625
|
+
});
|
|
626
|
+
const through = via === undefined ? named : named.filter((b) => b.fk.from.columns.join('+') === via);
|
|
627
|
+
if (!named.length) {
|
|
628
|
+
fail(findTable(schema, childName) || schema.tables.some((t) => t.name === childName)
|
|
629
|
+
? `${childName} has no reference to ${base.name}, so "${word} ${childName}" has no rows to ask about.`
|
|
630
|
+
: `No table called "${childName}" here.`, word.length + 1);
|
|
631
|
+
continue;
|
|
632
|
+
}
|
|
633
|
+
if (!through.length) {
|
|
634
|
+
fail(`${childName} points at ${base.name} through ${named.map((b) => b.fk.from.columns.join('+')).join(', ')}, not "${via}".`, condition.text.indexOf(via, word.length + childName.length + 1));
|
|
635
|
+
continue;
|
|
636
|
+
}
|
|
637
|
+
if (through.length > 1) {
|
|
638
|
+
const columns = through.map((b) => b.fk.from.columns.join('+'));
|
|
639
|
+
fail(`${childName} points at ${base.name} through ${columns.join(' and ')}: say which — ${word} ${childName} via ${columns[0]}.`, word.length + 1);
|
|
640
|
+
continue;
|
|
641
|
+
}
|
|
642
|
+
const { fk } = through[0];
|
|
643
|
+
if (fk.stitched) {
|
|
644
|
+
fail(`${childName} is in another source, on another connection, so a list of ${base.name} cannot ask about its rows: ` +
|
|
645
|
+
'each database filters what it holds.', word.length + 1);
|
|
646
|
+
continue;
|
|
647
|
+
}
|
|
648
|
+
const child = findTable(schema, fk.from.table);
|
|
649
|
+
let filter;
|
|
650
|
+
if (rest !== undefined) {
|
|
651
|
+
const restAt = condition.text.length - rest.length;
|
|
652
|
+
const parsed = parseViewQuery(schema, child.id, rest, now, { me: options.me, columnValues: options.columnValues });
|
|
653
|
+
for (const error of parsed.errors)
|
|
654
|
+
fail(error.message, restAt + (error.at ?? 0));
|
|
655
|
+
if (parsed.errors.length)
|
|
656
|
+
continue;
|
|
657
|
+
if (parsed.columns.length || parsed.orderBy.length || parsed.limit !== undefined) {
|
|
658
|
+
fail(`"${word} ${childName}" takes conditions only: a show, sort or limit there has no rows to shape.`, restAt);
|
|
659
|
+
continue;
|
|
660
|
+
}
|
|
661
|
+
const unread = parsed.filter.groups
|
|
662
|
+
.flat()
|
|
663
|
+
.flatMap((c) => (c.any ? [c.path] : [c.path, ...(c.other === undefined ? [] : [c.other])]))
|
|
664
|
+
.find((path) => {
|
|
665
|
+
if (path.startsWith('any ') || path.startsWith('no '))
|
|
666
|
+
return true;
|
|
667
|
+
const { resolved } = tryResolvePath(schema, child.id, path);
|
|
668
|
+
return !resolved || Boolean(resolved.rollup || resolved.computed || resolved.tags || resolved.jsonKey || path.includes('$'));
|
|
669
|
+
});
|
|
670
|
+
if (unread !== undefined) {
|
|
671
|
+
fail(/^(?:any|no)\s/i.test(unread)
|
|
672
|
+
? `"${word} ${childName}" cannot ask about rows of its own rows: say that as its own condition.`
|
|
673
|
+
: `"${unread}" is not a stored column of ${child.name}, and "${word} ${childName}" reads only those.`, restAt + Math.max(0, rest.indexOf(unread)));
|
|
674
|
+
continue;
|
|
675
|
+
}
|
|
676
|
+
const stitched = stitchedListProblem(schema, child.id, { filter: parsed.filter });
|
|
677
|
+
if (stitched) {
|
|
678
|
+
fail(stitched, restAt);
|
|
679
|
+
continue;
|
|
680
|
+
}
|
|
681
|
+
filter = {
|
|
682
|
+
groups: parsed.filter.groups.map((group) => group.map((c) => c.other === undefined ? { column: c.path, op: c.op, value: c.value } : { column: c.path, op: c.op, other: c.other })),
|
|
683
|
+
};
|
|
684
|
+
}
|
|
685
|
+
const standin = `${ANY_STANDIN}${anys.size}`;
|
|
686
|
+
const said = condition.text.replace(/\s+/g, ' ');
|
|
687
|
+
anys.set(standin, {
|
|
688
|
+
path: said,
|
|
689
|
+
op: word.toLowerCase() === 'no' ? 'isNull' : 'isNotNull',
|
|
690
|
+
any: { via: `${child.id}.${fk.from.columns.join('+')}`, ...(filter ? { filter } : {}) },
|
|
691
|
+
});
|
|
692
|
+
extra.push({ name: standin, type: 'INTEGER', nullable: true, primaryKey: false });
|
|
693
|
+
const written = `${standin} ${word.toLowerCase() === 'no' ? 'is empty' : 'is not empty'}`;
|
|
694
|
+
text += body.slice(last, at);
|
|
695
|
+
splices.push({ at, was: condition.text.length, from: text.length, length: written.length });
|
|
696
|
+
text += written;
|
|
697
|
+
last = at + condition.text.length;
|
|
698
|
+
}
|
|
699
|
+
if (errors.length)
|
|
700
|
+
return { ...same, errors };
|
|
701
|
+
text += body.slice(last);
|
|
702
|
+
return {
|
|
703
|
+
text,
|
|
704
|
+
errors,
|
|
705
|
+
at: (position) => {
|
|
706
|
+
let shift = 0;
|
|
707
|
+
for (const splice of splices) {
|
|
708
|
+
if (position < splice.from)
|
|
709
|
+
break;
|
|
710
|
+
if (position < splice.from + splice.length)
|
|
711
|
+
return splice.at;
|
|
712
|
+
shift = splice.from + splice.length - (splice.at + splice.was);
|
|
713
|
+
}
|
|
714
|
+
return position - shift;
|
|
715
|
+
},
|
|
716
|
+
};
|
|
717
|
+
}
|
|
718
|
+
/**
|
|
719
|
+
* How many rows the view matches, ignoring the page.
|
|
720
|
+
*
|
|
721
|
+
* A separate compilation rather than a wrapper around the other one, because
|
|
722
|
+
* the two differ in more than a keyword: the SELECT list, the ORDER BY and the
|
|
723
|
+
* page are all irrelevant here, and every aggregate in the view would be a
|
|
724
|
+
* correlated subquery evaluated per row for a number nobody reads.
|
|
725
|
+
*
|
|
726
|
+
* The count is exact rather than estimated, and it is *base rows* — which is
|
|
727
|
+
* only true because every join is to-one. That is the same property the whole
|
|
728
|
+
* design rests on: a view never multiplies, so counting it is counting the
|
|
729
|
+
* table it started from. With `limit`, it stops after that many.
|
|
730
|
+
*/
|
|
731
|
+
export function compileViewCount(schema, view, style, scope, limit) {
|
|
732
|
+
schema = withViewReferences(schema, view.references);
|
|
733
|
+
const base = findTable(schema, view.base);
|
|
734
|
+
if (!base) {
|
|
735
|
+
throw new PathError(`No table called "${view.base}" in this database, so the view has nothing to start from.`, view.base, view.base, 0);
|
|
736
|
+
}
|
|
737
|
+
const relations = new ScopedRelations(schema, style, scope);
|
|
738
|
+
const baseRelation = relations.table(base.id);
|
|
739
|
+
const plan = new JoinPlan(schema, base, style, relations.table);
|
|
740
|
+
const { qualify, membership, exists } = viewPaths(schema, view, plan, style, relations);
|
|
741
|
+
const conditions = viewConditions(view);
|
|
742
|
+
for (const group of conditions?.groups ?? []) {
|
|
743
|
+
for (const condition of group) {
|
|
744
|
+
if (condition.any) {
|
|
745
|
+
exists(condition.any);
|
|
746
|
+
continue;
|
|
747
|
+
}
|
|
748
|
+
qualify(condition.column);
|
|
749
|
+
if (condition.other !== undefined)
|
|
750
|
+
qualify(condition.other);
|
|
751
|
+
}
|
|
752
|
+
}
|
|
753
|
+
const joins = plan.clauses();
|
|
754
|
+
const prefix = relations.finish();
|
|
755
|
+
const where = buildWhere(conditions, style, prefix.params.length + 1, qualify, qualify, membership, exists);
|
|
756
|
+
/* Only filter joins affect row count. Projection-only joins and scalar
|
|
757
|
+
aggregates deliberately do not register relations in this statement. */
|
|
758
|
+
const relation = `${baseRelation} ${style.quote(plan.baseAlias)}` + (joins.length ? `\n${joins.join('\n')}` : '') + where.text;
|
|
759
|
+
/* A bound stops the count the way every bounded count stops
|
|
760
|
+
(countStatement): at most `limit` base rows are read. */
|
|
761
|
+
const text = limit === undefined ? prefix.text + `SELECT COUNT(*) AS n\n FROM ${relation}` : prefix.text + countStatement(relation, limit);
|
|
762
|
+
return { text, params: [...prefix.params, ...where.params] };
|
|
763
|
+
}
|
|
764
|
+
/**
|
|
765
|
+
* Render a view as the SQL it will become, values inline.
|
|
766
|
+
*
|
|
767
|
+
* The mirror of `explain` in query.ts and used the same way: this string never
|
|
768
|
+
* reaches a database, and showing `"customer_1"."name" LIKE '%Harbour%'` next
|
|
769
|
+
* to the path that produced it is how someone learns what the composer is
|
|
770
|
+
* doing on their behalf.
|
|
771
|
+
*/
|
|
772
|
+
export function explainView(schema, view, style) {
|
|
773
|
+
const compiled = compileView(schema, withViewComputed(schema, withViewLabels(schema, view)), style);
|
|
774
|
+
let i = 0;
|
|
775
|
+
return compiled.text.replace(/\$\d+|\?/g, () => sqlLiteral(compiled.params[i++]));
|
|
776
|
+
}
|
|
777
|
+
/**
|
|
778
|
+
* One parameter, written the way SQL would have to spell it.
|
|
779
|
+
*
|
|
780
|
+
* Exported because a breakdown explains itself the same way a view does, and
|
|
781
|
+
* two spellings of `NULL` — or two different ideas about escaping a quote —
|
|
782
|
+
* would show up as a difference between two panes that claim to be the same
|
|
783
|
+
* statement.
|
|
784
|
+
*/
|
|
785
|
+
export function sqlLiteral(value) {
|
|
786
|
+
if (value === null || value === undefined)
|
|
787
|
+
return 'NULL';
|
|
788
|
+
if (typeof value === 'number' || typeof value === 'boolean')
|
|
789
|
+
return String(value);
|
|
790
|
+
return `'${String(value).replace(/'/g, "''")}'`;
|
|
791
|
+
}
|
|
792
|
+
/* ---------- config file support ----------
|
|
793
|
+
|
|
794
|
+
Views can be predefined in `tablewalk.json` beside `connections`, and are
|
|
795
|
+
validated the same strict way: a malformed entry is an error naming the
|
|
796
|
+
entry, never a silent skip. Quietly dropping a view someone wrote is the
|
|
797
|
+
worst outcome — they see a shorter list and no reason for it. */
|
|
798
|
+
export function parseViewsConfig(raw, where) {
|
|
799
|
+
const views = raw?.views;
|
|
800
|
+
if (views === undefined)
|
|
801
|
+
return [];
|
|
802
|
+
if (!Array.isArray(views))
|
|
803
|
+
throw new Error(`${where}: "views" must be an array.`);
|
|
804
|
+
const seen = new Set();
|
|
805
|
+
return views.map((entry, i) => {
|
|
806
|
+
const at = `${where}: view ${i + 1}`;
|
|
807
|
+
if (!entry || typeof entry !== 'object' || Array.isArray(entry)) {
|
|
808
|
+
throw new Error(`${at} is not an object.`);
|
|
809
|
+
}
|
|
810
|
+
const v = entry;
|
|
811
|
+
if (typeof v.name !== 'string' || !v.name.trim())
|
|
812
|
+
throw new Error(`${at} has no "name".`);
|
|
813
|
+
/* A view may be written down instead of built out of objects. The text
|
|
814
|
+
form needs a schema to resolve, which a config file does not have, so
|
|
815
|
+
it is carried through and expanded when a connection is open — and any
|
|
816
|
+
error it holds is reported against that view rather than swallowed. */
|
|
817
|
+
const text = v.text === undefined ? undefined : String(v.text).trim();
|
|
818
|
+
if (v.text !== undefined && (typeof v.text !== 'string' || !text)) {
|
|
819
|
+
throw new Error(`${at} ("${v.name}") has a "text" that is not a non-empty string.`);
|
|
820
|
+
}
|
|
821
|
+
if (text && v.base !== undefined) {
|
|
822
|
+
throw new Error(`${at} ("${v.name}") has both "text" and "base". The text form already names its table.`);
|
|
823
|
+
}
|
|
824
|
+
if (!text && (typeof v.base !== 'string' || !v.base.trim())) {
|
|
825
|
+
throw new Error(`${at} ("${v.name}") has no "base" table.`);
|
|
826
|
+
}
|
|
827
|
+
const id = v.id === undefined ? `config-${slug(v.name, 'view')}` : String(v.id);
|
|
828
|
+
if (v.id !== undefined && (typeof v.id !== 'string' || !v.id.trim())) {
|
|
829
|
+
throw new Error(`${at} ("${v.name}") has an "id" that is not a non-empty string.`);
|
|
830
|
+
}
|
|
831
|
+
if (seen.has(id))
|
|
832
|
+
throw new Error(`${at} ("${v.name}") repeats the id "${id}".`);
|
|
833
|
+
seen.add(id);
|
|
834
|
+
const columns = parseColumns(v.columns, at, String(v.name));
|
|
835
|
+
const aggregates = parseAggregates(v.aggregates, at, String(v.name));
|
|
836
|
+
const filter = parseFilter(v.filter, at, String(v.name));
|
|
837
|
+
const orderBy = parseOrder(v.orderBy, at, String(v.name));
|
|
838
|
+
if (v.limit !== undefined && (typeof v.limit !== 'number' || !Number.isFinite(v.limit))) {
|
|
839
|
+
throw new Error(`${at} ("${v.name}") has a "limit" that is not a number.`);
|
|
840
|
+
}
|
|
841
|
+
if (v.connection !== undefined && typeof v.connection !== 'string') {
|
|
842
|
+
throw new Error(`${at} ("${v.name}") has a "connection" that is not a string.`);
|
|
843
|
+
}
|
|
844
|
+
return {
|
|
845
|
+
id,
|
|
846
|
+
name: v.name.trim(),
|
|
847
|
+
base: text ? text.trim().split(/\s+/)[0] : v.base.trim(),
|
|
848
|
+
text,
|
|
849
|
+
columns,
|
|
850
|
+
aggregates,
|
|
851
|
+
filter,
|
|
852
|
+
orderBy,
|
|
853
|
+
limit: v.limit,
|
|
854
|
+
connection: v.connection,
|
|
855
|
+
source: 'config',
|
|
856
|
+
};
|
|
857
|
+
});
|
|
858
|
+
}
|
|
859
|
+
function parseColumns(raw, at, name) {
|
|
860
|
+
if (raw === undefined)
|
|
861
|
+
return [];
|
|
862
|
+
if (!Array.isArray(raw))
|
|
863
|
+
throw new Error(`${at} ("${name}") has "columns" that is not an array.`);
|
|
864
|
+
return raw.map((col, j) => {
|
|
865
|
+
const where = `${at} ("${name}"), column ${j + 1}`;
|
|
866
|
+
if (typeof col === 'string') {
|
|
867
|
+
if (!col.trim())
|
|
868
|
+
throw new Error(`${where} is empty.`);
|
|
869
|
+
return { path: col.trim() };
|
|
870
|
+
}
|
|
871
|
+
if (!col || typeof col !== 'object')
|
|
872
|
+
throw new Error(`${where} is not a string or an object.`);
|
|
873
|
+
const c = col;
|
|
874
|
+
if (typeof c.path !== 'string' || !c.path.trim())
|
|
875
|
+
throw new Error(`${where} has no "path".`);
|
|
876
|
+
if (c.alias !== undefined && (typeof c.alias !== 'string' || !c.alias.trim())) {
|
|
877
|
+
throw new Error(`${where} has an "alias" that is not a non-empty string.`);
|
|
878
|
+
}
|
|
879
|
+
return { path: c.path.trim(), alias: c.alias };
|
|
880
|
+
});
|
|
881
|
+
}
|
|
882
|
+
function parseAggregates(raw, at, name) {
|
|
883
|
+
if (raw === undefined)
|
|
884
|
+
return undefined;
|
|
885
|
+
if (!Array.isArray(raw))
|
|
886
|
+
throw new Error(`${at} ("${name}") has "aggregates" that is not an array.`);
|
|
887
|
+
return raw.map((entry, j) => {
|
|
888
|
+
const where = `${at} ("${name}"), aggregate ${j + 1}`;
|
|
889
|
+
if (!entry || typeof entry !== 'object')
|
|
890
|
+
throw new Error(`${where} is not an object.`);
|
|
891
|
+
const a = entry;
|
|
892
|
+
if (typeof a.fn !== 'string' || !AGGREGATE_FNS.includes(a.fn)) {
|
|
893
|
+
throw new Error(`${where} has an unknown "fn". Use one of: ${AGGREGATE_FNS.join(', ')}.`);
|
|
894
|
+
}
|
|
895
|
+
if (typeof a.via !== 'string' || !a.via.trim())
|
|
896
|
+
throw new Error(`${where} has no "via".`);
|
|
897
|
+
if (a.fn !== 'count' && (typeof a.column !== 'string' || !a.column.trim())) {
|
|
898
|
+
throw new Error(`${where} is a ${a.fn} and needs a "column" to summarise.`);
|
|
899
|
+
}
|
|
900
|
+
if (a.on !== undefined && typeof a.on !== 'string') {
|
|
901
|
+
throw new Error(`${where} has an "on" that is not a string.`);
|
|
902
|
+
}
|
|
903
|
+
if (a.alias !== undefined && (typeof a.alias !== 'string' || !a.alias.trim())) {
|
|
904
|
+
throw new Error(`${where} has an "alias" that is not a non-empty string.`);
|
|
905
|
+
}
|
|
906
|
+
return {
|
|
907
|
+
fn: a.fn,
|
|
908
|
+
via: a.via.trim(),
|
|
909
|
+
on: a.on,
|
|
910
|
+
column: typeof a.column === 'string' ? a.column.trim() : undefined,
|
|
911
|
+
alias: a.alias,
|
|
912
|
+
};
|
|
913
|
+
});
|
|
914
|
+
}
|
|
915
|
+
const OPS = ['=', '!=', '>', '>=', '<', '<=', 'contains', 'startsWith', 'endsWith', 'like', 'in', 'isNull', 'isNotNull'];
|
|
916
|
+
function parseFilter(raw, at, name) {
|
|
917
|
+
if (raw === undefined)
|
|
918
|
+
return undefined;
|
|
919
|
+
if (!raw || typeof raw !== 'object' || Array.isArray(raw)) {
|
|
920
|
+
throw new Error(`${at} ("${name}") has a "filter" that is not an object.`);
|
|
921
|
+
}
|
|
922
|
+
const groups = raw.groups;
|
|
923
|
+
if (!Array.isArray(groups))
|
|
924
|
+
throw new Error(`${at} ("${name}") has a "filter" with no "groups" array.`);
|
|
925
|
+
return {
|
|
926
|
+
groups: groups.map((group, g) => {
|
|
927
|
+
if (!Array.isArray(group))
|
|
928
|
+
throw new Error(`${at} ("${name}"), filter group ${g + 1} is not an array.`);
|
|
929
|
+
return group.map((cond, c) => {
|
|
930
|
+
const where = `${at} ("${name}"), filter group ${g + 1} condition ${c + 1}`;
|
|
931
|
+
if (!cond || typeof cond !== 'object')
|
|
932
|
+
throw new Error(`${where} is not an object.`);
|
|
933
|
+
const x = cond;
|
|
934
|
+
if (typeof x.path !== 'string' || !x.path.trim())
|
|
935
|
+
throw new Error(`${where} has no "path".`);
|
|
936
|
+
if (typeof x.op !== 'string' || !OPS.includes(x.op)) {
|
|
937
|
+
throw new Error(`${where} has an unknown operator "${String(x.op)}". Use one of: ${OPS.join(', ')}.`);
|
|
938
|
+
}
|
|
939
|
+
if (x.other !== undefined && (typeof x.other !== 'string' || !x.other.trim())) {
|
|
940
|
+
throw new Error(`${where} has an "other" that is not a path.`);
|
|
941
|
+
}
|
|
942
|
+
return typeof x.other === 'string'
|
|
943
|
+
? { path: x.path.trim(), op: x.op, other: x.other.trim() }
|
|
944
|
+
: { path: x.path.trim(), op: x.op, value: x.value };
|
|
945
|
+
});
|
|
946
|
+
}),
|
|
947
|
+
};
|
|
948
|
+
}
|
|
949
|
+
function parseOrder(raw, at, name) {
|
|
950
|
+
if (raw === undefined)
|
|
951
|
+
return undefined;
|
|
952
|
+
if (!Array.isArray(raw))
|
|
953
|
+
throw new Error(`${at} ("${name}") has an "orderBy" that is not an array.`);
|
|
954
|
+
return raw.map((entry, j) => {
|
|
955
|
+
const where = `${at} ("${name}"), orderBy ${j + 1}`;
|
|
956
|
+
if (typeof entry === 'string')
|
|
957
|
+
return { path: entry, direction: 'asc' };
|
|
958
|
+
if (!entry || typeof entry !== 'object')
|
|
959
|
+
throw new Error(`${where} is not a string or an object.`);
|
|
960
|
+
const o = entry;
|
|
961
|
+
if (typeof o.path !== 'string' || !o.path.trim())
|
|
962
|
+
throw new Error(`${where} has no "path".`);
|
|
963
|
+
const direction = o.direction === undefined ? 'asc' : o.direction;
|
|
964
|
+
if (direction !== 'asc' && direction !== 'desc') {
|
|
965
|
+
throw new Error(`${where} has a "direction" that is not "asc" or "desc".`);
|
|
966
|
+
}
|
|
967
|
+
return { path: o.path.trim(), direction };
|
|
968
|
+
});
|
|
969
|
+
}
|
|
970
|
+
/**
|
|
971
|
+
* Check a view against a schema without running it.
|
|
972
|
+
*
|
|
973
|
+
* Used by `GET /api/views` so a config view naming a table that does not exist
|
|
974
|
+
* on this connection is listed *with its reason*, rather than either vanishing
|
|
975
|
+
* or blowing up the endpoint for every other view beside it.
|
|
976
|
+
*/
|
|
977
|
+
export function validateView(schema, view) {
|
|
978
|
+
try {
|
|
979
|
+
/* Any style will do: this compiles to throw away the text and keep the
|
|
980
|
+
error, and no dialect refuses a view another accepts. */
|
|
981
|
+
compileView(schema, view, ANSI_STYLE);
|
|
982
|
+
return undefined;
|
|
983
|
+
}
|
|
984
|
+
catch (err) {
|
|
985
|
+
return err.message;
|
|
986
|
+
}
|
|
987
|
+
}
|