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,128 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Lambda handler for creating votes.
|
|
3
|
+
|
|
4
|
+
REFACTORED VERSION using Factory Pattern.
|
|
5
|
+
Reduces code and centralizes auth strategy configuration.
|
|
6
|
+
|
|
7
|
+
The handler type is determined by environment variables:
|
|
8
|
+
- AUTH_TYPE=secure (default) - API Gateway authorizer
|
|
9
|
+
- AUTH_TYPE=api_key - x-api-key header validation
|
|
10
|
+
- AUTH_TYPE=public - No authentication
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from typing import Dict, Any
|
|
14
|
+
from geek_cafe_saas_sdk.lambda_handlers import create_handler
|
|
15
|
+
from geek_cafe_saas_sdk.domains.voting.services import VoteService
|
|
16
|
+
from geek_cafe_saas_sdk.utilities.response import error_response
|
|
17
|
+
|
|
18
|
+
# ⚡ Initialize handler at module level for Lambda warm starts
|
|
19
|
+
# Factory automatically selects handler based on AUTH_TYPE env var:
|
|
20
|
+
# - secure (default): API Gateway authorizer (Cognito/Lambda)
|
|
21
|
+
# - api_key: Validates x-api-key header
|
|
22
|
+
# - public: No authentication required
|
|
23
|
+
#
|
|
24
|
+
# This automatically handles:
|
|
25
|
+
# - Authentication (based on AUTH_TYPE)
|
|
26
|
+
# - Request body parsing
|
|
27
|
+
# - Case conversion (camelCase → snake_case)
|
|
28
|
+
# - Service pooling (connection reuse)
|
|
29
|
+
# - User context extraction
|
|
30
|
+
# - CORS headers (apply_cors=True by default)
|
|
31
|
+
# - Error handling (apply_error_handling=True by default)
|
|
32
|
+
handler = create_handler(
|
|
33
|
+
service_class=VoteService,
|
|
34
|
+
require_body=True,
|
|
35
|
+
convert_case=True
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
def lambda_handler(event: Dict[str, Any], context: Any, injected_service=None) -> Dict[str, Any]:
|
|
39
|
+
"""
|
|
40
|
+
Create or update a vote.
|
|
41
|
+
|
|
42
|
+
Args:
|
|
43
|
+
event: API Gateway event
|
|
44
|
+
context: Lambda context
|
|
45
|
+
injected_service: Optional VoteService for testing (Moto)
|
|
46
|
+
|
|
47
|
+
Expected request body:
|
|
48
|
+
{
|
|
49
|
+
"userId": "user_id",
|
|
50
|
+
"targetId": "health-meter-ui-choice",
|
|
51
|
+
"choiceId": "gauge" | "traffic_light",
|
|
52
|
+
"voteType": "single_choice",
|
|
53
|
+
"availableChoices": ["gauge", "traffic_light"],
|
|
54
|
+
"content": {
|
|
55
|
+
"description": "User preference for health meter display type",
|
|
56
|
+
"metadata": {...}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
"""
|
|
60
|
+
return handler.execute(event, context, create_vote, injected_service)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def create_vote(
|
|
64
|
+
event: Dict[str, Any],
|
|
65
|
+
service: VoteService,
|
|
66
|
+
user_context: Dict[str, str]
|
|
67
|
+
) -> Any:
|
|
68
|
+
"""
|
|
69
|
+
Business logic for creating a vote.
|
|
70
|
+
|
|
71
|
+
All boilerplate has been handled by the wrapper:
|
|
72
|
+
✅ API key validation
|
|
73
|
+
✅ Body parsing and case conversion
|
|
74
|
+
✅ Service initialization
|
|
75
|
+
✅ User context extraction
|
|
76
|
+
|
|
77
|
+
Focus purely on business logic here.
|
|
78
|
+
|
|
79
|
+
Args:
|
|
80
|
+
event: Enhanced event with parsed_body containing snake_case data
|
|
81
|
+
service: VoteService instance (pooled for warm starts)
|
|
82
|
+
user_context: Extracted user info (user_id, tenant_id, etc.)
|
|
83
|
+
|
|
84
|
+
Returns:
|
|
85
|
+
ServiceResult that will be formatted into Lambda response
|
|
86
|
+
"""
|
|
87
|
+
# Get parsed and converted body (camelCase → snake_case already done)
|
|
88
|
+
payload = event["parsed_body"]
|
|
89
|
+
|
|
90
|
+
# Validate required fields
|
|
91
|
+
target_id = payload.get("target_id")
|
|
92
|
+
if not target_id:
|
|
93
|
+
return error_response("target_id is required", 400)
|
|
94
|
+
|
|
95
|
+
choice_id = payload.get("choice_id")
|
|
96
|
+
if not choice_id:
|
|
97
|
+
return error_response("choice_id is required", 400)
|
|
98
|
+
|
|
99
|
+
vote_type = payload.get("vote_type", "single_choice")
|
|
100
|
+
available_choices = payload.get("available_choices", [])
|
|
101
|
+
content = payload.get("content", {})
|
|
102
|
+
|
|
103
|
+
# Validate choice_id is in available_choices if provided
|
|
104
|
+
if available_choices and choice_id not in available_choices:
|
|
105
|
+
return error_response(
|
|
106
|
+
f"choice_id '{choice_id}' must be one of: {available_choices}", 400
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
# Get user info from context
|
|
110
|
+
tenant_id = user_context.get("tenant_id", "anonymous")
|
|
111
|
+
# we will get the logged in user if there is one, or a sudo user id sent by the client
|
|
112
|
+
user_id = user_context.get("user_id") or payload.get("user_id", "anonymous")
|
|
113
|
+
|
|
114
|
+
# Create the vote based on type
|
|
115
|
+
if vote_type == "single_choice":
|
|
116
|
+
result = service.create_single_choice_vote(
|
|
117
|
+
tenant_id=tenant_id,
|
|
118
|
+
user_id=user_id,
|
|
119
|
+
target_id=target_id,
|
|
120
|
+
choice_id=choice_id,
|
|
121
|
+
available_choices=available_choices if available_choices else None,
|
|
122
|
+
content=content if content else None,
|
|
123
|
+
)
|
|
124
|
+
else:
|
|
125
|
+
return error_response(f"Unsupported vote_type: {vote_type}", 400)
|
|
126
|
+
|
|
127
|
+
# Return ServiceResult - handler will automatically format to Lambda response
|
|
128
|
+
return result
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# src/geek_cafe_saas_sdk/lambda_handlers/votes/delete/app.py
|
|
2
|
+
|
|
3
|
+
from typing import Dict, Any
|
|
4
|
+
|
|
5
|
+
from geek_cafe_saas_sdk.services import VoteService
|
|
6
|
+
from geek_cafe_saas_sdk.lambda_handlers import ServicePool
|
|
7
|
+
from geek_cafe_saas_sdk.utilities.response import service_result_to_response, error_response, success_response
|
|
8
|
+
from geek_cafe_saas_sdk.utilities.lambda_event_utility import LambdaEventUtility
|
|
9
|
+
|
|
10
|
+
vote_service_pool = ServicePool(VoteService)
|
|
11
|
+
|
|
12
|
+
def handler(event: Dict[str, Any], context: object, injected_service=None) -> Dict[str, Any]:
|
|
13
|
+
"""
|
|
14
|
+
Lambda handler for deleting a vote by its ID.
|
|
15
|
+
|
|
16
|
+
Args:
|
|
17
|
+
event: API Gateway event
|
|
18
|
+
context: Lambda context
|
|
19
|
+
injected_service: Optional VoteService for testing
|
|
20
|
+
"""
|
|
21
|
+
try:
|
|
22
|
+
vote_service = injected_service if injected_service else vote_service_pool.get()
|
|
23
|
+
user_id = LambdaEventUtility.get_authenticated_user_id(event)
|
|
24
|
+
tenant_id = LambdaEventUtility.get_authenticated_user_tenant_id(event)
|
|
25
|
+
resource_id = LambdaEventUtility.get_value_from_path_parameters(event, 'id')
|
|
26
|
+
|
|
27
|
+
if not resource_id:
|
|
28
|
+
return error_response("Vote ID is required in the path.", "VALIDATION_ERROR", 400)
|
|
29
|
+
|
|
30
|
+
result = vote_service.delete(
|
|
31
|
+
resource_id=resource_id,
|
|
32
|
+
tenant_id=tenant_id,
|
|
33
|
+
user_id=user_id
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
if result.success:
|
|
37
|
+
return success_response(message="Vote deleted successfully", status_code=204)
|
|
38
|
+
return service_result_to_response(result)
|
|
39
|
+
|
|
40
|
+
except Exception as e:
|
|
41
|
+
return error_response(f"An unexpected error occurred: {str(e)}", "INTERNAL_ERROR", 500)
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# src/geek_cafe_saas_sdk/lambda_handlers/votes/get/app.py
|
|
2
|
+
|
|
3
|
+
from typing import Dict, Any
|
|
4
|
+
|
|
5
|
+
from geek_cafe_saas_sdk.services import VoteService
|
|
6
|
+
from geek_cafe_saas_sdk.lambda_handlers import ServicePool
|
|
7
|
+
from geek_cafe_saas_sdk.utilities.response import service_result_to_response, error_response
|
|
8
|
+
from geek_cafe_saas_sdk.utilities.lambda_event_utility import LambdaEventUtility
|
|
9
|
+
|
|
10
|
+
vote_service_pool = ServicePool(VoteService)
|
|
11
|
+
|
|
12
|
+
def handler(event: Dict[str, Any], context: object, injected_service=None) -> Dict[str, Any]:
|
|
13
|
+
"""
|
|
14
|
+
Lambda handler for retrieving a single vote by its ID.
|
|
15
|
+
|
|
16
|
+
Args:
|
|
17
|
+
event: API Gateway event
|
|
18
|
+
context: Lambda context
|
|
19
|
+
injected_service: Optional VoteService for testing
|
|
20
|
+
"""
|
|
21
|
+
try:
|
|
22
|
+
vote_service = injected_service if injected_service else vote_service_pool.get()
|
|
23
|
+
user_id = LambdaEventUtility.get_authenticated_user_id(event)
|
|
24
|
+
tenant_id = LambdaEventUtility.get_authenticated_user_tenant_id(event)
|
|
25
|
+
resource_id = LambdaEventUtility.get_value_from_path_parameters(event, 'id')
|
|
26
|
+
|
|
27
|
+
if not resource_id:
|
|
28
|
+
return error_response("Vote ID is required in the path.", "VALIDATION_ERROR", 400)
|
|
29
|
+
|
|
30
|
+
result = vote_service.get_by_id(
|
|
31
|
+
resource_id=resource_id,
|
|
32
|
+
tenant_id=tenant_id,
|
|
33
|
+
user_id=user_id
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
return service_result_to_response(result)
|
|
37
|
+
|
|
38
|
+
except Exception as e:
|
|
39
|
+
return error_response(f"An unexpected error occurred: {str(e)}", "INTERNAL_ERROR", 500)
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# src/geek_cafe_saas_sdk/lambda_handlers/votes/list/app.py
|
|
2
|
+
|
|
3
|
+
from typing import Dict, Any
|
|
4
|
+
|
|
5
|
+
from geek_cafe_saas_sdk.services import VoteService
|
|
6
|
+
from geek_cafe_saas_sdk.lambda_handlers import ServicePool
|
|
7
|
+
from geek_cafe_saas_sdk.utilities.response import service_result_to_response, error_response
|
|
8
|
+
from geek_cafe_saas_sdk.utilities.lambda_event_utility import LambdaEventUtility
|
|
9
|
+
|
|
10
|
+
vote_service_pool = ServicePool(VoteService)
|
|
11
|
+
|
|
12
|
+
def handler(event: Dict[str, Any], context: object, injected_service=None) -> Dict[str, Any]:
|
|
13
|
+
"""
|
|
14
|
+
Lambda handler for listing votes with optional filters.
|
|
15
|
+
|
|
16
|
+
Args:
|
|
17
|
+
event: API Gateway event
|
|
18
|
+
context: Lambda context
|
|
19
|
+
injected_service: Optional VoteService for testing
|
|
20
|
+
"""
|
|
21
|
+
try:
|
|
22
|
+
vote_service = injected_service if injected_service else vote_service_pool.get()
|
|
23
|
+
user_id = LambdaEventUtility.get_authenticated_user_id(event)
|
|
24
|
+
tenant_id = LambdaEventUtility.get_authenticated_user_tenant_id(event)
|
|
25
|
+
query_params = event.get('queryStringParameters', {}) or {}
|
|
26
|
+
|
|
27
|
+
# Check for target_id in query params
|
|
28
|
+
target_id = query_params.get('target_id')
|
|
29
|
+
if target_id:
|
|
30
|
+
result = vote_service.list_by_target(target_id=target_id)
|
|
31
|
+
else:
|
|
32
|
+
# Default to listing by user
|
|
33
|
+
result = vote_service.list_by_user(user_id=user_id)
|
|
34
|
+
|
|
35
|
+
return service_result_to_response(result)
|
|
36
|
+
|
|
37
|
+
except Exception as e:
|
|
38
|
+
return error_response(f"An unexpected error occurred: {str(e)}", "INTERNAL_ERROR", 500)
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# src/geek_cafe_saas_sdk/lambda_handlers/votes/update/app.py
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from typing import Dict, Any
|
|
5
|
+
|
|
6
|
+
from geek_cafe_saas_sdk.services import VoteService
|
|
7
|
+
from geek_cafe_saas_sdk.lambda_handlers import ServicePool
|
|
8
|
+
from geek_cafe_saas_sdk.utilities.response import service_result_to_response, error_response
|
|
9
|
+
from geek_cafe_saas_sdk.utilities.lambda_event_utility import LambdaEventUtility
|
|
10
|
+
|
|
11
|
+
vote_service_pool = ServicePool(VoteService)
|
|
12
|
+
|
|
13
|
+
def handler(event: Dict[str, Any], context: object, injected_service=None) -> Dict[str, Any]:
|
|
14
|
+
"""
|
|
15
|
+
Lambda handler for updating an existing vote.
|
|
16
|
+
|
|
17
|
+
Args:
|
|
18
|
+
event: API Gateway event
|
|
19
|
+
context: Lambda context
|
|
20
|
+
injected_service: Optional VoteService for testing
|
|
21
|
+
"""
|
|
22
|
+
try:
|
|
23
|
+
vote_service = injected_service if injected_service else vote_service_pool.get()
|
|
24
|
+
body = LambdaEventUtility.get_body_from_event(event)
|
|
25
|
+
user_id = LambdaEventUtility.get_authenticated_user_id(event)
|
|
26
|
+
tenant_id = LambdaEventUtility.get_authenticated_user_tenant_id(event)
|
|
27
|
+
resource_id = LambdaEventUtility.get_value_from_path_parameters(event, 'id')
|
|
28
|
+
|
|
29
|
+
if not resource_id:
|
|
30
|
+
return error_response("Vote ID is required in the path.", "VALIDATION_ERROR", 400)
|
|
31
|
+
|
|
32
|
+
result = vote_service.update(
|
|
33
|
+
resource_id=resource_id,
|
|
34
|
+
tenant_id=tenant_id,
|
|
35
|
+
user_id=user_id,
|
|
36
|
+
updates=body
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
return service_result_to_response(result)
|
|
40
|
+
|
|
41
|
+
except json.JSONDecodeError:
|
|
42
|
+
return error_response("Invalid JSON in request body.", "VALIDATION_ERROR", 400)
|
|
43
|
+
except Exception as e:
|
|
44
|
+
return error_response(f"An unexpected error occurred: {str(e)}", "INTERNAL_ERROR", 500)
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
|
|
2
|
+
from boto3_assist.dynamodb.dynamodb_index import DynamoDBIndex, DynamoDBKey
|
|
3
|
+
from boto3_assist.utilities.string_utility import StringUtility
|
|
4
|
+
import datetime as dt
|
|
5
|
+
from typing import Dict, Any
|
|
6
|
+
from geek_cafe_saas_sdk.models.base_model import BaseModel
|
|
7
|
+
|
|
8
|
+
class Vote(BaseModel):
|
|
9
|
+
def __init__(self):
|
|
10
|
+
super().__init__()
|
|
11
|
+
self._content: Dict[str, Any] = {}
|
|
12
|
+
self._target_id: str | None = None
|
|
13
|
+
|
|
14
|
+
# Enhanced voting system fields
|
|
15
|
+
self._vote_type: str = "single_choice" # single_choice, multi_select, ranking, rating
|
|
16
|
+
self._choices: Dict[str, Any] = {} # Flexible choice storage
|
|
17
|
+
self._max_selections: int | None = None # For multi_select limits
|
|
18
|
+
|
|
19
|
+
# Legacy fields (for backward compatibility)
|
|
20
|
+
self._up_vote: int = 0
|
|
21
|
+
self._down_vote: int = 0
|
|
22
|
+
|
|
23
|
+
self._setup_indexes()
|
|
24
|
+
|
|
25
|
+
def _setup_indexes(self):
|
|
26
|
+
primary: DynamoDBIndex = DynamoDBIndex()
|
|
27
|
+
primary.name = "primary"
|
|
28
|
+
primary.partition_key.attribute_name = "pk"
|
|
29
|
+
primary.partition_key.value = lambda: DynamoDBKey.build_key(
|
|
30
|
+
("vote", self.id)
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
primary.sort_key.attribute_name = "sk"
|
|
34
|
+
primary.sort_key.value = lambda: DynamoDBKey.build_key(("vote", self.id))
|
|
35
|
+
self.indexes.add_primary(primary)
|
|
36
|
+
|
|
37
|
+
## GSI: 1
|
|
38
|
+
# GSI: all votes
|
|
39
|
+
gsi: DynamoDBIndex = DynamoDBIndex()
|
|
40
|
+
|
|
41
|
+
gsi.name = "gsi1"
|
|
42
|
+
gsi.partition_key.attribute_name = f"{gsi.name}_pk"
|
|
43
|
+
gsi.partition_key.value = lambda: DynamoDBKey.build_key(("vote", "all"))
|
|
44
|
+
gsi.sort_key.attribute_name = f"{gsi.name}_sk"
|
|
45
|
+
gsi.sort_key.value = lambda: DynamoDBKey.build_key(
|
|
46
|
+
("ts", self.created_utc_ts)
|
|
47
|
+
)
|
|
48
|
+
self.indexes.add_secondary(gsi)
|
|
49
|
+
|
|
50
|
+
## GSI: 2
|
|
51
|
+
# GSI: all votes by user, sorted by created ts
|
|
52
|
+
gsi: DynamoDBIndex = DynamoDBIndex()
|
|
53
|
+
|
|
54
|
+
gsi.name = "gsi2"
|
|
55
|
+
gsi.partition_key.attribute_name = f"{gsi.name}_pk"
|
|
56
|
+
gsi.partition_key.value = lambda: DynamoDBKey.build_key(("user", self.user_id))
|
|
57
|
+
gsi.sort_key.attribute_name = f"{gsi.name}_sk"
|
|
58
|
+
gsi.sort_key.value = lambda: DynamoDBKey.build_key(
|
|
59
|
+
("model", "vote"),("ts", self.created_utc_ts)
|
|
60
|
+
)
|
|
61
|
+
self.indexes.add_secondary(gsi)
|
|
62
|
+
|
|
63
|
+
## GSI: 3
|
|
64
|
+
# GSI: all votes by tenant, sorted by created ts
|
|
65
|
+
gsi: DynamoDBIndex = DynamoDBIndex()
|
|
66
|
+
|
|
67
|
+
gsi.name = "gsi3"
|
|
68
|
+
gsi.partition_key.attribute_name = f"{gsi.name}_pk"
|
|
69
|
+
gsi.partition_key.value = lambda: DynamoDBKey.build_key(("tenant", self.tenant_id))
|
|
70
|
+
gsi.sort_key.attribute_name = f"{gsi.name}_sk"
|
|
71
|
+
gsi.sort_key.value = lambda: DynamoDBKey.build_key(
|
|
72
|
+
("model", "vote"),("ts", self.created_utc_ts)
|
|
73
|
+
)
|
|
74
|
+
self.indexes.add_secondary(gsi)
|
|
75
|
+
|
|
76
|
+
## GSI: 4
|
|
77
|
+
# GSI: enforce uniqueness helper - all votes by user+target (one per target per user)
|
|
78
|
+
gsi: DynamoDBIndex = DynamoDBIndex()
|
|
79
|
+
|
|
80
|
+
gsi.name = "gsi4"
|
|
81
|
+
gsi.partition_key.attribute_name = f"{gsi.name}_pk"
|
|
82
|
+
gsi.partition_key.value = lambda: DynamoDBKey.build_key(("user", self.user_id))
|
|
83
|
+
gsi.sort_key.attribute_name = f"{gsi.name}_sk"
|
|
84
|
+
gsi.sort_key.value = lambda: DynamoDBKey.build_key(
|
|
85
|
+
("target", self.target_id)
|
|
86
|
+
)
|
|
87
|
+
self.indexes.add_secondary(gsi)
|
|
88
|
+
|
|
89
|
+
## GSI: 5
|
|
90
|
+
# GSI: all votes for a target
|
|
91
|
+
gsi: DynamoDBIndex = DynamoDBIndex()
|
|
92
|
+
|
|
93
|
+
gsi.name = "gsi5"
|
|
94
|
+
gsi.partition_key.attribute_name = f"{gsi.name}_pk"
|
|
95
|
+
gsi.partition_key.value = lambda: DynamoDBKey.build_key(("target", self.target_id))
|
|
96
|
+
gsi.sort_key.attribute_name = f"{gsi.name}_sk"
|
|
97
|
+
gsi.sort_key.value = lambda: DynamoDBKey.build_key(
|
|
98
|
+
("ts", self.created_utc_ts)
|
|
99
|
+
)
|
|
100
|
+
self.indexes.add_secondary(gsi)
|
|
101
|
+
|
|
102
|
+
@property
|
|
103
|
+
def content(self) -> Dict[str, Any]:
|
|
104
|
+
return self._content
|
|
105
|
+
|
|
106
|
+
@content.setter
|
|
107
|
+
def content(self, value: Dict[str, Any]):
|
|
108
|
+
self._content = value
|
|
109
|
+
|
|
110
|
+
@property
|
|
111
|
+
def up_vote(self) -> int:
|
|
112
|
+
return self._up_vote
|
|
113
|
+
|
|
114
|
+
@up_vote.setter
|
|
115
|
+
def up_vote(self, value: int):
|
|
116
|
+
self._up_vote = value
|
|
117
|
+
|
|
118
|
+
@property
|
|
119
|
+
def down_vote(self) -> int:
|
|
120
|
+
return self._down_vote
|
|
121
|
+
|
|
122
|
+
@down_vote.setter
|
|
123
|
+
def down_vote(self, value: int):
|
|
124
|
+
self._down_vote = value
|
|
125
|
+
|
|
126
|
+
@property
|
|
127
|
+
def target_id(self) -> str | None:
|
|
128
|
+
return self._target_id
|
|
129
|
+
|
|
130
|
+
@target_id.setter
|
|
131
|
+
def target_id(self, value: str | None):
|
|
132
|
+
self._target_id = value
|
|
133
|
+
|
|
134
|
+
@property
|
|
135
|
+
def vote_type(self) -> str:
|
|
136
|
+
return self._vote_type
|
|
137
|
+
|
|
138
|
+
@vote_type.setter
|
|
139
|
+
def vote_type(self, value: str):
|
|
140
|
+
self._vote_type = value
|
|
141
|
+
|
|
142
|
+
@property
|
|
143
|
+
def choices(self) -> Dict[str, Any]:
|
|
144
|
+
return self._choices
|
|
145
|
+
|
|
146
|
+
@choices.setter
|
|
147
|
+
def choices(self, value: Dict[str, Any]):
|
|
148
|
+
self._choices = value
|
|
149
|
+
|
|
150
|
+
@property
|
|
151
|
+
def max_selections(self) -> int | None:
|
|
152
|
+
return self._max_selections
|
|
153
|
+
|
|
154
|
+
@max_selections.setter
|
|
155
|
+
def max_selections(self, value: int | None):
|
|
156
|
+
self._max_selections = value
|
|
157
|
+
|
|
158
|
+
# Helper methods for different voting patterns
|
|
159
|
+
def set_single_choice(self, choice_id: str, available_choices: list[str] = None):
|
|
160
|
+
"""Set a single choice vote (A/B test, single selection)."""
|
|
161
|
+
self.vote_type = "single_choice"
|
|
162
|
+
self.choices = {choice_id: {"selected": True, "value": 1}}
|
|
163
|
+
|
|
164
|
+
# Set legacy fields for backward compatibility
|
|
165
|
+
if available_choices and len(available_choices) >= 2:
|
|
166
|
+
self.up_vote = 1 if choice_id == available_choices[0] else 0
|
|
167
|
+
self.down_vote = 1 if choice_id == available_choices[1] else 0
|
|
168
|
+
else:
|
|
169
|
+
self.up_vote = 1
|
|
170
|
+
self.down_vote = 0
|
|
171
|
+
|
|
172
|
+
def set_multi_select(self, selected_choices: list[str], available_choices: list[str] = None, max_selections: int = None):
|
|
173
|
+
"""Set multiple choice selections."""
|
|
174
|
+
self.vote_type = "multi_select"
|
|
175
|
+
self.max_selections = max_selections
|
|
176
|
+
|
|
177
|
+
# Build choices dict
|
|
178
|
+
if available_choices:
|
|
179
|
+
self.choices = {
|
|
180
|
+
choice: {"selected": choice in selected_choices, "value": 1 if choice in selected_choices else 0}
|
|
181
|
+
for choice in available_choices
|
|
182
|
+
}
|
|
183
|
+
else:
|
|
184
|
+
self.choices = {choice: {"selected": True, "value": 1} for choice in selected_choices}
|
|
185
|
+
|
|
186
|
+
# Set legacy fields
|
|
187
|
+
self.up_vote = len(selected_choices)
|
|
188
|
+
self.down_vote = 0
|
|
189
|
+
|
|
190
|
+
def set_ranking(self, ranked_choices: list[str]):
|
|
191
|
+
"""Set ranked choices (1st, 2nd, 3rd preference)."""
|
|
192
|
+
self.vote_type = "ranking"
|
|
193
|
+
self.choices = {
|
|
194
|
+
choice: {"rank": idx + 1, "value": len(ranked_choices) - idx}
|
|
195
|
+
for idx, choice in enumerate(ranked_choices)
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
# Set legacy fields
|
|
199
|
+
self.up_vote = len(ranked_choices)
|
|
200
|
+
self.down_vote = 0
|
|
201
|
+
|
|
202
|
+
def set_rating(self, ratings: Dict[str, float]):
|
|
203
|
+
"""Set rating votes (1-5 stars per option)."""
|
|
204
|
+
self.vote_type = "rating"
|
|
205
|
+
self.choices = {
|
|
206
|
+
choice: {"rating": rating, "value": rating}
|
|
207
|
+
for choice, rating in ratings.items()
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
# Set legacy fields (average rating)
|
|
211
|
+
avg_rating = sum(ratings.values()) / len(ratings) if ratings else 0
|
|
212
|
+
self.up_vote = int(avg_rating)
|
|
213
|
+
self.down_vote = 0
|
|
214
|
+
|
|
215
|
+
def get_selected_choices(self) -> list[str]:
|
|
216
|
+
"""Get list of selected choices for any vote type."""
|
|
217
|
+
if self.vote_type == "single_choice":
|
|
218
|
+
return [choice for choice, data in self.choices.items() if data.get("selected")]
|
|
219
|
+
elif self.vote_type == "multi_select":
|
|
220
|
+
return [choice for choice, data in self.choices.items() if data.get("selected")]
|
|
221
|
+
elif self.vote_type == "ranking":
|
|
222
|
+
# Return choices sorted by rank
|
|
223
|
+
ranked = sorted(self.choices.items(), key=lambda x: x[1].get("rank", 999))
|
|
224
|
+
return [choice for choice, _ in ranked]
|
|
225
|
+
elif self.vote_type == "rating":
|
|
226
|
+
return list(self.choices.keys())
|
|
227
|
+
return []
|
|
228
|
+
|
|
229
|
+
def get_choice_value(self, choice_id: str) -> Any:
|
|
230
|
+
"""Get the value for a specific choice."""
|
|
231
|
+
return self.choices.get(choice_id, {}).get("value", 0)
|