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,195 @@
|
|
|
1
|
+
from api_foundry_query_engine.dao.sql_query_handler import (
|
|
2
|
+
SQLSchemaQueryHandler,
|
|
3
|
+
)
|
|
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
|
+
from api_foundry_query_engine.utils.logger import logger
|
|
8
|
+
|
|
9
|
+
log = logger(__name__)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class SQLRestoreSchemaQueryHandler(SQLSchemaQueryHandler):
|
|
13
|
+
"""Handler for restoring soft-deleted records."""
|
|
14
|
+
|
|
15
|
+
def __init__(
|
|
16
|
+
self, operation: Operation, schema_object: SchemaObject, engine: str
|
|
17
|
+
) -> None:
|
|
18
|
+
super().__init__(operation, schema_object, engine)
|
|
19
|
+
|
|
20
|
+
def check_permission(self) -> bool:
|
|
21
|
+
"""Check if user has permission to restore records."""
|
|
22
|
+
# if permissions are not defined then no restrictions are applied
|
|
23
|
+
if not self.schema_object.permissions:
|
|
24
|
+
return True
|
|
25
|
+
|
|
26
|
+
# Use the proper provider-action-role structure
|
|
27
|
+
provider_permissions = self.schema_object.permissions.get("default", {})
|
|
28
|
+
|
|
29
|
+
# Check for explicit restore permissions first
|
|
30
|
+
restore_permissions = provider_permissions.get("restore", {})
|
|
31
|
+
write_permissions = provider_permissions.get("write", {})
|
|
32
|
+
|
|
33
|
+
for role in self.operation.roles:
|
|
34
|
+
# Try restore permissions first
|
|
35
|
+
role_permissions = restore_permissions.get(role)
|
|
36
|
+
log.info("role: %s, restore_permissions: %s", role, role_permissions)
|
|
37
|
+
|
|
38
|
+
# If not found, fall back to write permissions
|
|
39
|
+
if role_permissions is None:
|
|
40
|
+
role_permissions = write_permissions.get(role)
|
|
41
|
+
log.info("Fallback to write permissions: %s", role_permissions)
|
|
42
|
+
|
|
43
|
+
# If still not found, check wildcard "*"
|
|
44
|
+
if role_permissions is None:
|
|
45
|
+
role_permissions = restore_permissions.get(
|
|
46
|
+
"*"
|
|
47
|
+
) or write_permissions.get("*")
|
|
48
|
+
log.info("Fallback to wildcard role '*': %s", role_permissions)
|
|
49
|
+
if role_permissions is None:
|
|
50
|
+
continue
|
|
51
|
+
|
|
52
|
+
# Handle both boolean and object permission formats
|
|
53
|
+
if isinstance(role_permissions, bool):
|
|
54
|
+
allowed = role_permissions
|
|
55
|
+
else:
|
|
56
|
+
# For complex permission objects, existence means allowed
|
|
57
|
+
allowed = True
|
|
58
|
+
|
|
59
|
+
if allowed:
|
|
60
|
+
return True
|
|
61
|
+
|
|
62
|
+
return False
|
|
63
|
+
|
|
64
|
+
def _get_restore_update_values(self) -> str:
|
|
65
|
+
"""Generate SET clause for restore operation."""
|
|
66
|
+
self.store_placeholders = {}
|
|
67
|
+
columns = []
|
|
68
|
+
|
|
69
|
+
# Process soft delete properties
|
|
70
|
+
soft_delete_props = self.schema_object.get_soft_delete_properties()
|
|
71
|
+
|
|
72
|
+
for _, prop in soft_delete_props.items():
|
|
73
|
+
strategy = prop.get_soft_delete_strategy()
|
|
74
|
+
config = prop.get_soft_delete_config()
|
|
75
|
+
column_name = prop.column_name
|
|
76
|
+
|
|
77
|
+
if strategy == "null_check":
|
|
78
|
+
columns.append(f"{column_name} = NULL")
|
|
79
|
+
elif strategy == "boolean_flag":
|
|
80
|
+
active_value = config.get("active_value", True)
|
|
81
|
+
columns.append(f"{column_name} = {str(active_value).lower()}")
|
|
82
|
+
elif strategy == "exclude_values":
|
|
83
|
+
restore_value = config.get("restore_value")
|
|
84
|
+
if restore_value:
|
|
85
|
+
if isinstance(restore_value, str):
|
|
86
|
+
columns.append(f"{column_name} = '{restore_value}'")
|
|
87
|
+
else:
|
|
88
|
+
columns.append(f"{column_name} = {restore_value}")
|
|
89
|
+
|
|
90
|
+
# Process audit fields for restore action
|
|
91
|
+
audit_props = self.schema_object.get_soft_delete_audit_properties()
|
|
92
|
+
claims = self.operation.claims or {}
|
|
93
|
+
|
|
94
|
+
for _, prop in audit_props.items():
|
|
95
|
+
config = prop.get_soft_delete_config()
|
|
96
|
+
action = config.get("action", "")
|
|
97
|
+
|
|
98
|
+
if action == "restore" and "sub" in claims:
|
|
99
|
+
placeholder_key = f"audit_{prop.api_name}"
|
|
100
|
+
columns.append(f"{prop.column_name} = %({placeholder_key})s")
|
|
101
|
+
self.store_placeholders[placeholder_key] = claims["sub"]
|
|
102
|
+
elif action == "restore_timestamp":
|
|
103
|
+
columns.append(f"{prop.column_name} = CURRENT_TIMESTAMP")
|
|
104
|
+
|
|
105
|
+
return " SET " + ", ".join(columns) if columns else ""
|
|
106
|
+
|
|
107
|
+
@property
|
|
108
|
+
def search_condition(self) -> str:
|
|
109
|
+
"""Override to include soft-deleted records in restore search."""
|
|
110
|
+
self.search_placeholders = {}
|
|
111
|
+
conditions = []
|
|
112
|
+
|
|
113
|
+
# Don't apply soft delete filtering for restore operations
|
|
114
|
+
# We want to find the soft-deleted records to restore them
|
|
115
|
+
|
|
116
|
+
for name, value in self.operation.query_params.items():
|
|
117
|
+
if "." in name:
|
|
118
|
+
raise ApplicationException(
|
|
119
|
+
400, "Selection on relations is not supported"
|
|
120
|
+
)
|
|
121
|
+
prop = self.schema_object.properties.get(name)
|
|
122
|
+
if not prop:
|
|
123
|
+
raise ApplicationException(
|
|
124
|
+
500, f"Search condition column not found {name}"
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
assignment, holders = self.search_value_assignment(prop, value)
|
|
128
|
+
conditions.append(assignment)
|
|
129
|
+
self.search_placeholders.update(holders)
|
|
130
|
+
|
|
131
|
+
# Add condition to only restore soft-deleted records
|
|
132
|
+
soft_delete_conditions = self._get_soft_delete_restore_conditions()
|
|
133
|
+
if soft_delete_conditions:
|
|
134
|
+
conditions.append(f"({soft_delete_conditions})")
|
|
135
|
+
|
|
136
|
+
return f" WHERE {' AND '.join(conditions)}" if conditions else ""
|
|
137
|
+
|
|
138
|
+
def _get_soft_delete_restore_conditions(self) -> str:
|
|
139
|
+
"""Generate conditions to identify soft-deleted records for restore."""
|
|
140
|
+
conditions = []
|
|
141
|
+
|
|
142
|
+
soft_delete_props = self.schema_object.get_soft_delete_properties()
|
|
143
|
+
|
|
144
|
+
for _, prop in soft_delete_props.items():
|
|
145
|
+
strategy = prop.get_soft_delete_strategy()
|
|
146
|
+
config = prop.get_soft_delete_config()
|
|
147
|
+
column_name = prop.column_name
|
|
148
|
+
|
|
149
|
+
if strategy == "null_check":
|
|
150
|
+
conditions.append(f"{column_name} IS NOT NULL")
|
|
151
|
+
elif strategy == "boolean_flag":
|
|
152
|
+
active_value = config.get("active_value", True)
|
|
153
|
+
inactive_value = not active_value
|
|
154
|
+
value_str = str(inactive_value).lower()
|
|
155
|
+
conditions.append(f"{column_name} = {value_str}")
|
|
156
|
+
elif strategy == "exclude_values":
|
|
157
|
+
excluded_values = config.get("values", [])
|
|
158
|
+
if excluded_values:
|
|
159
|
+
# For restore, we want records that ARE in the excluded values
|
|
160
|
+
formatted_values = ", ".join(
|
|
161
|
+
f"'{val}'" if isinstance(val, str) else str(val)
|
|
162
|
+
for val in excluded_values
|
|
163
|
+
)
|
|
164
|
+
conditions.append(f"{column_name} IN ({formatted_values})")
|
|
165
|
+
|
|
166
|
+
# For multiple soft delete fields, use AND logic
|
|
167
|
+
# A record is considered soft-deleted if ALL soft delete conditions are true
|
|
168
|
+
return " AND ".join(conditions) if conditions else ""
|
|
169
|
+
|
|
170
|
+
@property
|
|
171
|
+
def sql(self) -> str:
|
|
172
|
+
if not self.check_permission():
|
|
173
|
+
raise ApplicationException(
|
|
174
|
+
403,
|
|
175
|
+
f"Subject is not allowed to restore "
|
|
176
|
+
f"{self.schema_object.api_name}",
|
|
177
|
+
)
|
|
178
|
+
|
|
179
|
+
if not self.schema_object.has_soft_delete_support():
|
|
180
|
+
raise ApplicationException(
|
|
181
|
+
400,
|
|
182
|
+
f"Schema object {self.schema_object.api_name} does not support "
|
|
183
|
+
+ "soft delete operations",
|
|
184
|
+
)
|
|
185
|
+
|
|
186
|
+
update_clause = self._get_restore_update_values()
|
|
187
|
+
if not update_clause:
|
|
188
|
+
raise ApplicationException(
|
|
189
|
+
400, f"No restore fields available for {self.schema_object.api_name}"
|
|
190
|
+
)
|
|
191
|
+
|
|
192
|
+
return (
|
|
193
|
+
f"UPDATE {self.table_expression}{update_clause}"
|
|
194
|
+
+ f"{self.search_condition} RETURNING {self.select_list}"
|
|
195
|
+
)
|
|
@@ -0,0 +1,433 @@
|
|
|
1
|
+
import re
|
|
2
|
+
from typing import Match
|
|
3
|
+
from api_foundry_query_engine.dao.sql_query_handler import (
|
|
4
|
+
SQLSchemaQueryHandler,
|
|
5
|
+
)
|
|
6
|
+
from api_foundry_query_engine.utils.app_exception import ApplicationException
|
|
7
|
+
from api_foundry_query_engine.utils.api_model import SchemaObjectProperty
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class SQLSelectSchemaQueryHandler(SQLSchemaQueryHandler):
|
|
11
|
+
def _soft_delete_where_clause(self) -> str:
|
|
12
|
+
"""
|
|
13
|
+
Generate WHERE clause to filter out soft-deleted records.
|
|
14
|
+
|
|
15
|
+
Uses smart conflict detection - if query explicitly requests
|
|
16
|
+
soft-deleted values, those filters are skipped to allow access.
|
|
17
|
+
Adds table prefixes for JOIN queries.
|
|
18
|
+
"""
|
|
19
|
+
prefix = self.prefix_map[str(self.schema_object.api_name)]
|
|
20
|
+
conditions = []
|
|
21
|
+
|
|
22
|
+
soft_delete_props = self.schema_object.get_soft_delete_properties()
|
|
23
|
+
conflicts = self._has_soft_delete_conflicts()
|
|
24
|
+
|
|
25
|
+
for prop_name, prop in soft_delete_props.items():
|
|
26
|
+
# Skip filtering if user explicitly queries for soft-deleted values
|
|
27
|
+
if conflicts.get(prop_name, False):
|
|
28
|
+
continue
|
|
29
|
+
|
|
30
|
+
strategy = prop.get_soft_delete_strategy()
|
|
31
|
+
config = prop.get_soft_delete_config()
|
|
32
|
+
column_name = prop.column_name
|
|
33
|
+
|
|
34
|
+
if strategy == "null_check":
|
|
35
|
+
conditions.append(f"{prefix}.{column_name} IS NULL")
|
|
36
|
+
elif strategy == "boolean_flag":
|
|
37
|
+
active_value = config.get("active_value", True)
|
|
38
|
+
conditions.append(f"{prefix}.{column_name} = {active_value}")
|
|
39
|
+
elif strategy == "exclude_values":
|
|
40
|
+
excluded_values = config.get("values", [])
|
|
41
|
+
if excluded_values:
|
|
42
|
+
# Format values for SQL IN clause
|
|
43
|
+
formatted_values = ", ".join(
|
|
44
|
+
f"'{val}'" if isinstance(val, str) else str(val)
|
|
45
|
+
for val in excluded_values
|
|
46
|
+
)
|
|
47
|
+
conditions.append(
|
|
48
|
+
f"{prefix}.{column_name} NOT IN ({formatted_values})"
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
return " AND ".join(conditions) if conditions else ""
|
|
52
|
+
|
|
53
|
+
def _template_where(self, expr: str) -> str:
|
|
54
|
+
if not expr:
|
|
55
|
+
return expr
|
|
56
|
+
|
|
57
|
+
def _quote(val: object) -> str:
|
|
58
|
+
if val is None:
|
|
59
|
+
return "NULL"
|
|
60
|
+
if isinstance(val, (int, float)):
|
|
61
|
+
return str(val)
|
|
62
|
+
s = str(val).replace("'", "''")
|
|
63
|
+
return f"'{s}'"
|
|
64
|
+
|
|
65
|
+
def _replace(m: Match[str]) -> str:
|
|
66
|
+
key = m.group(1)
|
|
67
|
+
claims = self.operation.claims or {}
|
|
68
|
+
return _quote(claims.get(key))
|
|
69
|
+
|
|
70
|
+
return re.sub(r"\$\{claims\.([A-Za-z0-9_]+)\}", _replace, expr)
|
|
71
|
+
|
|
72
|
+
def _row_where_clause(self) -> str:
|
|
73
|
+
perms = getattr(self.schema_object, "permissions", None) or {}
|
|
74
|
+
# provider-first default
|
|
75
|
+
if "default" in perms:
|
|
76
|
+
provider = perms.get("default", {}) or {}
|
|
77
|
+
read_map = provider.get("read", {}) or {}
|
|
78
|
+
role_permissions = perms.get("default", {})
|
|
79
|
+
else:
|
|
80
|
+
# legacy role-first -> synthesize read map
|
|
81
|
+
read_map = {}
|
|
82
|
+
role_permissions = perms
|
|
83
|
+
for role, role_perms in perms.items():
|
|
84
|
+
if isinstance(role_perms, dict):
|
|
85
|
+
read_map[role] = role_perms.get("read")
|
|
86
|
+
|
|
87
|
+
filters = []
|
|
88
|
+
for role in self.operation.roles or []:
|
|
89
|
+
# Check for role-level WHERE clause (hybrid approach)
|
|
90
|
+
role_where = None
|
|
91
|
+
if isinstance(role_permissions.get(role), dict):
|
|
92
|
+
role_where = role_permissions[role].get("where")
|
|
93
|
+
|
|
94
|
+
# Check for operation-level WHERE clause
|
|
95
|
+
operation_where = None
|
|
96
|
+
rule = read_map.get(role)
|
|
97
|
+
if isinstance(rule, dict):
|
|
98
|
+
operation_where = rule.get("where")
|
|
99
|
+
|
|
100
|
+
# Operation-level takes precedence, fallback to role-level
|
|
101
|
+
where_clause = operation_where if operation_where else role_where
|
|
102
|
+
|
|
103
|
+
if isinstance(where_clause, str) and where_clause.strip():
|
|
104
|
+
filters.append(self._template_where(where_clause))
|
|
105
|
+
|
|
106
|
+
if not filters:
|
|
107
|
+
return ""
|
|
108
|
+
return "(" + ") OR (".join(filters) + ")"
|
|
109
|
+
|
|
110
|
+
def __init__(self, operation, schema_object, engine: str) -> None:
|
|
111
|
+
super().__init__(operation, schema_object, engine)
|
|
112
|
+
# Lazy cache for selection results
|
|
113
|
+
self._selection_results = None
|
|
114
|
+
|
|
115
|
+
@property
|
|
116
|
+
def sql(self) -> str:
|
|
117
|
+
# order is important here table_expression must be last
|
|
118
|
+
search_condition = self.search_condition
|
|
119
|
+
order_by_expression = self.order_by_expression
|
|
120
|
+
select_list = self.select_list
|
|
121
|
+
table_expression = self.table_expression
|
|
122
|
+
|
|
123
|
+
return (
|
|
124
|
+
f"SELECT {select_list}"
|
|
125
|
+
+ f" FROM {table_expression}"
|
|
126
|
+
+ search_condition
|
|
127
|
+
+ order_by_expression
|
|
128
|
+
+ self.limit_expression
|
|
129
|
+
+ self.offset_expression
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
@property
|
|
133
|
+
def select_list(self) -> str:
|
|
134
|
+
if self.operation.metadata_params.get("count", False):
|
|
135
|
+
return "count(*)"
|
|
136
|
+
return super().select_list
|
|
137
|
+
|
|
138
|
+
@property
|
|
139
|
+
def search_condition(self) -> str:
|
|
140
|
+
self.search_placeholders = {}
|
|
141
|
+
conditions = []
|
|
142
|
+
|
|
143
|
+
# Add soft delete filtering first
|
|
144
|
+
soft_delete_filter = self._soft_delete_where_clause()
|
|
145
|
+
if soft_delete_filter:
|
|
146
|
+
conditions.append(soft_delete_filter)
|
|
147
|
+
|
|
148
|
+
for name, value in self.operation.query_params.items():
|
|
149
|
+
parts = name.split(".")
|
|
150
|
+
|
|
151
|
+
try:
|
|
152
|
+
if len(parts) > 1:
|
|
153
|
+
if parts[0] not in self.schema_object.relations:
|
|
154
|
+
raise ApplicationException(
|
|
155
|
+
400,
|
|
156
|
+
"Invalid selection property "
|
|
157
|
+
+ str(self.schema_object.api_name)
|
|
158
|
+
+ " does not have a property "
|
|
159
|
+
+ parts[0],
|
|
160
|
+
)
|
|
161
|
+
relation = self.schema_object.relations[parts[0]]
|
|
162
|
+
if parts[1] not in relation.child_schema_object.properties:
|
|
163
|
+
raise ApplicationException(
|
|
164
|
+
400,
|
|
165
|
+
"Property not found, "
|
|
166
|
+
+ str(relation.child_schema_object.api_name)
|
|
167
|
+
+ " does not have property "
|
|
168
|
+
+ parts[1],
|
|
169
|
+
)
|
|
170
|
+
prop = relation.child_schema_object.properties[parts[1]]
|
|
171
|
+
prefix = self.prefix_map[parts[0]]
|
|
172
|
+
else:
|
|
173
|
+
prop = self.schema_object.properties[parts[0]]
|
|
174
|
+
prefix = self.prefix_map[str(self.schema_object.api_name)]
|
|
175
|
+
except KeyError as exc:
|
|
176
|
+
raise ApplicationException(
|
|
177
|
+
500,
|
|
178
|
+
(
|
|
179
|
+
"Invalid query parameter, property not found. "
|
|
180
|
+
+ "schema object: "
|
|
181
|
+
+ str(self.schema_object.api_name)
|
|
182
|
+
+ ", property: "
|
|
183
|
+
+ name
|
|
184
|
+
),
|
|
185
|
+
) from exc
|
|
186
|
+
|
|
187
|
+
assignment, holders = self.search_value_assignment(prop, value, prefix)
|
|
188
|
+
self.active_prefixes.add(prefix)
|
|
189
|
+
conditions.append(assignment)
|
|
190
|
+
self.search_placeholders.update(holders)
|
|
191
|
+
# append row-level filters if present
|
|
192
|
+
row_filter = self._row_where_clause()
|
|
193
|
+
if row_filter:
|
|
194
|
+
conditions.append(f"({row_filter})")
|
|
195
|
+
|
|
196
|
+
return (" WHERE " + " AND ".join(conditions)) if conditions else ""
|
|
197
|
+
|
|
198
|
+
@property
|
|
199
|
+
def table_expression(self) -> str:
|
|
200
|
+
joins = []
|
|
201
|
+
parent_prefix = self.prefix_map[str(self.schema_object.api_name)]
|
|
202
|
+
for _, relation in self.schema_object.relations.items():
|
|
203
|
+
child_prefix = self.prefix_map[str(relation.api_name)]
|
|
204
|
+
if child_prefix in self.active_prefixes:
|
|
205
|
+
joins.append(
|
|
206
|
+
"INNER JOIN "
|
|
207
|
+
+ str(relation.child_schema_object.qualified_name)
|
|
208
|
+
+ " AS "
|
|
209
|
+
+ child_prefix
|
|
210
|
+
+ " ON "
|
|
211
|
+
+ parent_prefix
|
|
212
|
+
+ "."
|
|
213
|
+
+ relation.parent_property
|
|
214
|
+
+ " = "
|
|
215
|
+
+ child_prefix
|
|
216
|
+
+ "."
|
|
217
|
+
+ relation.child_property
|
|
218
|
+
)
|
|
219
|
+
|
|
220
|
+
return (
|
|
221
|
+
str(self.schema_object.qualified_name)
|
|
222
|
+
+ " AS "
|
|
223
|
+
+ str(self.prefix_map[str(self.schema_object.api_name)])
|
|
224
|
+
+ (f" {' '.join(joins)}" if len(joins) > 0 else "")
|
|
225
|
+
)
|
|
226
|
+
|
|
227
|
+
@property
|
|
228
|
+
def selection_results(self) -> dict:
|
|
229
|
+
if self._selection_results is None:
|
|
230
|
+
self._selection_results = {}
|
|
231
|
+
if "count" in self.operation.metadata_params:
|
|
232
|
+
self._selection_results = {
|
|
233
|
+
"count": SchemaObjectProperty(
|
|
234
|
+
{
|
|
235
|
+
"api_name": "count",
|
|
236
|
+
"api_type": "integer",
|
|
237
|
+
"column_name": "count(*)",
|
|
238
|
+
"column_type": "integer",
|
|
239
|
+
}
|
|
240
|
+
)
|
|
241
|
+
}
|
|
242
|
+
return self._selection_results
|
|
243
|
+
|
|
244
|
+
filter_str = self.operation.metadata_params.get("properties", ".*")
|
|
245
|
+
|
|
246
|
+
for relation, reg_exs in self.get_regex_map(filter_str).items():
|
|
247
|
+
# Extract the schema object for the current entity
|
|
248
|
+
relation_property = self.schema_object.relations.get(relation)
|
|
249
|
+
|
|
250
|
+
if relation_property:
|
|
251
|
+
if relation_property.type == "array":
|
|
252
|
+
continue
|
|
253
|
+
|
|
254
|
+
# Use a default value if relation_property is None
|
|
255
|
+
schema_object = relation_property.child_schema_object
|
|
256
|
+
else:
|
|
257
|
+
schema_object = self.schema_object
|
|
258
|
+
|
|
259
|
+
if relation not in self.prefix_map:
|
|
260
|
+
raise ApplicationException(
|
|
261
|
+
400,
|
|
262
|
+
"Bad object association: "
|
|
263
|
+
+ str(schema_object.api_name)
|
|
264
|
+
+ " does not have a "
|
|
265
|
+
+ relation
|
|
266
|
+
+ " property",
|
|
267
|
+
)
|
|
268
|
+
# Filter and prefix keys for the current entity
|
|
269
|
+
# and regular expressions
|
|
270
|
+
allowed_properties = self.check_permissions(
|
|
271
|
+
"read",
|
|
272
|
+
schema_object.permissions,
|
|
273
|
+
schema_object.properties,
|
|
274
|
+
)
|
|
275
|
+
filtered_keys = self.filter_and_prefix_keys(
|
|
276
|
+
reg_exs,
|
|
277
|
+
allowed_properties,
|
|
278
|
+
self.prefix_map[relation],
|
|
279
|
+
)
|
|
280
|
+
|
|
281
|
+
# Extend the result map with the filtered keys
|
|
282
|
+
self._selection_results.update(filtered_keys)
|
|
283
|
+
|
|
284
|
+
if len(self._selection_results) == 0:
|
|
285
|
+
raise ApplicationException(
|
|
286
|
+
403,
|
|
287
|
+
(
|
|
288
|
+
"After applying permissions there are no properties "
|
|
289
|
+
"returned in response"
|
|
290
|
+
),
|
|
291
|
+
)
|
|
292
|
+
return self._selection_results
|
|
293
|
+
|
|
294
|
+
def get_regex_map(self, filter_str: str) -> dict:
|
|
295
|
+
result = {}
|
|
296
|
+
|
|
297
|
+
for flt in filter_str.split():
|
|
298
|
+
parts = flt.split(":")
|
|
299
|
+
entity = parts[0] if len(parts) > 1 else self.schema_object.api_name
|
|
300
|
+
expression = parts[-1]
|
|
301
|
+
|
|
302
|
+
# Check if entity already exists in result, if not, initialize
|
|
303
|
+
# it with an empty list
|
|
304
|
+
if entity not in result:
|
|
305
|
+
result[entity] = []
|
|
306
|
+
|
|
307
|
+
# Append the expression to the list of expressions for the entity
|
|
308
|
+
result[entity].append(expression)
|
|
309
|
+
|
|
310
|
+
return result
|
|
311
|
+
|
|
312
|
+
def marshal_record(self, record) -> dict:
|
|
313
|
+
object_set = {}
|
|
314
|
+
for name, value in record.items():
|
|
315
|
+
prop = self.selection_results[name]
|
|
316
|
+
parts = name.split(".")
|
|
317
|
+
component = (
|
|
318
|
+
parts[0]
|
|
319
|
+
if len(parts) > 1
|
|
320
|
+
else self.prefix_map[str(self.schema_object.api_name)]
|
|
321
|
+
)
|
|
322
|
+
obj = object_set.get(component, {})
|
|
323
|
+
if not obj:
|
|
324
|
+
object_set[component] = obj
|
|
325
|
+
obj[prop.api_name] = prop.convert_to_api_value(value)
|
|
326
|
+
|
|
327
|
+
result = object_set[self.prefix_map[str(self.schema_object.api_name)]]
|
|
328
|
+
for name, prefix in self.prefix_map.items():
|
|
329
|
+
if name != self.schema_object.api_name and prefix in object_set:
|
|
330
|
+
result[name] = object_set[prefix]
|
|
331
|
+
|
|
332
|
+
return result
|
|
333
|
+
|
|
334
|
+
@property
|
|
335
|
+
def order_by_expression(self) -> str:
|
|
336
|
+
fields_str = self.operation.metadata_params.get("sort", None)
|
|
337
|
+
if not fields_str:
|
|
338
|
+
return ""
|
|
339
|
+
|
|
340
|
+
# determine the columns requested
|
|
341
|
+
fields = fields_str.replace(",", " ").split()
|
|
342
|
+
|
|
343
|
+
order_set = []
|
|
344
|
+
use_prefixes = False
|
|
345
|
+
for field in fields:
|
|
346
|
+
# handle order
|
|
347
|
+
field_parts = field.split(":")
|
|
348
|
+
field_name = field_parts[0]
|
|
349
|
+
|
|
350
|
+
order = "asc" if len(field_parts) == 1 else field_parts[1]
|
|
351
|
+
if order != "desc" and order != "asc":
|
|
352
|
+
raise ApplicationException(400, f"unrecognized sorting order: {field}")
|
|
353
|
+
|
|
354
|
+
# handle entity prefix
|
|
355
|
+
field_parts = field_name.split(".")
|
|
356
|
+
if len(field_parts) == 1:
|
|
357
|
+
prefix = self.prefix_map[str(self.schema_object.api_name)]
|
|
358
|
+
prop = self.schema_object.properties.get(field_parts[0])
|
|
359
|
+
if not prop:
|
|
360
|
+
raise ApplicationException(
|
|
361
|
+
400,
|
|
362
|
+
f"Invalid order by property, schema object: {self.schema_object.api_name} does not have a property: {field_parts[0]}", # noqa E501
|
|
363
|
+
)
|
|
364
|
+
column = prop.column_name
|
|
365
|
+
else:
|
|
366
|
+
# Extract the schema object for the current entity
|
|
367
|
+
relation_property = self.schema_object.relations.get(field_parts[0])
|
|
368
|
+
if not relation_property:
|
|
369
|
+
raise ApplicationException(
|
|
370
|
+
400,
|
|
371
|
+
f"Invalid order by property, schema object: {self.schema_object.api_name} does not have a property: {field_parts[0]}", # noqa E501
|
|
372
|
+
)
|
|
373
|
+
|
|
374
|
+
if relation_property:
|
|
375
|
+
if relation_property.type == "array":
|
|
376
|
+
raise ApplicationException(
|
|
377
|
+
400,
|
|
378
|
+
f"Invalid order by array property is not supported, schema object: {self.schema_object.api_name} property: {field_parts[0]}", # noqa E501
|
|
379
|
+
)
|
|
380
|
+
|
|
381
|
+
# Use a default value if relation_property is None
|
|
382
|
+
schema_object = relation_property.child_schema_object
|
|
383
|
+
else:
|
|
384
|
+
schema_object = self.schema_object
|
|
385
|
+
|
|
386
|
+
prefix = self.prefix_map[field_parts[0]]
|
|
387
|
+
prop = schema_object.properties.get(field_parts[1])
|
|
388
|
+
if not prop:
|
|
389
|
+
raise ApplicationException(
|
|
390
|
+
400,
|
|
391
|
+
f"Invalid order by property, schema object: {schema_object.api_name} does not have a property: {field_parts[1]}", # noqa E501
|
|
392
|
+
)
|
|
393
|
+
column = prop.column_name
|
|
394
|
+
self.active_prefixes.add(prefix)
|
|
395
|
+
use_prefixes = True
|
|
396
|
+
|
|
397
|
+
order_set.append((prefix, column, order))
|
|
398
|
+
|
|
399
|
+
if len(order_set) == 0:
|
|
400
|
+
return ""
|
|
401
|
+
order_parts = []
|
|
402
|
+
for prefix, column, order in order_set:
|
|
403
|
+
if use_prefixes:
|
|
404
|
+
order_parts.append(f"{prefix}.{column} {order}")
|
|
405
|
+
else:
|
|
406
|
+
order_parts.append(f"{column} {order}")
|
|
407
|
+
return " ORDER BY " + ", ".join(order_parts)
|
|
408
|
+
|
|
409
|
+
@property
|
|
410
|
+
def limit_expression(self) -> str:
|
|
411
|
+
limit_str = self.operation.metadata_params.get("limit", None)
|
|
412
|
+
if not limit_str:
|
|
413
|
+
return ""
|
|
414
|
+
|
|
415
|
+
if isinstance(limit_str, str) and not limit_str.isdigit():
|
|
416
|
+
raise ApplicationException(
|
|
417
|
+
400, f"Limit is not an valid integer {limit_str}"
|
|
418
|
+
)
|
|
419
|
+
|
|
420
|
+
return f" LIMIT {limit_str}"
|
|
421
|
+
|
|
422
|
+
@property
|
|
423
|
+
def offset_expression(self) -> str:
|
|
424
|
+
offset_str = self.operation.metadata_params.get("offset", None)
|
|
425
|
+
if not offset_str:
|
|
426
|
+
return ""
|
|
427
|
+
|
|
428
|
+
if isinstance(offset_str, str) and not offset_str.isdigit():
|
|
429
|
+
raise ApplicationException(
|
|
430
|
+
400, f"Offset is not an valid integer {offset_str}"
|
|
431
|
+
)
|
|
432
|
+
|
|
433
|
+
return f" offset {offset_str}"
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
from typing import Optional
|
|
2
|
+
|
|
3
|
+
from api_foundry_query_engine.dao.sql_query_handler import SQLSchemaQueryHandler
|
|
4
|
+
from api_foundry_query_engine.dao.sql_select_query_handler import (
|
|
5
|
+
SQLSelectSchemaQueryHandler,
|
|
6
|
+
)
|
|
7
|
+
from api_foundry_query_engine.operation import Operation
|
|
8
|
+
from api_foundry_query_engine.utils.api_model import SchemaObjectAssociation
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class SQLSubselectSchemaQueryHandler(SQLSelectSchemaQueryHandler):
|
|
12
|
+
def __init__(
|
|
13
|
+
self,
|
|
14
|
+
operation: Operation,
|
|
15
|
+
relation: SchemaObjectAssociation,
|
|
16
|
+
parent_generator: SQLSchemaQueryHandler,
|
|
17
|
+
) -> None:
|
|
18
|
+
super().__init__(
|
|
19
|
+
operation, relation.child_schema_object, parent_generator.engine
|
|
20
|
+
)
|
|
21
|
+
self.relation = relation
|
|
22
|
+
self.parent_generator = parent_generator
|
|
23
|
+
|
|
24
|
+
@property
|
|
25
|
+
def selection_results(self) -> dict:
|
|
26
|
+
filter_str = self.operation.metadata_params.get("properties", ".*")
|
|
27
|
+
result = {self.relation.child_property: self.relation.child_property}
|
|
28
|
+
|
|
29
|
+
for relation_name, reg_exs in self.get_regex_map(filter_str).items():
|
|
30
|
+
if relation_name != self.relation.api_name:
|
|
31
|
+
continue
|
|
32
|
+
|
|
33
|
+
schema_object = self.relation.child_schema_object
|
|
34
|
+
|
|
35
|
+
# Filter and prefix keys for the current entity and regular expressions
|
|
36
|
+
filtered_keys = self.filter_and_prefix_keys(
|
|
37
|
+
reg_exs, schema_object.properties
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
# Extend the result map with the filtered keys
|
|
41
|
+
result.update(filtered_keys)
|
|
42
|
+
|
|
43
|
+
return result
|
|
44
|
+
|
|
45
|
+
@property
|
|
46
|
+
def placeholders(self) -> dict:
|
|
47
|
+
return self.search_placeholders
|
|
48
|
+
|
|
49
|
+
@property
|
|
50
|
+
def sql(self) -> Optional[str]:
|
|
51
|
+
if len(self.select_list_columns) == 1: # then it only contains the key
|
|
52
|
+
return None
|
|
53
|
+
|
|
54
|
+
sql = (
|
|
55
|
+
f"SELECT {self.select_list} "
|
|
56
|
+
+ f"FROM {self.relation.child_schema_object.qualified_name} "
|
|
57
|
+
+ f"WHERE {self.relation.child_property} "
|
|
58
|
+
+ f"IN ( SELECT {self.relation.parent_property} "
|
|
59
|
+
+ f"FROM {self.parent_generator.table_expression}"
|
|
60
|
+
+ f"{self.parent_generator.search_condition} "
|
|
61
|
+
# + f"{order_by} {limit} {offset})"
|
|
62
|
+
+ ")"
|
|
63
|
+
)
|
|
64
|
+
self.search_placeholders = self.parent_generator.search_placeholders
|
|
65
|
+
# self._execute_sql(args["cursor"], sql, query_parameters)
|
|
66
|
+
return sql
|