medusajs/medusa · error · Error

Cannot update role parent relationship: this would create a

Error message

Cannot update role parent relationship: this would create a circular dependency (role_id: ${role_id}, parent_id: ${parent_id})

What it means

Thrown by the RBAC module when an update to a role-parent relationship would create a circular dependency in the hierarchy. checkForCycle is evaluated with the proposed role_id/parent_id before the update is persisted.

Source

Thrown at packages/modules/rbac/src/services/rbac-module-service.ts:294

  ): Promise<RbacRoleParentDTO[]> {
    for (const parent of data) {
      const { role_id, parent_id } = parent

      if (parent_id) {
        if (role_id === parent_id) {
          throw new Error(
            `Cannot update role parent relationship: a role cannot be its own parent (role_id: ${role_id})`
          )
        }

        const wouldCreateCycle = await this.rbacRepository_.checkForCycle(
          role_id!,
          parent_id,
          sharedContext
        )

        if (wouldCreateCycle) {
          throw new Error(
            `Cannot update role parent relationship: this would create a circular dependency (role_id: ${role_id}, parent_id: ${parent_id})`
          )
        }
      }
    }

    return await super.updateRbacRoleParents(data, sharedContext)
  }
}

View on GitHub (pinned to 5e06e544a2)

Solutions

  1. Choose a new parent outside the current role's descendant chain
  2. If restructuring a whole subtree, re-point children first (bottom-up) so no intermediate state is cyclic
  3. Verify the intended tree shape in a dry-run/topological pass before applying updates

Example fix

// before
// existing: A parent of B
await rbacService.updateRbacRoleParents([
  { id: bRelationId, role_id: b, parent_id: a }, // cycle
])
// after
await rbacService.updateRbacRoleParents([
  { id: bRelationId, role_id: b, parent_id: rootRole },
])
Defensive patterns

Strategy: validation

Validate before calling

async function assertNoUpdateCycle(rbac: any, u: { role_id: string; parent_id: string }) {
  const seen = new Set<string>()
  let current = u.parent_id
  while (current && !seen.has(current)) {
    if (current === u.role_id) throw new Error('Update would create a cycle')
    seen.add(current)
    const parents = await rbac.listRbacRoleParents({ role_id: current })
    current = parents[0]?.parent_id
  }
}

Type guard

async function isNonCyclicUpdate(rbac: any, u: { role_id: string; parent_id: string }): Promise<boolean> {
  const seen = new Set<string>()
  let cur = u.parent_id
  while (cur && !seen.has(cur)) {
    if (cur === u.role_id) return false
    seen.add(cur)
    cur = (await rbac.listRbacRoleParents({ role_id: cur }))[0]?.parent_id
  }
  return true
}

Try / catch

try {
  await rbacService.updateRbacRoleParents(updates)
} catch (e) {
  if (/circular dependency/.test(e.message)) {
    // alert: re-parenting attempted under own subtree; pick another parent
  } else throw e
}

Prevention

When it happens

Trigger: Calling updateRbacRoleParents where the new parent_id is a descendant of role_id — e.g. after A->B exists, updating B's relation to point at A.

Common situations: Re-parenting operations that move a subtree under one of its own descendants, hierarchy imports run as updates, or races where two concurrent re-parents each pass the check individually.

Related errors


AI-assisted analysis of medusajs/medusa@5e06e544a2 (2026-08-27). Data as JSON: /api/errors/b4f65e0039078068. Report an issue: GitHub.