feat(10-02): filter choices and a fully typed admin API proven on the wire

- pact.FilterOptions on the model serves a scope filter's choices; a scope
  filter whose model lacks it fails activation (D-27)
- GET /{vendor}/{plugin}/{controller}/filters/{scope}/options answers a
  declared scope filter behind the controller permission with localized
  {value, label} choices, 404 otherwise
- Every admin route documents a typed success schema, and protected routes
  document 401, 403 and 404 (422 on writes); SuccessEnvelope is gone and
  logout writes a typed AdminLogoutData
- jsonScalar and fieldContext decode their served shapes
- TestPhase10OpenAPIConformance calls every inventoried route through the
  assembled router on PostgreSQL and decodes each body into its documented
  type with unknown fields disallowed, checking admin.json's schema ref
- The SPA aliases every new schema type; Tailwind no longer scans the
  generated API files, so API changes do not churn boardwalk/dist
This commit is contained in:
Jakub Zych
2026-09-27 16:27:42 +02:00
parent c87148a34f
commit 9f296b0484
16 changed files with 1653 additions and 49 deletions

View File

@@ -36,10 +36,9 @@ type SuccessMeta struct {
LastPage int `json:"last_page,omitempty"`
}
// SuccessEnvelope is the D-10 success envelope.
type SuccessEnvelope struct {
Data any `json:"data"`
Meta SuccessMeta `json:"meta"`
// AdminLogoutData is the POST /auth/logout payload.
type AdminLogoutData struct {
Status string `json:"status"`
}
// AdminLoginData is the admin login and refresh payload. Bearer transport
@@ -139,8 +138,10 @@ func AdminLang() {}
// @Tags admin
// @Produce json
// @Security BackendBearer
// @Success 200 {object} SuccessEnvelope
// @Success 200 {object} Envelope[AdminLogoutData]
// @Failure 401 {object} ErrorEnvelope
// @Failure 403 {object} ErrorEnvelope
// @Failure 404 {object} ErrorEnvelope
// @Router /auth/logout [post]
func AdminLogout() {}
@@ -152,6 +153,8 @@ func AdminLogout() {}
// @Security BackendBearer
// @Success 200 {object} Envelope[AdminProfile]
// @Failure 401 {object} ErrorEnvelope
// @Failure 403 {object} ErrorEnvelope
// @Failure 404 {object} ErrorEnvelope
// @Router /auth/me [get]
func AdminMe() {}
@@ -164,6 +167,7 @@ func AdminMe() {}
// @Success 200 {object} Envelope[[]NavigationEntry]
// @Failure 401 {object} ErrorEnvelope
// @Failure 403 {object} ErrorEnvelope
// @Failure 404 {object} ErrorEnvelope
// @Router /navigation [get]
func AdminNavigation() {}
@@ -173,9 +177,10 @@ func AdminNavigation() {}
// @Tags admin
// @Produce json
// @Security BackendBearer
// @Success 200 {object} SuccessEnvelope
// @Success 200 {object} Envelope[[]SettingsEntry]
// @Failure 401 {object} ErrorEnvelope
// @Failure 403 {object} ErrorEnvelope
// @Failure 404 {object} ErrorEnvelope
// @Router /settings [get]
func AdminSettingsList() {}
@@ -186,7 +191,7 @@ func AdminSettingsList() {}
// @Produce json
// @Security BackendBearer
// @Param code path string true "Settings code"
// @Success 200 {object} SuccessEnvelope
// @Success 200 {object} Envelope[FormView]
// @Failure 401 {object} ErrorEnvelope
// @Failure 403 {object} ErrorEnvelope
// @Failure 404 {object} ErrorEnvelope
@@ -200,7 +205,7 @@ func AdminSettingsSchema() {}
// @Produce json
// @Security BackendBearer
// @Param code path string true "Settings code"
// @Success 200 {object} SuccessEnvelope
// @Success 200 {object} Envelope[SettingsResult]
// @Failure 401 {object} ErrorEnvelope
// @Failure 403 {object} ErrorEnvelope
// @Failure 404 {object} ErrorEnvelope
@@ -215,10 +220,11 @@ func AdminSettingsGet() {}
// @Produce json
// @Security BackendBearer
// @Param code path string true "Settings code"
// @Success 200 {object} SuccessEnvelope
// @Success 200 {object} Envelope[SettingsResult]
// @Failure 401 {object} ErrorEnvelope
// @Failure 403 {object} ErrorEnvelope
// @Failure 422 {object} ErrorEnvelope
// @Failure 404 {object} ErrorEnvelope
// @Router /settings/{code} [put]
func AdminSettingsPut() {}
@@ -293,6 +299,24 @@ func AdminRelationSchema() {}
// @Router /{vendor}/{plugin}/{controller}/fields/{field}/options [get]
func AdminFieldOptions() {}
// AdminFilterOptions documents the model-backed filter options route (D-27).
//
// @Summary Filter scope options
// @Description Choices of a declared scope filter of the controller's list, from the model's FilterOptions. {scope} is the filter name used as filter[<scope>]; labels are localized.
// @Tags admin
// @Produce json
// @Security BackendBearer
// @Param vendor path string true "Vendor"
// @Param plugin path string true "Plugin"
// @Param controller path string true "Controller"
// @Param scope path string true "Filter name"
// @Success 200 {object} Envelope[[]FilterOption]
// @Failure 401 {object} ErrorEnvelope
// @Failure 403 {object} ErrorEnvelope
// @Failure 404 {object} ErrorEnvelope
// @Router /{vendor}/{plugin}/{controller}/filters/{scope}/options [get]
func AdminFilterOptions() {}
// AdminList documents the record list route.
//
// @Summary List admin records
@@ -311,6 +335,7 @@ func AdminFieldOptions() {}
// @Failure 401 {object} ErrorEnvelope
// @Failure 403 {object} ErrorEnvelope
// @Failure 422 {object} ErrorEnvelope
// @Failure 404 {object} ErrorEnvelope
// @Router /{vendor}/{plugin}/{controller} [get]
func AdminList() {}
@@ -329,6 +354,7 @@ func AdminList() {}
// @Failure 401 {object} ErrorEnvelope
// @Failure 403 {object} ErrorEnvelope
// @Failure 422 {object} ErrorEnvelope
// @Failure 404 {object} ErrorEnvelope
// @Router /{vendor}/{plugin}/{controller} [post]
func AdminCreate() {}
@@ -342,10 +368,12 @@ func AdminCreate() {}
// @Param vendor path string true "Vendor"
// @Param plugin path string true "Plugin"
// @Param controller path string true "Controller"
// @Success 200 {object} SuccessEnvelope
// @Success 200 {object} Envelope[BulkResult]
// @Failure 401 {object} ErrorEnvelope
// @Failure 403 {object} ErrorEnvelope
// @Failure 422 {object} ErrorEnvelope
// @Failure 404 {object} ErrorEnvelope
// @Failure 409 {object} ErrorEnvelope
// @Router /{vendor}/{plugin}/{controller}/bulk-delete [post]
func AdminBulkDelete() {}
@@ -395,10 +423,11 @@ func AdminUpdate() {}
// @Param plugin path string true "Plugin"
// @Param controller path string true "Controller"
// @Param id path integer true "Record id"
// @Success 200 {object} SuccessEnvelope
// @Success 200 {object} Envelope[BulkResult]
// @Failure 401 {object} ErrorEnvelope
// @Failure 403 {object} ErrorEnvelope
// @Failure 404 {object} ErrorEnvelope
// @Failure 422 {object} ErrorEnvelope
// @Router /{vendor}/{plugin}/{controller}/{id} [delete]
func AdminDelete() {}
@@ -413,9 +442,11 @@ func AdminDelete() {}
// @Param controller path string true "Controller"
// @Param id path integer true "Owner id"
// @Param name path string true "Relation name"
// @Success 200 {object} SuccessEnvelope
// @Success 200 {object} ListEnvelope[[]AdminRecord]
// @Failure 401 {object} ErrorEnvelope
// @Failure 403 {object} ErrorEnvelope
// @Failure 404 {object} ErrorEnvelope
// @Failure 422 {object} ErrorEnvelope
// @Router /{vendor}/{plugin}/{controller}/{id}/relations/{name} [get]
func AdminRelationLinked() {}
@@ -430,9 +461,11 @@ func AdminRelationLinked() {}
// @Param controller path string true "Controller"
// @Param id path integer true "Owner id"
// @Param name path string true "Relation name"
// @Success 200 {object} SuccessEnvelope
// @Success 200 {object} ListEnvelope[[]AdminRecord]
// @Failure 401 {object} ErrorEnvelope
// @Failure 403 {object} ErrorEnvelope
// @Failure 404 {object} ErrorEnvelope
// @Failure 422 {object} ErrorEnvelope
// @Router /{vendor}/{plugin}/{controller}/{id}/relations/{name}/candidates [get]
func AdminRelationCandidates() {}
@@ -448,10 +481,11 @@ func AdminRelationCandidates() {}
// @Param controller path string true "Controller"
// @Param id path integer true "Owner id"
// @Param name path string true "Relation name"
// @Success 200 {object} SuccessEnvelope
// @Success 200 {object} Envelope[RelationMutationResult]
// @Failure 401 {object} ErrorEnvelope
// @Failure 403 {object} ErrorEnvelope
// @Failure 422 {object} ErrorEnvelope
// @Failure 404 {object} ErrorEnvelope
// @Router /{vendor}/{plugin}/{controller}/{id}/relations/{name}/link [post]
func AdminRelationLink() {}
@@ -467,9 +501,10 @@ func AdminRelationLink() {}
// @Param controller path string true "Controller"
// @Param id path integer true "Owner id"
// @Param name path string true "Relation name"
// @Success 200 {object} SuccessEnvelope
// @Success 200 {object} Envelope[RelationMutationResult]
// @Failure 401 {object} ErrorEnvelope
// @Failure 403 {object} ErrorEnvelope
// @Failure 422 {object} ErrorEnvelope
// @Failure 404 {object} ErrorEnvelope
// @Router /{vendor}/{plugin}/{controller}/{id}/relations/{name}/unlink [post]
func AdminRelationUnlink() {}