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:
@@ -48,6 +48,11 @@ type service struct {
|
||||
// insecureCookie drops Secure from the admin cookie (backend.cookie_secure
|
||||
// false, development only); the zero value keeps the cookie Secure.
|
||||
insecureCookie bool
|
||||
// uploadBytes and defaultBytes are http.body_limits.upload_bytes and
|
||||
// default_bytes (0 when not configured): the file routes cap their own
|
||||
// bodies, since surf applies no body limit to raw routes.
|
||||
uploadBytes int64
|
||||
defaultBytes int64
|
||||
}
|
||||
|
||||
// adminPrefix returns the mount path; a zero service uses the default.
|
||||
@@ -92,6 +97,17 @@ func Activate(app *backpack.App, plugins []party.Plugin) (*Routes, error) {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
uploadBytes, err := configBytes(app, "http.body_limits.upload_bytes")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defaultBytes, err := configBytes(app, "http.body_limits.default_bytes")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := checkFileLimits(reg, uploadBytes); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := compileContributions(reg, plugins); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -147,6 +163,8 @@ func Activate(app *backpack.App, plugins []party.Plugin) (*Routes, error) {
|
||||
prefix: prefix,
|
||||
|
||||
insecureCookie: !secureCookie,
|
||||
uploadBytes: uploadBytes,
|
||||
defaultBytes: defaultBytes,
|
||||
}
|
||||
spa, err := boardwalk.Handler(prefix, http.HandlerFunc(writeNotFound))
|
||||
if err != nil {
|
||||
@@ -257,6 +275,10 @@ func (s *service) mount(r pact.Router) {
|
||||
constrainRelation(g)
|
||||
g.Post("/{vendor}/{plugin}/{controller}/{id}/relations/{name}/unlink", requireAjax(s.relationUnlink))
|
||||
constrainRelation(g)
|
||||
// File routes of `type: fileupload` fields (D-09). {id} 0 is the
|
||||
// record being created in the X-Session-Key session.
|
||||
g.Post("/{vendor}/{plugin}/{controller}/{id}/files/{field}", requireAjax(s.fileUpload))
|
||||
constrainFile(g)
|
||||
})
|
||||
// The SPA shell: public, no guard. ServeMux prefers every API pattern
|
||||
// above over the {path...} wildcard.
|
||||
@@ -282,6 +304,11 @@ func constrainRelation(g pact.Router) {
|
||||
g.Where("name", "[A-Za-z_][A-Za-z0-9_]*")
|
||||
}
|
||||
|
||||
func constrainFile(g pact.Router) {
|
||||
constrainController(g)
|
||||
g.Where("field", "[A-Za-z_][A-Za-z0-9_]*")
|
||||
}
|
||||
|
||||
func constrainNested(g pact.Router) {
|
||||
constrainController(g)
|
||||
g.Where("segment", "[A-Za-z_][A-Za-z0-9_]*")
|
||||
@@ -291,6 +318,7 @@ func constrainNested(g pact.Router) {
|
||||
// nestedGet serves the logical routes
|
||||
//
|
||||
// GET /{vendor}/{plugin}/{controller}/{id}/relations/{name}
|
||||
// GET /{vendor}/{plugin}/{controller}/{id}/files/{field}
|
||||
// GET /{vendor}/{plugin}/{controller}/fields/{field}/options
|
||||
// GET /{vendor}/{plugin}/{controller}/filters/{scope}/options
|
||||
//
|
||||
@@ -307,6 +335,9 @@ func (s *service) nestedGet(w http.ResponseWriter, r *http.Request) {
|
||||
s.filterOptions(w, r)
|
||||
case segment == "relations":
|
||||
s.relationLinked(w, r)
|
||||
case segment == "files":
|
||||
r.SetPathValue("field", name)
|
||||
s.fileList(w, r)
|
||||
default:
|
||||
writeNotFound(w, r)
|
||||
}
|
||||
@@ -635,6 +666,11 @@ func (s *service) create(w http.ResponseWriter, r *http.Request) {
|
||||
if !s.operationDeclared(w, r, cc, "create") {
|
||||
return
|
||||
}
|
||||
key, _, err := sessionKeyFrom(r)
|
||||
if err != nil {
|
||||
writeCRUDError(w, err)
|
||||
return
|
||||
}
|
||||
body, err := decodeObject(r)
|
||||
if err != nil {
|
||||
writeCRUDError(w, err)
|
||||
@@ -645,7 +681,7 @@ func (s *service) create(w http.ResponseWriter, r *http.Request) {
|
||||
WriteError(w, http.StatusInternalServerError, "error", msgServerError)
|
||||
return
|
||||
}
|
||||
rec, err := svc.CreateRecord(r.Context(), cc, RecordInput{Body: body})
|
||||
rec, err := svc.CreateRecord(r.Context(), cc, RecordInput{Body: body, SessionKey: key})
|
||||
if err != nil {
|
||||
writeCRUDError(w, err)
|
||||
return
|
||||
@@ -664,6 +700,11 @@ func (s *service) update(w http.ResponseWriter, r *http.Request) {
|
||||
writeCRUDError(w, err)
|
||||
return
|
||||
}
|
||||
key, _, err := sessionKeyFrom(r)
|
||||
if err != nil {
|
||||
writeCRUDError(w, err)
|
||||
return
|
||||
}
|
||||
body, err := decodeObject(r)
|
||||
if err != nil {
|
||||
writeCRUDError(w, err)
|
||||
@@ -674,7 +715,7 @@ func (s *service) update(w http.ResponseWriter, r *http.Request) {
|
||||
WriteError(w, http.StatusInternalServerError, "error", msgServerError)
|
||||
return
|
||||
}
|
||||
rec, err := svc.UpdateRecord(r.Context(), cc, id, RecordInput{Body: body})
|
||||
rec, err := svc.UpdateRecord(r.Context(), cc, id, RecordInput{Body: body, SessionKey: key})
|
||||
if err != nil {
|
||||
writeCRUDError(w, err)
|
||||
return
|
||||
|
||||
Reference in New Issue
Block a user