The value item
See also: Sections — typed-column storage ·
section_record· Locator · Components
The value item is the atomic unit of stored data in Dédalo v7. Every
component's value — text, a date, an IRI, a media reference, a relation — is an
array of value items, and each item is a small object built around three
recurring keys: a server-minted id, an optional lang, and the payload. This
page documents the envelope itself: its shape, the meaning of each key, how the
payload changes per data type, raw vs. resolved access, and the changed_data
edits the client sends back on save.
This is a data-type page: it is about the format of the value, not about
any one component. For where these items physically live (the typed JSONB
columns of the matrix table) see Sections; for how a
component reads and writes its slice see section_record.
1. What it is
In classical SQL a field holds one scalar: a name column holds "Alicia".
Dédalo never stores a bare scalar. A component's value for one record is
always an array, and every position in that array is a self-describing
value item:
[
{ "id": 1, "lang": "lg-spa", "value": "L'Horta Sud" }
]
Why an array of self-describing items, even for a "single value" field?
- Multivalue by default. A component may hold several values
(several names, several links). Even a conceptually single-valued component
stores
[item]— an array of one — never the scalar itself. - Multilingual. A translatable component interleaves one item per language
in the same flat array; the
langkey tells them apart. - Stable addressing. The
idgives every item a permanent identity, so edits, dataframe attachments and Time Machine references survive reordering, pagination and re-saves. - Empty positions are real.
{"value":""}or{"value":null}are kept on purpose (never pruned), so a multivalue slot — and any dataframe attached to itsid— survives.
The item is the row of a component
Where the section is the row of the matrix
table, the value item is the row inside a component's value. A
component value is a little table of items keyed by id.
2. Canonical JSON shape
The consolidated v7 envelope is { id, lang?, <payload> }. The shape of the
payload is what differs between data types.
Literal "value-property" items
The eight models declaring importValueProperty: true
(src/core/components/types.ts, checked through usesImportValueProperty(model)
in src/core/components/registry.ts) carry their payload under an explicit
value key:
{ "id": 1, "lang": "lg-eng", "value": "Hello world" }
component_email · component_filter_records · component_info ·
component_input_text · component_json · component_number ·
component_password · component_text_area
For these models value is a scalar (string or number), except
component_json, whose value is whatever JSON was stored.
Structural items (date, iri, geo, media)
Structural components do not use a value wrapper. The payload fields are
flattened directly onto the item, next to id (and lang when translatable).
An IRI item (component_iri, stored in the iri column):
{ "id": 1, "iri": "https://dedalo.dev", "title": "Dédalo web site" }
A date item (component_date, stored in the date column — always
lg-nolan; range mode shown):
{
"start": { "year": 2012, "month": 11, "day": 7, "time": 64638475292 },
"end": { "year": 2012, "month": 12, "day": 8, "time": 64641254135 }
}
A geolocation item (component_geolocation, stored in the geo column —
always lg-nolan, GeoJSON-shaped):
{
"type": "FeatureCollection",
"features": [
{ "type": "Feature", "properties": {},
"geometry": { "type": "Point", "coordinates": [-0.36, 39.46] } }
]
}
A media item (component_image and the other media models, media column)
carries id, a quality map and file metadata directly on the item.
Structural payloads are not under value
Only the eight components_using_value_property models wrap their payload
in a value key. component_date, component_iri,
component_geolocation and the media components flatten their payload
fields directly onto the item object. Do not assume
item.value exists for those types.
Relation items (locators)
Relation components store a locator as the item — there is
no value key. The locator minimally is:
{ "type": "dd151", "section_tipo": "es1", "section_id": 3896,
"from_component_tipo": "rsc91" }
A translatable relation also carries lang; an id is optional and is used
for dataframe pairing and ordering. section_tipo and section_id are the
mandatory core of every locator; everything else is optional and
relation-flavour specific (src/core/concepts/locator.ts).
Summary of the envelope
| key | type | present when | meaning |
|---|---|---|---|
id |
int | minted on save for non-relations; optional on relations | stable per-item identity within the component value |
lang |
string | translatable components | lg-xxx or lg-nolan |
value |
scalar / JSON | only the 8 value-property models | the literal payload |
| (flattened payload) | object fields | date / iri / geo / media | iri+title, start/end, GeoJSON, media metadata |
| (locator fields) | type, section_tipo, section_id, from_component_tipo |
relation components | the locator pointing at another record |
3. The id — server-minted, stable, unique within the component
id is an integer, unique within this component's items in this record,
server-minted, and never recycled.
- It is the pairing key for dataframes — uncertainty, qualifiers and context
attach to a main item by its
id(the unifiedid_keycontract; seecomponent_dataframe). - It is the reference key for Time Machine playback.
- It is the addressing key for client edits:
update/removetarget an item byid, not by array index — which is what makes editing robust to reordering and pagination.
Minting
Whenever a component's data is saved, each item lacking a valid id (present,
non-null, non-empty) is given one. Allocation is atomic:
allocateComponentItemId() (src/core/db/matrix_write.ts) does the
increment as one UPDATE … SET meta = jsonb_set(…, count + 1) RETURNING,
relying on Postgres's own row-level lock to serialize concurrent callers —
two allocations against the same row can never observe the same
pre-increment count. The counter lives in the meta column.
Absorbing explicit ids (imports, migrations, restores)
Items that already carry an id (from an import, a migration, or restored data)
keep it. absorbComponentItemIds() (src/core/db/matrix_write.ts) collects
every incoming id and raises the counter to GREATEST(persisted, incoming max)
— never lowers it. Because ids are never recycled, dataframe id_key
pairings and Time Machine references stay valid across edits and reorderings.
4. The lang dimension
Translation is governed by the component flag
component_common::$supports_translation (independent of the ontology
$translatable flag).
- Non-translatable (
supports_translation === false): language methods collapse to the full-data path, and the single item useslg-nolan(DEDALO_DATA_NOLAN).component_dateandcomponent_geolocation, for example, always storelg-nolan. - Translatable: the array holds one logical position per language, all
interleaved in the same flat array.
filterItemsByLang(items, lang)(src/core/resolve/component_data.ts) narrows it to the items whoselangmatches.
This split is driven by descriptor.classSupportsTranslation on each model's
descriptor (e.g. component_input_text/descriptor.ts sets
classSupportsTranslation: true), consumed by resolveComponentValue()
(src/core/resolve/component_data.ts) to decide whether to lang-filter a
component's items.
[
{ "id": 1, "lang": "lg-eng", "value": "South Horta" },
{ "id": 1, "lang": "lg-spa", "value": "Huerta Sur" },
{ "id": 1, "lang": "lg-cat", "value": "L'Horta Sud" }
]
Same id, different lang
The three rows above are the same logical value in three languages, so
they share id: 1. Inserting a translation for an existing value reuses
that value's id — the update targets the existing item, not a new one.
5. Database column and keying
A value item never floats free: it lives inside a typed JSONB column of the
record's matrix row, and inside that column it is one of an array keyed by
component tipo. Which column a component writes into is not hardcoded per
component — it is resolved through getColumnNameByModel(model)
(src/core/ontology/resolver.ts), which reads the column field off each
model's descriptor in the component registry
(src/core/components/registry.ts). See
Sections — typed-column storage for the full table.
| component model | column | item carries |
|---|---|---|
component_input_text, component_text_area, component_email, component_password |
string |
{id, lang?, value} |
component_number |
number |
{id, value} |
component_date |
date |
{id?, start, end?, …} (lg-nolan) |
component_iri |
iri |
{id, iri, title} |
component_geolocation |
geo |
GeoJSON item (lg-nolan) |
component_image/av/pdf/3d/svg |
media |
media item (id, quality, …) |
component_json, component_info, component_security_access, component_filter_records, component_inverse |
misc |
direct object |
component_select, component_check_box, component_radio_button, component_portal, component_relation_*, component_filter, component_dataframe |
relation |
locator |
The per-component id counter lives in the meta column, e.g.
{"dd750":[{"count":3}], "dd201":[{"count":1}]}.
Inside a column the data is keyed by component tipo:
// the `string` column of one record
{
"rsc85": [ { "id": 1, "lang": "lg-spa", "value": "Alicia" } ],
"rsc86": [ { "id": 1, "lang": "lg-spa", "value": "Gutierrez" } ]
}
// the `iri` column of one record
{
"dd85": [ { "id": 1, "iri": "https://dedalo.dev", "title": "Dédalo web site" } ]
}
So the full address of a single value item is
(section_tipo, section_id) → column → component tipo → array index by id.
6. Components that produce / use the item
Every data-bearing component produces value items; the envelope's variant depends on the component family:
- Value-property literals —
component_input_text,component_text_area,component_email,component_password,component_number,component_json,component_info,component_filter_records— produce{id, lang?, value}. - Structural literals —
component_date,component_iri,component_geolocation— flatten their payload onto the item. - Media —
component_image,component_av,component_pdf,component_3d,component_svg— media items. - Relations — every component extending
component_relation_common(select, check_box, radio_button, portal, relation_parent/children/related, filter, dataframe) — store locators.
7. Raw data vs. resolved value
Two distinct notions:
-
Raw stored data — the array of value items exactly as persisted, returned by
readComponentItems(record, tipo, model)(src/core/resolve/component_data.ts).resolveComponentValue()/filterItemsByLang()narrow that array by language. Raw data for a relation is the locator — it is not dereferenced. -
Resolved value — the flattened display string, built by
resolveCellValue()(src/core/resolve/relation_list.ts), which dispatches on each model's declaredflatValuefamily (getFlatValueFamily,src/core/components/registry.ts) —string,datalist,date,iri,mediaorsection_id. For relations this dereferences each locator into its label; the raw item read never does. The export path (src/diffusion/export/atoms.ts) builds the same flattened strings for tabular export.
Terminology: data
v7 uses the term data throughout for the raw stored value — never the
older dato term.
8. Client-side model — how datum.data carries the items
The server sends a component to the client as a datum: a JSON object with
two properties, context (the structure/ontology) and data (the value). See
Components — Datum and the
context/data layers.
{
"context": { /* model, tipo, view, properties, permissions, … */ },
"data": {
"section_id": 1,
"section_tipo": "rsc197",
"tipo": "rsc85",
"lang": "lg-spa",
"value": [
{ "id": 1, "lang": "lg-spa", "value": "Alicia" }
],
"changed_data": []
}
}
datum.data.value is the array of value items — the same {id, lang?,
value|locator} envelopes the server stores. In the JS component instance this
array becomes self.data.entries
(client/dedalo/core/component_common/js/component_common.js): the getter
returns this.data.entries, and applying a change resolves the item to edit
by matching entry.id === changed_id — id-keyed, never index-keyed:
const idx = self.data.entries?.findIndex(entry => entry?.id === changed_id)
datum.data.changed_data starts empty and accumulates the edits the user makes.
9. The changed_data shape sent on save
Edits travel back to the server as a changed_data array of change objects.
Each object is dispatched on action by
src/core/section/record/save_component.ts — insert, update, remove,
set_data, sort_data, sort_by_column and add_new_element are ported
(applyUpdate() / applySortData() / applySortByColumn() /
applyAddNewElement()); force_save is not yet covered and throws loudly
("Phase 5 uncovered scope") rather than mis-handling silently.
insert | update | remove | clear | set_data | sort_data |
sort_by_column | add_new_element | force_save
A change object carries:
action— one of the above.id— the stable item id targeted.nulloninsert(a new id is minted on save). Aremovemust carry one: a remove that names no id is refused (record.remove_without_id) and writes nothing, because a missing id is an unresolved target, not a request to delete everything. To empty the component in every language, sendclear— the action that says so.value— the payload: a single value item, an array forset_data, ornull.
// insert a new value (id minted server-side on save)
{ "action": "insert", "id": null, "value": { "value": "New", "lang": "lg-eng" } }
// update an existing item, addressed by its stable id
{ "action": "update", "id": "1", "value": { "id": 1, "lang": "lg-eng", "value": "Edited" } }
// reorder (move item from key 0 to key 2)
{ "action": "sort_data", "source_key": 0, "target_key": 2 }
On save, set_data() snapshots the prior state into $db_data (a JSON clone) so
the diff against the new data drives Time Machine versioning;
get_time_machine_data_to_save() merges the component's language slice with all
its dataframe items under the main tipo (reverse-split on TM playback).
Targeting items by id (not array index) is what keeps these edits correct
across reordering and pagination.
Why id-targeting matters
A user may reorder or paginate before saving. Because changed_data
references items by their stable id, an update still lands on the right
item even though its array position changed since it was loaded.
10. v7 consolidation / evolution
- One envelope for everything. v7 consolidated the per-component data shapes
onto a single
{id, lang?, payload}item. Thevalue-key form is now a registry (components_using_value_property) rather than ad-hoc per-component logic; structural and relation items flatten their payload instead. - Server-minted, never-recycled ids. The atomic, advisory-locked counter in
the
metacolumn replaced fragile index/position addressing. This is the backbone of the unified dataframeid_keycontract and Time Machine. datanotdato. The raw value isdata(get_data()); the v6dato/get_dato()naming is retired (a few header comments still say "dato").- Raw vs. resolved split via the atoms contract.
get_value()andget_grid_value()both route throughget_export_value(), so display resolution (locator dereferencing, media URLs) is one parity-tested path, separate from rawget_data(). - Empty values preserved. v7 deliberately keeps
{"value":""}/{"value":null}so multivalue positions and dataframe attachments survive.
See also
- Sections — typed-column storage — which JSONB column a component model writes to, keyed by component tipo.
section_record— the per-record typed-column container, id allocation, and save path.- Locator — the pointer that is a relation component's value item.
component_dataframe— how frame records pair to a main item by itsid(theid_keycontract).- Components — Datum / data / context — the server→client transport this page's items ride inside.
- Request config — how
context+dataare built and delivered.