Skip to content

Commit f4dff52

Browse files
committed
refactor: consolidate item deletion and queries
1 parent 4dedf03 commit f4dff52

1 file changed

Lines changed: 77 additions & 96 deletions

File tree

internal/services/item_crud_service.go

Lines changed: 77 additions & 96 deletions
Original file line numberDiff line numberDiff line change
@@ -114,13 +114,7 @@ func (s *ItemCRUDService) DeleteSingleWithMetadata(itemID int, metadata itemeven
114114
}); err != nil {
115115
return err
116116
}
117-
repository.InvalidateItemListCountCache(s.db, workspaceID)
118-
119-
// Live-update publish (WI-483): the delete has committed.
120-
PublishItemChange(itemID, ItemChangeDeleted)
121-
if parentID != nil {
122-
PublishItemChange(*parentID, ItemChangeUpdated)
123-
}
117+
s.finishItemDeletion(workspaceID, []int{itemID}, parentID)
124118
return nil
125119
}
126120

@@ -131,7 +125,6 @@ func (s *ItemCRUDService) Delete(itemID int) (*DeleteResult, error) {
131125

132126
// DeleteWithMetadata deletes an item subtree and records every removed item.
133127
func (s *ItemCRUDService) DeleteWithMetadata(itemID int, metadata itemevents.Metadata) (*DeleteResult, error) {
134-
// Get parent ID before deleting
135128
parentID, err := s.repo.GetParentID(itemID)
136129
if err != nil {
137130
if err == repository.ErrNotFound {
@@ -140,86 +133,81 @@ func (s *ItemCRUDService) DeleteWithMetadata(itemID int, metadata itemevents.Met
140133
return nil, err
141134
}
142135
workspaceID, _ := s.repo.GetWorkspaceID(itemID)
143-
144-
// Get all descendant IDs for cascade operations
145136
descendantIDs, err := s.repo.GetDescendantIDs(itemID)
146137
if err != nil {
147138
return nil, fmt.Errorf("failed to get descendants: %w", err)
148139
}
149-
150-
// Delete all related data for item and descendants
151140
allIDs := append([]int{itemID}, descendantIDs...)
152-
if err := database.WithTx(s.db, func(tx database.Tx) error {
153-
items, err := s.repo.FindByIDsForUpdateContext(context.Background(), tx, allIDs)
141+
if err := s.deleteItemSubtree(itemID, allIDs, len(descendantIDs), metadata); err != nil {
142+
return nil, err
143+
}
144+
s.finishItemDeletion(workspaceID, allIDs, parentID)
145+
return &DeleteResult{
146+
DeletedCount: len(allIDs),
147+
DescendantIDs: descendantIDs,
148+
AffectedParent: parentID,
149+
}, nil
150+
}
151+
152+
func (s *ItemCRUDService) deleteItemSubtree(rootID int, itemIDs []int, descendantCount int, metadata itemevents.Metadata) error {
153+
return database.WithTx(s.db, func(tx database.Tx) error {
154+
ctx := context.Background()
155+
items, err := s.repo.FindByIDsForUpdateContext(ctx, tx, itemIDs)
154156
if err != nil {
155157
return err
156158
}
157-
if len(items) != len(allIDs) {
159+
if len(items) != len(itemIDs) {
158160
return repository.ErrNotFound
159161
}
160162
if metadata.OccurredAt.IsZero() {
161163
metadata.OccurredAt = time.Now()
162164
}
163165
recorder := itemevents.NewRecorder(s.db)
164-
if err := recordRemovedItemLinks(context.Background(), s.db, tx, allIDs, metadata); err != nil {
166+
if err := recordRemovedItemLinks(ctx, s.db, tx, itemIDs, metadata); err != nil {
165167
return err
166168
}
167169
for _, item := range items {
168-
descendantCount := 0
169-
if item.ID == itemID {
170-
descendantCount = len(descendantIDs)
170+
removedDescendants := 0
171+
if item.ID == rootID {
172+
removedDescendants = descendantCount
171173
}
172-
if _, err := recorder.Deleted(context.Background(), tx, item, descendantCount, metadata); err != nil {
174+
if _, err := recorder.Deleted(ctx, tx, item, removedDescendants, metadata); err != nil {
173175
return err
174176
}
175177
}
176-
for _, id := range allIDs {
177-
// Delete watches
178-
if err := s.repo.DeleteItemWatches(tx, id); err != nil {
178+
for _, id := range itemIDs {
179+
if err := s.deleteItemRelationsTx(tx, id); err != nil {
179180
return err
180181
}
181-
182-
// Delete history
183-
if err := s.repo.DeleteItemHistory(tx, id); err != nil {
184-
return err
185-
}
186-
187-
// Delete links
188-
if err := s.repo.DeleteItemLinks(tx, id); err != nil {
189-
return err
190-
}
191-
192-
// Clear worklog references
193-
if err := s.repo.ClearWorklogItemReferences(tx, id); err != nil {
194-
return err
195-
}
196-
197-
// Delete the item itself
198182
if err := s.repo.Delete(tx, id); err != nil {
199183
return err
200184
}
201185
}
202186
return nil
203-
}); err != nil {
204-
return nil, err
187+
})
188+
}
189+
190+
func (s *ItemCRUDService) deleteItemRelationsTx(tx database.Tx, itemID int) error {
191+
if err := s.repo.DeleteItemWatches(tx, itemID); err != nil {
192+
return err
205193
}
206-
repository.InvalidateItemListCountCache(s.db, workspaceID)
194+
if err := s.repo.DeleteItemHistory(tx, itemID); err != nil {
195+
return err
196+
}
197+
if err := s.repo.DeleteItemLinks(tx, itemID); err != nil {
198+
return err
199+
}
200+
return s.repo.ClearWorklogItemReferences(tx, itemID)
201+
}
207202

208-
// Live-update publish (WI-483): the cascade delete committed. Announce every
209-
// removed item (so anyone viewing a descendant reconciles) and refresh the
210-
// affected parent's child list.
211-
for _, id := range allIDs {
203+
func (s *ItemCRUDService) finishItemDeletion(workspaceID int, itemIDs []int, parentID *int) {
204+
repository.InvalidateItemListCountCache(s.db, workspaceID)
205+
for _, id := range itemIDs {
212206
PublishItemChange(id, ItemChangeDeleted)
213207
}
214208
if parentID != nil {
215209
PublishItemChange(*parentID, ItemChangeUpdated)
216210
}
217-
218-
return &DeleteResult{
219-
DeletedCount: len(allIDs),
220-
DescendantIDs: descendantIDs,
221-
AffectedParent: parentID,
222-
}, nil
223211
}
224212

225213
// CopyOptions contains options for copying an item
@@ -485,6 +473,31 @@ func (s *ItemCRUDService) evaluateQLContext(requestCtx context.Context, qlQuery
485473
return qlSQL, qlArgs, nil
486474
}
487475

476+
type resolvedItemListQL struct {
477+
sql string
478+
args []any
479+
collectionResolved bool
480+
}
481+
482+
func (s *ItemCRUDService) resolveItemListQLContext(ctx context.Context, qlQuery string, collectionID int, subQL string, userID int) (resolvedItemListQL, error) {
483+
qlQuery, collectionResolved, err := s.resolveCollectionQLContext(ctx, qlQuery, collectionID)
484+
if err != nil {
485+
return resolvedItemListQL{}, err
486+
}
487+
if subQL = strings.TrimSpace(subQL); subQL != "" {
488+
if qlQuery == "" {
489+
qlQuery = subQL
490+
} else {
491+
qlQuery = "(" + qlQuery + ") AND (" + subQL + ")"
492+
}
493+
}
494+
qlSQL, qlArgs, err := s.evaluateQLContext(ctx, qlQuery, cql.UserContext(userID))
495+
if err != nil {
496+
return resolvedItemListQL{}, err
497+
}
498+
return resolvedItemListQL{sql: qlSQL, args: qlArgs, collectionResolved: collectionResolved}, nil
499+
}
500+
488501
// BacklogParams contains parameters for retrieving backlog items
489502
type BacklogParams struct {
490503
WorkspaceID int // 0 if not specified (collection-only query)
@@ -521,33 +534,17 @@ func (s *ItemCRUDService) GetBacklogItemsContext(ctx context.Context, params Bac
521534
StatusIDs: backlogStatusIDs,
522535
}
523536

524-
// Resolve QL query from collection or direct parameter
525-
qlQuery, collectionResolved, err := s.resolveCollectionQLContext(ctx, params.QLQuery, params.CollectionID)
537+
resolvedQL, err := s.resolveItemListQLContext(ctx, params.QLQuery, params.CollectionID, params.SubQLQuery, params.UserID)
526538
if err != nil {
527539
return nil, 0, err
528540
}
529-
530-
// Combine with sub-filter QL if provided
531-
if subQL := strings.TrimSpace(params.SubQLQuery); subQL != "" {
532-
if qlQuery != "" {
533-
qlQuery = "(" + qlQuery + ") AND (" + subQL + ")"
534-
} else {
535-
qlQuery = subQL
536-
}
537-
}
538-
539-
// Evaluate QL query into SQL
540-
qlSQL, qlArgs, err := s.evaluateQLContext(ctx, qlQuery, cql.UserContext(params.UserID))
541-
if err != nil {
542-
return nil, 0, err
543-
}
544-
if qlSQL != "" {
545-
filters.QLQuery = qlSQL
546-
filters.QLArgs = qlArgs
541+
if resolvedQL.sql != "" {
542+
filters.QLQuery = resolvedQL.sql
543+
filters.QLArgs = resolvedQL.args
547544
}
548545

549546
// Apply workspace_id filter only when no collection was resolved
550-
if !collectionResolved && params.WorkspaceID > 0 {
547+
if !resolvedQL.collectionResolved && params.WorkspaceID > 0 {
551548
filters.WorkspaceID = &params.WorkspaceID
552549
}
553550

@@ -596,39 +593,23 @@ func (s *ItemCRUDService) ListWithQLPageContext(ctx context.Context, params List
596593

597594
filters := params.Filters
598595

599-
// Resolve QL query from collection or direct parameter
600-
qlQuery, collectionResolved, err := s.resolveCollectionQLContext(ctx, params.QLQuery, params.CollectionID)
601-
if err != nil {
602-
return repository.ItemListPage{}, err
603-
}
604-
605-
// Combine with sub-filter QL if provided
606-
if subQL := strings.TrimSpace(params.SubQLQuery); subQL != "" {
607-
if qlQuery != "" {
608-
qlQuery = "(" + qlQuery + ") AND (" + subQL + ")"
609-
} else {
610-
qlQuery = subQL
611-
}
612-
}
613-
614-
// Evaluate QL query into SQL
615-
qlSQL, qlArgs, err := s.evaluateQLContext(ctx, qlQuery, cql.UserContext(params.UserID))
596+
resolvedQL, err := s.resolveItemListQLContext(ctx, params.QLQuery, params.CollectionID, params.SubQLQuery, params.UserID)
616597
if err != nil {
617598
return repository.ItemListPage{}, err
618599
}
619-
if qlSQL != "" {
620-
filters.QLQuery = qlSQL
621-
filters.QLArgs = qlArgs
600+
if resolvedQL.sql != "" {
601+
filters.QLQuery = resolvedQL.sql
602+
filters.QLArgs = resolvedQL.args
622603
}
623604

624605
// If collection was resolved but produced no effective query, return empty results.
625606
// A collection with no filter means "nothing to show yet."
626-
if collectionResolved && filters.QLQuery == "" {
607+
if resolvedQL.collectionResolved && filters.QLQuery == "" {
627608
return repository.ItemListPage{Items: []models.Item{}}, nil
628609
}
629610

630611
// Apply workspace_id filter only when no collection was resolved
631-
if !collectionResolved && params.WorkspaceID > 0 {
612+
if !resolvedQL.collectionResolved && params.WorkspaceID > 0 {
632613
filters.WorkspaceID = &params.WorkspaceID
633614
}
634615

0 commit comments

Comments
 (0)