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

@@ -183,12 +183,17 @@ func splitParameters(v any, consumes []string) (params []any, body map[string]an
if !ok {
return nil, nil
}
var form map[string]any
for _, item := range list {
p, ok := item.(map[string]any)
if !ok {
params = append(params, item)
continue
}
if in, _ := p["in"].(string); in == "formData" {
form = addFormField(form, p)
continue
}
if in, _ := p["in"].(string); in == "body" {
content := map[string]any{}
for _, ct := range consumes {
@@ -209,9 +214,61 @@ func splitParameters(v any, consumes []string) (params []any, body map[string]an
}
params = append(params, convertParameter(p))
}
if form != nil && body == nil {
body = formBody(form, consumes)
}
return params, body
}
// addFormField folds one Swagger 2.0 formData parameter into an object
// schema: a file becomes a binary string, any other type keeps its schema.
func addFormField(form map[string]any, p map[string]any) map[string]any {
if form == nil {
form = map[string]any{"type": "object", "properties": map[string]any{}}
}
name, _ := p["name"].(string)
prop := map[string]any{}
for k, v := range p {
if _, ok := paramSchemaKeys[k]; ok {
prop[k] = v
}
}
if prop["type"] == "file" {
prop["type"] = "string"
prop["format"] = "binary"
}
if desc, ok := p["description"]; ok {
prop["description"] = desc
}
form["properties"].(map[string]any)[name] = prop
if req, _ := p["required"].(bool); req {
required, _ := form["required"].([]any)
form["required"] = append(required, name)
}
return form
}
// formBody is the OpenAPI 3 requestBody of an operation's formData
// parameters, under multipart/form-data unless the operation declares only
// application/x-www-form-urlencoded.
func formBody(form map[string]any, consumes []string) map[string]any {
ct := "multipart/form-data"
for _, c := range consumes {
if c == "application/x-www-form-urlencoded" {
ct = c
}
if c == "multipart/form-data" {
ct = c
break
}
}
body := map[string]any{"content": map[string]any{ct: map[string]any{"schema": form}}}
if req, ok := form["required"].([]any); ok && len(req) > 0 {
body["required"] = true
}
return body
}
var paramSchemaKeys = map[string]struct{}{
"type": {}, "format": {}, "items": {}, "enum": {}, "default": {},
"minimum": {}, "maximum": {}, "minLength": {}, "maxLength": {},