Skip to content

[SILO-1087] feat: add IssueRelations external API#8763

Open
Saurabhkmr98 wants to merge 2 commits intopreviewfrom
feat-issue_relations_api
Open

[SILO-1087] feat: add IssueRelations external API#8763
Saurabhkmr98 wants to merge 2 commits intopreviewfrom
feat-issue_relations_api

Conversation

@Saurabhkmr98
Copy link
Member

@Saurabhkmr98 Saurabhkmr98 commented Mar 16, 2026

Description

  • Introduces the work item relations API, including serializers, permissions, and OpenAPI documentation for list/create operations.
  • Added serializers for issue relation create and response payloads.
  • Implemented relation list/create API behavior with proper permissions and query handling.
  • Added work_item_relation_docs decorator and exported it for OpenAPI tagging and defaults.

URL - /api/v1/workspaces/{slug}/projects/{project_id}/work-items/{issue_id}/relations/

POST endpoint

Sample Payload

  {
    "relation_type": "blocking",
    "issues": [
      "550e8400-e29b-41d4-a716-446655440000",
      "550e8400-e29b-41d4-a716-446655440001"
    ]
  }

Sample Response

  [
    {
      "id": "550e8400-e29b-41d4-a716-446655440000",
      "name": "Fix authentication bug",
      "sequence_id": 42,
      "project_id": "550e8400-e29b-41d4-a716-446655440001",
      "relation_type": "blocked_by",
      "state_id": "550e8400-e29b-41d4-a716-446655440002",
      "priority": "high",
      "type_id": "550e8400-e29b-41d4-a716-446655440003",
      "is_epic": false,
      "created_at": "2024-01-15T10:00:00Z",
      "updated_at": "2024-01-15T10:00:00Z",
      "created_by": "550e8400-e29b-41d4-a716-446655440004",
      "updated_by": "550e8400-e29b-41d4-a716-446655440004"
    }
  ]

GET endpoint

Sample response

{
    "blocking": [
      "550e8400-e29b-41d4-a716-446655440000",
      "550e8400-e29b-41d4-a716-446655440001"
    ],
    "blocked_by": ["550e8400-e29b-41d4-a716-446655440002"],
    "duplicate": [],
    "relates_to": ["550e8400-e29b-41d4-a716-446655440003"],
    "start_after": [],
    "start_before": ["550e8400-e29b-41d4-a716-446655440004"],
    "finish_after": [],
    "finish_before": []
  }

Type of Change

  • Bug fix (non-breaking change which fixes an issue)
  • Feature (non-breaking change which adds functionality)
  • Improvement (change that would cause existing functionality to not work as expected)
  • Code refactoring
  • Performance improvements
  • Documentation update

Screenshots and Media (if applicable)

Test Scenarios

References

Summary by CodeRabbit

  • New Features

    • Work item relationship management: create and list relationships between work items.
    • Support for multiple relation types (blocking, blocked_by, duplicate, relates_to, start/finish before/after).
    • Responses return relations grouped by type and support bulk creation.
  • Documentation

    • Added OpenAPI documentation for work item relations endpoints.

@makeplane
Copy link

makeplane bot commented Mar 16, 2026

Linked to Plane Work Item(s)

This comment was auto-generated by Plane

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Mar 16, 2026

📝 Walkthrough

Walkthrough

Adds issue-relation support: new serializers, a GET/POST API endpoint to list and bulk-create typed relations, URL routing, and OpenAPI decorator for relation endpoints.

Changes

Cohort / File(s) Summary
Serializers
apps/api/plane/api/serializers/__init__.py, apps/api/plane/api/serializers/issue.py
Introduced IssueRelationCreateSerializer, IssueRelationResponseSerializer, IssueRelationRemoveSerializer, IssueRelationSerializer, and RelatedIssueSerializer; exported the new serializers in package __init__.py.
API views
apps/api/plane/api/views/issue.py, apps/api/plane/api/views/__init__.py
Added IssueRelationListCreateAPIEndpoint with GET (aggregates relations by type using ArrayAgg/Coalesce) and POST (validates, computes actual relation, bulk-creates with ignore_conflicts, logs activity, re-fetches and serializes results). Exported the endpoint in views __init__.
Routing
apps/api/plane/api/urls/work_item.py
Added route workspaces/<str:slug>/projects/<uuid:project_id>/work-items/<uuid:issue_id>/relations/ mapped to the new endpoint allowing GET and POST.
OpenAPI docs
apps/api/plane/utils/openapi/__init__.py, apps/api/plane/utils/openapi/decorators.py
Added work_item_relation_docs decorator (default tag "Work Item Relations", required workspace/project params, standard error responses) and exported it in the openapi package.

Sequence Diagram(s)

sequenceDiagram
    participant Client
    participant Endpoint as IssueRelationListCreateAPIEndpoint
    participant DB as Database
    participant Serializer as Serializers

    rect rgba(100, 150, 200, 0.5)
    Note over Client,Endpoint: GET /relations/
    Client->>Endpoint: GET /relations/
    Endpoint->>DB: Query IssueRelation with ArrayAgg/Coalesce
    DB-->>Endpoint: Aggregated relation IDs by type
    Endpoint->>Serializer: IssueRelationResponseSerializer
    Serializer-->>Endpoint: Grouped relations payload
    Endpoint-->>Client: 200 OK with grouped relations
    end

    rect rgba(200, 150, 100, 0.5)
    Note over Client,Endpoint: POST /relations/
    Client->>Endpoint: POST /relations/ (relation_type, issue_ids)
    Endpoint->>Serializer: IssueRelationCreateSerializer (validate)
    Serializer-->>Endpoint: Validated data
    Endpoint->>DB: Compute actual_relation, bulk create IssueRelation (ignore_conflicts)
    DB-->>Endpoint: Created/ignored rows
    Endpoint->>DB: Re-fetch created relations with select_related
    DB-->>Endpoint: Enriched relation records
    Endpoint->>Serializer: RelatedIssueSerializer / IssueRelationSerializer
    Serializer-->>Endpoint: Serialized created relations
    Endpoint-->>Client: 201 Created with relation metadata
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Poem

🐰 In burrows of code relations weave,
IDs hop, types group, and never leave,
A GET to gather, a POST to sow,
Connections sprout where data flows,
✨ Hop—our relations now nicely grow!

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description check ✅ Passed The PR description is comprehensive and follows the template structure, including a detailed description of changes, type of change (Feature), API endpoints with sample payloads/responses, and references to the related work.
Docstring Coverage ✅ Passed Docstring coverage is 91.67% which is sufficient. The required threshold is 80.00%.
Title check ✅ Passed The title clearly and specifically summarizes the main change: adding an external API for issue relations.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat-issue_relations_api

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🧹 Nitpick comments (1)
apps/api/plane/api/views/issue.py (1)

2249-2252: Remove pagination params from relation-list docs (endpoint is not paginated).

get() returns a grouped object, not a paginated list, so cursor/per_page in docs is misleading.

Also applies to: 2283-2330

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/api/plane/api/views/issue.py` around lines 2249 - 2252, Remove the
pagination parameters from the relation-list endpoint documentation because
get() returns a grouped object rather than a paginated list; specifically remove
CURSOR_PARAMETER and PER_PAGE_PARAMETER (and any mentions of
ORDER_BY_PARAMETER/CURSOR usage) from the parameter array where
ISSUE_ID_PARAMETER is used for the relation-list docs referenced around the
get() handler, and do the same cleanup for the second occurrence noted (the
block around the other relation-list docs). Ensure the docs only include
ISSUE_ID_PARAMETER and any relevant non-pagination params so the OpenAPI docs
reflect the non-paginated grouped response.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@apps/api/plane/api/serializers/issue.py`:
- Around line 533-534: The serializer currently uses PrimaryKeyRelatedField for
the scalar UUID source "related_issue.project_id" (in the project_id field)
which expects a model instance; change the field to
serializers.UUIDField(source="related_issue.project_id", read_only=True) and do
the same for the other occurrence of project_id elsewhere in the file (the
duplicate at the later block around sequence_id), while leaving sequence_id as
serializers.IntegerField(source="related_issue.sequence_id", read_only=True);
ensure both project_id declarations reference the scalar UUID source and are
read_only UUIDField instances.

In `@apps/api/plane/api/views/issue.py`:
- Around line 2391-2418: The bulk_create call
(IssueRelation.objects.bulk_create) can insert relations with issue IDs that
don't belong to the same project/workspace and may raise IntegrityError; before
calling bulk_create, fetch and validate that the source issue_id and every ID in
serializer.validated_data["issues"] exist and belong to the same
Project/workspace (use Issue.objects.filter(pk__in=ids, project_id=project_id,
workspace_id=project.workspace_id) and compare counts or returned IDs), and
return a 400 Response if any IDs are missing/out-of-scope; only then proceed to
build the IssueRelation instances (respecting is_reverse and
get_actual_relation) and call bulk_create with ignore_conflicts.
- Around line 2403-2460: The bulk_create call uses ignore_conflicts=True which
silently skips existing issue-pair rows that have a different relation_type,
then still returns 201 and possibly an empty/partial result; fix by pre-checking
for conflicting existing relations before creating: query IssueRelation for the
same issue/related_issue pairs (use the same logic as refetch_filter but without
relation_type or with exclude(relation_type=actual_relation)) to find rows where
a pair exists with a different relation_type, and if any are found return a 409
response listing the conflicting pairs (or otherwise surface an error) instead
of proceeding to IssueRelation.objects.bulk_create with ignore_conflicts=True;
keep the rest of the flow (refetch_filter, refetched_relations, serializer
selection) unchanged but only execute them after the conflict check passes.

---

Nitpick comments:
In `@apps/api/plane/api/views/issue.py`:
- Around line 2249-2252: Remove the pagination parameters from the relation-list
endpoint documentation because get() returns a grouped object rather than a
paginated list; specifically remove CURSOR_PARAMETER and PER_PAGE_PARAMETER (and
any mentions of ORDER_BY_PARAMETER/CURSOR usage) from the parameter array where
ISSUE_ID_PARAMETER is used for the relation-list docs referenced around the
get() handler, and do the same cleanup for the second occurrence noted (the
block around the other relation-list docs). Ensure the docs only include
ISSUE_ID_PARAMETER and any relevant non-pagination params so the OpenAPI docs
reflect the non-paginated grouped response.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 496d6ae4-e178-48a4-be19-e5a7bd9bb0d2

📥 Commits

Reviewing files that changed from the base of the PR and between 588dc29 and 7fe16b0.

📒 Files selected for processing (7)
  • apps/api/plane/api/serializers/__init__.py
  • apps/api/plane/api/serializers/issue.py
  • apps/api/plane/api/urls/work_item.py
  • apps/api/plane/api/views/__init__.py
  • apps/api/plane/api/views/issue.py
  • apps/api/plane/utils/openapi/__init__.py
  • apps/api/plane/utils/openapi/decorators.py

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

♻️ Duplicate comments (1)
apps/api/plane/api/serializers/issue.py (1)

624-626: ⚠️ Potential issue | 🔴 Critical

Use UUIDField for project_id on the reverse serializer.

Line 625 points PrimaryKeyRelatedField at issue.project_id, which is already a UUID scalar. DRF relation fields expect a related object and will try to serialize .pk, so reverse-relation responses can fail here. Switch this to serializers.UUIDField(source="issue.project_id", read_only=True).

💡 Proposed fix
-    project_id = serializers.PrimaryKeyRelatedField(source="issue.project_id", read_only=True)
+    project_id = serializers.UUIDField(source="issue.project_id", read_only=True)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/api/plane/api/serializers/issue.py` around lines 624 - 626, Change the
serializer field for project_id to use serializers.UUIDField instead of
serializers.PrimaryKeyRelatedField: update the field declaration (currently
project_id = serializers.PrimaryKeyRelatedField(source="issue.project_id",
read_only=True)) to project_id =
serializers.UUIDField(source="issue.project_id", read_only=True) so the reverse
serializer emits the UUID scalar from issue.project_id rather than treating it
as a related-object field; leave id and sequence_id declarations unchanged.
🧹 Nitpick comments (1)
apps/api/plane/api/views/issue.py (1)

2442-2448: issue__type is still missing from the refetch query.

Reverse responses serialize issue.type.id and issue.type.is_epic, so this query still does per-row lookups even though the comment says N+1s are being avoided. Add issue__type to select_related() here.

♻️ Proposed fix
         refetched_relations = IssueRelation.objects.filter(
             refetch_filter,
             workspace__slug=slug,
         ).select_related(
+            "issue__type",
             "issue__state",
             "related_issue__state",
         )
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/api/plane/api/views/issue.py` around lines 2442 - 2448, The refetch
query on IssueRelation (refetched_relations =
IssueRelation.objects.filter(...).select_related(...)) omits the related issue
type so reverse responses still trigger per-row lookups; update the
select_related call on IssueRelation to include "issue__type" (alongside the
existing "issue__state" and "related_issue__state") so that issue.type.id and
issue.type.is_epic are fetched in the same query.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@apps/api/plane/api/views/issue.py`:
- Around line 2394-2412: The bulk_create block can create duplicate logical
relations for symmetric types like "duplicate" and "relates_to" because you only
flip asymmetric types via is_reverse; update the logic around IssueRelation bulk
creation so symmetric relations are normalized or pre-checked: detect when
relation_type is symmetric (e.g., "duplicate", "relates_to"), then for each
candidate pair normalize the order (canonicalize by id or tuple sort) or query
existing IssueRelation rows for either (issue, issue_id) or (issue_id, issue)
and filter out those already present before calling
IssueRelation.objects.bulk_create; keep references to get_actual_relation,
relation_type, is_reverse, issues, and the IssueRelation bulk_create call to
locate and modify the code.
- Around line 2351-2354: The POST 201 OpenApiResponse currently claims
IssueRelationSerializer[] but actually returns IssueRelationSerializer[] or
RelatedIssueSerializer[] depending on the relation_type; update the OpenAPI
response to document both shapes explicitly (or normalize the response to a
single serializer) — e.g., replace the single IssueRelationSerializer(many=True)
with a polymorphic/OneOf response that includes
IssueRelationSerializer(many=True) and RelatedIssueSerializer(many=True) (using
your OpenAPI helper / drf-spectacular OneOf construct), and apply the same
change for the other POST response instance referenced (the one around lines
2450-2452); ensure the relation_type parameter is noted in the operation
description so consumers know which variant will be returned.
- Around line 2297-2300: Ensure the code validates that the requested issue_id
belongs to the route's (slug, project_id) and returns a 404 if not, then
restrict the IssueRelation query to that project: first fetch or get Issue (or
Issue.objects.filter(pk=issue_id, workspace__slug=slug, project__id=project_id))
and raise Http404 if absent, and then build issue_relation_qs using
IssueRelation.objects.filter((Q(issue_id=issue_id) |
Q(related_issue_id=issue_id)), workspace__slug=slug, project__id=project_id) so
relations are limited to the same project; update any variables (e.g.,
issue_relation_qs, issue_id checks) accordingly.

---

Duplicate comments:
In `@apps/api/plane/api/serializers/issue.py`:
- Around line 624-626: Change the serializer field for project_id to use
serializers.UUIDField instead of serializers.PrimaryKeyRelatedField: update the
field declaration (currently project_id =
serializers.PrimaryKeyRelatedField(source="issue.project_id", read_only=True))
to project_id = serializers.UUIDField(source="issue.project_id", read_only=True)
so the reverse serializer emits the UUID scalar from issue.project_id rather
than treating it as a related-object field; leave id and sequence_id
declarations unchanged.

---

Nitpick comments:
In `@apps/api/plane/api/views/issue.py`:
- Around line 2442-2448: The refetch query on IssueRelation (refetched_relations
= IssueRelation.objects.filter(...).select_related(...)) omits the related issue
type so reverse responses still trigger per-row lookups; update the
select_related call on IssueRelation to include "issue__type" (alongside the
existing "issue__state" and "related_issue__state") so that issue.type.id and
issue.type.is_epic are fetched in the same query.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: e6f7dbc2-e245-47be-9c51-7baf4f2c0c63

📥 Commits

Reviewing files that changed from the base of the PR and between 7fe16b0 and 1931ea0.

📒 Files selected for processing (2)
  • apps/api/plane/api/serializers/issue.py
  • apps/api/plane/api/views/issue.py

@Saurabhkmr98 Saurabhkmr98 changed the title [SILO-1087] feat: Added IssueRelations external API [SILO-1087] feat: add IssueRelations external API Mar 25, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants