# GB5 BI Platform: Developer Playbook

The architecture, the reasoning behind it, and how to extend it. Read
[Analytics_Reference.md](Analytics_Reference.md) alongside this for exact routes/DTOs/file paths.

## The layered architecture

```
Backend (gb5 repo)                                   Frontend (gb4.7mfe repo)
───────────────────                                  ────────────────────────
MBICATALOG (dataset registry, 3 kinds)         ──►    Portlet config screen
  ├─ Warehouse: MWAREHOUSEFACT/MEASURE                (pick catalog, pick/author
  ├─ ApiService: MBIFIELDMAPPING                       a BI View, field-mapping
  └─ AnalysisQuery: MANALYSISQUERYFIELDS (read-only)    review, live preview)
MBIVIEW (saved Dimension/Measure/WidgetType
  definition, independent of any Portlet)
        │
        ▼
IDatasetResolver.ResolveAsync
        │
        ▼
RunBIQuery (AnalysisAggregationService)         ──►    Gb5AnalysisAggregationService
        │                                                       │
        ▼                                                       ▼
AnalysisResult (flat JSON, always)              gb5-widget (resolvedType() picks an
                                                 adapter by dimension count, or a BI
                                                 View's own WidgetType wins outright)
                                                       │
                             ┌─────────────┬───────────┼───────────┬─────────────┐
                             ▼             ▼           ▼           ▼
                       kpi-card-adapter  apex-charts- table-adapter pivot-table-
                       (0 dims)          adapter (1)  (2+ dims)     adapter (explicit
                                                                    override only —
                                                                    client-side pivot,
                                                                    see below)
```

`gb5-widget` and its adapters are **dashboard-agnostic by design** — they never import
`features/common/components/gbpubsub` or `features/gbdashboard/model/idashboard`. The dashboard
integration (drill events → cross-portlet filtering) is a separate layer
(`Gb5WidgetPortletComponent`) that translates `gb5-widget`'s generic `GB5SelectionEvent` into a
dashboard-specific `DashboardWidgetInteraction`, and is also the layer that decides BIViewId-first
vs. legacy-scalar-fallback rendering. Keep this boundary when extending — a new adapter should
never need to know it might be running inside a dashboard portlet.

**Framework/Solution dependency direction — a hard constraint, not a style preference.**
`GB5Framework` (where `PortletBLL`/`PortletTypeVersionBLL` live) is foundational and must never
depend on any `GB5Solution/*` module (where `AnalyticsBLL`/`BIViewBLL` live). This is exactly why
`SupportedWidgetTypes` (see Reference) is a declarative FE-filtered allow-list, not a server-side
hard gate at Portlet-save time — enforcing it there would require Framework to call into
Solution's `BIViewBLL` to look up a `BIViewId`'s own `WidgetType`, inverting the direction. If a
future need genuinely requires that enforcement, it has to happen from the Solution side (e.g.
`BIViewBLL.Save` calling *into* something Framework exposes, never the reverse), or via an event/
message rather than a direct call.

## Adding a new adapter (the pattern all four follow)

Every adapter matches the same structural contract (`ChartAdapter` in `gb5-widget.model.ts` —
documentation-only, not compiler-enforced, since signal-based `input()`/`output()` members aren't
class-shape-checkable against a plain interface; each adapter's own `.spec.ts` is what actually
verifies it):

```ts
definition = input.required<AnalysisQueryDefinition>();
result = input.required<AnalysisResult>();
config = input<GB5WidgetAdapterConfig | undefined>();
select = output<GB5SelectionEvent>();
```

(The pivot-table adapter additionally has a `mode: 'design' | 'run'` input and a `saveLayout`
output — not part of the shared contract, since only a pivot has a concept of an editable,
savable layout. See its own Reference entry before assuming every future adapter needs these too.)

To add a new one:
1. Build the component under `features/gb5widget/adapters/<name>-adapter/`, matching the shared
   contract above exactly.
2. Add the new member to `GB5WidgetType` in `gb5-widget.model.ts`.
3. Add it to `Gb5WidgetComponent`'s `imports` array and add an `@case` to
   `gb5-widget.component.html` mirroring the existing cases exactly.
4. Decide where it fits in `resolvedType()`'s auto-resolution heuristic by dimension count, or
   leave it explicit-override-only (like the pivot-table adapter — no dimension count naturally
   implies "render this as a pivot").
5. Write a `.spec.ts` mirroring an existing adapter's test structure — this is genuinely
   load-bearing, not boilerplate. Every real bug found in this engine during live verification
   (see the Catalog's Portlet Config Screen entry) was in code that had already passed
   `tsc --noEmit` and a naive read-through; only actually exercising the DOM/component logic
   caught them. Same lesson applies to `BLL` cache-invalidation: nothing in a `dotnet build` or a
   code read catches "this write path never invalidates the read cache it should" — only calling
   the endpoints in sequence (write, then read) does. Do this for any new mutate-shaped BLL method.

## Adding a new `DatasetKind`

`MBICATALOG.DATASETKIND` has three values, two of which are real:
- `0 = Warehouse` — implemented, governed, the most performant/trustworthy source.
- `1 = AnalysisQuery` — implemented (picker branch wired up), bridges into the separate,
  pre-existing ad-hoc query designer; `QUERYTYPE=DIRECT` (raw SQL) queries are excluded pending a
  future safe-executor rewrite in that separate system.
- `2 = ApiService` — implemented, backs both the original standalone pilot flow and the newer
  "turn an existing report into a catalog" flow (`CreateBICatalogFromReportView`).

`IDatasetResolver.ResolveAsync` is the one place that branches on this — every caller (the Config
UI's picker endpoints, `RunBIQuery`) already goes through it rather than re-deriving resolution
logic. If you add a fourth kind, that's the only method that should need a new branch.

## Adding filter support

`AnalysisQueryDefinition.Filters` (`{ Field, Operator, Values }[]`) still exists in the contract
and is threaded through the FE model — nothing reads it server-side yet.
`AnalysisAggregationService`'s `ExecuteAsync` would need a `WHERE`-clause equivalent built from
`Filters`, reusing `WarehouseDAL`'s existing `ResolveDimensionColumn` allowlist-based column
resolution (never build a filter clause from raw client-supplied field names). On the FE side, the
Portlet config screen would need a filter-builder UI — there's no existing pattern in this repo
specific to reuse; `analysisquerydesigner.component.ts` has its own filter UI for the separate
ad-hoc-query system and might be worth a look for UX ideas, but don't reuse its backend.

## The pivot table: what was actually built, and why it's simpler than an earlier draft proposed

**Correction, this pass**: an earlier version of this Playbook proposed that a real pivot needed
new backend aggregation logic (grouping by a row axis, then further splitting by a column axis)
and a new 2D `AnalysisResult` shape. **That was not what got built, and isn't needed.** The actual
approach:

1. `RunBIQuery`/`AnalysisAggregationService` are completely unchanged — they still return the
   exact same flat `AnalysisResult.Data` every other adapter already consumes.
2. `AnalysisQueryDefinition.Rows`/`.Columns` (`string[]`) are read **client-side only**, by the
   pivot-table adapter, as the *initial suggested* row/column split — WebDataRocks itself does all
   the actual axis-splitting, re-aggregation, and subtotal computation in the browser, from the
   same flat data.
3. `ag-Grid`'s pivot mode is genuinely not an option here — confirmed directly from
   `ag-grid-community`'s own README, which lists Pivoting as ❌ for Community, ✅ for
   Enterprise-only (a paid license, not installed in this repo). This is exactly why a *different*
   library (WebDataRocks) was brought in for this one adapter, rather than extending the existing
   table adapter's `ag-Grid` wrapper.
4. A saved layout override (`GB5WidgetAdapterConfig.pivotSlice`) is stored on `MBIVIEW.ADAPTERCONFIGJSON`
   as plain JSON and simply substituted in ahead of the definition's own `Rows`/`Columns` on
   render — no new backend concept, just an additional field on an already-JSON column.

If a future need genuinely requires *server-side* pivoted aggregation (e.g. a pivot too large to
ship as flat rows to the browser), that would still need the backend work the original draft
described — but that's a distinct, larger, not-yet-justified effort from what's live today. Don't
conflate "client-side pivot via WebDataRocks" (done) with "backend-aggregated pivot" (not started,
not currently planned) when scoping future work here.

**A separate, pre-existing BE-side "DataPivot" mechanism must never be confused with this.**
`MREPORTVIEW.ISDATAPIVOT`/`PIVOTFORMAT` and `GB5Shared/Export/Pivot/DataPivotEngine.cs` are a
completely different, older system for the *legacy Report/View* framework — its output columns are
computed at run time from whatever distinct values exist in the data
(`"{distinctValue}_{measureLabel}"`), which is exactly why a report flagged `ISDATAPIVOT=1` is
permanently excluded from ever backing a BI Catalog (see `BICatalogBLL.CreateFromReportView`'s own
explicit rejection) — there's no stable field list to register field mappings against, structurally,
not just as a v1 gap.

## Known gotchas

- **`BaseEndpoint`'s cache key has no route/endpoint-type component of its own** — it's whatever
  string `GetCacheKey()` returns. Two different endpoints that happen to generate the same
  `KeyGeneration(objectId, objectTypeId, level, login)` inputs for the same request will silently
  serve each other's cached response. Give any new picker endpoint scoped by the same id as an
  existing one a distinguishing suffix (e.g. `"{id}-dimensions"` vs. `"{id}-measures"`).
- **A new mutate-shaped BLL method that inserts/updates/deletes must call `KeyInvalidate` for
  every read endpoint's cache key its write affects — there is no framework-level enforcement of
  this, and nothing in a normal build or review catches its absence.** Confirmed the hard way,
  twice in this engine's own history: `BIViewBLL.Save`/`Delete` and `BIFieldMappingBLL.SaveMapping`
  both shipped without this at first, and the symptom (a freshly-saved row invisible to its own
  list/picker endpoint) only surfaced when the write and the read were actually called back to
  back — not from reading the code. Reproduce the exact key each affected read endpoint's own
  `GetCacheKey()` builds (same objectId/objectTypeId/level/login inputs) and invalidate it with
  `KeyInvalidate.AllInvalidateCache(key)` after every successful write.
- **A route or resource-path rename can silently orphan a value already stored in the database.**
  This engine's own `/Analysis/*` → `/BI/*` route rename left one `MBICATALOG` row's stored
  `ApiResourcePath` pointing at the now-dead old route — a live 404 for anyone querying that
  catalog, invisible in a `dotnet build`. Any future route/path rename that a database value could
  reference (not just this engine — any stored URL/path anywhere) needs an explicit sweep for
  stored references, not just a code-side rename.
- **A report's "real" callable address is not always where you'd expect it.** `MREPORT.REPORTURI`
  is the literal sentinel `'NONE'` on the overwhelming majority of real reports — the real address
  usually lives on `MREPORT.WEBSERVICEID` → `MWEBSERVICE.URITEMPLATE` instead. But that data is,
  on GB5DEMO today, almost entirely legacy GB4 WCF `.svc` endpoints requiring base-URI and
  path-parameter resolution this engine's own HTTP-calling code doesn't do — don't assume a
  populated `WEBSERVICEID` means "callable by this engine" without checking for `{placeholder}`/
  `.svc`/`/gb4/` markers first (see `BICatalogBLL.ResolveApiResourcePath` for the exact check).
- **`Meta.MeasuresUsed[].Format` is `null` on the wire today**, even though the underlying
  measure/field-mapping DTOs have real format-string values in the database — a plumbing gap that
  predates this engine and hasn't been closed. Every adapter renders raw numeric values with no
  currency/number formatting as a result; don't assume `Format` is populated.
- **`<option [value]="numericField">` inside a reactive form silently stringifies the bound
  value** — use `[ngValue]` for any numeric (or otherwise non-string) option value bound to a
  `formControlName` select. Cost real debugging time in the Portlet config screen originally; the
  newer BI View/Widget Type dropdowns added on top of it already use `[ngValue]` correctly.
- **`computed()` does not track reactive-form control values** — `form.controls.X.value` is a
  plain mutable property read, not a signal; a `computed()` that reads it memoizes its first
  result and never updates. Use a plain method instead when a value needs to reflect current form
  state and is called from the template.
