medusajs/medusa · error · Error

Cannot update role parent relationship: a role cannot be its

Error message

Cannot update role parent relationship: a role cannot be its own parent (role_id: ${role_id})

What it means

Thrown by the RBAC module when updating an existing role-parent relationship if the update sets parent_id equal to role_id, making the role its own parent. It fires inside updateRbacRoleParents before any write, only when parent_id is provided in the update payload.

Source

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

        )
      }
    }

    return await super.createRbacRoleParents(data, sharedContext)
  }

  @InjectManager()
  // @ts-expect-error
  async updateRbacRoleParents(
    data: UpdateRbacRoleParentDTO[],
    @MedusaContext() sharedContext: Context = {}
  ): 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})`
          )
        }
      }
    }

View on GitHub (pinned to 5e06e544a2)

Solutions

  1. Send parent_id of a different role, or omit parent_id entirely if it should not change
  2. To remove a parent, use the delete relation API rather than setting parent_id = role_id
  3. Guard the edit form against submitting the current role as its own parent

Example fix

// before
await rbacService.updateRbacRoleParents([
  { id: relId, role_id: roleId, parent_id: roleId },
])
// after
await rbacService.updateRbacRoleParents([
  { id: relId, role_id: roleId, parent_id: otherRoleId },
])
Defensive patterns

Strategy: validation

Validate before calling

function assertUpdateNotSelfParent(updates: { role_id?: string; parent_id?: string }[]) {
  for (const u of updates) {
    if (u.parent_id && u.role_id && u.role_id === u.parent_id) {
      throw new Error(`Update sets role ${u.role_id} as its own parent`)
    }
  }
}

Type guard

function isSafeParentUpdate(u: { role_id?: string; parent_id?: string }): boolean {
  return !u.parent_id || !u.role_id || u.role_id !== u.parent_id
}

Try / catch

try {
  await rbacService.updateRbacRoleParents(updates)
} catch (e) {
  if (/cannot be its own parent/.test(e.message)) {
    updates = updates.filter(isSafeParentUpdate)
    await rbacService.updateRbacRoleParents(updates)
  } else throw e
}

Prevention

When it happens

Trigger: Calling updateRbacRoleParents with { id, role_id: X, parent_id: X } — i.e. an update payload whose new parent equals the role being updated.

Common situations: Edit forms that submit the role itself as parent when the parent field is left blank but defaulted, or PATCH payloads echoing back role_id into parent_id.

Related errors


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