Codegen entity meta-model — Presentation

Table of Contents

This page is a segment of the Codegen org-entity meta-model hub, covering the ** Presentation drawer: the knobs an entity authors for its UI. The TypeScript facets that once read them are gone; the surviving reader is ores.cpp.presentation, which renders a domain type for display. The knobs themselves are unchanged. Reading across the facets by name — :domain_class:, :save_message_type:, etc. below are just pointers back to types C++ Domain and C++ Protocol already generated — so this page assumes those two.

1. Physical model mapping

Facet Description
ores.cpp.presentation Server-side rendering of a domain type for display.

2. The one thing to internalise before reading the knob tables

Eight *_convention entities (currency-like lookup entities whose saves require a change-reason code) were regenerated without :has_change_reason_cache: true in their presentation drawer. Nothing errored — codegen ran clean, the build succeeded, the tests passed. The only symptom was an empty, unusable change-reason dropdown in the save dialog, found by hand during manual QA weeks later (task AA97E310, PRs #1571/#1590).

That's the general failure mode for every knob in the Behavioural group below, not a one-off bug: codegen's templates gate optional behaviour with a mustache section tag, {{#domain_entity.<knob>}}, and mustache's rule for section tags is "false or absent renders as nothing" — there is no way for the template to tell "author explicitly said no" apart from "author forgot". An omitted knob is therefore indistinguishable, at generation time, from a deliberate false; the only way to discover you got it wrong is a human noticing the missing feature, later, by hand. Treat every knob below as something to set on purpose, not something safe to leave out and hope the default is right.

3. Worked example

book, trimmed to the knobs this page documents. book is party-scoped with a UUID primary key, so it also exercises parent_entity_singular and has_uuid_primary_key:

** Presentation
,:PROPERTIES:
,:domain_include:          ores.refdata.api/domain/book.hpp
,:domain_class:            refdata::domain::book
,:protocol_include:        ores.refdata.api/messaging/book_protocol.hpp
,:collection_name:         books
,:item_var:                book
,:key_field:                name
,:has_uuid_primary_key:    true
,:get_request_class:       refdata::messaging::get_books_request
,:get_response_class:      refdata::messaging::get_books_response
,:get_message_type:        get_books_request
,:save_request_class:      refdata::messaging::save_book_request
,:save_response_class:     refdata::messaging::save_book_response
,:save_message_type:       save_book_request
,:save_request_item_field: book
,:delete_request_class:    refdata::messaging::delete_book_request
,:delete_response_class:   refdata::messaging::delete_book_response
,:delete_message_type:     delete_book_request
,:history_request_class:   refdata::messaging::get_book_history_request
,:history_response_class:  refdata::messaging::get_book_history_response
,:history_message_type:    get_book_history_request
,:has_change_reason_cache: true
,:parent_entity_singular:  portfolio
,:settings_group:          BookListWindow
,:window_title:            Books
,:icon:                    Book
,:END:

Feature diagram of the Behavioural group below (the non-structural domain_entity variability; naming/wiring and overrides omitted, they aren't real branch points):

entity_meta_model_cpp_qt.svg

4. Naming and wiring

This group is the bulk of the drawer, but conceptually the simplest: every property is a pointer to a class, header, or NATS message type that C++ Domain or C++ Protocol already generated. Get one wrong and generation still succeeds — it's just wired to the wrong class, which almost always fails at compile time. That's a much safer failure mode than the silent Behavioural gaps below, which is why this group gets a lighter treatment here.

  • Domain and protocol pointers — :domain_include:, :domain_class:, :protocol_include:, :collection_name:, :item_var:, :key_field:.
  • Per-verb NATS message wiring, one triple/quad per CRUD verb — get/list (:get_request_class:, :get_response_class:, :get_message_type:), save (adds :save_request_item_field:), delete, and history. Each triple names the request struct, response struct, and message-type identifier C++ Protocol generated for that verb.
  • Cross-client invalidation — :changed_event_class:, :changed_event_include: point at C++ NATS's eventing facet output, so a client knows which event to listen for to refresh itself.
  • Window chrome — :settings_group:, :window_title:, :icon:: cosmetic, but still required per entity (no sensible default exists for a window title).

5. Behavioural

Every property here is, in MASD Variability terms, a feature — and specifically MASD's non-structural kind: none of them change the entity's own logical shape, only which generated UI code paths exist for it. (Contrast Keys and columns's :has_tenant_id:, which does add a field — structural variability, a different MASD category entirely.) Every property here is a boolean or identifier that changes what gets generated, and every one defaults to "off"/absent if you don't set it — the silent-omission trap described above, which in MDE terms is this whole group's implicit presence condition (IPC) being false, not the true default the wider literature usually recommends as the safer choice.

  • Key shape — :has_uuid_primary_key: (default: false, treated as a text PK). Also drives :key_field_is_uuid:, which wraps the key in boost::uuids::to_string() when :key_field: equals the primary-key column.
  • Change tracking — :has_change_reason_cache: (default: false, no change-reason dropdown). Set true whenever the DB layer has a change_reason_code column — see the worked bug above.
  • API surface — :has_explorer_api: (queryable via the generic Explorer API), :has_pagination: (list window paginates instead of loading everything).
  • Import/export — :has_csv_xml_io: turns on a whole family of paired properties (:csv_export_class:, :xml_export_class:, :xml_import_class:, and their matching _include=/=_method properties) naming the hand-written import/export helper classes; set them together or not at all.
  • Miscellany — :has_export_macro: (DLL export macro, Windows builds), :has_version_navigation: (prev/next version controls on the detail dialog).
  • Parent scoping — :parent_entity_singular:, :parent_entity_pascal:: set when the entity is only ever listed under a parent window (e.g. book under portfolio) rather than having its own top-level entry.
  • :has_readonly_paginated_list: (default: false) — strips every add/edit/delete/history affordance from the generated controller and top-level window: no toolbar actions, no detail-dialog wiring, no version navigation, no history dialog. Reload and pagination are untouched (:has_pagination: is independent and orthogonal). Use for a junction-backed or materialised dataset with nothing to edit (first consumer: calendar_dates, scoped by calendar_code) — pair it with disabling this entity's detail-dialog archetypes individually via the entity's :ores.*.enabled: drawer overrides, since no detail dialog is wired to when this knob is set.
  • Parent-scoped lists — :has_parent_scoped_list: (default: false), paired with :parent_key_field: (the protocol get-request field, e.g. calendar_code) and :parent_key_param: (the C++ member/ parameter name) – both required when the knob is set, since the parent key belongs to a different entity and neither can be derived. Only meaningful alongside :has_readonly_paginated_list: (a still-full-CRUD entity has no analogous need). When set, the deleted templates wired it through three generated classes: the client model's constructor took the parent key and sent it on every paginated fetch alongside offset/limit; the list window threaded it through to the model it created; and the controller exposed an openForParent() entry point that opened the read-only list filtered by that key. Do not conflate this with :parent_entity_singular:=/:has_parent_relationship:= above, which solves that unrelated problem.

6. Overrides

Both of these have sensible defaults derived from the entity's other knobs — set them only when a specific entity's generated protocol diverges from that default naming convention:

  • :delete_request_id_field: defaults to ids when :has_uuid_primary_key: is true, else {pk_column}s. Currency's protocol names this field iso_codes instead, so currency sets it explicitly.
  • :history_response_data_field: defaults to history. Protocols predating that naming convention name the field after the entity's plural collection instead — set explicitly when regenerating one of those.

7. Per-field knobs

The deleted templates were the only readers of these knobs; nothing consumes them today. They stay as the model's authored record of the intent, until the TypeScript UI grows the equivalent controls.

The *** Detail fields and *** Columns tables (one row per entity field, not the whole-entity drawer above) carry their own per-field properties, layered on top of the plain field=/=label=/ =type columns every field needs:

  • badge_key — instead of rendering this field's value as plain text, render it as a coloured badge, resolved via ores.dq's code_domain=/=badge_definition lookup. Applies in both the detail dialog and the list column. Omit only when the field genuinely has no DQ badge mapping — most fields.
  • flag_source, combo_* — dropdown/combo-box wiring for a soft-FK field, so the detail dialog offers a picker instead of a free-text box. The full property set (fetch function, watcher name, sort/tooltip fields, …) isn't enumerated on this page yet; see an existing entity with a combo field, e.g. book's book_status column, for the complete worked shape. (Gap: this needs its own worked example on this page — flag for follow-up.)
  • tooltip_field — list-column only. When set, the generated Client<Entity>Model::data() returns the value of the named field as the tooltip on this column's cells. Used for columns whose displayed text is a human-friendly stand-in for the real identifier (e.g. the host DisplayName column tooltips the external_id). Plain-field access only; no field_access nesting.
  • data_paste — list-column only. When set to a paste-block kind UUID, the generated data() case for this column emits the pasted body inside a case <EnumName>: { ... } block instead of the plain type-driven return. The body is pasted verbatim (a <<paste:UUID>> marker is emitted per column and resolved by the standard paste mechanism, so the block must be declared in the same model with :implements: <that UUID>). Use for display logic a single field cannot express — fallbacks, empty-value placeholders, lookups. The {{item_var}} name (e.g. host) is in scope inside the body.

8. See also

Emacs 29.3 (Org mode 9.6.15)