From 7cbe200801d7cfab1e02991119a81419b4a1f8eb Mon Sep 17 00:00:00 2001 From: Yaro Kasear Date: Fri, 5 Sep 2025 16:41:16 -0500 Subject: [PATCH] Fixes on dot resolution not understanding root aliases. --- crudkit/core/service.py | 38 +++++++++++++++----------- crudkit/core/spec.py | 59 +++++++++++++++++++++-------------------- 2 files changed, 53 insertions(+), 44 deletions(-) diff --git a/crudkit/core/service.py b/crudkit/core/service.py index a7da4e5..34b218a 100644 --- a/crudkit/core/service.py +++ b/crudkit/core/service.py @@ -18,37 +18,44 @@ class CRUDService(Generic[T]): def get_query(self): if self.polymorphic: poly_model = with_polymorphic(self.model, '*') - return self.session.query(poly_model) + return self.session.query(poly_model), poly_model else: base_only = with_polymorphic(self.model, [], flat=True) - return self.session.query(base_only) + return self.session.query(base_only), base_only def get(self, id: int, include_deleted: bool = False) -> T | None: - obj = self.get_query().filter_by(id=id).first() - if obj is None: - return None - if self.supports_soft_delete and not include_deleted and obj.is_deleted: - return None - return obj + query, root_alias = self.get_query() + + if self.supports_soft_delete and not include_deleted: + query = query.filter(getattr(root_alias, "is_deleted") == False) + + query = query.filter(getattr(root_alias, "id") == id) + + obj = query.first() + return obj or None def list(self, params=None) -> list[T]: - query = self.get_query() + query, root_alias = self.get_query() if params: if self.supports_soft_delete: - include_deleted = False include_deleted = _is_truthy(params.get('include_deleted')) if not include_deleted: - query = query.filter(self.model.is_deleted == False) - spec = CRUDSpec(self.model, params) + query = query.filter(getattr(root_alias, "is_deleted") == False) + + spec = CRUDSpec(self.model, params, root_alias) filters = spec.parse_filters() order_by = spec.parse_sort() limit, offset = spec.parse_pagination() - for parent, relationship_attr, alias in spec.get_join_paths(): - query = query.join(alias, relationship_attr.of_type(alias), isouter=True) + for parent_alias, relationship_attr, target_alias in spec.get_join_paths(): + query = query.join( + target_alias, + relationship_attr.of_type(target_alias), + isouter=True + ) - for eager in spec.get_eager_loads(): + for eager in spec.get_eager_loads(root_alias): query = query.options(eager) if filters: @@ -56,6 +63,7 @@ class CRUDService(Generic[T]): if order_by: query = query.order_by(*order_by) query = query.offset(offset).limit(limit) + return query.all() def create(self, data: dict, actor=None) -> T: diff --git a/crudkit/core/spec.py b/crudkit/core/spec.py index 5840071..26348ea 100644 --- a/crudkit/core/spec.py +++ b/crudkit/core/spec.py @@ -14,36 +14,40 @@ OPERATORS = { } class CRUDSpec: - def __init__(self, model, params): + def __init__(self, model, params, root_alias): self.model = model self.params = params + self.root_alias = root_alias self.eager_paths: Set[Tuple[str, ...]] = set() self.join_paths: List[Tuple[object, InstrumentedAttribute, object]] = [] self.alias_map: Dict[Tuple[str, ...], object] = {} def _resolve_column(self, path: str): - current_model = self.model - current_alias = self.model + current_alias = self.root_alias parts = path.split('.') - join_path = [] + join_path: list[str] = [] for i, attr in enumerate(parts): - if not hasattr(current_model, attr): + try: + attr_obj = getattr(current_alias, attr) + except AttributeError: return None, None - attr_obj = getattr(current_model, attr) - if isinstance(attr_obj, InstrumentedAttribute): - if hasattr(attr_obj.property, 'direction'): - join_path.append(attr) - path_key = tuple(join_path) - alias = self.alias_map.get(path_key) - if not alias: - alias = aliased(attr_obj.property.mapper.class_) - self.alias_map[path_key] = alias - self.join_paths.append((current_alias, attr_obj, alias)) - current_model = attr_obj.property.mapper.class_ - current_alias = alias - else: - return getattr(current_alias, attr), tuple(join_path) if join_path else None + + prop = getattr(attr_obj, "property", None) + if prop is not None and hasattr(prop, "direction"): + join_path.append(attr) + path_key = tuple(join_path) + alias = self.alias_map.get(path_key) + if not alias: + alias = aliased(prop.mapper.class_) + self.alias_map[path_key] = alias + self.join_paths.append((current_alias, attr_obj, alias)) + current_alias = alias + continue + + if isinstance(attr_obj, InstrumentedAttribute) or hasattr(attr_obj, "clauses"): + return attr_obj, tuple(join_path) if join_path else None + return None, None def parse_filters(self): @@ -90,19 +94,16 @@ class CRUDSpec: offset = int(self.params.get('offset', 0)) return limit, offset - def get_eager_loads(self): + def get_eager_loads(self, root_alias): loads = [] for path in self.eager_paths: - current = self.model + current = root_alias loader = None - for attr in path: - attr_obj = getattr(current, attr) - if loader is None: - loader = joinedload(attr_obj) - else: - loader = loader.joinedload(attr_obj) - current = attr_obj.property.mapper.class_ - if loader: + for name in path: + rel_attr = getattr(current, name) + loader = (joinedload(rel_attr) if loader is None else loader.joinedload(name)) + current = rel_attr.property.mapper.class_ + if loader is not None: loads.append(loader) return loads