api-foundry-query-engine 0.8.39__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.
- api_foundry_query_engine/.pre-commit-config.yaml +22 -0
- api_foundry_query_engine/__init__.py +1 -0
- api_foundry_query_engine/adapters/adapter.py +73 -0
- api_foundry_query_engine/adapters/case_change_adapter.py +79 -0
- api_foundry_query_engine/adapters/gateway_adapter.py +191 -0
- api_foundry_query_engine/adapters/security_adapter.py +106 -0
- api_foundry_query_engine/connectors/connection.py +32 -0
- api_foundry_query_engine/connectors/connection_factory.py +115 -0
- api_foundry_query_engine/connectors/oracle_connector.py +29 -0
- api_foundry_query_engine/connectors/postgres_connection.py +142 -0
- api_foundry_query_engine/dao/batch_operation_handler.py +295 -0
- api_foundry_query_engine/dao/dao.py +23 -0
- api_foundry_query_engine/dao/operation_dao.py +186 -0
- api_foundry_query_engine/dao/sql_custom_query_handler.py +68 -0
- api_foundry_query_engine/dao/sql_delete_query_handler.py +146 -0
- api_foundry_query_engine/dao/sql_insert_query_handler.py +187 -0
- api_foundry_query_engine/dao/sql_query_handler.py +713 -0
- api_foundry_query_engine/dao/sql_restore_query_handler.py +195 -0
- api_foundry_query_engine/dao/sql_select_query_handler.py +433 -0
- api_foundry_query_engine/dao/sql_subselect_query_handler.py +66 -0
- api_foundry_query_engine/dao/sql_update_query_handler.py +198 -0
- api_foundry_query_engine/lambda_handler.py +63 -0
- api_foundry_query_engine/operation.py +104 -0
- api_foundry_query_engine/services/service.py +75 -0
- api_foundry_query_engine/services/transactional_service.py +52 -0
- api_foundry_query_engine/utils/api_model.py +380 -0
- api_foundry_query_engine/utils/app_exception.py +22 -0
- api_foundry_query_engine/utils/claims_check.py +471 -0
- api_foundry_query_engine/utils/dependency_resolver.py +157 -0
- api_foundry_query_engine/utils/gateway_operation.py +279 -0
- api_foundry_query_engine/utils/logger.py +60 -0
- api_foundry_query_engine/utils/reference_resolver.py +222 -0
- api_foundry_query_engine/utils/token_decoder.py +624 -0
- api_foundry_query_engine-0.8.39.dist-info/METADATA +21 -0
- api_foundry_query_engine-0.8.39.dist-info/RECORD +37 -0
- api_foundry_query_engine-0.8.39.dist-info/WHEEL +4 -0
- api_foundry_query_engine-0.8.39.dist-info/licenses/LICENSE +201 -0
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
import re
|
|
2
|
+
from typing import Match
|
|
3
|
+
from api_foundry_query_engine.dao.sql_query_handler import SQLSchemaQueryHandler
|
|
4
|
+
from api_foundry_query_engine.operation import Operation
|
|
5
|
+
from api_foundry_query_engine.utils.app_exception import ApplicationException
|
|
6
|
+
from api_foundry_query_engine.utils.api_model import SchemaObject
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class SQLUpdateSchemaQueryHandler(SQLSchemaQueryHandler):
|
|
10
|
+
def __init__(
|
|
11
|
+
self, operation: Operation, schema_object: SchemaObject, engine: str
|
|
12
|
+
) -> None:
|
|
13
|
+
super().__init__(operation, schema_object, engine)
|
|
14
|
+
|
|
15
|
+
def _template_where(self, expr: str) -> str:
|
|
16
|
+
"""Template substitution for WHERE clause expressions with claim values."""
|
|
17
|
+
if not expr:
|
|
18
|
+
return expr
|
|
19
|
+
|
|
20
|
+
def _quote(val: object) -> str:
|
|
21
|
+
if val is None:
|
|
22
|
+
return "NULL"
|
|
23
|
+
if isinstance(val, (int, float)):
|
|
24
|
+
return str(val)
|
|
25
|
+
s = str(val).replace("'", "''")
|
|
26
|
+
return f"'{s}'"
|
|
27
|
+
|
|
28
|
+
def _replace(m: Match[str]) -> str:
|
|
29
|
+
key = m.group(1)
|
|
30
|
+
claims = self.operation.claims or {}
|
|
31
|
+
return _quote(claims.get(key))
|
|
32
|
+
|
|
33
|
+
return re.sub(r"\$\{claims\.([A-Za-z0-9_]+)\}", _replace, expr)
|
|
34
|
+
|
|
35
|
+
def _row_where_clause(self) -> str:
|
|
36
|
+
"""
|
|
37
|
+
Generate row-level WHERE clause based on write permissions.
|
|
38
|
+
Only applies to UPDATE operations, not CREATE.
|
|
39
|
+
"""
|
|
40
|
+
perms = getattr(self.schema_object, "permissions", None) or {}
|
|
41
|
+
# provider-first default
|
|
42
|
+
if "default" in perms:
|
|
43
|
+
provider = perms.get("default", {}) or {}
|
|
44
|
+
write_map = provider.get("write", {}) or {}
|
|
45
|
+
role_permissions = perms.get("default", {})
|
|
46
|
+
else:
|
|
47
|
+
# legacy role-first -> synthesize write map
|
|
48
|
+
write_map = {}
|
|
49
|
+
role_permissions = perms
|
|
50
|
+
for role, role_perms in perms.items():
|
|
51
|
+
if isinstance(role_perms, dict):
|
|
52
|
+
write_map[role] = role_perms.get("write")
|
|
53
|
+
|
|
54
|
+
filters = []
|
|
55
|
+
for role in self.operation.roles or []:
|
|
56
|
+
# Check for role-level WHERE clause (hybrid approach)
|
|
57
|
+
role_where = None
|
|
58
|
+
if isinstance(role_permissions.get(role), dict):
|
|
59
|
+
role_where = role_permissions[role].get("where")
|
|
60
|
+
|
|
61
|
+
# Check for operation-level WHERE clause
|
|
62
|
+
operation_where = None
|
|
63
|
+
rule = write_map.get(role)
|
|
64
|
+
if isinstance(rule, dict):
|
|
65
|
+
operation_where = rule.get("where")
|
|
66
|
+
|
|
67
|
+
# Operation-level takes precedence, fallback to role-level
|
|
68
|
+
where_clause = operation_where if operation_where else role_where
|
|
69
|
+
|
|
70
|
+
if isinstance(where_clause, str) and where_clause.strip():
|
|
71
|
+
filters.append(self._template_where(where_clause))
|
|
72
|
+
|
|
73
|
+
if not filters:
|
|
74
|
+
return ""
|
|
75
|
+
return "(" + ") OR (".join(filters) + ")"
|
|
76
|
+
|
|
77
|
+
@property
|
|
78
|
+
def sql(self) -> str:
|
|
79
|
+
concurrency_property = self.schema_object.concurrency_property
|
|
80
|
+
if not concurrency_property:
|
|
81
|
+
return (
|
|
82
|
+
f"UPDATE {self.table_expression}{self.update_values}"
|
|
83
|
+
+ f"{self.search_condition} RETURNING {self.select_list}"
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
if not self.operation.query_params.get(concurrency_property.api_name):
|
|
87
|
+
raise ApplicationException(
|
|
88
|
+
400,
|
|
89
|
+
"Missing required concurrency management property. "
|
|
90
|
+
+ f"schema_object: {self.schema_object.api_name}, "
|
|
91
|
+
+ f"property: {concurrency_property.api_name}",
|
|
92
|
+
)
|
|
93
|
+
if self.operation.store_params.get(concurrency_property.api_name):
|
|
94
|
+
raise ApplicationException(
|
|
95
|
+
400,
|
|
96
|
+
"For updating concurrency managed schema objects the current version "
|
|
97
|
+
+ " may not be supplied as a storage parameter. "
|
|
98
|
+
+ f"schema_object: {self.schema_object.api_name}, "
|
|
99
|
+
+ f"property: {concurrency_property.api_name}",
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
return f"UPDATE {self.table_expression}{self.update_values}, {concurrency_property.column_name} = {self.concurrency_generator(concurrency_property)} {self.search_condition} RETURNING {self.select_list}" # noqa E501
|
|
103
|
+
|
|
104
|
+
@property
|
|
105
|
+
def update_values(self) -> str:
|
|
106
|
+
allowed_property_names = self.check_permissions(
|
|
107
|
+
"write", self.schema_object.permissions, self.schema_object.properties
|
|
108
|
+
)
|
|
109
|
+
allowed_properties = {
|
|
110
|
+
k: v
|
|
111
|
+
for k, v in self.schema_object.properties.items()
|
|
112
|
+
if k in allowed_property_names
|
|
113
|
+
}
|
|
114
|
+
self.store_placeholders = {}
|
|
115
|
+
columns = []
|
|
116
|
+
invalid_columns = []
|
|
117
|
+
|
|
118
|
+
import json
|
|
119
|
+
|
|
120
|
+
# First, validate that user is not trying to set injected properties
|
|
121
|
+
for property_name, property in self.schema_object.properties.items():
|
|
122
|
+
if property.inject_value and property_name in self.operation.store_params:
|
|
123
|
+
raise ApplicationException(
|
|
124
|
+
403,
|
|
125
|
+
f"Property '{property_name}' is auto-injected and "
|
|
126
|
+
+ "cannot be set manually",
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
for name, value in self.operation.store_params.items():
|
|
130
|
+
property = allowed_properties.get(name, None)
|
|
131
|
+
if property is None:
|
|
132
|
+
invalid_columns.append(name)
|
|
133
|
+
continue
|
|
134
|
+
|
|
135
|
+
placeholder = (
|
|
136
|
+
str(property.api_name) if property.api_name is not None else name
|
|
137
|
+
)
|
|
138
|
+
column_name = property.column_name
|
|
139
|
+
|
|
140
|
+
columns.append(f"{column_name} = {self.placeholder(property, placeholder)}")
|
|
141
|
+
# Serialize embedded objects to JSON
|
|
142
|
+
if property.api_type == "object":
|
|
143
|
+
self.store_placeholders[placeholder] = json.dumps(value)
|
|
144
|
+
else:
|
|
145
|
+
self.store_placeholders[placeholder] = property.convert_to_db_value(
|
|
146
|
+
value
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
# Inject values from claims/timestamps/etc for properties with
|
|
150
|
+
# x-af-inject-value on UPDATE
|
|
151
|
+
for property_name, property in self.schema_object.properties.items():
|
|
152
|
+
if property.inject_value and "update" in property.inject_on:
|
|
153
|
+
injected_value = self.extract_injected_value(property.inject_value)
|
|
154
|
+
if injected_value is not None:
|
|
155
|
+
placeholder_key = f"__inject_{property_name}"
|
|
156
|
+
column_name = property.column_name
|
|
157
|
+
columns.append(
|
|
158
|
+
f"{column_name} = {self.placeholder(property, placeholder_key)}"
|
|
159
|
+
)
|
|
160
|
+
self.store_placeholders[
|
|
161
|
+
placeholder_key
|
|
162
|
+
] = property.convert_to_db_value(injected_value)
|
|
163
|
+
elif property.required:
|
|
164
|
+
raise ApplicationException(
|
|
165
|
+
400,
|
|
166
|
+
f"Required injected property '{property_name}' "
|
|
167
|
+
+ f"could not be populated from '{property.inject_value}'",
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
if invalid_columns:
|
|
171
|
+
raise ApplicationException(
|
|
172
|
+
403,
|
|
173
|
+
f"Subject does not have permission to update properties: "
|
|
174
|
+
f"{invalid_columns}",
|
|
175
|
+
)
|
|
176
|
+
return f" SET {', '.join(columns)}"
|
|
177
|
+
|
|
178
|
+
@property
|
|
179
|
+
def search_condition(self) -> str:
|
|
180
|
+
"""
|
|
181
|
+
Override to add permission-based WHERE clause for UPDATE operations.
|
|
182
|
+
"""
|
|
183
|
+
# Get the base search condition from parent class
|
|
184
|
+
base_condition = super().search_condition
|
|
185
|
+
|
|
186
|
+
# Get row-level permission filters
|
|
187
|
+
row_filter = self._row_where_clause()
|
|
188
|
+
|
|
189
|
+
# If we have both, combine them
|
|
190
|
+
if base_condition and row_filter:
|
|
191
|
+
# base_condition already has " WHERE ", so add AND
|
|
192
|
+
return f"{base_condition} AND ({row_filter})"
|
|
193
|
+
elif row_filter:
|
|
194
|
+
# Only row filter, add WHERE prefix
|
|
195
|
+
return f" WHERE ({row_filter})"
|
|
196
|
+
else:
|
|
197
|
+
# Only base condition or neither
|
|
198
|
+
return base_condition
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import logging
|
|
3
|
+
import os
|
|
4
|
+
from typing import Mapping, Any
|
|
5
|
+
|
|
6
|
+
from api_foundry_query_engine.utils.api_model import set_api_model
|
|
7
|
+
from api_foundry_query_engine.utils.app_exception import ApplicationException
|
|
8
|
+
from api_foundry_query_engine.adapters.gateway_adapter import GatewayAdapter
|
|
9
|
+
from api_foundry_query_engine.utils.token_decoder import token_decoder
|
|
10
|
+
from api_foundry_query_engine.utils.claims_check import claims_check
|
|
11
|
+
|
|
12
|
+
logging.basicConfig(level=os.getenv("LOG_LEVEL", "INFO"))
|
|
13
|
+
log = logging.getLogger(__name__)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class QueryEngine:
|
|
17
|
+
def __init__(self, config: Mapping[str, str]):
|
|
18
|
+
self.adapter = GatewayAdapter(config)
|
|
19
|
+
|
|
20
|
+
def handler(self, event) -> dict[str, Any]:
|
|
21
|
+
log.debug("event: %s", event)
|
|
22
|
+
try:
|
|
23
|
+
response = self.adapter.process_event(event)
|
|
24
|
+
|
|
25
|
+
# Ensure the response conforms to API Gateway requirements
|
|
26
|
+
return {
|
|
27
|
+
"isBase64Encoded": False,
|
|
28
|
+
"statusCode": 200,
|
|
29
|
+
"headers": {"Content-Type": "application/json"},
|
|
30
|
+
"body": json.dumps(response),
|
|
31
|
+
}
|
|
32
|
+
except ApplicationException as e:
|
|
33
|
+
log.error("exception: %s", e, exc_info=True)
|
|
34
|
+
return {
|
|
35
|
+
"isBase64Encoded": False,
|
|
36
|
+
"statusCode": e.status_code,
|
|
37
|
+
"headers": {"Content-Type": "application/json"},
|
|
38
|
+
"body": json.dumps({"message": "exception: %s" % e}),
|
|
39
|
+
}
|
|
40
|
+
except RuntimeError as e:
|
|
41
|
+
log.error("runtime error: %s", e, exc_info=True)
|
|
42
|
+
return {
|
|
43
|
+
"isBase64Encoded": False,
|
|
44
|
+
"statusCode": 500,
|
|
45
|
+
"headers": {"Content-Type": "application/json"},
|
|
46
|
+
"body": json.dumps({"message": f"runtime error: {e}"}),
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
@token_decoder()
|
|
51
|
+
@claims_check()
|
|
52
|
+
def handler(event, _):
|
|
53
|
+
if not hasattr(handler, "engine_config"):
|
|
54
|
+
log.info("Loading engine config from environment variables")
|
|
55
|
+
handler.engine_config = os.environ
|
|
56
|
+
log.info("engine_config: %s", handler.engine_config)
|
|
57
|
+
|
|
58
|
+
if not hasattr(handler, "query_engine"):
|
|
59
|
+
set_api_model(handler.engine_config)
|
|
60
|
+
log.info("Creating QueryEngine instance")
|
|
61
|
+
handler.query_engine = QueryEngine(handler.engine_config)
|
|
62
|
+
|
|
63
|
+
return handler.query_engine.handler(event)
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
from typing import Any, Dict, List, Optional
|
|
2
|
+
|
|
3
|
+
from api_foundry_query_engine.utils.logger import logger
|
|
4
|
+
|
|
5
|
+
logger = logger(__name__)
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class Operation:
|
|
9
|
+
"""
|
|
10
|
+
Represents an action to be performed on an entity.
|
|
11
|
+
|
|
12
|
+
The `Operation` class encapsulates the details required
|
|
13
|
+
to execute an operation on a given entity, including
|
|
14
|
+
query parameters for selecting records, parameters for
|
|
15
|
+
storing or updating values, metadata for operational
|
|
16
|
+
instructions, and roles defining the contexts in which
|
|
17
|
+
the operation can be performed.
|
|
18
|
+
|
|
19
|
+
Attributes:
|
|
20
|
+
entity (str): The name of the resource or object being
|
|
21
|
+
targeted (e.g., "User", "Order").
|
|
22
|
+
action (str): The type of action being performed
|
|
23
|
+
(e.g., "create", "read", "update", "delete").
|
|
24
|
+
query_params (dict): Parameters used to filter or select
|
|
25
|
+
the records affected by the operation.
|
|
26
|
+
store_params (dict): Parameters that define the values to
|
|
27
|
+
be stored or updated for the selected records.
|
|
28
|
+
metadata_params (dict): Additional instructions for the
|
|
29
|
+
operation, such as sorting or pagination.
|
|
30
|
+
roles (dict): Defines the roles under which the operation
|
|
31
|
+
is allowed to be performed.
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
def __init__(
|
|
35
|
+
self,
|
|
36
|
+
*,
|
|
37
|
+
entity: str,
|
|
38
|
+
action: str,
|
|
39
|
+
query_params: Optional[Dict[str, Any]] = None,
|
|
40
|
+
store_params: Optional[Dict[str, Any]] = None,
|
|
41
|
+
metadata_params: Optional[Dict[str, Any]] = None,
|
|
42
|
+
claims: Optional[Dict[str, Any]] = None,
|
|
43
|
+
):
|
|
44
|
+
"""
|
|
45
|
+
Initializes the Operation instance.
|
|
46
|
+
|
|
47
|
+
Args:
|
|
48
|
+
entity (str): The name of the entity to perform the operation on.
|
|
49
|
+
action (str): The action to perform on the entity.
|
|
50
|
+
query_params (dict, optional): Parameters for selecting
|
|
51
|
+
affected records (default: {}).
|
|
52
|
+
store_params (dict, optional): Parameters defining new or
|
|
53
|
+
updated values (default: {}).
|
|
54
|
+
metadata_params (dict, optional): Metadata for the
|
|
55
|
+
operation, such as sorting or pagination (default: {}).
|
|
56
|
+
roles (dict, optional): Defines the roles allowed to
|
|
57
|
+
perform the operation (default: {}).
|
|
58
|
+
"""
|
|
59
|
+
# The target entity for the operation (e.g., "User", "Order").
|
|
60
|
+
self.entity = entity
|
|
61
|
+
|
|
62
|
+
# The type of action to perform (e.g., "create",
|
|
63
|
+
# "read", "update", "delete").
|
|
64
|
+
self.action = action
|
|
65
|
+
|
|
66
|
+
# Query parameters to filter or identify the affected records.
|
|
67
|
+
self.query_params = query_params or {}
|
|
68
|
+
|
|
69
|
+
# Parameters defining the values to be stored or updated
|
|
70
|
+
# for the operation.
|
|
71
|
+
self.store_params = store_params or {}
|
|
72
|
+
|
|
73
|
+
# Metadata for operational instructions like sorting,
|
|
74
|
+
# limiting, or offsetting results.
|
|
75
|
+
self.metadata_params = metadata_params or {}
|
|
76
|
+
|
|
77
|
+
# Roles defining the context in which the operation is allowed.
|
|
78
|
+
self.claims = claims or {}
|
|
79
|
+
|
|
80
|
+
# Log the operation for debugging and audit purposes
|
|
81
|
+
logger.info(
|
|
82
|
+
"Operation created: entity=%s, action=%s, "
|
|
83
|
+
"query_params=%s, store_params=%s, "
|
|
84
|
+
"metadata_params=%s, claims=%s",
|
|
85
|
+
self.entity,
|
|
86
|
+
self.action,
|
|
87
|
+
self.query_params,
|
|
88
|
+
self.store_params,
|
|
89
|
+
self.metadata_params,
|
|
90
|
+
self.claims,
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
@property
|
|
94
|
+
def roles(self) -> List[str]:
|
|
95
|
+
"""Get the roles from claims."""
|
|
96
|
+
return self.claims.get("roles", []) if self.claims else []
|
|
97
|
+
|
|
98
|
+
def subject(self) -> Optional[str]:
|
|
99
|
+
"""Get the subject from claims."""
|
|
100
|
+
return self.claims.get("sub") if self.claims else None
|
|
101
|
+
|
|
102
|
+
def groups(self) -> List[str]:
|
|
103
|
+
"""Get the groups from claims."""
|
|
104
|
+
return self.claims.get("groups", []) if self.claims else []
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import hashlib
|
|
2
|
+
import json
|
|
3
|
+
|
|
4
|
+
from api_foundry_query_engine.utils.logger import logger
|
|
5
|
+
from api_foundry_query_engine.operation import Operation
|
|
6
|
+
|
|
7
|
+
log = logger(__name__)
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class Service:
|
|
11
|
+
def __init__(self, config: dict = None):
|
|
12
|
+
if not config:
|
|
13
|
+
config = {}
|
|
14
|
+
self.config = config
|
|
15
|
+
|
|
16
|
+
def execute(self, operation: Operation) -> list[dict]:
|
|
17
|
+
raise NotImplementedError
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class ServiceAdapter(Service):
|
|
21
|
+
def __init__(self, config: dict = None):
|
|
22
|
+
if not config:
|
|
23
|
+
config = {}
|
|
24
|
+
super().__init__(config)
|
|
25
|
+
|
|
26
|
+
def execute(self, operation: Operation) -> list[dict]:
|
|
27
|
+
return super().execute(operation)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class MutationPublisher(ServiceAdapter):
|
|
31
|
+
def __init__(self, config: dict = None):
|
|
32
|
+
if not config:
|
|
33
|
+
config = {}
|
|
34
|
+
super().__init__(config)
|
|
35
|
+
|
|
36
|
+
def execute(self, operation):
|
|
37
|
+
result = super().execute(operation)
|
|
38
|
+
self.publish_notification(operation)
|
|
39
|
+
return result
|
|
40
|
+
|
|
41
|
+
def publish_notification(self, operation):
|
|
42
|
+
topic_arn = self.config.get("BROADCAST_TOPIC", None)
|
|
43
|
+
log.debug("Topic ARN: %s", topic_arn)
|
|
44
|
+
|
|
45
|
+
if topic_arn is not None:
|
|
46
|
+
log.debug("Sending message")
|
|
47
|
+
message = {
|
|
48
|
+
"entity": operation.api_name,
|
|
49
|
+
"action": operation.action,
|
|
50
|
+
"store_params": operation.store_params,
|
|
51
|
+
"query_params": operation.query_params,
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
message_str = json.dumps({"default": json.dumps(message)})
|
|
55
|
+
log.debug("message_str: %s", message_str)
|
|
56
|
+
hash_object = hashlib.sha256(message_str.encode("utf-8"))
|
|
57
|
+
hex_dig = hash_object.hexdigest()
|
|
58
|
+
|
|
59
|
+
msg_id = self.__client("sns").publish(
|
|
60
|
+
TopicArn=topic_arn,
|
|
61
|
+
MessageStructure="json",
|
|
62
|
+
MessageDeduplicationId=hex_dig,
|
|
63
|
+
MessageGroupId=operation.api_name,
|
|
64
|
+
Message=message_str,
|
|
65
|
+
)
|
|
66
|
+
log.info("publish msg id %s", msg_id)
|
|
67
|
+
|
|
68
|
+
def __client(self, client_type):
|
|
69
|
+
import boto3
|
|
70
|
+
|
|
71
|
+
region = self.config.get("AWS_REGION", "us-east-1")
|
|
72
|
+
session = boto3.Session()
|
|
73
|
+
if session:
|
|
74
|
+
return session.client(client_type, region_name=region)
|
|
75
|
+
return boto3.client(client_type, region_name=region)
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import traceback
|
|
2
|
+
from typing import Mapping
|
|
3
|
+
|
|
4
|
+
from api_foundry_query_engine.utils.logger import logger
|
|
5
|
+
from api_foundry_query_engine.utils.app_exception import ApplicationException
|
|
6
|
+
from api_foundry_query_engine.operation import Operation
|
|
7
|
+
from api_foundry_query_engine.services.service import ServiceAdapter
|
|
8
|
+
from api_foundry_query_engine.connectors.connection_factory import ConnectionFactory
|
|
9
|
+
from api_foundry_query_engine.dao.operation_dao import OperationDAO
|
|
10
|
+
from api_foundry_query_engine.utils.api_model import (
|
|
11
|
+
get_path_operation,
|
|
12
|
+
get_schema_object,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
log = logger(__name__)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class TransactionalService(ServiceAdapter):
|
|
19
|
+
def __init__(self, config: Mapping[str, str]):
|
|
20
|
+
super().__init__()
|
|
21
|
+
self.config = config
|
|
22
|
+
self.connection_factory = ConnectionFactory(config)
|
|
23
|
+
|
|
24
|
+
def execute(self, operation: Operation) -> list[dict]:
|
|
25
|
+
path_operation = get_path_operation(operation.entity, operation.action)
|
|
26
|
+
if path_operation:
|
|
27
|
+
database = path_operation.database
|
|
28
|
+
else:
|
|
29
|
+
schema_object = get_schema_object(operation.entity)
|
|
30
|
+
if schema_object:
|
|
31
|
+
database = schema_object.database
|
|
32
|
+
else:
|
|
33
|
+
raise ApplicationException(
|
|
34
|
+
500, f"Unknown operation: {operation.entity}"
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
# Pass config to connection_factory if needed (future extension)
|
|
38
|
+
connection = self.connection_factory.get_connection(database)
|
|
39
|
+
|
|
40
|
+
try:
|
|
41
|
+
result = OperationDAO(operation, connection.engine()).execute(connection)
|
|
42
|
+
if operation.action != "read":
|
|
43
|
+
connection.commit()
|
|
44
|
+
if isinstance(result, dict):
|
|
45
|
+
return [result]
|
|
46
|
+
return result
|
|
47
|
+
except Exception as error:
|
|
48
|
+
log.error("transaction exception: %s", error)
|
|
49
|
+
log.error("traceback: %s", traceback.format_exc())
|
|
50
|
+
raise error
|
|
51
|
+
finally:
|
|
52
|
+
connection.close()
|