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,68 @@
|
|
|
1
|
+
from typing import List, Dict
|
|
2
|
+
import re
|
|
3
|
+
|
|
4
|
+
from api_foundry_query_engine.dao.sql_query_handler import SQLQueryHandler
|
|
5
|
+
from api_foundry_query_engine.utils.api_model import SchemaObjectProperty, PathOperation
|
|
6
|
+
from api_foundry_query_engine.operation import Operation
|
|
7
|
+
from api_foundry_query_engine.utils.app_exception import ApplicationException
|
|
8
|
+
from api_foundry_query_engine.utils.logger import logger
|
|
9
|
+
|
|
10
|
+
log = logger(__name__)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class SQLCustomQueryHandler(SQLQueryHandler):
|
|
14
|
+
def __init__(
|
|
15
|
+
self, operation: Operation, path_operation: PathOperation, engine: str
|
|
16
|
+
) -> None:
|
|
17
|
+
super().__init__(operation, engine)
|
|
18
|
+
self.path_operation = path_operation
|
|
19
|
+
|
|
20
|
+
@property
|
|
21
|
+
def sql(self) -> str:
|
|
22
|
+
if not hasattr(self, "_sql"):
|
|
23
|
+
self._compile()
|
|
24
|
+
return self._sql
|
|
25
|
+
|
|
26
|
+
@property
|
|
27
|
+
def placeholders(self) -> Dict[str, SchemaObjectProperty]:
|
|
28
|
+
if not hasattr(self, "_placeholders"):
|
|
29
|
+
self._compile()
|
|
30
|
+
return self._placeholders
|
|
31
|
+
|
|
32
|
+
@property
|
|
33
|
+
def select_list_columns(self) -> List[str]:
|
|
34
|
+
raise NotImplementedError()
|
|
35
|
+
|
|
36
|
+
@property
|
|
37
|
+
def selection_results(self) -> Dict:
|
|
38
|
+
if not hasattr(self, "_selection_results"):
|
|
39
|
+
self._selection_results = self.check_permissions(
|
|
40
|
+
"read", self.path_operation.permissions, self.path_operation.outputs
|
|
41
|
+
)
|
|
42
|
+
log.debug("selection_results: %s", self._selection_results)
|
|
43
|
+
return self._selection_results
|
|
44
|
+
|
|
45
|
+
def _compile(self):
|
|
46
|
+
placeholder_pattern = re.compile(r":(\w+)")
|
|
47
|
+
self._placeholders = dict()
|
|
48
|
+
result_sql = placeholder_pattern.sub(
|
|
49
|
+
self._get_placeholder_text, self.path_operation.sql
|
|
50
|
+
)
|
|
51
|
+
self._sql = re.sub(r"\s+", " ", result_sql).strip()
|
|
52
|
+
|
|
53
|
+
def _get_placeholder_text(self, match) -> str:
|
|
54
|
+
placeholder_name = match.group(1)
|
|
55
|
+
property = self.path_operation.inputs.get(placeholder_name)
|
|
56
|
+
if not property:
|
|
57
|
+
raise ApplicationException(
|
|
58
|
+
500,
|
|
59
|
+
f"Input parameter not defined for the placeholder: {placeholder_name}",
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
value = (
|
|
63
|
+
self.operation.query_params[placeholder_name]
|
|
64
|
+
if placeholder_name in self.operation.query_params
|
|
65
|
+
else property.default
|
|
66
|
+
)
|
|
67
|
+
self._placeholders.update(self.generate_placeholders(property, value))
|
|
68
|
+
return self.placeholder(property, placeholder_name)
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
from api_foundry_query_engine.dao.sql_query_handler import SQLSchemaQueryHandler
|
|
2
|
+
from api_foundry_query_engine.operation import Operation
|
|
3
|
+
from api_foundry_query_engine.utils.app_exception import ApplicationException
|
|
4
|
+
from api_foundry_query_engine.utils.api_model import SchemaObject
|
|
5
|
+
from api_foundry_query_engine.utils.logger import logger
|
|
6
|
+
|
|
7
|
+
log = logger(__name__)
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class SQLDeleteSchemaQueryHandler(SQLSchemaQueryHandler):
|
|
11
|
+
def __init__(
|
|
12
|
+
self, operation: Operation, schema_object: SchemaObject, engine: str
|
|
13
|
+
) -> None:
|
|
14
|
+
super().__init__(operation, schema_object, engine)
|
|
15
|
+
|
|
16
|
+
def check_permission(self) -> bool:
|
|
17
|
+
"""
|
|
18
|
+
Checks the user's permissions for the specified permission type.
|
|
19
|
+
|
|
20
|
+
Args:
|
|
21
|
+
permission_type (str): The type of permission to check ("read" or "write").
|
|
22
|
+
properties (List[str], optional): Specific properties to check. If None,
|
|
23
|
+
all schema properties are checked.
|
|
24
|
+
|
|
25
|
+
Returns:
|
|
26
|
+
List[str]: A list of properties the user is permitted to access.
|
|
27
|
+
"""
|
|
28
|
+
# if permissions are not defined then no restrictions are applied
|
|
29
|
+
if not self.schema_object.permissions:
|
|
30
|
+
return True
|
|
31
|
+
|
|
32
|
+
# Use the proper provider-action-role structure
|
|
33
|
+
provider_permissions = self.schema_object.permissions.get("default", {})
|
|
34
|
+
delete_permissions = provider_permissions.get("delete", {})
|
|
35
|
+
|
|
36
|
+
for role in self.operation.roles:
|
|
37
|
+
role_permissions = delete_permissions.get(role)
|
|
38
|
+
log.info("role: %s, role_permissions: %s", role, role_permissions)
|
|
39
|
+
|
|
40
|
+
# If no permissions found for role, check wildcard "*"
|
|
41
|
+
if role_permissions is None:
|
|
42
|
+
role_permissions = delete_permissions.get("*")
|
|
43
|
+
log.info("Fallback to wildcard role '*': %s", role_permissions)
|
|
44
|
+
if role_permissions is None:
|
|
45
|
+
continue
|
|
46
|
+
|
|
47
|
+
# Handle both boolean and object permission formats
|
|
48
|
+
if isinstance(role_permissions, bool):
|
|
49
|
+
allowed = role_permissions
|
|
50
|
+
else:
|
|
51
|
+
# For complex permission objects, existence means allowed
|
|
52
|
+
allowed = True
|
|
53
|
+
|
|
54
|
+
if allowed:
|
|
55
|
+
return True
|
|
56
|
+
|
|
57
|
+
return False
|
|
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
|
+
|
|
109
|
+
@property
|
|
110
|
+
def sql(self) -> str:
|
|
111
|
+
if not self.check_permission():
|
|
112
|
+
raise ApplicationException(
|
|
113
|
+
403, f"Subject is not allowed to delete {self.schema_object.api_name}"
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
concurrency_property = self.schema_object.concurrency_property
|
|
117
|
+
if concurrency_property:
|
|
118
|
+
if not self.operation.query_params.get(concurrency_property.api_name):
|
|
119
|
+
raise ApplicationException(
|
|
120
|
+
400,
|
|
121
|
+
"Missing required concurrency management property. "
|
|
122
|
+
+ f"schema_object: {self.schema_object.api_name}, "
|
|
123
|
+
+ f"property: {concurrency_property.api_name}",
|
|
124
|
+
)
|
|
125
|
+
if self.operation.store_params.get(concurrency_property.api_name):
|
|
126
|
+
raise ApplicationException(
|
|
127
|
+
400,
|
|
128
|
+
"For updating concurrency managed schema objects the current "
|
|
129
|
+
+ "version may not be supplied as a storage parameter. "
|
|
130
|
+
+ f"schema_object: {self.schema_object.api_name}, "
|
|
131
|
+
+ f"property: {concurrency_property.api_name}",
|
|
132
|
+
)
|
|
133
|
+
|
|
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
|
+
)
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
from typing import Optional
|
|
2
|
+
from api_foundry_query_engine.dao.sql_query_handler import SQLSchemaQueryHandler
|
|
3
|
+
from api_foundry_query_engine.operation import Operation
|
|
4
|
+
from api_foundry_query_engine.utils.app_exception import ApplicationException
|
|
5
|
+
from api_foundry_query_engine.utils.api_model import SchemaObject, SchemaObjectProperty
|
|
6
|
+
from api_foundry_query_engine.utils.logger import logger
|
|
7
|
+
|
|
8
|
+
log = logger(__name__)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class SQLInsertSchemaQueryHandler(SQLSchemaQueryHandler):
|
|
12
|
+
key_property: Optional[SchemaObjectProperty]
|
|
13
|
+
|
|
14
|
+
def __init__(
|
|
15
|
+
self, operation: Operation, schema_object: SchemaObject, engine: str
|
|
16
|
+
) -> None:
|
|
17
|
+
super().__init__(operation, schema_object, engine)
|
|
18
|
+
self.key_property = schema_object.primary_key
|
|
19
|
+
if self.key_property:
|
|
20
|
+
if self.key_property.key_type == "auto":
|
|
21
|
+
if operation.store_params.get(self.key_property.column_name):
|
|
22
|
+
raise ApplicationException(
|
|
23
|
+
400,
|
|
24
|
+
"Primary key values cannot be inserted when key type"
|
|
25
|
+
+ f" is auto. schema_object: {schema_object.api_name}",
|
|
26
|
+
)
|
|
27
|
+
elif self.key_property.key_type == "required":
|
|
28
|
+
if not operation.store_params.get(self.key_property.column_name):
|
|
29
|
+
raise ApplicationException(
|
|
30
|
+
400,
|
|
31
|
+
"Primary key values must be provided when key type is"
|
|
32
|
+
+ f" required. schema_object: {schema_object.api_name}",
|
|
33
|
+
)
|
|
34
|
+
self.concurrency_property = schema_object.concurrency_property
|
|
35
|
+
if self.concurrency_property and operation.store_params.get(
|
|
36
|
+
self.concurrency_property.api_name
|
|
37
|
+
):
|
|
38
|
+
raise ApplicationException(
|
|
39
|
+
400,
|
|
40
|
+
"Versioned properties can not be supplied a store parameters. "
|
|
41
|
+
+ f"schema_object: {schema_object.api_name}, "
|
|
42
|
+
+ f"property: {self.concurrency_property.api_name}",
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
@property
|
|
46
|
+
def sql(self) -> str:
|
|
47
|
+
self.concurrency_property = self.schema_object.concurrency_property
|
|
48
|
+
# Get columns to return - use read permissions if available,
|
|
49
|
+
# otherwise fall back to primary key
|
|
50
|
+
returning_clause = self._get_returning_clause()
|
|
51
|
+
|
|
52
|
+
if not self.concurrency_property:
|
|
53
|
+
return (
|
|
54
|
+
f"INSERT INTO {self.table_expression}{self.insert_values} "
|
|
55
|
+
+ returning_clause
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
if self.operation.store_params.get(self.concurrency_property.api_name):
|
|
59
|
+
raise ApplicationException(
|
|
60
|
+
400,
|
|
61
|
+
"When inserting schema objects with a version property "
|
|
62
|
+
+ "the a version must not be supplied as a storage parameter."
|
|
63
|
+
+ f" schema_object: {self.schema_object.api_name}, "
|
|
64
|
+
+ f"property: {self.concurrency_property.api_name}",
|
|
65
|
+
)
|
|
66
|
+
return (
|
|
67
|
+
f"INSERT INTO {self.table_expression}{self.insert_values}"
|
|
68
|
+
+ returning_clause
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
def _get_returning_clause(self) -> str:
|
|
72
|
+
"""
|
|
73
|
+
Get RETURNING clause with fallback to primary key
|
|
74
|
+
if no read permissions.
|
|
75
|
+
"""
|
|
76
|
+
try:
|
|
77
|
+
select_list = self.select_list
|
|
78
|
+
if select_list:
|
|
79
|
+
return f"RETURNING {select_list}"
|
|
80
|
+
except ApplicationException:
|
|
81
|
+
# If read permissions filter everything,
|
|
82
|
+
# return at least the primary key
|
|
83
|
+
pass
|
|
84
|
+
|
|
85
|
+
# Fallback to primary key
|
|
86
|
+
pk_property = self.schema_object.primary_key
|
|
87
|
+
if pk_property:
|
|
88
|
+
return f"RETURNING {pk_property.column_name}"
|
|
89
|
+
|
|
90
|
+
# Last resort - return nothing
|
|
91
|
+
# (should never happen with properly defined schemas)
|
|
92
|
+
return ""
|
|
93
|
+
|
|
94
|
+
@property
|
|
95
|
+
def insert_values(self) -> str:
|
|
96
|
+
self.store_placeholders = {}
|
|
97
|
+
placeholders = []
|
|
98
|
+
columns = []
|
|
99
|
+
|
|
100
|
+
allowed_property_names = self.check_permissions(
|
|
101
|
+
"write", self.schema_object.permissions, self.schema_object.properties
|
|
102
|
+
)
|
|
103
|
+
allowed_properties = {
|
|
104
|
+
k: v
|
|
105
|
+
for k, v in self.schema_object.properties.items()
|
|
106
|
+
if k in allowed_property_names
|
|
107
|
+
}
|
|
108
|
+
log.info("allowed properties: %s", allowed_properties)
|
|
109
|
+
|
|
110
|
+
import json
|
|
111
|
+
|
|
112
|
+
# First, validate that user is not trying to set injected properties
|
|
113
|
+
for property_name, property in self.schema_object.properties.items():
|
|
114
|
+
if property.inject_value and property_name in self.operation.store_params:
|
|
115
|
+
raise ApplicationException(
|
|
116
|
+
403,
|
|
117
|
+
f"Property '{property_name}' is auto-injected and "
|
|
118
|
+
+ "cannot be set manually",
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
for name, value in self.operation.store_params.items():
|
|
122
|
+
parts = name.split(".")
|
|
123
|
+
|
|
124
|
+
if len(parts) > 1:
|
|
125
|
+
raise ApplicationException(
|
|
126
|
+
400,
|
|
127
|
+
"Properties can not be set on associated objects " + name,
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
property = allowed_properties.get(parts[0], None)
|
|
131
|
+
if property is None:
|
|
132
|
+
if parts[0] not in self.schema_object.properties:
|
|
133
|
+
raise ApplicationException(400, f"Invalid property: {name}")
|
|
134
|
+
else:
|
|
135
|
+
raise ApplicationException(
|
|
136
|
+
403,
|
|
137
|
+
f"Subject is not allowed to create with property: {parts[0]}",
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
columns.append(property.column_name)
|
|
141
|
+
if property.api_name is None:
|
|
142
|
+
raise ApplicationException(
|
|
143
|
+
400, f"Property '{name}' does not have a valid api_name."
|
|
144
|
+
)
|
|
145
|
+
placeholders.append(self.placeholder(property, property.api_name))
|
|
146
|
+
# Serialize embedded objects to JSON
|
|
147
|
+
if property.api_type == "object":
|
|
148
|
+
self.store_placeholders[property.api_name] = json.dumps(value)
|
|
149
|
+
else:
|
|
150
|
+
self.store_placeholders[
|
|
151
|
+
property.api_name
|
|
152
|
+
] = property.convert_to_db_value(value)
|
|
153
|
+
|
|
154
|
+
# Inject values from claims/timestamps/etc for properties with
|
|
155
|
+
# x-af-inject-value on CREATE
|
|
156
|
+
for property_name, property in self.schema_object.properties.items():
|
|
157
|
+
if property.inject_value and "create" in property.inject_on:
|
|
158
|
+
injected_value = self.extract_injected_value(property.inject_value)
|
|
159
|
+
if injected_value is not None:
|
|
160
|
+
placeholder_key = f"__inject_{property_name}"
|
|
161
|
+
columns.append(property.column_name)
|
|
162
|
+
placeholders.append(self.placeholder(property, placeholder_key))
|
|
163
|
+
self.store_placeholders[
|
|
164
|
+
placeholder_key
|
|
165
|
+
] = property.convert_to_db_value(injected_value)
|
|
166
|
+
elif property.required:
|
|
167
|
+
raise ApplicationException(
|
|
168
|
+
400,
|
|
169
|
+
f"Required injected property '{property_name}' "
|
|
170
|
+
+ f"could not be populated from '{property.inject_value}'",
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
if self.key_property:
|
|
174
|
+
if self.key_property.key_type == "sequence":
|
|
175
|
+
columns.append(self.key_property.column_name)
|
|
176
|
+
placeholders.append(f"nextval('{self.key_property.sequence_name}')")
|
|
177
|
+
|
|
178
|
+
if self.concurrency_property:
|
|
179
|
+
columns.append(self.concurrency_property.column_name)
|
|
180
|
+
if self.concurrency_property.column_type == "integer":
|
|
181
|
+
placeholders.append("1")
|
|
182
|
+
else:
|
|
183
|
+
placeholders.append(
|
|
184
|
+
self.concurrency_generator(self.concurrency_property)
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
return f" ( {', '.join(columns)} ) VALUES ( {', '.join(placeholders)})"
|