diff --git a/store/rdbms/generic_upgrades.go b/store/rdbms/generic_upgrades.go index 62ce67d72..e16cab3ef 100644 --- a/store/rdbms/generic_upgrades.go +++ b/store/rdbms/generic_upgrades.go @@ -86,10 +86,15 @@ func (g genericUpgrades) Upgrade(ctx context.Context, t *ddl.Table) error { return g.all(ctx, g.CreateAutomationSessionIndexes, ) - //case "compose_attachment_binds": - // return g.all(ctx, - // g.MigrateComposeAttachmentsToBindsTable, - // ) + //case "compose_attachment_binds": + // return g.all(ctx, + // g.MigrateComposeAttachmentsToBindsTable, + // ) + + case "reports": + return g.all(ctx, + g.AddScenariosField, + ) } return nil @@ -278,6 +283,20 @@ func (g genericUpgrades) DropOrganisationTable(ctx context.Context) error { return err } +func (g genericUpgrades) AddScenariosField(ctx context.Context) error { + var ( + col = &ddl.Column{ + Name: "scenarios", + Type: ddl.ColumnType{Type: ddl.ColumnTypeJson}, + IsNull: false, + DefaultValue: "NULL", + } + ) + + _, err := g.u.AddColumn(ctx, "reports", col) + return err +} + func (g genericUpgrades) AlterUsersDropOrganisation(ctx context.Context) error { _, err := g.u.DropColumn(ctx, "users", "rel_organisation") return err diff --git a/store/rdbms/rdbms_schema.go b/store/rdbms/rdbms_schema.go index 369de857a..60ff00eb9 100644 --- a/store/rdbms/rdbms_schema.go +++ b/store/rdbms/rdbms_schema.go @@ -368,6 +368,7 @@ func (Schema) Reports() *Table { ID, ColumnDef("handle", ColumnTypeVarchar, ColumnTypeLength(handleLength)), ColumnDef("meta", ColumnTypeJson), + ColumnDef("scenarios", ColumnTypeJson), ColumnDef("sources", ColumnTypeJson), ColumnDef("blocks", ColumnTypeJson), diff --git a/store/rdbms/reports.gen.go b/store/rdbms/reports.gen.go index b3a7cdc4d..300907bad 100644 --- a/store/rdbms/reports.gen.go +++ b/store/rdbms/reports.gen.go @@ -451,6 +451,7 @@ func (s Store) internalReportRowScanner(row rowScanner) (res *types.Report, err &res.ID, &res.Handle, &res.Meta, + &res.Scenarios, &res.Sources, &res.Blocks, &res.OwnedBy, @@ -502,6 +503,7 @@ func (Store) reportColumns(aa ...string) []string { alias + "id", alias + "handle", alias + "meta", + alias + "scenarios", alias + "sources", alias + "blocks", alias + "owned_by", @@ -539,6 +541,7 @@ func (s Store) internalReportEncoder(res *types.Report) store.Payload { "id": res.ID, "handle": res.Handle, "meta": res.Meta, + "scenarios": res.Scenarios, "sources": res.Sources, "blocks": res.Blocks, "owned_by": res.OwnedBy, diff --git a/store/reports.yaml b/store/reports.yaml index a966a48f8..2b6f0aace 100644 --- a/store/reports.yaml +++ b/store/reports.yaml @@ -5,6 +5,7 @@ fields: - { field: ID } - { field: Handle, sortable: true } - { field: Meta, type: '*ReportMeta' } + - { field: Scenarios, type: 'ReportScenarioSet' } - { field: Sources, type: 'ReportDataSourceSet' } - { field: Blocks, type: 'ReportBlockSet' } - { field: OwnedBy } diff --git a/system/rest.yaml b/system/rest.yaml index bc1b7fb71..3fa30ba9c 100644 --- a/system/rest.yaml +++ b/system/rest.yaml @@ -1391,6 +1391,7 @@ endpoints: post: - { name: handle, type: string, title: Client handle } - { name: meta, type: '*types.ReportMeta', title: Additional info, parser: types.ParseReportMeta } + - { name: scenarios, type: "types.ReportScenarioSet", title: Report scenarios } - { name: sources, type: "types.ReportDataSourceSet", title: Report source definitions } - { name: blocks, type: "types.ReportBlockSet", title: Report blocks definition } - { name: labels, type: 'map[string]string', title: Labels, parser: label.ParseStrings } @@ -1407,6 +1408,7 @@ endpoints: post: - { name: handle, type: string, title: Client handle } - { name: meta, type: '*types.ReportMeta', title: Additional info, parser: types.ParseReportMeta } + - { name: scenarios, type: "types.ReportScenarioSet", title: Report scenarios } - { name: sources, type: "types.ReportDataSourceSet", title: Report sources definition } - { name: blocks, type: "types.ReportBlockSet", title: Report blocks definition } - { name: labels, type: 'map[string]string', title: Labels, parser: label.ParseStrings } diff --git a/system/rest/report.go b/system/rest/report.go index 8094df14e..513791cee 100644 --- a/system/rest/report.go +++ b/system/rest/report.go @@ -93,11 +93,12 @@ func (ctrl *Report) Create(ctx context.Context, r *request.ReportCreate) (interf var ( err error app = &types.Report{ - Handle: r.Handle, - Meta: r.Meta, - Sources: r.Sources, - Blocks: r.Blocks, - Labels: r.Labels, + Handle: r.Handle, + Meta: r.Meta, + Scenarios: r.Scenarios, + Sources: r.Sources, + Blocks: r.Blocks, + Labels: r.Labels, } ) @@ -109,12 +110,13 @@ func (ctrl *Report) Update(ctx context.Context, r *request.ReportUpdate) (interf var ( err error app = &types.Report{ - ID: r.ReportID, - Handle: r.Handle, - Meta: r.Meta, - Sources: r.Sources, - Blocks: r.Blocks, - Labels: r.Labels, + ID: r.ReportID, + Handle: r.Handle, + Meta: r.Meta, + Scenarios: r.Scenarios, + Sources: r.Sources, + Blocks: r.Blocks, + Labels: r.Labels, } ) diff --git a/system/rest/request/report.go b/system/rest/request/report.go index d61068390..b49452d59 100644 --- a/system/rest/request/report.go +++ b/system/rest/request/report.go @@ -79,6 +79,11 @@ type ( // Additional info Meta *types.ReportMeta + // Scenarios POST parameter + // + // Report scenarios + Scenarios types.ReportScenarioSet + // Sources POST parameter // // Report source definitions @@ -111,6 +116,11 @@ type ( // Additional info Meta *types.ReportMeta + // Scenarios POST parameter + // + // Report scenarios + Scenarios types.ReportScenarioSet + // Sources POST parameter // // Report sources definition @@ -286,11 +296,12 @@ func NewReportCreate() *ReportCreate { // Auditable returns all auditable/loggable parameters func (r ReportCreate) Auditable() map[string]interface{} { return map[string]interface{}{ - "handle": r.Handle, - "meta": r.Meta, - "sources": r.Sources, - "blocks": r.Blocks, - "labels": r.Labels, + "handle": r.Handle, + "meta": r.Meta, + "scenarios": r.Scenarios, + "sources": r.Sources, + "blocks": r.Blocks, + "labels": r.Labels, } } @@ -304,6 +315,11 @@ func (r ReportCreate) GetMeta() *types.ReportMeta { return r.Meta } +// Auditable returns all auditable/loggable parameters +func (r ReportCreate) GetScenarios() types.ReportScenarioSet { + return r.Scenarios +} + // Auditable returns all auditable/loggable parameters func (r ReportCreate) GetSources() types.ReportDataSourceSet { return r.Sources @@ -359,6 +375,13 @@ func (r *ReportCreate) Fill(req *http.Request) (err error) { } } + //if val, ok := req.Form["scenarios[]"]; ok && len(val) > 0 { + // r.Scenarios, err = types.ReportScenarioSet(val), nil + // if err != nil { + // return err + // } + //} + //if val, ok := req.Form["sources[]"]; ok && len(val) > 0 { // r.Sources, err = types.ReportDataSourceSet(val), nil // if err != nil { @@ -397,12 +420,13 @@ func NewReportUpdate() *ReportUpdate { // Auditable returns all auditable/loggable parameters func (r ReportUpdate) Auditable() map[string]interface{} { return map[string]interface{}{ - "reportID": r.ReportID, - "handle": r.Handle, - "meta": r.Meta, - "sources": r.Sources, - "blocks": r.Blocks, - "labels": r.Labels, + "reportID": r.ReportID, + "handle": r.Handle, + "meta": r.Meta, + "scenarios": r.Scenarios, + "sources": r.Sources, + "blocks": r.Blocks, + "labels": r.Labels, } } @@ -421,6 +445,11 @@ func (r ReportUpdate) GetMeta() *types.ReportMeta { return r.Meta } +// Auditable returns all auditable/loggable parameters +func (r ReportUpdate) GetScenarios() types.ReportScenarioSet { + return r.Scenarios +} + // Auditable returns all auditable/loggable parameters func (r ReportUpdate) GetSources() types.ReportDataSourceSet { return r.Sources @@ -476,6 +505,13 @@ func (r *ReportUpdate) Fill(req *http.Request) (err error) { } } + //if val, ok := req.Form["scenarios[]"]; ok && len(val) > 0 { + // r.Scenarios, err = types.ReportScenarioSet(val), nil + // if err != nil { + // return err + // } + //} + //if val, ok := req.Form["sources[]"]; ok && len(val) > 0 { // r.Sources, err = types.ReportDataSourceSet(val), nil // if err != nil { diff --git a/system/service/report.go b/system/service/report.go index dc539ced1..e1959505b 100644 --- a/system/service/report.go +++ b/system/service/report.go @@ -184,6 +184,7 @@ func (svc *report) Update(ctx context.Context, upd *types.Report) (report *types // Assign changed values after afterUpdate events are emitted report.Handle = upd.Handle report.Meta = upd.Meta + report.Scenarios = upd.Scenarios report.Sources = upd.Sources report.Blocks = upd.Blocks report.UpdatedAt = now() diff --git a/system/types/report.go b/system/types/report.go index 879201aed..b71727573 100644 --- a/system/types/report.go +++ b/system/types/report.go @@ -16,8 +16,9 @@ type ( Handle string `json:"handle"` Meta *ReportMeta `json:"meta,omitempty"` - Sources ReportDataSourceSet `json:"sources"` - Blocks ReportBlockSet `json:"blocks"` + Scenarios ReportScenarioSet `json:"scenarios,omitempty"` + Sources ReportDataSourceSet `json:"sources"` + Blocks ReportBlockSet `json:"blocks"` // Report labels Labels map[string]string `json:"labels,omitempty"` @@ -31,6 +32,14 @@ type ( DeletedAt *time.Time `json:"deletedAt,omitempty"` } + ReportScenarioSet []*ReportScenario + ScenarioFilterMap map[string]*report.Filter + ReportScenario struct { + // ScenarioID uint64 `json:"scenarioID,string,omitempty"` + Label string `json:"label"` + Filters ScenarioFilterMap `json:"filters,omitempty"` + } + ReportDataSource struct { Meta interface{} `json:"meta,omitempty"` Step *report.StepDefinition `json:"step"` @@ -164,6 +173,26 @@ func (vv *ReportDataSourceSet) Scan(value interface{}) error { return nil } +// Scan on ReportScenarioSet gracefully handles conversion from NULL +func (vv ReportScenarioSet) Value() (driver.Value, error) { + return json.Marshal(vv) +} + +func (vv *ReportScenarioSet) Scan(value interface{}) error { + //lint:ignore S1034 This typecast is intentional, we need to get []byte out of a []uint8 + switch value.(type) { + case nil: + *vv = ReportScenarioSet{} + case []uint8: + b := value.([]byte) + if err := json.Unmarshal(b, vv); err != nil { + return fmt.Errorf("cannot scan '%v' into ReportDataSourceSet: %w", string(b), err) + } + } + + return nil +} + // func (r *Report) decodeTranslations(tt locale.ResourceTranslationIndex) { // var aux *locale.ResourceTranslation diff --git a/tests/system/report_test.go b/tests/system/report_test.go new file mode 100644 index 000000000..e2b0e000a --- /dev/null +++ b/tests/system/report_test.go @@ -0,0 +1,94 @@ +package system + +import ( + "context" + "fmt" + "net/http" + "testing" + "time" + + "github.com/cortezaproject/corteza-server/pkg/id" + "github.com/cortezaproject/corteza-server/store" + "github.com/cortezaproject/corteza-server/system/service" + "github.com/cortezaproject/corteza-server/system/types" + "github.com/cortezaproject/corteza-server/tests/helpers" +) + +func (h helper) createReport(report *types.Report) *types.Report { + if report.ID == 0 { + report.ID = id.Next() + } + + if report.CreatedAt.IsZero() { + report.CreatedAt = time.Now() + } + + h.a.NoError(service.DefaultStore.CreateReport(context.Background(), report)) + return report +} + +func (h helper) clearReports() { + h.noError(store.TruncateReports(context.Background(), service.DefaultStore)) +} + +func (h helper) lookupReportByHandle(handle string) *types.Report { + res, err := store.LookupReportByHandle(context.Background(), service.DefaultStore, handle) + h.noError(err) + return res +} + +func TestReportScenarios_create(t *testing.T) { + h := newHelper(t) + h.clearReports() + helpers.AllowMe(h, types.ComponentRbacResource(), "report.create") + + h.apiInit(). + Post("/reports/"). + Header("Accept", "application/json"). + JSON(`{ + "handle": "test_report", + "scenarios": [{ "label": "scenario 1", "filters": { "ds_1": { "raw": "field == 'value'" } } }] + }`). + Expect(t). + Status(http.StatusOK). + Assert(helpers.AssertNoErrors). + End() + + r := h.lookupReportByHandle("test_report") + h.a.NotNil(r) + + h.a.Len(r.Scenarios, 1) + sc := r.Scenarios[0] + h.a.NotNil(sc.Filters) + h.a.Contains(sc.Filters, "ds_1") +} + +func TestReportScenarios_update(t *testing.T) { + h := newHelper(t) + h.clearReports() + helpers.AllowMe(h, types.ReportRbacResource(0), "update") + + r := h.createReport(&types.Report{ + Handle: "test_report", + }) + + h.apiInit(). + Put(fmt.Sprintf("/reports/%d", r.ID)). + Header("Accept", "application/json"). + JSON(`{ + "handle": "test_report", + "scenarios": [{ "label": "scenario 1", "filters": { "ds_1": { "raw": "field == 'value'" } } }] + }`). + Expect(t). + Status(http.StatusOK). + Assert(helpers.AssertNoErrors). + End() + + r = h.lookupReportByHandle("test_report") + h.a.NotNil(r) + + h.a.Len(r.Scenarios, 1) + sc := r.Scenarios[0] + h.a.NotNil(sc.Filters) + h.a.Contains(sc.Filters, "ds_1") +}