Skip to content

feat(flow): object-read can fan out one item per object - #2237

Merged
rubenvdlinde merged 1 commit into
developmentfrom
feat/object-read-fanout
Jul 31, 2026
Merged

feat(flow): object-read can fan out one item per object#2237
rubenvdlinde merged 1 commit into
developmentfrom
feat/object-read-fanout

Conversation

@rubenvdlinde

Copy link
Copy Markdown
Contributor

Follows #2236, which I shipped returning a list under output — then tried to build the reaper it exists for, and it did not compose.

Why the list shape does not work for the motivating case

Every other node in the engine acts per item, and nothing expands a list inside an item's json back into items — openregister.loop batches the items on the walk, not the entries of a field.

So a read returning {objects: [...]} can be counted and branched on, and cannot be acted on one row at a time.

That is exactly what a reaper does: read stale locks, then delete each one. With the list shape its delete step sees a single item whose uuid is not a field, and fails with:

The match value for "uuid" could not be resolved from the item

Measured while building it, not predicted.

The change

fanOut: true emits one item per object, carrying the incoming record onto each so the run keeps what it was holding. No matches means no items, which ends the branch — the same contract openregister.loop has for an empty input, and not an error, because "nothing to reap" is the ordinary case.

The list stays the default. It is the right shape for the other half of the uses — "how many are there", "is there any" — where fanning out turns one decision into N. Both shapes are pinned by tests.

I flagged this exact trade-off in #2235 and picked the side that did not serve the case the node was built for.

Verification

15,571 unit tests green (3 new); phpcs (full tree, CI settings) and phpstan clean.

I shipped the read node returning a LIST under `output`, tried to build the
reaper it exists for, and it did not compose.

Every other node in the engine acts per item, and nothing expands a list inside
an item's json back into items — `openregister.loop` batches the items on the
walk, not the entries of a field. So a read that returns `{objects: [...]}` can
be counted and branched on, and cannot be acted on one row at a time.

Which is the motivating case. A reaper reads stale locks and then DELETES each
one; with the list shape its delete step sees a single item whose `uuid` is not
a field, and fails with "the match value for uuid could not be resolved from
the item". Measured, not predicted.

`fanOut: true` emits one item per object, carrying the incoming record onto each
so the run keeps what it was holding. No matches means no items, which ends the
branch — the same contract `openregister.loop` has for an empty input, and not
an error, because "nothing to reap" is the ordinary case.

The list stays the DEFAULT: it is the right shape for the other half of the uses
— "how many are there", "is there any" — where fanning out turns one decision
into N. Both shapes are pinned.

Refs #2235. 15,571 unit tests green (3 new); phpcs, phpstan clean.
@rubenvdlinde
rubenvdlinde merged commit 77fd8b3 into development Jul 31, 2026
24 checks passed
@rubenvdlinde
rubenvdlinde deleted the feat/object-read-fanout branch July 31, 2026 15:25
@github-actions

Copy link
Copy Markdown
Contributor

Quality Report — ConductionNL/openregister @ 6d6b3a4

Check PHP Vue Security License Tests
lint
phpcs
phpmd
psalm
phpstan
phpmetrics
eslint
stylelint
composer ✅ 174/174
npm ✅ 616/616
PHPUnit
Newman
Playwright ⏭️

Quality workflow — 2026-07-31 15:28 UTC

Download the full PDF report from the workflow artifacts.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant