api-foundry-query-engine 0.8.10__tar.gz → 0.8.12__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.10 → api_foundry_query_engine-0.8.12}/PKG-INFO +7 -1
- {api_foundry_query_engine-0.8.10 → api_foundry_query_engine-0.8.12}/api_foundry_query_engine/adapters/gateway_adapter.py +53 -8
- {api_foundry_query_engine-0.8.10 → api_foundry_query_engine-0.8.12}/api_foundry_query_engine/dao/sql_select_query_handler.py +97 -46
- {api_foundry_query_engine-0.8.10 → api_foundry_query_engine-0.8.12}/api_foundry_query_engine/lambda_handler.py +15 -17
- api_foundry_query_engine-0.8.12/api_foundry_query_engine/operation.py +87 -0
- {api_foundry_query_engine-0.8.10 → api_foundry_query_engine-0.8.12}/api_foundry_query_engine/utils/api_model.py +3 -1
- {api_foundry_query_engine-0.8.10 → api_foundry_query_engine-0.8.12}/pyproject.toml +18 -1
- api_foundry_query_engine-0.8.10/api_foundry_query_engine/connectors/athena_connector.py +0 -120
- api_foundry_query_engine-0.8.10/api_foundry_query_engine/operation.py +0 -59
- api_foundry_query_engine-0.8.10/api_foundry_query_engine/services/security_service.py +0 -105
- {api_foundry_query_engine-0.8.10 → api_foundry_query_engine-0.8.12}/.gitignore +0 -0
- {api_foundry_query_engine-0.8.10 → api_foundry_query_engine-0.8.12}/LICENSE +0 -0
- {api_foundry_query_engine-0.8.10 → api_foundry_query_engine-0.8.12}/api_foundry_query_engine/.pre-commit-config.yaml +0 -0
- {api_foundry_query_engine-0.8.10 → api_foundry_query_engine-0.8.12}/api_foundry_query_engine/__init__.py +0 -0
- {api_foundry_query_engine-0.8.10 → api_foundry_query_engine-0.8.12}/api_foundry_query_engine/adapters/adapter.py +0 -0
- {api_foundry_query_engine-0.8.10 → api_foundry_query_engine-0.8.12}/api_foundry_query_engine/adapters/case_change_adapter.py +0 -0
- {api_foundry_query_engine-0.8.10 → api_foundry_query_engine-0.8.12}/api_foundry_query_engine/adapters/security_adapter.py +0 -0
- {api_foundry_query_engine-0.8.10 → api_foundry_query_engine-0.8.12}/api_foundry_query_engine/connectors/connection.py +0 -0
- {api_foundry_query_engine-0.8.10 → api_foundry_query_engine-0.8.12}/api_foundry_query_engine/connectors/connection_factory.py +0 -0
- {api_foundry_query_engine-0.8.10 → api_foundry_query_engine-0.8.12}/api_foundry_query_engine/connectors/oracle_connector.py +0 -0
- {api_foundry_query_engine-0.8.10 → api_foundry_query_engine-0.8.12}/api_foundry_query_engine/connectors/postgres_connection.py +0 -0
- {api_foundry_query_engine-0.8.10 → api_foundry_query_engine-0.8.12}/api_foundry_query_engine/dao/dao.py +0 -0
- {api_foundry_query_engine-0.8.10 → api_foundry_query_engine-0.8.12}/api_foundry_query_engine/dao/operation_dao.py +0 -0
- {api_foundry_query_engine-0.8.10 → api_foundry_query_engine-0.8.12}/api_foundry_query_engine/dao/sql_custom_query_handler.py +0 -0
- {api_foundry_query_engine-0.8.10 → api_foundry_query_engine-0.8.12}/api_foundry_query_engine/dao/sql_delete_query_handler.py +0 -0
- {api_foundry_query_engine-0.8.10 → api_foundry_query_engine-0.8.12}/api_foundry_query_engine/dao/sql_insert_query_handler.py +0 -0
- {api_foundry_query_engine-0.8.10 → api_foundry_query_engine-0.8.12}/api_foundry_query_engine/dao/sql_query_handler.py +0 -0
- {api_foundry_query_engine-0.8.10 → api_foundry_query_engine-0.8.12}/api_foundry_query_engine/dao/sql_subselect_query_handler.py +0 -0
- {api_foundry_query_engine-0.8.10 → api_foundry_query_engine-0.8.12}/api_foundry_query_engine/dao/sql_update_query_handler.py +0 -0
- {api_foundry_query_engine-0.8.10 → api_foundry_query_engine-0.8.12}/api_foundry_query_engine/services/service.py +0 -0
- {api_foundry_query_engine-0.8.10 → api_foundry_query_engine-0.8.12}/api_foundry_query_engine/services/transactional_service.py +0 -0
- {api_foundry_query_engine-0.8.10 → api_foundry_query_engine-0.8.12}/api_foundry_query_engine/utils/app_exception.py +0 -0
- {api_foundry_query_engine-0.8.10 → api_foundry_query_engine-0.8.12}/api_foundry_query_engine/utils/logger.py +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.12
|
|
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
|
|
@@ -12,3 +12,9 @@ Requires-Python: >=3.9
|
|
|
12
12
|
Requires-Dist: boto3
|
|
13
13
|
Requires-Dist: pyhumps
|
|
14
14
|
Requires-Dist: pyyaml
|
|
15
|
+
Provides-Extra: dev
|
|
16
|
+
Requires-Dist: black>=23.0.0; extra == 'dev'
|
|
17
|
+
Requires-Dist: fixture-foundry>=0.1.0; extra == 'dev'
|
|
18
|
+
Requires-Dist: isort>=5.12.0; extra == 'dev'
|
|
19
|
+
Requires-Dist: pytest-cov>=4.0.0; extra == 'dev'
|
|
20
|
+
Requires-Dist: pytest>=7.0; extra == 'dev'
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import json
|
|
2
|
+
from typing import Any, Dict, List, Optional, Tuple
|
|
2
3
|
|
|
3
4
|
from api_foundry_query_engine.adapters.adapter import Adapter
|
|
4
5
|
from api_foundry_query_engine.operation import Operation
|
|
6
|
+
from api_foundry_query_engine.utils.app_exception import ApplicationException
|
|
5
7
|
|
|
6
8
|
actions_map = {
|
|
7
9
|
"GET": "read",
|
|
@@ -12,7 +14,7 @@ actions_map = {
|
|
|
12
14
|
|
|
13
15
|
|
|
14
16
|
class GatewayAdapter(Adapter):
|
|
15
|
-
def marshal(self, result:
|
|
17
|
+
def marshal(self, result: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
|
|
16
18
|
"""
|
|
17
19
|
Marshal the result into a event response
|
|
18
20
|
|
|
@@ -24,7 +26,7 @@ class GatewayAdapter(Adapter):
|
|
|
24
26
|
"""
|
|
25
27
|
return super().marshal(result)
|
|
26
28
|
|
|
27
|
-
def unmarshal(self, event):
|
|
29
|
+
def unmarshal(self, event: Dict[str, Any]) -> Operation:
|
|
28
30
|
"""
|
|
29
31
|
Get parameters from the Lambda event.
|
|
30
32
|
|
|
@@ -34,8 +36,15 @@ class GatewayAdapter(Adapter):
|
|
|
34
36
|
Returns:
|
|
35
37
|
- tuple: Tuple containing data, query and metadata parameters.
|
|
36
38
|
"""
|
|
37
|
-
|
|
38
|
-
|
|
39
|
+
resource = event.get("resource")
|
|
40
|
+
if resource is not None and "/" in resource:
|
|
41
|
+
parts = resource.split("/")
|
|
42
|
+
entity = parts[1] if len(parts) > 1 else None
|
|
43
|
+
else:
|
|
44
|
+
entity = None
|
|
45
|
+
|
|
46
|
+
method = str(event.get("httpMethod", "")).upper()
|
|
47
|
+
action = actions_map.get(method, "read")
|
|
39
48
|
|
|
40
49
|
event_params = {}
|
|
41
50
|
|
|
@@ -56,11 +65,40 @@ class GatewayAdapter(Adapter):
|
|
|
56
65
|
if body is not None and len(body) > 0:
|
|
57
66
|
store_params = json.loads(body)
|
|
58
67
|
|
|
59
|
-
roles = []
|
|
60
68
|
authorizer_info = event.get("requestContext", {}).get("authorizer", {})
|
|
61
69
|
claims = authorizer_info.get("claims", {})
|
|
62
70
|
roles = claims.get("roles", [])
|
|
63
|
-
|
|
71
|
+
groups = claims.get("groups", [])
|
|
72
|
+
subject = claims.get("sub")
|
|
73
|
+
permissions = claims.get("permissions", [])
|
|
74
|
+
scope_str = claims.get("scope")
|
|
75
|
+
|
|
76
|
+
# Enforce OAuth scopes (simulating API Gateway authorizer behavior)
|
|
77
|
+
# Required scope pattern: read|write|delete:<entity>
|
|
78
|
+
if entity and scope_str:
|
|
79
|
+
required_action = {
|
|
80
|
+
"GET": "read",
|
|
81
|
+
"POST": "write",
|
|
82
|
+
"PUT": "write",
|
|
83
|
+
"PATCH": "write",
|
|
84
|
+
"DELETE": "delete",
|
|
85
|
+
}.get(method, "read")
|
|
86
|
+
required_scope = f"{required_action}:{entity}"
|
|
87
|
+
token_scopes = set(str(scope_str).split())
|
|
88
|
+
|
|
89
|
+
def _has_scope(required: str) -> bool:
|
|
90
|
+
return (
|
|
91
|
+
required in token_scopes
|
|
92
|
+
or f"{required_action}:*" in token_scopes
|
|
93
|
+
or "*" in token_scopes
|
|
94
|
+
or "*:*" in token_scopes
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
if not _has_scope(required_scope):
|
|
98
|
+
raise ApplicationException(
|
|
99
|
+
401,
|
|
100
|
+
("insufficient_scope: required_scope=" + required_scope),
|
|
101
|
+
)
|
|
64
102
|
|
|
65
103
|
return Operation(
|
|
66
104
|
entity=entity,
|
|
@@ -69,10 +107,15 @@ class GatewayAdapter(Adapter):
|
|
|
69
107
|
query_params=query_params,
|
|
70
108
|
metadata_params=metadata_params,
|
|
71
109
|
roles=roles,
|
|
110
|
+
groups=groups,
|
|
72
111
|
subject=subject,
|
|
112
|
+
permissions=permissions,
|
|
113
|
+
claims=claims,
|
|
73
114
|
)
|
|
74
115
|
|
|
75
|
-
def _convert_parameters(
|
|
116
|
+
def _convert_parameters(
|
|
117
|
+
self, parameters: Optional[Dict[str, Any]]
|
|
118
|
+
) -> Optional[Dict[str, Any]]:
|
|
76
119
|
"""
|
|
77
120
|
Convert parameters to appropriate types.
|
|
78
121
|
|
|
@@ -96,7 +139,9 @@ class GatewayAdapter(Adapter):
|
|
|
96
139
|
result[parameter] = value
|
|
97
140
|
return result
|
|
98
141
|
|
|
99
|
-
def split_params(
|
|
142
|
+
def split_params(
|
|
143
|
+
self, parameters: Dict[str, Any]
|
|
144
|
+
) -> Tuple[Dict[str, Any], Dict[str, Any]]:
|
|
100
145
|
"""
|
|
101
146
|
Split a dictionary into two dictionaries based on keys.
|
|
102
147
|
|
|
@@ -1,17 +1,60 @@
|
|
|
1
|
-
|
|
2
|
-
from
|
|
1
|
+
import re
|
|
2
|
+
from typing import Match
|
|
3
|
+
from api_foundry_query_engine.dao.sql_query_handler import (
|
|
4
|
+
SQLSchemaQueryHandler,
|
|
5
|
+
)
|
|
3
6
|
from api_foundry_query_engine.utils.app_exception import ApplicationException
|
|
4
|
-
from api_foundry_query_engine.utils.api_model import
|
|
5
|
-
from api_foundry_query_engine.utils.logger import logger
|
|
6
|
-
|
|
7
|
-
log = logger(__name__)
|
|
7
|
+
from api_foundry_query_engine.utils.api_model import SchemaObjectProperty
|
|
8
8
|
|
|
9
9
|
|
|
10
10
|
class SQLSelectSchemaQueryHandler(SQLSchemaQueryHandler):
|
|
11
|
-
def
|
|
12
|
-
|
|
13
|
-
|
|
11
|
+
def _template_where(self, expr: str) -> str:
|
|
12
|
+
if not expr:
|
|
13
|
+
return expr
|
|
14
|
+
|
|
15
|
+
def _quote(val: object) -> str:
|
|
16
|
+
if val is None:
|
|
17
|
+
return "NULL"
|
|
18
|
+
if isinstance(val, (int, float)):
|
|
19
|
+
return str(val)
|
|
20
|
+
s = str(val).replace("'", "''")
|
|
21
|
+
return f"'{s}'"
|
|
22
|
+
|
|
23
|
+
def _replace(m: Match[str]) -> str:
|
|
24
|
+
key = m.group(1)
|
|
25
|
+
claims = self.operation.claims or {}
|
|
26
|
+
return _quote(claims.get(key))
|
|
27
|
+
|
|
28
|
+
return re.sub(r"\$\{claims\.([A-Za-z0-9_]+)\}", _replace, expr)
|
|
29
|
+
|
|
30
|
+
def _row_where_clause(self) -> str:
|
|
31
|
+
perms = getattr(self.schema_object, "permissions", None) or {}
|
|
32
|
+
# provider-first default
|
|
33
|
+
if "default" in perms:
|
|
34
|
+
provider = perms.get("default", {}) or {}
|
|
35
|
+
read_map = provider.get("read", {}) or {}
|
|
36
|
+
else:
|
|
37
|
+
# legacy role-first -> synthesize read map
|
|
38
|
+
read_map = {}
|
|
39
|
+
for role, role_perms in perms.items():
|
|
40
|
+
if isinstance(role_perms, dict):
|
|
41
|
+
read_map[role] = role_perms.get("read")
|
|
42
|
+
|
|
43
|
+
filters = []
|
|
44
|
+
for role in self.operation.roles or []:
|
|
45
|
+
rule = read_map.get(role)
|
|
46
|
+
if isinstance(rule, dict):
|
|
47
|
+
where = rule.get("where")
|
|
48
|
+
if isinstance(where, str) and where.strip():
|
|
49
|
+
filters.append(self._template_where(where))
|
|
50
|
+
if not filters:
|
|
51
|
+
return ""
|
|
52
|
+
return "(" + ") OR (".join(filters) + ")"
|
|
53
|
+
|
|
54
|
+
def __init__(self, operation, schema_object, engine: str) -> None:
|
|
14
55
|
super().__init__(operation, schema_object, engine)
|
|
56
|
+
# Lazy cache for selection results
|
|
57
|
+
self._selection_results = None
|
|
15
58
|
|
|
16
59
|
@property
|
|
17
60
|
def sql(self) -> str:
|
|
@@ -63,38 +106,39 @@ class SQLSelectSchemaQueryHandler(SQLSchemaQueryHandler):
|
|
|
63
106
|
+ " does not have property "
|
|
64
107
|
+ parts[1],
|
|
65
108
|
)
|
|
66
|
-
|
|
109
|
+
prop = relation.child_schema_object.properties[parts[1]]
|
|
67
110
|
prefix = self.prefix_map[parts[0]]
|
|
68
111
|
else:
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
500,
|
|
73
|
-
"schema_object.api_name is None, cannot use as key in prefix_map",
|
|
74
|
-
)
|
|
75
|
-
prefix = self.prefix_map[self.schema_object.api_name]
|
|
76
|
-
except KeyError:
|
|
112
|
+
prop = self.schema_object.properties[parts[0]]
|
|
113
|
+
prefix = self.prefix_map[str(self.schema_object.api_name)]
|
|
114
|
+
except KeyError as exc:
|
|
77
115
|
raise ApplicationException(
|
|
78
116
|
500,
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
117
|
+
(
|
|
118
|
+
"Invalid query parameter, property not found. "
|
|
119
|
+
+ "schema object: "
|
|
120
|
+
+ str(self.schema_object.api_name)
|
|
121
|
+
+ ", property: "
|
|
122
|
+
+ name
|
|
123
|
+
),
|
|
124
|
+
) from exc
|
|
125
|
+
|
|
126
|
+
assignment, holders = self.search_value_assignment(prop, value, prefix)
|
|
87
127
|
self.active_prefixes.add(prefix)
|
|
88
128
|
conditions.append(assignment)
|
|
89
129
|
self.search_placeholders.update(holders)
|
|
130
|
+
# append row-level filters if present
|
|
131
|
+
row_filter = self._row_where_clause()
|
|
132
|
+
if row_filter:
|
|
133
|
+
conditions.append(f"({row_filter})")
|
|
90
134
|
|
|
91
|
-
return
|
|
135
|
+
return (" WHERE " + " AND ".join(conditions)) if conditions else ""
|
|
92
136
|
|
|
93
137
|
@property
|
|
94
138
|
def table_expression(self) -> str:
|
|
95
139
|
joins = []
|
|
96
140
|
parent_prefix = self.prefix_map[str(self.schema_object.api_name)]
|
|
97
|
-
for
|
|
141
|
+
for _, relation in self.schema_object.relations.items():
|
|
98
142
|
child_prefix = self.prefix_map[str(relation.api_name)]
|
|
99
143
|
if child_prefix in self.active_prefixes:
|
|
100
144
|
joins.append(
|
|
@@ -121,7 +165,7 @@ class SQLSelectSchemaQueryHandler(SQLSchemaQueryHandler):
|
|
|
121
165
|
|
|
122
166
|
@property
|
|
123
167
|
def selection_results(self) -> dict:
|
|
124
|
-
if
|
|
168
|
+
if self._selection_results is None:
|
|
125
169
|
self._selection_results = {}
|
|
126
170
|
if "count" in self.operation.metadata_params:
|
|
127
171
|
self._selection_results = {
|
|
@@ -163,10 +207,14 @@ class SQLSelectSchemaQueryHandler(SQLSchemaQueryHandler):
|
|
|
163
207
|
# Filter and prefix keys for the current entity
|
|
164
208
|
# and regular expressions
|
|
165
209
|
allowed_properties = self.check_permissions(
|
|
166
|
-
"read",
|
|
210
|
+
"read",
|
|
211
|
+
schema_object.permissions,
|
|
212
|
+
schema_object.properties,
|
|
167
213
|
)
|
|
168
214
|
filtered_keys = self.filter_and_prefix_keys(
|
|
169
|
-
reg_exs,
|
|
215
|
+
reg_exs,
|
|
216
|
+
allowed_properties,
|
|
217
|
+
self.prefix_map[relation],
|
|
170
218
|
)
|
|
171
219
|
|
|
172
220
|
# Extend the result map with the filtered keys
|
|
@@ -175,15 +223,18 @@ class SQLSelectSchemaQueryHandler(SQLSchemaQueryHandler):
|
|
|
175
223
|
if len(self._selection_results) == 0:
|
|
176
224
|
raise ApplicationException(
|
|
177
225
|
402,
|
|
178
|
-
|
|
226
|
+
(
|
|
227
|
+
"After applying permissions there are no properties "
|
|
228
|
+
"returned in response"
|
|
229
|
+
),
|
|
179
230
|
)
|
|
180
231
|
return self._selection_results
|
|
181
232
|
|
|
182
|
-
def get_regex_map(self, filter_str: str) -> dict
|
|
233
|
+
def get_regex_map(self, filter_str: str) -> dict:
|
|
183
234
|
result = {}
|
|
184
235
|
|
|
185
|
-
for
|
|
186
|
-
parts =
|
|
236
|
+
for flt in filter_str.split():
|
|
237
|
+
parts = flt.split(":")
|
|
187
238
|
entity = parts[0] if len(parts) > 1 else self.schema_object.api_name
|
|
188
239
|
expression = parts[-1]
|
|
189
240
|
|
|
@@ -200,17 +251,17 @@ class SQLSelectSchemaQueryHandler(SQLSchemaQueryHandler):
|
|
|
200
251
|
def marshal_record(self, record) -> dict:
|
|
201
252
|
object_set = {}
|
|
202
253
|
for name, value in record.items():
|
|
203
|
-
|
|
254
|
+
prop = self.selection_results[name]
|
|
204
255
|
parts = name.split(".")
|
|
205
256
|
component = (
|
|
206
257
|
parts[0]
|
|
207
258
|
if len(parts) > 1
|
|
208
259
|
else self.prefix_map[str(self.schema_object.api_name)]
|
|
209
260
|
)
|
|
210
|
-
|
|
211
|
-
if not
|
|
212
|
-
object_set[component] =
|
|
213
|
-
|
|
261
|
+
obj = object_set.get(component, {})
|
|
262
|
+
if not obj:
|
|
263
|
+
object_set[component] = obj
|
|
264
|
+
obj[prop.api_name] = prop.convert_to_api_value(value)
|
|
214
265
|
|
|
215
266
|
result = object_set[self.prefix_map[str(self.schema_object.api_name)]]
|
|
216
267
|
for name, prefix in self.prefix_map.items():
|
|
@@ -243,13 +294,13 @@ class SQLSelectSchemaQueryHandler(SQLSchemaQueryHandler):
|
|
|
243
294
|
field_parts = field_name.split(".")
|
|
244
295
|
if len(field_parts) == 1:
|
|
245
296
|
prefix = self.prefix_map[str(self.schema_object.api_name)]
|
|
246
|
-
|
|
247
|
-
if not
|
|
297
|
+
prop = self.schema_object.properties.get(field_parts[0])
|
|
298
|
+
if not prop:
|
|
248
299
|
raise ApplicationException(
|
|
249
300
|
400,
|
|
250
301
|
f"Invalid order by property, schema object: {self.schema_object.api_name} does not have a property: {field_parts[0]}", # noqa E501
|
|
251
302
|
)
|
|
252
|
-
column =
|
|
303
|
+
column = prop.column_name
|
|
253
304
|
else:
|
|
254
305
|
# Extract the schema object for the current entity
|
|
255
306
|
relation_property = self.schema_object.relations.get(field_parts[0])
|
|
@@ -272,13 +323,13 @@ class SQLSelectSchemaQueryHandler(SQLSchemaQueryHandler):
|
|
|
272
323
|
schema_object = self.schema_object
|
|
273
324
|
|
|
274
325
|
prefix = self.prefix_map[field_parts[0]]
|
|
275
|
-
|
|
276
|
-
if not
|
|
326
|
+
prop = schema_object.properties.get(field_parts[1])
|
|
327
|
+
if not prop:
|
|
277
328
|
raise ApplicationException(
|
|
278
329
|
400,
|
|
279
330
|
f"Invalid order by property, schema object: {schema_object.api_name} does not have a property: {field_parts[1]}", # noqa E501
|
|
280
331
|
)
|
|
281
|
-
column =
|
|
332
|
+
column = prop.column_name
|
|
282
333
|
self.active_prefixes.add(prefix)
|
|
283
334
|
use_prefixes = True
|
|
284
335
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import json
|
|
2
2
|
import logging
|
|
3
3
|
import os
|
|
4
|
-
from typing import Optional, Mapping
|
|
4
|
+
from typing import Optional, Mapping, Any
|
|
5
5
|
|
|
6
6
|
from api_foundry_query_engine.utils.api_model import set_api_model
|
|
7
7
|
from api_foundry_query_engine.utils.app_exception import ApplicationException
|
|
@@ -14,8 +14,8 @@ class QueryEngine:
|
|
|
14
14
|
def __init__(self, config: Mapping[str, str]):
|
|
15
15
|
self.adapter = GatewayAdapter(config)
|
|
16
16
|
|
|
17
|
-
def handler(self, event):
|
|
18
|
-
log.debug(
|
|
17
|
+
def handler(self, event) -> dict[str, Any]:
|
|
18
|
+
log.debug("event: %s", event)
|
|
19
19
|
try:
|
|
20
20
|
response = self.adapter.process_event(event)
|
|
21
21
|
|
|
@@ -27,20 +27,20 @@ class QueryEngine:
|
|
|
27
27
|
"body": json.dumps(response),
|
|
28
28
|
}
|
|
29
29
|
except ApplicationException as e:
|
|
30
|
-
log.error(
|
|
30
|
+
log.error("exception: %s", e, exc_info=True)
|
|
31
31
|
return {
|
|
32
32
|
"isBase64Encoded": False,
|
|
33
33
|
"statusCode": e.status_code,
|
|
34
34
|
"headers": {"Content-Type": "application/json"},
|
|
35
|
-
"body": json.dumps({"message":
|
|
35
|
+
"body": json.dumps({"message": "exception: %s" % e}),
|
|
36
36
|
}
|
|
37
|
-
except
|
|
38
|
-
log.error(
|
|
37
|
+
except RuntimeError as e:
|
|
38
|
+
log.error("runtime error: %s", e, exc_info=True)
|
|
39
39
|
return {
|
|
40
40
|
"isBase64Encoded": False,
|
|
41
41
|
"statusCode": 500,
|
|
42
42
|
"headers": {"Content-Type": "application/json"},
|
|
43
|
-
"body": json.dumps({"message": f"
|
|
43
|
+
"body": json.dumps({"message": f"runtime error: {e}"}),
|
|
44
44
|
}
|
|
45
45
|
|
|
46
46
|
|
|
@@ -49,16 +49,14 @@ query_engine: Optional[QueryEngine] = None
|
|
|
49
49
|
|
|
50
50
|
|
|
51
51
|
def handler(event, _):
|
|
52
|
-
|
|
53
|
-
log.info(f"engine_config: {engine_config}")
|
|
54
|
-
if engine_config is None:
|
|
52
|
+
if not hasattr(handler, "engine_config"):
|
|
55
53
|
log.info("Loading engine config from environment variables")
|
|
56
|
-
engine_config = os.environ
|
|
57
|
-
log.info(f"engine_config: {engine_config}")
|
|
54
|
+
handler.engine_config = os.environ
|
|
55
|
+
log.info(f"engine_config: {handler.engine_config}")
|
|
58
56
|
|
|
59
|
-
if
|
|
60
|
-
set_api_model(engine_config)
|
|
57
|
+
if not hasattr(handler, "query_engine"):
|
|
58
|
+
set_api_model(handler.engine_config)
|
|
61
59
|
log.info("Creating QueryEngine instance")
|
|
62
|
-
query_engine = QueryEngine(engine_config)
|
|
60
|
+
handler.query_engine = QueryEngine(handler.engine_config)
|
|
63
61
|
|
|
64
|
-
return query_engine.handler(event)
|
|
62
|
+
return handler.query_engine.handler(event)
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
from typing import Any, Dict, Optional
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class Operation:
|
|
5
|
+
"""
|
|
6
|
+
Represents an action to be performed on an entity.
|
|
7
|
+
|
|
8
|
+
The `Operation` class encapsulates the details required
|
|
9
|
+
to execute an operation on a given entity, including
|
|
10
|
+
query parameters for selecting records, parameters for
|
|
11
|
+
storing or updating values, metadata for operational
|
|
12
|
+
instructions, and roles defining the contexts in which
|
|
13
|
+
the operation can be performed.
|
|
14
|
+
|
|
15
|
+
Attributes:
|
|
16
|
+
entity (str): The name of the resource or object being
|
|
17
|
+
targeted (e.g., "User", "Order").
|
|
18
|
+
action (str): The type of action being performed
|
|
19
|
+
(e.g., "create", "read", "update", "delete").
|
|
20
|
+
query_params (dict): Parameters used to filter or select
|
|
21
|
+
the records affected by the operation.
|
|
22
|
+
store_params (dict): Parameters that define the values to
|
|
23
|
+
be stored or updated for the selected records.
|
|
24
|
+
metadata_params (dict): Additional instructions for the
|
|
25
|
+
operation, such as sorting or pagination.
|
|
26
|
+
roles (dict): Defines the roles under which the operation
|
|
27
|
+
is allowed to be performed.
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
def __init__(
|
|
31
|
+
self,
|
|
32
|
+
*,
|
|
33
|
+
entity: str,
|
|
34
|
+
action: str,
|
|
35
|
+
query_params: Optional[Dict[str, Any]] = None,
|
|
36
|
+
store_params: Optional[Dict[str, Any]] = None,
|
|
37
|
+
metadata_params: Optional[Dict[str, Any]] = None,
|
|
38
|
+
roles: Optional[Dict[str, Any]] = None,
|
|
39
|
+
scope: Optional[Dict[str, Any]] = None,
|
|
40
|
+
groups: Optional[Dict[str, Any]] = None,
|
|
41
|
+
permissions: Optional[Dict[str, Any]] = None,
|
|
42
|
+
subject: Optional[str] = None,
|
|
43
|
+
claims: Optional[Dict[str, Any]] = None,
|
|
44
|
+
):
|
|
45
|
+
"""
|
|
46
|
+
Initializes the Operation instance.
|
|
47
|
+
|
|
48
|
+
Args:
|
|
49
|
+
entity (str): The name of the entity to perform the operation on.
|
|
50
|
+
action (str): The action to perform on the entity.
|
|
51
|
+
query_params (dict, optional): Parameters for selecting
|
|
52
|
+
affected records (default: {}).
|
|
53
|
+
store_params (dict, optional): Parameters defining new or
|
|
54
|
+
updated values (default: {}).
|
|
55
|
+
metadata_params (dict, optional): Metadata for the
|
|
56
|
+
operation, such as sorting or pagination (default: {}).
|
|
57
|
+
roles (dict, optional): Defines the roles allowed to
|
|
58
|
+
perform the operation (default: {}).
|
|
59
|
+
"""
|
|
60
|
+
# The target entity for the operation (e.g., "User", "Order").
|
|
61
|
+
self.entity = entity
|
|
62
|
+
|
|
63
|
+
# The type of action to perform (e.g., "create",
|
|
64
|
+
# "read", "update", "delete").
|
|
65
|
+
self.action = action
|
|
66
|
+
|
|
67
|
+
# Query parameters to filter or identify the affected records.
|
|
68
|
+
self.query_params = query_params or {}
|
|
69
|
+
|
|
70
|
+
# Parameters defining the values to be stored or updated
|
|
71
|
+
# for the operation.
|
|
72
|
+
self.store_params = store_params or {}
|
|
73
|
+
|
|
74
|
+
# Metadata for operational instructions like sorting,
|
|
75
|
+
# limiting, or offsetting results.
|
|
76
|
+
self.metadata_params = metadata_params or {}
|
|
77
|
+
|
|
78
|
+
# Roles defining the context in which the operation is allowed.
|
|
79
|
+
self.scope = scope or {}
|
|
80
|
+
self.roles = roles or []
|
|
81
|
+
self.groups = groups or []
|
|
82
|
+
self.permissions = permissions or []
|
|
83
|
+
|
|
84
|
+
self.subject = subject
|
|
85
|
+
# Full set of claims (when available). Used for row-level filters
|
|
86
|
+
# like where: "id = ${claims.sub}".
|
|
87
|
+
self.claims = claims or ({"sub": subject} if subject is not None else {})
|
|
@@ -155,7 +155,9 @@ class SchemaObject:
|
|
|
155
155
|
self.database: str = str(data.get("database"))
|
|
156
156
|
self.schema: Optional[str] = data.get("schema")
|
|
157
157
|
self.table_name: str = str(data.get("table_name"))
|
|
158
|
-
self.qualified_name: str =
|
|
158
|
+
self.qualified_name: str = (
|
|
159
|
+
f"{self.schema}.{self.table_name}" if self.schema else self.table_name
|
|
160
|
+
)
|
|
159
161
|
self.properties: Dict[str, SchemaObjectProperty] = {
|
|
160
162
|
name: SchemaObjectProperty(prop_data)
|
|
161
163
|
for name, prop_data in data.get("properties", {}).items()
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "api-foundry-query-engine"
|
|
7
|
-
version = "0.8.
|
|
7
|
+
version = "0.8.12"
|
|
8
8
|
authors = [
|
|
9
9
|
{ name="Daniel Repik", email="danrepik@icloud.com" },
|
|
10
10
|
]
|
|
@@ -19,6 +19,14 @@ dependencies = [
|
|
|
19
19
|
"pyyaml",
|
|
20
20
|
"pyhumps",
|
|
21
21
|
]
|
|
22
|
+
[project.optional-dependencies]
|
|
23
|
+
dev = [
|
|
24
|
+
"pytest>=7.0",
|
|
25
|
+
"pytest-cov>=4.0.0",
|
|
26
|
+
"black>=23.0.0",
|
|
27
|
+
"isort>=5.12.0",
|
|
28
|
+
"fixture_foundry>=0.1.0"
|
|
29
|
+
]
|
|
22
30
|
|
|
23
31
|
[project.urls]
|
|
24
32
|
"Documentation" = "https://github.com/DanRepik/api-foundry"
|
|
@@ -30,6 +38,15 @@ include = ["api_foundry_query_engine"]
|
|
|
30
38
|
[tool.hatch.build.targets.wheel]
|
|
31
39
|
include = ["api_foundry_query_engine"]
|
|
32
40
|
|
|
41
|
+
[tool.hatch.envs.dev]
|
|
42
|
+
dependencies = [
|
|
43
|
+
"pytest>=7.0",
|
|
44
|
+
"pytest-cov>=4.0.0",
|
|
45
|
+
"black>=23.0.0",
|
|
46
|
+
"isort>=5.12.0",
|
|
47
|
+
"fixture_foundry>=0.1.0",
|
|
48
|
+
]
|
|
49
|
+
|
|
33
50
|
[project.scripts]
|
|
34
51
|
|
|
35
52
|
[tool.pytest.ini_options]
|
|
@@ -1,120 +0,0 @@
|
|
|
1
|
-
from api_foundry_query_engine.connectors.connection import Connection, Cursor
|
|
2
|
-
from api_foundry_query_engine.utils.logger import logger
|
|
3
|
-
import boto3
|
|
4
|
-
import time
|
|
5
|
-
|
|
6
|
-
# Initialize the logger
|
|
7
|
-
log = logger(__name__)
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
class AthenaCursor(Cursor):
|
|
11
|
-
def __init__(self, athena_client, db_config):
|
|
12
|
-
self.athena_client = athena_client
|
|
13
|
-
self.db_config = db_config
|
|
14
|
-
|
|
15
|
-
def execute(self, sql: str, parameters: dict, result_columns: list[str]) -> list:
|
|
16
|
-
"""
|
|
17
|
-
Execute a SQL statement on the Athena database.
|
|
18
|
-
|
|
19
|
-
Parameters:
|
|
20
|
-
- sql (str): The SQL query to execute.
|
|
21
|
-
- parameters (dict): Parameters for the query (currently unused; substitute manually in the query).
|
|
22
|
-
- result_columns (list[str]): Column names for the results.
|
|
23
|
-
|
|
24
|
-
Returns:
|
|
25
|
-
- list[dict]: Query results as a list of dictionaries.
|
|
26
|
-
"""
|
|
27
|
-
# Substitute parameters into the query manually (Athena doesn't support placeholders)
|
|
28
|
-
for key, value in parameters.items():
|
|
29
|
-
placeholder = f":{key}"
|
|
30
|
-
if isinstance(value, str):
|
|
31
|
-
value = f"'{value}'" # Wrap strings in quotes
|
|
32
|
-
sql = sql.replace(placeholder, str(value))
|
|
33
|
-
|
|
34
|
-
log.info(f"Executing query: {sql}")
|
|
35
|
-
|
|
36
|
-
# Start the query execution
|
|
37
|
-
response = self.athena_client.start_query_execution(
|
|
38
|
-
QueryString=sql,
|
|
39
|
-
QueryExecutionContext={"Database": self.db_config["database"]},
|
|
40
|
-
ResultConfiguration={"OutputLocation": self.db_config["output_location"]},
|
|
41
|
-
)
|
|
42
|
-
query_execution_id = response["QueryExecutionId"]
|
|
43
|
-
log.info(f"Query execution started: {query_execution_id}")
|
|
44
|
-
|
|
45
|
-
# Wait for the query to complete
|
|
46
|
-
while True:
|
|
47
|
-
status_response = self.athena_client.get_query_execution(
|
|
48
|
-
QueryExecutionId=query_execution_id
|
|
49
|
-
)
|
|
50
|
-
status = status_response["QueryExecution"]["Status"]["State"]
|
|
51
|
-
|
|
52
|
-
if status in ["SUCCEEDED", "FAILED", "CANCELLED"]:
|
|
53
|
-
break
|
|
54
|
-
time.sleep(2)
|
|
55
|
-
|
|
56
|
-
if status == "FAILED":
|
|
57
|
-
raise Exception(
|
|
58
|
-
f"Query failed: {status_response['QueryExecution']['Status']['StateChangeReason']}"
|
|
59
|
-
)
|
|
60
|
-
elif status == "CANCELLED":
|
|
61
|
-
raise Exception("Query was cancelled.")
|
|
62
|
-
|
|
63
|
-
# Fetch results
|
|
64
|
-
results_response = self.athena_client.get_query_results(
|
|
65
|
-
QueryExecutionId=query_execution_id
|
|
66
|
-
)
|
|
67
|
-
rows = results_response["ResultSet"]["Rows"]
|
|
68
|
-
|
|
69
|
-
# Convert rows to dictionaries
|
|
70
|
-
result = []
|
|
71
|
-
for row in rows[1:]: # Skip the first row (header row)
|
|
72
|
-
record = {
|
|
73
|
-
col: value.get("VarCharValue", None)
|
|
74
|
-
for col, value in zip(result_columns, row["Data"])
|
|
75
|
-
}
|
|
76
|
-
result.append(record)
|
|
77
|
-
|
|
78
|
-
return result
|
|
79
|
-
|
|
80
|
-
def close(self):
|
|
81
|
-
log.info("Closing Athena cursor (no persistent connection to close).")
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
class AthenaConnection(Connection):
|
|
85
|
-
def __init__(self, db_config: dict) -> None:
|
|
86
|
-
"""
|
|
87
|
-
Initialize the AthenaConnection with the given configuration.
|
|
88
|
-
|
|
89
|
-
Parameters:
|
|
90
|
-
- db_config (dict): A dictionary containing Athena connection configuration.
|
|
91
|
-
Required keys:
|
|
92
|
-
- 'region': AWS region of the Athena service.
|
|
93
|
-
- 'output_location': S3 bucket location for query results.
|
|
94
|
-
- 'database': Athena database name.
|
|
95
|
-
"""
|
|
96
|
-
super().__init__(db_config)
|
|
97
|
-
self.athena_client = self.get_athena_client()
|
|
98
|
-
|
|
99
|
-
def engine(self) -> str:
|
|
100
|
-
return "athena"
|
|
101
|
-
|
|
102
|
-
def cursor(self) -> Cursor:
|
|
103
|
-
return AthenaCursor(self.athena_client, self.db_config)
|
|
104
|
-
|
|
105
|
-
def commit(self):
|
|
106
|
-
# Athena queries are read-only; no commit operation is required
|
|
107
|
-
log.info("Athena does not support transactions. Commit is a no-op.")
|
|
108
|
-
|
|
109
|
-
def close(self):
|
|
110
|
-
# Athena uses a stateless API, so there's nothing to close
|
|
111
|
-
log.info("Closing Athena connection (no persistent connection to close).")
|
|
112
|
-
|
|
113
|
-
def get_athena_client(self):
|
|
114
|
-
"""
|
|
115
|
-
Get a Boto3 client for Athena.
|
|
116
|
-
"""
|
|
117
|
-
region = self.db_config.get("region")
|
|
118
|
-
if not region:
|
|
119
|
-
raise ValueError("Athena configuration must include 'region'.")
|
|
120
|
-
return boto3.client("athena", region_name=region)
|
|
@@ -1,59 +0,0 @@
|
|
|
1
|
-
class Operation:
|
|
2
|
-
"""
|
|
3
|
-
Represents an action to be performed on an entity.
|
|
4
|
-
|
|
5
|
-
The `Operation` class encapsulates the details required to execute an operation
|
|
6
|
-
on a given entity, including query parameters for selecting records, parameters
|
|
7
|
-
for storing or updating values, metadata for operational instructions, and
|
|
8
|
-
roles defining the contexts in which the operation can be performed.
|
|
9
|
-
|
|
10
|
-
Attributes:
|
|
11
|
-
entity (str): The name of the resource or object being targeted (e.g., "User", "Order").
|
|
12
|
-
action (str): The type of action being performed (e.g., "create", "read", "update", "delete").
|
|
13
|
-
query_params (dict): Parameters used to filter or select the records affected by the operation.
|
|
14
|
-
store_params (dict): Parameters that define the values to be stored or updated for the selected records.
|
|
15
|
-
metadata_params (dict): Additional instructions for the operation, such as sorting or pagination.
|
|
16
|
-
roles (dict): Defines the roles under which the operation is allowed to be performed.
|
|
17
|
-
"""
|
|
18
|
-
|
|
19
|
-
def __init__(
|
|
20
|
-
self,
|
|
21
|
-
*,
|
|
22
|
-
entity: str,
|
|
23
|
-
action: str,
|
|
24
|
-
query_params: dict = {},
|
|
25
|
-
store_params: dict = {},
|
|
26
|
-
metadata_params: dict = {},
|
|
27
|
-
roles: dict = {},
|
|
28
|
-
subject: str = None,
|
|
29
|
-
):
|
|
30
|
-
"""
|
|
31
|
-
Initializes the Operation instance.
|
|
32
|
-
|
|
33
|
-
Args:
|
|
34
|
-
entity (str): The name of the entity to perform the operation on.
|
|
35
|
-
action (str): The action to perform on the entity.
|
|
36
|
-
query_params (dict, optional): Parameters for selecting affected records (default: {}).
|
|
37
|
-
store_params (dict, optional): Parameters defining new or updated values (default: {}).
|
|
38
|
-
metadata_params (dict, optional): Metadata for the operation, such as sorting or pagination (default: {}).
|
|
39
|
-
roles (dict, optional): Defines the roles allowed to perform the operation (default: {}).
|
|
40
|
-
"""
|
|
41
|
-
# The target entity for the operation (e.g., "User", "Order").
|
|
42
|
-
self.entity = entity
|
|
43
|
-
|
|
44
|
-
# The type of action to perform (e.g., "create", "read", "update", "delete").
|
|
45
|
-
self.action = action
|
|
46
|
-
|
|
47
|
-
# Query parameters to filter or identify the affected records.
|
|
48
|
-
self.query_params = query_params
|
|
49
|
-
|
|
50
|
-
# Parameters defining the values to be stored or updated for the operation.
|
|
51
|
-
self.store_params = store_params
|
|
52
|
-
|
|
53
|
-
# Metadata for operational instructions like sorting, limiting, or offsetting results.
|
|
54
|
-
self.metadata_params = metadata_params
|
|
55
|
-
|
|
56
|
-
# Roles defining the context in which the operation is allowed.
|
|
57
|
-
self.roles = roles
|
|
58
|
-
|
|
59
|
-
self.subject = subject
|
|
@@ -1,105 +0,0 @@
|
|
|
1
|
-
from api_foundry_query_engine.utils.logger import logger
|
|
2
|
-
from api_foundry_query_engine.operation import Operation
|
|
3
|
-
from api_foundry_query_engine.services.service import ServiceAdapter
|
|
4
|
-
|
|
5
|
-
log = logger(__name__)
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
class SecurityService(ServiceAdapter):
|
|
9
|
-
def execute(self, operation: Operation) -> list[dict]:
|
|
10
|
-
"""
|
|
11
|
-
Execute the operation after validating security instructions.
|
|
12
|
-
|
|
13
|
-
Parameters:
|
|
14
|
-
- operation (Operation): The operation to execute.
|
|
15
|
-
|
|
16
|
-
Returns:
|
|
17
|
-
- list[dict]: The result of the operation, filtered based on security rules.
|
|
18
|
-
"""
|
|
19
|
-
log.debug(f"Executing operation: {operation}")
|
|
20
|
-
|
|
21
|
-
# Resolve the schema or path operation to retrieve the `security` attribute
|
|
22
|
-
schema_or_path = self.schema_or_path_resolver(
|
|
23
|
-
operation.entity, operation.action
|
|
24
|
-
)
|
|
25
|
-
if not schema_or_path or "security" not in schema_or_path:
|
|
26
|
-
raise ValueError(
|
|
27
|
-
f"No security instructions found for entity {operation.entity}"
|
|
28
|
-
)
|
|
29
|
-
|
|
30
|
-
security_rules = schema_or_path["security"]
|
|
31
|
-
|
|
32
|
-
# Validate query and store parameters
|
|
33
|
-
self._validate_query_params(operation.query_params, security_rules)
|
|
34
|
-
self._validate_store_params(operation.store_params, security_rules)
|
|
35
|
-
|
|
36
|
-
# Execute the next service
|
|
37
|
-
result = self.next_service.execute(operation)
|
|
38
|
-
|
|
39
|
-
# Filter the result based on security rules
|
|
40
|
-
filtered_result = self._filter_result(result, security_rules)
|
|
41
|
-
return filtered_result
|
|
42
|
-
|
|
43
|
-
def _validate_query_params(self, query_params: dict, security_rules: dict):
|
|
44
|
-
"""
|
|
45
|
-
Validate query parameters against read permissions.
|
|
46
|
-
|
|
47
|
-
Parameters:
|
|
48
|
-
- query_params (dict): Query parameters to validate.
|
|
49
|
-
- security_rules (dict): Security rules for validation.
|
|
50
|
-
|
|
51
|
-
Raises:
|
|
52
|
-
- PermissionError: If a query parameter violates read permissions.
|
|
53
|
-
"""
|
|
54
|
-
read_permissions = security_rules.get("read", [])
|
|
55
|
-
invalid_params = [
|
|
56
|
-
key
|
|
57
|
-
for key in query_params
|
|
58
|
-
if key not in read_permissions and read_permissions != ["*"]
|
|
59
|
-
]
|
|
60
|
-
if invalid_params:
|
|
61
|
-
raise PermissionError(f"Query parameters not permitted: {invalid_params}")
|
|
62
|
-
|
|
63
|
-
def _validate_store_params(self, store_params: dict, security_rules: dict):
|
|
64
|
-
"""
|
|
65
|
-
Validate store parameters against write permissions.
|
|
66
|
-
|
|
67
|
-
Parameters:
|
|
68
|
-
- store_params (dict): Store parameters to validate.
|
|
69
|
-
- security_rules (dict): Security rules for validation.
|
|
70
|
-
|
|
71
|
-
Raises:
|
|
72
|
-
- PermissionError: If a store parameter violates write permissions.
|
|
73
|
-
"""
|
|
74
|
-
write_permissions = security_rules.get("write", [])
|
|
75
|
-
invalid_params = [
|
|
76
|
-
key
|
|
77
|
-
for key in store_params
|
|
78
|
-
if key not in write_permissions and write_permissions != ["*"]
|
|
79
|
-
]
|
|
80
|
-
if invalid_params:
|
|
81
|
-
raise PermissionError(f"Store parameters not permitted: {invalid_params}")
|
|
82
|
-
|
|
83
|
-
def _filter_result(self, result: list[dict], security_rules: dict) -> list[dict]:
|
|
84
|
-
"""
|
|
85
|
-
Filter the result based on read permissions.
|
|
86
|
-
|
|
87
|
-
Parameters:
|
|
88
|
-
- result (list[dict]): The original result set.
|
|
89
|
-
- security_rules (dict): Security rules for filtering.
|
|
90
|
-
|
|
91
|
-
Returns:
|
|
92
|
-
- list[dict]: The filtered result set.
|
|
93
|
-
"""
|
|
94
|
-
read_permissions = security_rules.get("read", [])
|
|
95
|
-
if read_permissions == ["*"]:
|
|
96
|
-
return result # No filtering needed
|
|
97
|
-
|
|
98
|
-
filtered_result = []
|
|
99
|
-
for record in result:
|
|
100
|
-
filtered_record = {
|
|
101
|
-
key: value for key, value in record.items() if key in read_permissions
|
|
102
|
-
}
|
|
103
|
-
filtered_result.append(filtered_record)
|
|
104
|
-
|
|
105
|
-
return filtered_result
|
|
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
|