CRUD operations
In-depth tour of POST, PATCH and DELETE โ what gets validated, what gets auto-filled, what raises which status.
POST โ create
Pipeline:
- Body parsed (and validated against
create_schemaif set). - Keys are split into regular fields and
custom_*fields. create_fieldswhitelist enforced โ any unknown key returns 403.- Auto-fill from session:
created_by_id,updated_by_id,owner_idare populated fromself.user['id']when those columns exist on the model. - Per-field validation against the model: empty strings on
blank=Falsefields raise 403,Noneonnull=Falsefields raise 403. model.objects.acreate(**to_save)runs.IntegrityErroris caught โ duplicate-entry messages are rewritten to"<value> already exist"(409).custom_*keys are saved throughobj._custom.aset(...).- Many-to-many fields are saved via
save_through_model(obj, name, ids). - Detail is fetched via
get_obj(obj.id)and returned. list:<model>cache namespace is invalidated.
PATCH โ update
Pipeline:
idis parsed from the URL.- Body parsed (and validated against
update_schemaif set). update_fieldswhitelist enforced โ unknown key โ 403.update_fieldsnot defined โ 500 ("Update fields not defined").- Row fetched by id โ and when
owner_fieldis set, also filtered through_ownership_filter()so cross-owner rows return 404. - For each field in body:
m2mfield โgetattr(obj, field).aset(value)custom_field โobj._custom.aset(name, value)- Foreign key โ key rewritten to
<field>_id, dict values reduced toid - Other โ
setattr(obj, key, value)and tracked inself.diff
model.objects.filter(pk=id, **owner).aupdate(**to_update)โ single SQL UPDATE, owner-scoped whenowner_fieldis set.- Detail re-fetched via
get_obj(id)and returned. list:<model>anddetail:<model>:<id>namespaces invalidated.
Diff tracking
Every field write populates self.diff with {old, new} pairs:
self.diff = {
'name': {'old': 'Acme', 'new': 'Acme Inc'},
'active': {'old': True, 'new': False},
}Use it from post_process to log audit trails:
async def post_process(self, response):
if self.diff and self.method == 'patch':
await audit_log(self.user['id'], self.model, self.id, self.diff)
return responseDELETE
Pipeline:
idparsed from the URL โ no id โ 404._ownership_filter()is applied. Withowner_fieldset, rows owned by other users return 404.queryset.filter(pk=id, **owner).adelete().- Failures bubble as 400 with the exception class + message.
- Cache namespaces invalidated.
Response: {"success": true, "id": <id>, "message": "Deleted"}.
Method-not-allowed
When the request method is not in allowed_methods, dispatch raises HTTPException(405, '<METHOD> not allowed') before any handler runs.
Hooks per method
| Hook | Runs on |
|---|---|
pre_process | Every method |
hydrate(body) | POST + PATCH (after JSON parse, before write) |
dehydrate(row) | Every row in the response |
alter_list / alter_detail | After handler, before serialize |
post_process(response) | Every method, last step |
๐
PUT is not implemented. 0-mcp treats PATCH as the partial update verb โ use it for both partial and full updates. To replace a row entirely, send all fields in a single PATCH.
0-mcp by Stamatios Stamou Jr โ github.com/ssjunior/0-mcp