geek-cafe-saas-sdk 0.7.4__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.
- geek_cafe_saas_sdk/__init__.py +1 -1
- geek_cafe_saas_sdk/core/anonymous_context.py +321 -0
- geek_cafe_saas_sdk/core/request_context.py +184 -0
- geek_cafe_saas_sdk/decorators/__init__.py +1 -1
- geek_cafe_saas_sdk/decorators/auth.py +6 -6
- geek_cafe_saas_sdk/decorators/core.py +44 -44
- geek_cafe_saas_sdk/domains/analytics/services/website_analytics_service.py +1 -3
- geek_cafe_saas_sdk/domains/analytics/services/website_analytics_summary_service.py +1 -3
- geek_cafe_saas_sdk/domains/analytics/services/website_analytics_tally_service.py +15 -3
- geek_cafe_saas_sdk/domains/auth/handlers/users/create/app.py +1 -1
- geek_cafe_saas_sdk/domains/auth/handlers/users/delete/app.py +1 -1
- geek_cafe_saas_sdk/domains/auth/handlers/users/get/app.py +1 -1
- geek_cafe_saas_sdk/domains/auth/handlers/users/list/app.py +1 -1
- geek_cafe_saas_sdk/domains/auth/handlers/users/update/app.py +1 -1
- geek_cafe_saas_sdk/domains/auth/services/resource_permission_service.py +1 -4
- geek_cafe_saas_sdk/domains/auth/services/user_service.py +0 -2
- geek_cafe_saas_sdk/domains/communities/handlers/communities/create/app.py +1 -1
- geek_cafe_saas_sdk/domains/communities/handlers/communities/delete/app.py +1 -1
- geek_cafe_saas_sdk/domains/communities/handlers/communities/get/app.py +1 -1
- geek_cafe_saas_sdk/domains/communities/handlers/communities/list/app.py +1 -1
- geek_cafe_saas_sdk/domains/communities/handlers/communities/update/app.py +1 -1
- geek_cafe_saas_sdk/domains/communities/services/community_member_service.py +1 -3
- geek_cafe_saas_sdk/domains/communities/services/community_service.py +3 -3
- geek_cafe_saas_sdk/domains/events/handlers/attendees/app.py +1 -1
- geek_cafe_saas_sdk/domains/events/handlers/cancel/app.py +1 -1
- geek_cafe_saas_sdk/domains/events/handlers/check_in/app.py +1 -1
- geek_cafe_saas_sdk/domains/events/handlers/create/app.py +1 -1
- geek_cafe_saas_sdk/domains/events/handlers/delete/app.py +1 -1
- geek_cafe_saas_sdk/domains/events/handlers/get/app.py +1 -1
- geek_cafe_saas_sdk/domains/events/handlers/invite/app.py +1 -1
- geek_cafe_saas_sdk/domains/events/handlers/list/app.py +1 -1
- geek_cafe_saas_sdk/domains/events/handlers/publish/app.py +1 -1
- geek_cafe_saas_sdk/domains/events/handlers/rsvp/app.py +1 -1
- geek_cafe_saas_sdk/domains/events/handlers/update/app.py +1 -1
- geek_cafe_saas_sdk/domains/events/services/event_attendee_service.py +1 -2
- geek_cafe_saas_sdk/domains/events/services/event_service.py +6 -4
- geek_cafe_saas_sdk/domains/files/handlers/README.md +1 -1
- geek_cafe_saas_sdk/domains/files/handlers/files/create/app.py +1 -1
- geek_cafe_saas_sdk/domains/files/handlers/files/download/app.py +1 -1
- geek_cafe_saas_sdk/domains/files/handlers/files/get/app.py +1 -1
- geek_cafe_saas_sdk/domains/files/handlers/files/list/app.py +1 -1
- geek_cafe_saas_sdk/domains/files/handlers/lineage/create_derived/app.py +1 -1
- geek_cafe_saas_sdk/domains/files/handlers/lineage/create_main/app.py +1 -1
- geek_cafe_saas_sdk/domains/files/handlers/lineage/download_bundle/app.py +1 -1
- geek_cafe_saas_sdk/domains/files/handlers/lineage/get_lineage/app.py +1 -1
- geek_cafe_saas_sdk/domains/files/handlers/lineage/prepare_bundle/app.py +1 -1
- geek_cafe_saas_sdk/domains/files/models/file.py +16 -6
- geek_cafe_saas_sdk/domains/files/services/directory_service.py +34 -9
- geek_cafe_saas_sdk/domains/files/services/file_system_service.py +38 -3
- geek_cafe_saas_sdk/domains/files/services/file_version_service.py +33 -36
- geek_cafe_saas_sdk/domains/messaging/handlers/chat_channels/create/app.py +1 -1
- geek_cafe_saas_sdk/domains/messaging/handlers/chat_channels/delete/app.py +1 -1
- geek_cafe_saas_sdk/domains/messaging/handlers/chat_channels/get/app.py +1 -1
- geek_cafe_saas_sdk/domains/messaging/handlers/chat_channels/list/app.py +1 -1
- geek_cafe_saas_sdk/domains/messaging/handlers/chat_channels/update/app.py +1 -1
- geek_cafe_saas_sdk/domains/messaging/handlers/chat_messages/create/app.py +1 -1
- geek_cafe_saas_sdk/domains/messaging/handlers/chat_messages/delete/app.py +1 -1
- geek_cafe_saas_sdk/domains/messaging/handlers/chat_messages/get/app.py +1 -1
- geek_cafe_saas_sdk/domains/messaging/handlers/chat_messages/list/app.py +1 -1
- geek_cafe_saas_sdk/domains/messaging/handlers/chat_messages/update/app.py +1 -1
- geek_cafe_saas_sdk/domains/messaging/handlers/contact_threads/create/app.py +1 -1
- geek_cafe_saas_sdk/domains/messaging/handlers/contact_threads/delete/app.py +1 -1
- geek_cafe_saas_sdk/domains/messaging/handlers/contact_threads/get/app.py +1 -1
- geek_cafe_saas_sdk/domains/messaging/handlers/contact_threads/list/app.py +3 -2
- geek_cafe_saas_sdk/domains/messaging/handlers/contact_threads/update/app.py +1 -1
- geek_cafe_saas_sdk/domains/messaging/services/chat_channel_service.py +35 -2
- geek_cafe_saas_sdk/domains/messaging/services/chat_message_service.py +20 -3
- geek_cafe_saas_sdk/domains/messaging/services/contact_thread_service.py +1 -3
- geek_cafe_saas_sdk/domains/notifications/handlers/create_webhook/app.py +1 -1
- geek_cafe_saas_sdk/domains/notifications/handlers/get/app.py +1 -1
- geek_cafe_saas_sdk/domains/notifications/handlers/get_preferences/app.py +1 -1
- geek_cafe_saas_sdk/domains/notifications/handlers/list/app.py +1 -1
- geek_cafe_saas_sdk/domains/notifications/handlers/list_webhooks/app.py +1 -1
- geek_cafe_saas_sdk/domains/notifications/handlers/mark_read/app.py +1 -1
- geek_cafe_saas_sdk/domains/notifications/handlers/send/app.py +1 -1
- geek_cafe_saas_sdk/domains/notifications/handlers/update_preferences/app.py +1 -1
- geek_cafe_saas_sdk/domains/notifications/services/notification_service.py +1 -2
- geek_cafe_saas_sdk/domains/payments/handlers/billing_accounts/create/app.py +1 -1
- geek_cafe_saas_sdk/domains/payments/handlers/billing_accounts/get/app.py +1 -1
- geek_cafe_saas_sdk/domains/payments/handlers/billing_accounts/update/app.py +1 -1
- geek_cafe_saas_sdk/domains/payments/handlers/payment_intents/create/app.py +1 -1
- geek_cafe_saas_sdk/domains/payments/handlers/payment_intents/get/app.py +1 -1
- geek_cafe_saas_sdk/domains/payments/handlers/payments/get/app.py +1 -1
- geek_cafe_saas_sdk/domains/payments/handlers/payments/list/app.py +1 -1
- geek_cafe_saas_sdk/domains/payments/handlers/payments/record/app.py +1 -1
- geek_cafe_saas_sdk/domains/payments/handlers/refunds/create/app.py +1 -1
- geek_cafe_saas_sdk/domains/payments/handlers/refunds/get/app.py +1 -1
- geek_cafe_saas_sdk/domains/payments/services/payment_service.py +1 -2
- geek_cafe_saas_sdk/domains/subscriptions/handlers/README.md +2 -2
- geek_cafe_saas_sdk/domains/subscriptions/handlers/addons/create/app.py +1 -1
- geek_cafe_saas_sdk/domains/subscriptions/handlers/addons/get/app.py +1 -1
- geek_cafe_saas_sdk/domains/subscriptions/handlers/addons/list/app.py +1 -1
- geek_cafe_saas_sdk/domains/subscriptions/handlers/addons/update/app.py +1 -1
- geek_cafe_saas_sdk/domains/subscriptions/handlers/discounts/create/app.py +1 -1
- geek_cafe_saas_sdk/domains/subscriptions/handlers/discounts/get/app.py +1 -1
- geek_cafe_saas_sdk/domains/subscriptions/handlers/discounts/validate/app.py +1 -1
- geek_cafe_saas_sdk/domains/subscriptions/handlers/plans/create/app.py +1 -1
- geek_cafe_saas_sdk/domains/subscriptions/handlers/plans/get/app.py +1 -1
- geek_cafe_saas_sdk/domains/subscriptions/handlers/plans/list/app.py +1 -1
- geek_cafe_saas_sdk/domains/subscriptions/handlers/plans/update/app.py +1 -1
- geek_cafe_saas_sdk/domains/subscriptions/handlers/usage/aggregate/app.py +1 -1
- geek_cafe_saas_sdk/domains/subscriptions/handlers/usage/record/app.py +1 -1
- geek_cafe_saas_sdk/domains/subscriptions/services/subscription_manager_service.py +0 -2
- geek_cafe_saas_sdk/domains/tenancy/handlers/subscriptions/activate/app.py +1 -1
- geek_cafe_saas_sdk/domains/tenancy/handlers/subscriptions/active/app.py +1 -1
- geek_cafe_saas_sdk/domains/tenancy/handlers/subscriptions/cancel/app.py +1 -1
- geek_cafe_saas_sdk/domains/tenancy/handlers/subscriptions/get/app.py +1 -1
- geek_cafe_saas_sdk/domains/tenancy/handlers/subscriptions/list/app.py +1 -1
- geek_cafe_saas_sdk/domains/tenancy/handlers/subscriptions/record_payment/app.py +1 -1
- geek_cafe_saas_sdk/domains/tenancy/handlers/tenants/get/app.py +1 -1
- geek_cafe_saas_sdk/domains/tenancy/handlers/tenants/me/app.py +1 -1
- geek_cafe_saas_sdk/domains/tenancy/handlers/tenants/signup/app.py +1 -1
- geek_cafe_saas_sdk/domains/tenancy/handlers/tenants/update/app.py +1 -1
- geek_cafe_saas_sdk/domains/tenancy/services/subscription_service.py +3 -3
- geek_cafe_saas_sdk/domains/tenancy/services/tenant_service.py +3 -3
- geek_cafe_saas_sdk/domains/voting/handlers/votes/delete/app.py +1 -1
- geek_cafe_saas_sdk/domains/voting/handlers/votes/get/app.py +1 -1
- geek_cafe_saas_sdk/domains/voting/handlers/votes/list/app.py +1 -1
- geek_cafe_saas_sdk/domains/voting/handlers/votes/update/app.py +1 -1
- geek_cafe_saas_sdk/domains/voting/services/vote_service.py +1 -5
- geek_cafe_saas_sdk/domains/voting/services/vote_summary_service.py +1 -5
- geek_cafe_saas_sdk/domains/voting/services/vote_tally_service.py +10 -3
- geek_cafe_saas_sdk/lambda_handlers/_base/base_handler.py +40 -6
- geek_cafe_saas_sdk/lambda_handlers/_base/service_pool.py +157 -12
- geek_cafe_saas_sdk/middleware/authorization.py +1 -1
- geek_cafe_saas_sdk/middleware/cors.py +8 -8
- geek_cafe_saas_sdk/services/database_service.py +76 -5
- {geek_cafe_saas_sdk-0.7.4.dist-info → geek_cafe_saas_sdk-0.8.0.dist-info}/METADATA +16 -16
- {geek_cafe_saas_sdk-0.7.4.dist-info → geek_cafe_saas_sdk-0.8.0.dist-info}/RECORD +131 -129
- {geek_cafe_saas_sdk-0.7.4.dist-info → geek_cafe_saas_sdk-0.8.0.dist-info}/WHEEL +0 -0
- {geek_cafe_saas_sdk-0.7.4.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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
113
|
-
|
|
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
|
-
#
|
|
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
|
-
#
|
|
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
|
|
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
|
-
#
|
|
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.
|
|
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, "
|
|
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
|
|
590
|
+
# Check if any non-deleted directories with this path exist
|
|
566
591
|
for directory in query_result.data:
|
|
567
|
-
if directory.
|
|
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
|
-
#
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
377
|
-
|
|
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
|
-
#
|
|
384
|
+
# Map results to FileVersion models
|
|
380
385
|
versions = []
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
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
|
|
581
|
-
file
|
|
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
|
|
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 =
|
|
605
|
+
gsi1_pk = DynamoDBKey.build_key(("file", file_id))
|
|
609
606
|
|
|
610
607
|
results = self.dynamodb.query(
|
|
611
|
-
key=Key(
|
|
608
|
+
key=Key("gsi1_pk").eq(gsi1_pk),
|
|
612
609
|
table_name=self.table_name,
|
|
613
610
|
index_name="gsi1",
|
|
614
|
-
ascending=False #
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
|
@@ -91,5 +91,6 @@ def list_contact_threads(
|
|
|
91
91
|
status=status,
|
|
92
92
|
tenant_id=tenant_id,
|
|
93
93
|
priority=priority,
|
|
94
|
-
limit=limit
|
|
94
|
+
limit=limit,
|
|
95
|
+
user_context=user_context
|
|
95
96
|
)
|
|
@@ -20,7 +20,7 @@ handler_wrapper = create_handler(
|
|
|
20
20
|
)
|
|
21
21
|
|
|
22
22
|
|
|
23
|
-
def
|
|
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
|
|