[SILO-1087] feat: add IssueRelations external API#8763
[SILO-1087] feat: add IssueRelations external API#8763Saurabhkmr98 wants to merge 2 commits intopreviewfrom
Conversation
|
Linked to Plane Work Item(s) This comment was auto-generated by Plane |
📝 WalkthroughWalkthroughAdds 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
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
Estimated code review effort🎯 4 (Complex) | ⏱️ ~50 minutes Poem
🚥 Pre-merge checks | ✅ 3✅ Passed checks (3 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
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, socursor/per_pagein 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
📒 Files selected for processing (7)
apps/api/plane/api/serializers/__init__.pyapps/api/plane/api/serializers/issue.pyapps/api/plane/api/urls/work_item.pyapps/api/plane/api/views/__init__.pyapps/api/plane/api/views/issue.pyapps/api/plane/utils/openapi/__init__.pyapps/api/plane/utils/openapi/decorators.py
There was a problem hiding this comment.
Actionable comments posted: 3
♻️ Duplicate comments (1)
apps/api/plane/api/serializers/issue.py (1)
624-626:⚠️ Potential issue | 🔴 CriticalUse
UUIDFieldforproject_idon the reverse serializer.Line 625 points
PrimaryKeyRelatedFieldatissue.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 toserializers.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__typeis still missing from the refetch query.Reverse responses serialize
issue.type.idandissue.type.is_epic, so this query still does per-row lookups even though the comment says N+1s are being avoided. Addissue__typetoselect_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
📒 Files selected for processing (2)
apps/api/plane/api/serializers/issue.pyapps/api/plane/api/views/issue.py
Description
URL -
/api/v1/workspaces/{slug}/projects/{project_id}/work-items/{issue_id}/relations/POST endpoint
Sample Payload
Sample Response
GET endpoint
Sample response
Type of Change
Screenshots and Media (if applicable)
Test Scenarios
References
Summary by CodeRabbit
New Features
Documentation