CRUD operations

In-depth tour of POST, PATCH and DELETE โ€” what gets validated, what gets auto-filled, what raises which status.

POST โ€” create

Pipeline:

  1. Body parsed (and validated against create_schema if set).
  2. Keys are split into regular fields and custom_* fields.
  3. create_fields whitelist enforced โ€” any unknown key returns 403.
  4. Auto-fill from session: created_by_id, updated_by_id, owner_id are populated from self.user['id'] when those columns exist on the model.
  5. Per-field validation against the model: empty strings on blank=False fields raise 403, None on null=False fields raise 403.
  6. model.objects.acreate(**to_save) runs.
  7. IntegrityError is caught โ€” duplicate-entry messages are rewritten to "<value> already exist" (409).
  8. custom_* keys are saved through obj._custom.aset(...).
  9. Many-to-many fields are saved via save_through_model(obj, name, ids).
  10. Detail is fetched via get_obj(obj.id) and returned.
  11. list:<model> cache namespace is invalidated.

PATCH โ€” update

Pipeline:

  1. id is parsed from the URL.
  2. Body parsed (and validated against update_schema if set).
  3. update_fields whitelist enforced โ€” unknown key โ†’ 403.
  4. update_fields not defined โ†’ 500 ("Update fields not defined").
  5. Row fetched by id โ€” and when owner_field is set, also filtered through _ownership_filter() so cross-owner rows return 404.
  6. For each field in body:
  1. model.objects.filter(pk=id, **owner).aupdate(**to_update) โ€” single SQL UPDATE, owner-scoped when owner_field is set.
  2. Detail re-fetched via get_obj(id) and returned.
  3. list:<model> and detail:<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 response

DELETE

Pipeline:

  1. id parsed from the URL โ€” no id โ†’ 404.
  2. _ownership_filter() is applied. With owner_field set, rows owned by other users return 404.
  3. queryset.filter(pk=id, **owner).adelete().
  4. Failures bubble as 400 with the exception class + message.
  5. 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

HookRuns on
pre_processEvery method
hydrate(body)POST + PATCH (after JSON parse, before write)
dehydrate(row)Every row in the response
alter_list / alter_detailAfter 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