Skip to content

A controlled_by_parent object may declare its master reference without required, so the master-access guard is the only thing preventing an unreachable orphan detail row #8772

Description

@qq9340100

Found while verifying #8688 (no PR there — that card stopped on a pinned-envelope fork). Not fixed here: fixing it changes the authoring/lint surface, which is a different package and a different decision from #8688's error envelope. Recorded so the dependency is visible, because it is the reason #8688's literal route was measured unsafe.

What was measured

assertControlledByParentWrite refuses an insert whose master FK is absent, unconditionally. Required-field validation does not cover the same ground. Probed against validateRecord's handling in packages/objectql/src/validation/record-validator.ts, over the shapes a controlled_by_parent object can actually declare:

master reference declarationvalidateRecord on an insert that omits it
master_detail, required: truerejects — VALIDATION_FAILED, fields: [{ field, code: 'required' }]
master_detail, required: true, explicit nullrejects, same
master_detail, no requiredno error — the write proceeds
master_detail, required: true + readonlyno error — the write proceeds
master_detail, required: true + systemno error — the write proceeds

The last two follow from record-validator.ts, which skips the required check for provenance-flagged fields before it is ever reached:

if(def.system||def.readonly)continue;

Why those shapes are reachable, not hypothetical

  1. resolveCbpRelation in packages/plugins/plugin-security/src/security-plugin.ts resolves the master relation through three fallbacks, and only the first tests required:

    pick((f)=>f?.type==='master_detail'&&f?.required)??pick((f)=>f?.type==='master_detail')??pick((f)=>f?.type==='lookup'&&f?.required);

    The second fallback is a supported, exercised shape — validate-security-posture.test.ts pins "stays silent on step 2: ANY master_detail (not marked required)".

  2. packages/lint/src/data-model-rules.ts emits only severity: 'warning' for relationship/master-detail-required, so nothing refuses the declaration at publish time:

    severity: 'warning',rule: 'relationship/master-detail-required',

Why it matters

There is no live defect today — the guard closes all five rows of the table, which is why this is filed as an observation rather than a bug. The finding is where the closure comes from: for three authorable shapes it comes from a security gate, not from the declaration. Two consequences:

Possible directions (not a recommendation — needs triage)

  1. Promote relationship/master-detail-required from warning to error, so a controlled_by_parent master reference that is not required fails lint. Loudest, and it breaks any existing app relying on the shape the posture test currently pins as supported.
  2. Have the authoring builder force required: true on a master_detail under controlled_by_parent, so the shape cannot be declared.
  3. Narrow resolveCbpRelation to the required fallback only — but that turns the other shapes into 422 INVALID_METADATA at write time rather than at authoring time, which moves the report later, not earlier.
  4. Accept it and document that the guard is the enforcement point for non-required master references, in which case the guard must not be moved without this being revisited.

Note that direction 3 interacts with the validate-security-posture.test.ts pin, and directions 1–2 are authoring-surface changes with migration cost — none of them is a drive-by.


Filed unassigned for triage. Related: #8688, #7474.

Generated by Claude Code

Metadata

Metadata

Assignees

No one assigned

    Type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions