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

@@ -82,7 +82,7 @@ export interface paths {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["cabana.SuccessEnvelope"];
"application/json": components["schemas"]["cabana.Envelope-cabana_AdminLogoutData"];
};
};
/** @description Unauthorized */
@@ -94,6 +94,24 @@ export interface paths {
"application/json": components["schemas"]["cabana.ErrorEnvelope"];
};
};
/** @description Forbidden */
403: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["cabana.ErrorEnvelope"];
};
};
/** @description Not Found */
404: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["cabana.ErrorEnvelope"];
};
};
};
};
delete?: never;
@@ -137,6 +155,24 @@ export interface paths {
"application/json": components["schemas"]["cabana.ErrorEnvelope"];
};
};
/** @description Forbidden */
403: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["cabana.ErrorEnvelope"];
};
};
/** @description Not Found */
404: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["cabana.ErrorEnvelope"];
};
};
};
};
put?: never;
@@ -287,6 +323,15 @@ export interface paths {
"application/json": components["schemas"]["cabana.ErrorEnvelope"];
};
};
/** @description Not Found */
404: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["cabana.ErrorEnvelope"];
};
};
};
};
put?: never;
@@ -320,7 +365,7 @@ export interface paths {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["cabana.SuccessEnvelope"];
"application/json": components["schemas"]["cabana.Envelope-array_cabana_SettingsEntry"];
};
};
/** @description Unauthorized */
@@ -341,6 +386,15 @@ export interface paths {
"application/json": components["schemas"]["cabana.ErrorEnvelope"];
};
};
/** @description Not Found */
404: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["cabana.ErrorEnvelope"];
};
};
};
};
put?: never;
@@ -377,7 +431,7 @@ export interface paths {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["cabana.SuccessEnvelope"];
"application/json": components["schemas"]["cabana.Envelope-cabana_SettingsResult"];
};
};
/** @description Unauthorized */
@@ -428,7 +482,7 @@ export interface paths {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["cabana.SuccessEnvelope"];
"application/json": components["schemas"]["cabana.Envelope-cabana_SettingsResult"];
};
};
/** @description Unauthorized */
@@ -449,6 +503,15 @@ export interface paths {
"application/json": components["schemas"]["cabana.ErrorEnvelope"];
};
};
/** @description Not Found */
404: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["cabana.ErrorEnvelope"];
};
};
/** @description Unprocessable Entity */
422: {
headers: {
@@ -493,7 +556,7 @@ export interface paths {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["cabana.SuccessEnvelope"];
"application/json": components["schemas"]["cabana.Envelope-cabana_FormView"];
};
};
/** @description Unauthorized */
@@ -595,6 +658,15 @@ export interface paths {
"application/json": components["schemas"]["cabana.ErrorEnvelope"];
};
};
/** @description Not Found */
404: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["cabana.ErrorEnvelope"];
};
};
/** @description Unprocessable Entity */
422: {
headers: {
@@ -654,6 +726,15 @@ export interface paths {
"application/json": components["schemas"]["cabana.ErrorEnvelope"];
};
};
/** @description Not Found */
404: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["cabana.ErrorEnvelope"];
};
};
/** @description Unprocessable Entity */
422: {
headers: {
@@ -703,7 +784,7 @@ export interface paths {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["cabana.SuccessEnvelope"];
"application/json": components["schemas"]["cabana.Envelope-cabana_BulkResult"];
};
};
/** @description Unauthorized */
@@ -724,6 +805,24 @@ export interface paths {
"application/json": components["schemas"]["cabana.ErrorEnvelope"];
};
};
/** @description Not Found */
404: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["cabana.ErrorEnvelope"];
};
};
/** @description Conflict */
409: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["cabana.ErrorEnvelope"];
};
};
/** @description Unprocessable Entity */
422: {
headers: {
@@ -832,6 +931,81 @@ export interface paths {
patch?: never;
trace?: never;
};
"/{vendor}/{plugin}/{controller}/filters/{scope}/options": {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
/**
* 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.
*/
get: {
parameters: {
query?: never;
header?: never;
path: {
/** @description Vendor */
vendor: string;
/** @description Plugin */
plugin: string;
/** @description Controller */
controller: string;
/** @description Filter name */
scope: string;
};
cookie?: never;
};
requestBody?: never;
responses: {
/** @description OK */
200: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["cabana.Envelope-array_cabana_FilterOption"];
};
};
/** @description Unauthorized */
401: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["cabana.ErrorEnvelope"];
};
};
/** @description Forbidden */
403: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["cabana.ErrorEnvelope"];
};
};
/** @description Not Found */
404: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["cabana.ErrorEnvelope"];
};
};
};
};
put?: never;
post?: never;
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
"/{vendor}/{plugin}/{controller}/schema/form": {
parameters: {
query?: never;
@@ -1200,7 +1374,7 @@ export interface paths {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["cabana.SuccessEnvelope"];
"application/json": components["schemas"]["cabana.Envelope-cabana_BulkResult"];
};
};
/** @description Unauthorized */
@@ -1230,6 +1404,15 @@ export interface paths {
"application/json": components["schemas"]["cabana.ErrorEnvelope"];
};
};
/** @description Unprocessable Entity */
422: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["cabana.ErrorEnvelope"];
};
};
};
};
options?: never;
@@ -1271,7 +1454,7 @@ export interface paths {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["cabana.SuccessEnvelope"];
"application/json": components["schemas"]["cabana.ListEnvelope-array_cabana_AdminRecord"];
};
};
/** @description Unauthorized */
@@ -1292,6 +1475,24 @@ export interface paths {
"application/json": components["schemas"]["cabana.ErrorEnvelope"];
};
};
/** @description Not Found */
404: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["cabana.ErrorEnvelope"];
};
};
/** @description Unprocessable Entity */
422: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["cabana.ErrorEnvelope"];
};
};
};
};
put?: never;
@@ -1336,7 +1537,7 @@ export interface paths {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["cabana.SuccessEnvelope"];
"application/json": components["schemas"]["cabana.ListEnvelope-array_cabana_AdminRecord"];
};
};
/** @description Unauthorized */
@@ -1357,6 +1558,24 @@ export interface paths {
"application/json": components["schemas"]["cabana.ErrorEnvelope"];
};
};
/** @description Not Found */
404: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["cabana.ErrorEnvelope"];
};
};
/** @description Unprocessable Entity */
422: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["cabana.ErrorEnvelope"];
};
};
};
};
put?: never;
@@ -1403,7 +1622,7 @@ export interface paths {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["cabana.SuccessEnvelope"];
"application/json": components["schemas"]["cabana.Envelope-cabana_RelationMutationResult"];
};
};
/** @description Unauthorized */
@@ -1424,6 +1643,15 @@ export interface paths {
"application/json": components["schemas"]["cabana.ErrorEnvelope"];
};
};
/** @description Not Found */
404: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["cabana.ErrorEnvelope"];
};
};
/** @description Unprocessable Entity */
422: {
headers: {
@@ -1477,7 +1705,7 @@ export interface paths {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["cabana.SuccessEnvelope"];
"application/json": components["schemas"]["cabana.Envelope-cabana_RelationMutationResult"];
};
};
/** @description Unauthorized */
@@ -1498,6 +1726,15 @@ export interface paths {
"application/json": components["schemas"]["cabana.ErrorEnvelope"];
};
};
/** @description Not Found */
404: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["cabana.ErrorEnvelope"];
};
};
/** @description Unprocessable Entity */
422: {
headers: {
@@ -1529,6 +1766,9 @@ export interface components {
login?: string;
password: string;
};
"cabana.AdminLogoutData": {
status: string;
};
"cabana.AdminProfile": {
email: string;
first_name: string;
@@ -1550,18 +1790,37 @@ export interface components {
label?: string;
name: string;
};
"cabana.BulkResult": {
deleted: number;
};
"cabana.Envelope-array_cabana_FilterOption": {
data: components["schemas"]["cabana.FilterOption"][];
meta: components["schemas"]["cabana.SuccessMeta"];
};
"cabana.Envelope-array_cabana_NavigationEntry": {
data: components["schemas"]["cabana.NavigationEntry"][];
meta: components["schemas"]["cabana.SuccessMeta"];
};
"cabana.Envelope-array_cabana_SettingsEntry": {
data: components["schemas"]["cabana.SettingsEntry"][];
meta: components["schemas"]["cabana.SuccessMeta"];
};
"cabana.Envelope-cabana_AdminLoginData": {
data: components["schemas"]["cabana.AdminLoginData"];
meta: components["schemas"]["cabana.SuccessMeta"];
};
"cabana.Envelope-cabana_AdminLogoutData": {
data: components["schemas"]["cabana.AdminLogoutData"];
meta: components["schemas"]["cabana.SuccessMeta"];
};
"cabana.Envelope-cabana_AdminProfile": {
data: components["schemas"]["cabana.AdminProfile"];
meta: components["schemas"]["cabana.SuccessMeta"];
};
"cabana.Envelope-cabana_BulkResult": {
data: components["schemas"]["cabana.BulkResult"];
meta: components["schemas"]["cabana.SuccessMeta"];
};
"cabana.Envelope-cabana_FormView": {
data: components["schemas"]["cabana.FormView"];
meta: components["schemas"]["cabana.SuccessMeta"];
@@ -1574,10 +1833,18 @@ export interface components {
data: components["schemas"]["cabana.ListSchema"];
meta: components["schemas"]["cabana.SuccessMeta"];
};
"cabana.Envelope-cabana_RelationMutationResult": {
data: components["schemas"]["cabana.RelationMutationResult"];
meta: components["schemas"]["cabana.SuccessMeta"];
};
"cabana.Envelope-cabana_RelationSchema": {
data: components["schemas"]["cabana.RelationSchema"];
meta: components["schemas"]["cabana.SuccessMeta"];
};
"cabana.Envelope-cabana_SettingsResult": {
data: components["schemas"]["cabana.SettingsResult"];
meta: components["schemas"]["cabana.SuccessMeta"];
};
"cabana.ErrorBody": {
code: string;
details: {
@@ -1588,6 +1855,10 @@ export interface components {
"cabana.ErrorEnvelope": {
error: components["schemas"]["cabana.ErrorBody"];
};
"cabana.FilterOption": {
label: string;
value: string;
};
"cabana.FormField": {
attributes?: {
[key: string]: components["schemas"]["cabana.jsonScalar"];
@@ -1766,6 +2037,10 @@ export interface components {
unlinkSelected: components["schemas"]["cabana.MessageForms"];
unlinked: components["schemas"]["cabana.MessageForms"];
};
"cabana.RelationMutationResult": {
linked?: number;
removed?: number;
};
"cabana.RelationOption": {
label: string;
value: number;
@@ -1791,9 +2066,21 @@ export interface components {
name: string;
url?: string;
};
"cabana.SuccessEnvelope": {
data: unknown;
meta: components["schemas"]["cabana.SuccessMeta"];
"cabana.SettingsEntry": {
category: string;
code: string;
description: string;
icon: string;
keywords: string[];
label: string;
model: string;
order: number;
};
"cabana.SettingsResult": {
data: {
[key: string]: unknown;
};
exists: boolean;
};
"cabana.SuccessMeta": {
last_page?: number;

View File

@@ -6,11 +6,33 @@ type Schemas = components['schemas']
export type AdminLoginData = Schemas['cabana.AdminLoginData']
export type AdminLoginRequest = Schemas['cabana.AdminLoginRequest']
export type AdminLogoutData = Schemas['cabana.AdminLogoutData']
export type AdminProfile = Schemas['cabana.AdminProfile']
export type NavigationEntry = Schemas['cabana.NavigationEntry']
export type SettingsEntry = Schemas['cabana.SettingsEntry']
export type SettingsResult = Schemas['cabana.SettingsResult']
export type ListSchema = Schemas['cabana.ListSchema']
export type ListColumn = Schemas['cabana.ListColumn']
export type ListFilter = Schemas['cabana.ListFilter']
export type ListMeta = Schemas['cabana.ListMeta']
export type ListMessages = Schemas['cabana.ListMessages']
export type FormView = Schemas['cabana.FormView']
export type FormField = Schemas['cabana.FormField']
export type FormOption = Schemas['cabana.FormOption']
export type FormMessages = Schemas['cabana.FormMessages']
export type FormRedirects = Schemas['cabana.FormRedirects']
export type RelationSchema = Schemas['cabana.RelationSchema']
export type RelationMessages = Schemas['cabana.RelationMessages']
export type RelationMutationResult = Schemas['cabana.RelationMutationResult']
export type RelationOption = Schemas['cabana.RelationOption']
export type FilterOption = Schemas['cabana.FilterOption']
export type RecordMeta = Schemas['cabana.RecordMeta']
export type RecordEnvelope = Schemas['cabana.RecordEnvelope']
export type BulkResult = Schemas['cabana.BulkResult']
/** One message as CLDR plural forms; "other" is always present (D-13). */
export type MessageForms = Schemas['cabana.MessageForms']
/** The public backend::lang bundle: full key to CLDR forms (D-20). */
export type LangBundle = Schemas['cabana.LangBundle']
export type ErrorEnvelope = Schemas['cabana.ErrorEnvelope']
/** One record: a string-keyed map read through its list or form schema. */
export type AdminRecord = Schemas['cabana.ListEnvelope-array_cabana_AdminRecord']['data'][number]
export type AdminRecord = Schemas['cabana.AdminRecord']