Runtime & sources
A report page makes two calls: one for its structure, once, and one for its data on every filter change. This page covers what happens on the second.
The two calls
Section titled “The two calls”| Call | Returns | Gate |
|---|---|---|
GET /api/v1/genie/reports/{key} |
Filters, widgets, layout, dataset names and modes. SQL-free. | Authentication only |
POST /api/v1/genie/reports/{key}/data |
Every dataset a visible widget reads, executed once each | List on the report’s resource |
{key} is the report’s Name or Slug. Structure is cached client-side for the life of the page, so
applying a filter re-runs only the data call. See
report endpoints for the payloads.
The structure response is projected through the same [SqlBody] stripping that object metadata uses, so
no query text ever reaches a browser.
And two more, for an author’s draft preview
Section titled “And two more, for an author’s draft preview”An unpublished draft is rendered by a matching pair on the designer’s controller:
| Call | Returns | Gate |
|---|---|---|
GET /api/v1/genie/report-designer/definitions/{key}/draft/structure |
The draft’s structure. SQL-free, same projection. | Update on ReportDefinitions |
POST /api/v1/genie/report-designer/definitions/{key}/draft/data |
The draft’s datasets | Update on ReportDefinitions |
Separate endpoints rather than a ?draft=1 flag on the pair above, for two reasons. The published
structure call is ungated beyond authentication — correct for a live layout, a disclosure for
someone’s work in progress. And that route’s query string is the report’s filter namespace, which an
author may legitimately declare a filter named draft in.
Both require Update, matching each other deliberately: a weaker gate on structure would let a client
render a layout it can never fill, which reads as a bug rather than as a permission.
Everything else about execution is identical to the published path — the same read-only guard, the same
{{permission-filter}} requirement, the same row caps. A preview whose rows differ from production’s
would be worse than no preview.
What actually gets executed
Section titled “What actually gets executed”Per request, in order:
- Resolve the definition (from the parse cache when warm).
- Authorize
List— after resolution, so an unknown report is a 404 rather than a 401. - Drop widgets whose
RolesAllowedexcludes the caller. - Sanitize the caller’s arguments against the declared filter names.
- Bind every declared filter, as SQL
NULLwhen blank. - Gate on a blank
Requiredfilter — returning with no dataset executed. - Seed session parameters and the caller’s capabilities.
- Execute each dataset a surviving widget reads — deduplicated.
Two consequences worth designing around:
- A dataset no visible widget reads is never queried. A
<DataSet>left declared but unreferenced costs nothing, and a widget withheld byRolesAllowedtakes its dataset with it. - A shared dataset is queried once. Three tiles over one
SingleRowdataset is one round trip.
An embedded <View> is absent from all of this: it fetches through the ordinary object endpoints under
its own resource.
Parameters available to a dataset
Section titled “Parameters available to a dataset”| Parameter | Source |
|---|---|
@{FilterName} |
Each declared filter. A DateRange binds @{Name}From and @{Name}To. |
@SessionUserId, @SessionCompanyId, @SessionTimeZone, … |
The session parameter providers, the same set a view’s SQL sees. |
@PermissionCapabilities |
A comma-separated list of the caller’s capability names, so a dataset can gate on a business operation. |
Anything the caller sends that the report does not declare is dropped before a single query runs —
the same ParameterSanitizer the grid uses. Caller-supplied Session* keys are always discarded; the
framework’s own are seeded afterwards.
Sources
Section titled “Sources”A dataset can point at a named, host-owned datasource rather than the app’s own database:
<DataSet Name="DsFlow" Source="InventoryReports"> <![CDATA[ SELECT … ]]></DataSet>The host resolves that name under Genie:ReportingSources:
"ConnectionStrings": { "SqlServer": "Server=.;Database=InventorySample;Integrated Security=true;…"},"Genie": { "Sources": { "InventoryReports": { "Dialect": "SqlServer", "Connection": "SqlServer" } }}| Key | Meaning |
|---|---|
Dialect |
SqlServer, PostgreSql or ClickHouse. |
Connection |
Either a ConnectionStrings key (as above) or a literal connection string. |
Connection is looked up as a name first; a miss means the value is the connection string. That is
unambiguous in practice — a literal contains = and ; and can never be a key. An empty resolution
throws, naming the source.
In code, UseSource(name, dialect, connection) on the GenieBuilder overrides a configured entry,
the same way UseConnectionString does.
Why the indirection. It keeps environment detail out of the model file: the same *.report.xml
imports into staging and production and reads each one’s own database. It is also where a report’s
read-only guarantee really comes from — point a source at a least-privilege read-only login and no
authoring mistake can write.
An unknown source name throws, listing what is configured. It deliberately does not fall back to the app’s own database: that would read the wrong data and look like it worked.
Row caps
Section titled “Row caps”| Mode | Rows read |
|---|---|
SingleRow |
1 |
MultiRow |
1000 |
The cap is enforced while reading the result, not by rewriting the SQL. A report dataset orders in
its own ORDER BY (a widget never sorts), and on SQL Server an ordered query cannot be wrapped in a
derived table — so a SELECT TOP (n) * FROM (…) cap would break exactly the datasets that need one.
The MultiRow cap is a backstop against an unbounded scan reaching the browser, not a substitute for
authoring a sensible query. A dashboard widget is a top-N list: say TOP 10 in the SQL.
SELECT-only
Section titled “SELECT-only”Every dataset is checked before it reaches a database — at import and again at execution. A query is rejected when it:
- is empty;
- contains more than one statement (a single trailing
;is tolerated); - contains a SQL comment (
--,/* */) or a dollar-quoted body — both can hide keywords from the check, so describe the query in an XML comment instead; - uses
SELECT … INTO, which writes a table; - uses any write, DDL or execution verb (
INSERT,UPDATE,DELETE,MERGE,DROP,EXEC, …); - reads a system catalog;
- contains no
SELECTat all.
This is a guard, not a sandbox. It stops the obvious mistake and the obvious smuggling attempt; the real boundary is the source’s own permissions.
Row-level security
Section titled “Row-level security”A report honours the row filter attached to the caller’s List capability, but the author must say
where it goes, using the placeholder:
<DataSet Name="DsRows"> <![CDATA[ SELECT p.Name, p.Sku FROM [Inventory].[Products] p WHERE p.IsDeleted = 0 AND {{permission-filter}} ORDER BY p.Name ]]></DataSet>With no filter resolved the placeholder becomes (1=1), leaving the query shape intact.
If a filter is resolved and the dataset has no placeholder, the request fails with a message
naming the dataset. A grid appends a residual predicate through its query builder; a report has none,
and wrapping the SQL would break the ORDER BY a report dataset relies on. Failing loudly is the only
remaining honest option — the alternative would be quietly serving unscoped rows.
Report datasets are read-only, so only the Filter stage of row security applies; conditions and constraints guard writes, and a report has none.
A draft preview is not exempt from any of this. The filter is resolved against the report’s own resource name — the stored row’s, never the draft document’s, which the author is free to change — so renaming a draft is not a way out of a row filter. And the missing-placeholder failure applies too, where it is more useful than in production: it is the earliest moment an author can discover the omission, and it turns a security bug into an authoring error at design time.
Where to go next
Section titled “Where to go next”- Authoring a report — the
*.report.xmlreference. - Report pages overview — import, versioning and permissions.
- Configuration — every
Genie:*section, includingSources.