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:
@@ -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": {},
|
||||
|
||||
@@ -201,3 +201,36 @@ func TestMalformedShapesPassThrough(t *testing.T) {
|
||||
t.Fatalf("foreign ref rewritten: %v", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestConvertFormData(t *testing.T) {
|
||||
op := decode(t, `{
|
||||
"consumes": ["multipart/form-data"],
|
||||
"parameters": [
|
||||
{"name": "id", "in": "path", "required": true, "type": "integer"},
|
||||
{"name": "file_data", "in": "formData", "required": true, "type": "file", "description": "The file"},
|
||||
{"name": "note", "in": "formData", "type": "string"}
|
||||
],
|
||||
"responses": {"201": {"description": "Created"}}
|
||||
}`)
|
||||
out := convertOperation(op, nil, nil)
|
||||
params := out["parameters"].([]any)
|
||||
if len(params) != 1 || params[0].(map[string]any)["name"] != "id" {
|
||||
t.Fatalf("parameters = %#v", params)
|
||||
}
|
||||
body := out["requestBody"].(map[string]any)
|
||||
if body["required"] != true {
|
||||
t.Fatalf("request body = %#v", body)
|
||||
}
|
||||
schema := body["content"].(map[string]any)["multipart/form-data"].(map[string]any)["schema"].(map[string]any)
|
||||
want := map[string]any{
|
||||
"type": "object",
|
||||
"properties": map[string]any{
|
||||
"file_data": map[string]any{"type": "string", "format": "binary", "description": "The file"},
|
||||
"note": map[string]any{"type": "string"},
|
||||
},
|
||||
"required": []any{"file_data"},
|
||||
}
|
||||
if !reflect.DeepEqual(schema, want) {
|
||||
t.Fatalf("form schema = %#v", schema)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user