Skip to content

Kontrak REST Surface /_ui/ ​

Status: Draft · Kontrak: ../spec/backend/01-core-basic.md §8.1

Surface /_ui/ adalah kontrak publik untuk setiap klien FormSpec — SPA bawaan pun memakainya. Halaman ini menjelaskannya sekali, supaya klien baru tidak menemukannya dengan trial-and-error (gap #47).

Untuk kontrak satu entity, jangan menyalin dari sini — cetak dari sumber yang sama dengan yang mendaftarkan route:

bash
formspec describe entity order --spec examples/kafe/spec

Perintah itu memakai generator route yang sama dengan server (api.UIRoutesForEntity), jadi ia tidak bisa menyebut endpoint yang tidak ada atau melewatkan yang ada.

1. Bentuk path ​

/{workspace}/_ui/entity/{module}/{entity}[/{id}][/{field}|/{action}]
  • {entity} adalah metadata.name (singular) — bukan plural. Plural hanya dipakai di nama permission dan URL api/v1.
  • Semua endpoint entity berada di bawah prefix workspace. Workspace dapat berupa slug (kafe) atau UUID.
MethodPathAksiPermission
GET…/{module}/{entity}list{module}.{plural}.list
GET…/{module}/{entity}/{id}find{module}.{plural}.view
POST…/{module}/{entity}create{module}.{plural}.create
PATCH…/{module}/{entity}/{id}update{module}.{plural}.update
DELETE…/{module}/{entity}/{id}delete{module}.{plural}.delete
POST…/{module}/{entity}/{id}/submit (juga cancel, amend)lifecycle{module}.{plural}.{action}
POST…/{module}/{entity}/{id}/{action}custom action ber-impl{module}.{plural}.{action}
POST/GET…/{module}/{entity}/{id}/{field}upload / unduh field fileupdate / view

Aturan yang sering mengejutkan, dan bukan detail implementasi:

  • Route hanya ada kalau spec-nya memang menghasilkan aksi itu. Aksi disabled: true tidak punya route (dan tidak punya permission terdaftar); entity lifecycle-free tidak punya submit/cancel/amend; entity characteristic: summary hanya list + find.
  • Transisi state machine tanpa impl tidak punya endpoint sendiri. Ia diterapkan lewat update; guard transisinya yang memvalidasi perpindahan. Hanya aksi dengan impl yang punya route {id}/{action}.
  • Route {id}/{field} hanya melayani field bertipe file/attachment; segmen lain di posisi itu menjawab 404, bukan 403 ([#52]).

2. Request body ​

Body bersifat flat — envelope ditolak.

jsonc
// create / update / action
{ "number": "ORD-2026-00001", "channel": "qr_table" }   // ✅
{ "data": { "number": "..." } }                          // ❌ 400 unknown field: "data"

Nilai mengikuti tipe field (../spec/backend/05-field-types.md): money dikirim sebagai {"amount": "25000", "currency": "IDR"} (server menormalisasi angka/string telanjang, §2), decimal sebagai string desimal, relation sebagai id target, child sebagai array objek baris.

update mengirim hanya field yang berubah, dan wajib membawa version untuk optimistic concurrency — mismatch → 409 CONFLICT.

3. Response envelope ​

jsonc
// list
{ "data": [ … ],
  "meta": { "page": 1, "per_page": 20, "total": 137, "total_pages": 7 },
  "links": { "first": "…", "last": "…" } }

// satu record (create / find / update)
{ "data": { … }, "meta": { "request_id": "…", "timestamp": "…" } }

// error
{ "error": { "code": "VALIDATION_ERROR", "message": "…", "details": { … } },
  "meta": { "timestamp": "…" } }

Kode error: VALIDATION_ERROR (422), UNAUTHORIZED (401), FORBIDDEN (403), NOT_FOUND (404), CONFLICT (409), STATE_TRANSITION_ERROR (422), INTERNAL_ERROR (500). Daftar lengkap + kode kanonik FORMSPEC.*: ../spec/backend/error-glossary.yaml.

4. Query untuk list ​

?page=1&per_page=20&sort=-transaction_date&direction=asc&fields=number,status&search=kopi
?filter[status][eq]=paid&filter[total_amount][gte]=50000
  • per_page default 20, maksimum 100 (di atas maksimum di-clamp, bukan ditolak).
  • Operator filter: eq neq gt gte lt lte between in nin like ilike null notnull.
  • sort atas field JSONB di-cast ke tipe field, jadi urutannya numerik/temporal — bukan leksikografis.
  • Parameter row_scope bukan filter. Kalau sebuah entity mendeklarasikan scope from: route, parameter itu adalah konteks request: nilainya dipakai engine untuk membatasi baris, dan nilainya tidak bisa dilebarkan klien (../spec/backend/01-core-basic.md §1.7).

5. Surface approval — …/_ui/workflow/… ​

kind: ApprovalInbox (../kind/ui/ApprovalInbox.md) adalah satu-satunya kind yang sumbernya bukan entity: barisnya hidup di tabel framework formspec_workflow_approval, jadi tidak ada route entity yang bisa mengeksposnya. Karena itu ia punya surface sendiri, di /_ui/ dan bukan /api/v1/ — sama seperti print.

GET  /{workspace}/_ui/workflow/approvals?app={app}
POST /{workspace}/_ui/workflow/approvals/{id}   → {"decision":"approve"|"reject"}

GET membalas { data: [ … ], meta: { … } } (envelope §3) dengan item:

FieldIsi
idid baris approval — dipakai oleh POST
entity, record_idrecord yang sedang diputuskan
workflow, workflow_moduleapproval gate yang mengawal transisi — namanya {entity}.{transition} (field dipertahankan demi kompatibilitas klien)
from, to, active_step, total_stepstransisi yang menunggu dan posisi step
title, descriptionlabel tugas dari steps[].title/description; diisi dari nama transisi bila tak dideklarasikan
display_fields[{ field, label?, type?, value }] — nilai record yang approver butuhkan (label/type diambil dari deklarasi field entity, jadi klien memformatnya seperti tabel)
can_decidepemanggil memegang permission yang menggerbangi transisi itu

Aturan yang bukan detail implementasi:

  • Barisnya sudah tersaring, bukan daftar penuh. Yang dikembalikan hanya tugas yang boleh ditindak pemanggil: App yang dipakai (module-nya), workspace-nya, dan langkah yang role-nya dipegang pemanggil. Pemohon tidak pernah melihat permintaannya sendiri (§ core-extended 7.4.5).
  • can_decide memisahkan dua pertanyaan. Role pada langkah menentukan apa yang terdaftar (list), sedangkan permission yang menggerbangi route transisi menentukan apa yang bisa dijalankan (POST). Tugas dengan can_decide: false tetap terdaftar — itu antrean pemanggil — tetapi POST membalas 403.
  • display_fields hanya terisi bila pemanggil memegang {module}.{plural}.view. Tugasnya tetap terlihat; nilainya tidak. Membaca record demi label tugas tidak boleh menjadi jalan memutar izin baca.
  • Nilai display_fields jatuh ke input pemohon. Transisi yang di-intercept tidak menulis apa pun sampai approval selesai, jadi sebuah field yang diisi pemohon (void_reason) hanya ada di params baris approval. Nilai record tetap menang bila sudah ada.
  • POST mendelegasikan ke mesin approval yang sama dengan halaman record (PATCH/aksi transisi): quorum, larangan menyetujui permintaan sendiri, audit bertanda tangan, dan emit event transisi semuanya berlaku. 403/409 yang sama juga bisa muncul di sini — lihat §4 core-extended.
  • Idempotensi bukan milik endpoint ini. Tugas yang sudah diputuskan hilang dari daftar (status berubah dari pending), jadi keputusan kedua membalas 404 alih-alih “sukses” kedua kali.
  • app opsional bila workspace hanya punya satu App atau sesi sudah terikat pada satu App. Bila lebih dari satu dan tidak ada yang ditunjuk, permintaan ditolak — sama seperti /_meta/ui.
  • Realtime belum berlaku untuk permukaan ini: hub WS mendorong per {module}/{entity}, sedangkan approval bukan entity (realtime: true pada kind belum dihormati — todo 5.13.7 ⏸️). Klien harus me-refresh.

GET /_ui/print/{module}/{name}/{id} (§7) adalah endpoint non-entity lain di surface yang sama; keduanya berada di /_ui/ karena memakai autentikasi sesi, bukan spec.expose.

6. Surface publik ​

App dengan access: public membuka sebagian endpoint untuk anonim, tetapi hanya entity+aksi yang dibaca view publiknya — grant itu diturunkan dari permukaan App, dan sebuah grant boleh membatasi baris-nya lewat scope yang berasal dari param route pada Table block (../spec/frontend/05-app-kinds.md §1.1). Ringkasnya: grant berlaku untuk anonim; pemanggil yang sudah terautentikasi tetap wajib memegang permission entity itu.

7. Di luar cakupan halaman ini ​

Surface eksternal (/{workspace}/api/v1/…) punya kontrak terpisah dan deny-by-default lewat spec.expose (§8.2/§8.4). Print, Report, dan dashboard memakai endpoint-nya sendiri di surface yang sama (§5 mencakup approval); kontrak HTTP per entity dicetak oleh formspec describe, sedangkan kontrak kind ada di ../kind/.

Standar terbuka (CC0) dengan implementasi referensi.