Skip to content

Add opt-in scoping of nested mutations to the parent relation - #2798

Open
spawnia wants to merge 13 commits into
masterfrom
fix-2744-nested-scope
Open

spawnia wants to merge 13 commits into
masterfrom
fix-2744-nested-scope

Conversation

@spawnia

@spawnia spawnia commented Oct 11, 2026 •

Copy link
Copy Markdown
Collaborator

Resolves #2744

Nested delete, disconnect, update and upsert act on models of other parents.
The new opt-in option nested_mutations_scope_to_parent limits them to related models, and v7 makes it the default.

  • Added or updated tests
  • Documented user facing changes
With the option enabled, any ID not related to the parent causes the same error

The error names the rejected IDs, e.g. "Cannot modify a model that is not related to the given parent: 3, 7."
Missing and foreign IDs get the same error, so a client cannot probe which IDs exist elsewhere.
All IDs of a delete or disconnect are checked before any model changes.

upsert is the exception: an ID that exists nowhere still creates a model, so client-generated IDs keep working.
An ID or identifyingColumns match that exists outside the relation causes the error.

connect, sync and BelongsTo update/upsert stay unscoped

Their purpose is to establish new relationships.
The security section of the nested mutations docs now names them as an authorization concern.

Disabled by default, so v6 behaviour is unchanged

UPGRADE.md describes the v7 change: the option is removed and scoping always applies.
Clients that move models between parents through update or upsert must use connect.

Builds on two closed PRs

Breaking changes

None in v6.
v7 makes scoping the default, see UPGRADE.md.

Changelog

Added

🤖 Generated with Claude Code

With `nested_mutations_scope_to_parent` enabled, nested delete,
disconnect, update and upsert only act on models related to the
parent and throw an error otherwise. Upsert still creates a model
when the given ID exists nowhere.

Resolves #2744
Supersedes #2454 and #2751

Co-authored-by: Lucas Lenz <lucas.lenz@mailbox.org>

🤖 Generated with Claude Code
@spawnia
spawnia requested a balanced review from Copilot October 11, 2026 11:52
@spawnia
spawnia marked this pull request as ready for review October 11, 2026 11:55

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Warning

Copilot couldn't run its full agentic review because it didn't start before the timeout. Make sure your repository has a runner available, or add a copilot-code-review.yml file specifying one with the runs-on attribute. See the docs for more details.

Copilot review overview

1 open finding
What changed in this PR

Adds an opt-in safety feature to scope nested mutation operations (delete, disconnect, update, upsert) to the parent relation, preventing cross-parent modifications unless explicitly allowed (with v7 planned to make this default).

Changes:

  • Introduces nested_mutations_scope_to_parent config and ParentRelationScope enforcement utilities.
  • Updates nested mutation execution to scope delete/disconnect and to pass parent relations into UpdateModel/UpsertModel for scoped lookup.
  • Adds/updates integration tests and updates docs, CHANGELOG, and UPGRADE guidance.
File Description
tests/​Integration/​Schema/​Directives/​UpsertDirectiveTest.php Adds coverage for scoped nested upsert via identifying columns
tests/​Integration/​Execution/​MutationExecutor/​MorphToManyTest.php Adds scoped upsert negative test for MorphToMany
tests/​Integration/​Execution/​MutationExecutor/​MorphOneTest.php Adds scoped upsert negative test for MorphOne
tests/​Integration/​Execution/​MutationExecutor/​MorphManyTest.php Adds scoped delete/upsert negative tests for MorphMany
tests/​Integration/​Execution/​MutationExecutor/​HasOneTest.php Adds scoped delete/update/upsert negative tests for HasOne
tests/​Integration/​Execution/​MutationExecutor/​HasManyTest.php Adds scoped delete/disconnect/update/upsert tests for HasMany
tests/​Integration/​Execution/​MutationExecutor/​BelongsToTest.php Verifies BelongsTo remains unscoped even when option enabled
tests/​Integration/​Execution/​MutationExecutor/​BelongsToManyTest.php Adds scoped delete/update/upsert tests for BelongsToMany
src/​lighthouse.php Adds new config option with inline documentation
src/​Schema/​Directives/​UpsertManyDirective.php Passes parent relation through to upsert execution
src/​Schema/​Directives/​UpsertDirective.php Passes parent relation through to upsert execution
src/​Schema/​Directives/​UpdateManyDirective.php Passes parent relation through to update execution
src/​Schema/​Directives/​UpdateDirective.php Passes parent relation through to update execution
src/​Execution/​Arguments/​UpsertModel.php Adds scoped lookup support via parent relation
src/​Execution/​Arguments/​UpdateModel.php Adds scoped lookup support via parent relation
src/​Execution/​Arguments/​ParentRelationScope.php New helper implementing scoping rules and consistent error
src/​Execution/​Arguments/​NestedOneToOne.php Routes nested delete/update/upsert through scoping logic
src/​Execution/​Arguments/​NestedOneToMany.php Routes nested disconnect/delete/update/upsert through scoping logic
src/​Execution/​Arguments/​NestedManyToMany.php Routes nested delete/update/upsert through scoping logic
docs/​master/​eloquent/​nested-mutations.md Documents new config and which operations remain unscoped
docs/​6/​eloquent/​nested-mutations.md Backports documentation for v6 users
UPGRADE.md Documents v7 behavior change (option removed; always scoped)
CHANGELOG.md Notes new opt-in config option

🧠 Review effort: Lite


💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread tests/Integration/Execution/MutationExecutor/BelongsToTest.php
spawnia and others added 11 commits October 13, 2026 20:20
`destroy()` and `detach()` accepted a scalar, so `delete: ID` worked before.

🤖 Generated with Claude Code
🤖 Generated with Claude Code
Callers no longer branch on applies(), and unscoped lookups run the same queries as before the option existed.

🤖 Generated with Claude Code
With `nested_mutations_scope_to_parent`, `@delete(relation: ...)` deleted models of other parents.

🤖 Generated with Claude Code
An array identifying value crashed with "Array to string conversion".

🤖 Generated with Claude Code
@spawnia

spawnia commented Oct 25, 2026

Copy link
Copy Markdown
Collaborator Author

🤖 This branch conflicts with master and needs an update before it can merge.

The local review is done and its fixes are pushed, including scoping for the nested @delete(relation:) argument.
CI on the latest push has to pass before merging.

# Conflicts:
#	src/Schema/Directives/DeleteDirective.php
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.

Nested mutations allow operations on records outside parent relationship scope

2 participants