Document references and labels
Updated
A saved document can appear in places that were not modelled as a typed document
reference: register sources, report group keys, settings payloads, custom form
widgets, or a generic Guid column. Those values read like documents when they
really are document row ids, without adding a new field kind to the metadata
system.
Why there is no new field kind#
The storage invariant did not change: a reference-like value in ZuloOne is still
a bare Guid. A document row lives in the table of its document type, and the
key alone does not name that table. Adding a DocumentReference EDT would have
solved only future hand-authored fields and would have left the important cases
unfixed: report cells, register sources and old custom payloads that already
carry document ids.
Instead the renderer treats every non-empty Guid as a candidate. If the server
can prove that the id belongs to a readable document, the UI shows a document
label and can open the card. If it cannot, the original value remains visible.
That fallback is important: a wrong label is worse than a raw id.
How a label is built#
POST /api/documents/resolve-labels accepts a batch of ids and returns only the
ones it can name for the caller — documents first, then
dictionary records and metadata ids:
{ "ids": ["7d8b4b7a-7f6b-4d2b-9a9e-1b0d5b84e6aa"] }
Each result includes the document row id, the owning document type id, the
localized document type caption, the human number and the document date. The
frontend DocumentReference renders that compactly as:
Sales realization № 100045 · 2026-10-01 a document row
Zhytomyr Corner Shop a dictionary record (its display format)
Sales realization a metadata id (the object type's caption)
The candidates are every GUID-valued field on the card — Guid fields and
also String/textarea fields whose value is a GUID. The Tax model is the case
that forced the widening: TaxDocument.SourceDocumentId (a document),
SourceDocumentType (a document TYPE id, stored as text), TaxSubmission.SourceId
(a tax return) and TaxMapping.SourceId (a customer) would otherwise show raw ids.
Editable cards use the same resolved text as the field value. They do not show a
second raw GUID input next to a successful label: the raw id is behind the small
Edit raw id affordance. Press it when you need to paste or repair the stored
id; Esc or leaving the raw input returns to the label view. If the value does
not resolve — unknown id, unreadable object or a non-GUID string — the renderer
keeps the ordinary text input so the stored value stays visible and editable.
Document labels remain links and open the referenced document card; dictionary
record and metadata labels are plain text.
The shared DocumentLabelResolver also serves report code. Its full formed name
has more context because grouped reports need it: document type caption,
localized subtype caption when present, ID (falling back to Number) and
DocumentDate. That format stays server-side so reports and labels do not grow
separate rules for the same document identity.
Permission stance#
The endpoint is not a privileged id oracle. For every candidate document type it
checks the same Read permission as opening the document card. Unknown ids,
ids from unreadable document types and non-document ids are omitted from the
response, not reported as a distinguishable error. A mixed batch therefore gives
usable labels for allowed documents without teaching the browser which forbidden
document types exist.
Batching and ownership lookup#
The client deduplicates and sorts ids before calling the endpoint, and caches the answer briefly. The server has no central "document row id → document type" index, so it walks readable document types. The important performance rule is one query per type over the still-unresolved ids, chunked at 500 ids, not one round trip per grid cell. The walk stops as soon as every id has an owner.
What a metadata author should do#
Nothing. Do not add a new EDT, do not mark a form field, and do not wrap values
in a { type, id } object. Any Guid value that is the MetaId of a document
row can be labelled by the UI. A form layout author only decides where the field
or widget appears; the label resolver is platform behaviour.
Traps#
- Do not infer type from the field. Generic report cells and register source ids often have no field metadata at all. The resolver must discover ownership from the stored row id.
- Do not turn an omitted result into "not found" UI. Omitted also means "not allowed" or "not a document". Keep the raw value.
- Do not add per-cell calls from a grid. The endpoint exists because a report page can carry hundreds of document ids.
- A GUID-looking string is still just a string. External ids, UUIDs from a tax authority or a package id stay raw because the resolver finds no owner — never because the UI guessed. Do not add "looks like a GUID" heuristics beyond asking the resolver.
What's not there yet#
- There is no metadata-declared
DocumentReferencefield kind. - The endpoint does not expose subtype captions to the frontend compact label;
report labels still have subtype context through
DocumentLabelResolver. - Typed dictionary references keep their own picker controls; the resolver only labels GUIDs that are not already a typed reference.
- Ids that point to something other than a document, a dictionary record or a metadata object (for example a register row) stay raw.
Where next#
- The forms system — where record forms render field slots.
- Register totals layouts — reports that use the same document label resolver.
- Platform architecture — why records and metadata travel as ids and captions, not generated UI code.