Skip to content

stripReadonlyFields uses Object.is to tell a hook write from a caller write, so a hook cannot clear a readonly field the caller also sent as null #14088

Description

@os-warren

Found while building an ObjectStack application in objectstack-ai/duly against published @objectstack/* 17.2.0. Filed here because the fix lands in packages/objectql.

The defect

stripReadonlyFields decides whether a readonly field's value came from the hook or from the caller by comparing Object.is(result[name], supplied[name]). When both are null the comparison cannot distinguish:

  • the hook deliberately wrote null (a clear), from
  • the hook never touched it, and the caller's null should be stripped

Both are deleted together, so the hook's clear is silently discarded.

Concrete failure

duly_task has a readonly completed_at stamped by a beforeUpdate hook on the transition into done and cleared on the transition out. Reopening a completed task works — unless the caller also sends completed_at: null, which is exactly what a form round-trip of the whole record does. Then:

  • the hook sets completed_at = null
  • the strip sees Object.is(null, null) and removes the key
  • the persisted row keeps its old completion timestamp while status is in_progress
  • no error is raised

Measured, not inferred: the record comes back in_progress with a stale completed_at.

Why it is worse than it looks

The corrupted state is exactly the one an application would use a validation rule to make impossible — "a completed task must carry a completion timestamp" cannot catch its inverse, a non-completed task that still carries one. Nothing downstream can tell the row apart from a genuinely completed one, and any on-time metric reading completed_at counts it.

The common paths (a bare { status } reopen, a partial patch) are unaffected, which is why this survives casual testing.

Suggested direction

The distinction the strip needs is provenance, and Object.is on values cannot carry it. Tracking which keys the hook chain actually assigned — rather than inferring it from value equality — is the shape that closes it. A narrower fix would be to treat a key the hook explicitly set as hook-owned regardless of value equality.

Provenance

Reported by a developer agent implementing objectstack-ai/duly#3, and deliberately not worked around in the application hook — a consumer-side hack around a producer defect is how a workaround becomes permanent. That judgement was right, and it is why this is filed here.

Unassigned and untriaged, per the single-producer rule for domain:*.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

bugSomething isn't workingdomain:enginepriority:p1High: required for production / M2

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions