geek-cafe-saas-sdk 0.7.0__py3-none-any.whl → 0.7.2__py3-none-any.whl

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

Potentially problematic release.


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

Files changed (79) hide show
  1. geek_cafe_saas_sdk/__init__.py +1 -1
  2. geek_cafe_saas_sdk/domains/files/models/directory.py +42 -6
  3. geek_cafe_saas_sdk/domains/files/models/file.py +40 -4
  4. geek_cafe_saas_sdk/domains/files/models/file_share.py +33 -0
  5. geek_cafe_saas_sdk/domains/files/models/file_version.py +24 -0
  6. geek_cafe_saas_sdk/domains/files/services/directory_service.py +54 -135
  7. geek_cafe_saas_sdk/domains/files/services/file_share_service.py +60 -136
  8. geek_cafe_saas_sdk/domains/files/services/file_system_service.py +43 -104
  9. geek_cafe_saas_sdk/domains/files/services/file_version_service.py +57 -131
  10. geek_cafe_saas_sdk/domains/messaging/services/contact_thread_service.py +55 -7
  11. geek_cafe_saas_sdk/domains/notifications/__init__.py +18 -0
  12. geek_cafe_saas_sdk/domains/notifications/handlers/__init__.py +1 -0
  13. geek_cafe_saas_sdk/domains/notifications/handlers/create_webhook/app.py +73 -0
  14. geek_cafe_saas_sdk/domains/notifications/handlers/get/app.py +40 -0
  15. geek_cafe_saas_sdk/domains/notifications/handlers/get_preferences/app.py +34 -0
  16. geek_cafe_saas_sdk/domains/notifications/handlers/list/app.py +43 -0
  17. geek_cafe_saas_sdk/domains/notifications/handlers/list_webhooks/app.py +40 -0
  18. geek_cafe_saas_sdk/domains/notifications/handlers/mark_read/app.py +40 -0
  19. geek_cafe_saas_sdk/domains/notifications/handlers/send/app.py +83 -0
  20. geek_cafe_saas_sdk/domains/notifications/handlers/update_preferences/app.py +45 -0
  21. geek_cafe_saas_sdk/domains/notifications/models/__init__.py +16 -0
  22. geek_cafe_saas_sdk/domains/notifications/models/notification.py +717 -0
  23. geek_cafe_saas_sdk/domains/notifications/models/notification_preference.py +365 -0
  24. geek_cafe_saas_sdk/domains/notifications/models/webhook_subscription.py +339 -0
  25. geek_cafe_saas_sdk/domains/notifications/services/__init__.py +10 -0
  26. geek_cafe_saas_sdk/domains/notifications/services/notification_service.py +576 -0
  27. geek_cafe_saas_sdk/domains/payments/__init__.py +16 -0
  28. geek_cafe_saas_sdk/domains/payments/handlers/README.md +334 -0
  29. geek_cafe_saas_sdk/domains/payments/handlers/__init__.py +6 -0
  30. geek_cafe_saas_sdk/domains/payments/handlers/billing_accounts/create/app.py +105 -0
  31. geek_cafe_saas_sdk/domains/payments/handlers/billing_accounts/get/app.py +60 -0
  32. geek_cafe_saas_sdk/domains/payments/handlers/billing_accounts/update/app.py +97 -0
  33. geek_cafe_saas_sdk/domains/payments/handlers/payment_intents/create/app.py +97 -0
  34. geek_cafe_saas_sdk/domains/payments/handlers/payment_intents/get/app.py +60 -0
  35. geek_cafe_saas_sdk/domains/payments/handlers/payments/get/app.py +60 -0
  36. geek_cafe_saas_sdk/domains/payments/handlers/payments/list/app.py +68 -0
  37. geek_cafe_saas_sdk/domains/payments/handlers/payments/record/app.py +118 -0
  38. geek_cafe_saas_sdk/domains/payments/handlers/refunds/create/app.py +89 -0
  39. geek_cafe_saas_sdk/domains/payments/handlers/refunds/get/app.py +60 -0
  40. geek_cafe_saas_sdk/domains/payments/models/__init__.py +17 -0
  41. geek_cafe_saas_sdk/domains/payments/models/billing_account.py +521 -0
  42. geek_cafe_saas_sdk/domains/payments/models/payment.py +639 -0
  43. geek_cafe_saas_sdk/domains/payments/models/payment_intent_ref.py +539 -0
  44. geek_cafe_saas_sdk/domains/payments/models/refund.py +404 -0
  45. geek_cafe_saas_sdk/domains/payments/services/__init__.py +11 -0
  46. geek_cafe_saas_sdk/domains/payments/services/payment_service.py +405 -0
  47. geek_cafe_saas_sdk/domains/subscriptions/__init__.py +19 -0
  48. geek_cafe_saas_sdk/domains/subscriptions/handlers/README.md +408 -0
  49. geek_cafe_saas_sdk/domains/subscriptions/handlers/__init__.py +1 -0
  50. geek_cafe_saas_sdk/domains/subscriptions/handlers/addons/create/app.py +81 -0
  51. geek_cafe_saas_sdk/domains/subscriptions/handlers/addons/get/app.py +48 -0
  52. geek_cafe_saas_sdk/domains/subscriptions/handlers/addons/list/app.py +54 -0
  53. geek_cafe_saas_sdk/domains/subscriptions/handlers/addons/update/app.py +54 -0
  54. geek_cafe_saas_sdk/domains/subscriptions/handlers/discounts/create/app.py +83 -0
  55. geek_cafe_saas_sdk/domains/subscriptions/handlers/discounts/get/app.py +47 -0
  56. geek_cafe_saas_sdk/domains/subscriptions/handlers/discounts/validate/app.py +62 -0
  57. geek_cafe_saas_sdk/domains/subscriptions/handlers/plans/create/app.py +82 -0
  58. geek_cafe_saas_sdk/domains/subscriptions/handlers/plans/get/app.py +48 -0
  59. geek_cafe_saas_sdk/domains/subscriptions/handlers/plans/list/app.py +66 -0
  60. geek_cafe_saas_sdk/domains/subscriptions/handlers/plans/update/app.py +54 -0
  61. geek_cafe_saas_sdk/domains/subscriptions/handlers/usage/aggregate/app.py +72 -0
  62. geek_cafe_saas_sdk/domains/subscriptions/handlers/usage/record/app.py +89 -0
  63. geek_cafe_saas_sdk/domains/subscriptions/models/__init__.py +13 -0
  64. geek_cafe_saas_sdk/domains/subscriptions/models/addon.py +604 -0
  65. geek_cafe_saas_sdk/domains/subscriptions/models/discount.py +492 -0
  66. geek_cafe_saas_sdk/domains/subscriptions/models/plan.py +569 -0
  67. geek_cafe_saas_sdk/domains/subscriptions/models/usage_record.py +300 -0
  68. geek_cafe_saas_sdk/domains/subscriptions/services/__init__.py +10 -0
  69. geek_cafe_saas_sdk/domains/subscriptions/services/subscription_manager_service.py +694 -0
  70. geek_cafe_saas_sdk/domains/tenancy/models/subscription.py +123 -1
  71. geek_cafe_saas_sdk/domains/tenancy/services/subscription_service.py +213 -0
  72. geek_cafe_saas_sdk/lambda_handlers/_base/base_handler.py +7 -0
  73. geek_cafe_saas_sdk/services/database_service.py +10 -6
  74. geek_cafe_saas_sdk/utilities/environment_variables.py +16 -0
  75. geek_cafe_saas_sdk/utilities/logging_utility.py +77 -0
  76. {geek_cafe_saas_sdk-0.7.0.dist-info → geek_cafe_saas_sdk-0.7.2.dist-info}/METADATA +1 -1
  77. {geek_cafe_saas_sdk-0.7.0.dist-info → geek_cafe_saas_sdk-0.7.2.dist-info}/RECORD +79 -20
  78. {geek_cafe_saas_sdk-0.7.0.dist-info → geek_cafe_saas_sdk-0.7.2.dist-info}/WHEEL +0 -0
  79. {geek_cafe_saas_sdk-0.7.0.dist-info → geek_cafe_saas_sdk-0.7.2.dist-info}/licenses/LICENSE +0 -0
@@ -3,7 +3,7 @@ Geek Cafe Services - Base Reusable Services for SaaS
3
3
 
4
4
  Version 0.6.0 adds File System Service
5
5
  """
6
- __version__ = "0.7.0"
6
+ __version__ = "0.7.2"
7
7
 
8
8
  # Import main modules for easier access
9
9
  from . import services
@@ -21,12 +21,12 @@ class Directory(BaseModel):
21
21
  Moving a file updates its directory_id, not its S3 location.
22
22
 
23
23
  Access Patterns (DynamoDB Keys):
24
- - pk: DIRECTORY#{tenant_id}#{directory_id}
25
- - sk: METADATA
26
- - gsi1_pk: TENANT#{tenant_id}
27
- - gsi1_sk: PATH#{full_path}
28
- - gsi2_pk: DIRECTORY#{tenant_id}#{parent_id}
29
- - gsi2_sk: NAME#{directory_name}
24
+ - pk: directory#{tenant_id}#{directory_id}
25
+ - sk: metadata
26
+ - gsi1_pk: tenant#{tenant_id}
27
+ - gsi1_sk: path#{full_path}
28
+ - gsi2_pk: directory#{tenant_id}#{parent_id}
29
+ - gsi2_sk: name#{directory_name}
30
30
  """
31
31
 
32
32
  def __init__(self):
@@ -61,6 +61,42 @@ class Directory(BaseModel):
61
61
 
62
62
  # Timestamps (inherited from BaseModel)
63
63
  # created_utc_ts, updated_utc_ts
64
+
65
+ # CRITICAL: Call _setup_indexes() as LAST line in __init__
66
+ self._setup_indexes()
67
+
68
+ def _setup_indexes(self):
69
+ """Setup DynamoDB indexes for directory queries."""
70
+
71
+ # Primary index: Directory by ID
72
+ primary = DynamoDBIndex()
73
+ primary.name = "primary"
74
+ primary.partition_key.attribute_name = "pk"
75
+ primary.partition_key.value = lambda: DynamoDBKey.build_key(("directory", self.id))
76
+ primary.sort_key.attribute_name = "sk"
77
+ primary.sort_key.value = lambda: "metadata"
78
+ self.indexes.add_primary(primary)
79
+
80
+ # GSI1: Directories by path
81
+ gsi = DynamoDBIndex()
82
+ gsi.name = "gsi1"
83
+ gsi.partition_key.attribute_name = f"{gsi.name}_pk"
84
+ gsi.partition_key.value = lambda: DynamoDBKey.build_key(("tenant", self.tenant_id))
85
+ gsi.sort_key.attribute_name = f"{gsi.name}_sk"
86
+ gsi.sort_key.value = lambda: DynamoDBKey.build_key(("path", self.full_path))
87
+ self.indexes.add_secondary(gsi)
88
+
89
+ # GSI2: Subdirectories by parent
90
+ gsi = DynamoDBIndex()
91
+ gsi.name = "gsi2"
92
+ gsi.partition_key.attribute_name = f"{gsi.name}_pk"
93
+ if self.parent_id:
94
+ gsi.partition_key.value = lambda: DynamoDBKey.build_key(("directory", self.tenant_id), ("parent", self.parent_id))
95
+ else:
96
+ gsi.partition_key.value = lambda: DynamoDBKey.build_key(("directory", self.tenant_id), ("root", "root"))
97
+ gsi.sort_key.attribute_name = f"{gsi.name}_sk"
98
+ gsi.sort_key.value = lambda: DynamoDBKey.build_key(("name", self.directory_name))
99
+ self.indexes.add_secondary(gsi)
64
100
 
65
101
  # Properties - Identity
66
102
  @property
@@ -25,10 +25,10 @@ class File(BaseModel):
25
25
 
26
26
  Access Patterns (DynamoDB Keys):
27
27
  - pk: FILE#{tenant_id}#{file_id}
28
- - sk: METADATA
29
- - gsi1_pk: TENANT#{tenant_id}
30
- - gsi1_sk: DIRECTORY#{directory_id}#{file_name}
31
- - gsi2_pk: TENANT#{tenant_id}#USER#{owner_id}
28
+ - sk: metadata
29
+ - gsi1_pk: tenant#{tenant_id}
30
+ - gsi1_sk: directory#{directory_id}#{file_name}
31
+ - gsi2_pk: tenant#{tenant_id}#USER#{owner_id}
32
32
  - gsi2_sk: FILE#{created_utc_ts}
33
33
 
34
34
  Versioning Strategies:
@@ -87,6 +87,42 @@ class File(BaseModel):
87
87
 
88
88
  # Timestamps (inherited from BaseModel)
89
89
  # created_utc_ts, updated_utc_ts, deleted_utc_ts
90
+
91
+ # CRITICAL: Call _setup_indexes() as LAST line in __init__
92
+ self._setup_indexes()
93
+
94
+ def _setup_indexes(self):
95
+ """Setup DynamoDB indexes for file queries."""
96
+
97
+ # Primary index: File by ID
98
+ primary = DynamoDBIndex()
99
+ primary.name = "primary"
100
+ primary.partition_key.attribute_name = "pk"
101
+ primary.partition_key.value = lambda: DynamoDBKey.build_key(("file", self.id))
102
+ primary.sort_key.attribute_name = "sk"
103
+ primary.sort_key.value = lambda: "metadata"
104
+ self.indexes.add_primary(primary)
105
+
106
+ # GSI1: Files by directory
107
+ gsi = DynamoDBIndex()
108
+ gsi.name = "gsi1"
109
+ gsi.partition_key.attribute_name = f"{gsi.name}_pk"
110
+ gsi.partition_key.value = lambda: DynamoDBKey.build_key(("tenant", self.tenant_id))
111
+ gsi.sort_key.attribute_name = f"{gsi.name}_sk"
112
+ if self.directory_id:
113
+ gsi.sort_key.value = lambda: DynamoDBKey.build_key(("directory", self.directory_id), ("file", self.file_name))
114
+ else:
115
+ gsi.sort_key.value = lambda: DynamoDBKey.build_key(("root", "file"), ("name", self.file_name))
116
+ self.indexes.add_secondary(gsi)
117
+
118
+ # GSI2: Files by owner
119
+ gsi = DynamoDBIndex()
120
+ gsi.name = "gsi2"
121
+ gsi.partition_key.attribute_name = f"{gsi.name}_pk"
122
+ gsi.partition_key.value = lambda: DynamoDBKey.build_key(("tenant", self.tenant_id), ("user", self.owner_id))
123
+ gsi.sort_key.attribute_name = f"{gsi.name}_sk"
124
+ gsi.sort_key.value = lambda: DynamoDBKey.build_key(("file", self.created_utc_ts))
125
+ self.indexes.add_secondary(gsi)
90
126
 
91
127
  # Properties - File Identity (alias for BaseModel.id)
92
128
  @property
@@ -63,6 +63,39 @@ class FileShare(BaseModel):
63
63
 
64
64
  # Timestamps (inherited from BaseModel)
65
65
  # created_utc_ts
66
+
67
+ # CRITICAL: Call _setup_indexes() as LAST line in __init__
68
+ self._setup_indexes()
69
+
70
+ def _setup_indexes(self):
71
+ """Setup DynamoDB indexes for file share queries."""
72
+
73
+ # Primary index: Share by ID
74
+ primary = DynamoDBIndex()
75
+ primary.name = "primary"
76
+ primary.partition_key.attribute_name = "pk"
77
+ primary.partition_key.value = lambda: DynamoDBKey.build_key(("share", self.id))
78
+ primary.sort_key.attribute_name = "sk"
79
+ primary.sort_key.value = lambda: DynamoDBKey.build_key(("share", self.id))
80
+ self.indexes.add_primary(primary)
81
+
82
+ # GSI1: Shares by file
83
+ gsi = DynamoDBIndex()
84
+ gsi.name = "gsi1"
85
+ gsi.partition_key.attribute_name = f"{gsi.name}_pk"
86
+ gsi.partition_key.value = lambda: DynamoDBKey.build_key(("file", self.file_id))
87
+ gsi.sort_key.attribute_name = f"{gsi.name}_sk"
88
+ gsi.sort_key.value = lambda: DynamoDBKey.build_key(("created", self.created_utc_ts))
89
+ self.indexes.add_secondary(gsi)
90
+
91
+ # GSI2: Shares by shared_with_user
92
+ gsi = DynamoDBIndex()
93
+ gsi.name = "gsi2"
94
+ gsi.partition_key.attribute_name = f"{gsi.name}_pk"
95
+ gsi.partition_key.value = lambda: DynamoDBKey.build_key(("shared_with", self.shared_with_user_id))
96
+ gsi.sort_key.attribute_name = f"{gsi.name}_sk"
97
+ gsi.sort_key.value = lambda: DynamoDBKey.build_key(("created", self.created_utc_ts))
98
+ self.indexes.add_secondary(gsi)
66
99
 
67
100
  # Properties - Identity
68
101
  @property
@@ -54,6 +54,30 @@ class FileVersion(BaseModel):
54
54
 
55
55
  # Timestamps (inherited from BaseModel)
56
56
  # created_utc_ts - when this version was created
57
+
58
+ # CRITICAL: Call _setup_indexes() as LAST line in __init__
59
+ self._setup_indexes()
60
+
61
+ def _setup_indexes(self):
62
+ """Setup DynamoDB indexes for file version queries."""
63
+
64
+ # Primary index: Version by ID
65
+ primary = DynamoDBIndex()
66
+ primary.name = "primary"
67
+ primary.partition_key.attribute_name = "pk"
68
+ primary.partition_key.value = lambda: DynamoDBKey.build_key(("file_version", self.id))
69
+ primary.sort_key.attribute_name = "sk"
70
+ primary.sort_key.value = lambda: DynamoDBKey.build_key(("version", self.id))
71
+ self.indexes.add_primary(primary)
72
+
73
+ # GSI1: Versions by file (ordered by version number)
74
+ gsi = DynamoDBIndex()
75
+ gsi.name = "gsi1"
76
+ gsi.partition_key.attribute_name = f"{gsi.name}_pk"
77
+ gsi.partition_key.value = lambda: DynamoDBKey.build_key(("file", self.file_id))
78
+ gsi.sort_key.attribute_name = f"{gsi.name}_sk"
79
+ gsi.sort_key.value = lambda: DynamoDBKey.build_key(("version", self.version_number))
80
+ self.indexes.add_secondary(gsi)
57
81
 
58
82
  # Properties - Identity
59
83
  @property
@@ -113,28 +113,11 @@ class DirectoryService(DatabaseService[Directory]):
113
113
  directory.total_size = 0
114
114
 
115
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}"
116
+ directory.prep_for_save()
117
+ save_result = self._save_model(directory)
129
118
 
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
- )
119
+ if not save_result.success:
120
+ return save_result
138
121
 
139
122
  # Update parent's subdirectory count
140
123
  if parent_id:
@@ -172,22 +155,13 @@ class DirectoryService(DatabaseService[Directory]):
172
155
  ServiceResult with Directory model
173
156
  """
174
157
  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
- )
158
+ # Use helper method with tenant check
159
+ directory = self._get_model_by_id_with_tenant_check(resource_id, Directory, tenant_id)
182
160
 
183
161
  # Check if directory exists
184
- if not result or 'Item' not in result:
162
+ if not directory:
185
163
  raise NotFoundError(f"Directory not found: {resource_id}")
186
164
 
187
- # Convert to Directory model
188
- directory = Directory()
189
- directory.map(result['Item'])
190
-
191
165
  # Access control: Check if user is owner
192
166
  if directory.owner_id != user_id:
193
167
  # TODO: Check shared access
@@ -292,27 +266,8 @@ class DirectoryService(DatabaseService[Directory]):
292
266
  directory.updated_utc_ts = dt.datetime.now(dt.UTC).timestamp()
293
267
 
294
268
  # 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)
269
+ directory.prep_for_save()
270
+ return self._save_model(directory)
316
271
 
317
272
  except (ValidationError, AccessDeniedError) as e:
318
273
  return ServiceResult.error_result(
@@ -369,17 +324,11 @@ class DirectoryService(DatabaseService[Directory]):
369
324
  directory.status = "deleted"
370
325
  directory.deleted_utc_ts = dt.datetime.now(dt.UTC).timestamp()
371
326
 
372
- pk = f"DIR#{tenant_id}#{resource_id}"
373
- sk = "METADATA"
327
+ directory.prep_for_save()
328
+ save_result = self._save_model(directory)
374
329
 
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
- )
330
+ if not save_result.success:
331
+ return save_result
383
332
 
384
333
  # Update parent's subdirectory count
385
334
  if directory.parent_id:
@@ -419,30 +368,26 @@ class DirectoryService(DatabaseService[Directory]):
419
368
  ServiceResult with list of Directory models
420
369
  """
421
370
  try:
422
- gsi1_pk = f"TENANT#{tenant_id}"
371
+ # Use GSI2 to query subdirectories by parent
372
+ temp_directory = Directory()
373
+ temp_directory.tenant_id = tenant_id
374
+ temp_directory.parent_id = parent_id # None for root directories
423
375
 
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
- )
376
+ # Query using helper method
377
+ query_result = self._query_by_index(temp_directory, "gsi2", limit=limit, ascending=True)
378
+
379
+ if not query_result.success:
380
+ return query_result
436
381
 
382
+ # Filter results
437
383
  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:
384
+ for directory in query_result.data:
385
+ # Debug: print what we're seeing
386
+ # print(f"Found dir: {directory.directory_name}, parent_id={directory.parent_id}, expected={parent_id}")
387
+ # Filter out deleted directories and apply access control
388
+ if directory.status != "deleted" and directory.owner_id == user_id:
389
+ # Only include directories whose parent_id matches what we're looking for
390
+ if directory.parent_id == parent_id:
446
391
  directories.append(directory)
447
392
 
448
393
  return ServiceResult.success_result(directories)
@@ -570,24 +515,11 @@ class DirectoryService(DatabaseService[Directory]):
570
515
  directory.updated_utc_ts = dt.datetime.now(dt.UTC).timestamp()
571
516
 
572
517
  # 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}"
518
+ directory.prep_for_save()
519
+ save_result = self._save_model(directory)
586
520
 
587
- self.dynamodb.save(
588
- item=item,
589
- table_name=self.table_name
590
- )
521
+ if not save_result.success:
522
+ return save_result
591
523
 
592
524
  # Update parent counts
593
525
  if old_parent_id:
@@ -619,21 +551,23 @@ class DirectoryService(DatabaseService[Directory]):
619
551
  ) -> bool:
620
552
  """Check if directory name already exists in parent."""
621
553
  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}"
554
+ # Use GSI2 to query subdirectories by parent
555
+ temp_directory = Directory()
556
+ temp_directory.tenant_id = tenant_id
557
+ temp_directory.parent_id = parent_id
558
+ temp_directory.directory_name = directory_name
627
559
 
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
- )
560
+ query_result = self._query_by_index(temp_directory, "gsi2", limit=1)
561
+
562
+ if not query_result.success:
563
+ return False
634
564
 
635
- items = results.get('Items', [])
636
- return len(items) > 0
565
+ # Check if any non-deleted directories with this name exist
566
+ for directory in query_result.data:
567
+ if directory.directory_name == directory_name and directory.status != "deleted":
568
+ return True
569
+
570
+ return False
637
571
 
638
572
  except Exception:
639
573
  return False
@@ -646,29 +580,14 @@ class DirectoryService(DatabaseService[Directory]):
646
580
  ) -> None:
647
581
  """Increment or decrement subdirectory count."""
648
582
  try:
649
- pk = f"DIR#{tenant_id}#{directory_id}"
650
- sk = "METADATA"
583
+ # Get current directory using helper
584
+ directory = self._get_model_by_id_with_tenant_check(directory_id, Directory, tenant_id)
651
585
 
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
-
586
+ if directory:
662
587
  directory.subdirectory_count = max(0, directory.subdirectory_count + delta)
663
588
 
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
- )
589
+ directory.prep_for_save()
590
+ self._save_model(directory)
672
591
  except Exception:
673
592
  # Silent fail - this is a best-effort update
674
593
  pass