component_filter_records
Overview
{
"could_be_translatable" : false,
"is_literal": true,
"is_related": false,
"is_media": false,
"modes": ["edit","list","tm","search"],
"default_tools" : [
"tool_propagate_component_data",
"tool_time_machine"
],
"render_views" :[
{
"view" : "default | line | print",
"mode" : "edit"
},
{
"view" : "default | mini | text",
"mode" : "list | tm"
},
{
"view" : "default",
"mode" : "search"
}
],
"data": "array of entries",
"sample_data": [
{ "id": 1, "tipo": "mdcat3112", "value": [1, 8, 9] },
{ "id": 2, "tipo": "rsc202", "value": [8, 150, 201] },
{ "id": 3, "tipo": "oh1", "value": [1, 3, 4] }
],
"value": "array of entries",
"sample_value": [
{ "id": 1, "tipo": "mdcat3112", "value": [1, 8, 9] }
]
}
Classifier flags
could_be_translatable, is_literal, is_related and is_media are client-model
classifiers derived from the component typology, not stored datum fields. As a
literal-direct component, component_filter_records
is literal (is_literal: true), it is not a relation locator (is_related: false),
not media (is_media: false), and not translatable
(could_be_translatable: false): its single static instance is loaded under
DEDALO_DATA_NOLAN.
Definition
component_filter_records manages record-level access control: it stores, per target
section, the explicit list of record ids (section_ids) a given user is allowed to access.
It is a configuration component used exclusively inside the Users section (dd128),
where each user record carries one instance of it (the standard tipo is dd478,
DEDALO_USER_COMPONENT_FILTER_RECORDS_TIPO).
Dédalo already restricts access through areas/projects (see component_filter and
component_filter_master). Those grant or deny a user a whole section/area.
component_filter_records exists to provide a finer grain:
within a section the user is otherwise authorized for, restrict visibility down to a specific
set of record ids — regardless of project assignment.
The restriction is live: there is no feature flag to turn it on. Whenever the logged
user's stored datum names the section being searched, the search adds a
section_id IN (...) restriction for that section. A user with no entry for a section —
the overwhelmingly common case — searches it exactly as before, with a byte-identical
query.
Saving an allow-list takes effect immediately
There is no staging step and no global switch: the moment an entry is saved on a user record, that user's next search of the named section is restricted. Clear the row (empty the input) to remove the restriction — an entry saved with an empty id list is read as no restriction, never as sees nothing. The restriction applies to global admins too, so an allow-list written onto an administrator's own account restricts that administrator.
When to use it
- A cataloguer should only see a handful of records of a section (e.g. only the 12 archaeological objects on loan to their institution), even though they have read/write permission on that whole section.
- A reviewer is granted access to a curated subset of
oh1(Cultural objects) records for a temporary campaign, without touching their project membership.
When not to use it
- To grant or deny access to an entire section/area — use the project filter components
(
component_filter/component_filter_master) instead. - As a normal data field in cataloguing sections. This is a permissions/configuration component; it only makes sense inside the Users section.
- As the only barrier around sensitive records. It narrows what a user can reach through the search path; it is one restriction ANDed on top of the section grant and the project filter, not a replacement for either.
Data model
Data: array of entry objects.
Value: array of entry objects (same shape as data), or null.
Storage: the component stores its data in the matrix misc column (a direct
object, not a relation locator). It is not translatable, so there is a single instance
under DEDALO_DATA_NOLAN and no per-language rows.
Each entry pairs a target section tipo with the array of allowed section_ids for
that section, plus the standard counter-assigned id:
[
{ "id": 1, "tipo": "mdcat3112", "value": [1, 8, 9] },
{ "id": 2, "tipo": "rsc202", "value": [8, 150, 201] },
{ "id": 3, "tipo": "oh1", "value": [1, 3, 4] }
]
id— counter-assigned entry id, used to targetupdate/removeoperations on the item array.tipo— the target section ontology tipo the restriction applies to.value— array of integersection_ids the user may access in that section. Client-side the input is validated to positive, de-duplicated integers; search mode keeps the raw comma split until saved.
In the JSON-API datum the array is delivered under the data item's entries property,
and in edit and search modes the resolved list of authorized sections is attached as
datalist (see below). list and tm carry no datalist key at all — those views read
only the stored entries.
Consumption shape. The search layer reads the stored entries array into a map keyed by
section tipo (section_tipo → id array) before it can restrict anything; that reader is
getUserFilterRecords() (src/core/security/filter_records.ts). It is deliberately strict,
because every id it returns reaches SQL as an integer literal:
- an entry with no
tiponames no section and contributes nothing (legacy data does carry these, with a nested{tipo, value}payload undervalue); - ids are coerced to integers and kept only when positive; anything else is dropped;
- an entry whose id list ends up empty is skipped — it is no restriction, not a lockout;
- two entries for the same section (only reachable from hand-edited data) are unioned.
The result is cached per user and dropped whenever any Users-section record is written, so an edited allow-list applies to the next search.
Ontology instantiation
Define the node like any other component, declaring its model, tipo, parent (the
section it lives in) and the language descriptors. Because it is non-translatable, it is
instantiated under lg-nolan.
Node definition (the canonical placement is the Users section dd128, tipo dd478):
{
"tipo" : "dd478",
"model" : "component_filter_records",
"parent" : "dd128",
"section_tipo" : "dd128",
"lg-eng" : "Filter records by id",
"translatable" : false
}
Minimal/empty properties block (this component takes no required ontology properties):
{
"properties": {}
}
section_tipo/parent wire the component into its owning section. The TS server has no
per-component factory to call: the descriptor at
src/core/components/component_filter_records/descriptor.ts (column: 'misc', no
classSupportsTranslation flag) resolves through the same generic
src/core/resolve/component_data.ts as any other literal component, and the
authorized-sections datalist is built by getFilterRecordsDatalist(userId, lang)
(src/core/api/handlers/filter_records_datalist.ts), attached by the model's emit hook
(src/core/components/component_filter_records/emit.ts).
Properties & options
This component defines no bespoke ontology properties. Its properties block is normally
empty ({}), as shown in the shipped sample context. Behaviour is driven by:
- the
datalist— not an ontology property, computed server-side bygetFilterRecordsDatalist()(src/core/api/handlers/filter_records_datalist.ts, built fromgetAuthorizedAreasForUser(),src/core/security/permissions.ts). It lists every section the logged user is authorized for, keeping only areas whosemodel === 'section'and whose permissionvalue >= 2(write or higher), resolving each section label from the ontology and sorting alphabetically by label. Each datalist item is{ tipo, permissions, label }. - the stored datum itself — there is no config key. The presence of an entry for a section IS the switch; see Definition.
The datalist is the EDITOR's list, not the edited user's
It is resolved from the logged user's own authorized areas, so it describes what the person filling the form may restrict. Editing another user's record does not widen it: a section the editor cannot administer cannot be given a restriction here.
The standard generic component properties (e.g. mandatory, css, request_config) may be
attached through the ontology node like any component; none are specific to or required by
component_filter_records. If you need behaviour beyond the above, verify in ontology.
Render views & modes
The client model wires modes to renders in js/component_filter_records.js
(edit, list, tm → list render, search). The views actually present in the source:
| Mode | View | Source | Behaviour |
|---|---|---|---|
edit |
default |
view_default_edit_filter_records.js |
Grid (tipo / section label / value input). One editable comma-separated id input per authorized section from the datalist. Header row with tipo, Section, Value. |
edit |
line |
view_default_edit_filter_records.js |
Same render as default, label removed (compact inline). |
edit |
print |
render_edit_component_filter_records.js |
Same render as default but forced read-only (permissions = 1): rows render through get_content_value_read, only entries present in the value are shown. |
list / tm |
default |
view_default_list_filter_records.js |
Read-only string: each entry serialized as JSON, joined by context.fields_separator. |
list / tm |
mini |
view_mini_list_filter_records.js |
Minimal wrapper, same JSON-joined string. |
list / tm |
text |
view_text_list_filter_records.js |
Plain span, entries serialized as JSON, newline-separated. |
search |
default |
render_search_component_filter_records.js |
One text input per authorized section; on change publishes change_search_element (raw comma split, no integer validation). |
tm (Time Machine) reuses the list render. The shared change logic lives in
change_handler() / build_changed_data_item() in the client model: in edit it saves on
every change (change_value), in search it updates the instance and publishes the search
event.
Permission-aware edit render
In edit, when permissions === 1 the view renders only the stored entries read-only;
with write permission (> 1) it renders one editable input per authorized section and
exposes the toolbar buttons (tools + fullscreen).
Import / export model
component_filter_records defines no import, export or diffusion handling of its own — it
goes through the shared engines like any other literal component. Its data is a plain JSON
array of entries, so import/export use that array directly:
[
{ "id": 1, "tipo": "mdcat3112", "value": [1, 8, 9] },
{ "id": 2, "tipo": "rsc202", "value": [8, 150, 201] }
]
Because this is a configuration component living in the Users section, it is rarely part of
ordinary catalogue import/export flows. See the general
importing data and exporting data docs for the
shared component contract (the dedalo_data wrapper, atoms, NDJSON flat-table protocol).
Notes
- Typology: literal-direct component (data column
misc). The TS server has no class hierarchy —src/core/components/component_filter_records/descriptor.tsdeclarescolumn: 'misc'. It is not a relation component despite pointing at section records — the targets are stored as bare integer ids insidevalue, not as locators. - Default tools (from the shipped
context.json):tool_propagate_component_dataandtool_time_machine. Saves are recorded in Time Machine like any component (tmmode is read-only). - Server entry point — enforcement.
getUserFilterRecords()(src/core/security/filter_records.ts) reads the logged user's datum, and the search assembler (src/core/search/sql_assembler.ts) turns it into onesection_id IN (...)predicate ANDed into the query it builds. Because the per-record scope check runs that same assembler, the restriction covers the list, the row count, every branch of a multi-section search and the "may this principal open this record" probe — one predicate, every door. A multi-section search gates only the branches the datum names; sibling sections pass through untouched. - Datalist source:
getFilterRecordsDatalist()(src/core/api/handlers/filter_records_datalist.ts) depends on the logged user's authorized areas, so the set of selectable sections inedit/searchreflects current permissions, filtered to sections with write-or-higher access (value >= 2). - Observers/observables: none configured for this component.
- Read path — one attachment point. The datalist is attached by the model's emit hook
(
src/core/components/component_filter_records/emit.ts), which runs inside the shared emission (src/core/section/read.ts,emitDdoData). The full-section read and the component-levelget_datatherefore serve the same key:edit/searchcarry the authorized-sections list,list/tmcarry nodatalistkey. Identity comes from the request-scoped principal; a call with no principal (no request scope — timer/scheduler work, a unit harness) gets an empty array rather than somebody else's sections. A background job is not that case: it runs under its submitter's principal, snapshotted at submit, so it serves the submitter's list. - Search-panel instances. A filter row in the search panel addresses no record (its
section_idis a client-mintedsearch_<n>sentinel). The component still answers with its own item — emptyentries, fulldatalist, the sentinel id echoed verbatim — because the search render iteratesdatalistunconditionally and an absent item would leave the row unrendered. - Related components:
component_filterandcomponent_filter_master(project/area-level access),component_security_access(also stored inmisc), and component_portal (relation-based linking, by contrast).