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.
Files changed (37) hide show
  1. api_foundry_query_engine/.pre-commit-config.yaml +22 -0
  2. api_foundry_query_engine/__init__.py +1 -0
  3. api_foundry_query_engine/adapters/adapter.py +73 -0
  4. api_foundry_query_engine/adapters/case_change_adapter.py +79 -0
  5. api_foundry_query_engine/adapters/gateway_adapter.py +191 -0
  6. api_foundry_query_engine/adapters/security_adapter.py +106 -0
  7. api_foundry_query_engine/connectors/connection.py +32 -0
  8. api_foundry_query_engine/connectors/connection_factory.py +115 -0
  9. api_foundry_query_engine/connectors/oracle_connector.py +29 -0
  10. api_foundry_query_engine/connectors/postgres_connection.py +142 -0
  11. api_foundry_query_engine/dao/batch_operation_handler.py +295 -0
  12. api_foundry_query_engine/dao/dao.py +23 -0
  13. api_foundry_query_engine/dao/operation_dao.py +186 -0
  14. api_foundry_query_engine/dao/sql_custom_query_handler.py +68 -0
  15. api_foundry_query_engine/dao/sql_delete_query_handler.py +146 -0
  16. api_foundry_query_engine/dao/sql_insert_query_handler.py +187 -0
  17. api_foundry_query_engine/dao/sql_query_handler.py +713 -0
  18. api_foundry_query_engine/dao/sql_restore_query_handler.py +195 -0
  19. api_foundry_query_engine/dao/sql_select_query_handler.py +433 -0
  20. api_foundry_query_engine/dao/sql_subselect_query_handler.py +66 -0
  21. api_foundry_query_engine/dao/sql_update_query_handler.py +198 -0
  22. api_foundry_query_engine/lambda_handler.py +63 -0
  23. api_foundry_query_engine/operation.py +104 -0
  24. api_foundry_query_engine/services/service.py +75 -0
  25. api_foundry_query_engine/services/transactional_service.py +52 -0
  26. api_foundry_query_engine/utils/api_model.py +380 -0
  27. api_foundry_query_engine/utils/app_exception.py +22 -0
  28. api_foundry_query_engine/utils/claims_check.py +471 -0
  29. api_foundry_query_engine/utils/dependency_resolver.py +157 -0
  30. api_foundry_query_engine/utils/gateway_operation.py +279 -0
  31. api_foundry_query_engine/utils/logger.py +60 -0
  32. api_foundry_query_engine/utils/reference_resolver.py +222 -0
  33. api_foundry_query_engine/utils/token_decoder.py +624 -0
  34. api_foundry_query_engine-0.8.39.dist-info/METADATA +21 -0
  35. api_foundry_query_engine-0.8.39.dist-info/RECORD +37 -0
  36. api_foundry_query_engine-0.8.39.dist-info/WHEEL +4 -0
  37. 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)})"