Skip to content

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 lang key tells them apart.
  • Stable addressing. The id gives 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 its id — 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 unified id_key contract; see component_dataframe).
  • It is the reference key for Time Machine playback.
  • It is the addressing key for client edits: update / remove target an item by id, 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 uses lg-nolan (DEDALO_DATA_NOLAN). component_date and component_geolocation, for example, always store lg-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 whose lang matches.

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:


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 declared flatValue family (getFlatValueFamily, src/core/components/registry.ts) — string, datalist, date, iri, media or section_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.tsinsert, 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. null on insert (a new id is minted on save). A remove must 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, send clear — the action that says so.
  • value — the payload: a single value item, an array for set_data, or null.
// 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. The value-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 meta column replaced fragile index/position addressing. This is the backbone of the unified dataframe id_key contract and Time Machine.
  • data not dato. The raw value is data (get_data()); the v6 dato / get_dato() naming is retired (a few header comments still say "dato").
  • Raw vs. resolved split via the atoms contract. get_value() and get_grid_value() both route through get_export_value(), so display resolution (locator dereferencing, media URLs) is one parity-tested path, separate from raw get_data().
  • Empty values preserved. v7 deliberately keeps {"value":""} / {"value":null} so multivalue positions and dataframe attachments survive.

See also