Skip to content
Payment Emulator LabDocumentationOpen console

Runtime

The list contract

Every listing in the control plane speaks one contract. A client learns it once,and a new endpoint cannot invent its own.

3 min read

Índice / Index · Español

Every listing in the control plane speaks one contract. A client learns it once, and a new endpoint cannot invent its own.

text
GET /applications?page=2&pageSize=25&sort=-createdAt,name&search=ana&filter[state]=captured
Parameter Default Meaning
page 1 1-based
pageSize 25, capped at 200 An unbounded page size is an accidental denial of service on any table with history behind it
sort per endpoint Comma-separated; a leading - is descending
search Free text, case-insensitive, across the fields the endpoint opens
filter[field] Matched exactly or as a substring, as the endpoint declares

Every listing answers the same envelope:

json
{ "data": [  ], "meta": { "page": 2, "pageSize": 25, "total": 314, "lastPage": 13 } }

#Whitelists, not conveniences

Each endpoint declares a ListSpec: which fields may be sorted, which may be filtered and how, which are searched, and the order used when the caller asks for none.

This is not ceremony. An open sort lets anyone order by an unindexed column on a table with history behind it, and an open filter lets them probe fields the endpoint never meant to expose. A field that is not declared is an error, not a silently ignored parameter:

Error Cause
unsortable_field sort named a field the endpoint did not open, and the answer lists the ones it did
unfilterable_field filter[…] named a field the endpoint did not open

#Two properties that are easy to lose

A listing is never unordered. When the caller asks for no order, the endpoint's default applies — and the primary key is always appended last. Ordering by a column with ties leaves rows within a tie in whatever order the database happened to find them, and that order is not stable between two queries, so page 2 of a listing sorted by name can repeat a row from page 1 and skip another entirely. A unique tiebreaker is what makes paging mean anything.

A filter narrows and never widens. The endpoint's own scope — the application a listing belongs to, the rows this caller may see — is intersected with the caller's filters. A filter can never be a way out of a tenant. See identity and scope.

#Listings that are not tables

The built-in scenarios and the provider catalogue live in code, not in the database. They are paginated by the same helper over an array, with the same whitelists enforced identically, because a client should not have to tell those listings apart from the rest.

#The console side

useEntityCollection implements the caller's half once: params, fetching, paging, sorting, filtering, searching, and optional URL sync so a filtered view is a link. core/data/serialization/list-params.serializer.ts maps its shape onto the query above — filters are nested rather than spread at the top level, so a filter called search, page or sort cannot collide with the contract itself.

A column that claims sortable must name a field the endpoint declared sortable, or the API answers unsortable_field and the operator finds out. That the two agree is deliberately not restated in the columns file: a second copy of the rule could disagree with the first.

#Where it lives

  • apps/api/src/core/http/list-query.dto.ts — the query shape and the envelope.
  • apps/api/src/core/http/paginate.ts — the whitelists, the tiebreaker, and the array variant.
  • apps/console/app/core/composables/collection/useEntityCollection.ts — the caller's half.
  • apps/api/test/integration/list-contract.spec.ts — what pins it.
Payment Emulator Lab · RonuSoftwareMIT