tool_propagate_component_data
Batch replace / add / delete of a single component's data across every record matched by a search query object (SQO), tracked as one bulk process for audit and time-machine reversion.
What it does / why & when to use it
tool_propagate_component_data takes the value the user has just composed in one component and propagates it to the same component across many records at once — the whole filtered selection of the current section. It is the bulk-edit counterpart to editing a single record by hand: instead of opening 800 coins one by one, you set the value once and apply it to all 800.
Three propagation modes are supported:
- Replace — overwrite the component's existing value in the chosen language with the new value.
- Add — append the new value(s) to a multi-value component, skipping items already present (not allowed for mono-value components).
- Delete — remove the given value(s) from a multi-value component (locator-aware for relation components).
Concrete heritage scenario: a numismatics cataloguer has filtered the Coins section down to the 800 issues of one mint, all of which were wrongly catalogued with an empty Conservation field. They open the tool on the Conservation component, set the correct term once, press Replace, and the value lands on all 800 records in one background pass. Later the institution decides to retire a deprecated keyword: with the same tool in Delete mode they strip that one relation from every record that still carries it, leaving the rest of each record's keywords untouched. Because the whole run is recorded as a single bulk process, the change is reversible from the Time Machine.
Use it when: the same component value must be set, appended, or removed across a record set defined by a section filter. Do not use it for one-off edits (edit the record directly) or for cross-component transforms (it writes one component_tipo to itself).
How it works (server + client)
Server (tools/tool_propagate_component_data/server/{index,propagate}.ts) — a single action, propagate_component_data, runs the whole batch:
- Validates the required options and the
actionvalue (replace|delete|add). - Permission gate:
apiActionsdeclarespermission: nullhere because the target is a(section_tipo, component_tipo)pair with no single record, and the request carries the component side of that pair undercomponent_tipo, nottipo— the field name the declarativetipogate kind reads. The handler therefore gates imperatively viagetPermissions(section_tipo, component_tipo)itself, at write level 2. Propagation is a bulk write, so the caller must hold write (level 2) on the pair. - Resolves the component model; refuses
addfor mono-value components (theCOMPONENTS_WITH_RELATIONS/monovalue set defined inpropagate.ts). - Runs the SQO with the full result set (via
buildSearchSql, no limit). - Security total check: if the server's row count is greater than the client-supplied
total, it aborts — the SQO may have drifted between client and server, so a larger-than-expected match set is refused rather than silently over-writing more records than the user saw. - Creates a bulk process record (section
dd800, label componentdd796) viacreateSectionRecord. The returnedbulk_process_idis the audit handle for the whole run. - Iterates the result rows. For each record it reads the component's current data, computes
finalDataper action (replace overwrites; delete unsets matching items — locator-keyed for relation components, then re-indexes; add appends absent items) via the pure, unit-tested coreapplyPropagation(propagate.ts), and only writes when the data actually changed. The write path is the same direct onetool_time_machine.apply_valueuses —persistRecordKeys+recordTimeMachine(NOT the genericsaveComponentData) — because only that path threads abulk_process_idinto the TM row, which is what makes the whole run revertible as a unit later. - Returns
{result, msg, errors, action, section_label, total, counter, memory}; per-row failures are collected inerrors(and downgrade the message to "done with warnings") without aborting the batch.
The action is also listed in backgroundRunnable; the client always invokes it with background_running: true. scheduleBackground (src/core/tools/background.ts) runs the handler as a fire-and-forget promise and returns {result:true, background_job_id} immediately — see Server contract. ⬜ Gap: the client polls dd_utils_api.get_process_status for live per-row progress (see Client below). That action exists and is registered (src/core/api/handlers/dd_utils_api.ts), but it reads from a different job registry — the pfile-keyed store in src/core/media/jobs.ts used for AV transcode and backup jobs — than the one scheduleBackground writes to (the UUID-keyed job table in src/core/tools/background.ts, exposed via getBackgroundJob(id)). A background propagate run therefore starts and completes correctly, but polling it through get_process_status with the {pid, pfile} shape the client sends will not find a matching record; the client would need to poll getBackgroundJob(id) (keyed by the returned background_job_id) instead to observe progress.
Client (tools/tool_propagate_component_data/js/):
tool_propagate_component_data.jsis the instance. Onbuild()it locates themain_elementddo (the component being propagated) and callsget_component_to_propagate(), which spins up a temporary, standalone instance of that same component (fakesection_id,is_temporal,id_variant: 'propagate_…') seeded with the caller's current value — the editable widget the user composes the propagation value in.propagate_component_data(action)reads the live SQO from the section (self.caller.caller?.caller.rqo.sqo, cleaned tooffset=0/limit=0), builds thedd_tools_api/tool_requestRQO withbackground_running:true, and fires it throughdata_manager.requestwith a long timeout (one retry, 3600 s).render_tool_propagate_component_data.jsbuilds the UI: the temporary component widget, a Replace / Add / Delete button row (the Add button is hidden when the model is mono-value, read from thecomponents_monovalueclient config), and an info line stating the field and the affected record count. It refuses to render if the caller is not a section ineditmode. On click it confirms with the user (a second, stronger confirmation when no filter is applied — the action would touch all records), then streams live progress throughdd_utils_api.get_process_status(render_stream/read_stream), locking the UI until the process reports done — on this engine that polling loop does not observe progress for a propagate run, because the background job it started is not recorded in the storeget_process_statusreads from (see the gap noted above).
Actions & options
apiActions = { propagate_component_data: { permission: null, handler: propagateComponentData } } and backgroundRunnable = ['propagate_component_data']. permission: null here means the framework runs no declarative gate — the handler gates imperatively via getPermissions at level 2 on the (section_tipo, component_tipo) pair (this is also the defense-in-depth gate for the background path, since scheduleBackground does not re-run the declarative gate — there being none to re-run here).
| Action | Permission gate | Background | Reads from options |
|---|---|---|---|
propagate_component_data |
permission: null + imperative getPermissions(section_tipo, component_tipo) >= 2 inside the handler |
yes (backgroundRunnable; client sends background_running:true) |
see below |
Key options read by propagate_component_data:
| Option | Type | Meaning |
|---|---|---|
section_tipo |
string (req.) | Target section. Used for the write gate and component resolution. |
component_tipo |
string (req.) | The component to write. Model resolved via the ontology resolver. |
action |
string (req.) | replace | delete | add. Any other value is refused. |
lang |
string (req.) | Language the data is read/written in. |
propagate_data_value |
mixed | The value to propagate. For replace: the full new value. For add/delete: the item(s) to append/remove (cast to array; delete on relations matches by section_tipo+section_id locator). Optional for delete-nothing/replace-with-empty. |
sqo |
object (req.) | The selection to act on. Server forces the full result set (no limit/offset). |
total |
int (req.) | Record count the client saw. Server aborts if its match count exceeds this (anti-drift security check). |
bulk_process_label |
string (opt.) | Human-readable name stored on the bulk process record (dd800/dd796); defaults to "Propagate <action> to <component_label>". |
background_running |
bool | Set true by the client to run the action detached (scheduleBackground). |
Response: { result, msg, errors, action, section_label, total, counter, memory } — counter is the number of rows actually processed; errors holds any per-row warnings.
How it is registered & surfaced
tools/tool_propagate_component_data/register.json is a column-keyed dump (string/relation/misc/… keyed by component tipo — a seeded matrix-row snapshot, not a hand-authored file); importTools() passes it through as-is (see register.json reference). The essentials it carries:
dd1326name =tool_propagate_component_data;dd1327version (2.0.4);dd1328minimum Dédalo version (6.2.5);dd1644developer (Dédalo team);dd799label /dd612description (multi-language).dd1330affected_models — a long list of component models (resolved against the models section dd1342). The tool therefore attaches to the components it can edit, not to whole sections.dd1331show_in_inspector =trueanddd1332show_in_component =true(the relation targets resolve to dd64 section_id1= yes), so the button can render both in the inspector panel and inline on the component.dd1333require_translatable =false(dd64 section_id2= no);dd1354active =true.dd1633default_config carriescomponents_monovalue(flagged"client": true) — the list of mono-value component models the client uses to hide the Add button and the server uses to refuse theaddaction.dd1372labels supply the localized UI strings (do_replace,tool_do_add,tool_do_delete,content_will_be_added_removed,will_replaced_all_records,bulk_process_label, …).
Surfacing (in getElementTools, src/core/tools/registry.ts): because affected_models lists component models, the Propagate button appears on those components while a section is in edit mode — the client explicitly refuses to render unless its caller chain resolves to a section in edit mode.
Examples
Client-side tool_request (built by tool_propagate_component_data.js::propagate_component_data, sent through dd_tools_api):
const source = create_source(self, 'propagate_component_data') // → tool_propagate_component_data::propagate_component_data
const sqo = clone(section.rqo.sqo)
sqo.offset = 0
sqo.limit = 0
const rqo = {
dd_api : 'dd_tools_api',
action : 'tool_request',
source : source,
options : {
background_running : true, // run detached (scheduleBackground)
section_tipo : 'rsc167', // Coins section
section_id : self.main_element.section_id,
component_tipo : 'rsc167-conservation',// the component being propagated
action : 'replace', // replace | add | delete
lang : 'lg-eng',
propagate_data_value : self.component_to_propagate.data.entries, // the composed value
bulk_process_label : 'Data propagation | Replace',
sqo : sqo, // the current filtered selection
total : self.total // anti-drift count the user saw
}
}
const api_response = await data_manager.request({ use_worker:true, body:rqo, retries:1, timeout:3600*1000 })
// api_response → { pid, pfile, ... }; progress is then streamed via dd_utils_api::get_process_status
A successful (non-background) response object:
{
"result": true,
"msg": "OK. replace data of 'Conservation' in section 'Coins' successfully.",
"errors": [],
"action": "replace",
"section_label": "Coins",
"total": 800,
"counter": 800,
"memory": "42.5 MB"
}
To undo the whole run, find the matching bulk process in the Time Machine and revert it — every write in the batch was stamped with the same bulk_process_id.
Related
- tool_export — the read-side bulk operation over a section selection; see Exporting data.
tool_propagate_component_datais the write-side bulk counterpart over the same SQO model. tool_update_cache— another background, SQO-driven bulk operation over a component's records (cache regeneration;update_cacheis inbackgroundRunnabletoo), sharing the same background-execution shape.- tool_time_machine — where a bulk run is reverted as a unit (every write in the batch shares one
bulk_process_id); sourcecore/services/service_time_machine/,core/tm_record/. - Search subsystem (SQO): the selection contract this tool consumes —
src/core/search/(dedalo-searchskill). - Creating new tools · Server contract — the tool model,
apiActions, gates, background execution and lifecycle this page builds on. - Source:
tools/tool_propagate_component_data/server/{index,propagate}.ts,tools/tool_propagate_component_data/js/{tool_propagate_component_data,render_tool_propagate_component_data}.js,tools/tool_propagate_component_data/register.json.