api-foundry-query-engine 0.8.32__tar.gz → 0.8.34__tar.gz

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.
Files changed (38) hide show
  1. {api_foundry_query_engine-0.8.32 → api_foundry_query_engine-0.8.34}/PKG-INFO +1 -1
  2. {api_foundry_query_engine-0.8.32 → api_foundry_query_engine-0.8.34}/api_foundry_query_engine/adapters/gateway_adapter.py +32 -20
  3. {api_foundry_query_engine-0.8.32 → api_foundry_query_engine-0.8.34}/api_foundry_query_engine/connectors/postgres_connection.py +18 -3
  4. api_foundry_query_engine-0.8.34/api_foundry_query_engine/dao/batch_operation_handler.py +295 -0
  5. {api_foundry_query_engine-0.8.32 → api_foundry_query_engine-0.8.34}/api_foundry_query_engine/dao/operation_dao.py +23 -1
  6. {api_foundry_query_engine-0.8.32 → api_foundry_query_engine-0.8.34}/api_foundry_query_engine/dao/sql_delete_query_handler.py +63 -4
  7. {api_foundry_query_engine-0.8.32 → api_foundry_query_engine-0.8.34}/api_foundry_query_engine/dao/sql_insert_query_handler.py +28 -0
  8. {api_foundry_query_engine-0.8.32 → api_foundry_query_engine-0.8.34}/api_foundry_query_engine/dao/sql_query_handler.py +192 -1
  9. api_foundry_query_engine-0.8.34/api_foundry_query_engine/dao/sql_restore_query_handler.py +194 -0
  10. {api_foundry_query_engine-0.8.32 → api_foundry_query_engine-0.8.34}/api_foundry_query_engine/dao/sql_select_query_handler.py +47 -0
  11. api_foundry_query_engine-0.8.34/api_foundry_query_engine/dao/sql_update_query_handler.py +197 -0
  12. {api_foundry_query_engine-0.8.32 → api_foundry_query_engine-0.8.34}/api_foundry_query_engine/utils/api_model.py +58 -4
  13. api_foundry_query_engine-0.8.34/api_foundry_query_engine/utils/dependency_resolver.py +157 -0
  14. api_foundry_query_engine-0.8.34/api_foundry_query_engine/utils/reference_resolver.py +222 -0
  15. {api_foundry_query_engine-0.8.32 → api_foundry_query_engine-0.8.34}/pyproject.toml +1 -1
  16. api_foundry_query_engine-0.8.32/api_foundry_query_engine/dao/sql_update_query_handler.py +0 -81
  17. {api_foundry_query_engine-0.8.32 → api_foundry_query_engine-0.8.34}/.gitignore +0 -0
  18. {api_foundry_query_engine-0.8.32 → api_foundry_query_engine-0.8.34}/LICENSE +0 -0
  19. {api_foundry_query_engine-0.8.32 → api_foundry_query_engine-0.8.34}/api_foundry_query_engine/.pre-commit-config.yaml +0 -0
  20. {api_foundry_query_engine-0.8.32 → api_foundry_query_engine-0.8.34}/api_foundry_query_engine/__init__.py +0 -0
  21. {api_foundry_query_engine-0.8.32 → api_foundry_query_engine-0.8.34}/api_foundry_query_engine/adapters/adapter.py +0 -0
  22. {api_foundry_query_engine-0.8.32 → api_foundry_query_engine-0.8.34}/api_foundry_query_engine/adapters/case_change_adapter.py +0 -0
  23. {api_foundry_query_engine-0.8.32 → api_foundry_query_engine-0.8.34}/api_foundry_query_engine/adapters/security_adapter.py +0 -0
  24. {api_foundry_query_engine-0.8.32 → api_foundry_query_engine-0.8.34}/api_foundry_query_engine/connectors/connection.py +0 -0
  25. {api_foundry_query_engine-0.8.32 → api_foundry_query_engine-0.8.34}/api_foundry_query_engine/connectors/connection_factory.py +0 -0
  26. {api_foundry_query_engine-0.8.32 → api_foundry_query_engine-0.8.34}/api_foundry_query_engine/connectors/oracle_connector.py +0 -0
  27. {api_foundry_query_engine-0.8.32 → api_foundry_query_engine-0.8.34}/api_foundry_query_engine/dao/dao.py +0 -0
  28. {api_foundry_query_engine-0.8.32 → api_foundry_query_engine-0.8.34}/api_foundry_query_engine/dao/sql_custom_query_handler.py +0 -0
  29. {api_foundry_query_engine-0.8.32 → api_foundry_query_engine-0.8.34}/api_foundry_query_engine/dao/sql_subselect_query_handler.py +0 -0
  30. {api_foundry_query_engine-0.8.32 → api_foundry_query_engine-0.8.34}/api_foundry_query_engine/lambda_handler.py +0 -0
  31. {api_foundry_query_engine-0.8.32 → api_foundry_query_engine-0.8.34}/api_foundry_query_engine/operation.py +0 -0
  32. {api_foundry_query_engine-0.8.32 → api_foundry_query_engine-0.8.34}/api_foundry_query_engine/services/service.py +0 -0
  33. {api_foundry_query_engine-0.8.32 → api_foundry_query_engine-0.8.34}/api_foundry_query_engine/services/transactional_service.py +0 -0
  34. {api_foundry_query_engine-0.8.32 → api_foundry_query_engine-0.8.34}/api_foundry_query_engine/utils/app_exception.py +0 -0
  35. {api_foundry_query_engine-0.8.32 → api_foundry_query_engine-0.8.34}/api_foundry_query_engine/utils/claims_check.py +0 -0
  36. {api_foundry_query_engine-0.8.32 → api_foundry_query_engine-0.8.34}/api_foundry_query_engine/utils/gateway_operation.py +0 -0
  37. {api_foundry_query_engine-0.8.32 → api_foundry_query_engine-0.8.34}/api_foundry_query_engine/utils/logger.py +0 -0
  38. {api_foundry_query_engine-0.8.32 → api_foundry_query_engine-0.8.34}/api_foundry_query_engine/utils/token_decoder.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: api-foundry-query-engine
3
- Version: 0.8.32
3
+ Version: 0.8.34
4
4
  Summary: The AWS lambda service handler use by the `api_foundry` project is a powerful tool designed to automate the deployment of REST APIs on AWS using Lambda services to access and interact with relational databases (RDBMS). This project leverages the OpenAPI specification to define and manage the APIs
5
5
  Project-URL: Documentation, https://github.com/DanRepik/api-foundry
6
6
  Project-URL: Source, https://github.com/DanRepik/api-foundry
@@ -46,26 +46,7 @@ class GatewayAdapter(Adapter):
46
46
  method = str(event.get("httpMethod", "")).upper()
47
47
  action = actions_map.get(method, "read")
48
48
 
49
- event_params = {}
50
-
51
- path_parameters = self._convert_parameters(event.get("pathParameters"))
52
- if path_parameters is not None:
53
- event_params.update(path_parameters)
54
-
55
- queryStringParameters = self._convert_parameters(
56
- event.get("queryStringParameters")
57
- )
58
- if queryStringParameters is not None:
59
- event_params.update(queryStringParameters)
60
-
61
- query_params, metadata_params = self.split_params(event_params)
62
-
63
- store_params = {}
64
- body = event.get("body")
65
- if body is not None and len(body) > 0:
66
- store_params = json.loads(body)
67
-
68
- # Extract JWT claims from API Gateway event
49
+ # Extract JWT claims early for batch operations
69
50
  claims = event.get("requestContext", {}).get("authorizer", {})
70
51
 
71
52
  # Handle different authorizer types
@@ -89,6 +70,37 @@ class GatewayAdapter(Adapter):
89
70
  else:
90
71
  # Non-dict authorizer context
91
72
  claims = {}
73
+
74
+ # Handle batch requests
75
+ if entity == "batch" and method == "POST":
76
+ body = event.get("body")
77
+ if body:
78
+ batch_request = json.loads(body)
79
+ return Operation(
80
+ entity="batch",
81
+ action="create",
82
+ store_params=batch_request,
83
+ claims=claims,
84
+ )
85
+
86
+ event_params = {}
87
+
88
+ path_parameters = self._convert_parameters(event.get("pathParameters"))
89
+ if path_parameters is not None:
90
+ event_params.update(path_parameters)
91
+
92
+ queryStringParameters = self._convert_parameters(
93
+ event.get("queryStringParameters")
94
+ )
95
+ if queryStringParameters is not None:
96
+ event_params.update(queryStringParameters)
97
+
98
+ query_params, metadata_params = self.split_params(event_params)
99
+
100
+ store_params = {}
101
+ body = event.get("body")
102
+ if body is not None and len(body) > 0:
103
+ store_params = json.loads(body)
92
104
  scope_str = claims.get("scope")
93
105
 
94
106
  # Enforce OAuth scopes (simulating API Gateway authorizer behavior)
@@ -42,13 +42,25 @@ class PostgresCursor(Cursor):
42
42
  return result
43
43
  except IntegrityError as err:
44
44
  # Handle integrity constraint violation (e.g., duplicate key)
45
- raise Exception(409, err.pgerror)
45
+ from api_foundry_query_engine.utils.app_exception import (
46
+ ApplicationException,
47
+ )
48
+
49
+ raise ApplicationException(409, err.pgerror)
46
50
  except ProgrammingError as err:
47
51
  # Handle programming errors (e.g., syntax error in SQL)
48
- raise Exception(400, err.pgerror)
52
+ from api_foundry_query_engine.utils.app_exception import (
53
+ ApplicationException,
54
+ )
55
+
56
+ raise ApplicationException(400, err.pgerror)
49
57
  except Error as err:
50
58
  # Handle other database errors
51
- raise Exception(500, err.pgerror)
59
+ from api_foundry_query_engine.utils.app_exception import (
60
+ ApplicationException,
61
+ )
62
+
63
+ raise ApplicationException(500, err.pgerror)
52
64
 
53
65
  def close(self):
54
66
  self.__cursor.close()
@@ -68,6 +80,9 @@ class PostgresConnection(Connection):
68
80
  def commit(self):
69
81
  self.__connection.commit()
70
82
 
83
+ def rollback(self):
84
+ self.__connection.rollback()
85
+
71
86
  def get_connection(self):
72
87
  """
73
88
  Get a connection to the PostgreSQL database.
@@ -0,0 +1,295 @@
1
+ """
2
+ Batch Operation Handler
3
+
4
+ Orchestrates execution of multiple operations with dependency resolution,
5
+ reference substitution, and transaction management.
6
+ """
7
+
8
+ from typing import Dict, Any, List, Optional
9
+ from api_foundry_query_engine.operation import Operation
10
+ from api_foundry_query_engine.dao.operation_dao import OperationDAO
11
+ from api_foundry_query_engine.utils.dependency_resolver import DependencyResolver
12
+ from api_foundry_query_engine.utils.reference_resolver import ReferenceResolver
13
+ from api_foundry_query_engine.utils.app_exception import ApplicationException
14
+ from api_foundry_query_engine.utils.logger import logger
15
+
16
+ log = logger(__name__)
17
+
18
+
19
+ class BatchOperationHandler:
20
+ """Handles execution of batch operations with dependencies."""
21
+
22
+ def __init__(self, batch_request: Dict[str, Any], connection, engine: str):
23
+ """
24
+ Initialize the batch operation handler.
25
+
26
+ Args:
27
+ batch_request: Batch request with 'operations' and 'options'
28
+ connection: Database connection for executing operations
29
+ engine: Database engine type (postgres, mysql, etc.)
30
+ """
31
+ self.operations = batch_request.get("operations", [])
32
+ self.options = batch_request.get("options", {})
33
+ self.connection = connection
34
+ self.engine = engine
35
+ self.results: Dict[str, Dict[str, Any]] = {}
36
+ self.failed_operations: List[str] = []
37
+
38
+ # Validate batch request
39
+ self._validate_batch_request()
40
+
41
+ # Resolve execution order
42
+ self.resolver = DependencyResolver(self.operations)
43
+ self.execution_order = self.resolver.get_execution_order()
44
+
45
+ def _validate_batch_request(self):
46
+ """Validate the batch request structure."""
47
+ if not self.operations:
48
+ raise ApplicationException(
49
+ 400, "Batch request must contain at least one operation"
50
+ )
51
+
52
+ if len(self.operations) > 100:
53
+ raise ApplicationException(
54
+ 400,
55
+ f"Batch size exceeds maximum (100). "
56
+ f"Requested: {len(self.operations)}",
57
+ )
58
+
59
+ # Auto-generate IDs for operations that don't have them
60
+ seen_ids = set()
61
+ for i, op in enumerate(self.operations):
62
+ # Auto-generate ID if not provided
63
+ if "id" not in op or not op["id"]:
64
+ op["id"] = f"op_{i}"
65
+
66
+ # Check for duplicate IDs
67
+ if op["id"] in seen_ids:
68
+ raise ApplicationException(
69
+ 400,
70
+ f"Duplicate operation ID '{op['id']}' found",
71
+ )
72
+ seen_ids.add(op["id"])
73
+
74
+ # Validate required fields
75
+ if "entity" not in op:
76
+ raise ApplicationException(
77
+ 400,
78
+ f"Operation '{op['id']}' missing required field 'entity'",
79
+ )
80
+ if "action" not in op:
81
+ raise ApplicationException(
82
+ 400,
83
+ f"Operation '{op['id']}' missing required field 'action'",
84
+ )
85
+ if op["action"] not in ["create", "read", "update", "delete"]:
86
+ raise ApplicationException(
87
+ 400,
88
+ f"Operation '{op['id']}' has invalid action " f"'{op['action']}'",
89
+ )
90
+
91
+ def execute(self) -> Dict[str, Any]:
92
+ """
93
+ Execute all operations in dependency order.
94
+
95
+ Returns:
96
+ Dictionary with 'success', 'results', and optionally 'errors'
97
+
98
+ Raises:
99
+ ApplicationException: On validation or execution errors
100
+ """
101
+ atomic = self.options.get("atomic", True)
102
+ continue_on_error = self.options.get("continueOnError", False)
103
+
104
+ log.info(
105
+ "Executing batch with %d operations (atomic=%s, continue=%s)",
106
+ len(self.operations),
107
+ atomic,
108
+ continue_on_error,
109
+ )
110
+
111
+ try:
112
+ for op_id in self.execution_order:
113
+ # Check if we should skip this operation
114
+ if self._should_skip_operation(op_id):
115
+ self.results[op_id] = {
116
+ "status": "skipped",
117
+ "reason": "Dependency failed",
118
+ }
119
+ continue
120
+
121
+ # Execute the operation
122
+ try:
123
+ result = self._execute_operation(op_id)
124
+ # Unwrap single-item lists for easier reference access
125
+ # e.g., $ref:op_0.invoice_id instead of $ref:op_0.0.invoice_id
126
+ if isinstance(result, list) and len(result) == 1:
127
+ result = result[0]
128
+ self.results[op_id] = {"status": "completed", "data": result}
129
+ log.info("Operation '%s' completed successfully", op_id)
130
+
131
+ except ApplicationException as e:
132
+ log.error("Operation '%s' failed: %s", op_id, e.message)
133
+ self.results[op_id] = {
134
+ "status": "failed",
135
+ "error": e.message,
136
+ "statusCode": e.status_code,
137
+ }
138
+ self.failed_operations.append(op_id)
139
+
140
+ # Rollback the failed transaction
141
+ # In non-atomic mode, this allows next operation to proceed
142
+ if not atomic:
143
+ self.connection.rollback()
144
+ log.info("Failed operation rolled back (non-atomic mode)")
145
+
146
+ # Stop on error if not continuing
147
+ if not continue_on_error:
148
+ if atomic:
149
+ self.connection.rollback()
150
+ log.info("Transaction rolled back")
151
+ raise ApplicationException(
152
+ 400,
153
+ f"Batch failed at operation '{op_id}': {e.message}",
154
+ ) from e
155
+
156
+ # Commit each operation if not atomic and it succeeded
157
+ if not atomic and op_id not in self.failed_operations:
158
+ self.connection.commit()
159
+ log.info("Operation '%s' committed (non-atomic mode)", op_id)
160
+
161
+ # Commit transaction if atomic and all succeeded
162
+ if atomic and not self.failed_operations:
163
+ self.connection.commit()
164
+ log.info("Batch transaction committed successfully")
165
+ elif atomic and self.failed_operations and continue_on_error:
166
+ self.connection.rollback()
167
+ log.info("Batch had failures, transaction rolled back")
168
+
169
+ # Build response
170
+ success = len(self.failed_operations) == 0
171
+ response = {"success": success, "results": self.results}
172
+
173
+ if self.failed_operations:
174
+ response["failedOperations"] = self.failed_operations
175
+
176
+ return response
177
+
178
+ except Exception as e:
179
+ # Rollback on any unexpected error
180
+ if atomic:
181
+ self.connection.rollback()
182
+ log.error("Batch failed, transaction rolled back")
183
+ raise e
184
+
185
+ def _should_skip_operation(self, op_id: str) -> bool:
186
+ """
187
+ Determine if operation should be skipped due to failed dependencies.
188
+
189
+ Args:
190
+ op_id: Operation ID to check
191
+
192
+ Returns:
193
+ True if operation should be skipped
194
+ """
195
+ op = next(o for o in self.operations if o["id"] == op_id)
196
+ depends_on = op.get("depends_on", [])
197
+
198
+ for dep_id in depends_on:
199
+ if dep_id in self.failed_operations:
200
+ log.info(
201
+ "Skipping operation '%s' due to failed dependency '%s'",
202
+ op_id,
203
+ dep_id,
204
+ )
205
+ return True
206
+ if dep_id not in self.results:
207
+ log.warning(
208
+ "Operation '%s' depends on '%s' which hasn't executed yet",
209
+ op_id,
210
+ dep_id,
211
+ )
212
+ return True
213
+ if self.results[dep_id].get("status") != "completed":
214
+ log.info(
215
+ "Skipping operation '%s' due to incomplete dependency '%s'",
216
+ op_id,
217
+ dep_id,
218
+ )
219
+ return True
220
+
221
+ return False
222
+
223
+ def _execute_operation(self, op_id: str) -> Any:
224
+ """
225
+ Execute a single operation.
226
+
227
+ Args:
228
+ op_id: Operation ID to execute
229
+
230
+ Returns:
231
+ Operation result data
232
+
233
+ Raises:
234
+ ApplicationException: On execution error
235
+ """
236
+ # Get operation definition
237
+ op_def = next(o for o in self.operations if o["id"] == op_id)
238
+
239
+ # Resolve references in parameters
240
+ ref_resolver = ReferenceResolver(self.results)
241
+ query_params = ref_resolver.resolve_parameters(
242
+ op_def.get("query_params", {}), op_id
243
+ )
244
+ store_params = ref_resolver.resolve_parameters(
245
+ op_def.get("store_params", {}), op_id
246
+ )
247
+ metadata_params = ref_resolver.resolve_parameters(
248
+ op_def.get("metadata_params", {}), op_id
249
+ )
250
+
251
+ # Get claims from operation or use empty dict
252
+ claims = op_def.get("claims", {})
253
+
254
+ # Create Operation object
255
+ operation = Operation(
256
+ entity=op_def["entity"],
257
+ action=op_def["action"],
258
+ query_params=query_params,
259
+ store_params=store_params,
260
+ metadata_params=metadata_params,
261
+ claims=claims,
262
+ )
263
+
264
+ log.debug(
265
+ "Executing operation '%s': %s %s",
266
+ op_id,
267
+ operation.action,
268
+ operation.entity,
269
+ )
270
+
271
+ # Execute through OperationDAO
272
+ dao = OperationDAO(operation, self.engine)
273
+ result = dao.execute(self.connection)
274
+
275
+ return result
276
+
277
+ def get_operation_summary(self) -> Dict[str, int]:
278
+ """
279
+ Get summary statistics of batch execution.
280
+
281
+ Returns:
282
+ Dictionary with counts of completed, failed, skipped operations
283
+ """
284
+ summary = {"total": len(self.operations), "completed": 0, "failed": 0}
285
+
286
+ for op_result in self.results.values():
287
+ status = op_result.get("status")
288
+ if status == "completed":
289
+ summary["completed"] += 1
290
+ elif status == "failed":
291
+ summary["failed"] += 1
292
+ elif status == "skipped":
293
+ summary["skipped"] = summary.get("skipped", 0) + 1
294
+
295
+ return summary
@@ -16,6 +16,9 @@ from api_foundry_query_engine.dao.sql_subselect_query_handler import (
16
16
  from api_foundry_query_engine.dao.sql_update_query_handler import (
17
17
  SQLUpdateSchemaQueryHandler,
18
18
  )
19
+ from api_foundry_query_engine.dao.sql_restore_query_handler import (
20
+ SQLRestoreSchemaQueryHandler,
21
+ )
19
22
  from api_foundry_query_engine.utils.app_exception import ApplicationException
20
23
  from api_foundry_query_engine.dao.dao import DAO
21
24
  from api_foundry_query_engine.connectors.connection import Cursor
@@ -80,6 +83,10 @@ class OperationDAO(DAO):
80
83
  self._query_handler = SQLDeleteSchemaQueryHandler(
81
84
  self.operation, schema_object, self.engine
82
85
  )
86
+ elif self.operation.action == "restore":
87
+ self._query_handler = SQLRestoreSchemaQueryHandler(
88
+ self.operation, schema_object, self.engine
89
+ )
83
90
  else:
84
91
  raise ApplicationException(
85
92
  400, f"Invalid operation action: {self.operation.action}"
@@ -101,6 +108,21 @@ class OperationDAO(DAO):
101
108
 
102
109
  # Use self.operation if operation is not provided
103
110
  op = operation if operation is not None else self.operation
111
+
112
+ # Check if this is a batch operation
113
+ if op.entity == "batch" and op.action == "create":
114
+ from api_foundry_query_engine.dao.batch_operation_handler import (
115
+ BatchOperationHandler,
116
+ )
117
+
118
+ # Extract batch request from store_params
119
+ batch_request = op.store_params
120
+
121
+ # Execute batch
122
+ handler = BatchOperationHandler(batch_request, connector, self.engine)
123
+ return handler.execute()
124
+
125
+ # Standard operation handling
104
126
  # Assume connector has a 'cursor()' method to get a Cursor
105
127
  cursor = connector.cursor()
106
128
 
@@ -110,7 +132,7 @@ class OperationDAO(DAO):
110
132
  if op.metadata_params.get("count", False):
111
133
  return result[0]
112
134
  self.__fetch_many(result, cursor)
113
- elif op.action in ["update", "delete"] and len(result) == 0:
135
+ elif op.action in ["update", "delete", "restore"] and len(result) == 0:
114
136
  raise ApplicationException(400, "No records were modified")
115
137
 
116
138
  return result
@@ -56,6 +56,56 @@ class SQLDeleteSchemaQueryHandler(SQLSchemaQueryHandler):
56
56
 
57
57
  return False
58
58
 
59
+ def _has_soft_delete_fields(self) -> bool:
60
+ """Check if schema object supports soft delete."""
61
+ return self.schema_object.has_soft_delete_support()
62
+
63
+ def _get_soft_delete_update_values(self) -> str:
64
+ """Generate SET clause for soft delete operation."""
65
+ self.store_placeholders = {}
66
+ columns = []
67
+
68
+ # Process soft delete properties
69
+ soft_delete_props = self.schema_object.get_soft_delete_properties()
70
+
71
+ for _, prop in soft_delete_props.items():
72
+ strategy = prop.get_soft_delete_strategy()
73
+ config = prop.get_soft_delete_config()
74
+ column_name = prop.column_name
75
+
76
+ if strategy == "null_check":
77
+ # Set timestamp fields to CURRENT_TIMESTAMP
78
+ if prop.api_type in ["date-time", "datetime"]:
79
+ columns.append(f"{column_name} = CURRENT_TIMESTAMP")
80
+ else:
81
+ columns.append(f"{column_name} = 'deleted'")
82
+ elif strategy == "boolean_flag":
83
+ inactive_value = not config.get("active_value", True)
84
+ value_str = str(inactive_value).lower()
85
+ columns.append(f"{column_name} = {value_str}")
86
+ elif strategy == "exclude_values":
87
+ delete_value = config.get("delete_value")
88
+ if delete_value:
89
+ if isinstance(delete_value, str):
90
+ columns.append(f"{column_name} = '{delete_value}'")
91
+ else:
92
+ columns.append(f"{column_name} = {delete_value}")
93
+
94
+ # Process audit fields
95
+ audit_props = self.schema_object.get_soft_delete_audit_properties()
96
+ claims = self.operation.claims or {}
97
+
98
+ for _, prop in audit_props.items():
99
+ config = prop.get_soft_delete_config()
100
+ action = config.get("action", "")
101
+
102
+ if action == "delete" and "sub" in claims:
103
+ placeholder_key = f"audit_{prop.api_name}"
104
+ columns.append(f"{prop.column_name} = %({placeholder_key})s")
105
+ self.store_placeholders[placeholder_key] = claims["sub"]
106
+
107
+ return " SET " + ", ".join(columns) if columns else ""
108
+
59
109
  @property
60
110
  def sql(self) -> str:
61
111
  if not self.check_permission():
@@ -81,7 +131,16 @@ class SQLDeleteSchemaQueryHandler(SQLSchemaQueryHandler):
81
131
  + f"property: {concurrency_property.api_name}",
82
132
  )
83
133
 
84
- return (
85
- f"DELETE FROM {self.table_expression}{self.search_condition} "
86
- + f"RETURNING {self.select_list}"
87
- )
134
+ # Use soft delete if supported, otherwise hard delete
135
+ if self._has_soft_delete_fields():
136
+ update_clause = self._get_soft_delete_update_values()
137
+ return (
138
+ f"UPDATE {self.table_expression}{update_clause}"
139
+ + f"{self.search_condition} RETURNING {self.select_list}"
140
+ )
141
+ else:
142
+ # Fall back to hard delete for tables without soft delete support
143
+ return (
144
+ f"DELETE FROM {self.table_expression}{self.search_condition} "
145
+ + f"RETURNING {self.select_list}"
146
+ )
@@ -82,6 +82,15 @@ class SQLInsertSchemaQueryHandler(SQLSchemaQueryHandler):
82
82
 
83
83
  import json
84
84
 
85
+ # First, validate that user is not trying to set injected properties
86
+ for property_name, property in self.schema_object.properties.items():
87
+ if property.inject_value and property_name in self.operation.store_params:
88
+ raise ApplicationException(
89
+ 403,
90
+ f"Property '{property_name}' is auto-injected and "
91
+ + "cannot be set manually",
92
+ )
93
+
85
94
  for name, value in self.operation.store_params.items():
86
95
  parts = name.split(".")
87
96
 
@@ -115,6 +124,25 @@ class SQLInsertSchemaQueryHandler(SQLSchemaQueryHandler):
115
124
  property.api_name
116
125
  ] = property.convert_to_db_value(value)
117
126
 
127
+ # Inject values from claims/timestamps/etc for properties with
128
+ # x-af-inject-value on CREATE
129
+ for property_name, property in self.schema_object.properties.items():
130
+ if property.inject_value and "create" in property.inject_on:
131
+ injected_value = self.extract_injected_value(property.inject_value)
132
+ if injected_value is not None:
133
+ placeholder_key = f"__inject_{property_name}"
134
+ columns.append(property.column_name)
135
+ placeholders.append(self.placeholder(property, placeholder_key))
136
+ self.store_placeholders[
137
+ placeholder_key
138
+ ] = property.convert_to_db_value(injected_value)
139
+ elif property.required:
140
+ raise ApplicationException(
141
+ 400,
142
+ f"Required injected property '{property_name}' "
143
+ + f"could not be populated from '{property.inject_value}'",
144
+ )
145
+
118
146
  if self.key_property:
119
147
  if self.key_property.key_type == "sequence":
120
148
  columns.append(self.key_property.column_name)