geek-cafe-saas-sdk 0.7.5__py3-none-any.whl → 0.8.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of geek-cafe-saas-sdk might be problematic. Click here for more details.

Files changed (131) hide show
  1. geek_cafe_saas_sdk/__init__.py +1 -1
  2. geek_cafe_saas_sdk/core/anonymous_context.py +321 -0
  3. geek_cafe_saas_sdk/core/request_context.py +184 -0
  4. geek_cafe_saas_sdk/decorators/__init__.py +1 -1
  5. geek_cafe_saas_sdk/decorators/auth.py +6 -6
  6. geek_cafe_saas_sdk/decorators/core.py +44 -44
  7. geek_cafe_saas_sdk/domains/analytics/services/website_analytics_service.py +1 -3
  8. geek_cafe_saas_sdk/domains/analytics/services/website_analytics_summary_service.py +1 -3
  9. geek_cafe_saas_sdk/domains/analytics/services/website_analytics_tally_service.py +15 -3
  10. geek_cafe_saas_sdk/domains/auth/handlers/users/create/app.py +1 -1
  11. geek_cafe_saas_sdk/domains/auth/handlers/users/delete/app.py +1 -1
  12. geek_cafe_saas_sdk/domains/auth/handlers/users/get/app.py +1 -1
  13. geek_cafe_saas_sdk/domains/auth/handlers/users/list/app.py +1 -1
  14. geek_cafe_saas_sdk/domains/auth/handlers/users/update/app.py +1 -1
  15. geek_cafe_saas_sdk/domains/auth/services/resource_permission_service.py +1 -4
  16. geek_cafe_saas_sdk/domains/auth/services/user_service.py +0 -2
  17. geek_cafe_saas_sdk/domains/communities/handlers/communities/create/app.py +1 -1
  18. geek_cafe_saas_sdk/domains/communities/handlers/communities/delete/app.py +1 -1
  19. geek_cafe_saas_sdk/domains/communities/handlers/communities/get/app.py +1 -1
  20. geek_cafe_saas_sdk/domains/communities/handlers/communities/list/app.py +1 -1
  21. geek_cafe_saas_sdk/domains/communities/handlers/communities/update/app.py +1 -1
  22. geek_cafe_saas_sdk/domains/communities/services/community_member_service.py +1 -3
  23. geek_cafe_saas_sdk/domains/communities/services/community_service.py +3 -3
  24. geek_cafe_saas_sdk/domains/events/handlers/attendees/app.py +1 -1
  25. geek_cafe_saas_sdk/domains/events/handlers/cancel/app.py +1 -1
  26. geek_cafe_saas_sdk/domains/events/handlers/check_in/app.py +1 -1
  27. geek_cafe_saas_sdk/domains/events/handlers/create/app.py +1 -1
  28. geek_cafe_saas_sdk/domains/events/handlers/delete/app.py +1 -1
  29. geek_cafe_saas_sdk/domains/events/handlers/get/app.py +1 -1
  30. geek_cafe_saas_sdk/domains/events/handlers/invite/app.py +1 -1
  31. geek_cafe_saas_sdk/domains/events/handlers/list/app.py +1 -1
  32. geek_cafe_saas_sdk/domains/events/handlers/publish/app.py +1 -1
  33. geek_cafe_saas_sdk/domains/events/handlers/rsvp/app.py +1 -1
  34. geek_cafe_saas_sdk/domains/events/handlers/update/app.py +1 -1
  35. geek_cafe_saas_sdk/domains/events/services/event_attendee_service.py +1 -2
  36. geek_cafe_saas_sdk/domains/events/services/event_service.py +6 -4
  37. geek_cafe_saas_sdk/domains/files/handlers/README.md +1 -1
  38. geek_cafe_saas_sdk/domains/files/handlers/files/create/app.py +1 -1
  39. geek_cafe_saas_sdk/domains/files/handlers/files/download/app.py +1 -1
  40. geek_cafe_saas_sdk/domains/files/handlers/files/get/app.py +1 -1
  41. geek_cafe_saas_sdk/domains/files/handlers/files/list/app.py +1 -1
  42. geek_cafe_saas_sdk/domains/files/handlers/lineage/create_derived/app.py +1 -1
  43. geek_cafe_saas_sdk/domains/files/handlers/lineage/create_main/app.py +1 -1
  44. geek_cafe_saas_sdk/domains/files/handlers/lineage/download_bundle/app.py +1 -1
  45. geek_cafe_saas_sdk/domains/files/handlers/lineage/get_lineage/app.py +1 -1
  46. geek_cafe_saas_sdk/domains/files/handlers/lineage/prepare_bundle/app.py +1 -1
  47. geek_cafe_saas_sdk/domains/files/models/file.py +16 -6
  48. geek_cafe_saas_sdk/domains/files/services/directory_service.py +34 -9
  49. geek_cafe_saas_sdk/domains/files/services/file_system_service.py +38 -3
  50. geek_cafe_saas_sdk/domains/files/services/file_version_service.py +33 -36
  51. geek_cafe_saas_sdk/domains/messaging/handlers/chat_channels/create/app.py +1 -1
  52. geek_cafe_saas_sdk/domains/messaging/handlers/chat_channels/delete/app.py +1 -1
  53. geek_cafe_saas_sdk/domains/messaging/handlers/chat_channels/get/app.py +1 -1
  54. geek_cafe_saas_sdk/domains/messaging/handlers/chat_channels/list/app.py +1 -1
  55. geek_cafe_saas_sdk/domains/messaging/handlers/chat_channels/update/app.py +1 -1
  56. geek_cafe_saas_sdk/domains/messaging/handlers/chat_messages/create/app.py +1 -1
  57. geek_cafe_saas_sdk/domains/messaging/handlers/chat_messages/delete/app.py +1 -1
  58. geek_cafe_saas_sdk/domains/messaging/handlers/chat_messages/get/app.py +1 -1
  59. geek_cafe_saas_sdk/domains/messaging/handlers/chat_messages/list/app.py +1 -1
  60. geek_cafe_saas_sdk/domains/messaging/handlers/chat_messages/update/app.py +1 -1
  61. geek_cafe_saas_sdk/domains/messaging/handlers/contact_threads/create/app.py +1 -1
  62. geek_cafe_saas_sdk/domains/messaging/handlers/contact_threads/delete/app.py +1 -1
  63. geek_cafe_saas_sdk/domains/messaging/handlers/contact_threads/get/app.py +1 -1
  64. geek_cafe_saas_sdk/domains/messaging/handlers/contact_threads/list/app.py +1 -1
  65. geek_cafe_saas_sdk/domains/messaging/handlers/contact_threads/update/app.py +1 -1
  66. geek_cafe_saas_sdk/domains/messaging/services/chat_channel_service.py +35 -2
  67. geek_cafe_saas_sdk/domains/messaging/services/chat_message_service.py +20 -3
  68. geek_cafe_saas_sdk/domains/messaging/services/contact_thread_service.py +1 -3
  69. geek_cafe_saas_sdk/domains/notifications/handlers/create_webhook/app.py +1 -1
  70. geek_cafe_saas_sdk/domains/notifications/handlers/get/app.py +1 -1
  71. geek_cafe_saas_sdk/domains/notifications/handlers/get_preferences/app.py +1 -1
  72. geek_cafe_saas_sdk/domains/notifications/handlers/list/app.py +1 -1
  73. geek_cafe_saas_sdk/domains/notifications/handlers/list_webhooks/app.py +1 -1
  74. geek_cafe_saas_sdk/domains/notifications/handlers/mark_read/app.py +1 -1
  75. geek_cafe_saas_sdk/domains/notifications/handlers/send/app.py +1 -1
  76. geek_cafe_saas_sdk/domains/notifications/handlers/update_preferences/app.py +1 -1
  77. geek_cafe_saas_sdk/domains/notifications/services/notification_service.py +1 -2
  78. geek_cafe_saas_sdk/domains/payments/handlers/billing_accounts/create/app.py +1 -1
  79. geek_cafe_saas_sdk/domains/payments/handlers/billing_accounts/get/app.py +1 -1
  80. geek_cafe_saas_sdk/domains/payments/handlers/billing_accounts/update/app.py +1 -1
  81. geek_cafe_saas_sdk/domains/payments/handlers/payment_intents/create/app.py +1 -1
  82. geek_cafe_saas_sdk/domains/payments/handlers/payment_intents/get/app.py +1 -1
  83. geek_cafe_saas_sdk/domains/payments/handlers/payments/get/app.py +1 -1
  84. geek_cafe_saas_sdk/domains/payments/handlers/payments/list/app.py +1 -1
  85. geek_cafe_saas_sdk/domains/payments/handlers/payments/record/app.py +1 -1
  86. geek_cafe_saas_sdk/domains/payments/handlers/refunds/create/app.py +1 -1
  87. geek_cafe_saas_sdk/domains/payments/handlers/refunds/get/app.py +1 -1
  88. geek_cafe_saas_sdk/domains/payments/services/payment_service.py +1 -2
  89. geek_cafe_saas_sdk/domains/subscriptions/handlers/README.md +2 -2
  90. geek_cafe_saas_sdk/domains/subscriptions/handlers/addons/create/app.py +1 -1
  91. geek_cafe_saas_sdk/domains/subscriptions/handlers/addons/get/app.py +1 -1
  92. geek_cafe_saas_sdk/domains/subscriptions/handlers/addons/list/app.py +1 -1
  93. geek_cafe_saas_sdk/domains/subscriptions/handlers/addons/update/app.py +1 -1
  94. geek_cafe_saas_sdk/domains/subscriptions/handlers/discounts/create/app.py +1 -1
  95. geek_cafe_saas_sdk/domains/subscriptions/handlers/discounts/get/app.py +1 -1
  96. geek_cafe_saas_sdk/domains/subscriptions/handlers/discounts/validate/app.py +1 -1
  97. geek_cafe_saas_sdk/domains/subscriptions/handlers/plans/create/app.py +1 -1
  98. geek_cafe_saas_sdk/domains/subscriptions/handlers/plans/get/app.py +1 -1
  99. geek_cafe_saas_sdk/domains/subscriptions/handlers/plans/list/app.py +1 -1
  100. geek_cafe_saas_sdk/domains/subscriptions/handlers/plans/update/app.py +1 -1
  101. geek_cafe_saas_sdk/domains/subscriptions/handlers/usage/aggregate/app.py +1 -1
  102. geek_cafe_saas_sdk/domains/subscriptions/handlers/usage/record/app.py +1 -1
  103. geek_cafe_saas_sdk/domains/subscriptions/services/subscription_manager_service.py +0 -2
  104. geek_cafe_saas_sdk/domains/tenancy/handlers/subscriptions/activate/app.py +1 -1
  105. geek_cafe_saas_sdk/domains/tenancy/handlers/subscriptions/active/app.py +1 -1
  106. geek_cafe_saas_sdk/domains/tenancy/handlers/subscriptions/cancel/app.py +1 -1
  107. geek_cafe_saas_sdk/domains/tenancy/handlers/subscriptions/get/app.py +1 -1
  108. geek_cafe_saas_sdk/domains/tenancy/handlers/subscriptions/list/app.py +1 -1
  109. geek_cafe_saas_sdk/domains/tenancy/handlers/subscriptions/record_payment/app.py +1 -1
  110. geek_cafe_saas_sdk/domains/tenancy/handlers/tenants/get/app.py +1 -1
  111. geek_cafe_saas_sdk/domains/tenancy/handlers/tenants/me/app.py +1 -1
  112. geek_cafe_saas_sdk/domains/tenancy/handlers/tenants/signup/app.py +1 -1
  113. geek_cafe_saas_sdk/domains/tenancy/handlers/tenants/update/app.py +1 -1
  114. geek_cafe_saas_sdk/domains/tenancy/services/subscription_service.py +3 -3
  115. geek_cafe_saas_sdk/domains/tenancy/services/tenant_service.py +3 -3
  116. geek_cafe_saas_sdk/domains/voting/handlers/votes/delete/app.py +1 -1
  117. geek_cafe_saas_sdk/domains/voting/handlers/votes/get/app.py +1 -1
  118. geek_cafe_saas_sdk/domains/voting/handlers/votes/list/app.py +1 -1
  119. geek_cafe_saas_sdk/domains/voting/handlers/votes/update/app.py +1 -1
  120. geek_cafe_saas_sdk/domains/voting/services/vote_service.py +1 -5
  121. geek_cafe_saas_sdk/domains/voting/services/vote_summary_service.py +1 -5
  122. geek_cafe_saas_sdk/domains/voting/services/vote_tally_service.py +10 -3
  123. geek_cafe_saas_sdk/lambda_handlers/_base/base_handler.py +40 -6
  124. geek_cafe_saas_sdk/lambda_handlers/_base/service_pool.py +157 -12
  125. geek_cafe_saas_sdk/middleware/authorization.py +1 -1
  126. geek_cafe_saas_sdk/middleware/cors.py +8 -8
  127. geek_cafe_saas_sdk/services/database_service.py +76 -5
  128. {geek_cafe_saas_sdk-0.7.5.dist-info → geek_cafe_saas_sdk-0.8.0.dist-info}/METADATA +16 -16
  129. {geek_cafe_saas_sdk-0.7.5.dist-info → geek_cafe_saas_sdk-0.8.0.dist-info}/RECORD +131 -129
  130. {geek_cafe_saas_sdk-0.7.5.dist-info → geek_cafe_saas_sdk-0.8.0.dist-info}/WHEEL +0 -0
  131. {geek_cafe_saas_sdk-0.7.5.dist-info → geek_cafe_saas_sdk-0.8.0.dist-info}/licenses/LICENSE +0 -0
@@ -17,7 +17,7 @@ handler_wrapper = create_handler(
17
17
  )
18
18
 
19
19
 
20
- def handler(event: Dict[str, Any], context: Any, injected_service=None) -> Dict[str, Any]:
20
+ def lambda_handler(event: Dict[str, Any], context: Any, injected_service=None) -> Dict[str, Any]:
21
21
  """
22
22
  Get file metadata by ID.
23
23
 
@@ -17,7 +17,7 @@ handler_wrapper = create_handler(
17
17
  )
18
18
 
19
19
 
20
- def handler(event: Dict[str, Any], context: Any, injected_service=None) -> Dict[str, Any]:
20
+ def lambda_handler(event: Dict[str, Any], context: Any, injected_service=None) -> Dict[str, Any]:
21
21
  """
22
22
  List files.
23
23
 
@@ -18,7 +18,7 @@ handler_wrapper = create_handler(
18
18
  )
19
19
 
20
20
 
21
- def handler(event: Dict[str, Any], context: Any, injected_service=None) -> Dict[str, Any]:
21
+ def lambda_handler(event: Dict[str, Any], context: Any, injected_service=None) -> Dict[str, Any]:
22
22
  """
23
23
  Create a derived file from a main file (e.g., data cleaning).
24
24
 
@@ -18,7 +18,7 @@ handler_wrapper = create_handler(
18
18
  )
19
19
 
20
20
 
21
- def handler(event: Dict[str, Any], context: Any, injected_service=None) -> Dict[str, Any]:
21
+ def lambda_handler(event: Dict[str, Any], context: Any, injected_service=None) -> Dict[str, Any]:
22
22
  """
23
23
  Create a main file from an original file (e.g., XLS → CSV conversion).
24
24
 
@@ -18,7 +18,7 @@ handler_wrapper = create_handler(
18
18
  )
19
19
 
20
20
 
21
- def handler(event: Dict[str, Any], context: Any, injected_service=None) -> Dict[str, Any]:
21
+ def lambda_handler(event: Dict[str, Any], context: Any, injected_service=None) -> Dict[str, Any]:
22
22
  """
23
23
  Download complete lineage bundle with file content.
24
24
 
@@ -17,7 +17,7 @@ handler_wrapper = create_handler(
17
17
  )
18
18
 
19
19
 
20
- def handler(event: Dict[str, Any], context: Any, injected_service=None) -> Dict[str, Any]:
20
+ def lambda_handler(event: Dict[str, Any], context: Any, injected_service=None) -> Dict[str, Any]:
21
21
  """
22
22
  Get complete lineage for a file.
23
23
 
@@ -17,7 +17,7 @@ handler_wrapper = create_handler(
17
17
  )
18
18
 
19
19
 
20
- def handler(event: Dict[str, Any], context: Any, injected_service=None) -> Dict[str, Any]:
20
+ def lambda_handler(event: Dict[str, Any], context: Any, injected_service=None) -> Dict[str, Any]:
21
21
  """
22
22
  Prepare lineage bundle for a file (metadata only, no file content).
23
23
 
@@ -103,16 +103,14 @@ class File(BaseModel):
103
103
  primary.sort_key.value = lambda: "metadata"
104
104
  self.indexes.add_primary(primary)
105
105
 
106
- # GSI1: Files by directory
106
+ # GSI1: Files by directory (id)
107
107
  gsi = DynamoDBIndex()
108
108
  gsi.name = "gsi1"
109
109
  gsi.partition_key.attribute_name = f"{gsi.name}_pk"
110
110
  gsi.partition_key.value = lambda: DynamoDBKey.build_key(("tenant", self.tenant_id))
111
- gsi.sort_key.attribute_name = f"{gsi.name}_sk"
112
- if self.directory_id:
113
- gsi.sort_key.value = lambda: DynamoDBKey.build_key(("directory", self.directory_id), ("file", self.file_name))
114
- else:
115
- gsi.sort_key.value = lambda: DynamoDBKey.build_key(("root", "file"), ("name", self.file_name))
111
+ gsi.sort_key.attribute_name = f"{gsi.name}_sk"
112
+ gsi.sort_key.value = lambda: DynamoDBKey.build_key(("directory", self.directory_id), ("file", self.file_name))
113
+
116
114
  self.indexes.add_secondary(gsi)
117
115
 
118
116
  # GSI2: Files by owner
@@ -123,6 +121,18 @@ class File(BaseModel):
123
121
  gsi.sort_key.attribute_name = f"{gsi.name}_sk"
124
122
  gsi.sort_key.value = lambda: DynamoDBKey.build_key(("file", self.created_utc_ts))
125
123
  self.indexes.add_secondary(gsi)
124
+
125
+
126
+
127
+ # GSI3: Files by directory (virtual path)
128
+ gsi = DynamoDBIndex()
129
+ gsi.name = "gsi3"
130
+ gsi.partition_key.attribute_name = f"{gsi.name}_pk"
131
+ gsi.partition_key.value = lambda: DynamoDBKey.build_key(("tenant", self.tenant_id))
132
+ gsi.sort_key.attribute_name = f"{gsi.name}_sk"
133
+ gsi.sort_key.value = lambda: DynamoDBKey.build_key(("directory", self.virtual_path), ("file", self.file_name))
134
+
135
+ self.indexes.add_secondary(gsi)
126
136
 
127
137
  # Properties - File Identity (alias for BaseModel.id)
128
138
  @property
@@ -265,8 +265,11 @@ class DirectoryService(DatabaseService[Directory]):
265
265
  # Update timestamp
266
266
  directory.updated_utc_ts = dt.datetime.now(dt.UTC).timestamp()
267
267
 
268
- # Save to DynamoDB
268
+ # Call prep_for_save() AFTER all properties are set
269
+ # This ensures GSI lambdas evaluate with the updated values
269
270
  directory.prep_for_save()
271
+
272
+ # Save to DynamoDB
270
273
  return self._save_model(directory)
271
274
 
272
275
  except (ValidationError, AccessDeniedError) as e:
@@ -514,8 +517,11 @@ class DirectoryService(DatabaseService[Directory]):
514
517
  directory.parent_id = new_parent_id
515
518
  directory.updated_utc_ts = dt.datetime.now(dt.UTC).timestamp()
516
519
 
517
- # Save to DynamoDB
520
+ # Call prep_for_save() AFTER all properties (parent_id, full_path) are set
521
+ # This ensures GSI lambdas evaluate with the updated values
518
522
  directory.prep_for_save()
523
+
524
+ # Save to DynamoDB
519
525
  save_result = self._save_model(directory)
520
526
 
521
527
  if not save_result.success:
@@ -549,22 +555,41 @@ class DirectoryService(DatabaseService[Directory]):
549
555
  directory_name: str,
550
556
  parent_id: Optional[str]
551
557
  ) -> bool:
552
- """Check if directory name already exists in parent."""
558
+ """Check if directory with this full path already exists.
559
+
560
+ Args:
561
+ tenant_id: Tenant ID
562
+ directory_name: Name of directory to check
563
+ parent_id: Parent directory ID (None for root)
564
+
565
+ Returns:
566
+ True if a non-deleted directory with this path exists
567
+ """
553
568
  try:
554
- # Use GSI2 to query subdirectories by parent
569
+ # Build the expected full_path for the new directory
570
+ if parent_id:
571
+ # Get parent to build full path
572
+ parent = self._get_model_by_id(parent_id, Directory)
573
+ if not parent:
574
+ return False # Parent doesn't exist, can't check
575
+ expected_full_path = f"{parent.full_path}/{directory_name}"
576
+ else:
577
+ # Root directory
578
+ expected_full_path = f"/{directory_name}"
579
+
580
+ # Query GSI1 by full_path to check for duplicates
555
581
  temp_directory = Directory()
556
582
  temp_directory.tenant_id = tenant_id
557
- temp_directory.parent_id = parent_id
558
- temp_directory.directory_name = directory_name
583
+ temp_directory.full_path = expected_full_path
559
584
 
560
- query_result = self._query_by_index(temp_directory, "gsi2", limit=1)
585
+ query_result = self._query_by_index(temp_directory, "gsi1", limit=1)
561
586
 
562
587
  if not query_result.success:
563
588
  return False
564
589
 
565
- # Check if any non-deleted directories with this name exist
590
+ # Check if any non-deleted directories with this path exist
566
591
  for directory in query_result.data:
567
- if directory.directory_name == directory_name and directory.status != "deleted":
592
+ if directory.status != "deleted":
568
593
  return True
569
594
 
570
595
  return False
@@ -14,6 +14,7 @@ from geek_cafe_saas_sdk.core.service_errors import ValidationError, NotFoundErro
14
14
  from geek_cafe_saas_sdk.core.error_codes import ErrorCode
15
15
  from geek_cafe_saas_sdk.domains.files.models.file import File
16
16
  from geek_cafe_saas_sdk.domains.files.services.s3_file_service import S3FileService
17
+ from geek_cafe_saas_sdk.domains.files.services.directory_service import DirectoryService
17
18
  import os
18
19
  from pathlib import Path
19
20
 
@@ -36,7 +37,8 @@ class FileSystemService(DatabaseService[File]):
36
37
  dynamodb: Optional[DynamoDB] = None,
37
38
  table_name: Optional[str] = None,
38
39
  s3_service: Optional[S3FileService] = None,
39
- default_bucket: Optional[str] = None
40
+ default_bucket: Optional[str] = None,
41
+ request_context: Optional[Dict[str, str]] = None
40
42
  ):
41
43
  """
42
44
  Initialize FileSystemService.
@@ -47,9 +49,10 @@ class FileSystemService(DatabaseService[File]):
47
49
  s3_service: S3FileService instance
48
50
  default_bucket: Default S3 bucket
49
51
  """
50
- super().__init__(dynamodb=dynamodb, table_name=table_name)
52
+ super().__init__(dynamodb=dynamodb, table_name=table_name, request_context=request_context)
51
53
  self.s3_service = s3_service or S3FileService(default_bucket=default_bucket)
52
54
  self.default_bucket = default_bucket or os.getenv("S3_FILE_BUCKET")
55
+ self.directory_service = DirectoryService(dynamodb=dynamodb, table_name=table_name, request_context=request_context)
53
56
 
54
57
  def create(
55
58
  self,
@@ -280,6 +283,16 @@ class FileSystemService(DatabaseService[File]):
280
283
  "status", "derived_file_count"
281
284
  ]
282
285
 
286
+ # Track if GSI-affecting fields changed
287
+ directory_changed = "directory_id" in updates
288
+ name_changed = "file_name" in updates
289
+
290
+ old_directory_id = file.directory_id
291
+ old_file_name = file.file_name
292
+ old_virtual_path = file.virtual_path
293
+
294
+
295
+ # Apply updates (only allowed fields)
283
296
  for field, value in updates.items():
284
297
  if field in allowed_fields:
285
298
  setattr(file, field, value)
@@ -288,8 +301,30 @@ class FileSystemService(DatabaseService[File]):
288
301
  import datetime as dt
289
302
  file.updated_utc_ts = dt.datetime.now(dt.UTC).timestamp()
290
303
 
291
- # Save to DynamoDB
304
+ # If directory or name changed, manually update the GSI1 lambda
305
+ # because it was set during init and needs to be recreated with new values
306
+ if directory_changed or name_changed:
307
+ # get the directory
308
+ directory_result = self.directory_service.get_by_id(
309
+ resource_id=file.directory_id,
310
+ tenant_id=tenant_id,
311
+ user_id=user_id
312
+ )
313
+ if not directory_result.success:
314
+ return directory_result
315
+
316
+ directory = directory_result.data
317
+
318
+ # update the file path
319
+ file.virtual_path = f"{directory.full_path}/{file.file_name}"
320
+
321
+ # update the file name
322
+ file.file_name = file.file_name
323
+
324
+ # Call prep_for_save() AFTER all properties are set
292
325
  file.prep_for_save()
326
+
327
+ # Save to DynamoDB
293
328
  return self._save_model(file)
294
329
 
295
330
  except AccessDeniedError as e:
@@ -8,6 +8,7 @@ MIT License. See Project Root for the license information.
8
8
  from typing import Dict, Any, Optional, List
9
9
  from boto3.dynamodb.conditions import Key
10
10
  from boto3_assist.dynamodb.dynamodb import DynamoDB
11
+ from boto3_assist.dynamodb.dynamodb_index import DynamoDBKey
11
12
  from geek_cafe_saas_sdk.services.database_service import DatabaseService
12
13
  from geek_cafe_saas_sdk.core.service_result import ServiceResult
13
14
  from geek_cafe_saas_sdk.core.service_errors import ValidationError, NotFoundError, AccessDeniedError
@@ -38,7 +39,8 @@ class FileVersionService(DatabaseService[FileVersion]):
38
39
  table_name: Optional[str] = None,
39
40
  s3_service: Optional[S3FileService] = None,
40
41
  default_bucket: Optional[str] = None,
41
- max_versions: Optional[int] = None
42
+ max_versions: Optional[int] = None,
43
+ request_context: Optional[Dict[str, str]] = None
42
44
  ):
43
45
  """
44
46
  Initialize FileVersionService.
@@ -50,7 +52,7 @@ class FileVersionService(DatabaseService[FileVersion]):
50
52
  default_bucket: Default S3 bucket
51
53
  max_versions: Maximum versions to retain (default: 25)
52
54
  """
53
- super().__init__(dynamodb=dynamodb, table_name=table_name)
55
+ super().__init__(dynamodb=dynamodb, table_name=table_name, request_context=request_context)
54
56
  self.s3_service = s3_service or S3FileService(default_bucket=default_bucket)
55
57
  self.default_bucket = default_bucket or os.getenv("S3_FILE_BUCKET")
56
58
  self.max_versions = max_versions or int(os.getenv("FILE_MAX_VERSIONS", "25"))
@@ -92,13 +94,12 @@ class FileVersionService(DatabaseService[FileVersion]):
92
94
  "versioning_strategy"
93
95
  )
94
96
 
95
- # Get current version number
96
- current_version_num = self._get_latest_version_number(tenant_id, file_id)
97
+ # Get current version number from file metadata (more reliable than GSI query)
98
+ current_version_num = file.version_count or 0
97
99
  new_version_num = current_version_num + 1
98
100
 
99
101
  # Create FileVersion model
100
102
  version = FileVersion()
101
- version.prep_for_save()
102
103
  version.tenant_id = tenant_id
103
104
  version.file_id = file_id
104
105
  version.version_number = new_version_num
@@ -110,6 +111,9 @@ class FileVersionService(DatabaseService[FileVersion]):
110
111
  version.is_current = True
111
112
  version.status = "active"
112
113
 
114
+ # Must call prep_for_save before accessing version_id
115
+ version.prep_for_save()
116
+
113
117
  # Build S3 key for this version
114
118
  s3_key = f"{tenant_id}/files/{file_id}/versions/{version.version_id}/{file.file_name}"
115
119
  version.s3_bucket = self.default_bucket
@@ -132,8 +136,7 @@ class FileVersionService(DatabaseService[FileVersion]):
132
136
  if file.current_version_id:
133
137
  self._mark_version_as_not_current(tenant_id, file_id, file.current_version_id)
134
138
 
135
- # Save version metadata to DynamoDB
136
- version.prep_for_save()
139
+ # Save version metadata to DynamoDB (already called prep_for_save above)
137
140
  save_result = self._save_model(version)
138
141
 
139
142
  if not save_result.success:
@@ -367,21 +370,26 @@ class FileVersionService(DatabaseService[FileVersion]):
367
370
  )
368
371
 
369
372
  # Use GSI1 to query versions by file
370
- temp_version = FileVersion()
371
- temp_version.file_id = file_id
372
-
373
- # Query using helper method (descending to get newest first)
374
- query_result = self._query_by_index(temp_version, "gsi1", limit=limit, ascending=False)
373
+ gsi1_pk = DynamoDBKey.build_key(("file", file_id))
375
374
 
376
- if not query_result.success:
377
- return query_result
375
+ # Query using boto3_assist DynamoDB query
376
+ results = self.dynamodb.query(
377
+ key=Key("gsi1_pk").eq(gsi1_pk),
378
+ table_name=self.table_name,
379
+ index_name="gsi1",
380
+ ascending=False, # Descending order (newest first)
381
+ limit=limit
382
+ )
378
383
 
379
- # Filter results
384
+ # Map results to FileVersion models
380
385
  versions = []
381
- for version in query_result.data:
382
- # Filter out archived versions (which includes deleted ones)
383
- if version.status == "active":
384
- versions.append(version)
386
+ if results and 'Items' in results:
387
+ for item in results['Items']:
388
+ version = FileVersion()
389
+ version.map(item)
390
+ # Filter out archived versions (which includes deleted ones)
391
+ if version.status == "active":
392
+ versions.append(version)
385
393
 
386
394
  return ServiceResult.success_result(versions)
387
395
 
@@ -577,21 +585,10 @@ class FileVersionService(DatabaseService[FileVersion]):
577
585
  ) -> None:
578
586
  """Update file record with current version info."""
579
587
  try:
580
- file = File()
581
- file.id = file_id
582
- file.tenant_id = tenant_id
583
-
584
- key = file.get_key("gsi1")
585
-
586
- result = self.dynamodb.get(
587
- table_name=self.table_name,
588
- key=key
589
- )
588
+ # Use DatabaseService helper to get the file (uses primary key)
589
+ file = self._get_model_by_id_with_tenant_check(file_id, File, tenant_id)
590
590
 
591
- if result and 'Item' in result:
592
- file = File()
593
- file.map(result['Item'])
594
-
591
+ if file:
595
592
  file.current_version_id = version_id
596
593
  file.version_count = version_number
597
594
  file.updated_utc_ts = dt.datetime.now(dt.UTC).timestamp()
@@ -605,13 +602,13 @@ class FileVersionService(DatabaseService[FileVersion]):
605
602
  """Apply version retention policy (delete old versions beyond max_versions)."""
606
603
  try:
607
604
  # Get all versions
608
- gsi1_pk = f"FILE#{tenant_id}#{file_id}"
605
+ gsi1_pk = DynamoDBKey.build_key(("file", file_id))
609
606
 
610
607
  results = self.dynamodb.query(
611
- key=Key('gsi1_pk').eq(gsi1_pk) & Key('gsi1_sk').begins_with("VERSION#"),
608
+ key=Key("gsi1_pk").eq(gsi1_pk),
612
609
  table_name=self.table_name,
613
610
  index_name="gsi1",
614
- ascending=False # Newest first
611
+ ascending=False # Descending order (newest first)
615
612
  )
616
613
 
617
614
  items = results.get('Items', [])
@@ -16,7 +16,7 @@ handler_wrapper = create_handler(
16
16
  )
17
17
 
18
18
 
19
- def handler(event: Dict[str, Any], context: Any, injected_service=None) -> Dict[str, Any]:
19
+ def lambda_handler(event: Dict[str, Any], context: Any, injected_service=None) -> Dict[str, Any]:
20
20
  """
21
21
  Create a new chat channel.
22
22
 
@@ -15,7 +15,7 @@ handler_wrapper = create_handler(
15
15
  )
16
16
 
17
17
 
18
- def handler(event: Dict[str, Any], context: Any, injected_service=None) -> Dict[str, Any]:
18
+ def lambda_handler(event: Dict[str, Any], context: Any, injected_service=None) -> Dict[str, Any]:
19
19
  """
20
20
  Delete (soft delete) a chat channel.
21
21
 
@@ -15,7 +15,7 @@ handler_wrapper = create_handler(
15
15
  )
16
16
 
17
17
 
18
- def handler(event: Dict[str, Any], context: Any, injected_service=None) -> Dict[str, Any]:
18
+ def lambda_handler(event: Dict[str, Any], context: Any, injected_service=None) -> Dict[str, Any]:
19
19
  """
20
20
  Get a chat channel by ID.
21
21
 
@@ -18,7 +18,7 @@ handler_wrapper = create_handler(
18
18
  )
19
19
 
20
20
 
21
- def handler(event: Dict[str, Any], context: Any, injected_service=None) -> Dict[str, Any]:
21
+ def lambda_handler(event: Dict[str, Any], context: Any, injected_service=None) -> Dict[str, Any]:
22
22
  """
23
23
  List chat channels based on query parameters.
24
24
 
@@ -19,7 +19,7 @@ handler_wrapper = create_handler(
19
19
  )
20
20
 
21
21
 
22
- def handler(event: Dict[str, Any], context: Any, injected_service=None) -> Dict[str, Any]:
22
+ def lambda_handler(event: Dict[str, Any], context: Any, injected_service=None) -> Dict[str, Any]:
23
23
  """
24
24
  Update a chat channel.
25
25
 
@@ -16,7 +16,7 @@ handler_wrapper = create_handler(
16
16
  )
17
17
 
18
18
 
19
- def handler(event: Dict[str, Any], context: Any, injected_service=None) -> Dict[str, Any]:
19
+ def lambda_handler(event: Dict[str, Any], context: Any, injected_service=None) -> Dict[str, Any]:
20
20
  """
21
21
  Create a new chat message.
22
22
 
@@ -15,7 +15,7 @@ handler_wrapper = create_handler(
15
15
  )
16
16
 
17
17
 
18
- def handler(event: Dict[str, Any], context: Any, injected_service=None) -> Dict[str, Any]:
18
+ def lambda_handler(event: Dict[str, Any], context: Any, injected_service=None) -> Dict[str, Any]:
19
19
  """
20
20
  Delete (soft delete) a chat message.
21
21
 
@@ -15,7 +15,7 @@ handler_wrapper = create_handler(
15
15
  )
16
16
 
17
17
 
18
- def handler(event: Dict[str, Any], context: Any, injected_service=None) -> Dict[str, Any]:
18
+ def lambda_handler(event: Dict[str, Any], context: Any, injected_service=None) -> Dict[str, Any]:
19
19
  """
20
20
  Get a chat message by ID.
21
21
 
@@ -18,7 +18,7 @@ handler_wrapper = create_handler(
18
18
  )
19
19
 
20
20
 
21
- def handler(event: Dict[str, Any], context: Any, injected_service=None) -> Dict[str, Any]:
21
+ def lambda_handler(event: Dict[str, Any], context: Any, injected_service=None) -> Dict[str, Any]:
22
22
  """
23
23
  List chat messages based on query parameters.
24
24
 
@@ -18,7 +18,7 @@ handler_wrapper = create_handler(
18
18
  )
19
19
 
20
20
 
21
- def handler(event: Dict[str, Any], context: Any, injected_service=None) -> Dict[str, Any]:
21
+ def lambda_handler(event: Dict[str, Any], context: Any, injected_service=None) -> Dict[str, Any]:
22
22
  """
23
23
  Update a chat message.
24
24
 
@@ -19,7 +19,7 @@ handler_wrapper = create_handler(
19
19
  )
20
20
 
21
21
 
22
- def handler(event: Dict[str, Any], context: Any, injected_service=None) -> Dict[str, Any]:
22
+ def lambda_handler(event: Dict[str, Any], context: Any, injected_service=None) -> Dict[str, Any]:
23
23
  """
24
24
  Create a new contact thread.
25
25
 
@@ -15,7 +15,7 @@ handler_wrapper = create_handler(
15
15
  )
16
16
 
17
17
 
18
- def handler(event: Dict[str, Any], context: Any, injected_service=None) -> Dict[str, Any]:
18
+ def lambda_handler(event: Dict[str, Any], context: Any, injected_service=None) -> Dict[str, Any]:
19
19
  """
20
20
  Delete (soft delete) a contact thread.
21
21
 
@@ -15,7 +15,7 @@ handler_wrapper = create_handler(
15
15
  )
16
16
 
17
17
 
18
- def handler(event: Dict[str, Any], context: Any, injected_service=None) -> Dict[str, Any]:
18
+ def lambda_handler(event: Dict[str, Any], context: Any, injected_service=None) -> Dict[str, Any]:
19
19
  """
20
20
  Get a contact thread by ID.
21
21
 
@@ -18,7 +18,7 @@ handler_wrapper = create_handler(
18
18
  )
19
19
 
20
20
 
21
- def handler(event: Dict[str, Any], context: Any, injected_service=None) -> Dict[str, Any]:
21
+ def lambda_handler(event: Dict[str, Any], context: Any, injected_service=None) -> Dict[str, Any]:
22
22
  """
23
23
  List contact threads based on query parameters.
24
24
 
@@ -20,7 +20,7 @@ handler_wrapper = create_handler(
20
20
  )
21
21
 
22
22
 
23
- def handler(event: Dict[str, Any], context: Any, injected_service=None) -> Dict[str, Any]:
23
+ def lambda_handler(event: Dict[str, Any], context: Any, injected_service=None) -> Dict[str, Any]:
24
24
  """
25
25
  Update a contact thread.
26
26