feat(12.2-02): add the fileupload field with deferred uploads committed on save

- type: fileupload compiles the D-08 keys and binds to the model's attach.Relation at boot
- X-Session-Key (cabana.SessionKeyHeader) carries the form session key; RecordInput.SessionKey
- GET and POST .../{id}/files/{field}: list with pending uploads, multipart upload into attach.Store
- the create and update save attaches the session's pending files in its transaction
- swagger2openapi folds formData parameters into a multipart requestBody
- admin OpenAPI, TS types, conformance cases, README and forms docs
This commit is contained in:
Jakub Zych
2026-10-02 18:04:34 +02:00
parent edda803dc1
commit 044e0450ef
22 changed files with 2183 additions and 21 deletions

View File

@@ -191,6 +191,24 @@
],
"type": "object"
},
"cabana.Envelope-array_cabana_FileItem": {
"properties": {
"data": {
"items": {
"$ref": "#/components/schemas/cabana.FileItem"
},
"type": "array"
},
"meta": {
"$ref": "#/components/schemas/cabana.SuccessMeta"
}
},
"required": [
"data",
"meta"
],
"type": "object"
},
"cabana.Envelope-array_cabana_FilterOption": {
"properties": {
"data": {
@@ -320,6 +338,21 @@
],
"type": "object"
},
"cabana.Envelope-cabana_FileItem": {
"properties": {
"data": {
"$ref": "#/components/schemas/cabana.FileItem"
},
"meta": {
"$ref": "#/components/schemas/cabana.SuccessMeta"
}
},
"required": [
"data",
"meta"
],
"type": "object"
},
"cabana.Envelope-cabana_FormView": {
"properties": {
"data": {
@@ -456,6 +489,55 @@
],
"type": "object"
},
"cabana.FileItem": {
"properties": {
"content_type": {
"type": "string"
},
"created_at": {
"type": "string"
},
"description": {
"type": "string"
},
"file_name": {
"type": "string"
},
"file_size": {
"type": "integer"
},
"id": {
"type": "integer"
},
"pending": {
"type": "boolean"
},
"sort_order": {
"type": "integer"
},
"thumb_url": {
"type": "string"
},
"title": {
"type": "string"
},
"url": {
"type": "string"
}
},
"required": [
"content_type",
"created_at",
"description",
"file_name",
"file_size",
"id",
"pending",
"sort_order",
"title"
],
"type": "object"
},
"cabana.FilterOption": {
"properties": {
"label": {
@@ -499,6 +581,13 @@
"emptyOption": {
"type": "string"
},
"fileTypes": {
"description": "FileTypes are the allowed lower-case extensions of a fileupload field.",
"items": {
"type": "string"
},
"type": "array"
},
"fill": {
"description": "Fill lists the fields of the same form the action writes back (D-07).",
"items": {
@@ -506,9 +595,35 @@
},
"type": "array"
},
"imageHeight": {
"type": "integer"
},
"imageWidth": {
"description": "ImageWidth and ImageHeight are the preview size of an image upload.",
"type": "integer"
},
"label": {
"type": "string"
},
"maxFiles": {
"description": "MaxFiles caps the number of files of an attachMany field.",
"type": "integer"
},
"maxFilesize": {
"description": "MaxFilesize is the largest accepted file in megabytes.",
"type": "number"
},
"mimeTypes": {
"description": "MimeTypes are the allowed MIME patterns (or extensions) of a\nfileupload field.",
"items": {
"type": "string"
},
"type": "array"
},
"mode": {
"description": "Mode is the fileupload mode (image or file, default file).",
"type": "string"
},
"multiple": {
"type": "boolean"
},
@@ -528,6 +643,14 @@
"description": "Path names the controller partial of a `type: partial` field: the\ntemplate {ConfigDir}/_{path}.htm (D-09).",
"type": "string"
},
"prompt": {
"description": "Prompt is the upload button text, localized per request.",
"type": "string"
},
"protected": {
"description": "Protected is true for a fileupload field whose relation is not\npublic: its files are served only through the admin file routes.",
"type": "boolean"
},
"readOnly": {
"type": "boolean"
},
@@ -546,9 +669,21 @@
"tab": {
"type": "string"
},
"thumbOptions": {
"allOf": [
{
"$ref": "#/components/schemas/cabana.ThumbOptions"
}
],
"description": "ThumbOptions carries the preview thumbnail mode."
},
"type": {
"type": "string"
},
"useCaption": {
"description": "UseCaption lets the admin edit each file's title and description.",
"type": "boolean"
},
"widget": {
"description": "Widget is the custom-element tag of a `type: widget` field (D-06).",
"type": "string"
@@ -1384,6 +1519,17 @@
},
"type": "object"
},
"cabana.ThumbOptions": {
"properties": {
"mode": {
"type": "string"
}
},
"required": [
"mode"
],
"type": "object"
},
"cabana.ToolbarAction": {
"properties": {
"label": {
@@ -2178,6 +2324,14 @@
"schema": {
"type": "string"
}
},
{
"description": "Form session key: the save attaches the files uploaded under it",
"in": "header",
"name": "X-Session-Key",
"schema": {
"type": "string"
}
}
],
"requestBody": {
@@ -3414,6 +3568,14 @@
"schema": {
"type": "integer"
}
},
{
"description": "Form session key: the save applies the file uploads and removals held against it",
"in": "header",
"name": "X-Session-Key",
"schema": {
"type": "string"
}
}
],
"requestBody": {
@@ -3490,6 +3652,277 @@
]
}
},
"/{vendor}/{plugin}/{controller}/{id}/files/{field}": {
"get": {
"description": "The files attached to the record minus the session's pending removals, plus the session's pending uploads, in sort_order. id 0 is the record being created in the X-Session-Key session (the key is then required). url and thumb_url are set only for a public relation.",
"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": "Owner id (0 for the record being created)",
"in": "path",
"name": "id",
"required": true,
"schema": {
"type": "integer"
}
},
{
"description": "fileupload field name",
"in": "path",
"name": "field",
"required": true,
"schema": {
"type": "string"
}
},
{
"description": "Form session key (32-128 characters of A-Z a-z 0-9 _ -)",
"in": "header",
"name": "X-Session-Key",
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/cabana.Envelope-array_cabana_FileItem"
}
}
},
"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": "List the files of a fileupload field",
"tags": [
"admin"
]
},
"post": {
"description": "Stores one multipart file_data part and binds it to the X-Session-Key session; the record's next create or update save with the same key attaches it. id 0 is the record being created. A body over the upload cap answers 413 payload_too_large; a file over maxFilesize, of a type the field does not allow, or that fails the image check answers 422 on the field.",
"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": "Owner id (0 for the record being created)",
"in": "path",
"name": "id",
"required": true,
"schema": {
"type": "integer"
}
},
{
"description": "fileupload field name",
"in": "path",
"name": "field",
"required": true,
"schema": {
"type": "string"
}
},
{
"description": "Form session key (32-128 characters of A-Z a-z 0-9 _ -)",
"in": "header",
"name": "X-Session-Key",
"required": true,
"schema": {
"type": "string"
}
}
],
"requestBody": {
"content": {
"multipart/form-data": {
"schema": {
"properties": {
"file_data": {
"description": "The file",
"format": "binary",
"type": "string"
}
},
"required": [
"file_data"
],
"type": "object"
}
}
},
"required": true
},
"responses": {
"201": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/cabana.Envelope-cabana_FileItem"
}
}
},
"description": "Created"
},
"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"
},
"413": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/cabana.ErrorEnvelope"
}
}
},
"description": "Request Entity Too Large"
},
"422": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/cabana.ErrorEnvelope"
}
}
},
"description": "Unprocessable Entity"
}
},
"security": [
{
"BackendBearer": []
}
],
"summary": "Upload a file to a fileupload field",
"tags": [
"admin"
]
}
},
"/{vendor}/{plugin}/{controller}/{id}/relations/{name}": {
"get": {
"parameters": [