feat(10-02): relation field options and relation saves with labels
- FieldRelationContract/FieldRelationProvider bind every type: relation
field to a belongsTo foreign key or a belongsToMany pivot; activation
fails naming plugin, controller and field on a missing or broken contract
- GET /{vendor}/{plugin}/{controller}/fields/{field}/options serves
{value, label} pages scoped by pact.RelationExtendOptionsQuery, behind
the controller permission; read-only and non-relation fields are 404
- Saves apply present relation keys after the Before hook: ids are
revalidated through the same scoped query (422 and full rollback
otherwise), belongsTo sets the foreign key, belongsToMany replaces pivot
rows in submitted order with the order column set to the index
- Show, create and update return relation values in data and meta.labels
- A belongsTo on a protected fill key is read-only (D-26)
- One six-segment GET pattern dispatches relation lists and field options,
which ServeMux cannot register side by side
- Admin OpenAPI documents the options route and RecordEnvelope
This commit is contained in:
@@ -69,6 +69,10 @@
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"cabana.AdminRecord": {
|
||||
"additionalProperties": {},
|
||||
"type": "object"
|
||||
},
|
||||
"cabana.AdminRoleSummary": {
|
||||
"properties": {
|
||||
"code": {
|
||||
@@ -273,6 +277,24 @@
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"cabana.ListEnvelope-array_cabana_RelationOption": {
|
||||
"properties": {
|
||||
"data": {
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/cabana.RelationOption"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"meta": {
|
||||
"$ref": "#/components/schemas/cabana.ListMeta"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"data",
|
||||
"meta"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"cabana.ListFilter": {
|
||||
"properties": {
|
||||
"column": {
|
||||
@@ -481,6 +503,53 @@
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"cabana.RecordEnvelope": {
|
||||
"properties": {
|
||||
"data": {
|
||||
"$ref": "#/components/schemas/cabana.AdminRecord"
|
||||
},
|
||||
"meta": {
|
||||
"$ref": "#/components/schemas/cabana.RecordMeta"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"data",
|
||||
"meta"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"cabana.RecordMeta": {
|
||||
"properties": {
|
||||
"labels": {
|
||||
"additionalProperties": {
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/cabana.RelationOption"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"type": "object"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"labels"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"cabana.RelationOption": {
|
||||
"properties": {
|
||||
"label": {
|
||||
"type": "string"
|
||||
},
|
||||
"value": {
|
||||
"type": "integer"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"label",
|
||||
"value"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"cabana.RowAction": {
|
||||
"properties": {
|
||||
"label": {
|
||||
@@ -1144,6 +1213,7 @@
|
||||
]
|
||||
},
|
||||
"post": {
|
||||
"description": "Relation fields are sent by field name with ids ({\"genre\": 3, \"artists\": [4, 9]}); the response carries the same shape plus meta.labels.",
|
||||
"parameters": [
|
||||
{
|
||||
"description": "Vendor",
|
||||
@@ -1174,15 +1244,15 @@
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"201": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/cabana.SuccessEnvelope"
|
||||
"$ref": "#/components/schemas/cabana.RecordEnvelope"
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": "OK"
|
||||
"description": "Created"
|
||||
},
|
||||
"401": {
|
||||
"content": {
|
||||
@@ -1310,6 +1380,134 @@
|
||||
]
|
||||
}
|
||||
},
|
||||
"/{vendor}/{plugin}/{controller}/fields/{field}/options": {
|
||||
"get": {
|
||||
"description": "Choices for a writable `type: relation` field: value is the related id, label its nameFrom column. Read-only and non-relation fields answer 404.",
|
||||
"parameters": [
|
||||
{
|
||||
"description": "Vendor",
|
||||
"in": "path",
|
||||
"name": "vendor",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"description": "Plugin",
|
||||
"in": "path",
|
||||
"name": "plugin",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"description": "Controller",
|
||||
"in": "path",
|
||||
"name": "controller",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"description": "Relation field name",
|
||||
"in": "path",
|
||||
"name": "field",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"description": "Case-insensitive label search",
|
||||
"in": "query",
|
||||
"name": "search",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"description": "Page",
|
||||
"in": "query",
|
||||
"name": "page",
|
||||
"schema": {
|
||||
"type": "integer"
|
||||
}
|
||||
},
|
||||
{
|
||||
"description": "Options per page (1-100, default 20)",
|
||||
"in": "query",
|
||||
"name": "per_page",
|
||||
"schema": {
|
||||
"type": "integer"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/cabana.ListEnvelope-array_cabana_RelationOption"
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": "OK"
|
||||
},
|
||||
"401": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/cabana.ErrorEnvelope"
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": "Unauthorized"
|
||||
},
|
||||
"403": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/cabana.ErrorEnvelope"
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": "Forbidden"
|
||||
},
|
||||
"404": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/cabana.ErrorEnvelope"
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": "Not Found"
|
||||
},
|
||||
"422": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/cabana.ErrorEnvelope"
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": "Unprocessable Entity"
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"BackendBearer": []
|
||||
}
|
||||
],
|
||||
"summary": "Relation field options",
|
||||
"tags": [
|
||||
"admin"
|
||||
]
|
||||
}
|
||||
},
|
||||
"/{vendor}/{plugin}/{controller}/schema/form": {
|
||||
"get": {
|
||||
"parameters": [
|
||||
@@ -1707,7 +1905,7 @@
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/cabana.SuccessEnvelope"
|
||||
"$ref": "#/components/schemas/cabana.RecordEnvelope"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -1798,7 +1996,7 @@
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/cabana.SuccessEnvelope"
|
||||
"$ref": "#/components/schemas/cabana.RecordEnvelope"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -1824,6 +2022,16 @@
|
||||
},
|
||||
"description": "Forbidden"
|
||||
},
|
||||
"404": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/cabana.ErrorEnvelope"
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": "Not Found"
|
||||
},
|
||||
"422": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
|
||||
135
admin/src/api/schema.d.ts
vendored
135
admin/src/api/schema.d.ts
vendored
@@ -568,7 +568,10 @@ export interface paths {
|
||||
};
|
||||
};
|
||||
put?: never;
|
||||
/** Create an admin record */
|
||||
/**
|
||||
* Create an admin record
|
||||
* @description Relation fields are sent by field name with ids ({"genre": 3, "artists": [4, 9]}); the response carries the same shape plus meta.labels.
|
||||
*/
|
||||
post: {
|
||||
parameters: {
|
||||
query?: never;
|
||||
@@ -585,13 +588,13 @@ export interface paths {
|
||||
};
|
||||
requestBody?: never;
|
||||
responses: {
|
||||
/** @description OK */
|
||||
200: {
|
||||
/** @description Created */
|
||||
201: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"application/json": components["schemas"]["cabana.SuccessEnvelope"];
|
||||
"application/json": components["schemas"]["cabana.RecordEnvelope"];
|
||||
};
|
||||
};
|
||||
/** @description Unauthorized */
|
||||
@@ -699,6 +702,97 @@ export interface paths {
|
||||
patch?: never;
|
||||
trace?: never;
|
||||
};
|
||||
"/{vendor}/{plugin}/{controller}/fields/{field}/options": {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
/**
|
||||
* Relation field options
|
||||
* @description Choices for a writable `type: relation` field: value is the related id, label its nameFrom column. Read-only and non-relation fields answer 404.
|
||||
*/
|
||||
get: {
|
||||
parameters: {
|
||||
query?: {
|
||||
/** @description Case-insensitive label search */
|
||||
search?: string;
|
||||
/** @description Page */
|
||||
page?: number;
|
||||
/** @description Options per page (1-100, default 20) */
|
||||
per_page?: number;
|
||||
};
|
||||
header?: never;
|
||||
path: {
|
||||
/** @description Vendor */
|
||||
vendor: string;
|
||||
/** @description Plugin */
|
||||
plugin: string;
|
||||
/** @description Controller */
|
||||
controller: string;
|
||||
/** @description Relation field name */
|
||||
field: string;
|
||||
};
|
||||
cookie?: never;
|
||||
};
|
||||
requestBody?: never;
|
||||
responses: {
|
||||
/** @description OK */
|
||||
200: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"application/json": components["schemas"]["cabana.ListEnvelope-array_cabana_RelationOption"];
|
||||
};
|
||||
};
|
||||
/** @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"];
|
||||
};
|
||||
};
|
||||
/** @description Unprocessable Entity */
|
||||
422: {
|
||||
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;
|
||||
@@ -943,7 +1037,7 @@ export interface paths {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"application/json": components["schemas"]["cabana.SuccessEnvelope"];
|
||||
"application/json": components["schemas"]["cabana.RecordEnvelope"];
|
||||
};
|
||||
};
|
||||
/** @description Unauthorized */
|
||||
@@ -1000,7 +1094,7 @@ export interface paths {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"application/json": components["schemas"]["cabana.SuccessEnvelope"];
|
||||
"application/json": components["schemas"]["cabana.RecordEnvelope"];
|
||||
};
|
||||
};
|
||||
/** @description Unauthorized */
|
||||
@@ -1021,6 +1115,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: {
|
||||
@@ -1396,6 +1499,9 @@ export interface components {
|
||||
login: string;
|
||||
role?: components["schemas"]["cabana.AdminRoleSummary"];
|
||||
};
|
||||
"cabana.AdminRecord": {
|
||||
[key: string]: unknown;
|
||||
};
|
||||
"cabana.AdminRoleSummary": {
|
||||
code: string;
|
||||
id: number;
|
||||
@@ -1453,6 +1559,10 @@ export interface components {
|
||||
}[];
|
||||
meta: components["schemas"]["cabana.ListMeta"];
|
||||
};
|
||||
"cabana.ListEnvelope-array_cabana_RelationOption": {
|
||||
data: components["schemas"]["cabana.RelationOption"][];
|
||||
meta: components["schemas"]["cabana.ListMeta"];
|
||||
};
|
||||
"cabana.ListFilter": {
|
||||
column?: string;
|
||||
falseValue?: components["schemas"]["cabana.jsonScalar"];
|
||||
@@ -1504,6 +1614,19 @@ export interface components {
|
||||
order: number;
|
||||
sideMenu: components["schemas"]["cabana.NavigationEntry"][];
|
||||
};
|
||||
"cabana.RecordEnvelope": {
|
||||
data: components["schemas"]["cabana.AdminRecord"];
|
||||
meta: components["schemas"]["cabana.RecordMeta"];
|
||||
};
|
||||
"cabana.RecordMeta": {
|
||||
labels: {
|
||||
[key: string]: components["schemas"]["cabana.RelationOption"][];
|
||||
};
|
||||
};
|
||||
"cabana.RelationOption": {
|
||||
label: string;
|
||||
value: number;
|
||||
};
|
||||
"cabana.RowAction": {
|
||||
label?: string;
|
||||
name: string;
|
||||
|
||||
Reference in New Issue
Block a user