thomaxxl / safrs

SqlAlchemy Flask-Restful Swagger Json:API OpenAPI
GNU General Public License v3.0
405 stars 73 forks source link

Read-only Columns #113

Closed JM0804 closed 6 days ago

JM0804 commented 2 years ago

I was wondering if it's possible to create columns which are read-only when exposed via the API.

For example, I have a data model with a last_completed column which I want to restrict to being set or updated only via a call to a custom RPC call named complete, exposed with @jsonapi_rpc. It also has some computed values such as due_date which are exposed with @jsonapi_attr:

import datetime
from dateutil.relativedelta import relativedelta
from safrs import SAFRSBase, jsonapi_attr, jsonapi_rpc
from typing import Optional

from app import db

class Task(SAFRSBase, db.Model):
    __tablename__ = 'tasks'
    id = db.Column(db.Integer, primary_key=True)
    last_completed = db.Column(db.Date, nullable=True)
    name = db.Column(db.String)

    @jsonapi_rpc(http_methods=["POST"])
    def complete(self):
        self.last_completed = datetime.datetime.now().date()

    @jsonapi_attr
    def due_date(self) -> Optional[datetime.datetime]:
        if self.last_completed:
            return self.last_completed + relativedelta(
                **{self.frequency_unit.value: self.frequency_value}
            )
        else:
            return None

To clarify, I would like last_completed and due_date to be available via GET but not PATCH or POST.

Attempting to set such a restriction with last_completed.http_methods = ['GET'] as suggested for relationships here doesn't work.

Perhaps there is something similar to the hidden columns feature that I am not seeing?

Many thanks :)

thomaxxl commented 2 years ago

Hi,

due_date in your example should not be available via PATCH or POST. It does show up in the swagger but it should be omitted from serialization when these requests are processe. The OAS/swagger should take this into account (i.e. not show the attribute unless a setter is defined), so I'll fix that. Anyway, it is possible to update/override the swagger manually using the custom_swagger argument to SAFRSAPI, example. So you can use this approach to remove the attributes from the OAS spec.

Indeed, you can also use hidden columns in combination with jsonapi_attr to create read-only attributes because hidden columns are also not (de)serialized. Note, column names prefixed with an underscore are automatically hidden.

JM0804 commented 2 years ago

Hi @thomaxxl,

Thanks so much for the speedy reply. Apologies for not getting back to you sooner.

I have made the following modifications and can confirm that the properties in question are silently rejected (i.e. ignored) when making a POST or PATCH request (there is a slight change also from the previous code where I am computing due_date only once rather than on every request, and have made this and the last_completed properties read-only):

import datetime
from dateutil.relativedelta import relativedelta
from safrs import SAFRSBase, jsonapi_attr, jsonapi_rpc
from typing import Optional

from app import db

class Task(SAFRSBase, db.Model):
    __tablename__ = 'tasks'
    id = db.Column(db.Integer, primary_key=True)
    name = db.Column(db.String)
    _due_date = db.Column(db.Date, nullable=True)
    _last_completed = db.Column(db.Date, nullable=True)

    @jsonapi_attr
    def due_date(self):
        return self._due_date

    @jsonapi_attr
    def last_completed(self):
        return self._last_completed

    @jsonapi_rpc(http_methods=["POST"])
    def complete(self):
        self._last_completed = datetime.datetime.now().date()
        self._due_date = self._last_completed + relativedelta(
            **{self.frequency_unit.value: self.frequency_value}
        )

I'm happy to consider this issue closed if you are :)