def from_query_object(cls, select: list[Union[str, dict]], join: dict[str, dict]): # type: ignore[override] # Check types if not isinstance(select, list): raise exc.QueryObjectError(f'"select" must be an array') if not isinstance(join, dict): raise exc.QueryObjectError(f'"join" must be an array') # Tell fields and relations apart field: Union[str, dict] fields: list[SelectedField] = [] relations: list[SelectedRelation] = [] for field in (*select, join): # str: 'field_name' if isinstance(field, str): fields.append(SelectedField( name=field, handler=None)) # type: ignore[arg-type] # dict: {'field_name': QueryObject} elif isinstance(field, dict): relations.extend( SelectedRelation( name=name, query=QueryObject.from_query_object( query)) # type: ignore[call-arg,type-arg] for name, query in field.items()) # anything else: not supported else: raise exc.QueryObjectError( f'Unsupported type encountered in "select": {field!r}') # Construct return cls(fields=fields, relations=relations)
def __init__(self, cursor: Optional[str], *, skip: Optional[int], limit: Optional[int]): super().__init__(cursor, limit=limit) # Parse the cursor if cursor is not None: self.cursor_value = SkipCursorData.decode(cursor) # The 'limit' value has to either be empty or remain the same. # You can't change the limit midway. if self.limit not in (None, self.cursor_value.limit): # type: ignore[union-attr] raise exc.QueryObjectError( 'You cannot adjust the "limit" while using cursor-based pagination. ' 'Either keep it constant, or replace with `null`.' ) self.limit = self.cursor_value.limit # type: ignore[union-attr] elif skip is not None and limit is not None: self.cursor_value = SkipCursorData(skip=skip, limit=limit) elif limit is not None: self.cursor_value = None self.limit = limit else: self.cursor_value = None self.limit = None self.page_info = None # type: ignore[assignment]
def apply_to_statement(self, query: QueryObject, target_Model: SAModelOrAlias, stmt: sa.sql.Select) -> sa.sql.Select: # Prepare the filter expression cursor = self.cursor_value if cursor is None: filter_expression = True limit = self.limit else: # Make sure the columns are still the same if set(cursor.cols) != query.sort.names: raise exc.QueryObjectError('You cannot adjust "sort" fields while using cursor-based pagination.') # Filter op = {'>': operator.gt, '<': operator.lt}[cursor.op] filter_expression = op( sa.tuple_(*( resolve_column_by_name(field.name, target_Model, where='skip') for field in query.sort.fields )), cursor.val ) limit = cursor.limit if limit is None: return stmt # Paginate # We will always load one more row to check if there's a next page if SA_14: return stmt.filter(filter_expression).limit(limit + 1) else: return stmt.where(filter_expression).limit(limit + 1)
def __init__(self, query: QueryObject, target_Model: SAModelOrAlias, settings: QuerySettings, skiplimit_op: SkipLimitOperation): super().__init__(query, target_Model, settings) # Prepare the limit self.skiplimit_op = skiplimit_op self.limit = self.settings.get_final_limit(self.skiplimit_op.limit) # Only one of these can be used, not simultaneously before = self.query.before.cursor after = self.query.after.cursor if sum([ before is not None, after is not None, self.query.skip.skip is not None, ]) > 1: raise exc.QueryObjectError( "Choose a pagination method and use either 'skip', or 'before', or 'after'." ) # Set self.cursor, self.direction if before is not None: self.cursor = before self.direction = -1 elif after is not None: self.cursor = after self.direction = +1 else: self.cursor = None self.direction = None
def from_query_object(cls, filter: dict): # type: ignore[override] # Check types if not isinstance(filter, dict): raise exc.QueryObjectError(f'"filter" must be an object') # Construct conditions = cls._parse_input_fields(filter) return cls(conditions=conditions)
def from_query_object(cls, sort: list[str]): # type: ignore[override] # Check types if not isinstance(sort, list): raise exc.QueryObjectError(f'"sort" must be an array') # Construct fields = [cls._parse_input_field(field) for field in sort] return cls(fields=fields)
def _parse_input_boolean_expression(cls, operator: str, conditions: Union[dict, list[dict]]): # Check types # $not is the only unary operator if operator == '$not': if not isinstance(conditions, dict): raise exc.QueryObjectError(f"{operator}'s operand must be an object") conditions = [conditions] # Every other operator receives a list of conditions else: if not isinstance(conditions, list): raise exc.QueryObjectError(f"{operator}'s operand must be an array") # Construct return BooleanFilterExpression( operator=operator, clauses=list(itertools.chain.from_iterable( cls._parse_input_fields(condition) for condition in conditions )) )
def _validate_operator_argument(self, condition: FieldFilterExpression): """ Validate or fail: that the operation and its arguments make sense Raises: exc.QueryObjectError """ operator = condition.operator # See if this operator requires array argument if operator in self.ARRAY_OPERATORS_WITH_ARRAY_ARGUMENT: if not _is_array(condition.value): raise exc.QueryObjectError( f'Filter: {operator} argument must be an array')
def ensure_query_object( cls, input: Optional[Union[QueryObject, QueryObjectDict]]) -> QueryObject: """ Construct a Query Object from any valid input """ if input is None: return cls.from_query_object({}) # type:ignore[typeddict-item] elif isinstance(input, QueryObject): return input elif isinstance(input, dict): return QueryObject.from_query_object(input) else: raise exc.QueryObjectError( f'QueryObject must be an object, "{type(input).__name__}" given' )
def __init__(self, name: str, sub_path: Optional[tuple[str, ...]], Model: SAModelOrAlias, context: NameContext): attribute = sainfo.columns.resolve_column_by_name(name, Model, where=context.value) self.context = context self.name = name self.sub_path = sub_path self.property = attribute.property self.is_array = sainfo.columns.is_array(attribute) self.is_json = sainfo.columns.is_json(attribute) if self.sub_path and not self.is_json: raise exc.QueryObjectError( f'Field "{self.name}" does not support dot-notation: not a JSON field' )
def _get_operator_lambda( self, operator: str, *, use_array: bool ) -> abc.Callable[[sa.sql.ColumnElement, sa.sql.ColumnElement, Any], sa.sql.ColumnElement]: """ Get a callable that implements the operator Args: operator: Operator name use_array: Shall we use the array version of this operator? """ # Find the operator try: if use_array: return self.ARRAY_OPERATORS[operator] else: return self.SCALAR_OPERATORS[operator] # Operator not found except KeyError: raise exc.QueryObjectError(f'Unsupported operator: {operator}')
def query_object( *, select: Optional[str] = fastapi.Query( None, title='The list of fields to select.', description='Example: `[id, login, { users: {... } }]`. JSON or YAML.', ), join: Optional[str] = fastapi.Query( None, title='The list of relations to select.', description='Example: `[users: {...}]`. JSON or YAML.', ), filter: Optional[str] = fastapi.Query( None, title='Filter criteria.', description= 'MongoDB format. Example: `{ age: { $gt: 18 } }`. JSON or YAML.'), sort: Optional[str] = fastapi.Query( None, title='Sorting order', description= 'List of columns with `+` or `-`. Example: `[ "login", "ctime-" ]`. JSON or YAML.', ), skip: Optional[int] = fastapi.Query( None, title='Pagination. The number of items to skip.'), limit: Optional[int] = fastapi.Query( None, title='Pagination. The number of items to include.'), before: Optional[str] = fastapi.Query( None, title='Pagination. A cursor to the previous page.', ), after: Optional[str] = fastapi.Query( None, title='Pagination. A cursor to the next page.'), ) -> Optional[QueryObject]: """ Get the JessiQL Query Object from the request parameters Example: /api/?select=[a, b, c]&filter={ age: { $gt: 18 } } Raises: exc.QueryObjectError """ # Empty? if not select and not filter and not sort and not skip and not limit: return None # Query Object dict try: query_object_dict: QueryObjectDict = dict( # type: ignore[typeddict-item] select=parse_serialized_argument('select', select), # join=parse_serialized_argument('join', join), filter=parse_serialized_argument('filter', filter), sort=parse_serialized_argument('sort', sort), skip=skip, limit=limit, before=before, after=after, ) except ArgumentValueError as e: raise exc.QueryObjectError( f'Query Object `{e.argument_name}` parsing failed: {e}') from e # Parse query_object = QueryObject.from_query_object(query_object_dict) # Convert return query_object
def from_query_object(cls, limit: Optional[int]): # type: ignore[override] if limit is None or isinstance(limit, int): return cls(limit=limit) else: raise exc.QueryObjectError(f'"limit" must be an integer')
def from_query_object(cls, skip: Optional[int]): # type: ignore[override] if skip is None or isinstance(skip, int): return cls(skip=skip) else: raise exc.QueryObjectError(f'"skip" must be an integer')