Skip to content

Details

There is no dedicated single-record route — a resource is read the same way it is listed, through Search, filtered down to one id. A record that does not exist, is soft-deleted, belongs to another tenant, or that the view preset says you may not see — someone else’s in a view: own resource — comes back the same way: an empty data array. There is no 404 or 403 to tell those cases apart, on purpose: the scope clause that excludes rows you cannot see and the where clause that excludes rows that do not exist are the same kind of filter, applied before the query ever runs, not a lookup that then checks a permission.

Every generated resource also exposes its own shape at GET /blog-posts/describe: the fields it has, everything a search request may name — sorts, filters, selects, includes, aggregates, whether it is paginated and, when it is, the accepted limits — and the validation rules create and update enforce, as JSON Schema reflected straight off the same Zod schemas the server validates against, not a hand-maintained copy of them.

The search half of that is not a copy either: the controller declares one BLOG_POST_CONTRACT object, parseSearchRequest validates against it and describe publishes it. So the endpoint cannot advertise a filter it rejects, or accept one it never mentioned — which it did, for id.

POST /blog-posts/search
Authorization: Bearer <token>
Content-Type: application/json

{
  "filters": [
    {
      "field": "id",
      "value": "cxmpl00000000000000000029"
    }
  ]
}