api-foundry-query-engine 0.8.63__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.63 → 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.63 → api_foundry_query_engine-0.8.64}/api_foundry_query_engine/dao/sql_insert_query_handler.py +21 -34
  4. {api_foundry_query_engine-0.8.63 → api_foundry_query_engine-0.8.64}/api_foundry_query_engine/dao/sql_query_handler.py +19 -88
  5. {api_foundry_query_engine-0.8.63 → api_foundry_query_engine-0.8.64}/api_foundry_query_engine/dao/sql_subselect_query_handler.py +1 -6
  6. {api_foundry_query_engine-0.8.63 → api_foundry_query_engine-0.8.64}/api_foundry_query_engine/dao/sql_update_query_handler.py +9 -18
  7. {api_foundry_query_engine-0.8.63 → api_foundry_query_engine-0.8.64}/api_foundry_query_engine/operation.py +3 -0
  8. {api_foundry_query_engine-0.8.63 → api_foundry_query_engine-0.8.64}/api_foundry_query_engine/utils/api_model.py +14 -35
  9. {api_foundry_query_engine-0.8.63 → api_foundry_query_engine-0.8.64}/api_foundry_query_engine/utils/claims_check.py +3 -4
  10. api_foundry_query_engine-0.8.63/api_foundry_query_engine/__init__.py +0 -1
  11. {api_foundry_query_engine-0.8.63 → api_foundry_query_engine-0.8.64}/.gitignore +0 -0
  12. {api_foundry_query_engine-0.8.63 → api_foundry_query_engine-0.8.64}/LICENSE +0 -0
  13. {api_foundry_query_engine-0.8.63 → api_foundry_query_engine-0.8.64}/api_foundry_query_engine/.pre-commit-config.yaml +0 -0
  14. {api_foundry_query_engine-0.8.63 → api_foundry_query_engine-0.8.64}/api_foundry_query_engine/adapters/adapter.py +0 -0
  15. {api_foundry_query_engine-0.8.63 → api_foundry_query_engine-0.8.64}/api_foundry_query_engine/adapters/case_change_adapter.py +0 -0
  16. {api_foundry_query_engine-0.8.63 → api_foundry_query_engine-0.8.64}/api_foundry_query_engine/adapters/gateway_adapter.py +0 -0
  17. {api_foundry_query_engine-0.8.63 → api_foundry_query_engine-0.8.64}/api_foundry_query_engine/adapters/security_adapter.py +0 -0
  18. {api_foundry_query_engine-0.8.63 → api_foundry_query_engine-0.8.64}/api_foundry_query_engine/connectors/connection.py +0 -0
  19. {api_foundry_query_engine-0.8.63 → api_foundry_query_engine-0.8.64}/api_foundry_query_engine/connectors/connection_factory.py +0 -0
  20. {api_foundry_query_engine-0.8.63 → api_foundry_query_engine-0.8.64}/api_foundry_query_engine/connectors/oracle_connector.py +0 -0
  21. {api_foundry_query_engine-0.8.63 → api_foundry_query_engine-0.8.64}/api_foundry_query_engine/connectors/postgres_connection.py +0 -0
  22. {api_foundry_query_engine-0.8.63 → 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.63 → api_foundry_query_engine-0.8.64}/api_foundry_query_engine/dao/dao.py +0 -0
  24. {api_foundry_query_engine-0.8.63 → api_foundry_query_engine-0.8.64}/api_foundry_query_engine/dao/operation_dao.py +0 -0
  25. {api_foundry_query_engine-0.8.63 → 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.63 → 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.63 → 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.63 → 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.63 → api_foundry_query_engine-0.8.64}/api_foundry_query_engine/lambda_handler.py +0 -0
  30. {api_foundry_query_engine-0.8.63 → api_foundry_query_engine-0.8.64}/api_foundry_query_engine/services/service.py +0 -0
  31. {api_foundry_query_engine-0.8.63 → api_foundry_query_engine-0.8.64}/api_foundry_query_engine/services/transactional_service.py +0 -0
  32. {api_foundry_query_engine-0.8.63 → api_foundry_query_engine-0.8.64}/api_foundry_query_engine/utils/app_exception.py +0 -0
  33. {api_foundry_query_engine-0.8.63 → api_foundry_query_engine-0.8.64}/api_foundry_query_engine/utils/dependency_resolver.py +0 -0
  34. {api_foundry_query_engine-0.8.63 → api_foundry_query_engine-0.8.64}/api_foundry_query_engine/utils/logger.py +0 -0
  35. {api_foundry_query_engine-0.8.63 → api_foundry_query_engine-0.8.64}/api_foundry_query_engine/utils/reference_resolver.py +0 -0
  36. {api_foundry_query_engine-0.8.63 → api_foundry_query_engine-0.8.64}/api_foundry_query_engine/utils/token_decoder.py +0 -0
  37. {api_foundry_query_engine-0.8.63 → 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.63
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"
@@ -12,9 +12,7 @@ log = logger(__name__)
12
12
  class SQLInsertSchemaQueryHandler(SQLSchemaQueryHandler):
13
13
  key_property: Optional[SchemaObjectProperty]
14
14
 
15
- def __init__(
16
- self, operation: Operation, schema_object: SchemaObject, engine: str
17
- ) -> None:
15
+ def __init__(self, operation: Operation, schema_object: SchemaObject, engine: str) -> None:
18
16
  super().__init__(operation, schema_object, engine)
19
17
  self.key_property = schema_object.primary_key
20
18
  if self.key_property:
@@ -33,9 +31,7 @@ class SQLInsertSchemaQueryHandler(SQLSchemaQueryHandler):
33
31
  + f" required. schema_object: {schema_object.api_name}",
34
32
  )
35
33
  self.concurrency_property = schema_object.concurrency_property
36
- if self.concurrency_property and operation.store_params.get(
37
- self.concurrency_property.api_name
38
- ):
34
+ if self.concurrency_property and operation.store_params.get(self.concurrency_property.api_name):
39
35
  raise ApplicationException(
40
36
  400,
41
37
  "Versioned properties can not be supplied a store parameters. "
@@ -62,7 +58,9 @@ class SQLInsertSchemaQueryHandler(SQLSchemaQueryHandler):
62
58
  self.schema_object.properties,
63
59
  )
64
60
  # Combine both (union of read and write)
65
- 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)
66
64
  return self._insert_selection_results
67
65
 
68
66
  @property
@@ -73,10 +71,7 @@ class SQLInsertSchemaQueryHandler(SQLSchemaQueryHandler):
73
71
  returning_clause = self._get_returning_clause()
74
72
 
75
73
  if not self.concurrency_property:
76
- return (
77
- f"INSERT INTO {self.table_expression}{self.insert_values} "
78
- + returning_clause
79
- )
74
+ return f"INSERT INTO {self.table_expression}{self.insert_values} " + returning_clause
80
75
 
81
76
  if self.operation.store_params.get(self.concurrency_property.api_name):
82
77
  raise ApplicationException(
@@ -86,10 +81,7 @@ class SQLInsertSchemaQueryHandler(SQLSchemaQueryHandler):
86
81
  + f" schema_object: {self.schema_object.api_name}, "
87
82
  + f"property: {self.concurrency_property.api_name}",
88
83
  )
89
- return (
90
- f"INSERT INTO {self.table_expression}{self.insert_values}"
91
- + returning_clause
92
- )
84
+ return f"INSERT INTO {self.table_expression}{self.insert_values} " + returning_clause
93
85
 
94
86
  def _get_returning_clause(self) -> str:
95
87
  """
@@ -122,11 +114,7 @@ class SQLInsertSchemaQueryHandler(SQLSchemaQueryHandler):
122
114
  allowed_property_names = self.check_permissions(
123
115
  "write", self.schema_object.permissions, self.schema_object.properties
124
116
  )
125
- allowed_properties = {
126
- k: v
127
- for k, v in self.schema_object.properties.items()
128
- if k in allowed_property_names
129
- }
117
+ allowed_properties = {k: v for k, v in self.schema_object.properties.items() if k in allowed_property_names}
130
118
  log.info("allowed properties: %s", allowed_properties)
131
119
 
132
120
  for property_name, property in self.schema_object.properties.items():
@@ -161,17 +149,13 @@ class SQLInsertSchemaQueryHandler(SQLSchemaQueryHandler):
161
149
 
162
150
  columns.append(property.column_name)
163
151
  if property.api_name is None:
164
- raise ApplicationException(
165
- 400, f"Property '{name}' does not have a valid api_name."
166
- )
152
+ raise ApplicationException(400, f"Property '{name}' does not have a valid api_name.")
167
153
  placeholders.append(self.placeholder(property, property.api_name))
168
154
  # Serialize embedded objects to JSON
169
155
  if property.api_type == "object":
170
156
  self.store_placeholders[property.api_name] = json.dumps(value)
171
157
  else:
172
- self.store_placeholders[
173
- property.api_name
174
- ] = property.convert_to_db_value(value)
158
+ self.store_placeholders[property.api_name] = property.convert_to_db_value(value)
175
159
 
176
160
  for property_name, property in self.schema_object.properties.items():
177
161
  inject_value = getattr(property, "inject_value", None)
@@ -181,12 +165,17 @@ class SQLInsertSchemaQueryHandler(SQLSchemaQueryHandler):
181
165
  if property_name in self.store_placeholders:
182
166
  continue
183
167
 
184
- injected_value = self.extract_injected_value(property)
185
- columns.append(property.column_name)
186
- placeholders.append(self.placeholder(property, property.api_name))
187
- self.store_placeholders[property.api_name] = property.convert_to_db_value(
188
- injected_value
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)
189
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)
190
179
 
191
180
  if self.key_property:
192
181
  if self.key_property.key_type == "sequence":
@@ -198,8 +187,6 @@ class SQLInsertSchemaQueryHandler(SQLSchemaQueryHandler):
198
187
  if self.concurrency_property.column_type == "integer":
199
188
  placeholders.append("1")
200
189
  else:
201
- placeholders.append(
202
- self.concurrency_generator(self.concurrency_property)
203
- )
190
+ placeholders.append(self.concurrency_generator(self.concurrency_property))
204
191
 
205
192
  return f" ( {', '.join(columns)} ) VALUES ( {', '.join(placeholders)})"
@@ -110,9 +110,7 @@ class SQLQueryHandler:
110
110
  operation: Operation
111
111
  engine: str
112
112
 
113
- def __init__(
114
- self, operation: Operation, engine: str
115
- ): # , schema_object: SchemaObject):
113
+ def __init__(self, operation: Operation, engine: str): # , schema_object: SchemaObject):
116
114
  self.operation = operation
117
115
  self.engine = engine
118
116
  self.__select_list_columns = None
@@ -182,9 +180,7 @@ class SQLQueryHandler:
182
180
  List[str]: A list of properties the user is permitted to access.
183
181
  """
184
182
  # if permissions are not defined then no restrictions are applied
185
- log.info(
186
- f"checking permissions permission_type: {permission_type}, permissions: {permissions}"
187
- )
183
+ log.info(f"checking permissions permission_type: {permission_type}, permissions: {permissions}")
188
184
  if not permissions:
189
185
  return properties
190
186
 
@@ -225,9 +221,7 @@ class SQLQueryHandler:
225
221
 
226
222
  if regex_pattern:
227
223
  # Use efficient regex dictionary filtering
228
- role_allowed = self._filter_properties_by_regex(
229
- properties, regex_pattern
230
- )
224
+ role_allowed = self._filter_properties_by_regex(properties, regex_pattern)
231
225
  log.info(
232
226
  "role: %s, pattern: %s, matched: %s",
233
227
  role,
@@ -286,9 +280,7 @@ class SQLQueryHandler:
286
280
 
287
281
  for action, rule in actions.items():
288
282
  # Normalize action names: create/update -> write
289
- normalized_action = (
290
- "write" if action in ("create", "update") else action
291
- )
283
+ normalized_action = "write" if action in ("create", "update") else action
292
284
 
293
285
  if normalized_action in normalized["default"]:
294
286
  normalized["default"][normalized_action][role] = rule
@@ -332,9 +324,7 @@ class SQLQueryHandler:
332
324
  return permission_rule
333
325
  elif isinstance(permission_rule, dict):
334
326
  # Support both 'properties' (preferred) and 'fields' (legacy)
335
- return permission_rule.get("properties") or permission_rule.get(
336
- "fields", ""
337
- )
327
+ return permission_rule.get("properties") or permission_rule.get("fields", "")
338
328
  else:
339
329
  return ""
340
330
 
@@ -342,38 +332,7 @@ class SQLQueryHandler:
342
332
  def selection_results(self) -> Dict:
343
333
  raise NotImplementedError()
344
334
 
345
- def extract_injected_value(
346
- self, property: SchemaObjectProperty
347
- ) -> Optional[object]:
348
- inject_value = getattr(property, "inject_value", None)
349
- if not inject_value:
350
- return None
351
-
352
- if inject_value.startswith("claim:"):
353
- claim_name = inject_value.split(":", 1)[1]
354
- return self.operation.claims.get(claim_name)
355
-
356
- if inject_value == "timestamp":
357
- return datetime.utcnow().isoformat()
358
-
359
- if inject_value == "date":
360
- return date.today().isoformat()
361
-
362
- if inject_value == "uuid":
363
- return str(uuid.uuid4())
364
-
365
- if inject_value.startswith("env:"):
366
- env_name = inject_value.split(":", 1)[1]
367
- return os.getenv(env_name)
368
-
369
- raise ApplicationException(
370
- 500,
371
- f"Unsupported inject_value source '{inject_value}' for property '{property.api_name}'",
372
- )
373
-
374
- def generate_sql_condition(
375
- self, property: SchemaObjectProperty, value, prefix: Optional[str] = None
376
- ) -> str:
335
+ def generate_sql_condition(self, property: SchemaObjectProperty, value, prefix: Optional[str] = None) -> str:
377
336
  operand = "="
378
337
  if isinstance(value, str):
379
338
  parts = value.split("::", 1)
@@ -385,9 +344,7 @@ class SQLQueryHandler:
385
344
  value_str = str(value)
386
345
 
387
346
  column = f"{prefix}.{property.column_name}" if prefix else property.column_name
388
- placeholder_name = (
389
- f"{prefix}_{property.api_name}" if prefix else property.api_name
390
- )
347
+ placeholder_name = f"{prefix}_{property.api_name}" if prefix else property.api_name
391
348
 
392
349
  if operand in ["between", "not-between"]:
393
350
  value_set = value_str.split(",")
@@ -395,17 +352,14 @@ class SQLQueryHandler:
395
352
  elif operand in ["in", "not-in"]:
396
353
  value_set = value_str.split(",")
397
354
  assignments = [
398
- self.placeholder(property, f"{placeholder_name}_{index}")
399
- for index, _ in enumerate(value_set)
355
+ self.placeholder(property, f"{placeholder_name}_{index}") for index, _ in enumerate(value_set)
400
356
  ]
401
357
  sql = f"{column} {'NOT ' if operand == 'not-in' else ''}IN ({', '.join(assignments)})" # noqa E501
402
358
  else:
403
359
  sql = f"{column} {operand} {self.placeholder(property, str(placeholder_name))}"
404
360
  return sql
405
361
 
406
- def generate_placeholders(
407
- self, property: SchemaObjectProperty, value, prefix: Optional[str] = None
408
- ) -> dict:
362
+ def generate_placeholders(self, property: SchemaObjectProperty, value, prefix: Optional[str] = None) -> dict:
409
363
  operand = "="
410
364
 
411
365
  if isinstance(value, str):
@@ -417,9 +371,7 @@ class SQLQueryHandler:
417
371
  else:
418
372
  value_str = str(value)
419
373
 
420
- placeholder_name = (
421
- f"{prefix}_{property.api_name}" if prefix else property.api_name
422
- )
374
+ placeholder_name = f"{prefix}_{property.api_name}" if prefix else property.api_name
423
375
  placeholders = {}
424
376
 
425
377
  if operand in ["between", "not-between"]:
@@ -459,9 +411,6 @@ class SQLQueryHandler:
459
411
  Raises:
460
412
  ApplicationException: If source format is invalid
461
413
  """
462
- import os
463
- import uuid
464
-
465
414
  if inject_value_source.startswith("claim:"):
466
415
  claim_key = inject_value_source[6:]
467
416
  return self.operation.claims.get(claim_key)
@@ -475,17 +424,13 @@ class SQLQueryHandler:
475
424
  env_key = inject_value_source[4:]
476
425
  return os.environ.get(env_key)
477
426
  else:
478
- raise ApplicationException(
479
- 400, f"Unknown inject value source: {inject_value_source}"
480
- )
427
+ raise ApplicationException(400, f"Unknown inject value source: {inject_value_source}")
481
428
 
482
429
 
483
430
  class SQLSchemaQueryHandler(SQLQueryHandler):
484
431
  schema_object: SchemaObject
485
432
 
486
- def __init__(
487
- self, operation: Operation, schema_object: SchemaObject, engine: str
488
- ) -> None:
433
+ def __init__(self, operation: Operation, schema_object: SchemaObject, engine: str) -> None:
489
434
  super().__init__(operation, engine)
490
435
  self.schema_object = schema_object
491
436
  self.single_table = self.__single_table()
@@ -514,10 +459,7 @@ class SQLSchemaQueryHandler(SQLQueryHandler):
514
459
  entity_lower = entity.lower()
515
460
  for i in range(1, len(entity_lower) + 1):
516
461
  substring = entity_lower[:i]
517
- if (
518
- substring not in self._prefix_map.values()
519
- and substring not in SQL_RESERVED_WORDS
520
- ):
462
+ if substring not in self._prefix_map.values() and substring not in SQL_RESERVED_WORDS:
521
463
  self._prefix_map[entity] = substring
522
464
  break
523
465
  return self._prefix_map
@@ -557,9 +499,7 @@ class SQLSchemaQueryHandler(SQLQueryHandler):
557
499
  allowed_properties = self.check_permissions(
558
500
  "read", self.schema_object.permissions, self.schema_object.properties
559
501
  )
560
- self.__selection_results = self.filter_and_prefix_keys(
561
- filters, allowed_properties
562
- )
502
+ self.__selection_results = self.filter_and_prefix_keys(filters, allowed_properties)
563
503
  return self.__selection_results
564
504
 
565
505
  def _has_soft_delete_conflicts(self) -> Dict[str, bool]:
@@ -648,8 +588,7 @@ class SQLSchemaQueryHandler(SQLQueryHandler):
648
588
  if excluded_values:
649
589
  # Format values for SQL IN clause
650
590
  formatted_values = ", ".join(
651
- f"'{val}'" if isinstance(val, str) else str(val)
652
- for val in excluded_values
591
+ f"'{val}'" if isinstance(val, str) else str(val) for val in excluded_values
653
592
  )
654
593
  conditions.append(f"{column_name} NOT IN ({formatted_values})")
655
594
 
@@ -667,20 +606,14 @@ class SQLSchemaQueryHandler(SQLQueryHandler):
667
606
 
668
607
  for name, value in self.operation.query_params.items():
669
608
  if "." in name:
670
- raise ApplicationException(
671
- 400, "Selection on relations is not supported"
672
- )
609
+ raise ApplicationException(400, "Selection on relations is not supported")
673
610
  property = self.schema_object.properties.get(name)
674
611
  if not property:
675
- raise ApplicationException(
676
- 500, f"Search condition column not found {name}"
677
- )
612
+ raise ApplicationException(500, f"Search condition column not found {name}")
678
613
  if (
679
614
  self.operation.action != "read"
680
615
  and isinstance(value, str)
681
- and re.match(
682
- r"^(lt|le|eq|ne|gt|ge|in|not-in|between|not-between)::(.+)$", value
683
- )
616
+ and re.match(r"^(lt|le|eq|ne|gt|ge|in|not-in|between|not-between)::(.+)$", value)
684
617
  and self.schema_object.concurrency_property
685
618
  ):
686
619
  raise ApplicationException(
@@ -696,9 +629,7 @@ class SQLSchemaQueryHandler(SQLQueryHandler):
696
629
  self.search_placeholders.update(holders)
697
630
  return f" WHERE {' AND '.join(conditions)}" if conditions else ""
698
631
 
699
- def filter_and_prefix_keys(
700
- self, regex_list: List[str], properties: dict, prefix: Optional[str] = None
701
- ) -> dict:
632
+ def filter_and_prefix_keys(self, regex_list: List[str], properties: dict, prefix: Optional[str] = None) -> dict:
702
633
  """
703
634
  Accepts a prefix string, list of regular expressions, and a dictionary.
704
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})"
@@ -8,9 +8,7 @@ from api_foundry_query_engine.utils.api_model import SchemaObject
8
8
 
9
9
 
10
10
  class SQLUpdateSchemaQueryHandler(SQLSchemaQueryHandler):
11
- def __init__(
12
- self, operation: Operation, schema_object: SchemaObject, engine: str
13
- ) -> None:
11
+ def __init__(self, operation: Operation, schema_object: SchemaObject, engine: str) -> None:
14
12
  super().__init__(operation, schema_object, engine)
15
13
 
16
14
  def _template_where(self, expr: str) -> str:
@@ -107,11 +105,7 @@ class SQLUpdateSchemaQueryHandler(SQLSchemaQueryHandler):
107
105
  allowed_property_names = self.check_permissions(
108
106
  "write", self.schema_object.permissions, self.schema_object.properties
109
107
  )
110
- allowed_properties = {
111
- k: v
112
- for k, v in self.schema_object.properties.items()
113
- if k in allowed_property_names
114
- }
108
+ allowed_properties = {k: v for k, v in self.schema_object.properties.items() if k in allowed_property_names}
115
109
  self.store_placeholders = {}
116
110
  columns = []
117
111
  invalid_columns = []
@@ -133,9 +127,7 @@ class SQLUpdateSchemaQueryHandler(SQLSchemaQueryHandler):
133
127
  invalid_columns.append(name)
134
128
  continue
135
129
 
136
- placeholder = (
137
- str(property.api_name) if property.api_name is not None else name
138
- )
130
+ placeholder = str(property.api_name) if property.api_name is not None else name
139
131
  column_name = property.column_name
140
132
 
141
133
  columns.append(f"{column_name} = {self.placeholder(property, placeholder)}")
@@ -143,9 +135,7 @@ class SQLUpdateSchemaQueryHandler(SQLSchemaQueryHandler):
143
135
  if property.api_type == "object":
144
136
  self.store_placeholders[placeholder] = json.dumps(value)
145
137
  else:
146
- self.store_placeholders[placeholder] = property.convert_to_db_value(
147
- value
148
- )
138
+ self.store_placeholders[placeholder] = property.convert_to_db_value(value)
149
139
 
150
140
  for property_name, property in self.schema_object.properties.items():
151
141
  inject_value = getattr(property, "inject_value", None)
@@ -154,18 +144,19 @@ class SQLUpdateSchemaQueryHandler(SQLSchemaQueryHandler):
154
144
  continue
155
145
 
156
146
  placeholder = (
157
- str(property.api_name) if property.api_name is not None else property_name
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)
158
150
  )
159
151
  columns.append(f"{property.column_name} = {self.placeholder(property, placeholder)}")
160
152
  self.store_placeholders[placeholder] = property.convert_to_db_value(
161
- self.extract_injected_value(property)
153
+ self.extract_injected_value(str(inject_value))
162
154
  )
163
155
 
164
156
  if invalid_columns:
165
157
  raise ApplicationException(
166
158
  403,
167
- f"Subject does not have permission to update properties: "
168
- f"{invalid_columns}",
159
+ f"Subject does not have permission to update properties: " f"{invalid_columns}",
169
160
  )
170
161
  return f" SET {', '.join(columns)}"
171
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.63"