Filtering API exampleΒΆ

from flask import Flask
from flask_sqlalchemy import SQLAlchemy
from flask_combo_jsonapi import Api, ResourceDetail, ResourceList
from sqlalchemy import String, func, Integer, Float
from sqlalchemy.dialects.postgresql import ARRAY
from marshmallow_jsonapi.flask import Schema
from marshmallow_jsonapi import fields


# SCHEMAS
TYPE_MAP = {
    int: Integer,
    float: Float,
    str: String,
}


class BaseInSqlFiltering:
    def _in_sql_filter_(self, marshmallow_field, model_column, value, operator):
        """
        Create sqlalchemy filter 'in'
        :param marshmallow_field:
        :param model_column: (sqlalchemy column)
        :param value: filter value
        :param operator: operator: "eq", "in"...
        :return:
        """
        raise NotImplementedError

    def _in__sql_filter_(self, *args, **kwargs):
        return self._in_sql_filter_(*args, **kwargs)

    def _notin_sql_filter_(self, *args, **kwargs):
        """
        Invert filter in
        :param marshmallow_field:
        :param model_column:
        :param value:
        :param operator:
        :return:
        """
        return ~self._in_sql_filter_(*args, **kwargs)

    def _notin__sql_filter_(self, *args, **kwargs):
        return self._notin_sql_filter_(*args, **kwargs)


class ListFieldBase(fields.List, BaseInSqlFiltering):
    def __init__(self, *args, **kwargs):
        # self.container = args[0]
        super().__init__(*args, **kwargs)


class ListField(ListFieldBase):
    def _in_sql_filter_(self, marshmallow_field, model_column, value, operator):
        """
        Create sqlalchemy filter 'in' for array
        :param marshmallow_field:
        :param model_column:
        :param value:
        :param operator:
        :return:
        """
        value = value if isinstance(value, list) else [value]
        value_type = type(value[0])
        if isinstance(model_column.type, ARRAY):
            value = func.cast(value, ARRAY(TYPE_MAP.get(value_type, String)))
        return model_column.op("&&")(value)

    def _ilike_in_str_array_sql_filter_(self, model_column, value, **kwargs):
        return func.array_to_string(model_column, '|').ilike(f'%{value}%')

# // SCHEMAS

# flask app


# Create the Flask application and the Flask-SQLAlchemy object.
app = Flask(__name__)
app.config['DEBUG'] = True
app.config['SQLALCHEMY_DATABASE_URI'] = 'postgresql+psycopg2://postgres:postgres@localhost:5432/postgres'
db = SQLAlchemy(app)


# Create model
class Person(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    name = db.Column(db.String)
    words = db.Column(db.ARRAY(db.TEXT))


# Create schema
class PersonSchema(Schema):
    class Meta:
        type_ = 'person'
        self_view = 'person_detail'
        self_view_kwargs = {'id': '<id>'}
        self_view_many = 'person_list'

    id = fields.Integer(as_string=True, dump_only=True)
    name = fields.String()
    words = ListField(fields.String())


# Create resource managers
class PersonList(ResourceList):
    schema = PersonSchema
    data_layer = {
        'session': db.session,
        'model': Person,
    }


class PersonDetail(ResourceDetail):
    schema = PersonSchema
    data_layer = {
        'session': db.session,
        'model': Person,
    }


@app.route("/create-persons")
def create_persons():
    # Create tables
    db.create_all()

    # create persons
    db.session.add(Person(name="John", words=["foo", "bar", "green-grass"]))
    db.session.add(Person(name="Sam", words=["spam", "eggs", "green-apple"]))
    db.session.commit()
    return {"message": "ok"}


# Create the API object
api = Api(app)
api.route(PersonList, 'person_list', '/persons')
api.route(PersonDetail, 'person_detail', '/persons/<int:id>')

# Start the flask loop
if __name__ == '__main__':
    app.run()

Check existing persons

Request:

GET /persons HTTP/1.1
Content-Type: application/vnd.api+json

Response:

HTTP/1.1 200 OK
Content-Type: application/vnd.api+json

{
  "data": [
    {
      "attributes": {
        "name": "John",
        "words": [
          "foo",
          "bar",
          "green-grass"
        ]
      },
      "id": "1",
      "links": {
        "self": "/persons/1"
      },
      "type": "person"
    },
    {
      "attributes": {
        "name": "Sam",
        "words": [
          "spam",
          "eggs",
          "green-apple"
        ]
      },
      "id": "2",
      "links": {
        "self": "/persons/2"
      },
      "type": "person"
    }
  ],
  "jsonapi": {
    "version": "1.0"
  },
  "links": {
    "self": "http://localhost:5000/persons"
  },
  "meta": {
    "count": 2
  }
}

Filter by word

[
  {
    "name": "words",
    "op": "in",
    "val": "spam"
  }
]

Request:

GET /persons?filter=%5B%7B%22name%22%3A%22words%22%2C%22op%22%3A%22in%22%2C%22val%22%3A%22spam%22%7D%5D HTTP/1.1
Content-Type: application/vnd.api+json

Response:

HTTP/1.1 200 OK
Content-Type: application/vnd.api+json

{
  "data": [
    {
      "attributes": {
        "name": "Sam",
        "words": [
          "spam",
          "eggs",
          "green-apple"
        ]
      },
      "id": "2",
      "links": {
        "self": "/persons/2"
      },
      "type": "person"
    }
  ],
  "jsonapi": {
    "version": "1.0"
  },
  "links": {
    "self": "http://localhost:5000/persons?filter=%5B%7B%22name%22%3A%22words%22%2C%22op%22%3A%22in%22%2C%22val%22%3A%22spam%22%7D%5D"
  },
  "meta": {
    "count": 1
  }
}

Filter by words

[
  {
    "name": "words",
    "op": "in",
    "val": ["bar", "eggs"]
  }
]

Request:

GET /persons?filter=%5B%7B%22name%22%3A%22words%22%2C%22op%22%3A%22in%22%2C%22val%22%3A%5B%22bar%22%2C%22eggs%22%5D%7D%5D HTTP/1.1
Content-Type: application/vnd.api+json

Response:

HTTP/1.1 200 OK
Content-Type: application/vnd.api+json

{
  "data": [
    {
      "attributes": {
        "name": "John",
        "words": [
          "foo",
          "bar",
          "green-grass"
        ]
      },
      "id": "1",
      "links": {
        "self": "/persons/1"
      },
      "type": "person"
    },
    {
      "attributes": {
        "name": "Sam",
        "words": [
          "spam",
          "eggs",
          "green-apple"
        ]
      },
      "id": "2",
      "links": {
        "self": "/persons/2"
      },
      "type": "person"
    }
  ],
  "jsonapi": {
    "version": "1.0"
  },
  "links": {
    "self": "http://localhost:5000/persons?filter=%5B%7B%22name%22%3A%22words%22%2C%22op%22%3A%22in%22%2C%22val%22%3A%5B%22bar%22%2C%22eggs%22%5D%7D%5D"
  },
  "meta": {
    "count": 2
  }
}

Filter by any word containing value

[
  {
    "name": "words",
    "op": "ilike_in_str_array",
    "val": "green"
  }
]

Request:

GET /persons?filter=%5B%7B%22name%22%3A%22words%22%2C%22op%22%3A%22ilike_in_str_array%22%2C%22val%22%3A%22green%22%7D%5D HTTP/1.1
Content-Type: application/vnd.api+json

Response:

HTTP/1.1 200 OK
Content-Type: application/vnd.api+json

{
  "data": [
    {
      "attributes": {
        "name": "John",
        "words": [
          "foo",
          "bar",
          "green-grass"
        ]
      },
      "id": "1",
      "links": {
        "self": "/persons/1"
      },
      "type": "person"
    },
    {
      "attributes": {
        "name": "Sam",
        "words": [
          "spam",
          "eggs",
          "green-apple"
        ]
      },
      "id": "2",
      "links": {
        "self": "/persons/2"
      },
      "type": "person"
    }
  ],
  "jsonapi": {
    "version": "1.0"
  },
  "links": {
    "self": "http://localhost:5000/persons?filter=%5B%7B%22name%22%3A%22words%22%2C%22op%22%3A%22ilike_in_str_array%22%2C%22val%22%3A%22green%22%7D%5D"
  },
  "meta": {
    "count": 2
  }
}