relation_list
The server logic that answers the question "who points at this record?" — it resolves the inverse references of a
(section_tipo, section_id)and renders them as a grouped grid of related records.See also: Components · component_portal · component_dataframe · Sections · Locator · SQO
Role
relation_list is neither a component nor a section: it is an ontology model in
its own right (model = 'relation_list'). Its behaviour is a plain function,
buildRelationList() in src/core/resolve/relation_list.ts, dispatched by the
get_relation_list action in src/core/section/read_facade.ts. It sits on the
inverse side of the relational graph that
component_portal and the other related
components build:
- A related component (portal, select, check_box, …) stores forward locators: "this record points at those records". Their selectable options are the datalist job — see Components — related components.
relation_listanswers the reverse question: "which records out there point back at this one?". It runs the inverse-locator search engine (findInverseReferences(),src/core/search/search_related.ts) and shapes the answer as a flat-table{context, data}grid, one block per relatedsection_tipo.
A relation_list node is a child of a section in the ontology, and its
relations array can name the fallback column tipos. The preferred column
source is the section_map relation_list scope.
buildRelationList() is a plain function taking every input explicitly: there
is no instance identity, no chainable setters, and no cache to purge.
Responsibilities
- Resolve inverse references — given
(section_tipo, section_id),findInverseReferences()(search/search_related.ts) returns every record that links back.buildRelationList()calls it with the host locator as the filter and itssectionTiposoption (defaulting to'all'— no owning-section narrowing). - Build the relation-list grid — group those inverse records by their
section_tipo, resolve the column set for each (getRelationListColumns(): thesection_maprelation_listscope, falling back to therelation_listontology node'srelations), and emit a{context, data}flat table. - Resolve cell values —
resolveCellValue()covers a wide, explicitly bounded model set (see Value scope below). - Serve the edit/count JSON —
get_relation_listinsrc/core/section/read_facade.tsreturns the grid formode === 'edit'and an empty shell ({context:[], data:[]}) for any other mode. A separate action (countInverseReferences()undermode:'related'SQO counts) serves the count path.
The edit panel resolves every referencing component
buildRelationList() — the panel you see in the record editor — resolves
against every referencing component. Its sectionTipos option can
narrow the owning sections counted as referrers: read_facade.ts
derives it from the client SQO's section_tipo axis, so the Referencias
panel sends all (no narrowing) while a header-open sends the one target
section. The diffusion path narrows more finely, per ddo (by owning
section and originating component) — see
Diffusion.
Key concepts
Inverse references (the core idea)
A forward relation is a locator stored on a record:
{type, section_id, section_tipo, from_component_tipo} pointing out at a
target. An inverse reference is the discovery of that locator from the
target's point of view: starting from (this section_tipo, this
section_id), find every record whose relations bag contains a locator
pointing here. findInverseReferences() performs that search;
buildRelationList() calls it exactly this way:
const hits = await findInverseReferences(
[{ section_tipo: hostSectionTipo, section_id: String(hostSectionId) }],
{ sectionTipos: 'all', limit: options.limit ?? false, offset: options.offset },
);
The dedicated related SQO mode is routed to its own search engine:
findInverseReferences()/countInverseReferences() live in
src/core/search/search_related.ts.
Column resolution: section_map scope, then ontology fallback
For each related section_tipo in the result, getRelationListColumns()
(resolve/relation_list.ts) resolves the columns in strict order:
section_maprelation_listscope (preferred) — a direct read of the section'ssection_mapchild node'sproperties.relation_list.term. When present, its term tipos are the columns.relation_listontology node fallback — the section's ownrelation_list-model child node'srelationsarray; each entry yields one column (component tipo).
Step 1 reads the section_map properties directly rather than through the
shared getSectionMapValue() resolver, precisely because it needs a strict
scope lookup with no chain fallback: a section that does not declare a
relation_list scope must fall through to step 2, not silently inherit main's
columns. See the section_map page.
If neither source yields columns, zero grid columns are emitted for that section.
Value scope: what resolveCellValue() covers
Cell values are resolved directly, and the coverage is explicitly bounded and ledgered:
| model family | how the cell value is built |
|---|---|
component_section_id |
the record's own numeric id, as a string |
string family (component_input_text, component_text_area, component_email, component_number) |
lang-sliced literal values, multi-item joined with the separator (default ' \| ', per-component fields_separator at deeper recursion levels) |
component_date |
the flat date atom, formatted on the node's date_mode by the engine's single date formatter (src/core/components/component_date/date_value.ts): Y/m/d, degrading to Y/m and then to a bare year as the stored parts thin out; ranges render start <> end; the clock (time, time_range, date_time) and period modes render their own shapes |
component_iri |
the iri value plus its dd560 label-dataframe pairing (id_key-matched), joined ', ' |
datalist-resolvable relation models (component_select, component_radio_button, component_check_box, component_autocomplete, component_autocomplete_hi, component_relation_model, component_portal) |
the resolved datalist label per locator, or — when the component declares export-atom children (a section_list-style config) — each child's own flat value, joined by the child's fields_separator |
media models (component_image, component_svg, component_pdf, component_av) |
the absolute URL of the model's default quality (1.5MB/web/404) under DEDALO_MEDIA_EXPORT_BASE; a missing env value or an unmapped default quality is ledgered as unresolved, never guessed |
| anything else | ledgered as unresolved — the cell value is null (key omitted from the row) and the model name is collected in RelationListResult.unresolved, surfaced to the caller as errors rather than silently guessed |
This "ledger, never guess" contract is deliberate: the resolver enumerates the models it has verified and reports anything outside that set rather than fabricating a value.
The {context, data} flat-table shape
contextis the header — a flat array of column descriptors. The first per-section entry is always the syntheticidcolumn, then one entry per resolved relation component:
{ "section_tipo": "oh1", "section_label": "Oral History",
"component_tipo": "oh22", "component_label": "title" }
datais the rows — a flat array where each record begins with anidmarker row{section_tipo, section_id, component_tipo:"id"}followed by one value row per column{section_tipo, section_id, component_tipo, value}(thevaluekey is omitted, notnull, whenresolveCellValue()returnsnull— check for its absence, not for a null, when consuming this on the client).
The client (client/dedalo/core/relation_list/js/render_relation_list.js)
re-groups this flat list by section_tipo and renders one CSS-grid block per
related section.
Data model / state
There is no persistent state for this subsystem — every input is an explicit function parameter and nothing is cached across calls:
| input | where it comes from |
|---|---|
| host record | hostSectionTipo, hostSectionId parameters to buildRelationList() |
mode |
checked once in read_facade.ts before calling — a non-edit mode never reaches buildRelationList() |
| pagination | options.limit / options.offset |
| counts | a separate dispatch branch (countInverseReferences() under a mode:'related' SQO), not a flag on the same function |
Instantiation & lifecycle
There is no constructor — call buildRelationList() directly:
import { buildRelationList } from 'src/core/resolve/relation_list.ts';
// who points at oral-history record oh1/1 ?
const grid = await buildRelationList('oh1', 1, { limit: false, lang: 'lg-eng' });
// grid = { context: [...columns...], data: [...rows...], unresolved: [...model names...] }
The API entry point is the get_relation_list branch of
src/core/section/read_facade.ts: it validates source.section_tipo/section_id,
gates on read permission (≥1) to the host section, returns the empty shell
for any mode other than 'edit', derives sectionTipos from the client SQO's
section_tipo axis, and otherwise calls buildRelationList() and wraps the
result as {result:{context,data}, msg, errors?} — errors is populated from
grid.unresolved when any cell model went unresolved.
// src/core/section/read_facade.ts (get_relation_list branch, abbreviated)
if (source.action === 'get_relation_list') {
if ((source.mode ?? 'list') !== 'edit') {
return { status: 200, body: { result: { context: [], data: [] }, msg: 'OK' } };
}
// section_tipo axis narrows the owning sections ('all' = no narrowing)
const sectionTipos =
rawSectionTipos.length === 0 || rawSectionTipos.includes('all') ? 'all' : rawSectionTipos;
const relationList = await buildRelationList(hostSectionTipo, hostSectionId, {
limit: sqoOptions.limit ?? false,
offset: sqoOptions.offset,
lang: source.lang,
sectionTipos,
});
// ...
}
Public API
| function | module | purpose |
|---|---|---|
buildRelationList(hostSectionTipo, hostSectionId, options) |
resolve/relation_list.ts |
Group inverse references by section, resolve columns, build the {context, data} grid. The single entry point — there is no "just the locators, no grid" variant. |
resolveCellValue(...) |
resolve/relation_list.ts |
Per-cell value resolution, with the bounded model coverage above. Row building is inlined into buildRelationList()'s loop. |
findInverseReferences(locators, options) |
search/search_related.ts |
Run the inverse-locator search and return the matching records. |
countInverseReferences(locators, options) |
search/search_related.ts |
The count-only path (also used by the tree's "U" indexation icon). |
the get_relation_list read branch |
section/read_facade.ts |
Permission gate + mode check + sectionTipos derivation + envelope. |
Diffusion: publishing backlinks
Backlinks are publishable. The diffusion engine resolves a relation_list
hop through relationListLocators() (src/diffusion/resolve/resolver.ts),
which calls the same findInverseReferences() engine — and, unlike the edit
panel, it narrows the result per ddo:
section_filterrestricts which owning sections count as referrers;component_filterrestricts which relation component the reference must originate from;- results are ordered
section_idASC, andsection_idis kept as a string on the wire (the output formats are byte-sensitive to["4649"]vs[4649]).
Results are memoized per (record, section_filter, component_filter) for the
duration of a diffusion run. See
diffusion data flow.
How it fits with the rest of Dédalo
flowchart LR
REC["Record (section_tipo, section_id)"]
RL["buildRelationList()<br/>(resolve/relation_list.ts)"]
SR["findInverseReferences()<br/>(search/search_related.ts)"]
SM["section_map<br/>'relation_list' scope (strict, direct read)"]
ONT["relation_list ontology node<br/>.relations (fallback columns)"]
CELL["resolveCellValue()<br/>(bounded model coverage, ledgered)"]
JSON["{context, data, unresolved}<br/>flat table"]
JS["render_relation_list.js<br/>(CSS-grid blocks)"]
REC --> RL
RL -->|"inverse locator filter"| SR
SR -->|"records that point here"| RL
RL -->|"columns: scope first"| SM
RL -->|"columns: fallback"| ONT
RL -->|"row values"| CELL
RL --> JSON --> JS
Prose: A record asks buildRelationList() "who points at me?". It builds
the inverse-locator filter and hands it to findInverseReferences(), which
returns the records that link back. For each distinct related section, it
resolves the display columns — first from the section_map relation_list
scope, otherwise from the relation_list ontology node's relations — then
resolves each column's cell value via resolveCellValue()'s bounded, ledgered
model coverage. The result is a flat {context, data} table the client
re-groups into one CSS-grid block per related section.
Concrete neighbours:
- component_portal and the other
related components create the
forward locators
findInverseReferences()discovers in reverse. - component_dataframe stores its
frame relations in the same
relationsbag, so a relation-list grid over a frame's target surfaces dataframe-originated locators too. In the edit panel there is no way to scope them out; in diffusion, a ddocomponent_filterdoes exactly that. - section_map is the preferred column source via its
relation_listscope (read strictly and directly, not throughgetSectionMapValue()). - search / SQO — the
relatedsearch mode; the engine issrc/core/search/search_related.ts. - Diffusion — see above.
Examples
Resolve a record's backlinks as a grid (API path)
import { buildRelationList } from 'src/core/resolve/relation_list.ts';
// who points at oral-history record oh1/1 ?
const grid = await buildRelationList('oh1', 1, { limit: false, offset: 0, lang: 'lg-eng' });
// grid = { context: [...columns...], data: [...rows...], unresolved: [] }
Handling an unresolved cell model
const grid = await buildRelationList('rsc197', 7);
if (grid.unresolved.length > 0) {
// surfaced to the API caller as `errors: ["unresolved relation_list cell
// model: <model>", ...]` — the cell's `value` key is simply absent, never
// a guessed or null placeholder.
}
Related
- component_portal — the canonical
forward relation that
relation_listdiscovers in reverse. - component_dataframe — frame relations
stored in the same
relationsbag. - Related components — every component that writes forward locators.
- Sections / relations bag —
where the locators
relation_listreads actually live. - SQO — the
related-mode query; the engine issrc/core/search/search_related.ts. - Locator — the typed pointer at both ends of the relation.
- section_map — the preferred column-source resolver.