geek-cafe-saas-sdk 0.6.0__py3-none-any.whl → 0.7.1__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 (94) hide show
  1. geek_cafe_saas_sdk/__init__.py +2 -2
  2. geek_cafe_saas_sdk/domains/files/handlers/README.md +446 -0
  3. geek_cafe_saas_sdk/domains/files/handlers/__init__.py +6 -0
  4. geek_cafe_saas_sdk/domains/files/handlers/files/create/app.py +121 -0
  5. geek_cafe_saas_sdk/domains/files/handlers/files/download/app.py +80 -0
  6. geek_cafe_saas_sdk/domains/files/handlers/files/get/app.py +62 -0
  7. geek_cafe_saas_sdk/domains/files/handlers/files/list/app.py +72 -0
  8. geek_cafe_saas_sdk/domains/files/handlers/lineage/create_derived/app.py +99 -0
  9. geek_cafe_saas_sdk/domains/files/handlers/lineage/create_main/app.py +104 -0
  10. geek_cafe_saas_sdk/domains/files/handlers/lineage/download_bundle/app.py +99 -0
  11. geek_cafe_saas_sdk/domains/files/handlers/lineage/get_lineage/app.py +68 -0
  12. geek_cafe_saas_sdk/domains/files/handlers/lineage/prepare_bundle/app.py +76 -0
  13. geek_cafe_saas_sdk/domains/files/models/__init__.py +17 -0
  14. geek_cafe_saas_sdk/domains/files/models/directory.py +42 -6
  15. geek_cafe_saas_sdk/domains/files/models/file.py +158 -16
  16. geek_cafe_saas_sdk/domains/files/models/file_share.py +33 -0
  17. geek_cafe_saas_sdk/domains/files/models/file_version.py +24 -0
  18. geek_cafe_saas_sdk/domains/files/services/__init__.py +21 -0
  19. geek_cafe_saas_sdk/domains/files/services/directory_service.py +54 -135
  20. geek_cafe_saas_sdk/domains/files/services/file_lineage_service.py +487 -0
  21. geek_cafe_saas_sdk/domains/files/services/file_share_service.py +37 -120
  22. geek_cafe_saas_sdk/domains/files/services/file_system_service.py +67 -103
  23. geek_cafe_saas_sdk/domains/files/services/file_version_service.py +44 -124
  24. geek_cafe_saas_sdk/domains/messaging/services/contact_thread_service.py +55 -7
  25. geek_cafe_saas_sdk/domains/notifications/__init__.py +18 -0
  26. geek_cafe_saas_sdk/domains/notifications/handlers/__init__.py +1 -0
  27. geek_cafe_saas_sdk/domains/notifications/handlers/create_webhook/app.py +73 -0
  28. geek_cafe_saas_sdk/domains/notifications/handlers/get/app.py +40 -0
  29. geek_cafe_saas_sdk/domains/notifications/handlers/get_preferences/app.py +34 -0
  30. geek_cafe_saas_sdk/domains/notifications/handlers/list/app.py +43 -0
  31. geek_cafe_saas_sdk/domains/notifications/handlers/list_webhooks/app.py +40 -0
  32. geek_cafe_saas_sdk/domains/notifications/handlers/mark_read/app.py +40 -0
  33. geek_cafe_saas_sdk/domains/notifications/handlers/send/app.py +83 -0
  34. geek_cafe_saas_sdk/domains/notifications/handlers/update_preferences/app.py +45 -0
  35. geek_cafe_saas_sdk/domains/notifications/models/__init__.py +16 -0
  36. geek_cafe_saas_sdk/domains/notifications/models/notification.py +717 -0
  37. geek_cafe_saas_sdk/domains/notifications/models/notification_preference.py +365 -0
  38. geek_cafe_saas_sdk/domains/notifications/models/webhook_subscription.py +339 -0
  39. geek_cafe_saas_sdk/domains/notifications/services/__init__.py +10 -0
  40. geek_cafe_saas_sdk/domains/notifications/services/notification_service.py +576 -0
  41. geek_cafe_saas_sdk/domains/payments/__init__.py +16 -0
  42. geek_cafe_saas_sdk/domains/payments/handlers/README.md +334 -0
  43. geek_cafe_saas_sdk/domains/payments/handlers/__init__.py +6 -0
  44. geek_cafe_saas_sdk/domains/payments/handlers/billing_accounts/create/app.py +105 -0
  45. geek_cafe_saas_sdk/domains/payments/handlers/billing_accounts/get/app.py +60 -0
  46. geek_cafe_saas_sdk/domains/payments/handlers/billing_accounts/update/app.py +97 -0
  47. geek_cafe_saas_sdk/domains/payments/handlers/payment_intents/create/app.py +97 -0
  48. geek_cafe_saas_sdk/domains/payments/handlers/payment_intents/get/app.py +60 -0
  49. geek_cafe_saas_sdk/domains/payments/handlers/payments/get/app.py +60 -0
  50. geek_cafe_saas_sdk/domains/payments/handlers/payments/list/app.py +68 -0
  51. geek_cafe_saas_sdk/domains/payments/handlers/payments/record/app.py +118 -0
  52. geek_cafe_saas_sdk/domains/payments/handlers/refunds/create/app.py +89 -0
  53. geek_cafe_saas_sdk/domains/payments/handlers/refunds/get/app.py +60 -0
  54. geek_cafe_saas_sdk/domains/payments/models/__init__.py +17 -0
  55. geek_cafe_saas_sdk/domains/payments/models/billing_account.py +521 -0
  56. geek_cafe_saas_sdk/domains/payments/models/payment.py +639 -0
  57. geek_cafe_saas_sdk/domains/payments/models/payment_intent_ref.py +539 -0
  58. geek_cafe_saas_sdk/domains/payments/models/refund.py +404 -0
  59. geek_cafe_saas_sdk/domains/payments/services/__init__.py +11 -0
  60. geek_cafe_saas_sdk/domains/payments/services/payment_service.py +405 -0
  61. geek_cafe_saas_sdk/domains/subscriptions/__init__.py +19 -0
  62. geek_cafe_saas_sdk/domains/subscriptions/handlers/README.md +408 -0
  63. geek_cafe_saas_sdk/domains/subscriptions/handlers/__init__.py +1 -0
  64. geek_cafe_saas_sdk/domains/subscriptions/handlers/addons/create/app.py +81 -0
  65. geek_cafe_saas_sdk/domains/subscriptions/handlers/addons/get/app.py +48 -0
  66. geek_cafe_saas_sdk/domains/subscriptions/handlers/addons/list/app.py +54 -0
  67. geek_cafe_saas_sdk/domains/subscriptions/handlers/addons/update/app.py +54 -0
  68. geek_cafe_saas_sdk/domains/subscriptions/handlers/discounts/create/app.py +83 -0
  69. geek_cafe_saas_sdk/domains/subscriptions/handlers/discounts/get/app.py +47 -0
  70. geek_cafe_saas_sdk/domains/subscriptions/handlers/discounts/validate/app.py +62 -0
  71. geek_cafe_saas_sdk/domains/subscriptions/handlers/plans/create/app.py +82 -0
  72. geek_cafe_saas_sdk/domains/subscriptions/handlers/plans/get/app.py +48 -0
  73. geek_cafe_saas_sdk/domains/subscriptions/handlers/plans/list/app.py +66 -0
  74. geek_cafe_saas_sdk/domains/subscriptions/handlers/plans/update/app.py +54 -0
  75. geek_cafe_saas_sdk/domains/subscriptions/handlers/usage/aggregate/app.py +72 -0
  76. geek_cafe_saas_sdk/domains/subscriptions/handlers/usage/record/app.py +89 -0
  77. geek_cafe_saas_sdk/domains/subscriptions/models/__init__.py +13 -0
  78. geek_cafe_saas_sdk/domains/subscriptions/models/addon.py +604 -0
  79. geek_cafe_saas_sdk/domains/subscriptions/models/discount.py +492 -0
  80. geek_cafe_saas_sdk/domains/subscriptions/models/plan.py +569 -0
  81. geek_cafe_saas_sdk/domains/subscriptions/models/usage_record.py +300 -0
  82. geek_cafe_saas_sdk/domains/subscriptions/services/__init__.py +10 -0
  83. geek_cafe_saas_sdk/domains/subscriptions/services/subscription_manager_service.py +694 -0
  84. geek_cafe_saas_sdk/domains/tenancy/models/subscription.py +123 -1
  85. geek_cafe_saas_sdk/domains/tenancy/services/subscription_service.py +213 -0
  86. geek_cafe_saas_sdk/lambda_handlers/_base/base_handler.py +7 -0
  87. geek_cafe_saas_sdk/services/database_service.py +10 -6
  88. geek_cafe_saas_sdk/utilities/cognito_utility.py +16 -26
  89. geek_cafe_saas_sdk/utilities/environment_variables.py +16 -0
  90. geek_cafe_saas_sdk/utilities/logging_utility.py +77 -0
  91. {geek_cafe_saas_sdk-0.6.0.dist-info → geek_cafe_saas_sdk-0.7.1.dist-info}/METADATA +11 -11
  92. {geek_cafe_saas_sdk-0.6.0.dist-info → geek_cafe_saas_sdk-0.7.1.dist-info}/RECORD +94 -23
  93. {geek_cafe_saas_sdk-0.6.0.dist-info → geek_cafe_saas_sdk-0.7.1.dist-info}/WHEEL +0 -0
  94. {geek_cafe_saas_sdk-0.6.0.dist-info → geek_cafe_saas_sdk-0.7.1.dist-info}/licenses/LICENSE +0 -0
@@ -109,27 +109,8 @@ class FileShareService(DatabaseService[FileShare]):
109
109
  share.access_count = 0
110
110
 
111
111
  # Save to DynamoDB
112
- pk = f"FILE#{tenant_id}#{file_id}"
113
- sk = f"SHARE#{share.share_id}"
114
-
115
- item = share.to_dictionary()
116
- item["pk"] = pk
117
- item["sk"] = sk
118
-
119
- # GSI1: Shares by file
120
- item["gsi1_pk"] = f"FILE#{tenant_id}#{file_id}"
121
- item["gsi1_sk"] = f"USER#{shared_with_user_id}"
122
-
123
- # GSI2: Shares with user
124
- item["gsi2_pk"] = f"TENANT#{tenant_id}#USER#{shared_with_user_id}"
125
- item["gsi2_sk"] = f"FILE#{file_id}#{share.created_utc_ts}"
126
-
127
- self.dynamodb.save(
128
- item=item,
129
- table_name=self.table_name
130
- )
131
-
132
- return ServiceResult.success_result(share)
112
+ share.prep_for_save()
113
+ return self._save_model(share)
133
114
 
134
115
  except (ValidationError, AccessDeniedError) as e:
135
116
  return ServiceResult.error_result(
@@ -163,23 +144,12 @@ class FileShareService(DatabaseService[FileShare]):
163
144
  ServiceResult with FileShare model
164
145
  """
165
146
  try:
166
- if not file_id:
167
- raise ValidationError("file_id is required", "file_id")
168
-
169
- pk = f"FILE#{tenant_id}#{file_id}"
170
- sk = f"SHARE#{resource_id}"
147
+ # Use helper method with tenant check
148
+ share = self._get_model_by_id_with_tenant_check(resource_id, FileShare, tenant_id)
171
149
 
172
- result = self.dynamodb.get(
173
- table_name=self.table_name,
174
- key={"pk": pk, "sk": sk}
175
- )
176
-
177
- if not result or 'Item' not in result:
150
+ if not share:
178
151
  raise NotFoundError(f"Share not found: {resource_id}")
179
152
 
180
- share = FileShare()
181
- share.map(result['Item'])
182
-
183
153
  # Access control: user must be sharer or sharee
184
154
  if share.shared_by != user_id and share.shared_with_user_id != user_id:
185
155
  raise AccessDeniedError("You do not have access to this share")
@@ -256,25 +226,8 @@ class FileShareService(DatabaseService[FileShare]):
256
226
  share.updated_utc_ts = dt.datetime.now(dt.UTC).timestamp()
257
227
 
258
228
  # Save to DynamoDB
259
- pk = f"FILE#{tenant_id}#{file_id}"
260
- sk = f"SHARE#{resource_id}"
261
-
262
- item = share.to_dictionary()
263
- item["pk"] = pk
264
- item["sk"] = sk
265
-
266
- # Preserve GSI keys
267
- item["gsi1_pk"] = f"FILE#{tenant_id}#{file_id}"
268
- item["gsi1_sk"] = f"USER#{share.shared_with_user_id}"
269
- item["gsi2_pk"] = f"TENANT#{tenant_id}#USER#{share.shared_with_user_id}"
270
- item["gsi2_sk"] = f"FILE#{file_id}#{share.created_utc_ts}"
271
-
272
- self.dynamodb.save(
273
- item=item,
274
- table_name=self.table_name
275
- )
276
-
277
- return ServiceResult.success_result(share)
229
+ share.prep_for_save()
230
+ return self._save_model(share)
278
231
 
279
232
  except (ValidationError, AccessDeniedError) as e:
280
233
  return ServiceResult.error_result(
@@ -323,23 +276,11 @@ class FileShareService(DatabaseService[FileShare]):
323
276
  share.status = "revoked"
324
277
  share.revoked_at = dt.datetime.now(dt.UTC).timestamp()
325
278
 
326
- pk = f"FILE#{tenant_id}#{file_id}"
327
- sk = f"SHARE#{resource_id}"
328
-
329
- item = share.to_dictionary()
330
- item["pk"] = pk
331
- item["sk"] = sk
332
-
333
- # Preserve GSI keys
334
- item["gsi1_pk"] = f"FILE#{tenant_id}#{file_id}"
335
- item["gsi1_sk"] = f"USER#{share.shared_with_user_id}"
336
- item["gsi2_pk"] = f"TENANT#{tenant_id}#USER#{share.shared_with_user_id}"
337
- item["gsi2_sk"] = f"FILE#{file_id}#{share.created_utc_ts}"
279
+ share.prep_for_save()
280
+ save_result = self._save_model(share)
338
281
 
339
- self.dynamodb.save(
340
- item=item,
341
- table_name=self.table_name
342
- )
282
+ if not save_result.success:
283
+ return save_result
343
284
 
344
285
  return ServiceResult.success_result(True)
345
286
 
@@ -383,21 +324,19 @@ class FileShareService(DatabaseService[FileShare]):
383
324
  error_code=ErrorCode.ACCESS_DENIED
384
325
  )
385
326
 
386
- # Query GSI1
387
- gsi1_pk = f"FILE#{tenant_id}#{file_id}"
327
+ # Use GSI1 to query shares by file
328
+ temp_share = FileShare()
329
+ temp_share.file_id = file_id
388
330
 
389
- results = self.dynamodb.query(
390
- key=Key('gsi1_pk').eq(gsi1_pk) & Key('gsi1_sk').begins_with("USER#"),
391
- table_name=self.table_name,
392
- index_name="gsi1",
393
- limit=limit
394
- )
331
+ # Query using helper method
332
+ query_result = self._query_by_index(temp_share, "gsi1", limit=limit, ascending=False)
395
333
 
334
+ if not query_result.success:
335
+ return query_result
336
+
337
+ # Filter results
396
338
  shares = []
397
- for item in results.get('Items', []):
398
- share = FileShare()
399
- share.map(item)
400
-
339
+ for share in query_result.data:
401
340
  # Include active and expired shares, exclude revoked
402
341
  if share.status != "revoked":
403
342
  shares.append(share)
@@ -429,21 +368,19 @@ class FileShareService(DatabaseService[FileShare]):
429
368
  ServiceResult with list of FileShare models
430
369
  """
431
370
  try:
432
- # Query GSI2
433
- gsi2_pk = f"TENANT#{tenant_id}#USER#{user_id}"
371
+ # Use GSI2 to query shares by shared_with_user
372
+ temp_share = FileShare()
373
+ temp_share.shared_with_user_id = user_id
434
374
 
435
- results = self.dynamodb.query(
436
- key=Key('gsi2_pk').eq(gsi2_pk) & Key('gsi2_sk').begins_with("FILE#"),
437
- table_name=self.table_name,
438
- index_name="gsi2",
439
- limit=limit
440
- )
375
+ # Query using helper method
376
+ query_result = self._query_by_index(temp_share, "gsi2", limit=limit, ascending=False)
377
+
378
+ if not query_result.success:
379
+ return query_result
441
380
 
381
+ # Filter results
442
382
  shares = []
443
- for item in results.get('Items', []):
444
- share = FileShare()
445
- share.map(item)
446
-
383
+ for share in query_result.data:
447
384
  # Only include active, non-expired shares
448
385
  if share.is_active:
449
386
  shares.append(share)
@@ -543,20 +480,12 @@ class FileShareService(DatabaseService[FileShare]):
543
480
  def _get_file(self, tenant_id: str, file_id: str, user_id: str) -> ServiceResult[File]:
544
481
  """Get file with access control."""
545
482
  try:
546
- pk = f"FILE#{tenant_id}#{file_id}"
547
- sk = "METADATA"
548
-
549
- result = self.dynamodb.get(
550
- table_name=self.table_name,
551
- key={"pk": pk, "sk": sk}
552
- )
483
+ # Use helper method with tenant check
484
+ file = self._get_model_by_id_with_tenant_check(file_id, File, tenant_id)
553
485
 
554
- if not result or 'Item' not in result:
486
+ if not file:
555
487
  raise NotFoundError(f"File not found: {file_id}")
556
488
 
557
- file = File()
558
- file.map(result['Item'])
559
-
560
489
  if file.owner_id != user_id:
561
490
  raise AccessDeniedError("You do not have access to this file")
562
491
 
@@ -572,7 +501,7 @@ class FileShareService(DatabaseService[FileShare]):
572
501
  """Get file without access control (for internal use)."""
573
502
  try:
574
503
  pk = f"FILE#{tenant_id}#{file_id}"
575
- sk = "METADATA"
504
+ sk = "metadata"
576
505
 
577
506
  result = self.dynamodb.get(
578
507
  table_name=self.table_name,
@@ -645,19 +574,7 @@ class FileShareService(DatabaseService[FileShare]):
645
574
  share.access_count += 1
646
575
  share.last_accessed_at = dt.datetime.now(dt.UTC).timestamp()
647
576
 
648
- item = share.to_dictionary()
649
- item["pk"] = pk
650
- item["sk"] = sk
651
-
652
- # Preserve GSI keys
653
- item["gsi1_pk"] = result['Item'].get('gsi1_pk')
654
- item["gsi1_sk"] = result['Item'].get('gsi1_sk')
655
- item["gsi2_pk"] = result['Item'].get('gsi2_pk')
656
- item["gsi2_sk"] = result['Item'].get('gsi2_sk')
657
-
658
- self.dynamodb.save(
659
- item=item,
660
- table_name=self.table_name
661
- )
577
+ share.prep_for_save()
578
+ self._save_model(share)
662
579
  except Exception:
663
580
  pass # Best effort
@@ -62,6 +62,12 @@ class FileSystemService(DatabaseService[File]):
62
62
  versioning_strategy: str = "explicit",
63
63
  description: Optional[str] = None,
64
64
  tags: Optional[List[str]] = None,
65
+ file_role: Optional[str] = None,
66
+ parent_file_id: Optional[str] = None,
67
+ original_file_id: Optional[str] = None,
68
+ transformation_type: Optional[str] = None,
69
+ transformation_operation: Optional[str] = None,
70
+ transformation_metadata: Optional[Dict[str, Any]] = None,
65
71
  **kwargs
66
72
  ) -> ServiceResult[File]:
67
73
  """
@@ -77,6 +83,12 @@ class FileSystemService(DatabaseService[File]):
77
83
  versioning_strategy: "s3_native" or "explicit"
78
84
  description: Optional description
79
85
  tags: Optional tags
86
+ file_role: Optional lineage role ("standalone", "original", "main", "derived")
87
+ parent_file_id: Optional parent file ID for lineage
88
+ original_file_id: Optional original file ID for lineage
89
+ transformation_type: Optional transformation type ("convert", "clean", "process")
90
+ transformation_operation: Optional operation name
91
+ transformation_metadata: Optional transformation metadata dict
80
92
 
81
93
  Returns:
82
94
  ServiceResult with File model
@@ -109,6 +121,20 @@ class FileSystemService(DatabaseService[File]):
109
121
  file.tags = tags or []
110
122
  file.status = "active"
111
123
 
124
+ # Set lineage fields if provided
125
+ if file_role:
126
+ file.file_role = file_role
127
+ if parent_file_id:
128
+ file.parent_file_id = parent_file_id
129
+ if original_file_id:
130
+ file.original_file_id = original_file_id
131
+ if transformation_type:
132
+ file.transformation_type = transformation_type
133
+ if transformation_operation:
134
+ file.transformation_operation = transformation_operation
135
+ if transformation_metadata:
136
+ file.transformation_metadata = transformation_metadata
137
+
112
138
  # Extract file extension
113
139
  file_path = Path(file_name)
114
140
  file.file_extension = file_path.suffix if file_path.suffix else None
@@ -148,28 +174,11 @@ class FileSystemService(DatabaseService[File]):
148
174
  )
149
175
 
150
176
  # Save metadata to DynamoDB
151
- pk = f"FILE#{tenant_id}#{file.file_id}"
152
- sk = "METADATA"
153
-
154
- item = file.to_dictionary()
155
- item["pk"] = pk
156
- item["sk"] = sk
157
-
158
- # GSI1: Files by directory
159
- item["gsi1_pk"] = f"TENANT#{tenant_id}"
160
- if directory_id:
161
- item["gsi1_sk"] = f"DIRECTORY#{directory_id}#{file_name}"
162
- else:
163
- item["gsi1_sk"] = f"DIRECTORY#ROOT#{file_name}"
164
-
165
- # GSI2: Files by owner
166
- item["gsi2_pk"] = f"TENANT#{tenant_id}#USER#{user_id}"
167
- item["gsi2_sk"] = f"FILE#{file.created_utc_ts}"
177
+ file.prep_for_save()
178
+ save_result = self._save_model(file)
168
179
 
169
- self.dynamodb.save(
170
- table_name=self.table_name,
171
- item=item
172
- )
180
+ if not save_result.success:
181
+ return save_result
173
182
 
174
183
  return ServiceResult.success_result(file)
175
184
 
@@ -203,23 +212,13 @@ class FileSystemService(DatabaseService[File]):
203
212
  ServiceResult with File model
204
213
  """
205
214
  try:
206
- pk = f"FILE#{tenant_id}#{resource_id}"
207
- sk = "METADATA"
215
+ # Use helper method with tenant check
216
+ file = self._get_model_by_id_with_tenant_check(resource_id, File, tenant_id)
208
217
 
209
- result = self.dynamodb.get(
210
- table_name=self.table_name,
211
- key={"pk": pk, "sk": sk}
212
- )
213
-
214
- # Check if file exists first (before checking ownership)
215
- # DynamoDB.get returns {'Item': {...}} or {'ResponseMetadata': {...}}
216
- if not result or 'Item' not in result:
218
+ # Check if file exists
219
+ if not file:
217
220
  raise NotFoundError(f"File not found: {resource_id}")
218
221
 
219
- # Convert to File model
220
- file = File()
221
- file.map(result['Item'])
222
-
223
222
  # Access control: Check if user is owner or has share access
224
223
  if file.owner_id != user_id:
225
224
  # TODO: Check FileShare for access
@@ -278,7 +277,7 @@ class FileSystemService(DatabaseService[File]):
278
277
  # Apply updates (only allowed fields)
279
278
  allowed_fields = [
280
279
  "file_name", "description", "tags", "directory_id",
281
- "status"
280
+ "status", "derived_file_count"
282
281
  ]
283
282
 
284
283
  for field, value in updates.items():
@@ -290,27 +289,8 @@ class FileSystemService(DatabaseService[File]):
290
289
  file.updated_utc_ts = dt.datetime.now(dt.UTC).timestamp()
291
290
 
292
291
  # Save to DynamoDB
293
- pk = f"FILE#{tenant_id}#{resource_id}"
294
- sk = "METADATA"
295
-
296
- item = file.to_dictionary()
297
- item["pk"] = pk
298
- item["sk"] = sk
299
-
300
- # Update GSI keys if directory changed
301
- if "directory_id" in updates:
302
- item["gsi1_pk"] = f"TENANT#{tenant_id}"
303
- if updates["directory_id"]:
304
- item["gsi1_sk"] = f"DIRECTORY#{updates['directory_id']}#{file.file_name}"
305
- else:
306
- item["gsi1_sk"] = f"DIRECTORY#ROOT#{file.file_name}"
307
-
308
- self.dynamodb.save(
309
- table_name=self.table_name,
310
- item=item
311
- )
312
-
313
- return ServiceResult.success_result(file)
292
+ file.prep_for_save()
293
+ return self._save_model(file)
314
294
 
315
295
  except AccessDeniedError as e:
316
296
  return ServiceResult.error_result(
@@ -371,7 +351,7 @@ class FileSystemService(DatabaseService[File]):
371
351
 
372
352
  # Delete from DynamoDB
373
353
  pk = f"FILE#{tenant_id}#{resource_id}"
374
- sk = "METADATA"
354
+ sk = "metadata"
375
355
 
376
356
  self.dynamodb.delete(
377
357
  primary_key={"pk": pk, "sk": sk},
@@ -383,17 +363,11 @@ class FileSystemService(DatabaseService[File]):
383
363
  file.status = "deleted"
384
364
  file.deleted_utc_ts = dt.datetime.now(dt.UTC).timestamp()
385
365
 
386
- pk = f"FILE#{tenant_id}#{resource_id}"
387
- sk = "METADATA"
388
-
389
- item = file.to_dictionary()
390
- item["pk"] = pk
391
- item["sk"] = sk
366
+ file.prep_for_save()
367
+ save_result = self._save_model(file)
392
368
 
393
- self.dynamodb.save(
394
- table_name=self.table_name,
395
- item=item
396
- )
369
+ if not save_result.success:
370
+ return save_result
397
371
 
398
372
  return ServiceResult.success_result(True)
399
373
 
@@ -481,32 +455,23 @@ class FileSystemService(DatabaseService[File]):
481
455
  ServiceResult with list of File models
482
456
  """
483
457
  try:
484
- gsi1_pk = f"TENANT#{tenant_id}"
458
+ # Use GSI1 to query files by directory
459
+ temp_file = File()
460
+ temp_file.tenant_id = tenant_id
461
+ temp_file.directory_id = directory_id # None for root files
485
462
 
486
- if directory_id:
487
- gsi1_sk_prefix = f"DIRECTORY#{directory_id}#"
488
- else:
489
- gsi1_sk_prefix = "DIRECTORY#ROOT#"
490
-
491
- # Query GSI1
492
- results = self.dynamodb.query(
493
- key=Key('gsi1_pk').eq(gsi1_pk) & Key('gsi1_sk').begins_with(gsi1_sk_prefix),
494
- table_name=self.table_name,
495
- index_name="gsi1",
496
- limit=limit
497
- )
463
+ # Query using helper method
464
+ query_result = self._query_by_index(temp_file, "gsi1", limit=limit, ascending=True)
465
+
466
+ if not query_result.success:
467
+ return query_result
498
468
 
469
+ # Filter results
499
470
  files = []
500
- for item in results.get('Items', []):
501
- file = File()
502
- file.map(item)
503
-
504
- # Filter out deleted files
505
- if file.status != "deleted":
506
- # Basic access control: show only owned files or shared files
507
- # TODO: Check FileShare for shared access
508
- if file.owner_id == user_id:
509
- files.append(file)
471
+ for file in query_result.data:
472
+ # Filter out deleted files and apply access control
473
+ if file.status != "deleted" and file.owner_id == user_id:
474
+ files.append(file)
510
475
 
511
476
  return ServiceResult.success_result(files)
512
477
 
@@ -541,21 +506,20 @@ class FileSystemService(DatabaseService[File]):
541
506
  if owner_id != user_id:
542
507
  raise AccessDeniedError("You can only list your own files")
543
508
 
544
- gsi2_pk = f"TENANT#{tenant_id}#USER#{owner_id}"
509
+ # Use GSI2 to query files by owner
510
+ temp_file = File()
511
+ temp_file.tenant_id = tenant_id
512
+ temp_file.owner_id = owner_id
545
513
 
546
- # Query GSI2
547
- results = self.dynamodb.query(
548
- key=Key('gsi2_pk').eq(gsi2_pk) & Key('gsi2_sk').begins_with("FILE#"),
549
- table_name=self.table_name,
550
- index_name="gsi2",
551
- limit=limit
552
- )
514
+ # Query using helper method
515
+ query_result = self._query_by_index(temp_file, "gsi2", limit=limit, ascending=False)
516
+
517
+ if not query_result.success:
518
+ return query_result
553
519
 
520
+ # Filter results
554
521
  files = []
555
- for item in results.get('Items', []):
556
- file = File()
557
- file.map(item)
558
-
522
+ for file in query_result.data:
559
523
  # Filter out deleted files
560
524
  if file.status != "deleted":
561
525
  files.append(file)