Coverage for functions \ flipdare \ firestore \ core \ sub_comment_transaction.py: 60%

50 statements  

« prev     ^ index     » next       coverage.py v7.13.0, created at 2026-05-08 12:22 +1000

1#!/usr/bin/env python 

2# Copyright (c) 2026 Flipdare Pty Ltd. All rights reserved. 

3# 

4# This file is part of Flipdare's proprietary software and contains 

5# confidential and copyrighted material. Unauthorised copying, 

6# modification, distribution, or use of this file is strictly 

7# prohibited without prior written permission from Flipdare Pty Ltd. 

8# 

9# This software includes third-party components licensed under MIT, 

10# BSD, and Apache 2.0 licences. See THIRD_PARTY_NOTICES for details. 

11# 

12 

13 

14from typing import Any 

15 

16from google.cloud.firestore import DocumentReference, Increment, Transaction, transactional 

17from google.cloud.firestore_v1 import DocumentSnapshot 

18from flipdare.error import DatabaseError 

19from flipdare.firestore.core.app_base_model import AppBaseModel 

20from flipdare.generated.shared.app_error_code import AppErrorCode 

21from flipdare.generated.shared.firestore_collections import FirestoreCollections 

22 

23__all__ = ["SubCommentTransaction"] 

24 

25 

26class SubCommentTransaction[T: AppBaseModel]: 

27 def __init__(self, parent_db: Any, sub_collection_name: FirestoreCollections) -> None: 

28 self.parent_db = parent_db 

29 self.sub_name = sub_collection_name 

30 self.client = parent_db.client 

31 

32 def create_with_increment( 

33 self, 

34 parent_id: str, 

35 model: T, 

36 count_field: str = "comment_count", 

37 ) -> DocumentReference: 

38 """Orchestrates the transaction to create a child and increment the parent.""" 

39 transaction = self.client.transaction() 

40 

41 # References 

42 parent_ref = self.client.collection(self.parent_db.collection_name).document(parent_id) 

43 child_ref = parent_ref.collection(self.sub_name).document() 

44 if not isinstance(child_ref, DocumentReference): 

45 msg = f"Failed to create child document for parent {parent_id}" 

46 raise DatabaseError( 

47 msg, 

48 error_code=AppErrorCode.DATABASE, 

49 collection_name=self.sub_name, 

50 document_id=parent_id, 

51 ) 

52 

53 @transactional 

54 def _run_transaction(trans: Transaction) -> None: 

55 # 1. READ: Must happen before writes 

56 snapshot = parent_ref.get(transaction=transaction) 

57 self._check_valid_snapshot(parent_id, snapshot) 

58 

59 # 2. WRITE: Create Child 

60 trans.set(child_ref, model.to_dict()) 

61 

62 # 3. WRITE: Increment Parent 

63 trans.update(parent_ref, {count_field: Increment(1)}) 

64 

65 _run_transaction(transaction) 

66 return child_ref 

67 

68 def delete_with_decrement( 

69 self, 

70 parent_id: str, 

71 child_id: str, 

72 count_field: str = "comment_count", 

73 ) -> None: 

74 """Atomically deletes a child document and decrements the parent counter.""" 

75 transaction = self.client.transaction() 

76 

77 # Define references 

78 parent_ref = self.client.collection(self.parent_db.collection_name).document(parent_id) 

79 child_ref = parent_ref.collection(self.sub_name).document(child_id) 

80 

81 @transactional 

82 def _run_delete_transaction(trans: Transaction) -> None: 

83 # 1. READ: Check both exist before performing any writes 

84 # Transactional reads are required to ensure data consistency 

85 parent_snap = parent_ref.get(transaction=trans) 

86 child_snap = child_ref.get(transaction=trans) 

87 

88 self._check_valid_snapshot(parent_id, parent_snap) 

89 self._check_valid_snapshot(child_id, child_snap) 

90 

91 assert isinstance(parent_snap, DocumentSnapshot) # narrowing 

92 current_count = parent_snap.get(count_field) or 0 

93 

94 # 2. WRITE: Delete the child document 

95 trans.delete(child_ref) 

96 

97 # 3. WRITE: Decrement the counter on the parent 

98 # Passing -1 to Increment() performs a server-side atomic subtraction 

99 if current_count > 0: 

100 trans.update(parent_ref, {count_field: Increment(-1)}) 

101 

102 _run_delete_transaction(transaction) 

103 

104 def _check_valid_snapshot(self, doc_id: str, snap: Any) -> None: 

105 if not isinstance(snap, DocumentSnapshot): 

106 msg = f"Got an child AwaitableSnapshot for id {doc_id}, check you didnt call await.." 

107 raise TypeError(msg) 

108 

109 if not snap.exists: 

110 raise ValueError(f"Failed to retrieve document {doc_id}.")