Skip to content

Non-persisted shapes (messages/DTOs) can't declare field-level references to entities #238

Description

@dmealing

Summary

There is no way to say "this non-persisted shape has a field that references entity X."
object.value is loader-banned from ALL identity/reference constructs (value purity —
subtype-rules.ts: "value objects MUST NOT have ANY identity (primary, secondary, reference)"
ERR_SUBTYPE_RULE_VIOLATION), and identity.reference/@references exist only on
object.entity, which implies a writable source/persistence a message does not have. The
only reference construct available on a value, relationship.association, names the target
entity but not which field carries the foreign key — so a value can gesture at an entity but
cannot bind an id field to it.

Why (concrete adopter need)

We are modeling a wire protocol (a poker engine's command messages) as MetaObjects. Each
message is a non-persisted shape that references domain entities: AskForStake carries a
tableId that references Table; TableState carries a gameHandId referencing GameHand;
AnswerForStake carries a sessionId referencing a Session. These are genuine references —
a dangling target should fail the load, and codegen should be able to navigate/type them — but
the message is neither persisted nor identity-bearing. It is a referencing value (a DTO), a
shape the current valueentity dichotomy has no slot for:

  • object.value → refuses the reference (value purity).
  • object.entity → forces a writable source + primary identity the message doesn't have.
  • relationship.association on a value → names Table but can't say tableId is the FK to it.

Relationship to #210 (and why this isn't a contradiction)

#210 pushes toward total value purity (retiring origin.* on object.value). This ask is not
"make values impure" — referencing an entity by id is not persistence. A DTO/message that
carries tableId and declares it references Table stores nothing, owns no identity, and has no
source; it only needs a navigation-only, non-enforced reference so the loader can resolve the
target (fail on dangling) and codegen can type it. Purity is about identity and storage;
this is about typed navigation on a transient shape.

Proposed shapes (either resolves it)

  1. A field-level, navigation-only reference on value objects — allow identity.reference
    with @enforce: false (the already-chartered logical-reference form) on object.value,
    OR a new field-level @references attribute that binds one field to a target entity FQN,
    resolved against the symbol table (dangling → ERR_INVALID_REFERENCE), emitting no FK/DDL.
    object.value: { name: AskForStake, children: [
      { field.int: { name: tableId, "@references": "gaming::common::poker::Table" } },  # navigation-only
      ... ] }
  2. A new non-persisted-but-referencing object subtype (a "message"/"dto" shape) — like
    object.value (no source, no primary identity, never a table) but permitted to carry
    identity.reference @enforce:false children, so a transient DTO can declare its FKs.

We are adopting shape (2) locally via a registered provider subtype meanwhile, but the field↔target
binding on a transient shape feels like it belongs in core: DTOs referencing entities is a
universal pattern (API payloads, events, wire messages), not a poker-specific one.

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions