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"
}
FieldTypeNotes
keystringThe output key in the report's per-plot data_fields block. Pattern ^[a-z][a-z0-9_]*$, ≤128 chars.
pathsstring[]Dotted-path lookups against the resolution root (see scope), tried in order. First non-null wins. 1–16 entries; each path ≤512 chars.
labelstring | omittedOptional 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: "plot" (default) — each path is a dotted lookup against the plot's own asset_entity.attributes — the raw upload row that POST /ingestions/assets wrote 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's facility_id NULL, so facility-scoped projections resolve to null for 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 valueBehaviour
field omittedInherit — 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:

FieldMeaning
valueFirst non-null match across the entry's paths against the plot's asset_entity.attributes; null when none resolved.
labelThe projection's configured display label; null when unset.
matched_pathWhich 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
  • label 1–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.