geek-cafe-saas-sdk 0.6.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 +9 -0
- geek_cafe_saas_sdk/core/__init__.py +11 -0
- geek_cafe_saas_sdk/core/audit_mixin.py +33 -0
- geek_cafe_saas_sdk/core/error_codes.py +132 -0
- geek_cafe_saas_sdk/core/service_errors.py +19 -0
- geek_cafe_saas_sdk/core/service_result.py +121 -0
- geek_cafe_saas_sdk/decorators/__init__.py +64 -0
- geek_cafe_saas_sdk/decorators/auth.py +373 -0
- geek_cafe_saas_sdk/decorators/core.py +358 -0
- geek_cafe_saas_sdk/domains/__init__.py +0 -0
- geek_cafe_saas_sdk/domains/analytics/__init__.py +0 -0
- geek_cafe_saas_sdk/domains/analytics/handlers/__init__.py +0 -0
- geek_cafe_saas_sdk/domains/analytics/models/__init__.py +9 -0
- geek_cafe_saas_sdk/domains/analytics/models/website_analytics.py +219 -0
- geek_cafe_saas_sdk/domains/analytics/models/website_analytics_summary.py +220 -0
- geek_cafe_saas_sdk/domains/analytics/services/__init__.py +11 -0
- geek_cafe_saas_sdk/domains/analytics/services/website_analytics_service.py +232 -0
- geek_cafe_saas_sdk/domains/analytics/services/website_analytics_summary_service.py +212 -0
- geek_cafe_saas_sdk/domains/analytics/services/website_analytics_tally_service.py +610 -0
- geek_cafe_saas_sdk/domains/auth/__init__.py +0 -0
- geek_cafe_saas_sdk/domains/auth/handlers/__init__.py +0 -0
- geek_cafe_saas_sdk/domains/auth/handlers/users/create/app.py +41 -0
- geek_cafe_saas_sdk/domains/auth/handlers/users/delete/app.py +41 -0
- geek_cafe_saas_sdk/domains/auth/handlers/users/get/app.py +39 -0
- geek_cafe_saas_sdk/domains/auth/handlers/users/list/app.py +36 -0
- geek_cafe_saas_sdk/domains/auth/handlers/users/update/app.py +44 -0
- geek_cafe_saas_sdk/domains/auth/models/__init__.py +13 -0
- geek_cafe_saas_sdk/domains/auth/models/permission.py +134 -0
- geek_cafe_saas_sdk/domains/auth/models/resource_permission.py +245 -0
- geek_cafe_saas_sdk/domains/auth/models/role.py +213 -0
- geek_cafe_saas_sdk/domains/auth/models/user.py +285 -0
- geek_cafe_saas_sdk/domains/auth/services/__init__.py +16 -0
- geek_cafe_saas_sdk/domains/auth/services/authorization_service.py +376 -0
- geek_cafe_saas_sdk/domains/auth/services/permission_registry.py +464 -0
- geek_cafe_saas_sdk/domains/auth/services/resource_permission_service.py +408 -0
- geek_cafe_saas_sdk/domains/auth/services/user_service.py +274 -0
- geek_cafe_saas_sdk/domains/communities/__init__.py +0 -0
- geek_cafe_saas_sdk/domains/communities/handlers/__init__.py +0 -0
- geek_cafe_saas_sdk/domains/communities/handlers/communities/create/app.py +41 -0
- geek_cafe_saas_sdk/domains/communities/handlers/communities/delete/app.py +41 -0
- geek_cafe_saas_sdk/domains/communities/handlers/communities/get/app.py +39 -0
- geek_cafe_saas_sdk/domains/communities/handlers/communities/list/app.py +36 -0
- geek_cafe_saas_sdk/domains/communities/handlers/communities/update/app.py +44 -0
- geek_cafe_saas_sdk/domains/communities/models/__init__.py +6 -0
- geek_cafe_saas_sdk/domains/communities/models/community.py +326 -0
- geek_cafe_saas_sdk/domains/communities/models/community_member.py +227 -0
- geek_cafe_saas_sdk/domains/communities/services/__init__.py +6 -0
- geek_cafe_saas_sdk/domains/communities/services/community_member_service.py +412 -0
- geek_cafe_saas_sdk/domains/communities/services/community_service.py +479 -0
- geek_cafe_saas_sdk/domains/events/__init__.py +0 -0
- geek_cafe_saas_sdk/domains/events/handlers/__init__.py +0 -0
- geek_cafe_saas_sdk/domains/events/handlers/attendees/app.py +67 -0
- geek_cafe_saas_sdk/domains/events/handlers/cancel/app.py +66 -0
- geek_cafe_saas_sdk/domains/events/handlers/check_in/app.py +60 -0
- geek_cafe_saas_sdk/domains/events/handlers/create/app.py +93 -0
- geek_cafe_saas_sdk/domains/events/handlers/delete/app.py +42 -0
- geek_cafe_saas_sdk/domains/events/handlers/get/app.py +39 -0
- geek_cafe_saas_sdk/domains/events/handlers/invite/app.py +98 -0
- geek_cafe_saas_sdk/domains/events/handlers/list/app.py +125 -0
- geek_cafe_saas_sdk/domains/events/handlers/publish/app.py +49 -0
- geek_cafe_saas_sdk/domains/events/handlers/rsvp/app.py +83 -0
- geek_cafe_saas_sdk/domains/events/handlers/update/app.py +44 -0
- geek_cafe_saas_sdk/domains/events/models/__init__.py +3 -0
- geek_cafe_saas_sdk/domains/events/models/event.py +681 -0
- geek_cafe_saas_sdk/domains/events/models/event_attendee.py +324 -0
- geek_cafe_saas_sdk/domains/events/services/__init__.py +9 -0
- geek_cafe_saas_sdk/domains/events/services/event_attendee_service.py +571 -0
- geek_cafe_saas_sdk/domains/events/services/event_service.py +684 -0
- geek_cafe_saas_sdk/domains/files/__init__.py +0 -0
- geek_cafe_saas_sdk/domains/files/models/__init__.py +0 -0
- geek_cafe_saas_sdk/domains/files/models/directory.py +258 -0
- geek_cafe_saas_sdk/domains/files/models/file.py +312 -0
- geek_cafe_saas_sdk/domains/files/models/file_share.py +268 -0
- geek_cafe_saas_sdk/domains/files/models/file_version.py +216 -0
- geek_cafe_saas_sdk/domains/files/services/__init__.py +0 -0
- geek_cafe_saas_sdk/domains/files/services/directory_service.py +701 -0
- geek_cafe_saas_sdk/domains/files/services/file_share_service.py +663 -0
- geek_cafe_saas_sdk/domains/files/services/file_system_service.py +575 -0
- geek_cafe_saas_sdk/domains/files/services/file_version_service.py +739 -0
- geek_cafe_saas_sdk/domains/files/services/s3_file_service.py +501 -0
- geek_cafe_saas_sdk/domains/messaging/__init__.py +0 -0
- geek_cafe_saas_sdk/domains/messaging/handlers/__init__.py +0 -0
- geek_cafe_saas_sdk/domains/messaging/handlers/chat_channels/create/app.py +86 -0
- geek_cafe_saas_sdk/domains/messaging/handlers/chat_channels/delete/app.py +65 -0
- geek_cafe_saas_sdk/domains/messaging/handlers/chat_channels/get/app.py +64 -0
- geek_cafe_saas_sdk/domains/messaging/handlers/chat_channels/list/app.py +97 -0
- geek_cafe_saas_sdk/domains/messaging/handlers/chat_channels/update/app.py +149 -0
- geek_cafe_saas_sdk/domains/messaging/handlers/chat_messages/create/app.py +67 -0
- geek_cafe_saas_sdk/domains/messaging/handlers/chat_messages/delete/app.py +65 -0
- geek_cafe_saas_sdk/domains/messaging/handlers/chat_messages/get/app.py +64 -0
- geek_cafe_saas_sdk/domains/messaging/handlers/chat_messages/list/app.py +102 -0
- geek_cafe_saas_sdk/domains/messaging/handlers/chat_messages/update/app.py +127 -0
- geek_cafe_saas_sdk/domains/messaging/handlers/contact_threads/create/app.py +94 -0
- geek_cafe_saas_sdk/domains/messaging/handlers/contact_threads/delete/app.py +66 -0
- geek_cafe_saas_sdk/domains/messaging/handlers/contact_threads/get/app.py +67 -0
- geek_cafe_saas_sdk/domains/messaging/handlers/contact_threads/list/app.py +95 -0
- geek_cafe_saas_sdk/domains/messaging/handlers/contact_threads/update/app.py +156 -0
- geek_cafe_saas_sdk/domains/messaging/models/__init__.py +13 -0
- geek_cafe_saas_sdk/domains/messaging/models/chat_channel.py +337 -0
- geek_cafe_saas_sdk/domains/messaging/models/chat_channel_member.py +180 -0
- geek_cafe_saas_sdk/domains/messaging/models/chat_message.py +426 -0
- geek_cafe_saas_sdk/domains/messaging/models/contact_thread.py +392 -0
- geek_cafe_saas_sdk/domains/messaging/services/__init__.py +11 -0
- geek_cafe_saas_sdk/domains/messaging/services/chat_channel_service.py +700 -0
- geek_cafe_saas_sdk/domains/messaging/services/chat_message_service.py +491 -0
- geek_cafe_saas_sdk/domains/messaging/services/contact_thread_service.py +497 -0
- geek_cafe_saas_sdk/domains/tenancy/__init__.py +0 -0
- geek_cafe_saas_sdk/domains/tenancy/handlers/__init__.py +0 -0
- geek_cafe_saas_sdk/domains/tenancy/handlers/subscriptions/activate/app.py +52 -0
- geek_cafe_saas_sdk/domains/tenancy/handlers/subscriptions/active/app.py +37 -0
- geek_cafe_saas_sdk/domains/tenancy/handlers/subscriptions/cancel/app.py +55 -0
- geek_cafe_saas_sdk/domains/tenancy/handlers/subscriptions/get/app.py +39 -0
- geek_cafe_saas_sdk/domains/tenancy/handlers/subscriptions/list/app.py +44 -0
- geek_cafe_saas_sdk/domains/tenancy/handlers/subscriptions/record_payment/app.py +56 -0
- geek_cafe_saas_sdk/domains/tenancy/handlers/tenants/get/app.py +39 -0
- geek_cafe_saas_sdk/domains/tenancy/handlers/tenants/me/app.py +37 -0
- geek_cafe_saas_sdk/domains/tenancy/handlers/tenants/signup/app.py +61 -0
- geek_cafe_saas_sdk/domains/tenancy/handlers/tenants/update/app.py +44 -0
- geek_cafe_saas_sdk/domains/tenancy/models/__init__.py +6 -0
- geek_cafe_saas_sdk/domains/tenancy/models/subscription.py +440 -0
- geek_cafe_saas_sdk/domains/tenancy/models/tenant.py +258 -0
- geek_cafe_saas_sdk/domains/tenancy/services/__init__.py +6 -0
- geek_cafe_saas_sdk/domains/tenancy/services/subscription_service.py +557 -0
- geek_cafe_saas_sdk/domains/tenancy/services/tenant_service.py +575 -0
- geek_cafe_saas_sdk/domains/voting/__init__.py +0 -0
- geek_cafe_saas_sdk/domains/voting/handlers/__init__.py +0 -0
- geek_cafe_saas_sdk/domains/voting/handlers/votes/create/app.py +128 -0
- geek_cafe_saas_sdk/domains/voting/handlers/votes/delete/app.py +41 -0
- geek_cafe_saas_sdk/domains/voting/handlers/votes/get/app.py +39 -0
- geek_cafe_saas_sdk/domains/voting/handlers/votes/list/app.py +38 -0
- geek_cafe_saas_sdk/domains/voting/handlers/votes/summerize/README.md +3 -0
- geek_cafe_saas_sdk/domains/voting/handlers/votes/update/app.py +44 -0
- geek_cafe_saas_sdk/domains/voting/models/__init__.py +9 -0
- geek_cafe_saas_sdk/domains/voting/models/vote.py +231 -0
- geek_cafe_saas_sdk/domains/voting/models/vote_summary.py +193 -0
- geek_cafe_saas_sdk/domains/voting/services/__init__.py +11 -0
- geek_cafe_saas_sdk/domains/voting/services/vote_service.py +264 -0
- geek_cafe_saas_sdk/domains/voting/services/vote_summary_service.py +198 -0
- geek_cafe_saas_sdk/domains/voting/services/vote_tally_service.py +533 -0
- geek_cafe_saas_sdk/lambda_handlers/README.md +404 -0
- geek_cafe_saas_sdk/lambda_handlers/__init__.py +67 -0
- geek_cafe_saas_sdk/lambda_handlers/_base/__init__.py +25 -0
- geek_cafe_saas_sdk/lambda_handlers/_base/api_key_handler.py +129 -0
- geek_cafe_saas_sdk/lambda_handlers/_base/authorized_secure_handler.py +218 -0
- geek_cafe_saas_sdk/lambda_handlers/_base/base_handler.py +185 -0
- geek_cafe_saas_sdk/lambda_handlers/_base/handler_factory.py +256 -0
- geek_cafe_saas_sdk/lambda_handlers/_base/public_handler.py +53 -0
- geek_cafe_saas_sdk/lambda_handlers/_base/secure_handler.py +89 -0
- geek_cafe_saas_sdk/lambda_handlers/_base/service_pool.py +94 -0
- geek_cafe_saas_sdk/lambda_handlers/directories/create/app.py +79 -0
- geek_cafe_saas_sdk/lambda_handlers/directories/delete/app.py +76 -0
- geek_cafe_saas_sdk/lambda_handlers/directories/get/app.py +74 -0
- geek_cafe_saas_sdk/lambda_handlers/directories/list/app.py +75 -0
- geek_cafe_saas_sdk/lambda_handlers/directories/move/app.py +79 -0
- geek_cafe_saas_sdk/lambda_handlers/files/delete/app.py +121 -0
- geek_cafe_saas_sdk/lambda_handlers/files/download/app.py +187 -0
- geek_cafe_saas_sdk/lambda_handlers/files/get/app.py +127 -0
- geek_cafe_saas_sdk/lambda_handlers/files/list/app.py +108 -0
- geek_cafe_saas_sdk/lambda_handlers/files/share/app.py +83 -0
- geek_cafe_saas_sdk/lambda_handlers/files/shares/list/app.py +84 -0
- geek_cafe_saas_sdk/lambda_handlers/files/shares/revoke/app.py +76 -0
- geek_cafe_saas_sdk/lambda_handlers/files/update/app.py +143 -0
- geek_cafe_saas_sdk/lambda_handlers/files/upload/app.py +151 -0
- geek_cafe_saas_sdk/middleware/__init__.py +36 -0
- geek_cafe_saas_sdk/middleware/auth.py +85 -0
- geek_cafe_saas_sdk/middleware/authorization.py +523 -0
- geek_cafe_saas_sdk/middleware/cors.py +63 -0
- geek_cafe_saas_sdk/middleware/error_handling.py +114 -0
- geek_cafe_saas_sdk/middleware/validation.py +80 -0
- geek_cafe_saas_sdk/models/__init__.py +20 -0
- geek_cafe_saas_sdk/models/base_model.py +233 -0
- geek_cafe_saas_sdk/services/__init__.py +18 -0
- geek_cafe_saas_sdk/services/database_service.py +441 -0
- geek_cafe_saas_sdk/utilities/__init__.py +88 -0
- geek_cafe_saas_sdk/utilities/cognito_utility.py +568 -0
- geek_cafe_saas_sdk/utilities/custom_exceptions.py +183 -0
- geek_cafe_saas_sdk/utilities/datetime_utility.py +410 -0
- geek_cafe_saas_sdk/utilities/dictionary_utility.py +78 -0
- geek_cafe_saas_sdk/utilities/dynamodb_utils.py +151 -0
- geek_cafe_saas_sdk/utilities/environment_loader.py +149 -0
- geek_cafe_saas_sdk/utilities/environment_variables.py +228 -0
- geek_cafe_saas_sdk/utilities/http_body_parameters.py +44 -0
- geek_cafe_saas_sdk/utilities/http_path_parameters.py +60 -0
- geek_cafe_saas_sdk/utilities/http_status_code.py +63 -0
- geek_cafe_saas_sdk/utilities/jwt_utility.py +234 -0
- geek_cafe_saas_sdk/utilities/lambda_event_utility.py +776 -0
- geek_cafe_saas_sdk/utilities/logging_utility.py +64 -0
- geek_cafe_saas_sdk/utilities/message_query_helper.py +340 -0
- geek_cafe_saas_sdk/utilities/response.py +209 -0
- geek_cafe_saas_sdk/utilities/string_functions.py +180 -0
- geek_cafe_saas_sdk-0.6.0.dist-info/METADATA +397 -0
- geek_cafe_saas_sdk-0.6.0.dist-info/RECORD +194 -0
- geek_cafe_saas_sdk-0.6.0.dist-info/WHEEL +4 -0
- geek_cafe_saas_sdk-0.6.0.dist-info/licenses/LICENSE +47 -0
|
@@ -0,0 +1,701 @@
|
|
|
1
|
+
"""
|
|
2
|
+
DirectoryService for virtual directory hierarchy management.
|
|
3
|
+
|
|
4
|
+
Geek Cafe, LLC
|
|
5
|
+
MIT License. See Project Root for the license information.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from typing import Dict, Any, Optional, List
|
|
9
|
+
from boto3.dynamodb.conditions import Key
|
|
10
|
+
from boto3_assist.dynamodb.dynamodb import DynamoDB
|
|
11
|
+
from geek_cafe_saas_sdk.services.database_service import DatabaseService
|
|
12
|
+
from geek_cafe_saas_sdk.core.service_result import ServiceResult
|
|
13
|
+
from geek_cafe_saas_sdk.core.service_errors import ValidationError, NotFoundError, AccessDeniedError
|
|
14
|
+
from geek_cafe_saas_sdk.core.error_codes import ErrorCode
|
|
15
|
+
from geek_cafe_saas_sdk.domains.files.models.directory import Directory
|
|
16
|
+
import datetime as dt
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class DirectoryService(DatabaseService[Directory]):
|
|
20
|
+
"""
|
|
21
|
+
Directory service for managing virtual directory hierarchy.
|
|
22
|
+
|
|
23
|
+
Handles:
|
|
24
|
+
- Directory creation and deletion
|
|
25
|
+
- Hierarchy traversal
|
|
26
|
+
- Path resolution
|
|
27
|
+
- Directory statistics (file count, size)
|
|
28
|
+
- Move/rename operations
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
def create(
|
|
32
|
+
self,
|
|
33
|
+
tenant_id: str,
|
|
34
|
+
user_id: str,
|
|
35
|
+
directory_name: str,
|
|
36
|
+
parent_id: Optional[str] = None,
|
|
37
|
+
description: Optional[str] = None,
|
|
38
|
+
color: Optional[str] = None,
|
|
39
|
+
icon: Optional[str] = None,
|
|
40
|
+
**kwargs
|
|
41
|
+
) -> ServiceResult[Directory]:
|
|
42
|
+
"""
|
|
43
|
+
Create a new directory.
|
|
44
|
+
|
|
45
|
+
Args:
|
|
46
|
+
tenant_id: Tenant ID
|
|
47
|
+
user_id: User ID (directory owner)
|
|
48
|
+
directory_name: Directory name
|
|
49
|
+
parent_id: Optional parent directory ID
|
|
50
|
+
description: Optional description
|
|
51
|
+
color: Optional color code
|
|
52
|
+
icon: Optional icon name
|
|
53
|
+
|
|
54
|
+
Returns:
|
|
55
|
+
ServiceResult with Directory model
|
|
56
|
+
"""
|
|
57
|
+
try:
|
|
58
|
+
# Validate inputs
|
|
59
|
+
if not directory_name or not directory_name.strip():
|
|
60
|
+
raise ValidationError("Directory name is required", "directory_name")
|
|
61
|
+
|
|
62
|
+
# Validate directory name (no special chars)
|
|
63
|
+
if '/' in directory_name or '\\' in directory_name:
|
|
64
|
+
raise ValidationError(
|
|
65
|
+
"Directory name cannot contain slashes",
|
|
66
|
+
"directory_name"
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
# If parent specified, verify it exists
|
|
70
|
+
parent_dir = None
|
|
71
|
+
if parent_id:
|
|
72
|
+
parent_result = self.get_by_id(parent_id, tenant_id, user_id)
|
|
73
|
+
if not parent_result.success:
|
|
74
|
+
return ServiceResult.error_result(
|
|
75
|
+
message=f"Parent directory not found: {parent_id}",
|
|
76
|
+
error_code=ErrorCode.NOT_FOUND
|
|
77
|
+
)
|
|
78
|
+
parent_dir = parent_result.data
|
|
79
|
+
|
|
80
|
+
# Check for duplicate name in same parent
|
|
81
|
+
duplicate_check = self._check_duplicate_name(
|
|
82
|
+
tenant_id, directory_name, parent_id
|
|
83
|
+
)
|
|
84
|
+
if duplicate_check:
|
|
85
|
+
raise ValidationError(
|
|
86
|
+
f"Directory '{directory_name}' already exists in this location",
|
|
87
|
+
"directory_name"
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
# Create Directory model
|
|
91
|
+
directory = Directory()
|
|
92
|
+
directory.prep_for_save()
|
|
93
|
+
directory.tenant_id = tenant_id
|
|
94
|
+
directory.owner_id = user_id
|
|
95
|
+
directory.directory_name = directory_name
|
|
96
|
+
directory.parent_id = parent_id
|
|
97
|
+
directory.description = description
|
|
98
|
+
directory.color = color
|
|
99
|
+
directory.icon = icon
|
|
100
|
+
directory.status = "active"
|
|
101
|
+
|
|
102
|
+
# Calculate depth and full path
|
|
103
|
+
if parent_dir:
|
|
104
|
+
directory.depth = parent_dir.depth + 1
|
|
105
|
+
directory.full_path = f"{parent_dir.full_path}/{directory_name}"
|
|
106
|
+
else:
|
|
107
|
+
directory.depth = 0
|
|
108
|
+
directory.full_path = f"/{directory_name}"
|
|
109
|
+
|
|
110
|
+
# Initialize counters
|
|
111
|
+
directory.file_count = 0
|
|
112
|
+
directory.subdirectory_count = 0
|
|
113
|
+
directory.total_size = 0
|
|
114
|
+
|
|
115
|
+
# Save to DynamoDB
|
|
116
|
+
pk = f"DIR#{tenant_id}#{directory.directory_id}"
|
|
117
|
+
sk = "METADATA"
|
|
118
|
+
|
|
119
|
+
item = directory.to_dictionary()
|
|
120
|
+
item["pk"] = pk
|
|
121
|
+
item["sk"] = sk
|
|
122
|
+
|
|
123
|
+
# GSI1: Directories by parent
|
|
124
|
+
item["gsi1_pk"] = f"TENANT#{tenant_id}"
|
|
125
|
+
if parent_id:
|
|
126
|
+
item["gsi1_sk"] = f"PARENT#{parent_id}#{directory_name}"
|
|
127
|
+
else:
|
|
128
|
+
item["gsi1_sk"] = f"PARENT#ROOT#{directory_name}"
|
|
129
|
+
|
|
130
|
+
# GSI2: Directories by owner
|
|
131
|
+
item["gsi2_pk"] = f"TENANT#{tenant_id}#USER#{user_id}"
|
|
132
|
+
item["gsi2_sk"] = f"DIR#{directory.created_utc_ts}"
|
|
133
|
+
|
|
134
|
+
self.dynamodb.save(
|
|
135
|
+
item=item,
|
|
136
|
+
table_name=self.table_name
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
# Update parent's subdirectory count
|
|
140
|
+
if parent_id:
|
|
141
|
+
self._increment_subdirectory_count(tenant_id, parent_id, 1)
|
|
142
|
+
|
|
143
|
+
return ServiceResult.success_result(directory)
|
|
144
|
+
|
|
145
|
+
except ValidationError as e:
|
|
146
|
+
return ServiceResult.error_result(
|
|
147
|
+
message=str(e),
|
|
148
|
+
error_code=ErrorCode.VALIDATION_ERROR
|
|
149
|
+
)
|
|
150
|
+
except Exception as e:
|
|
151
|
+
return ServiceResult.exception_result(
|
|
152
|
+
exception=e,
|
|
153
|
+
error_code=ErrorCode.INTERNAL_ERROR,
|
|
154
|
+
context="DirectoryService.create"
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
def get_by_id(
|
|
158
|
+
self,
|
|
159
|
+
resource_id: str,
|
|
160
|
+
tenant_id: str,
|
|
161
|
+
user_id: str
|
|
162
|
+
) -> ServiceResult[Directory]:
|
|
163
|
+
"""
|
|
164
|
+
Get directory by ID with access control.
|
|
165
|
+
|
|
166
|
+
Args:
|
|
167
|
+
resource_id: Directory ID
|
|
168
|
+
tenant_id: Tenant ID
|
|
169
|
+
user_id: User ID (for access control)
|
|
170
|
+
|
|
171
|
+
Returns:
|
|
172
|
+
ServiceResult with Directory model
|
|
173
|
+
"""
|
|
174
|
+
try:
|
|
175
|
+
pk = f"DIR#{tenant_id}#{resource_id}"
|
|
176
|
+
sk = "METADATA"
|
|
177
|
+
|
|
178
|
+
result = self.dynamodb.get(
|
|
179
|
+
table_name=self.table_name,
|
|
180
|
+
key={"pk": pk, "sk": sk}
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
# Check if directory exists
|
|
184
|
+
if not result or 'Item' not in result:
|
|
185
|
+
raise NotFoundError(f"Directory not found: {resource_id}")
|
|
186
|
+
|
|
187
|
+
# Convert to Directory model
|
|
188
|
+
directory = Directory()
|
|
189
|
+
directory.map(result['Item'])
|
|
190
|
+
|
|
191
|
+
# Access control: Check if user is owner
|
|
192
|
+
if directory.owner_id != user_id:
|
|
193
|
+
# TODO: Check shared access
|
|
194
|
+
raise AccessDeniedError("You do not have access to this directory")
|
|
195
|
+
|
|
196
|
+
return ServiceResult.success_result(directory)
|
|
197
|
+
|
|
198
|
+
except NotFoundError as e:
|
|
199
|
+
return ServiceResult.error_result(
|
|
200
|
+
message=str(e),
|
|
201
|
+
error_code=ErrorCode.NOT_FOUND
|
|
202
|
+
)
|
|
203
|
+
except AccessDeniedError as e:
|
|
204
|
+
return ServiceResult.error_result(
|
|
205
|
+
message=str(e),
|
|
206
|
+
error_code=ErrorCode.ACCESS_DENIED
|
|
207
|
+
)
|
|
208
|
+
except Exception as e:
|
|
209
|
+
return ServiceResult.exception_result(
|
|
210
|
+
exception=e,
|
|
211
|
+
error_code=ErrorCode.INTERNAL_ERROR,
|
|
212
|
+
context="DirectoryService.get_by_id"
|
|
213
|
+
)
|
|
214
|
+
|
|
215
|
+
def update(
|
|
216
|
+
self,
|
|
217
|
+
resource_id: str,
|
|
218
|
+
tenant_id: str,
|
|
219
|
+
user_id: str,
|
|
220
|
+
updates: Dict[str, Any]
|
|
221
|
+
) -> ServiceResult[Directory]:
|
|
222
|
+
"""
|
|
223
|
+
Update directory metadata.
|
|
224
|
+
|
|
225
|
+
Args:
|
|
226
|
+
resource_id: Directory ID
|
|
227
|
+
tenant_id: Tenant ID
|
|
228
|
+
user_id: User ID (for access control)
|
|
229
|
+
updates: Dictionary of fields to update
|
|
230
|
+
|
|
231
|
+
Returns:
|
|
232
|
+
ServiceResult with updated Directory model
|
|
233
|
+
"""
|
|
234
|
+
try:
|
|
235
|
+
# Get existing directory
|
|
236
|
+
get_result = self.get_by_id(resource_id, tenant_id, user_id)
|
|
237
|
+
if not get_result.success:
|
|
238
|
+
return get_result
|
|
239
|
+
|
|
240
|
+
directory = get_result.data
|
|
241
|
+
|
|
242
|
+
# Only owner can update
|
|
243
|
+
if directory.owner_id != user_id:
|
|
244
|
+
raise AccessDeniedError("Only the owner can update this directory")
|
|
245
|
+
|
|
246
|
+
# Apply updates (only allowed fields)
|
|
247
|
+
allowed_fields = [
|
|
248
|
+
"directory_name", "description", "color", "icon", "status"
|
|
249
|
+
]
|
|
250
|
+
|
|
251
|
+
# Handle directory rename
|
|
252
|
+
if "directory_name" in updates:
|
|
253
|
+
new_name = updates["directory_name"]
|
|
254
|
+
if not new_name or not new_name.strip():
|
|
255
|
+
raise ValidationError("Directory name cannot be empty", "directory_name")
|
|
256
|
+
|
|
257
|
+
if '/' in new_name or '\\' in new_name:
|
|
258
|
+
raise ValidationError(
|
|
259
|
+
"Directory name cannot contain slashes",
|
|
260
|
+
"directory_name"
|
|
261
|
+
)
|
|
262
|
+
|
|
263
|
+
# Check for duplicate
|
|
264
|
+
if new_name != directory.directory_name:
|
|
265
|
+
duplicate = self._check_duplicate_name(
|
|
266
|
+
tenant_id, new_name, directory.parent_id
|
|
267
|
+
)
|
|
268
|
+
if duplicate:
|
|
269
|
+
raise ValidationError(
|
|
270
|
+
f"Directory '{new_name}' already exists in this location",
|
|
271
|
+
"directory_name"
|
|
272
|
+
)
|
|
273
|
+
|
|
274
|
+
# Update full path
|
|
275
|
+
old_path = directory.full_path
|
|
276
|
+
if directory.parent_id:
|
|
277
|
+
# Get parent path
|
|
278
|
+
parent_result = self.get_by_id(directory.parent_id, tenant_id, user_id)
|
|
279
|
+
if parent_result.success:
|
|
280
|
+
directory.full_path = f"{parent_result.data.full_path}/{new_name}"
|
|
281
|
+
else:
|
|
282
|
+
directory.full_path = f"/{new_name}"
|
|
283
|
+
|
|
284
|
+
directory.directory_name = new_name
|
|
285
|
+
|
|
286
|
+
# Apply other updates
|
|
287
|
+
for field, value in updates.items():
|
|
288
|
+
if field in allowed_fields and field != "directory_name":
|
|
289
|
+
setattr(directory, field, value)
|
|
290
|
+
|
|
291
|
+
# Update timestamp
|
|
292
|
+
directory.updated_utc_ts = dt.datetime.now(dt.UTC).timestamp()
|
|
293
|
+
|
|
294
|
+
# Save to DynamoDB
|
|
295
|
+
pk = f"DIR#{tenant_id}#{resource_id}"
|
|
296
|
+
sk = "METADATA"
|
|
297
|
+
|
|
298
|
+
item = directory.to_dictionary()
|
|
299
|
+
item["pk"] = pk
|
|
300
|
+
item["sk"] = sk
|
|
301
|
+
|
|
302
|
+
# Update GSI keys if name changed
|
|
303
|
+
if "directory_name" in updates:
|
|
304
|
+
item["gsi1_pk"] = f"TENANT#{tenant_id}"
|
|
305
|
+
if directory.parent_id:
|
|
306
|
+
item["gsi1_sk"] = f"PARENT#{directory.parent_id}#{directory.directory_name}"
|
|
307
|
+
else:
|
|
308
|
+
item["gsi1_sk"] = f"PARENT#ROOT#{directory.directory_name}"
|
|
309
|
+
|
|
310
|
+
self.dynamodb.save(
|
|
311
|
+
item=item,
|
|
312
|
+
table_name=self.table_name
|
|
313
|
+
)
|
|
314
|
+
|
|
315
|
+
return ServiceResult.success_result(directory)
|
|
316
|
+
|
|
317
|
+
except (ValidationError, AccessDeniedError) as e:
|
|
318
|
+
return ServiceResult.error_result(
|
|
319
|
+
message=str(e),
|
|
320
|
+
error_code=ErrorCode.VALIDATION_ERROR if isinstance(e, ValidationError) else ErrorCode.ACCESS_DENIED
|
|
321
|
+
)
|
|
322
|
+
except Exception as e:
|
|
323
|
+
return ServiceResult.exception_result(
|
|
324
|
+
exception=e,
|
|
325
|
+
error_code=ErrorCode.INTERNAL_ERROR,
|
|
326
|
+
context="DirectoryService.update"
|
|
327
|
+
)
|
|
328
|
+
|
|
329
|
+
def delete(
|
|
330
|
+
self,
|
|
331
|
+
resource_id: str,
|
|
332
|
+
tenant_id: str,
|
|
333
|
+
user_id: str,
|
|
334
|
+
recursive: bool = False
|
|
335
|
+
) -> ServiceResult[bool]:
|
|
336
|
+
"""
|
|
337
|
+
Delete directory.
|
|
338
|
+
|
|
339
|
+
Args:
|
|
340
|
+
resource_id: Directory ID
|
|
341
|
+
tenant_id: Tenant ID
|
|
342
|
+
user_id: User ID (for access control)
|
|
343
|
+
recursive: If True, delete subdirectories and files. If False, fail if not empty.
|
|
344
|
+
|
|
345
|
+
Returns:
|
|
346
|
+
ServiceResult with success boolean
|
|
347
|
+
"""
|
|
348
|
+
try:
|
|
349
|
+
# Get existing directory
|
|
350
|
+
get_result = self.get_by_id(resource_id, tenant_id, user_id)
|
|
351
|
+
if not get_result.success:
|
|
352
|
+
return get_result
|
|
353
|
+
|
|
354
|
+
directory = get_result.data
|
|
355
|
+
|
|
356
|
+
# Only owner can delete
|
|
357
|
+
if directory.owner_id != user_id:
|
|
358
|
+
raise AccessDeniedError("Only the owner can delete this directory")
|
|
359
|
+
|
|
360
|
+
# Check if directory is empty
|
|
361
|
+
if not recursive and (directory.file_count > 0 or directory.subdirectory_count > 0):
|
|
362
|
+
raise ValidationError(
|
|
363
|
+
"Directory is not empty. Use recursive=True to delete contents.",
|
|
364
|
+
"recursive"
|
|
365
|
+
)
|
|
366
|
+
|
|
367
|
+
# TODO: If recursive, delete all subdirectories and files
|
|
368
|
+
# For now, just soft delete the directory
|
|
369
|
+
directory.status = "deleted"
|
|
370
|
+
directory.deleted_utc_ts = dt.datetime.now(dt.UTC).timestamp()
|
|
371
|
+
|
|
372
|
+
pk = f"DIR#{tenant_id}#{resource_id}"
|
|
373
|
+
sk = "METADATA"
|
|
374
|
+
|
|
375
|
+
item = directory.to_dictionary()
|
|
376
|
+
item["pk"] = pk
|
|
377
|
+
item["sk"] = sk
|
|
378
|
+
|
|
379
|
+
self.dynamodb.save(
|
|
380
|
+
item=item,
|
|
381
|
+
table_name=self.table_name
|
|
382
|
+
)
|
|
383
|
+
|
|
384
|
+
# Update parent's subdirectory count
|
|
385
|
+
if directory.parent_id:
|
|
386
|
+
self._increment_subdirectory_count(tenant_id, directory.parent_id, -1)
|
|
387
|
+
|
|
388
|
+
return ServiceResult.success_result(True)
|
|
389
|
+
|
|
390
|
+
except (ValidationError, AccessDeniedError) as e:
|
|
391
|
+
return ServiceResult.error_result(
|
|
392
|
+
message=str(e),
|
|
393
|
+
error_code=ErrorCode.VALIDATION_ERROR if isinstance(e, ValidationError) else ErrorCode.ACCESS_DENIED
|
|
394
|
+
)
|
|
395
|
+
except Exception as e:
|
|
396
|
+
return ServiceResult.exception_result(
|
|
397
|
+
exception=e,
|
|
398
|
+
error_code=ErrorCode.INTERNAL_ERROR,
|
|
399
|
+
context="DirectoryService.delete"
|
|
400
|
+
)
|
|
401
|
+
|
|
402
|
+
def list_subdirectories(
|
|
403
|
+
self,
|
|
404
|
+
tenant_id: str,
|
|
405
|
+
parent_id: Optional[str],
|
|
406
|
+
user_id: str,
|
|
407
|
+
limit: int = 50
|
|
408
|
+
) -> ServiceResult[List[Directory]]:
|
|
409
|
+
"""
|
|
410
|
+
List subdirectories in a directory.
|
|
411
|
+
|
|
412
|
+
Args:
|
|
413
|
+
tenant_id: Tenant ID
|
|
414
|
+
parent_id: Parent directory ID (None for root)
|
|
415
|
+
user_id: User ID (for access control)
|
|
416
|
+
limit: Maximum number of results
|
|
417
|
+
|
|
418
|
+
Returns:
|
|
419
|
+
ServiceResult with list of Directory models
|
|
420
|
+
"""
|
|
421
|
+
try:
|
|
422
|
+
gsi1_pk = f"TENANT#{tenant_id}"
|
|
423
|
+
|
|
424
|
+
if parent_id:
|
|
425
|
+
gsi1_sk_prefix = f"PARENT#{parent_id}#"
|
|
426
|
+
else:
|
|
427
|
+
gsi1_sk_prefix = "PARENT#ROOT#"
|
|
428
|
+
|
|
429
|
+
# Query GSI1
|
|
430
|
+
results = self.dynamodb.query(
|
|
431
|
+
key=Key('gsi1_pk').eq(gsi1_pk) & Key('gsi1_sk').begins_with(gsi1_sk_prefix),
|
|
432
|
+
table_name=self.table_name,
|
|
433
|
+
index_name="gsi1",
|
|
434
|
+
limit=limit
|
|
435
|
+
)
|
|
436
|
+
|
|
437
|
+
directories = []
|
|
438
|
+
for item in results.get('Items', []):
|
|
439
|
+
directory = Directory()
|
|
440
|
+
directory.map(item)
|
|
441
|
+
|
|
442
|
+
# Filter out deleted directories
|
|
443
|
+
if directory.status != "deleted":
|
|
444
|
+
# Basic access control: show only owned directories
|
|
445
|
+
if directory.owner_id == user_id:
|
|
446
|
+
directories.append(directory)
|
|
447
|
+
|
|
448
|
+
return ServiceResult.success_result(directories)
|
|
449
|
+
|
|
450
|
+
except Exception as e:
|
|
451
|
+
return ServiceResult.exception_result(
|
|
452
|
+
exception=e,
|
|
453
|
+
error_code=ErrorCode.INTERNAL_ERROR,
|
|
454
|
+
context="DirectoryService.list_subdirectories"
|
|
455
|
+
)
|
|
456
|
+
|
|
457
|
+
def get_path_components(
|
|
458
|
+
self,
|
|
459
|
+
directory_id: str,
|
|
460
|
+
tenant_id: str,
|
|
461
|
+
user_id: str
|
|
462
|
+
) -> ServiceResult[List[Directory]]:
|
|
463
|
+
"""
|
|
464
|
+
Get all directories in the path from root to target directory.
|
|
465
|
+
|
|
466
|
+
Args:
|
|
467
|
+
directory_id: Target directory ID
|
|
468
|
+
tenant_id: Tenant ID
|
|
469
|
+
user_id: User ID
|
|
470
|
+
|
|
471
|
+
Returns:
|
|
472
|
+
ServiceResult with list of Directory models (root to target)
|
|
473
|
+
"""
|
|
474
|
+
try:
|
|
475
|
+
path = []
|
|
476
|
+
current_id = directory_id
|
|
477
|
+
|
|
478
|
+
# Traverse up to root
|
|
479
|
+
while current_id:
|
|
480
|
+
result = self.get_by_id(current_id, tenant_id, user_id)
|
|
481
|
+
if not result.success:
|
|
482
|
+
return result
|
|
483
|
+
|
|
484
|
+
directory = result.data
|
|
485
|
+
path.insert(0, directory) # Prepend to build root-to-target order
|
|
486
|
+
current_id = directory.parent_id
|
|
487
|
+
|
|
488
|
+
return ServiceResult.success_result(path)
|
|
489
|
+
|
|
490
|
+
except Exception as e:
|
|
491
|
+
return ServiceResult.exception_result(
|
|
492
|
+
exception=e,
|
|
493
|
+
error_code=ErrorCode.INTERNAL_ERROR,
|
|
494
|
+
context="DirectoryService.get_path_components"
|
|
495
|
+
)
|
|
496
|
+
|
|
497
|
+
def move_directory(
|
|
498
|
+
self,
|
|
499
|
+
directory_id: str,
|
|
500
|
+
new_parent_id: Optional[str],
|
|
501
|
+
tenant_id: str,
|
|
502
|
+
user_id: str
|
|
503
|
+
) -> ServiceResult[Directory]:
|
|
504
|
+
"""
|
|
505
|
+
Move directory to a new parent.
|
|
506
|
+
|
|
507
|
+
Args:
|
|
508
|
+
directory_id: Directory to move
|
|
509
|
+
new_parent_id: New parent directory ID (None for root)
|
|
510
|
+
tenant_id: Tenant ID
|
|
511
|
+
user_id: User ID
|
|
512
|
+
|
|
513
|
+
Returns:
|
|
514
|
+
ServiceResult with updated Directory model
|
|
515
|
+
"""
|
|
516
|
+
try:
|
|
517
|
+
# Get directory to move
|
|
518
|
+
get_result = self.get_by_id(directory_id, tenant_id, user_id)
|
|
519
|
+
if not get_result.success:
|
|
520
|
+
return get_result
|
|
521
|
+
|
|
522
|
+
directory = get_result.data
|
|
523
|
+
|
|
524
|
+
# Only owner can move
|
|
525
|
+
if directory.owner_id != user_id:
|
|
526
|
+
raise AccessDeniedError("Only the owner can move this directory")
|
|
527
|
+
|
|
528
|
+
# Can't move to itself
|
|
529
|
+
if directory_id == new_parent_id:
|
|
530
|
+
raise ValidationError("Cannot move directory to itself", "new_parent_id")
|
|
531
|
+
|
|
532
|
+
# Verify new parent exists and is not a descendant
|
|
533
|
+
if new_parent_id:
|
|
534
|
+
parent_result = self.get_by_id(new_parent_id, tenant_id, user_id)
|
|
535
|
+
if not parent_result.success:
|
|
536
|
+
return ServiceResult.error_result(
|
|
537
|
+
message=f"Target parent directory not found: {new_parent_id}",
|
|
538
|
+
error_code=ErrorCode.NOT_FOUND
|
|
539
|
+
)
|
|
540
|
+
|
|
541
|
+
parent = parent_result.data
|
|
542
|
+
|
|
543
|
+
# Check if new parent is a descendant (would create cycle)
|
|
544
|
+
if self._is_descendant(tenant_id, user_id, new_parent_id, directory_id):
|
|
545
|
+
raise ValidationError(
|
|
546
|
+
"Cannot move directory into its own subdirectory",
|
|
547
|
+
"new_parent_id"
|
|
548
|
+
)
|
|
549
|
+
|
|
550
|
+
# Check for duplicate name
|
|
551
|
+
duplicate = self._check_duplicate_name(
|
|
552
|
+
tenant_id, directory.directory_name, new_parent_id
|
|
553
|
+
)
|
|
554
|
+
if duplicate:
|
|
555
|
+
raise ValidationError(
|
|
556
|
+
f"Directory '{directory.directory_name}' already exists in target location",
|
|
557
|
+
"directory_name"
|
|
558
|
+
)
|
|
559
|
+
|
|
560
|
+
# Update depth and path
|
|
561
|
+
directory.depth = parent.depth + 1
|
|
562
|
+
directory.full_path = f"{parent.full_path}/{directory.directory_name}"
|
|
563
|
+
else:
|
|
564
|
+
# Moving to root
|
|
565
|
+
directory.depth = 0
|
|
566
|
+
directory.full_path = f"/{directory.directory_name}"
|
|
567
|
+
|
|
568
|
+
old_parent_id = directory.parent_id
|
|
569
|
+
directory.parent_id = new_parent_id
|
|
570
|
+
directory.updated_utc_ts = dt.datetime.now(dt.UTC).timestamp()
|
|
571
|
+
|
|
572
|
+
# Save to DynamoDB
|
|
573
|
+
pk = f"DIR#{tenant_id}#{directory_id}"
|
|
574
|
+
sk = "METADATA"
|
|
575
|
+
|
|
576
|
+
item = directory.to_dictionary()
|
|
577
|
+
item["pk"] = pk
|
|
578
|
+
item["sk"] = sk
|
|
579
|
+
|
|
580
|
+
# Update GSI1 for new parent
|
|
581
|
+
item["gsi1_pk"] = f"TENANT#{tenant_id}"
|
|
582
|
+
if new_parent_id:
|
|
583
|
+
item["gsi1_sk"] = f"PARENT#{new_parent_id}#{directory.directory_name}"
|
|
584
|
+
else:
|
|
585
|
+
item["gsi1_sk"] = f"PARENT#ROOT#{directory.directory_name}"
|
|
586
|
+
|
|
587
|
+
self.dynamodb.save(
|
|
588
|
+
item=item,
|
|
589
|
+
table_name=self.table_name
|
|
590
|
+
)
|
|
591
|
+
|
|
592
|
+
# Update parent counts
|
|
593
|
+
if old_parent_id:
|
|
594
|
+
self._increment_subdirectory_count(tenant_id, old_parent_id, -1)
|
|
595
|
+
if new_parent_id:
|
|
596
|
+
self._increment_subdirectory_count(tenant_id, new_parent_id, 1)
|
|
597
|
+
|
|
598
|
+
return ServiceResult.success_result(directory)
|
|
599
|
+
|
|
600
|
+
except (ValidationError, AccessDeniedError) as e:
|
|
601
|
+
return ServiceResult.error_result(
|
|
602
|
+
message=str(e),
|
|
603
|
+
error_code=ErrorCode.VALIDATION_ERROR if isinstance(e, ValidationError) else ErrorCode.ACCESS_DENIED
|
|
604
|
+
)
|
|
605
|
+
except Exception as e:
|
|
606
|
+
return ServiceResult.exception_result(
|
|
607
|
+
exception=e,
|
|
608
|
+
error_code=ErrorCode.INTERNAL_ERROR,
|
|
609
|
+
context="DirectoryService.move_directory"
|
|
610
|
+
)
|
|
611
|
+
|
|
612
|
+
# Helper methods
|
|
613
|
+
|
|
614
|
+
def _check_duplicate_name(
|
|
615
|
+
self,
|
|
616
|
+
tenant_id: str,
|
|
617
|
+
directory_name: str,
|
|
618
|
+
parent_id: Optional[str]
|
|
619
|
+
) -> bool:
|
|
620
|
+
"""Check if directory name already exists in parent."""
|
|
621
|
+
try:
|
|
622
|
+
gsi1_pk = f"TENANT#{tenant_id}"
|
|
623
|
+
if parent_id:
|
|
624
|
+
gsi1_sk = f"PARENT#{parent_id}#{directory_name}"
|
|
625
|
+
else:
|
|
626
|
+
gsi1_sk = f"PARENT#ROOT#{directory_name}"
|
|
627
|
+
|
|
628
|
+
results = self.dynamodb.query(
|
|
629
|
+
key=Key('gsi1_pk').eq(gsi1_pk) & Key('gsi1_sk').eq(gsi1_sk),
|
|
630
|
+
table_name=self.table_name,
|
|
631
|
+
index_name="gsi1",
|
|
632
|
+
limit=1
|
|
633
|
+
)
|
|
634
|
+
|
|
635
|
+
items = results.get('Items', [])
|
|
636
|
+
return len(items) > 0
|
|
637
|
+
|
|
638
|
+
except Exception:
|
|
639
|
+
return False
|
|
640
|
+
|
|
641
|
+
def _increment_subdirectory_count(
|
|
642
|
+
self,
|
|
643
|
+
tenant_id: str,
|
|
644
|
+
directory_id: str,
|
|
645
|
+
delta: int
|
|
646
|
+
) -> None:
|
|
647
|
+
"""Increment or decrement subdirectory count."""
|
|
648
|
+
try:
|
|
649
|
+
pk = f"DIR#{tenant_id}#{directory_id}"
|
|
650
|
+
sk = "METADATA"
|
|
651
|
+
|
|
652
|
+
# Get current directory
|
|
653
|
+
result = self.dynamodb.get(
|
|
654
|
+
table_name=self.table_name,
|
|
655
|
+
key={"pk": pk, "sk": sk}
|
|
656
|
+
)
|
|
657
|
+
|
|
658
|
+
if result and 'Item' in result:
|
|
659
|
+
directory = Directory()
|
|
660
|
+
directory.map(result['Item'])
|
|
661
|
+
|
|
662
|
+
directory.subdirectory_count = max(0, directory.subdirectory_count + delta)
|
|
663
|
+
|
|
664
|
+
item = directory.to_dictionary()
|
|
665
|
+
item["pk"] = pk
|
|
666
|
+
item["sk"] = sk
|
|
667
|
+
|
|
668
|
+
self.dynamodb.save(
|
|
669
|
+
item=item,
|
|
670
|
+
table_name=self.table_name
|
|
671
|
+
)
|
|
672
|
+
except Exception:
|
|
673
|
+
# Silent fail - this is a best-effort update
|
|
674
|
+
pass
|
|
675
|
+
|
|
676
|
+
def _is_descendant(
|
|
677
|
+
self,
|
|
678
|
+
tenant_id: str,
|
|
679
|
+
user_id: str,
|
|
680
|
+
potential_descendant_id: str,
|
|
681
|
+
ancestor_id: str
|
|
682
|
+
) -> bool:
|
|
683
|
+
"""Check if potential_descendant is a descendant of ancestor."""
|
|
684
|
+
try:
|
|
685
|
+
current_id = potential_descendant_id
|
|
686
|
+
|
|
687
|
+
# Traverse up the tree
|
|
688
|
+
while current_id:
|
|
689
|
+
if current_id == ancestor_id:
|
|
690
|
+
return True
|
|
691
|
+
|
|
692
|
+
result = self.get_by_id(current_id, tenant_id, user_id)
|
|
693
|
+
if not result.success:
|
|
694
|
+
return False
|
|
695
|
+
|
|
696
|
+
current_id = result.data.parent_id
|
|
697
|
+
|
|
698
|
+
return False
|
|
699
|
+
|
|
700
|
+
except Exception:
|
|
701
|
+
return False
|