I have a User and Role model with a many-to-many relationship
class User(BaseModel, TimestampableMixin):
username = Column(String(MEDIUM_STRING_LENGTH), nullable=False, unique=True)
roles = relationship('Role', secondary='user_roles', back_populates='users')
class Role(BaseModel, TimestampableMixin):
label = Column(String(MEDIUM_STRING_LENGTH), nullable=False, unique=True)
users = relationship('User', secondary='user_roles', back_populates='roles')
class UserRole(BaseModel, TimestampableMixin):
user_id = Column(ForeignKey('users.id', ondelete=CASCADE, onupdate=CASCADE), nullable=False, index=True)
role_id = Column(ForeignKey('roles.id', onupdate=CASCADE), nullable=False, index=True)
I then defined schemas for User to nest Roles.
class RoleSchema(BaseSchema):
class Meta:
model = models.Role
class UserSchema(BaseSchema):
class Meta:
model = models.User
roles = fields.Nested('RoleSchema', many=True, exclude=['users'])
For serialization this is working great where a list of role objects are included in a user GET request. What also works is POSTing a user with new role objects embedded in the request. What I have not been able to figure out is how to POST/PUT a list of existing role ids rather than create new objects. For example, this request works:
{
"username": "testuser12",
"roles": [
{
"label": "newrole"
}
]
}
Response:
{
"createdTime": "2020-02-06T19:13:29Z",
"id": 4,
"modifiedTime": "2020-02-06T19:13:29Z",
"roles": [
{
"createdTime": "2020-02-06T19:13:29Z",
"id": 2,
"label": "newrole",
"modifiedTime": "2020-02-06T19:13:29Z"
}
],
"username": "testuser12"
}
But neither of these requests work:
{
"username": "testuser13",
"roles": [
1
]
}
{
"username": "testuser13",
"roles": [
{
"id": 1
}
]
}
I'm getting this response:
{
"errors": {
"error": [
"Unprocessable Entity"
],
"message": [
"The request was well-formed but was unable to be followed due to semantic errors."
]
}
}
I can tell I'm missing something in the schema to be able to ingest ids rather than objects, and I suspect I need to make use of dump_only/load_only and potentially a separate schema for PUT. But, I haven't been able to find an example anywhere online for this use case.
It may also be helpful to mention that I'm using flask-smorest
for the request validation and schema argument ingestion.
@B_API.route('/user/<user_id>')
class UserByIdResource(MethodView):
@B_API.response(schemas.UserSchema)
def get(self, user_id):
"""
Get a single user by id
"""
return models.User.query.get(user_id)
@B_API.arguments(schemas.UserSchema)
@B_API.response(schemas.UserSchema)
def put(self, updated_user, user_id):
"""
Update fields of an existing user
"""
models.User.query.get_or_404(user_id, description=f'User with id {user_id} not found')
user = updated_user.update_with_db(user_id)
return user
update_with_db looks like:
def update_with_db(self, id: int):
self.id = id
DB.session.merge(self)
DB.session.commit()
return self.query.get(id)
Thanks for any assistance.
I ended up resolving this by creating a separate PUT schema and using Pluck for role ids.
class UserSchema(BaseModelSchema):
class Meta:
model = models.User
roles = fields.Nested('RoleSchema', many=True, exclude=['users'])
class UserPutSchema(BaseModelSchema):
class Meta:
model = models.User
roles = fields.Pluck('RoleSchema', 'id', many=True)
These schemas are implemented with the following resource, taking in PUT schema for input validation, and returning a standard User Schema to comply with the GET and POST responses. This seems to work well, with the only downside being having to define a separate schema.
@B_API.arguments(schemas.UserPutSchema(partial=True))
@B_API.response(schemas.UserSchema)
def put(self, updated_user, user_id):
"""
Update fields of an existing user
"""
get_by_id(models.User, user_id)
user = updated_user.update_with_db(user_id)
return user
I can now make a PUT request like
PUT /user/1
{
"roles": [
1,
2
]
}
and get a response like
{
"createdTime": "2020-02-20T01:33:42Z",
"historys": [],
"id": 1,
"modifiedTime": "2020-02-20T01:33:42Z",
"roles": [
{
"createdTime": "2020-02-19T23:43:06Z",
"description": null,
"historys": [],
"id": 1,
"key": "TEST_ROLE_1",
"label": "role 1",
"modifiedTime": "2020-02-19T23:43:06Z"
},
{
"createdTime": "2020-02-19T23:43:06Z",
"description": null,
"historys": [],
"id": 2,
"key": "TEST_ROLE_2",
"label": "role 2",
"modifiedTime": "2020-02-19T23:43:06Z"
}
],
"username": "testuser1"
}
which is what I was aiming for.