fix: resolve dependents against every deleted resource - #1772
Open
HafizMMoaz wants to merge 1 commit into
Open
Conversation
Deleting a resource with `_dependent` worked by nullifying foreign keys and then removing the rows whose key was null. That has two problems. Rows that already held a null foreign key before the request were indistinguishable from rows nullified by it, so deleting one post removed every comment that was never attached to a post in the first place. The sweep also used the deleted resource's own foreign key for all the dependents, so only direct children could ever match. Deleting a village with `?_dependent=houses&_dependent=citizens` looked for `villageId` on the citizens, found none, and left them behind pointing at houses that no longer exist. Dependents are now matched by id against every resource deleted so far, repeating until nothing new is removed, so a chain of dependents is resolved whatever order it is listed in. Foreign keys are nullified afterwards and cover rows pointing at deleted dependents too. Dependents still have to be named explicitly, so nothing the caller did not ask for is deleted. Fixes typicode#1415
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #1415
Problem
Two bugs, one root cause.
destroyByIdnullifies foreign keys and then deletes the rows whose key is null:1. Rows that were already null are deleted. The sweep cannot tell a row nullified by this request from one that was never attached to anything:
{ "posts": [{ "id": "1" }, { "id": "2" }], "comments": [ { "id": "a", "postId": "1" }, { "id": "b", "postId": null }, // never belonged to a post { "id": "c", "postId": "2" } ] }2. Only direct children can match, which is #1415. The sweep uses the deleted resource's own foreign key for every dependent, so a grandchild keyed by something else never matches, even when it is named explicitly:
$ curl -X DELETE 'localhost:3000/villages/1?_dependent=houses&_dependent=citizens'deleteDependentslooks forvillageIdon the citizens, finds none, and leaves them behind pointing at houses that no longer exist. There is currently no request that deletes them.Fix
Deletion is now driven by ids rather than by nulls.
destroyByIdkeeps a map of resource name to the ids removed from it, seeded with the item being deleted. A dependent row is removed when its foreign key points at any id in that map, and its own id joins the map, so the next resource in the chain can be resolved against it.The passes repeat until nothing new is removed, which makes the result independent of the order the dependents are listed in. The loop is bounded by the number of dependents, since a chain cannot be deeper than that.
Foreign keys are nullified afterwards against the same map, so rows pointing at a deleted dependent are nullified too, not just rows pointing at the deleted item.
Because nothing keys off
nullany more, the first bug disappears with it.Behavior
?_dependent=houses&_dependent=citizensnow deletes both levels. Anything not named is left alone, so no data the caller did not ask for is removed. I kept it opt-in rather than cascading the whole graph implicitly, since that would silently widen what an existingDELETErequest deletes.Tests
Added to the
destroysuite inservice.test.ts:The two existing
destroytests are unchanged and still pass.pnpm test,pnpm lintandpnpm typecheckall pass.I also confirmed over HTTP that repeated
_dependentparams arrive as an array, so the request above works end to end.