DDS Report Data Fields
▶Report Data Fields
report_data_fields is a per-version projection mechanism on a DDS record. You declare a list of "key, paths" entries on the version; at report-build time, each plot's data_fields block surfaces one entry per projection — the resolver tries each path against the plot's raw upload row and the first non-null hit wins.
This lets you carry arbitrary caller-supplied CSV columns (a client_id, an internal_lot_id, a harvest_batch, a regulatory code) into the DDS report without a per-customer schema change in Epoch. The projection list is versioned alongside the DDS — re-cutting the version inherits the prior list unless you replace it.
▶Wire shape
Each entry:
{
"key": "client_id",
"paths": ["clientId", "client_id"],
"label": "Client ID"
}
| Field | Type | Notes |
|---|---|---|
key | string | The output key in the report's per-plot data_fields block. Pattern ^[a-z][a-z0-9_]*$, ≤128 chars. |
paths | string[] | Dotted-path lookups against the resolution root (see scope), tried in order. First non-null wins. 1–16 entries; each path ≤512 chars. |
label | string | omitted | Optional display label for the report renderer. 1–256 chars. |
scope | "plot" | "facility" | Which upload row paths resolve against. Defaults to "plot". See below. |
The list itself is capped at 64 entries per version.
▶What the paths resolve against (scope)
scope)-
scope: "plot"(default) — each path is a dotted lookup against the plot's ownasset_entity.attributes— the raw upload row thatPOST /ingestions/assetswrote for that plot. -
scope: "facility"— paths resolve against the attributes of the facility the plot maps to, resolved by walking from the plot up through its supply shed to the facility upload row. This lets a column uploaded on a facility file surface on every plot that rolls up to it.Precondition. A plot only has a facility when its supply shed carries a
facility_id— i.e. a facility file was ingested and matched. A plot-only upload leaves the shed'sfacility_idNULL, so facility-scoped projections resolve tonullfor those plots (the key is still emitted). Plots always get a supply shed during normalisation; the facility link is what requires a facility ingestion.
The lookup is pure object-key traversal — no array index syntax ([]), no function calls. Missing keys return null and the resolver advances to the next path.
If every path misses (or the scope's root is absent), the report still emits the key — with value null — so the projection list is observable on the report shape regardless of resolution outcome.
▶Configuring the list
▶At create
Include report_data_fields on the body of POST /regulations/eudr/audit-trail or POST /regulations/eudr/audit-trail/:recordId/tasks (the version-cut endpoint). Omitting it inherits from the previous version when cutting; defaults to an empty list on create.
POST /regulations/eudr/audit-trail
Content-Type: application/json
{
"data": {
"type": "dds_record",
"attributes": {
"filter": { "assets.task_id": { "in": ["..."] } },
"operator": { ... },
"commodity": { ... },
"report_data_fields": [
{ "key": "client_id", "paths": ["clientId", "client_id"], "label": "Client ID" },
{ "key": "harvest_batch", "paths": ["harvestBatch", "batch_id"] }
]
}
}
}
▶Tri-state semantics on version cut
| Body value | Behaviour |
|---|---|
| field omitted | Inherit — v_{n+1} carries forward v_n's list verbatim. |
report_data_fields: [] | Clear — v_{n+1} starts with no projections. |
report_data_fields: [...] | Replace — v_{n+1} uses the supplied list. |
The cut path is the only place inherit/clear/replace are distinguished. PUT .../data-fields (below) is full-replacement only.
▶Replace on the editable version
PUT /regulations/eudr/audit-trail/:recordId/data-fields
{
"data": {
"type": "dds_record_data_fields",
"attributes": {
"report_data_fields": [
{ "key": "client_id", "paths": ["clientId"] }
]
}
}
}
PUT (not PATCH) is deliberate — the wire intent is unambiguous full-replacement of the list on the record's current editable version. Empty array clears all projections.
▶Read the current list
GET /regulations/eudr/audit-trail/:recordId/data-fields
Returns the projection list as currently stored on the record's CURRENT version (the most-recent version_number), plus debuggable resolution metadata per entry where available.
▶How it surfaces in the report
The eudr_default_v1 report builder emits a data_fields block on every
plot_register entry (articles.deforestation.plot_register[]):
{
"plot_id": "11111111-1111-4111-9111-111111111111",
"...": "...",
"data_fields": {
"client_id": { "value": "ACME-7331", "label": "Client ID", "matched_path": "clientId" },
"harvest_batch": { "value": null, "label": "Harvest Batch", "matched_path": null }
}
}
Every projection key is present on every plot. Each entry carries:
| Field | Meaning |
|---|---|
value | First non-null match across the entry's paths against the plot's asset_entity.attributes; null when none resolved. |
label | The projection's configured display label; null when unset. |
matched_path | Which candidate path resolved; null on a miss. |
data_fields is {} when the version has no projections configured.
▶Examples
▶Pulling a client identifier
Some uploads carry a clientId column in the raw row. To surface it on the DDS report:
{ "key": "client_id", "paths": ["clientId", "client_id"], "label": "Client ID" }
Both spellings are tried in order — clientId first, client_id fallback.
▶Versioned migration
When a new mapper renames partId → part_id, add the new spelling to the front of the path list and re-cut the version:
{ "key": "part_id", "paths": ["part_id", "partId"] }
Old plots whose upload row still has partId keep resolving; new plots resolve through the new column. No backfill required.
▶Constraints recap
- ≤ 64 entries per version
- 1–16 paths per entry
key≤ 128 chars, matches^[a-z][a-z0-9_]*$- Each path ≤ 512 chars
label1–256 chars if supplied; otherwise omitted
Validation errors surface as 400 with the field path and a human-readable reason. The handler-side Zod schema is the only producer of valid payloads; the worker re-parses to defend against in-flight payloads but does not re-litigate the regex/cap rules.