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.
- {api_foundry_query_engine-0.8.32 → api_foundry_query_engine-0.8.34}/PKG-INFO +1 -1
- {api_foundry_query_engine-0.8.32 → api_foundry_query_engine-0.8.34}/api_foundry_query_engine/adapters/gateway_adapter.py +32 -20
- {api_foundry_query_engine-0.8.32 → api_foundry_query_engine-0.8.34}/api_foundry_query_engine/connectors/postgres_connection.py +18 -3
- api_foundry_query_engine-0.8.34/api_foundry_query_engine/dao/batch_operation_handler.py +295 -0
- {api_foundry_query_engine-0.8.32 → api_foundry_query_engine-0.8.34}/api_foundry_query_engine/dao/operation_dao.py +23 -1
- {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
- {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
- {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
- api_foundry_query_engine-0.8.34/api_foundry_query_engine/dao/sql_restore_query_handler.py +194 -0
- {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
- api_foundry_query_engine-0.8.34/api_foundry_query_engine/dao/sql_update_query_handler.py +197 -0
- {api_foundry_query_engine-0.8.32 → api_foundry_query_engine-0.8.34}/api_foundry_query_engine/utils/api_model.py +58 -4
- api_foundry_query_engine-0.8.34/api_foundry_query_engine/utils/dependency_resolver.py +157 -0
- api_foundry_query_engine-0.8.34/api_foundry_query_engine/utils/reference_resolver.py +222 -0
- {api_foundry_query_engine-0.8.32 → api_foundry_query_engine-0.8.34}/pyproject.toml +1 -1
- api_foundry_query_engine-0.8.32/api_foundry_query_engine/dao/sql_update_query_handler.py +0 -81
- {api_foundry_query_engine-0.8.32 → api_foundry_query_engine-0.8.34}/.gitignore +0 -0
- {api_foundry_query_engine-0.8.32 → api_foundry_query_engine-0.8.34}/LICENSE +0 -0
- {api_foundry_query_engine-0.8.32 → api_foundry_query_engine-0.8.34}/api_foundry_query_engine/.pre-commit-config.yaml +0 -0
- {api_foundry_query_engine-0.8.32 → api_foundry_query_engine-0.8.34}/api_foundry_query_engine/__init__.py +0 -0
- {api_foundry_query_engine-0.8.32 → api_foundry_query_engine-0.8.34}/api_foundry_query_engine/adapters/adapter.py +0 -0
- {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
- {api_foundry_query_engine-0.8.32 → api_foundry_query_engine-0.8.34}/api_foundry_query_engine/adapters/security_adapter.py +0 -0
- {api_foundry_query_engine-0.8.32 → api_foundry_query_engine-0.8.34}/api_foundry_query_engine/connectors/connection.py +0 -0
- {api_foundry_query_engine-0.8.32 → api_foundry_query_engine-0.8.34}/api_foundry_query_engine/connectors/connection_factory.py +0 -0
- {api_foundry_query_engine-0.8.32 → api_foundry_query_engine-0.8.34}/api_foundry_query_engine/connectors/oracle_connector.py +0 -0
- {api_foundry_query_engine-0.8.32 → api_foundry_query_engine-0.8.34}/api_foundry_query_engine/dao/dao.py +0 -0
- {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
- {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
- {api_foundry_query_engine-0.8.32 → api_foundry_query_engine-0.8.34}/api_foundry_query_engine/lambda_handler.py +0 -0
- {api_foundry_query_engine-0.8.32 → api_foundry_query_engine-0.8.34}/api_foundry_query_engine/operation.py +0 -0
- {api_foundry_query_engine-0.8.32 → api_foundry_query_engine-0.8.34}/api_foundry_query_engine/services/service.py +0 -0
- {api_foundry_query_engine-0.8.32 → api_foundry_query_engine-0.8.34}/api_foundry_query_engine/services/transactional_service.py +0 -0
- {api_foundry_query_engine-0.8.32 → api_foundry_query_engine-0.8.34}/api_foundry_query_engine/utils/app_exception.py +0 -0
- {api_foundry_query_engine-0.8.32 → api_foundry_query_engine-0.8.34}/api_foundry_query_engine/utils/claims_check.py +0 -0
- {api_foundry_query_engine-0.8.32 → api_foundry_query_engine-0.8.34}/api_foundry_query_engine/utils/gateway_operation.py +0 -0
- {api_foundry_query_engine-0.8.32 → api_foundry_query_engine-0.8.34}/api_foundry_query_engine/utils/logger.py +0 -0
- {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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
85
|
-
|
|
86
|
-
|
|
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)
|