api-foundry-query-engine 0.8.62__tar.gz → 0.8.64__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.62 → api_foundry_query_engine-0.8.64}/PKG-INFO +1 -1
- api_foundry_query_engine-0.8.64/api_foundry_query_engine/__init__.py +1 -0
- {api_foundry_query_engine-0.8.62 → api_foundry_query_engine-0.8.64}/api_foundry_query_engine/adapters/gateway_adapter.py +1 -3
- {api_foundry_query_engine-0.8.62 → api_foundry_query_engine-0.8.64}/api_foundry_query_engine/dao/sql_insert_query_handler.py +36 -52
- {api_foundry_query_engine-0.8.62 → api_foundry_query_engine-0.8.64}/api_foundry_query_engine/dao/sql_query_handler.py +21 -59
- {api_foundry_query_engine-0.8.62 → api_foundry_query_engine-0.8.64}/api_foundry_query_engine/dao/sql_subselect_query_handler.py +1 -6
- {api_foundry_query_engine-0.8.62 → api_foundry_query_engine-0.8.64}/api_foundry_query_engine/dao/sql_update_query_handler.py +26 -41
- {api_foundry_query_engine-0.8.62 → api_foundry_query_engine-0.8.64}/api_foundry_query_engine/operation.py +3 -0
- {api_foundry_query_engine-0.8.62 → api_foundry_query_engine-0.8.64}/api_foundry_query_engine/utils/api_model.py +14 -35
- {api_foundry_query_engine-0.8.62 → api_foundry_query_engine-0.8.64}/api_foundry_query_engine/utils/claims_check.py +3 -4
- api_foundry_query_engine-0.8.62/api_foundry_query_engine/__init__.py +0 -1
- {api_foundry_query_engine-0.8.62 → api_foundry_query_engine-0.8.64}/.gitignore +0 -0
- {api_foundry_query_engine-0.8.62 → api_foundry_query_engine-0.8.64}/LICENSE +0 -0
- {api_foundry_query_engine-0.8.62 → api_foundry_query_engine-0.8.64}/api_foundry_query_engine/.pre-commit-config.yaml +0 -0
- {api_foundry_query_engine-0.8.62 → api_foundry_query_engine-0.8.64}/api_foundry_query_engine/adapters/adapter.py +0 -0
- {api_foundry_query_engine-0.8.62 → api_foundry_query_engine-0.8.64}/api_foundry_query_engine/adapters/case_change_adapter.py +0 -0
- {api_foundry_query_engine-0.8.62 → api_foundry_query_engine-0.8.64}/api_foundry_query_engine/adapters/security_adapter.py +0 -0
- {api_foundry_query_engine-0.8.62 → api_foundry_query_engine-0.8.64}/api_foundry_query_engine/connectors/connection.py +0 -0
- {api_foundry_query_engine-0.8.62 → api_foundry_query_engine-0.8.64}/api_foundry_query_engine/connectors/connection_factory.py +0 -0
- {api_foundry_query_engine-0.8.62 → api_foundry_query_engine-0.8.64}/api_foundry_query_engine/connectors/oracle_connector.py +0 -0
- {api_foundry_query_engine-0.8.62 → api_foundry_query_engine-0.8.64}/api_foundry_query_engine/connectors/postgres_connection.py +0 -0
- {api_foundry_query_engine-0.8.62 → api_foundry_query_engine-0.8.64}/api_foundry_query_engine/dao/batch_operation_handler.py +0 -0
- {api_foundry_query_engine-0.8.62 → api_foundry_query_engine-0.8.64}/api_foundry_query_engine/dao/dao.py +0 -0
- {api_foundry_query_engine-0.8.62 → api_foundry_query_engine-0.8.64}/api_foundry_query_engine/dao/operation_dao.py +0 -0
- {api_foundry_query_engine-0.8.62 → api_foundry_query_engine-0.8.64}/api_foundry_query_engine/dao/sql_custom_query_handler.py +0 -0
- {api_foundry_query_engine-0.8.62 → api_foundry_query_engine-0.8.64}/api_foundry_query_engine/dao/sql_delete_query_handler.py +0 -0
- {api_foundry_query_engine-0.8.62 → api_foundry_query_engine-0.8.64}/api_foundry_query_engine/dao/sql_restore_query_handler.py +0 -0
- {api_foundry_query_engine-0.8.62 → api_foundry_query_engine-0.8.64}/api_foundry_query_engine/dao/sql_select_query_handler.py +0 -0
- {api_foundry_query_engine-0.8.62 → api_foundry_query_engine-0.8.64}/api_foundry_query_engine/lambda_handler.py +0 -0
- {api_foundry_query_engine-0.8.62 → api_foundry_query_engine-0.8.64}/api_foundry_query_engine/services/service.py +0 -0
- {api_foundry_query_engine-0.8.62 → api_foundry_query_engine-0.8.64}/api_foundry_query_engine/services/transactional_service.py +0 -0
- {api_foundry_query_engine-0.8.62 → api_foundry_query_engine-0.8.64}/api_foundry_query_engine/utils/app_exception.py +0 -0
- {api_foundry_query_engine-0.8.62 → api_foundry_query_engine-0.8.64}/api_foundry_query_engine/utils/dependency_resolver.py +0 -0
- {api_foundry_query_engine-0.8.62 → api_foundry_query_engine-0.8.64}/api_foundry_query_engine/utils/logger.py +0 -0
- {api_foundry_query_engine-0.8.62 → api_foundry_query_engine-0.8.64}/api_foundry_query_engine/utils/reference_resolver.py +0 -0
- {api_foundry_query_engine-0.8.62 → api_foundry_query_engine-0.8.64}/api_foundry_query_engine/utils/token_decoder.py +0 -0
- {api_foundry_query_engine-0.8.62 → api_foundry_query_engine-0.8.64}/pyproject.toml +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.64
|
|
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
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.8.64"
|
|
@@ -43,9 +43,7 @@ class GatewayAdapter(Adapter):
|
|
|
43
43
|
parts = [p for p in resource.split("/") if p]
|
|
44
44
|
# Skip common API prefixes like "api", "v1", "v2", etc.
|
|
45
45
|
api_prefixes = {"api", "v1", "v2", "v3"}
|
|
46
|
-
literal_parts = [
|
|
47
|
-
p for p in parts if p not in api_prefixes and not p.startswith("{")
|
|
48
|
-
]
|
|
46
|
+
literal_parts = [p for p in parts if p not in api_prefixes and not p.startswith("{")]
|
|
49
47
|
entity = "_".join(literal_parts) if literal_parts else None
|
|
50
48
|
else:
|
|
51
49
|
entity = None
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import json
|
|
1
2
|
from typing import Dict, Optional
|
|
2
3
|
from api_foundry_query_engine.dao.sql_query_handler import SQLSchemaQueryHandler
|
|
3
4
|
from api_foundry_query_engine.operation import Operation
|
|
@@ -11,9 +12,7 @@ log = logger(__name__)
|
|
|
11
12
|
class SQLInsertSchemaQueryHandler(SQLSchemaQueryHandler):
|
|
12
13
|
key_property: Optional[SchemaObjectProperty]
|
|
13
14
|
|
|
14
|
-
def __init__(
|
|
15
|
-
self, operation: Operation, schema_object: SchemaObject, engine: str
|
|
16
|
-
) -> None:
|
|
15
|
+
def __init__(self, operation: Operation, schema_object: SchemaObject, engine: str) -> None:
|
|
17
16
|
super().__init__(operation, schema_object, engine)
|
|
18
17
|
self.key_property = schema_object.primary_key
|
|
19
18
|
if self.key_property:
|
|
@@ -32,9 +31,7 @@ class SQLInsertSchemaQueryHandler(SQLSchemaQueryHandler):
|
|
|
32
31
|
+ f" required. schema_object: {schema_object.api_name}",
|
|
33
32
|
)
|
|
34
33
|
self.concurrency_property = schema_object.concurrency_property
|
|
35
|
-
if self.concurrency_property and operation.store_params.get(
|
|
36
|
-
self.concurrency_property.api_name
|
|
37
|
-
):
|
|
34
|
+
if self.concurrency_property and operation.store_params.get(self.concurrency_property.api_name):
|
|
38
35
|
raise ApplicationException(
|
|
39
36
|
400,
|
|
40
37
|
"Versioned properties can not be supplied a store parameters. "
|
|
@@ -61,7 +58,9 @@ class SQLInsertSchemaQueryHandler(SQLSchemaQueryHandler):
|
|
|
61
58
|
self.schema_object.properties,
|
|
62
59
|
)
|
|
63
60
|
# Combine both (union of read and write)
|
|
64
|
-
|
|
61
|
+
combined = {**read_props, **write_props}
|
|
62
|
+
filters = self.operation.metadata_params.get("_properties", ".*").split()
|
|
63
|
+
self._insert_selection_results = self.filter_and_prefix_keys(filters, combined)
|
|
65
64
|
return self._insert_selection_results
|
|
66
65
|
|
|
67
66
|
@property
|
|
@@ -72,10 +71,7 @@ class SQLInsertSchemaQueryHandler(SQLSchemaQueryHandler):
|
|
|
72
71
|
returning_clause = self._get_returning_clause()
|
|
73
72
|
|
|
74
73
|
if not self.concurrency_property:
|
|
75
|
-
return
|
|
76
|
-
f"INSERT INTO {self.table_expression}{self.insert_values} "
|
|
77
|
-
+ returning_clause
|
|
78
|
-
)
|
|
74
|
+
return f"INSERT INTO {self.table_expression}{self.insert_values} " + returning_clause
|
|
79
75
|
|
|
80
76
|
if self.operation.store_params.get(self.concurrency_property.api_name):
|
|
81
77
|
raise ApplicationException(
|
|
@@ -85,10 +81,7 @@ class SQLInsertSchemaQueryHandler(SQLSchemaQueryHandler):
|
|
|
85
81
|
+ f" schema_object: {self.schema_object.api_name}, "
|
|
86
82
|
+ f"property: {self.concurrency_property.api_name}",
|
|
87
83
|
)
|
|
88
|
-
return
|
|
89
|
-
f"INSERT INTO {self.table_expression}{self.insert_values}"
|
|
90
|
-
+ returning_clause
|
|
91
|
-
)
|
|
84
|
+
return f"INSERT INTO {self.table_expression}{self.insert_values} " + returning_clause
|
|
92
85
|
|
|
93
86
|
def _get_returning_clause(self) -> str:
|
|
94
87
|
"""
|
|
@@ -121,22 +114,18 @@ class SQLInsertSchemaQueryHandler(SQLSchemaQueryHandler):
|
|
|
121
114
|
allowed_property_names = self.check_permissions(
|
|
122
115
|
"write", self.schema_object.permissions, self.schema_object.properties
|
|
123
116
|
)
|
|
124
|
-
allowed_properties = {
|
|
125
|
-
k: v
|
|
126
|
-
for k, v in self.schema_object.properties.items()
|
|
127
|
-
if k in allowed_property_names
|
|
128
|
-
}
|
|
117
|
+
allowed_properties = {k: v for k, v in self.schema_object.properties.items() if k in allowed_property_names}
|
|
129
118
|
log.info("allowed properties: %s", allowed_properties)
|
|
130
119
|
|
|
131
|
-
import json
|
|
132
|
-
|
|
133
|
-
# First, validate that user is not trying to set injected properties
|
|
134
120
|
for property_name, property in self.schema_object.properties.items():
|
|
135
|
-
|
|
121
|
+
inject_value = getattr(property, "inject_value", None)
|
|
122
|
+
inject_on = getattr(property, "inject_on", None) or []
|
|
123
|
+
if not inject_value or "create" not in inject_on:
|
|
124
|
+
continue
|
|
125
|
+
if property_name in self.operation.store_params:
|
|
136
126
|
raise ApplicationException(
|
|
137
127
|
403,
|
|
138
|
-
f"Property '{property_name}' is auto-injected and "
|
|
139
|
-
+ "cannot be set manually",
|
|
128
|
+
f"Property '{property_name}' is auto-injected and cannot be set manually",
|
|
140
129
|
)
|
|
141
130
|
|
|
142
131
|
for name, value in self.operation.store_params.items():
|
|
@@ -160,36 +149,33 @@ class SQLInsertSchemaQueryHandler(SQLSchemaQueryHandler):
|
|
|
160
149
|
|
|
161
150
|
columns.append(property.column_name)
|
|
162
151
|
if property.api_name is None:
|
|
163
|
-
raise ApplicationException(
|
|
164
|
-
400, f"Property '{name}' does not have a valid api_name."
|
|
165
|
-
)
|
|
152
|
+
raise ApplicationException(400, f"Property '{name}' does not have a valid api_name.")
|
|
166
153
|
placeholders.append(self.placeholder(property, property.api_name))
|
|
167
154
|
# Serialize embedded objects to JSON
|
|
168
155
|
if property.api_type == "object":
|
|
169
156
|
self.store_placeholders[property.api_name] = json.dumps(value)
|
|
170
157
|
else:
|
|
171
|
-
self.store_placeholders[
|
|
172
|
-
property.api_name
|
|
173
|
-
] = property.convert_to_db_value(value)
|
|
158
|
+
self.store_placeholders[property.api_name] = property.convert_to_db_value(value)
|
|
174
159
|
|
|
175
|
-
# Inject values from claims/timestamps/etc for properties with
|
|
176
|
-
# x-af-inject-value on CREATE
|
|
177
160
|
for property_name, property in self.schema_object.properties.items():
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
161
|
+
inject_value = getattr(property, "inject_value", None)
|
|
162
|
+
inject_on = getattr(property, "inject_on", None) or []
|
|
163
|
+
if not inject_value or "create" not in inject_on:
|
|
164
|
+
continue
|
|
165
|
+
if property_name in self.store_placeholders:
|
|
166
|
+
continue
|
|
167
|
+
|
|
168
|
+
# Preserve legacy placeholder names for writable injected fields
|
|
169
|
+
# while using the API name for non-writable injected fields.
|
|
170
|
+
placeholder_name = (
|
|
171
|
+
f"__inject_{property_name}"
|
|
172
|
+
if property_name in allowed_properties
|
|
173
|
+
else (str(property.api_name) if property.api_name is not None else property_name)
|
|
174
|
+
)
|
|
175
|
+
injected_value = self.extract_injected_value(str(inject_value))
|
|
176
|
+
columns.append(property.column_name)
|
|
177
|
+
placeholders.append(self.placeholder(property, placeholder_name))
|
|
178
|
+
self.store_placeholders[placeholder_name] = property.convert_to_db_value(injected_value)
|
|
193
179
|
|
|
194
180
|
if self.key_property:
|
|
195
181
|
if self.key_property.key_type == "sequence":
|
|
@@ -201,8 +187,6 @@ class SQLInsertSchemaQueryHandler(SQLSchemaQueryHandler):
|
|
|
201
187
|
if self.concurrency_property.column_type == "integer":
|
|
202
188
|
placeholders.append("1")
|
|
203
189
|
else:
|
|
204
|
-
placeholders.append(
|
|
205
|
-
self.concurrency_generator(self.concurrency_property)
|
|
206
|
-
)
|
|
190
|
+
placeholders.append(self.concurrency_generator(self.concurrency_property))
|
|
207
191
|
|
|
208
192
|
return f" ( {', '.join(columns)} ) VALUES ( {', '.join(placeholders)})"
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import re
|
|
2
|
+
import os
|
|
3
|
+
import uuid
|
|
2
4
|
from typing import Optional, List, Dict
|
|
3
5
|
from datetime import datetime, date
|
|
4
6
|
|
|
@@ -108,9 +110,7 @@ class SQLQueryHandler:
|
|
|
108
110
|
operation: Operation
|
|
109
111
|
engine: str
|
|
110
112
|
|
|
111
|
-
def __init__(
|
|
112
|
-
self, operation: Operation, engine: str
|
|
113
|
-
): # , schema_object: SchemaObject):
|
|
113
|
+
def __init__(self, operation: Operation, engine: str): # , schema_object: SchemaObject):
|
|
114
114
|
self.operation = operation
|
|
115
115
|
self.engine = engine
|
|
116
116
|
self.__select_list_columns = None
|
|
@@ -180,9 +180,7 @@ class SQLQueryHandler:
|
|
|
180
180
|
List[str]: A list of properties the user is permitted to access.
|
|
181
181
|
"""
|
|
182
182
|
# if permissions are not defined then no restrictions are applied
|
|
183
|
-
log.info(
|
|
184
|
-
f"checking permissions permission_type: {permission_type}, permissions: {permissions}"
|
|
185
|
-
)
|
|
183
|
+
log.info(f"checking permissions permission_type: {permission_type}, permissions: {permissions}")
|
|
186
184
|
if not permissions:
|
|
187
185
|
return properties
|
|
188
186
|
|
|
@@ -223,9 +221,7 @@ class SQLQueryHandler:
|
|
|
223
221
|
|
|
224
222
|
if regex_pattern:
|
|
225
223
|
# Use efficient regex dictionary filtering
|
|
226
|
-
role_allowed = self._filter_properties_by_regex(
|
|
227
|
-
properties, regex_pattern
|
|
228
|
-
)
|
|
224
|
+
role_allowed = self._filter_properties_by_regex(properties, regex_pattern)
|
|
229
225
|
log.info(
|
|
230
226
|
"role: %s, pattern: %s, matched: %s",
|
|
231
227
|
role,
|
|
@@ -284,9 +280,7 @@ class SQLQueryHandler:
|
|
|
284
280
|
|
|
285
281
|
for action, rule in actions.items():
|
|
286
282
|
# Normalize action names: create/update -> write
|
|
287
|
-
normalized_action = (
|
|
288
|
-
"write" if action in ("create", "update") else action
|
|
289
|
-
)
|
|
283
|
+
normalized_action = "write" if action in ("create", "update") else action
|
|
290
284
|
|
|
291
285
|
if normalized_action in normalized["default"]:
|
|
292
286
|
normalized["default"][normalized_action][role] = rule
|
|
@@ -330,9 +324,7 @@ class SQLQueryHandler:
|
|
|
330
324
|
return permission_rule
|
|
331
325
|
elif isinstance(permission_rule, dict):
|
|
332
326
|
# Support both 'properties' (preferred) and 'fields' (legacy)
|
|
333
|
-
return permission_rule.get("properties") or permission_rule.get(
|
|
334
|
-
"fields", ""
|
|
335
|
-
)
|
|
327
|
+
return permission_rule.get("properties") or permission_rule.get("fields", "")
|
|
336
328
|
else:
|
|
337
329
|
return ""
|
|
338
330
|
|
|
@@ -340,9 +332,7 @@ class SQLQueryHandler:
|
|
|
340
332
|
def selection_results(self) -> Dict:
|
|
341
333
|
raise NotImplementedError()
|
|
342
334
|
|
|
343
|
-
def generate_sql_condition(
|
|
344
|
-
self, property: SchemaObjectProperty, value, prefix: Optional[str] = None
|
|
345
|
-
) -> str:
|
|
335
|
+
def generate_sql_condition(self, property: SchemaObjectProperty, value, prefix: Optional[str] = None) -> str:
|
|
346
336
|
operand = "="
|
|
347
337
|
if isinstance(value, str):
|
|
348
338
|
parts = value.split("::", 1)
|
|
@@ -354,9 +344,7 @@ class SQLQueryHandler:
|
|
|
354
344
|
value_str = str(value)
|
|
355
345
|
|
|
356
346
|
column = f"{prefix}.{property.column_name}" if prefix else property.column_name
|
|
357
|
-
placeholder_name =
|
|
358
|
-
f"{prefix}_{property.api_name}" if prefix else property.api_name
|
|
359
|
-
)
|
|
347
|
+
placeholder_name = f"{prefix}_{property.api_name}" if prefix else property.api_name
|
|
360
348
|
|
|
361
349
|
if operand in ["between", "not-between"]:
|
|
362
350
|
value_set = value_str.split(",")
|
|
@@ -364,17 +352,14 @@ class SQLQueryHandler:
|
|
|
364
352
|
elif operand in ["in", "not-in"]:
|
|
365
353
|
value_set = value_str.split(",")
|
|
366
354
|
assignments = [
|
|
367
|
-
self.placeholder(property, f"{placeholder_name}_{index}")
|
|
368
|
-
for index, _ in enumerate(value_set)
|
|
355
|
+
self.placeholder(property, f"{placeholder_name}_{index}") for index, _ in enumerate(value_set)
|
|
369
356
|
]
|
|
370
357
|
sql = f"{column} {'NOT ' if operand == 'not-in' else ''}IN ({', '.join(assignments)})" # noqa E501
|
|
371
358
|
else:
|
|
372
359
|
sql = f"{column} {operand} {self.placeholder(property, str(placeholder_name))}"
|
|
373
360
|
return sql
|
|
374
361
|
|
|
375
|
-
def generate_placeholders(
|
|
376
|
-
self, property: SchemaObjectProperty, value, prefix: Optional[str] = None
|
|
377
|
-
) -> dict:
|
|
362
|
+
def generate_placeholders(self, property: SchemaObjectProperty, value, prefix: Optional[str] = None) -> dict:
|
|
378
363
|
operand = "="
|
|
379
364
|
|
|
380
365
|
if isinstance(value, str):
|
|
@@ -386,9 +371,7 @@ class SQLQueryHandler:
|
|
|
386
371
|
else:
|
|
387
372
|
value_str = str(value)
|
|
388
373
|
|
|
389
|
-
placeholder_name =
|
|
390
|
-
f"{prefix}_{property.api_name}" if prefix else property.api_name
|
|
391
|
-
)
|
|
374
|
+
placeholder_name = f"{prefix}_{property.api_name}" if prefix else property.api_name
|
|
392
375
|
placeholders = {}
|
|
393
376
|
|
|
394
377
|
if operand in ["between", "not-between"]:
|
|
@@ -428,9 +411,6 @@ class SQLQueryHandler:
|
|
|
428
411
|
Raises:
|
|
429
412
|
ApplicationException: If source format is invalid
|
|
430
413
|
"""
|
|
431
|
-
import os
|
|
432
|
-
import uuid
|
|
433
|
-
|
|
434
414
|
if inject_value_source.startswith("claim:"):
|
|
435
415
|
claim_key = inject_value_source[6:]
|
|
436
416
|
return self.operation.claims.get(claim_key)
|
|
@@ -444,17 +424,13 @@ class SQLQueryHandler:
|
|
|
444
424
|
env_key = inject_value_source[4:]
|
|
445
425
|
return os.environ.get(env_key)
|
|
446
426
|
else:
|
|
447
|
-
raise ApplicationException(
|
|
448
|
-
400, f"Unknown inject value source: {inject_value_source}"
|
|
449
|
-
)
|
|
427
|
+
raise ApplicationException(400, f"Unknown inject value source: {inject_value_source}")
|
|
450
428
|
|
|
451
429
|
|
|
452
430
|
class SQLSchemaQueryHandler(SQLQueryHandler):
|
|
453
431
|
schema_object: SchemaObject
|
|
454
432
|
|
|
455
|
-
def __init__(
|
|
456
|
-
self, operation: Operation, schema_object: SchemaObject, engine: str
|
|
457
|
-
) -> None:
|
|
433
|
+
def __init__(self, operation: Operation, schema_object: SchemaObject, engine: str) -> None:
|
|
458
434
|
super().__init__(operation, engine)
|
|
459
435
|
self.schema_object = schema_object
|
|
460
436
|
self.single_table = self.__single_table()
|
|
@@ -483,10 +459,7 @@ class SQLSchemaQueryHandler(SQLQueryHandler):
|
|
|
483
459
|
entity_lower = entity.lower()
|
|
484
460
|
for i in range(1, len(entity_lower) + 1):
|
|
485
461
|
substring = entity_lower[:i]
|
|
486
|
-
if (
|
|
487
|
-
substring not in self._prefix_map.values()
|
|
488
|
-
and substring not in SQL_RESERVED_WORDS
|
|
489
|
-
):
|
|
462
|
+
if substring not in self._prefix_map.values() and substring not in SQL_RESERVED_WORDS:
|
|
490
463
|
self._prefix_map[entity] = substring
|
|
491
464
|
break
|
|
492
465
|
return self._prefix_map
|
|
@@ -526,9 +499,7 @@ class SQLSchemaQueryHandler(SQLQueryHandler):
|
|
|
526
499
|
allowed_properties = self.check_permissions(
|
|
527
500
|
"read", self.schema_object.permissions, self.schema_object.properties
|
|
528
501
|
)
|
|
529
|
-
self.__selection_results = self.filter_and_prefix_keys(
|
|
530
|
-
filters, allowed_properties
|
|
531
|
-
)
|
|
502
|
+
self.__selection_results = self.filter_and_prefix_keys(filters, allowed_properties)
|
|
532
503
|
return self.__selection_results
|
|
533
504
|
|
|
534
505
|
def _has_soft_delete_conflicts(self) -> Dict[str, bool]:
|
|
@@ -617,8 +588,7 @@ class SQLSchemaQueryHandler(SQLQueryHandler):
|
|
|
617
588
|
if excluded_values:
|
|
618
589
|
# Format values for SQL IN clause
|
|
619
590
|
formatted_values = ", ".join(
|
|
620
|
-
f"'{val}'" if isinstance(val, str) else str(val)
|
|
621
|
-
for val in excluded_values
|
|
591
|
+
f"'{val}'" if isinstance(val, str) else str(val) for val in excluded_values
|
|
622
592
|
)
|
|
623
593
|
conditions.append(f"{column_name} NOT IN ({formatted_values})")
|
|
624
594
|
|
|
@@ -636,20 +606,14 @@ class SQLSchemaQueryHandler(SQLQueryHandler):
|
|
|
636
606
|
|
|
637
607
|
for name, value in self.operation.query_params.items():
|
|
638
608
|
if "." in name:
|
|
639
|
-
raise ApplicationException(
|
|
640
|
-
400, "Selection on relations is not supported"
|
|
641
|
-
)
|
|
609
|
+
raise ApplicationException(400, "Selection on relations is not supported")
|
|
642
610
|
property = self.schema_object.properties.get(name)
|
|
643
611
|
if not property:
|
|
644
|
-
raise ApplicationException(
|
|
645
|
-
500, f"Search condition column not found {name}"
|
|
646
|
-
)
|
|
612
|
+
raise ApplicationException(500, f"Search condition column not found {name}")
|
|
647
613
|
if (
|
|
648
614
|
self.operation.action != "read"
|
|
649
615
|
and isinstance(value, str)
|
|
650
|
-
and re.match(
|
|
651
|
-
r"^(lt|le|eq|ne|gt|ge|in|not-in|between|not-between)::(.+)$", value
|
|
652
|
-
)
|
|
616
|
+
and re.match(r"^(lt|le|eq|ne|gt|ge|in|not-in|between|not-between)::(.+)$", value)
|
|
653
617
|
and self.schema_object.concurrency_property
|
|
654
618
|
):
|
|
655
619
|
raise ApplicationException(
|
|
@@ -665,9 +629,7 @@ class SQLSchemaQueryHandler(SQLQueryHandler):
|
|
|
665
629
|
self.search_placeholders.update(holders)
|
|
666
630
|
return f" WHERE {' AND '.join(conditions)}" if conditions else ""
|
|
667
631
|
|
|
668
|
-
def filter_and_prefix_keys(
|
|
669
|
-
self, regex_list: List[str], properties: dict, prefix: Optional[str] = None
|
|
670
|
-
) -> dict:
|
|
632
|
+
def filter_and_prefix_keys(self, regex_list: List[str], properties: dict, prefix: Optional[str] = None) -> dict:
|
|
671
633
|
"""
|
|
672
634
|
Accepts a prefix string, list of regular expressions, and a dictionary.
|
|
673
635
|
Returns a new dictionary containing items whose keys match any of the
|
|
@@ -51,16 +51,11 @@ class SQLSubselectSchemaQueryHandler(SQLSelectSchemaQueryHandler):
|
|
|
51
51
|
if len(self.select_list_columns) == 1: # then it only contains the key
|
|
52
52
|
return None
|
|
53
53
|
|
|
54
|
-
# Get the parent table alias to avoid ambiguous column references
|
|
55
|
-
parent_alias = self.parent_generator.prefix_map[
|
|
56
|
-
str(self.parent_generator.schema_object.api_name)
|
|
57
|
-
]
|
|
58
|
-
|
|
59
54
|
sql = (
|
|
60
55
|
f"SELECT {self.select_list} "
|
|
61
56
|
+ f"FROM {self.relation.child_schema_object.qualified_name} "
|
|
62
57
|
+ f"WHERE {self.relation.child_property} "
|
|
63
|
-
+ f"IN ( SELECT {
|
|
58
|
+
+ f"IN ( SELECT {self.relation.parent_property} "
|
|
64
59
|
+ f"FROM {self.parent_generator.table_expression}"
|
|
65
60
|
+ f"{self.parent_generator.search_condition} "
|
|
66
61
|
# + f"{order_by} {limit} {offset})"
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import json
|
|
1
2
|
import re
|
|
2
3
|
from typing import Match
|
|
3
4
|
from api_foundry_query_engine.dao.sql_query_handler import SQLSchemaQueryHandler
|
|
@@ -7,9 +8,7 @@ from api_foundry_query_engine.utils.api_model import SchemaObject
|
|
|
7
8
|
|
|
8
9
|
|
|
9
10
|
class SQLUpdateSchemaQueryHandler(SQLSchemaQueryHandler):
|
|
10
|
-
def __init__(
|
|
11
|
-
self, operation: Operation, schema_object: SchemaObject, engine: str
|
|
12
|
-
) -> None:
|
|
11
|
+
def __init__(self, operation: Operation, schema_object: SchemaObject, engine: str) -> None:
|
|
13
12
|
super().__init__(operation, schema_object, engine)
|
|
14
13
|
|
|
15
14
|
def _template_where(self, expr: str) -> str:
|
|
@@ -106,24 +105,20 @@ class SQLUpdateSchemaQueryHandler(SQLSchemaQueryHandler):
|
|
|
106
105
|
allowed_property_names = self.check_permissions(
|
|
107
106
|
"write", self.schema_object.permissions, self.schema_object.properties
|
|
108
107
|
)
|
|
109
|
-
allowed_properties = {
|
|
110
|
-
k: v
|
|
111
|
-
for k, v in self.schema_object.properties.items()
|
|
112
|
-
if k in allowed_property_names
|
|
113
|
-
}
|
|
108
|
+
allowed_properties = {k: v for k, v in self.schema_object.properties.items() if k in allowed_property_names}
|
|
114
109
|
self.store_placeholders = {}
|
|
115
110
|
columns = []
|
|
116
111
|
invalid_columns = []
|
|
117
112
|
|
|
118
|
-
import json
|
|
119
|
-
|
|
120
|
-
# First, validate that user is not trying to set injected properties
|
|
121
113
|
for property_name, property in self.schema_object.properties.items():
|
|
122
|
-
|
|
114
|
+
inject_value = getattr(property, "inject_value", None)
|
|
115
|
+
inject_on = getattr(property, "inject_on", None) or []
|
|
116
|
+
if not inject_value:
|
|
117
|
+
continue
|
|
118
|
+
if property_name in self.operation.store_params:
|
|
123
119
|
raise ApplicationException(
|
|
124
120
|
403,
|
|
125
|
-
f"Property '{property_name}' is auto-injected and "
|
|
126
|
-
+ "cannot be set manually",
|
|
121
|
+
f"Property '{property_name}' is auto-injected and cannot be set manually",
|
|
127
122
|
)
|
|
128
123
|
|
|
129
124
|
for name, value in self.operation.store_params.items():
|
|
@@ -132,9 +127,7 @@ class SQLUpdateSchemaQueryHandler(SQLSchemaQueryHandler):
|
|
|
132
127
|
invalid_columns.append(name)
|
|
133
128
|
continue
|
|
134
129
|
|
|
135
|
-
placeholder = (
|
|
136
|
-
str(property.api_name) if property.api_name is not None else name
|
|
137
|
-
)
|
|
130
|
+
placeholder = str(property.api_name) if property.api_name is not None else name
|
|
138
131
|
column_name = property.column_name
|
|
139
132
|
|
|
140
133
|
columns.append(f"{column_name} = {self.placeholder(property, placeholder)}")
|
|
@@ -142,36 +135,28 @@ class SQLUpdateSchemaQueryHandler(SQLSchemaQueryHandler):
|
|
|
142
135
|
if property.api_type == "object":
|
|
143
136
|
self.store_placeholders[placeholder] = json.dumps(value)
|
|
144
137
|
else:
|
|
145
|
-
self.store_placeholders[placeholder] = property.convert_to_db_value(
|
|
146
|
-
value
|
|
147
|
-
)
|
|
138
|
+
self.store_placeholders[placeholder] = property.convert_to_db_value(value)
|
|
148
139
|
|
|
149
|
-
# Inject values from claims/timestamps/etc for properties with
|
|
150
|
-
# x-af-inject-value on UPDATE
|
|
151
140
|
for property_name, property in self.schema_object.properties.items():
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
f"Required injected property '{property_name}' "
|
|
167
|
-
+ f"could not be populated from '{property.inject_value}'",
|
|
168
|
-
)
|
|
141
|
+
inject_value = getattr(property, "inject_value", None)
|
|
142
|
+
inject_on = getattr(property, "inject_on", None) or []
|
|
143
|
+
if not inject_value or "update" not in inject_on:
|
|
144
|
+
continue
|
|
145
|
+
|
|
146
|
+
placeholder = (
|
|
147
|
+
f"__inject_{property_name}"
|
|
148
|
+
if property_name in allowed_properties
|
|
149
|
+
else (str(property.api_name) if property.api_name is not None else property_name)
|
|
150
|
+
)
|
|
151
|
+
columns.append(f"{property.column_name} = {self.placeholder(property, placeholder)}")
|
|
152
|
+
self.store_placeholders[placeholder] = property.convert_to_db_value(
|
|
153
|
+
self.extract_injected_value(str(inject_value))
|
|
154
|
+
)
|
|
169
155
|
|
|
170
156
|
if invalid_columns:
|
|
171
157
|
raise ApplicationException(
|
|
172
158
|
403,
|
|
173
|
-
f"Subject does not have permission to update properties: "
|
|
174
|
-
f"{invalid_columns}",
|
|
159
|
+
f"Subject does not have permission to update properties: " f"{invalid_columns}",
|
|
175
160
|
)
|
|
176
161
|
return f" SET {', '.join(columns)}"
|
|
177
162
|
|
|
@@ -40,6 +40,7 @@ class Operation:
|
|
|
40
40
|
store_params: Optional[Dict[str, Any]] = None,
|
|
41
41
|
metadata_params: Optional[Dict[str, Any]] = None,
|
|
42
42
|
claims: Optional[Dict[str, Any]] = None,
|
|
43
|
+
roles: Optional[List[str]] = None,
|
|
43
44
|
):
|
|
44
45
|
"""
|
|
45
46
|
Initializes the Operation instance.
|
|
@@ -76,6 +77,8 @@ class Operation:
|
|
|
76
77
|
|
|
77
78
|
# Roles defining the context in which the operation is allowed.
|
|
78
79
|
self.claims = claims or {}
|
|
80
|
+
if roles is not None and "roles" not in self.claims:
|
|
81
|
+
self.claims["roles"] = roles
|
|
79
82
|
|
|
80
83
|
# Log the operation for debugging and audit purposes
|
|
81
84
|
logger.info(
|
|
@@ -207,9 +207,7 @@ class SchemaObjectAssociation:
|
|
|
207
207
|
raise ValueError(f"Primary key not defined for schema '{self.schema_name}'")
|
|
208
208
|
column_name = getattr(child_schema.primary_key, "column_name", None)
|
|
209
209
|
if column_name is None:
|
|
210
|
-
raise ValueError(
|
|
211
|
-
f"Primary key property does not have 'column_name' for schema '{self.schema_name}'"
|
|
212
|
-
)
|
|
210
|
+
raise ValueError(f"Primary key property does not have 'column_name' for schema '{self.schema_name}'")
|
|
213
211
|
return column_name
|
|
214
212
|
|
|
215
213
|
@property
|
|
@@ -220,14 +218,10 @@ class SchemaObjectAssociation:
|
|
|
220
218
|
if not parent_schema_obj:
|
|
221
219
|
raise ValueError(f"SchemaObject '{self.parent_schema}' not found")
|
|
222
220
|
if not parent_schema_obj.primary_key:
|
|
223
|
-
raise ValueError(
|
|
224
|
-
f"Primary key not defined for schema '{self.parent_schema}'"
|
|
225
|
-
)
|
|
221
|
+
raise ValueError(f"Primary key not defined for schema '{self.parent_schema}'")
|
|
226
222
|
column_name = getattr(parent_schema_obj.primary_key, "column_name", None)
|
|
227
223
|
if column_name is None:
|
|
228
|
-
raise ValueError(
|
|
229
|
-
f"Primary key property does not have 'column_name' for schema '{self.parent_schema}'"
|
|
230
|
-
)
|
|
224
|
+
raise ValueError(f"Primary key property does not have 'column_name' for schema '{self.parent_schema}'")
|
|
231
225
|
return column_name
|
|
232
226
|
|
|
233
227
|
def __repr__(self):
|
|
@@ -255,23 +249,16 @@ class SchemaObject:
|
|
|
255
249
|
self.database: str = str(data.get("database"))
|
|
256
250
|
self.schema: Optional[str] = data.get("schema")
|
|
257
251
|
self.table_name: str = str(data.get("table_name"))
|
|
258
|
-
self.qualified_name: str =
|
|
259
|
-
f"{self.schema}.{self.table_name}" if self.schema else self.table_name
|
|
260
|
-
)
|
|
252
|
+
self.qualified_name: str = f"{self.schema}.{self.table_name}" if self.schema else self.table_name
|
|
261
253
|
self.properties: Dict[str, SchemaObjectProperty] = {
|
|
262
|
-
name: SchemaObjectProperty(prop_data)
|
|
263
|
-
for name, prop_data in data.get("properties", {}).items()
|
|
254
|
+
name: SchemaObjectProperty(prop_data) for name, prop_data in data.get("properties", {}).items()
|
|
264
255
|
}
|
|
265
256
|
self.relations = {
|
|
266
|
-
name: SchemaObjectAssociation(
|
|
267
|
-
self.api_name if self.api_name is not None else "", assoc_data
|
|
268
|
-
)
|
|
257
|
+
name: SchemaObjectAssociation(self.api_name if self.api_name is not None else "", assoc_data)
|
|
269
258
|
for name, assoc_data in data.get("relations", {}).items()
|
|
270
259
|
}
|
|
271
260
|
self.concurrency_property = (
|
|
272
|
-
self.properties[str(data.get("concurrency_property"))]
|
|
273
|
-
if data.get("concurrency_property")
|
|
274
|
-
else None
|
|
261
|
+
self.properties[str(data.get("concurrency_property"))] if data.get("concurrency_property") else None
|
|
275
262
|
)
|
|
276
263
|
self._primary_key: str = str(data.get("primary_key"))
|
|
277
264
|
self.permissions = data.get("permissions")
|
|
@@ -293,8 +280,7 @@ class SchemaObject:
|
|
|
293
280
|
name: prop
|
|
294
281
|
for name, prop in self.properties.items()
|
|
295
282
|
if prop.is_soft_delete_field()
|
|
296
|
-
and prop.get_soft_delete_strategy()
|
|
297
|
-
in ["null_check", "boolean_flag", "exclude_values"]
|
|
283
|
+
and prop.get_soft_delete_strategy() in ["null_check", "boolean_flag", "exclude_values"]
|
|
298
284
|
}
|
|
299
285
|
|
|
300
286
|
def get_soft_delete_audit_properties(
|
|
@@ -304,8 +290,7 @@ class SchemaObject:
|
|
|
304
290
|
return {
|
|
305
291
|
name: prop
|
|
306
292
|
for name, prop in self.properties.items()
|
|
307
|
-
if prop.is_soft_delete_field()
|
|
308
|
-
and prop.get_soft_delete_strategy() == "audit_field"
|
|
293
|
+
if prop.is_soft_delete_field() and prop.get_soft_delete_strategy() == "audit_field"
|
|
309
294
|
}
|
|
310
295
|
|
|
311
296
|
def get_soft_delete_strategies(self) -> List[str]:
|
|
@@ -326,12 +311,10 @@ class PathOperation:
|
|
|
326
311
|
self.sql: str = data["sql"]
|
|
327
312
|
self.database: str = data["database"]
|
|
328
313
|
self.inputs: Dict[str, SchemaObjectProperty] = {
|
|
329
|
-
name: SchemaObjectProperty(input_data)
|
|
330
|
-
for name, input_data in data.get("inputs", {}).items()
|
|
314
|
+
name: SchemaObjectProperty(input_data) for name, input_data in data.get("inputs", {}).items()
|
|
331
315
|
}
|
|
332
316
|
self.outputs: Dict[str, SchemaObjectProperty] = {
|
|
333
|
-
name: SchemaObjectProperty(output_data)
|
|
334
|
-
for name, output_data in data.get("outputs", {}).items()
|
|
317
|
+
name: SchemaObjectProperty(output_data) for name, output_data in data.get("outputs", {}).items()
|
|
335
318
|
}
|
|
336
319
|
self.permissions = data.get("security")
|
|
337
320
|
|
|
@@ -345,12 +328,10 @@ class APIModel:
|
|
|
345
328
|
def __init__(self, config: Dict[str, Any]):
|
|
346
329
|
log.info("building api_model")
|
|
347
330
|
self.schema_objects = {
|
|
348
|
-
name: SchemaObject(schema_data)
|
|
349
|
-
for name, schema_data in config.get("schema_objects", {}).items()
|
|
331
|
+
name: SchemaObject(schema_data) for name, schema_data in config.get("schema_objects", {}).items()
|
|
350
332
|
}
|
|
351
333
|
self.path_operations = {
|
|
352
|
-
name: PathOperation(path_data)
|
|
353
|
-
for name, path_data in config.get("path_operations", {}).items()
|
|
334
|
+
name: PathOperation(path_data) for name, path_data in config.get("path_operations", {}).items()
|
|
354
335
|
}
|
|
355
336
|
|
|
356
337
|
def get_path_operation(self, path: str, method: str) -> Optional[PathOperation]:
|
|
@@ -373,8 +354,6 @@ def set_api_model(engine_config: Mapping[str, str]):
|
|
|
373
354
|
api_model = APIModel(yaml.safe_load(engine_config["API_SPEC"]))
|
|
374
355
|
else:
|
|
375
356
|
log.info("Loading API model from file")
|
|
376
|
-
with open(
|
|
377
|
-
os.environ.get("API_SPEC", "/var/task/api_spec.yaml"), "r"
|
|
378
|
-
) as file:
|
|
357
|
+
with open(os.environ.get("API_SPEC", "/var/task/api_spec.yaml"), "r") as file:
|
|
379
358
|
api_model = APIModel(yaml.safe_load(file))
|
|
380
359
|
log.info("Loaded API model: %s", api_model)
|
|
@@ -420,10 +420,9 @@ def _extract_entity_from_path(event: Dict[str, Any]) -> Optional[str]:
|
|
|
420
420
|
if not literal_parts:
|
|
421
421
|
return None
|
|
422
422
|
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
return "_".join(literal_parts)
|
|
423
|
+
# For API paths with prefixes (e.g. "/chinook-api/album"),
|
|
424
|
+
# the entity is the most specific trailing literal segment.
|
|
425
|
+
return literal_parts[-1]
|
|
427
426
|
|
|
428
427
|
|
|
429
428
|
def _scope_matches(user_scopes: list, required_scope: str, operation: str, entity: str) -> bool:
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__ = "0.8.62"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|