Skip to content

chore(appkit): relocate metric-view config to config/metric-views/definitions.json#487

Merged
atilafassina merged 1 commit into
mv-runtimefrom
mv-runtime-queries
Jul 22, 2026
Merged

chore(appkit): relocate metric-view config to config/metric-views/definitions.json#487
atilafassina merged 1 commit into
mv-runtimefrom
mv-runtime-queries

Conversation

@atilafassina

Copy link
Copy Markdown
Contributor

What

Moves the metric-view declaration file out of the queries folder into its own config surface:

config/queries/metric-views.json  →  config/metric-views/definitions.json

Stacked on #474 (mv-runtime) — this PR contains only the relocation commit on top of that branch.

Why

Metric views are independent of .sql queries: an app can declare metric views with no config/queries/ folder at all. Nesting the config under config/queries/ conflated the two surfaces and forced the type generator and runtime to gate the metric path on the presence of a queries folder. Giving metric views their own sibling directory (config/metric-views/, next to config/queries/ and config/agents/) decouples them.

Changes

  • AppManager gains a metricViewsDir (defaults to a sibling of queriesDir) and a readMetricViewsConfig() reader. The traversal guard is generalized to validate against any base dir rather than being pinned to the queries dir.
  • Type generator / Vite plugin accept an explicit metricViewsFolder, activate on either config surface, and watch config/metric-views/definitions.json by directory (not bare basename, so a stray definitions.json elsewhere won't trigger a regenerate).
  • generate-types CLI generates when either config/queries or config/metric-views exists, and emits metric types even for a query-less app.
  • Runtime registry reads definitions.json through readMetricViewsConfig (dev-tunnel-aware, traversal-guarded).
  • dev-playground config, template, docs, and the JSON schema description updated to the new path.

Testing

  • pnpm --filter=@databricks/appkit test — 2844 passed, 1 skipped
  • pnpm --filter=@databricks/appkit typecheck and pnpm --filter=shared typecheck — clean
  • biome check on all changed files — clean

This pull request and its description were written by Isaac.

@atilafassina
atilafassina requested a review from a team as a code owner July 22, 2026 15:26
@atilafassina
atilafassina requested review from MarioCadenas and Copilot and removed request for a team July 22, 2026 15:26

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR relocates the metric-view configuration from the queries surface to a dedicated config surface (config/metric-views/definitions.json), and updates the type generator, Vite integration, runtime registry loading, template/dev-playground config, and documentation to match.

Changes:

  • Introduces AppManager.metricViewsDir + readMetricViewsConfig() and generalizes the traversal guard to work for multiple config base directories.
  • Updates type generation (CLI + Vite plugin) to activate on either config/queries/ or config/metric-views/, and to watch definitions.json by directory.
  • Updates runtime metric registry loading and docs/schema/template/dev-playground to the new config path.

Reviewed changes

Copilot reviewed 24 out of 25 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
template/config/metric-views/definitions.json Adds the new template metric-views config surface.
packages/shared/src/schemas/metric-source.ts Updates schema docs/description to reference the new config path.
packages/shared/src/cli/commands/type-generator.d.ts Extends type-generator API typing with metricViewsFolder.
packages/shared/src/cli/commands/generate-types.ts Generates types when either config surface exists; wires metricViewsFolder.
packages/shared/src/cli/commands/generate-types.test.ts Updates CLI tests for the new folder/file location.
packages/appkit/src/type-generator/vite-plugin.ts Activates/watches on metric-views folder and passes explicit folders to generator.
packages/appkit/src/type-generator/tests/vite-plugin.test.ts Updates Vite plugin tests for directory-scoped definitions.json watching.
packages/appkit/src/type-generator/tests/sync-metric-views-types.test.ts Moves metric config fixture path to config/metric-views/definitions.json.
packages/appkit/src/type-generator/tests/mv-registry.test.ts Updates registry reader tests to use definitions.json.
packages/appkit/src/type-generator/tests/index.test.ts Updates typegen tests to place metric config under sibling metric-views/.
packages/appkit/src/type-generator/mv-registry/types.ts Updates type docs/terminology to definitions.json.
packages/appkit/src/type-generator/mv-registry/sync.ts Updates docstring to the new config filename.
packages/appkit/src/type-generator/mv-registry/config.ts Reads config from metric-views folder and updates error messages.
packages/appkit/src/type-generator/index.ts Adds metricViewsFolder option; emits metric types independent of queries.
packages/appkit/src/plugins/analytics/types.ts Updates runtime type docs to reference the new config path.
packages/appkit/src/plugins/analytics/tests/metric.test.ts Updates analytics metric tests to use AppManager(..., metricViewsDir) and definitions.json.
packages/appkit/src/plugins/analytics/mv/registry.ts Loads metric registry via AppManager.readMetricViewsConfig() under metricViewsDir.
packages/appkit/src/plugins/analytics/mv/constants.ts Updates config filename constant to definitions.json.
packages/appkit/src/plugins/analytics/analytics.ts Updates runtime comments to reflect the new metric-views config location.
packages/appkit/src/app/tests/read-config-file.test.ts Adds coverage for metricViewsDir defaults/overrides + readMetricViewsConfig.
packages/appkit/src/app/index.ts Adds metricViewsDir and metric-views-aware config reading + generalized traversal guard.
docs/static/schemas/metric-source.schema.json Updates schema description string to match new location.
docs/docs/plugins/analytics.md Updates docs to point to config/metric-views/definitions.json.
docs/docs/development/type-generation.md Updates typegen docs to new metric-views config path/behavior.
apps/dev-playground/config/metric-views/definitions.json Adds $schema and confirms dev-playground config path.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +76 to +80
const resolvedPath = path.resolve(filePath);
const resolvedBaseDir = path.resolve(baseDir);

if (!resolvedPath.startsWith(resolvedQueriesDir)) {
logger.error("Invalid query path: path traversal detected");
if (!resolvedPath.startsWith(resolvedBaseDir)) {
logger.error("Invalid config path: path traversal detected");
Comment on lines 382 to 386
server.watcher.on("change", (changedFile) => {
const isWatchedFile = watchFolders.some((folder) =>
changedFile.startsWith(folder),
);

Comment on lines +73 to +77
const hasQueries = fs.existsSync(queryFolder);
const hasMetricViews = fs.existsSync(metricViewsFolder);

// Generate when either config surface exists. Metric-view types are
// independent of `.sql` queries — an app can declare metric views in
…initions.json

Move the metric-view declaration file out of the queries folder into its own
config surface: config/queries/metric-views.json -> config/metric-views/definitions.json.

Metric views are independent of .sql queries, so both the type generator and
the runtime now gate on the metric-views folder rather than the queries folder:

- AppManager gains a metricViewsDir (sibling of queriesDir) and a
  readMetricViewsConfig() reader; the traversal guard is generalized to any
  base dir.
- type-generator/vite-plugin accept an explicit metricViewsFolder, activate on
  either config surface, and watch config/metric-views/definitions.json by
  directory (not bare basename).
- generate-types CLI generates when either config/queries or
  config/metric-views exists.
- Runtime registry reads definitions.json via readMetricViewsConfig.
- Update dev-playground config, docs, and the JSON schema description.
- Template ships config/metric-views/definitions.json guarded by
  {{if .plugins.analytics}} (same pattern as the config/queries/*.sql files),
  so it only scaffolds into apps that enable the analytics feature.

Co-authored-by: Isaac
Signed-off-by: Atila Fassina <atila@fassina.eu>
@atilafassina
atilafassina merged commit e506289 into mv-runtime Jul 22, 2026
@atilafassina
atilafassina deleted the mv-runtime-queries branch July 22, 2026 15:50
atilafassina added a commit that referenced this pull request Jul 22, 2026
…initions.json (#487)

Move the metric-view declaration file out of the queries folder into its own
config surface: config/queries/metric-views.json -> config/metric-views/definitions.json.

Metric views are independent of .sql queries, so both the type generator and
the runtime now gate on the metric-views folder rather than the queries folder:

- AppManager gains a metricViewsDir (sibling of queriesDir) and a
  readMetricViewsConfig() reader; the traversal guard is generalized to any
  base dir.
- type-generator/vite-plugin accept an explicit metricViewsFolder, activate on
  either config surface, and watch config/metric-views/definitions.json by
  directory (not bare basename).
- generate-types CLI generates when either config/queries or
  config/metric-views exists.
- Runtime registry reads definitions.json via readMetricViewsConfig.
- Update dev-playground config, docs, and the JSON schema description.
- Template ships config/metric-views/definitions.json guarded by
  {{if .plugins.analytics}} (same pattern as the config/queries/*.sql files),
  so it only scaffolds into apps that enable the analytics feature.

Co-authored-by: Isaac

Signed-off-by: Atila Fassina <atila@fassina.eu>
atilafassina added a commit that referenced this pull request Jul 23, 2026
* chore(appkit): metric-view route skeleton + measures-only SQL (PR2 phase 1)

POST /api/analytics/metric/:key over the standard SSE envelope, SP lane only.
Synchronous config-parse registration from config/queries/metric-views.json
against the landed metricSourceSchema (single metricViews map; lane derived
from executor). Measures-only buildMetricSql (SELECT MEASURE(m) AS m FROM
<fqn> [LIMIT n]) gated by MEASURE_NAME_PATTERN + assertSafeFqn — grammar gate
only, no name allowlist. 503 METRIC_REGISTRY_LOAD_FAILED on malformed config,
404 on unknown key (generic public bodies; detail to telemetry).

Signed-off-by: Atila Fassina <atila@fassina.eu>

* chore(appkit): metric dimensions + structured filter engine (PR2 phase 2a)

GROUP BY ALL for dimensions; recursive 12-operator filter engine
(equals/notEquals/in/notIn/gt/gte/lt/lte/contains/notContains/set/notSet)
with every value bound as a :f_<idx> named parameter — never interpolated.
member/dimension gated by DIMENSION_NAME_PATTERN; AND/OR composition; depth
capped at 8, enforced twice (iterative pre-Zod preCheckFilterDepth + renderer
re-check). Static (registration-free) request schema: operator enum,
per-operator value cardinality, group/values/limit caps. No name allowlist.

timeGrain application deliberately held (parsed + grammar-gated, not yet
applied) pending the grain-target decision — lands in phase 2b.

Co-authored-by: Isaac
Signed-off-by: Atila Fassina <atila@fassina.eu>

* chore(appkit): apply timeGrain via explicit timeDimension (PR2 phase 2b)

Resolves the grain-target gap: PR2 drops the registry that #341 used to infer
which dimension was temporal, so the grain's target is named explicitly. New
optional `timeDimension` request field; when `timeGrain` is set, that column
renders as date_trunc('<grain>', <col>) AS <col> (grain single-quoted literal
gated by TIME_GRAIN_PATTERN; column gated by DIMENSION_NAME_PATTERN — neither
can be a bind param). Other dimensions render bare. Two 400 rules in the
request schema superRefine: timeGrain without timeDimension; timeDimension not
in dimensions. Warehouse remains the authority on column temporality.

Deviates from the PRD's "date_trunc on time-typed dimensions" wording (which
presupposed the deleted registry) — the explicit-field shape is the runtime
query shape PR2 settles; PR5's hook contract inherits the timeDimension arg.

Co-authored-by: Isaac
Signed-off-by: Atila Fassina <atila@fassina.eu>

* chore(appkit): metric OBO dispatch + cache-key isolation (PR2 phase 3)

Lane dispatch from the registration (metric-views.json executor), not a URL
segment: OBO-lane metrics run on-behalf-of the requesting user via asUser(req)
with a per-user cache scope; SP-lane metrics run as the app service principal
with a shared scope. Executor + key computed inside a try so a missing/
whitespace OBO identity (from asUser/resolveUserId/deriveMetricExecutorKey)
lands on the canonical 401 envelope, not an uncaught 500.

composeMetricCacheKey builds metric:{key}:{argsHash}:{executorKey} over
canonicalized args (sorted measures/dimensions, order-independent filter
fingerprint via canonicalizeFilter, grain, timeDimension, limit).
deriveMetricExecutorKey returns "sp" for SP and a sha256 of the trimmed user
identity for OBO — the raw email/principal never enters the cache layer.

Co-authored-by: Isaac
Signed-off-by: Atila Fassina <atila@fassina.eu>

* chore(appkit): harden metric route lookup, arg uniqueness, sort key, format

Four defensive fixes to the PR2 metric-view runtime surfaced by adversarial
review:

- Registry lookup (B): build the registry with a null prototype and gate the
  route read with Object.hasOwn, so a metric key colliding with an inherited
  Object.prototype member (__proto__, constructor, toString, …) can no longer
  resolve to a truthy non-registration and bypass the unknown-key 404.

- Measure/dimension uniqueness (C): reject a name that repeats within measures,
  within dimensions, or across both. Measures and dimensions alias to their own
  name in the SELECT list (MEASURE(x) AS x, x), so a duplicate collapses to one
  row-object key and silently drops a value during row materialization.

- Filter sort key (D): delimit the (member, operator) sort key with "/" instead
  of a bare concatenation, so two distinct pairs cannot map to the same key and
  fork the cache on semantically equal filters. Matches the delimiter
  canonicalizeFilter already uses for its leaf fingerprint.

- Format (F): reject any format other than JSON_ARRAY (legacy aliases
  normalized first). The metric route delivers JSON rows only at v1; an Arrow
  request previously returned JSON silently instead of failing loud.

Adds 10 tests covering all four.

Co-authored-by: Isaac
Signed-off-by: Atila Fassina <atila@fassina.eu>

* chore(appkit): unify metric-view FQN grammar + quote at SQL interpolation

The runtime, the shared schema, and the type-generator disagreed on what a
metric-view `source` FQN may contain. The shared schema and typegen accept the
full UC quoted-identifier grammar (hyphens, non-ASCII) and typegen backtick-
quotes before interpolation; the runtime used a narrower ASCII allowlist
(assertSafeFqn) AND interpolated the FQN UNQUOTED. So a documented-legal UC
name like `prod-data.analytics.revenue` passed config + type generation but
threw (or, if the pattern were widened, emitted invalid SQL) at request time.

Converge on one grammar + one escaper:

- Move `isValidFqn` (three-part UC predicate) and `quoteFqnForSql` (backtick
  escaper) into the shared zod-free leaf `metric-fqn.ts`, beside
  `UC_FQN_PATTERN`. Grammar and quoting now have a single home both the
  type-generator and the analytics runtime import. `describe.ts` and
  `config.ts` import them back; `mv-registry.test.ts` imports the escaper from
  the leaf.
- Runtime `buildMetricSql` replaces the narrow `assertSafeFqn` regex with
  `quoteSafeFqn`: validate via `isValidFqn`, then interpolate the
  `quoteFqnForSql`-escaped FQN. Quoting is the injection boundary, so the
  runtime accepts exactly what UC (and the schema, and typegen) accept.
- `UC_THREE_PART_FQN_PATTERN` stays in `metric-source.ts` so zod still emits a
  JSON-schema `pattern`; it derives from the same per-segment charset as
  `isValidFqn`, so the two shapes cannot diverge.

Also folds in cache-key hardening (in-flight): salt the metric cache key with
`source` so repointing a metric key to a different FQN cannot stale-serve, and
only salt `timeDimension` when `timeGrain` is set (it has no SQL effect
otherwise). `renderDimensionClause` re-gates the grain against
TIME_GRAIN_PATTERN at its interpolation point.

Tests: requote the ~13 emitted-FROM assertions; add regression tests that a
hyphenated FQN is accepted+quoted and that a backtick-bearing segment is
neutralized by doubling.

Co-authored-by: Isaac
Signed-off-by: Atila Fassina <atila@fassina.eu>

* chore(appkit): load metric registry lazily with an mtime-validated cache

The metric registry memoized both success AND failure on the first
`/metric/:key` request and never re-read: editing `metric-views.json` needed a
server restart, and a transient first-request error latched a 503 forever —
unlike the sibling `.sql` query path, which re-reads `config/queries/` per
request. It was also a synchronous `readFileSync`, blocking the event loop for
every request (metric or not) under load.

Match the `.sql` path's behavior, then beat its per-request cost:

- `loadMetricRegistry` is now async (`fs.promises`) and stays a pure, stateless
  parse. Metric views are already heavier than a plain query on the warehouse
  side, so the SDK layer must not add a blocking read.
- New `getMetricRegistry(dir)` wraps it with a module-level cache keyed by the
  queries DIR (not the plugin instance): the registry is a pure function of the
  config file, warehouse-independent, so two plugins at one dir share one parse.
  Each request does a single async `stat`; the read + JSON.parse + zod
  validation are skipped when the file's (mtimeMs, size) signature is unchanged
  — steady-state cost is below the `.sql` path (a stat, not a full read).
- Failures are NOT cached (cache populated only on a successful parse), so a
  fixed config self-heals on the next request; an edit bumps mtime so a working
  config hot-reloads; an absent file stays dormant (ENOENT → empty registry).
- Deletes the `metricRegistry` / `metricRegistryLoadError` fields and the
  `_getMetricRegistry` memo; the route calls `getMetricRegistry` in a try/catch
  → 503 on throw.

Registry loading is now exercised through real files: `AnalyticsPlugin` takes a
test-only `queriesDir` config (documented `@internal`), so tests point the
loader at a temp dir and drive the real stat→read→cache path instead of poking
private state. Removes the `setRegistry` backdoor. Adds hot-reload, self-heal,
and mtime-cache tests.

Co-authored-by: Isaac
Signed-off-by: Atila Fassina <atila@fassina.eu>

* chore(appkit): quote measure/dimension identifiers + close review-round-4 gaps

Third adversarial review round (two reviewers, deduped). Highest-priority
finding: the FQN grammar drift fixed earlier was still present for
measures/dimensions/filter-members — the type-generator emits DESCRIBE column
names verbatim into the generated measureKeys/dimensionKeys unions, but the
runtime gated them on the narrow /^[a-zA-Z_][a-zA-Z0-9_]*$/ and interpolated
bare, so a UC column like `net-revenue` / `café_sales` typechecked yet 500'd at
runtime (generate-but-500, same class as the FQN bug).

A+C — quote column identifiers + validate early:
- Add `isValidColumnName` + `quoteIdentifier` to the shared zod-free leaf.
  `quoteIdentifier` is the single-identifier escaper (does NOT split on `.`, so
  a column literally named `net.revenue` becomes one delimited identifier);
  `quoteFqnForSql` now maps it over dot-split segments. The column grammar is
  the full delimited-identifier set — reject only control/newline (what cannot
  be safely quoted), matching exactly what typegen can emit.
- Runtime backtick-quotes measures (MEASURE(`x`) AS `x`), dimensions, the
  date_trunc column, and filter members. Quoting — not a narrow allowlist — is
  the injection boundary, so an injection-shaped name is neutralized (inert
  quoted column), not rejected. Row-key preservation holds: the warehouse
  reports the aliased column under the unquoted name, so `{ "net-revenue": … }`.
- Move identifier validation into `validateMetricRequest` (via a refine on
  measures/dimensions/timeDimension/filter.member) so a malformed identifier
  returns the canonical 400 instead of failing inside the SSE execute path as a
  retried 500 (finding C). The builder keeps its checks as defense-in-depth.
- Delete the now-unused MEASURE_NAME_PATTERN / DIMENSION_NAME_PATTERN.
- Fixed a regression this introduces: the filter sort key and canonicalize
  fingerprint used a "/" delimiter that was safe only while members couldn't
  contain "/"; now that members accept the full grammar, both JSON-encode the
  tuple so distinct (member, operator[, values]) pairs can't collide and
  fork/merge cache entries.

B — registry cache signature adds ctimeMs (from the same stat): a same-size,
same-mtime edit (equal-length source/executor swap on a coarse-mtime FS) now
invalidates, closing a stale-source/stale-lane serve. + same-size regression
test.

D — runtime config caps now match the type-generator: MAX_METRIC_VIEWS (200)
and per-segment length (255) enforced via the shared schema's superRefine, plus
a declarative maxLength (767) on `source` (the only cap `z.toJSONSchema` can
serialize — regenerated metric-source.schema.json). Refinements are invisible
to JSON-schema generation, so runtime + typegen stay the authoritative gates.

E — export test-only `__resetMetricRegistryCache` so cache isolation between
tests is intentional, not an accident of unique temp dirs. F — comment that a
non-ENOENT stat error is deliberately fatal-per-request (self-heals next call).

Co-authored-by: Isaac
Signed-off-by: Atila Fassina <atila@fassina.eu>

* refactor(appkit): split metric runtime modules

Signed-off-by: Atila Fassina <atila@fassina.eu>

* refactor(appkit): derive metric operator sets from shared bases

Remove the duplicated operator lists in mv/constants.ts: SINGLE_VALUE_OPERATORS
spreads STRING_OPERATORS, and METRIC_FILTER_OPERATORS derives from the union of
SINGLE_VALUE / LIST_VALUE / NULL sets rather than re-listing all twelve names.
One source of truth per operator category; the twelve-operator tuple can no
longer drift from the per-category sets.

Co-authored-by: Isaac
Signed-off-by: Atila Fassina <atila@fassina.eu>

* chore: adjust comments

* docs: metric-view runtime endpoint (#484)

* docs(appkit): document the metric-view runtime endpoint

Signed-off-by: Atila Fassina <atila@fassina.eu>

* fix(analytics): reject empty and-group in metric filter validation

An empty `and` group contributes no constraint and renders to no WHERE
clause — identical SQL to omitting `filter` entirely — but it canonicalizes
to a distinct cache key (`and()` vs `_`), needlessly splitting the cache
across semantically identical requests. Reject empty groups of either kind
so request shape maps one-to-one to cache key.

Addresses GitHub Copilot review finding on PR #474.

Co-authored-by: Isaac
Signed-off-by: Atila Fassina <atila@fassina.eu>

* refactor(analytics): route metric-view config reads through AppManager

The metric-view runtime read config/queries/metric-views.json via a
bespoke fs loader that ran parallel to AppManager, the gateway the
analytics plugin already uses for its sibling .sql query files. Centralize
the read through AppManager so both file types share one dev-aware read
path and one source of truth for the queries directory.

- AppManager gains readConfigFile(fileName, req?, devFileReader?): a
  narrow, domain-agnostic single-file read that applies the existing
  traversal guard and switches dev-tunnel vs direct-fs like getAppQuery.
  Returns null only for a genuine not-found and throws on any other error,
  so callers keep the dormant (404) vs unreadable/malformed (503,
  self-heals) distinction. Adds isDevRequest(req) and a read-only
  queriesDir getter; the dir is now overridable only via a test-only
  constructor arg.
- The metric registry reads through AppManager but keeps ownership of
  freshness (fs.stat) and parse+cache. Production behavior is unchanged
  (stat-signature cache preserved). A ?dev request bypasses stat+cache and
  re-reads every request, so a developer's local metric-views.json edits
  now hot-reload over the dev-remote tunnel the same way .sql edits do —
  the first dev-remote support for metric views.
- Delete the misleading test-only IAnalyticsConfig.queriesDir field, the
  plugin's _queriesDir, and the duplicated mv/constants.ts QUERIES_DIR;
  the route now reads through the plugin's shared this.app. Trim an
  orphaned FQN-check comment in the typegen config reader.

Resolves the PR #474 review: the AppManager-centralization thread (five
comments) and the orphaned-comment nit; the queriesDir-JSDoc finding is
resolved by deleting the field.

Co-authored-by: Isaac
Signed-off-by: Atila Fassina <atila@fassina.eu>

* fix(analytics): json-encode measures/dimensions in metric cache key

`composeMetricCacheKey` joined the sorted measures/dimensions lists with a
raw `.join(",")`. A comma is a legal identifier character —
`isValidColumnName` rejects only control characters and newlines — so
`["a,b"]` and `["a","b"]` produced the same key element while generating
different SQL, which could serve wrong cached rows. Encode each list with
`JSON.stringify` instead, matching the collision-safe encoding
`canonicalizeFilter` already uses for predicate members, so the cache key
stays one-to-one with the generated SQL.

Co-authored-by: Isaac
Signed-off-by: Atila Fassina <atila@fassina.eu>

* fix(analytics): render empty AND filter group as vacuous-true

`renderFilter` emitted `1 = 0` (vacuous-false) for an empty OR but returned
`null` for an empty AND, which the parent group drops. Dropping is only
correct at the top level: nested in an OR, an empty AND is identity-true, so
`TRUE OR P` must match all rows — but the drop collapsed it to `P` and
under-returned. Emit `1 = 1` for empty AND, parallel to the empty-OR
sentinel, so the group renders its Boolean identity element and is correct
in any position.

Unreachable through the route today (the validator rejects empty groups
with 400 before the renderer runs), but `renderFilter`'s empty-group
handling exists as the defense-in-depth fallback for a bypass, so it must
be semantically correct too.

Co-authored-by: Isaac
Signed-off-by: Atila Fassina <atila@fassina.eu>

* chore: adjust comments

* refactor(analytics): read metric-views.json per request, drop mtime cache

Remove the module-level mtime-validated registry memo (getMetricRegistry +
metricRegistryCache Map + __resetMetricRegistryCache) and have the metric
route call loadMetricRegistry directly, reading + parsing the config once per
request — matching the sibling `.sql` query path, which already re-reads config
every request with no cache. Per-request re-read still delivers hot-reload /
self-heal / dormant semantics; the parse cost on a <=200-entry config is
negligible next to the warehouse round-trip.

Deleting the cache removes its duplicate fs.stat ENOENT check, so absent-file
classification now flows through a single owner (readConfigFile ->
isNotFoundError) inside AppManager. isDevRequest, whose only external caller was
the deleted cache, is demoted to private; isNotFoundError (incl. its dev
message-match branch) is kept intact so dev and prod agree an absent config is
dormant.

Drops the now-unused RegistryCacheSignature type and the cache-mechanism tests
(mtime/ctime revalidation, dev-remote uncached suite); keeps the self-heal /
hot-reload route tests as the regression guarantee for per-request re-read.

Co-authored-by: Isaac
Signed-off-by: Atila Fassina <atila@fassina.eu>

* chore(appkit): relocate metric-view config to config/metric-views/definitions.json (#487)

Move the metric-view declaration file out of the queries folder into its own
config surface: config/queries/metric-views.json -> config/metric-views/definitions.json.

Metric views are independent of .sql queries, so both the type generator and
the runtime now gate on the metric-views folder rather than the queries folder:

- AppManager gains a metricViewsDir (sibling of queriesDir) and a
  readMetricViewsConfig() reader; the traversal guard is generalized to any
  base dir.
- type-generator/vite-plugin accept an explicit metricViewsFolder, activate on
  either config surface, and watch config/metric-views/definitions.json by
  directory (not bare basename).
- generate-types CLI generates when either config/queries or
  config/metric-views exists.
- Runtime registry reads definitions.json via readMetricViewsConfig.
- Update dev-playground config, docs, and the JSON schema description.
- Template ships config/metric-views/definitions.json guarded by
  {{if .plugins.analytics}} (same pattern as the config/queries/*.sql files),
  so it only scaffolds into apps that enable the analytics feature.

Co-authored-by: Isaac

Signed-off-by: Atila Fassina <atila@fassina.eu>

* refactor(analytics): hoist definitions.json filename to shared METRIC_CONFIG_FILE

Dedupe the four value-uses of the "definitions.json" basename onto a single
`METRIC_CONFIG_FILE` in the zod-free shared `metric-fqn` module: the analytics
runtime (mv/constants), the type-generator (config + vite-plugin), and the
generate-types CLI. Keeps one source of truth without pulling zod into the
type-generator's locked dependency graph. Addresses PR review feedback.

Co-authored-by: Isaac
Signed-off-by: Atila Fassina <atila@fassina.eu>

---------

Signed-off-by: Atila Fassina <atila@fassina.eu>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants