Stream's bucket and scenario must belong to the same scenario #51

Open
opened 2026-06-25 20:02:54 +02:00 by myrmidex · 0 comments
Owner

Context

Discovered during #50 (per-user ownership) planning. A Stream has two FKs:

  • scenario — required (nullable: false, CASCADE)
  • bucket — optional (nullable: true, SET NULL)

There is currently no DB constraint or validator guaranteeing that stream.bucket belongs to stream.scenario. As a result a stream in scenario B can reference a bucket that lives in scenario A — a cross-scenario inconsistency the domain never intends (an expense stream earmarks a bucket within its own scenario).

This is separate from ownership (#50). #50 ensures both the scenario and bucket IRIs on create belong to the caller, but two scenarios owned by the same user could still be crossed. This is a same-owner data-integrity gap, not an authorization gap.

Goal

When a Stream has a non-null bucket, that bucket MUST belong to the same scenario as the stream. Violations are rejected at the validation layer with a clean 422 (not a DB error, not a silent accept).

Approach (locked direction)

  • Cross-field validator in the Validator layer — consistent with the #31 invariant validators (e.g. the priority/overflow invariants), NOT a DB CHECK constraint (Doctrine + Postgres cross-table CHECK is impractical here; the relationship is row-level across stream/bucket).
  • Fires only when bucket is non-null (a null bucket — income/scenario-wide streams — needs no check).
  • Returns a violation with a precise propertyPath (bucket) so the 422 body mirrors the existing invariant-violation contract ({violations:[{propertyPath,message,code}]}, top-level un-prefixed violations).

Tests

  • Stream create with a bucket from a DIFFERENT scenario → 422, violation at bucket.
  • Stream create with a bucket from the SAME scenario → 201 (happy path).
  • Stream create with bucket = null → 201 (no check fires).
  • Persistence/unit test on the validator in isolation (same-scenario pass / cross-scenario fail).

Out of scope

  • Ownership enforcement (handled by #50).
  • Any change to bucket nullability semantics (income streams keep bucket = null).
## Context Discovered during #50 (per-user ownership) planning. A `Stream` has two FKs: - `scenario` — required (`nullable: false`, CASCADE) - `bucket` — optional (`nullable: true`, SET NULL) There is currently **no DB constraint or validator** guaranteeing that `stream.bucket` belongs to `stream.scenario`. As a result a stream in scenario **B** can reference a bucket that lives in scenario **A** — a cross-scenario inconsistency the domain never intends (an expense stream earmarks a bucket *within its own scenario*). This is **separate from ownership** (#50). #50 ensures both the scenario and bucket IRIs on create belong to the *caller*, but two scenarios owned by the *same* user could still be crossed. This is a same-owner data-integrity gap, not an authorization gap. ## Goal When a `Stream` has a non-null `bucket`, that bucket MUST belong to the same `scenario` as the stream. Violations are rejected at the validation layer with a clean **422** (not a DB error, not a silent accept). ## Approach (locked direction) - **Cross-field validator** in the Validator layer — consistent with the #31 invariant validators (e.g. the priority/overflow invariants), NOT a DB CHECK constraint (Doctrine + Postgres cross-table CHECK is impractical here; the relationship is row-level across `stream`/`bucket`). - Fires only when `bucket` is non-null (a null bucket — income/scenario-wide streams — needs no check). - Returns a violation with a precise `propertyPath` (`bucket`) so the 422 body mirrors the existing invariant-violation contract (`{violations:[{propertyPath,message,code}]}`, top-level un-prefixed `violations`). ## Tests - Stream create with a bucket from a DIFFERENT scenario → 422, violation at `bucket`. - Stream create with a bucket from the SAME scenario → 201 (happy path). - Stream create with `bucket = null` → 201 (no check fires). - Persistence/unit test on the validator in isolation (same-scenario pass / cross-scenario fail). ## Out of scope - Ownership enforcement (handled by #50). - Any change to bucket nullability semantics (income streams keep `bucket = null`).
myrmidex added this to the v0.4.0 milestone 2026-06-25 20:02:54 +02:00
myrmidex added the
enhancement
label 2026-06-25 20:02:54 +02:00
myrmidex self-assigned this 2026-06-25 20:02:54 +02:00
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference: lvl0/buckets#51
No description provided.