3
0

Support simple jsonl exports for records

This commit is contained in:
Denis Arh 2019-08-14 17:06:29 +02:00
parent c1e3231d00
commit 0707f139c4
6 changed files with 245 additions and 0 deletions

View File

@ -655,6 +655,48 @@
]
}
},
{
"name": "export",
"path": "/export{filename}.{ext}",
"method": "GET",
"title": "Exports records that match ",
"parameters": {
"path": [
{
"type": "string",
"name": "filename",
"required": false,
"title": "Filename to use"
},
{
"type": "string",
"name": "ext",
"required": true,
"title": "Export format"
}
],
"get": [
{
"name": "filter",
"type": "string",
"required": false,
"title": "Filtering condition"
},
{
"name": "sort",
"type": "string",
"required": false,
"title": "Sort field (default id desc)"
},
{
"name": "download",
"type": "bool",
"required": false,
"title": "Send headers to browser to trigger download/save-as"
}
]
}
},
{
"name": "create",
"method": "POST",

View File

@ -91,6 +91,48 @@
]
}
},
{
"Name": "export",
"Method": "GET",
"Title": "Exports records that match ",
"Path": "/export{filename}.{ext}",
"Parameters": {
"get": [
{
"name": "filter",
"required": false,
"title": "Filtering condition",
"type": "string"
},
{
"name": "sort",
"required": false,
"title": "Sort field (default id desc)",
"type": "string"
},
{
"name": "download",
"required": false,
"title": "Send headers to browser to trigger download/save-as",
"type": "bool"
}
],
"path": [
{
"name": "filename",
"required": false,
"title": "Filename to use",
"type": "string"
},
{
"name": "ext",
"required": true,
"title": "Export format",
"type": "string"
}
]
}
},
{
"Name": "create",
"Method": "POST",

View File

@ -31,6 +31,7 @@ import (
type RecordAPI interface {
Report(context.Context, *request.RecordReport) (interface{}, error)
List(context.Context, *request.RecordList) (interface{}, error)
Export(context.Context, *request.RecordExport) (interface{}, error)
Create(context.Context, *request.RecordCreate) (interface{}, error)
Read(context.Context, *request.RecordRead) (interface{}, error)
Update(context.Context, *request.RecordUpdate) (interface{}, error)
@ -42,6 +43,7 @@ type RecordAPI interface {
type Record struct {
Report func(http.ResponseWriter, *http.Request)
List func(http.ResponseWriter, *http.Request)
Export func(http.ResponseWriter, *http.Request)
Create func(http.ResponseWriter, *http.Request)
Read func(http.ResponseWriter, *http.Request)
Update func(http.ResponseWriter, *http.Request)
@ -91,6 +93,26 @@ func NewRecord(h RecordAPI) *Record {
resputil.JSON(w, value)
}
},
Export: func(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close()
params := request.NewRecordExport()
if err := params.Fill(r); err != nil {
logger.LogParamError("Record.Export", r, err)
resputil.JSON(w, err)
return
}
value, err := h.Export(r.Context(), params)
if err != nil {
logger.LogControllerError("Record.Export", r, err, params.Auditable())
resputil.JSON(w, err)
return
}
logger.LogControllerCall("Record.Export", r, params.Auditable())
if !serveHTTP(value, w, r) {
resputil.JSON(w, value)
}
},
Create: func(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close()
params := request.NewRecordCreate()
@ -199,6 +221,7 @@ func (h Record) MountRoutes(r chi.Router, middlewares ...func(http.Handler) http
r.Use(middlewares...)
r.Get("/namespace/{namespaceID}/module/{moduleID}/record/report", h.Report)
r.Get("/namespace/{namespaceID}/module/{moduleID}/record/", h.List)
r.Get("/namespace/{namespaceID}/module/{moduleID}/record/export{filename}.{ext}", h.Export)
r.Post("/namespace/{namespaceID}/module/{moduleID}/record/", h.Create)
r.Get("/namespace/{namespaceID}/module/{moduleID}/record/{recordID}", h.Read)
r.Post("/namespace/{namespaceID}/module/{moduleID}/record/{recordID}", h.Update)

View File

@ -2,6 +2,9 @@ package rest
import (
"context"
"encoding/json"
"fmt"
"net/http"
"github.com/titpetric/factory/resputil"
@ -154,6 +157,45 @@ func (ctrl *Record) Upload(ctx context.Context, r *request.RecordUpload) (interf
return makeAttachmentPayload(ctx, a, err)
}
func (ctrl *Record) Export(ctx context.Context, r *request.RecordExport) (interface{}, error) {
var (
m *types.Module
err error
)
if m, err = ctrl.module.With(ctx).FindByID(r.NamespaceID, r.ModuleID); err != nil {
return nil, err
}
_ = m
// will probably have to rewrite exporting into something more optimal:
// maybe pass encoding function/callback directly
rr, _, err := ctrl.record.With(ctx).Find(types.RecordFilter{
NamespaceID: r.NamespaceID,
ModuleID: r.ModuleID,
Filter: r.Filter,
Sort: r.Sort,
})
return func(w http.ResponseWriter, req *http.Request) {
var (
enc = json.NewEncoder(w)
filename = fmt.Sprintf("; filename=%s.%s", r.Filename, r.Ext)
)
if r.Download {
w.Header().Add("Content-Disposition", "attachment"+filename)
} else {
w.Header().Add("Content-Disposition", "inline"+filename)
}
_ = rr.Walk(func(record *types.Record) error {
return enc.Encode(record)
})
}, nil
}
func (ctrl Record) makePayload(ctx context.Context, m *types.Module, r *types.Record, err error) (*recordPayload, error) {
if err != nil || r == nil {
return nil, err

View File

@ -175,6 +175,81 @@ func (r *RecordList) Fill(req *http.Request) (err error) {
var _ RequestFiller = NewRecordList()
// Record export request parameters
type RecordExport struct {
Filter string
Sort string
Download bool
Filename string
Ext string
NamespaceID uint64 `json:",string"`
ModuleID uint64 `json:",string"`
}
func NewRecordExport() *RecordExport {
return &RecordExport{}
}
func (r RecordExport) Auditable() map[string]interface{} {
var out = map[string]interface{}{}
out["filter"] = r.Filter
out["sort"] = r.Sort
out["download"] = r.Download
out["filename"] = r.Filename
out["ext"] = r.Ext
out["namespaceID"] = r.NamespaceID
out["moduleID"] = r.ModuleID
return out
}
func (r *RecordExport) Fill(req *http.Request) (err error) {
if strings.ToLower(req.Header.Get("content-type")) == "application/json" {
err = json.NewDecoder(req.Body).Decode(r)
switch {
case err == io.EOF:
err = nil
case err != nil:
return errors.Wrap(err, "error parsing http request body")
}
}
if err = req.ParseForm(); err != nil {
return err
}
get := map[string]string{}
post := map[string]string{}
urlQuery := req.URL.Query()
for name, param := range urlQuery {
get[name] = string(param[0])
}
postVars := req.Form
for name, param := range postVars {
post[name] = string(param[0])
}
if val, ok := get["filter"]; ok {
r.Filter = val
}
if val, ok := get["sort"]; ok {
r.Sort = val
}
if val, ok := get["download"]; ok {
r.Download = parseBool(val)
}
r.Filename = chi.URLParam(req, "filename")
r.Ext = chi.URLParam(req, "ext")
r.NamespaceID = parseUInt64(chi.URLParam(req, "namespaceID"))
r.ModuleID = parseUInt64(chi.URLParam(req, "moduleID"))
return err
}
var _ RequestFiller = NewRecordExport()
// Record create request parameters
type RecordCreate struct {
Values types.RecordValueSet

View File

@ -681,6 +681,7 @@ Compose records
| ------ | -------- | ------- |
| `GET` | `/namespace/{namespaceID}/module/{moduleID}/record/report` | Generates report from module records |
| `GET` | `/namespace/{namespaceID}/module/{moduleID}/record/` | List/read records from module section |
| `GET` | `/namespace/{namespaceID}/module/{moduleID}/record/export{filename}.{ext}` | Exports records that match |
| `POST` | `/namespace/{namespaceID}/module/{moduleID}/record/` | Create record in module section |
| `GET` | `/namespace/{namespaceID}/module/{moduleID}/record/{recordID}` | Read records by ID from module section |
| `POST` | `/namespace/{namespaceID}/module/{moduleID}/record/{recordID}` | Update records in module section |
@ -724,6 +725,26 @@ Compose records
| namespaceID | uint64 | PATH | Namespace ID | N/A | YES |
| moduleID | uint64 | PATH | Module ID | N/A | YES |
## Exports records that match
#### Method
| URI | Protocol | Method | Authentication |
| --- | -------- | ------ | -------------- |
| `/namespace/{namespaceID}/module/{moduleID}/record/export{filename}.{ext}` | HTTP/S | GET | |
#### Request parameters
| Parameter | Type | Method | Description | Default | Required? |
| --------- | ---- | ------ | ----------- | ------- | --------- |
| filter | string | GET | Filtering condition | N/A | NO |
| sort | string | GET | Sort field (default id desc) | N/A | NO |
| download | bool | GET | Send headers to browser to trigger download/save-as | N/A | NO |
| filename | string | PATH | Filename to use | N/A | NO |
| ext | string | PATH | Export format | N/A | YES |
| namespaceID | uint64 | PATH | Namespace ID | N/A | YES |
| moduleID | uint64 | PATH | Module ID | N/A | YES |
## Create record in module section
#### Method