diff --git a/pkg/report/datasource.go b/pkg/report/datasource.go index a59900241..9faf73af5 100644 --- a/pkg/report/datasource.go +++ b/pkg/report/datasource.go @@ -21,6 +21,7 @@ type ( Datasource interface { Name() string Load(context.Context, ...*FrameDefinition) (Loader, Closer, error) + Describe() FrameDescriptionSet } // GroupableDatasource is able to provide groupped data diff --git a/pkg/report/frame.go b/pkg/report/frame.go index e99b0611e..1c4d409b3 100644 --- a/pkg/report/frame.go +++ b/pkg/report/frame.go @@ -54,6 +54,15 @@ type ( Paging *filter.Paging `json:"paging"` Sorting filter.SortExprSet `json:"sorting"` } + + FrameDescriptionSet []*FrameDescription + FrameDescription struct { + Source string `json:"source"` + Ref string `json:"ref,omitempty"` + Columns FrameColumnSet `json:"columns"` + + // @todo size and other shape related bits + } ) const ( diff --git a/pkg/report/model.go b/pkg/report/model.go index b66f94657..312530f37 100644 --- a/pkg/report/model.go +++ b/pkg/report/model.go @@ -19,6 +19,7 @@ type ( Add(...Step) M Run(context.Context) error Load(context.Context, ...*FrameDefinition) ([]*Frame, error) + Describe(source string) (FrameDescriptionSet, error) } StepSet []Step @@ -130,6 +131,15 @@ func (m *model) Run(ctx context.Context) (err error) { return nil } +func (m *model) Describe(source string) (out FrameDescriptionSet, err error) { + ds := m.datasources.Find(source) + if ds == nil { + return nil, fmt.Errorf("model does not contain the datasource: %s", source) + } + + return ds.Describe(), nil +} + func (m *model) Load(ctx context.Context, dd ...*FrameDefinition) ([]*Frame, error) { var err error diff --git a/pkg/report/step_join.go b/pkg/report/step_join.go index bce04f0ac..fe3898a6c 100644 --- a/pkg/report/step_join.go +++ b/pkg/report/step_join.go @@ -93,6 +93,34 @@ func (d *joinedDataset) Name() string { return d.def.Name } +func (d *joinedDataset) Describe() FrameDescriptionSet { + out := make(FrameDescriptionSet, 0, 2) + + local := d.base.Describe() + for _, l := range local { + out = append(out, + &FrameDescription{ + Source: d.Name(), + Ref: l.Source, + Columns: l.Columns, + }, + ) + } + + foreign := d.foreign.Describe() + for _, f := range foreign { + out = append(out, + &FrameDescription{ + Source: d.Name(), + Ref: f.Source, + Columns: f.Columns, + }, + ) + } + + return out +} + // @todo allow x-join filtering func (d *joinedDataset) Load(ctx context.Context, dd ...*FrameDefinition) (Loader, Closer, error) { // to hold closer references for all underlying datasources diff --git a/store/rdbms/compose_record_datasource.go b/store/rdbms/compose_record_datasource.go index 51edfec10..eb16414a0 100644 --- a/store/rdbms/compose_record_datasource.go +++ b/store/rdbms/compose_record_datasource.go @@ -174,6 +174,15 @@ func (r *recordDatasource) Group(d report.GroupDefinition, name string) (bool, e return true, nil } +func (r *recordDatasource) Describe() report.FrameDescriptionSet { + return report.FrameDescriptionSet{ + &report.FrameDescription{ + Source: r.Name(), + Columns: r.cols, + }, + } +} + func (r *recordDatasource) Load(ctx context.Context, dd ...*report.FrameDefinition) (l report.Loader, c report.Closer, err error) { def := dd[0] diff --git a/system/rest.yaml b/system/rest.yaml index f950f080e..31a239d2a 100644 --- a/system/rest.yaml +++ b/system/rest.yaml @@ -1435,6 +1435,16 @@ endpoints: name: reportID required: true title: Report ID + - name: describe + method: POST + title: Describe report (fresh) + path: "/describe" + parameters: + post: + - { name: sources, type: "types.ReportDataSourceSet", title: Report steps definition } + - { name: steps, type: "report.StepDefinitionSet", title: Report steps definition } + - { name: describe, type: "[]string", title: The source descriptions to generate } + # @todo better name - name: runFresh method: POST diff --git a/system/rest/handlers/report.go b/system/rest/handlers/report.go index 8e72d8a5d..2758b4663 100644 --- a/system/rest/handlers/report.go +++ b/system/rest/handlers/report.go @@ -25,6 +25,7 @@ type ( Read(context.Context, *request.ReportRead) (interface{}, error) Delete(context.Context, *request.ReportDelete) (interface{}, error) Undelete(context.Context, *request.ReportUndelete) (interface{}, error) + Describe(context.Context, *request.ReportDescribe) (interface{}, error) RunFresh(context.Context, *request.ReportRunFresh) (interface{}, error) Run(context.Context, *request.ReportRun) (interface{}, error) } @@ -37,6 +38,7 @@ type ( Read func(http.ResponseWriter, *http.Request) Delete func(http.ResponseWriter, *http.Request) Undelete func(http.ResponseWriter, *http.Request) + Describe func(http.ResponseWriter, *http.Request) RunFresh func(http.ResponseWriter, *http.Request) Run func(http.ResponseWriter, *http.Request) } @@ -140,6 +142,22 @@ func NewReport(h ReportAPI) *Report { api.Send(w, r, value) }, + Describe: func(w http.ResponseWriter, r *http.Request) { + defer r.Body.Close() + params := request.NewReportDescribe() + if err := params.Fill(r); err != nil { + api.Send(w, r, err) + return + } + + value, err := h.Describe(r.Context(), params) + if err != nil { + api.Send(w, r, err) + return + } + + api.Send(w, r, value) + }, RunFresh: func(w http.ResponseWriter, r *http.Request) { defer r.Body.Close() params := request.NewReportRunFresh() @@ -184,6 +202,7 @@ func (h Report) MountRoutes(r chi.Router, middlewares ...func(http.Handler) http r.Get("/reports/{reportID}", h.Read) r.Delete("/reports/{reportID}", h.Delete) r.Post("/reports/{reportID}/undelete", h.Undelete) + r.Post("/reports/describe", h.Describe) r.Post("/reports/run", h.RunFresh) r.Post("/reports/{reportID}/run", h.Run) }) diff --git a/system/rest/report.go b/system/rest/report.go index 509f34480..689295a63 100644 --- a/system/rest/report.go +++ b/system/rest/report.go @@ -30,6 +30,7 @@ type ( // @todo // Run(ctx context.Context, ID uint64, dd report.DatasetDefinitionSet) (rr *report.Matrix, err error) RunFresh(ctx context.Context, src types.ReportDataSourceSet, st report.StepDefinitionSet, dd report.FrameDefinitionSet) (rr []*report.Frame, err error) + DescribeFresh(ctx context.Context, src types.ReportDataSourceSet, st report.StepDefinitionSet, sources ...string) (out report.FrameDescriptionSet, err error) } reportAccessController interface { @@ -139,6 +140,10 @@ func (ctrl *Report) Run(ctx context.Context, r *request.ReportRun) (interface{}, return nil, nil } +func (ctrl *Report) Describe(ctx context.Context, r *request.ReportDescribe) (interface{}, error) { + return ctrl.report.DescribeFresh(ctx, r.Sources, r.Steps, r.Describe...) +} + func (ctrl *Report) RunFresh(ctx context.Context, r *request.ReportRunFresh) (interface{}, error) { rr, err := ctrl.report.RunFresh(ctx, r.Sources, r.Steps, r.Frames) return ctrl.makeReportFramePayload(ctx, rr, err) diff --git a/system/rest/request/report.go b/system/rest/request/report.go index 3691ac590..74962bc50 100644 --- a/system/rest/request/report.go +++ b/system/rest/request/report.go @@ -148,6 +148,23 @@ type ( ReportID uint64 `json:",string"` } + ReportDescribe struct { + // Sources POST parameter + // + // Report steps definition + Sources types.ReportDataSourceSet + + // Steps POST parameter + // + // Report steps definition + Steps report.StepDefinitionSet + + // Describe POST parameter + // + // The source descriptions to generate + Describe []string + } + ReportRunFresh struct { // Sources POST parameter // @@ -623,6 +640,81 @@ func (r *ReportUndelete) Fill(req *http.Request) (err error) { return err } +// NewReportDescribe request +func NewReportDescribe() *ReportDescribe { + return &ReportDescribe{} +} + +// Auditable returns all auditable/loggable parameters +func (r ReportDescribe) Auditable() map[string]interface{} { + return map[string]interface{}{ + "sources": r.Sources, + "steps": r.Steps, + "describe": r.Describe, + } +} + +// Auditable returns all auditable/loggable parameters +func (r ReportDescribe) GetSources() types.ReportDataSourceSet { + return r.Sources +} + +// Auditable returns all auditable/loggable parameters +func (r ReportDescribe) GetSteps() report.StepDefinitionSet { + return r.Steps +} + +// Auditable returns all auditable/loggable parameters +func (r ReportDescribe) GetDescribe() []string { + return r.Describe +} + +// Fill processes request and fills internal variables +func (r *ReportDescribe) 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 fmt.Errorf("error parsing http request body: %w", err) + } + } + + { + if err = req.ParseForm(); err != nil { + return err + } + + // POST params + + //if val, ok := req.Form["sources[]"]; ok && len(val) > 0 { + // r.Sources, err = types.ReportDataSourceSet(val), nil + // if err != nil { + // return err + // } + //} + + //if val, ok := req.Form["steps[]"]; ok && len(val) > 0 { + // r.Steps, err = report.StepDefinitionSet(val), nil + // if err != nil { + // return err + // } + //} + + //if val, ok := req.Form["describe[]"]; ok && len(val) > 0 { + // r.Describe, err = val, nil + // if err != nil { + // return err + // } + //} + } + + return err +} + // NewReportRunFresh request func NewReportRunFresh() *ReportRunFresh { return &ReportRunFresh{} diff --git a/system/service/report.go b/system/service/report.go index ec23b5828..6489f1c7a 100644 --- a/system/service/report.go +++ b/system/service/report.go @@ -288,6 +288,45 @@ func (svc *report) Run(ctx context.Context, ID uint64, dd rep.FrameDefinitionSet return nil, nil } +// actionlog? +func (svc *report) DescribeFresh(ctx context.Context, src types.ReportDataSourceSet, st rep.StepDefinitionSet, sources ...string) (out rep.FrameDescriptionSet, err error) { + // var ( + // aaProps = &reportActionProps{} + // ) + + out = make(rep.FrameDescriptionSet, 0, len(sources)*2) + + err = func() (err error) { + ss := src.ModelSteps() + ss = append(ss, st...) + + // Model the report + model, err := rep.Model(ctx, reporters, ss...) + if err != nil { + return + } + + err = model.Run(ctx) + if err != nil { + return + } + + var auxOut rep.FrameDescriptionSet + for _, s := range sources { + auxOut, err = model.Describe(s) + if err != nil { + return err + } + + out = append(out, auxOut...) + } + + return nil + }() + + return out, err +} + func (svc *report) RunFresh(ctx context.Context, src types.ReportDataSourceSet, st rep.StepDefinitionSet, dd rep.FrameDefinitionSet) (out []*rep.Frame, err error) { var ( aaProps = &reportActionProps{}