SQLAlchemy / jsonpatch - how to make patch paths case-insensitive?

1.1k views Asked by At

I've been trying to find some documentation for jsonpatch==1.16 on how to make PATCH paths case-insensitive. The problem is that:

PATCH /users/123
[
    {"op": "add", "path": "/firstname", "value": "Spammer"}
]

Seems to mandate that the DB (MySQL / MariaDB) column is also exactly firstname and not for example Firstname or FirstName. When I change the path in the JSON to /FirstName, which is what the DB column is, then the patch works just fine. But I'm not sure if you are supposed to use CamelCase in the JSON in this case? It seems a bit non-standard.

How can I make jsonpatch at least case-insensitive? Or alternatively, is there some way to insert some mapping in the middle, for example like this:

def users_mapping(self, path):
    select = {
        "/firstname": "FirstName",
        "/lastname": "last_name",  # Just an example
    }
    return select.get(path, None)

Using Python 3.5, SQLAlchemy 1.1.13 and Flask-SQLAlchemy 2.2

1

There are 1 answers

0
Juha Untinen On

Well, the answer is: yes, you can add mapping. Here's my implementation with some annotations:

The endpoint handler (eg. PATCH /news/123):

def patch(self, news_id):
    """Change an existing News item partially using an instruction-based JSON, 
    as defined by: https://tools.ietf.org/html/rfc6902
    """
    news_item = News.query.get_or_404(news_id)
    self.patch_item(news_item, request.get_json())
    db.session.commit()

    # asdict() comes from dictalchemy method make_class_dictable(news)
    return make_response(jsonify(news_item.asdict()), 200)

The method it calls:

# news = the db.Model for News, from SQLAlchemy
# patchdata = the JSON from the request, like this:
# [{"op": "add", "path": "/title", "value": "Example"}]
def patch_item(self, news, patchdata, **kwargs):
    # Map the values to DB column names
    mapped_patchdata = []
    for p in patchdata:
        # Replace eg. /title with /Title
        p = self.patch_mapping(p)
        mapped_patchdata.append(p)

    # This follows the normal JsonPatch procedure
    data = news.asdict(exclude_pk=True, **kwargs)
    # The only difference is that I pass the mapped version of the list
    patch = JsonPatch(mapped_patchdata)
    data = patch.apply(data)
    news.fromdict(data)

And the mapping implementation:

def patch_mapping(self, patch):
    """This is used to map a patch "path" or "from" to a custom value.
    Useful for when the patch path/from is not the same as the DB column name.

    Eg.
    PATCH /news/123
    [{ "op": "move", "from": "/title", "path": "/author" }]

    If the News column is "Title", having "/title" would fail to patch 
    because the case does not match. So the mapping converts this:
        { "op": "move", "from": "/title", "path": "/author" }
    To this:
        { "op": "move", "from": "/Title", "path": "/Author" }
    """

    # You can define arbitrary column names here.
    # As long as the DB column is identical, the patch will work just fine.
    mapping = {
        "/title": "/Title",
        "/contents": "/Contents",
        "/author": "/Author"
    }

    mutable = deepcopy(patch)
    for prop in patch:
        if prop == "path" or prop == "from":
            mutable[prop] = mapping.get(patch[prop], None)
    return mutable