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

@@ -695,7 +695,10 @@ export interface paths {
post: {
parameters: {
query?: never;
header?: never;
header?: {
/** @description Form session key: the save attaches the files uploaded under it */
"X-Session-Key"?: string;
};
path: {
/** @description Vendor */
vendor: string;
@@ -1561,7 +1564,10 @@ export interface paths {
put: {
parameters: {
query?: never;
header?: never;
header?: {
/** @description Form session key: the save applies the file uploads and removals held against it */
"X-Session-Key"?: string;
};
path: {
/** @description Vendor */
vendor: string;
@@ -1700,6 +1706,187 @@ export interface paths {
patch?: never;
trace?: never;
};
"/{vendor}/{plugin}/{controller}/{id}/files/{field}": {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
/**
* List the files of a fileupload field
* @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.
*/
get: {
parameters: {
query?: never;
header?: {
/** @description Form session key (32-128 characters of A-Z a-z 0-9 _ -) */
"X-Session-Key"?: string;
};
path: {
/** @description Vendor */
vendor: string;
/** @description Plugin */
plugin: string;
/** @description Controller */
controller: string;
/** @description Owner id (0 for the record being created) */
id: number;
/** @description fileupload field name */
field: string;
};
cookie?: never;
};
requestBody?: never;
responses: {
/** @description OK */
200: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["cabana.Envelope-array_cabana_FileItem"];
};
};
/** @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;
/**
* Upload a file to a fileupload field
* @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.
*/
post: {
parameters: {
query?: never;
header: {
/** @description Form session key (32-128 characters of A-Z a-z 0-9 _ -) */
"X-Session-Key": string;
};
path: {
/** @description Vendor */
vendor: string;
/** @description Plugin */
plugin: string;
/** @description Controller */
controller: string;
/** @description Owner id (0 for the record being created) */
id: number;
/** @description fileupload field name */
field: string;
};
cookie?: never;
};
requestBody: {
content: {
"multipart/form-data": {
/**
* Format: binary
* @description The file
*/
file_data: string;
};
};
};
responses: {
/** @description Created */
201: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["cabana.Envelope-cabana_FileItem"];
};
};
/** @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 Request Entity Too Large */
413: {
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"];
};
};
};
};
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
"/{vendor}/{plugin}/{controller}/{id}/relations/{name}": {
parameters: {
query?: never;
@@ -2124,6 +2311,10 @@ export interface components {
scripts: string[];
styles: string[];
};
"cabana.Envelope-array_cabana_FileItem": {
data: components["schemas"]["cabana.FileItem"][];
meta: components["schemas"]["cabana.SuccessMeta"];
};
"cabana.Envelope-array_cabana_FilterOption": {
data: components["schemas"]["cabana.FilterOption"][];
meta: components["schemas"]["cabana.SuccessMeta"];
@@ -2156,6 +2347,10 @@ export interface components {
data: components["schemas"]["cabana.BulkResult"];
meta: components["schemas"]["cabana.SuccessMeta"];
};
"cabana.Envelope-cabana_FileItem": {
data: components["schemas"]["cabana.FileItem"];
meta: components["schemas"]["cabana.SuccessMeta"];
};
"cabana.Envelope-cabana_FormView": {
data: components["schemas"]["cabana.FormView"];
meta: components["schemas"]["cabana.SuccessMeta"];
@@ -2194,6 +2389,19 @@ export interface components {
"cabana.ErrorEnvelope": {
error: components["schemas"]["cabana.ErrorBody"];
};
"cabana.FileItem": {
content_type: string;
created_at: string;
description: string;
file_name: string;
file_size: number;
id: number;
pending: boolean;
sort_order: number;
thumb_url?: string;
title: string;
url?: string;
};
"cabana.FilterOption": {
label: string;
value: string;
@@ -2210,9 +2418,25 @@ export interface components {
context?: components["schemas"]["cabana.fieldContext"];
default?: components["schemas"]["cabana.jsonScalar"];
emptyOption?: string;
/** @description FileTypes are the allowed lower-case extensions of a fileupload field. */
fileTypes?: string[];
/** @description Fill lists the fields of the same form the action writes back (D-07). */
fill?: string[];
imageHeight?: number;
/** @description ImageWidth and ImageHeight are the preview size of an image upload. */
imageWidth?: number;
label?: string;
/** @description MaxFiles caps the number of files of an attachMany field. */
maxFiles?: number;
/** @description MaxFilesize is the largest accepted file in megabytes. */
maxFilesize?: number;
/**
* @description MimeTypes are the allowed MIME patterns (or extensions) of a
* fileupload field.
*/
mimeTypes?: string[];
/** @description Mode is the fileupload mode (image or file, default file). */
mode?: string;
multiple?: boolean;
name: string;
nameFrom?: string;
@@ -2222,13 +2446,24 @@ export interface components {
* template {ConfigDir}/_{path}.htm (D-09).
*/
path?: string;
/** @description Prompt is the upload button text, localized per request. */
prompt?: string;
/**
* @description Protected is true for a fileupload field whose relation is not
* public: its files are served only through the admin file routes.
*/
protected?: boolean;
readOnly?: boolean;
relation?: string;
required?: boolean;
size?: string;
span?: string;
tab?: string;
/** @description ThumbOptions carries the preview thumbnail mode. */
thumbOptions?: components["schemas"]["cabana.ThumbOptions"];
type: string;
/** @description UseCaption lets the admin edit each file's title and description. */
useCaption?: boolean;
/** @description Widget is the custom-element tag of a `type: widget` field (D-06). */
widget?: string;
};
@@ -2469,6 +2704,9 @@ export interface components {
per_page?: number;
total?: number;
};
"cabana.ThumbOptions": {
mode: string;
};
"cabana.ToolbarAction": {
label: string;
name: string;