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.
Files changed (37) hide show
  1. {api_foundry_query_engine-0.8.62 → api_foundry_query_engine-0.8.64}/PKG-INFO +1 -1
  2. api_foundry_query_engine-0.8.64/api_foundry_query_engine/__init__.py +1 -0
  3. {api_foundry_query_engine-0.8.62 → api_foundry_query_engine-0.8.64}/api_foundry_query_engine/adapters/gateway_adapter.py +1 -3
  4. {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
  5. {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
  6. {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
  7. {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
  8. {api_foundry_query_engine-0.8.62 → api_foundry_query_engine-0.8.64}/api_foundry_query_engine/operation.py +3 -0
  9. {api_foundry_query_engine-0.8.62 → api_foundry_query_engine-0.8.64}/api_foundry_query_engine/utils/api_model.py +14 -35
  10. {api_foundry_query_engine-0.8.62 → api_foundry_query_engine-0.8.64}/api_foundry_query_engine/utils/claims_check.py +3 -4
  11. api_foundry_query_engine-0.8.62/api_foundry_query_engine/__init__.py +0 -1
  12. {api_foundry_query_engine-0.8.62 → api_foundry_query_engine-0.8.64}/.gitignore +0 -0
  13. {api_foundry_query_engine-0.8.62 → api_foundry_query_engine-0.8.64}/LICENSE +0 -0
  14. {api_foundry_query_engine-0.8.62 → api_foundry_query_engine-0.8.64}/api_foundry_query_engine/.pre-commit-config.yaml +0 -0
  15. {api_foundry_query_engine-0.8.62 → api_foundry_query_engine-0.8.64}/api_foundry_query_engine/adapters/adapter.py +0 -0
  16. {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
  17. {api_foundry_query_engine-0.8.62 → api_foundry_query_engine-0.8.64}/api_foundry_query_engine/adapters/security_adapter.py +0 -0
  18. {api_foundry_query_engine-0.8.62 → api_foundry_query_engine-0.8.64}/api_foundry_query_engine/connectors/connection.py +0 -0
  19. {api_foundry_query_engine-0.8.62 → api_foundry_query_engine-0.8.64}/api_foundry_query_engine/connectors/connection_factory.py +0 -0
  20. {api_foundry_query_engine-0.8.62 → api_foundry_query_engine-0.8.64}/api_foundry_query_engine/connectors/oracle_connector.py +0 -0
  21. {api_foundry_query_engine-0.8.62 → api_foundry_query_engine-0.8.64}/api_foundry_query_engine/connectors/postgres_connection.py +0 -0
  22. {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
  23. {api_foundry_query_engine-0.8.62 → api_foundry_query_engine-0.8.64}/api_foundry_query_engine/dao/dao.py +0 -0
  24. {api_foundry_query_engine-0.8.62 → api_foundry_query_engine-0.8.64}/api_foundry_query_engine/dao/operation_dao.py +0 -0
  25. {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
  26. {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
  27. {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
  28. {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
  29. {api_foundry_query_engine-0.8.62 → api_foundry_query_engine-0.8.64}/api_foundry_query_engine/lambda_handler.py +0 -0
  30. {api_foundry_query_engine-0.8.62 → api_foundry_query_engine-0.8.64}/api_foundry_query_engine/services/service.py +0 -0
  31. {api_foundry_query_engine-0.8.62 → api_foundry_query_engine-0.8.64}/api_foundry_query_engine/services/transactional_service.py +0 -0
  32. {api_foundry_query_engine-0.8.62 → api_foundry_query_engine-0.8.64}/api_foundry_query_engine/utils/app_exception.py +0 -0
  33. {api_foundry_query_engine-0.8.62 → api_foundry_query_engine-0.8.64}/api_foundry_query_engine/utils/dependency_resolver.py +0 -0
  34. {api_foundry_query_engine-0.8.62 → api_foundry_query_engine-0.8.64}/api_foundry_query_engine/utils/logger.py +0 -0
  35. {api_foundry_query_engine-0.8.62 → api_foundry_query_engine-0.8.64}/api_foundry_query_engine/utils/reference_resolver.py +0 -0
  36. {api_foundry_query_engine-0.8.62 → api_foundry_query_engine-0.8.64}/api_foundry_query_engine/utils/token_decoder.py +0 -0
  37. {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.62
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
- self._insert_selection_results = {**read_props, **write_props}
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
- if property.inject_value and property_name in self.operation.store_params:
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
- if property.inject_value and "create" in property.inject_on:
179
- injected_value = self.extract_injected_value(property.inject_value)
180
- if injected_value is not None:
181
- placeholder_key = f"__inject_{property_name}"
182
- columns.append(property.column_name)
183
- placeholders.append(self.placeholder(property, placeholder_key))
184
- self.store_placeholders[
185
- placeholder_key
186
- ] = property.convert_to_db_value(injected_value)
187
- elif property.required:
188
- raise ApplicationException(
189
- 400,
190
- f"Required injected property '{property_name}' "
191
- + f"could not be populated from '{property.inject_value}'",
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 {parent_alias}.{self.relation.parent_property} "
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
- if property.inject_value and property_name in self.operation.store_params:
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
- if property.inject_value and "update" in property.inject_on:
153
- injected_value = self.extract_injected_value(property.inject_value)
154
- if injected_value is not None:
155
- placeholder_key = f"__inject_{property_name}"
156
- column_name = property.column_name
157
- columns.append(
158
- f"{column_name} = {self.placeholder(property, placeholder_key)}"
159
- )
160
- self.store_placeholders[
161
- placeholder_key
162
- ] = property.convert_to_db_value(injected_value)
163
- elif property.required:
164
- raise ApplicationException(
165
- 400,
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
- if len(literal_parts) == 1:
424
- return literal_parts[0]
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"