fix(roles): PUT /v1/roles/{roleId} no longer returns roleChildren duplicated 4x (#37303) - #37344
Open
hassandotcms wants to merge 3 commits into
Open
fix(roles): PUT /v1/roles/{roleId} no longer returns roleChildren duplicated 4x (#37303)#37344hassandotcms wants to merge 3 commits into
hassandotcms wants to merge 3 commits into
Conversation
… PUT response (#37303) Defect spec for PUT /api/v1/roles/{roleId} returning each child of the updated role four times, with childCount inflated to match. Covers the curl reproduction, the verified root cause (populatChildrenForRolesHelper appends instead of replacing, four population passes on one Role instance per save), fix scope with explicit non-goals, regression risk, and measurable acceptance criteria. Spec-only commit — PR 1 of 2 per the Spec-Kit flow; implementation follows on this branch after spec approval.
) populateChildrenForRoles appended to a role's existing roleChildren instead of replacing it, so an instance that passed through the population path more than once per save() accumulated its children. PUT ran one instance through four times, returning each child 4x with childCount inflated to match; the DBFQN reparent cascade, seeded from the same list, also re-saved descendants redundantly. Persisted rows were always correct — only the response and the cascade work were wrong. Reset each input role's roleChildren before the chunked queries so population is idempotent (replace, not accumulate). Reset in the method, not the helper, which must keep accumulating across 200-role chunks. Also renames the private populatChildrenForRoles* family to populateChildrenForRoles* (all call sites in-file). Adds integration coverage: PUT reparent / plain update / move-to-root each return one child with childCount 1; factory-level idempotency (childless role stays null); and reparent keeps children de-duplicated at every depth.
6 tasks
hassandotcms
requested review from
dario-daza,
fabrizzio-dotCMS and
jcastro-dotcms
September 2, 2026 18:20
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 #37303
Problem
PUT /api/v1/roles/{roleId}returned the updated role with itsroleChildrenlist duplicated four times andchildCountinflated to match — a role with one child came back reporting four. Only the response body was wrong; the persisted rows were always correct (an immediateGETreturned the real children). Enough to break a client: the Roles (Beta) portlet splices the PUT response into its tree, and any integration readingchildCountto decide whether to recurse got the wrong answer. The reparentDBFQNcascade, seeded from the same list, also re-saved descendants redundantly.Root cause
RoleFactoryImpl.populatChildrenForRolesHelperappended to a role's existingroleChildreninstead of replacing it. ARoleinstance that arrived already populated therefore accumulated. Within onesave()the same instance passes through the population path four times (arrives populated from cache → theDBFQNcascade loop →setFQNForDB'sgetRoleByIdreload → end ofsave()), producing exactly four copies.toRoleViewsthen serialized whatever ids were in the list, andRoleView.childCountisgetRoleChildren().size().Fix
Give
populateChildrenForRolesreplace semantics: reset each input role'sroleChildrenbefore the chunked queries, so population is idempotent. The reset is in the method (not the helper, which must keep accumulating across the 200-role chunks of a single population). This corrects the PUT response, removes the redundant cascade re-saves, and stops the shared cache-resident instance from being polluted mid-transaction. Also renames the privatepopulatChildrenForRoles*method family topopulateChildrenForRoles*(all call sites are in-file).No REST contract change — response shape is untouched, only the values are corrected. No DB schema, ES mapping, or serialized-format change; rollback-safe.
Testing
childCount == 1matching a subsequent GET (RoleResourceIntegrationTest); factory-level idempotency, and a childless rolekeeps
roleChildren == null(RoleAPITest); reparent keeps childrende-duplicated at every depth of a 3-level subtree.
RoleResourceIntegrationTest,RoleAPITest,RoleResourceCountsIntegrationTest,RoleResourceUsersIntegrationTest— all green (80 tests, 0 failures).childCount: 1/[child](was 4), matching GET before and after; plain update and move-to-root bothchildCount: 1.