Exemple #1
0
    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)
Exemple #2
0
    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]
Exemple #3
0
    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)
Exemple #4
0
    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
Exemple #5
0
    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)
Exemple #6
0
    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)
Exemple #7
0
    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
            ))
        )
Exemple #8
0
    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')
Exemple #9
0
 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'
         )
Exemple #10
0
    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'
            )
Exemple #11
0
    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}')
Exemple #12
0
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
Exemple #13
0
 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')
Exemple #14
0
 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')