3
0

Base reporting service definitions

This commit is contained in:
Tomaž Jerman
2021-05-30 15:24:22 +02:00
parent 6751d0ec8b
commit 4aa429362b
30 changed files with 3836 additions and 0 deletions

10
def/system.report.yaml Normal file
View File

@@ -0,0 +1,10 @@
rbac:
operations:
read:
description: Read report
update:
description: Update report
delete:
description: Delete report
run:
description: Run report

View File

@@ -42,6 +42,11 @@ rbac:
templates.search:
description: List, search or filter templates
report.create:
description: Create report
reports.search:
description: List, search or filter reports
reminder.assign:
description: Assign reminders

View File

@@ -11,6 +11,7 @@ package resource
// - system.apigw-route.yaml
// - system.application.yaml
// - system.auth-client.yaml
// - system.report.yaml
// - system.role.yaml
// - system.template.yaml
// - system.user.yaml
@@ -72,6 +73,19 @@ func SystemAuthClientRbacReferences(authClient string) (res *Ref, pp []*Ref, err
return
}
// SystemReportRbacReferences generates RBAC references
//
// Resources with "envoy: false" are skipped
//
// This function is auto-generated
func SystemReportRbacReferences(report string) (res *Ref, pp []*Ref, err error) {
if report != "*" {
res = &Ref{ResourceType: types.ReportResourceType, Identifiers: MakeIdentifiers(report)}
}
return
}
// SystemRoleRbacReferences generates RBAC references
//
// Resources with "envoy: false" are skipped

View File

@@ -20,6 +20,7 @@ package resource
// - system.apigw-route.yaml
// - system.application.yaml
// - system.auth-client.yaml
// - system.report.yaml
// - system.role.yaml
// - system.template.yaml
// - system.user.yaml
@@ -210,6 +211,16 @@ func ParseRule(res string) (string, *Ref, []*Ref, error) {
)
return systemTypes.AuthClientResourceType, ref, pp, err
case systemTypes.ReportResourceType:
if len(path) != 1 {
return "", nil, nil, fmt.Errorf("expecting 1 reference components in path, got %d", len(path))
}
ref, pp, err := SystemReportRbacReferences(
// report
path[0],
)
return systemTypes.ReportResourceType, ref, pp, err
case systemTypes.RoleResourceType:
if len(path) != 1 {
return "", nil, nil, fmt.Errorf("expecting 1 reference components in path, got %d", len(path))

View File

@@ -36,6 +36,7 @@ package store
// - store/messagebus_queue_settings.yaml
// - store/rbac_rules.yaml
// - store/reminders.yaml
// - store/reports.yaml
// - store/role_members.yaml
// - store/roles.yaml
// - store/settings.yaml
@@ -81,6 +82,7 @@ type (
MessagebusQueueSettings
RbacRules
Reminders
Reports
RoleMembers
Roles
Settings

View File

@@ -11,6 +11,7 @@ package rdbms
import (
"context"
"database/sql"
"github.com/Masterminds/squirrel"
"github.com/cortezaproject/corteza-server/compose/types"
"github.com/cortezaproject/corteza-server/pkg/errors"

View File

@@ -67,6 +67,7 @@ func (s Schema) Tables() []*Table {
s.Labels(),
s.Flags(),
s.Templates(),
s.Reports(),
s.ComposeAttachment(),
s.ComposeChart(),
s.ComposeModule(),
@@ -351,6 +352,22 @@ func (Schema) Templates() *Table {
)
}
func (Schema) Reports() *Table {
return TableDef("reports",
ID,
ColumnDef("handle", ColumnTypeVarchar, ColumnTypeLength(handleLength)),
ColumnDef("meta", ColumnTypeJson),
ColumnDef("sources", ColumnTypeJson),
ColumnDef("projections", ColumnTypeJson),
ColumnDef("owned_by", ColumnTypeIdentifier),
CUDTimestamps,
CUDUsers,
AddIndex("unique_handle", IExpr("LOWER(handle)"), IWhere("LENGTH(handle) > 0 AND deleted_at IS NULL")),
)
}
func (Schema) ComposeAttachment() *Table {
// @todo merge with general attachment table

634
store/rdbms/reports.gen.go generated Normal file
View File

@@ -0,0 +1,634 @@
package rdbms
// This file is an auto-generated file
//
// Template: pkg/codegen/assets/store_rdbms.gen.go.tpl
// Definitions: store/reports.yaml
//
// Changes to this file may cause incorrect behavior
// and will be lost if the code is regenerated.
import (
"context"
"database/sql"
"github.com/Masterminds/squirrel"
"github.com/cortezaproject/corteza-server/pkg/errors"
"github.com/cortezaproject/corteza-server/pkg/filter"
"github.com/cortezaproject/corteza-server/store"
"github.com/cortezaproject/corteza-server/store/rdbms/builders"
"github.com/cortezaproject/corteza-server/system/types"
)
var _ = errors.Is
// SearchReports returns all matching rows
//
// This function calls convertReportFilter with the given
// types.ReportFilter and expects to receive a working squirrel.SelectBuilder
func (s Store) SearchReports(ctx context.Context, f types.ReportFilter) (types.ReportSet, types.ReportFilter, error) {
var (
err error
set []*types.Report
q squirrel.SelectBuilder
)
return set, f, func() error {
q, err = s.convertReportFilter(f)
if err != nil {
return err
}
// Paging enabled
// {search: {enablePaging:true}}
// Cleanup unwanted cursor values (only relevant is f.PageCursor, next&prev are reset and returned)
f.PrevPage, f.NextPage = nil, nil
if f.PageCursor != nil {
// Page cursor exists so we need to validate it against used sort
// To cover the case when paging cursor is set but sorting is empty, we collect the sorting instructions
// from the cursor.
// This (extracted sorting info) is then returned as part of response
if f.Sort, err = f.PageCursor.Sort(f.Sort); err != nil {
return err
}
}
// Make sure results are always sorted at least by primary keys
if f.Sort.Get("id") == nil {
f.Sort = append(f.Sort, &filter.SortExpr{
Column: "id",
Descending: f.Sort.LastDescending(),
})
}
// Cloned sorting instructions for the actual sorting
// Original are passed to the fetchFullPageOfUsers fn used for cursor creation so it MUST keep the initial
// direction information
sort := f.Sort.Clone()
// When cursor for a previous page is used it's marked as reversed
// This tells us to flip the descending flag on all used sort keys
if f.PageCursor != nil && f.PageCursor.ROrder {
sort.Reverse()
}
// Apply sorting expr from filter to query
if q, err = setOrderBy(q, sort, s.sortableReportColumns()); err != nil {
return err
}
set, f.PrevPage, f.NextPage, err = s.fetchFullPageOfReports(
ctx,
q, f.Sort, f.PageCursor,
f.Limit,
f.Check,
func(cur *filter.PagingCursor) squirrel.Sqlizer {
return builders.CursorCondition(cur, nil)
},
)
if err != nil {
return err
}
f.PageCursor = nil
return nil
}()
}
// fetchFullPageOfReports collects all requested results.
//
// Function applies:
// - cursor conditions (where ...)
// - limit
//
// Main responsibility of this function is to perform additional sequential queries in case when not enough results
// are collected due to failed check on a specific row (by check fn).
//
// Function then moves cursor to the last item fetched
func (s Store) fetchFullPageOfReports(
ctx context.Context,
q squirrel.SelectBuilder,
sort filter.SortExprSet,
cursor *filter.PagingCursor,
reqItems uint,
check func(*types.Report) (bool, error),
cursorCond func(*filter.PagingCursor) squirrel.Sqlizer,
) (set []*types.Report, prev, next *filter.PagingCursor, err error) {
var (
aux []*types.Report
// When cursor for a previous page is used it's marked as reversed
// This tells us to flip the descending flag on all used sort keys
reversedOrder = cursor != nil && cursor.ROrder
// copy of the select builder
tryQuery squirrel.SelectBuilder
// Copy no. of required items to limit
// Limit will change when doing subsequent queries to fill
// the set with all required items
limit = reqItems
// cursor to prev. page is only calculated when cursor is used
hasPrev = cursor != nil
// next cursor is calculated when there are more pages to come
hasNext bool
)
set = make([]*types.Report, 0, DefaultSliceCapacity)
for try := 0; try < MaxRefetches; try++ {
if cursor != nil {
tryQuery = q.Where(cursorCond(cursor))
} else {
tryQuery = q
}
if limit > 0 {
// fetching + 1 so we know if there are more items
// we can fetch (next-page cursor)
tryQuery = tryQuery.Limit(uint64(limit + 1))
}
if aux, err = s.QueryReports(ctx, tryQuery, check); err != nil {
return nil, nil, nil, err
}
if len(aux) == 0 {
// nothing fetched
break
}
// append fetched items
set = append(set, aux...)
if reqItems == 0 {
// no max requested items specified, break out
break
}
collected := uint(len(set))
if reqItems > collected {
// not enough items fetched, try again with adjusted limit
limit = reqItems - collected
if limit < MinEnsureFetchLimit {
// In case limit is set very low and we've missed records in the first fetch,
// make sure next fetch limit is a bit higher
limit = MinEnsureFetchLimit
}
// Update cursor so that it points to the last item fetched
cursor = s.collectReportCursorValues(set[collected-1], sort...)
// Copy reverse flag from sorting
cursor.LThen = sort.Reversed()
continue
}
if reqItems < collected {
set = set[:reqItems]
hasNext = true
}
break
}
collected := len(set)
if collected == 0 {
return nil, nil, nil, nil
}
if reversedOrder {
// Fetched set needs to be reversed because we've forced a descending order to get the previous page
for i, j := 0, collected-1; i < j; i, j = i+1, j-1 {
set[i], set[j] = set[j], set[i]
}
// when in reverse-order rules on what cursor to return change
hasPrev, hasNext = hasNext, hasPrev
}
if hasPrev {
prev = s.collectReportCursorValues(set[0], sort...)
prev.ROrder = true
prev.LThen = !sort.Reversed()
}
if hasNext {
next = s.collectReportCursorValues(set[collected-1], sort...)
next.LThen = sort.Reversed()
}
return set, prev, next, nil
}
// QueryReports queries the database, converts and checks each row and
// returns collected set
//
// Fn also returns total number of fetched items and last fetched item so that the caller can construct cursor
// for next page of results
func (s Store) QueryReports(
ctx context.Context,
q squirrel.Sqlizer,
check func(*types.Report) (bool, error),
) ([]*types.Report, error) {
var (
tmp = make([]*types.Report, 0, DefaultSliceCapacity)
set = make([]*types.Report, 0, DefaultSliceCapacity)
res *types.Report
// Query rows with
rows, err = s.Query(ctx, q)
)
if err != nil {
return nil, err
}
defer rows.Close()
for rows.Next() {
if err = rows.Err(); err == nil {
res, err = s.internalReportRowScanner(rows)
}
if err != nil {
return nil, err
}
tmp = append(tmp, res)
}
for _, res = range tmp {
// check fn set, call it and see if it passed the test
// if not, skip the item
if check != nil {
if chk, err := check(res); err != nil {
return nil, err
} else if !chk {
continue
}
}
set = append(set, res)
}
return set, nil
}
// LookupReportByID searches for report by ID
//
// It returns report even if deleted
func (s Store) LookupReportByID(ctx context.Context, id uint64) (*types.Report, error) {
return s.execLookupReport(ctx, squirrel.Eq{
s.preprocessColumn("rp.id", ""): store.PreprocessValue(id, ""),
})
}
// LookupReportByHandle searches for report by Handle
//
// It returns report even if deleted
func (s Store) LookupReportByHandle(ctx context.Context, handle string) (*types.Report, error) {
return s.execLookupReport(ctx, squirrel.Eq{
s.preprocessColumn("rp.handle", ""): store.PreprocessValue(handle, ""),
"rp.deleted_at": nil,
})
}
// CreateReport creates one or more rows in reports table
func (s Store) CreateReport(ctx context.Context, rr ...*types.Report) (err error) {
for _, res := range rr {
err = s.checkReportConstraints(ctx, res)
if err != nil {
return err
}
err = s.execCreateReports(ctx, s.internalReportEncoder(res))
if err != nil {
return err
}
}
return
}
// UpdateReport updates one or more existing rows in reports
func (s Store) UpdateReport(ctx context.Context, rr ...*types.Report) error {
return s.partialReportUpdate(ctx, nil, rr...)
}
// partialReportUpdate updates one or more existing rows in reports
func (s Store) partialReportUpdate(ctx context.Context, onlyColumns []string, rr ...*types.Report) (err error) {
for _, res := range rr {
err = s.checkReportConstraints(ctx, res)
if err != nil {
return err
}
err = s.execUpdateReports(
ctx,
squirrel.Eq{
s.preprocessColumn("rp.id", ""): store.PreprocessValue(res.ID, ""),
},
s.internalReportEncoder(res).Skip("id").Only(onlyColumns...))
if err != nil {
return err
}
}
return
}
// UpsertReport updates one or more existing rows in reports
func (s Store) UpsertReport(ctx context.Context, rr ...*types.Report) (err error) {
for _, res := range rr {
err = s.checkReportConstraints(ctx, res)
if err != nil {
return err
}
err = s.execUpsertReports(ctx, s.internalReportEncoder(res))
if err != nil {
return err
}
}
return nil
}
// DeleteReport Deletes one or more rows from reports table
func (s Store) DeleteReport(ctx context.Context, rr ...*types.Report) (err error) {
for _, res := range rr {
err = s.execDeleteReports(ctx, squirrel.Eq{
s.preprocessColumn("rp.id", ""): store.PreprocessValue(res.ID, ""),
})
if err != nil {
return err
}
}
return nil
}
// DeleteReportByID Deletes row from the reports table
func (s Store) DeleteReportByID(ctx context.Context, ID uint64) error {
return s.execDeleteReports(ctx, squirrel.Eq{
s.preprocessColumn("rp.id", ""): store.PreprocessValue(ID, ""),
})
}
// TruncateReports Deletes all rows from the reports table
func (s Store) TruncateReports(ctx context.Context) error {
return s.Truncate(ctx, s.reportTable())
}
// execLookupReport prepares Report query and executes it,
// returning types.Report (or error)
func (s Store) execLookupReport(ctx context.Context, cnd squirrel.Sqlizer) (res *types.Report, err error) {
var (
row rowScanner
)
row, err = s.QueryRow(ctx, s.reportsSelectBuilder().Where(cnd))
if err != nil {
return
}
res, err = s.internalReportRowScanner(row)
if err != nil {
return
}
return res, nil
}
// execCreateReports updates all matched (by cnd) rows in reports with given data
func (s Store) execCreateReports(ctx context.Context, payload store.Payload) error {
return s.Exec(ctx, s.InsertBuilder(s.reportTable()).SetMap(payload))
}
// execUpdateReports updates all matched (by cnd) rows in reports with given data
func (s Store) execUpdateReports(ctx context.Context, cnd squirrel.Sqlizer, set store.Payload) error {
return s.Exec(ctx, s.UpdateBuilder(s.reportTable("rp")).Where(cnd).SetMap(set))
}
// execUpsertReports inserts new or updates matching (by-primary-key) rows in reports with given data
func (s Store) execUpsertReports(ctx context.Context, set store.Payload) error {
upsert, err := s.config.UpsertBuilder(
s.config,
s.reportTable(),
set,
s.preprocessColumn("id", ""),
)
if err != nil {
return err
}
return s.Exec(ctx, upsert)
}
// execDeleteReports Deletes all matched (by cnd) rows in reports with given data
func (s Store) execDeleteReports(ctx context.Context, cnd squirrel.Sqlizer) error {
return s.Exec(ctx, s.DeleteBuilder(s.reportTable("rp")).Where(cnd))
}
func (s Store) internalReportRowScanner(row rowScanner) (res *types.Report, err error) {
res = &types.Report{}
if _, has := s.config.RowScanners["report"]; has {
scanner := s.config.RowScanners["report"].(func(_ rowScanner, _ *types.Report) error)
err = scanner(row, res)
} else {
err = row.Scan(
&res.ID,
&res.Handle,
&res.Meta,
&res.Sources,
&res.Projections,
&res.OwnedBy,
&res.CreatedBy,
&res.UpdatedBy,
&res.DeletedBy,
&res.CreatedAt,
&res.UpdatedAt,
&res.DeletedAt,
)
}
if err == sql.ErrNoRows {
return nil, store.ErrNotFound.Stack(1)
}
if err != nil {
return nil, errors.Store("could not scan report db row: %s", err).Wrap(err)
} else {
return res, nil
}
}
// QueryReports returns squirrel.SelectBuilder with set table and all columns
func (s Store) reportsSelectBuilder() squirrel.SelectBuilder {
return s.SelectBuilder(s.reportTable("rp"), s.reportColumns("rp")...)
}
// reportTable name of the db table
func (Store) reportTable(aa ...string) string {
var alias string
if len(aa) > 0 {
alias = " AS " + aa[0]
}
return "reports" + alias
}
// ReportColumns returns all defined table columns
//
// With optional string arg, all columns are returned aliased
func (Store) reportColumns(aa ...string) []string {
var alias string
if len(aa) > 0 {
alias = aa[0] + "."
}
return []string{
alias + "id",
alias + "handle",
alias + "meta",
alias + "sources",
alias + "projections",
alias + "owned_by",
alias + "created_by",
alias + "updated_by",
alias + "deleted_by",
alias + "created_at",
alias + "updated_at",
alias + "deleted_at",
}
}
// {true true false true true true}
// sortableReportColumns returns all Report columns flagged as sortable
//
// With optional string arg, all columns are returned aliased
func (Store) sortableReportColumns() map[string]string {
return map[string]string{
"id": "id", "handle": "handle", "created_at": "created_at",
"createdat": "created_at",
"updated_at": "updated_at",
"updatedat": "updated_at",
"deleted_at": "deleted_at",
"deletedat": "deleted_at",
}
}
// internalReportEncoder encodes fields from types.Report to store.Payload (map)
//
// Encoding is done by using generic approach or by calling encodeReport
// func when rdbms.customEncoder=true
func (s Store) internalReportEncoder(res *types.Report) store.Payload {
return store.Payload{
"id": res.ID,
"handle": res.Handle,
"meta": res.Meta,
"sources": res.Sources,
"projections": res.Projections,
"owned_by": res.OwnedBy,
"created_by": res.CreatedBy,
"updated_by": res.UpdatedBy,
"deleted_by": res.DeletedBy,
"created_at": res.CreatedAt,
"updated_at": res.UpdatedAt,
"deleted_at": res.DeletedAt,
}
}
// collectReportCursorValues collects values from the given resource that and sets them to the cursor
// to be used for pagination
//
// Values that are collected must come from sortable, unique or primary columns/fields
// At least one of the collected columns must be flagged as unique, otherwise fn appends primary keys at the end
//
// Known issue:
// when collecting cursor values for query that sorts by unique column with partial index (ie: unique handle on
// undeleted items)
func (s Store) collectReportCursorValues(res *types.Report, cc ...*filter.SortExpr) *filter.PagingCursor {
var (
cursor = &filter.PagingCursor{LThen: filter.SortExprSet(cc).Reversed()}
hasUnique bool
// All known primary key columns
pkId bool
collect = func(cc ...*filter.SortExpr) {
for _, c := range cc {
switch c.Column {
case "id":
cursor.Set(c.Column, res.ID, c.Descending)
pkId = true
case "handle":
cursor.Set(c.Column, res.Handle, c.Descending)
case "created_at":
cursor.Set(c.Column, res.CreatedAt, c.Descending)
case "updated_at":
cursor.Set(c.Column, res.UpdatedAt, c.Descending)
case "deleted_at":
cursor.Set(c.Column, res.DeletedAt, c.Descending)
}
}
}
)
collect(cc...)
if !hasUnique || !(pkId && true) {
collect(&filter.SortExpr{Column: "id", Descending: false})
}
return cursor
}
// checkReportConstraints performs lookups (on valid) resource to check if any of the values on unique fields
// already exists in the store
//
// Using built-in constraint checking would be more performant but unfortunately we cannot rely
// on the full support (MySQL does not support conditional indexes)
func (s *Store) checkReportConstraints(ctx context.Context, res *types.Report) error {
// Consider resource valid when all fields in unique constraint check lookups
// have valid (non-empty) value
//
// Only string and uint64 are supported for now
// feel free to add additional types if needed
var valid = true
valid = valid && len(res.Handle) > 0
if !valid {
return nil
}
{
ex, err := s.LookupReportByHandle(ctx, res.Handle)
if err == nil && ex != nil && ex.ID != res.ID {
return store.ErrNotUnique.Stack(1)
} else if !errors.IsNotFound(err) {
return err
}
}
return nil
}

23
store/rdbms/reports.go Normal file
View File

@@ -0,0 +1,23 @@
package rdbms
import (
"github.com/Masterminds/squirrel"
"github.com/cortezaproject/corteza-server/pkg/filter"
"github.com/cortezaproject/corteza-server/system/types"
)
func (s Store) convertReportFilter(f types.ReportFilter) (query squirrel.SelectBuilder, err error) {
query = s.reportsSelectBuilder()
query = filter.StateCondition(query, "rp.deleted_at", f.Deleted)
if len(f.LabeledIDs) > 0 {
query = query.Where(squirrel.Eq{"rp.id": f.LabeledIDs})
}
if f.Handle != "" {
query = query.Where(squirrel.Eq{"rp.handle": f.Handle})
}
return
}

85
store/reports.gen.go generated Normal file
View File

@@ -0,0 +1,85 @@
package store
// This file is auto-generated.
//
// Template: pkg/codegen/assets/store_base.gen.go.tpl
// Definitions: store/reports.yaml
//
// Changes to this file may cause incorrect behavior and will be lost if
// the code is regenerated.
import (
"context"
"github.com/cortezaproject/corteza-server/system/types"
)
type (
Reports interface {
SearchReports(ctx context.Context, f types.ReportFilter) (types.ReportSet, types.ReportFilter, error)
LookupReportByID(ctx context.Context, id uint64) (*types.Report, error)
LookupReportByHandle(ctx context.Context, handle string) (*types.Report, error)
CreateReport(ctx context.Context, rr ...*types.Report) error
UpdateReport(ctx context.Context, rr ...*types.Report) error
UpsertReport(ctx context.Context, rr ...*types.Report) error
DeleteReport(ctx context.Context, rr ...*types.Report) error
DeleteReportByID(ctx context.Context, ID uint64) error
TruncateReports(ctx context.Context) error
}
)
var _ *types.Report
var _ context.Context
// SearchReports returns all matching Reports from store
func SearchReports(ctx context.Context, s Reports, f types.ReportFilter) (types.ReportSet, types.ReportFilter, error) {
return s.SearchReports(ctx, f)
}
// LookupReportByID searches for report by ID
//
// It returns report even if deleted
func LookupReportByID(ctx context.Context, s Reports, id uint64) (*types.Report, error) {
return s.LookupReportByID(ctx, id)
}
// LookupReportByHandle searches for report by Handle
//
// It returns report even if deleted
func LookupReportByHandle(ctx context.Context, s Reports, handle string) (*types.Report, error) {
return s.LookupReportByHandle(ctx, handle)
}
// CreateReport creates one or more Reports in store
func CreateReport(ctx context.Context, s Reports, rr ...*types.Report) error {
return s.CreateReport(ctx, rr...)
}
// UpdateReport updates one or more (existing) Reports in store
func UpdateReport(ctx context.Context, s Reports, rr ...*types.Report) error {
return s.UpdateReport(ctx, rr...)
}
// UpsertReport creates new or updates existing one or more Reports in store
func UpsertReport(ctx context.Context, s Reports, rr ...*types.Report) error {
return s.UpsertReport(ctx, rr...)
}
// DeleteReport Deletes one or more Reports from store
func DeleteReport(ctx context.Context, s Reports, rr ...*types.Report) error {
return s.DeleteReport(ctx, rr...)
}
// DeleteReportByID Deletes Report from store
func DeleteReportByID(ctx context.Context, s Reports, ID uint64) error {
return s.DeleteReportByID(ctx, ID)
}
// TruncateReports Deletes all Reports from store
func TruncateReports(ctx context.Context, s Reports) error {
return s.TruncateReports(ctx)
}

36
store/reports.yaml Normal file
View File

@@ -0,0 +1,36 @@
import:
- github.com/cortezaproject/corteza-server/system/types
fields:
- { field: ID }
- { field: Handle, sortable: true }
- { field: Meta, type: '*ReportMeta' }
- { field: Sources, type: 'ReportDataSourceSet' }
- { field: Projections, type: 'ReportProjectionSet' }
- { field: OwnedBy }
- { field: CreatedBy }
- { field: UpdatedBy }
- { field: DeletedBy }
- { field: CreatedAt, sortable: true }
- { field: UpdatedAt, sortable: true }
- { field: DeletedAt, sortable: true }
lookups:
- fields: [ ID ]
description: |-
searches for report by ID
It returns report even if deleted
- fields: [ Handle ]
uniqueConstraintCheck: true
filter: { DeletedAt: nil }
description: |-
searches for report by Handle
It returns report even if deleted
rdbms:
alias: rp
table: reports
customFilterConverter: true

View File

@@ -34,6 +34,7 @@ package tests
// - store/messagebus_queue_settings.yaml
// - store/rbac_rules.yaml
// - store/reminders.yaml
// - store/reports.yaml
// - store/role_members.yaml
// - store/roles.yaml
// - store/settings.yaml
@@ -211,6 +212,11 @@ func testAllGenerated(t *testing.T, s store.Storer) {
testReminders(t, s)
})
// Run generated tests for Reports
t.Run("Reports", func(t *testing.T) {
testReports(t, s)
})
// Run generated tests for RoleMembers
t.Run("RoleMembers", func(t *testing.T) {
testRoleMembers(t, s)

View File

@@ -0,0 +1,11 @@
package tests
import (
"testing"
"github.com/cortezaproject/corteza-server/store"
)
func testReports(t *testing.T, s store.Reports) {
t.Skip("@todo")
}

View File

@@ -1341,6 +1341,122 @@ endpoints:
type: json.RawMessage
required: false
title: Rendering options
- title: Reports
path: "/reports"
entrypoint: report
authentication: []
imports:
- github.com/cortezaproject/corteza-server/pkg/label
- github.com/cortezaproject/corteza-server/pkg/report
- github.com/cortezaproject/corteza-server/system/types
apis:
- name: list
method: GET
title: List reports
path: "/"
parameters:
get:
- name: handle
required: false
title: Report handle
type: string
- name: deleted
required: false
title: Exclude (0, default), include (1) or return only (2) deleted reports
type: uint
- type: map[string]string
name: labels
title: Labels
parser: label.ParseStrings
- type: uint
name: limit
title: Limit
- type: string
name: pageCursor
title: Page cursor
- type: string
name: sort
title: Sort items
- name: create
method: POST
title: Create report
path: "/"
parameters:
post:
- { name: handle, type: string, title: Client handle }
- { name: meta, type: '*types.ReportMeta', title: Additional info, parser: types.ParseReportMeta }
- { name: sources, type: "types.ReportDataSourceSet", title: Report source definitions }
- { name: projections, type: "types.ReportProjectionSet", title: Report projections definition }
- { name: labels, type: 'map[string]string', title: Labels, parser: label.ParseStrings }
- name: update
method: PUT
title: Update report
path: "/{reportID}"
parameters:
path:
- type: uint64
name: reportID
required: true
title: Report ID
post:
- { name: handle, type: string, title: Client handle }
- { name: meta, type: '*types.ReportMeta', title: Additional info, parser: types.ParseReportMeta }
- { name: sources, type: "types.ReportDataSourceSet", title: Report sources definition }
- { name: projections, type: "types.ReportProjectionSet", title: Report projections definition }
- { name: labels, type: 'map[string]string', title: Labels, parser: label.ParseStrings }
- name: read
method: GET
title: Read report details
path: "/{reportID}"
parameters:
path:
- type: uint64
name: reportID
required: true
title: Report ID
- name: delete
method: DELETE
title: Remove report
path: "/{reportID}"
parameters:
path:
- type: uint64
name: reportID
required: true
title: Report ID
- name: undelete
method: POST
title: Undelete report
path: "/{reportID}/undelete"
parameters:
path:
- type: uint64
name: reportID
required: true
title: Report ID
# @todo better name
- name: runFresh
method: POST
title: Run report (fresh)
path: "/run"
parameters:
post:
- { name: sources, type: "types.ReportDataSourceSet", title: Report steps definition }
- { name: steps, type: "report.StepDefinitionSet", title: Report steps definition }
- { name: frames, type: "report.FrameDefinitionSet", title: Report data frame definitions }
- name: run
method: POST
title: Run report
path: "/{reportID}/run"
parameters:
path:
- type: uint64
name: reportID
required: true
title: Report ID
post:
- { name: frames, type: "report.FrameDefinitionSet", title: Report data frame definitions }
- title: Statistics
entrypoint: stats
path: "/stats"

View File

@@ -0,0 +1,190 @@
package handlers
// This file is auto-generated.
//
// Changes to this file may cause incorrect behavior and will be lost if
// the code is regenerated.
//
// Definitions file that controls how this file is generated:
//
import (
"context"
"github.com/cortezaproject/corteza-server/pkg/api"
"github.com/cortezaproject/corteza-server/system/rest/request"
"github.com/go-chi/chi"
"net/http"
)
type (
// Internal API interface
ReportAPI interface {
List(context.Context, *request.ReportList) (interface{}, error)
Create(context.Context, *request.ReportCreate) (interface{}, error)
Update(context.Context, *request.ReportUpdate) (interface{}, error)
Read(context.Context, *request.ReportRead) (interface{}, error)
Delete(context.Context, *request.ReportDelete) (interface{}, error)
Undelete(context.Context, *request.ReportUndelete) (interface{}, error)
RunFresh(context.Context, *request.ReportRunFresh) (interface{}, error)
Run(context.Context, *request.ReportRun) (interface{}, error)
}
// HTTP API interface
Report struct {
List func(http.ResponseWriter, *http.Request)
Create func(http.ResponseWriter, *http.Request)
Update func(http.ResponseWriter, *http.Request)
Read func(http.ResponseWriter, *http.Request)
Delete func(http.ResponseWriter, *http.Request)
Undelete func(http.ResponseWriter, *http.Request)
RunFresh func(http.ResponseWriter, *http.Request)
Run func(http.ResponseWriter, *http.Request)
}
)
func NewReport(h ReportAPI) *Report {
return &Report{
List: func(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close()
params := request.NewReportList()
if err := params.Fill(r); err != nil {
api.Send(w, r, err)
return
}
value, err := h.List(r.Context(), params)
if err != nil {
api.Send(w, r, err)
return
}
api.Send(w, r, value)
},
Create: func(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close()
params := request.NewReportCreate()
if err := params.Fill(r); err != nil {
api.Send(w, r, err)
return
}
value, err := h.Create(r.Context(), params)
if err != nil {
api.Send(w, r, err)
return
}
api.Send(w, r, value)
},
Update: func(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close()
params := request.NewReportUpdate()
if err := params.Fill(r); err != nil {
api.Send(w, r, err)
return
}
value, err := h.Update(r.Context(), params)
if err != nil {
api.Send(w, r, err)
return
}
api.Send(w, r, value)
},
Read: func(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close()
params := request.NewReportRead()
if err := params.Fill(r); err != nil {
api.Send(w, r, err)
return
}
value, err := h.Read(r.Context(), params)
if err != nil {
api.Send(w, r, err)
return
}
api.Send(w, r, value)
},
Delete: func(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close()
params := request.NewReportDelete()
if err := params.Fill(r); err != nil {
api.Send(w, r, err)
return
}
value, err := h.Delete(r.Context(), params)
if err != nil {
api.Send(w, r, err)
return
}
api.Send(w, r, value)
},
Undelete: func(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close()
params := request.NewReportUndelete()
if err := params.Fill(r); err != nil {
api.Send(w, r, err)
return
}
value, err := h.Undelete(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()
if err := params.Fill(r); err != nil {
api.Send(w, r, err)
return
}
value, err := h.RunFresh(r.Context(), params)
if err != nil {
api.Send(w, r, err)
return
}
api.Send(w, r, value)
},
Run: func(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close()
params := request.NewReportRun()
if err := params.Fill(r); err != nil {
api.Send(w, r, err)
return
}
value, err := h.Run(r.Context(), params)
if err != nil {
api.Send(w, r, err)
return
}
api.Send(w, r, value)
},
}
}
func (h Report) MountRoutes(r chi.Router, middlewares ...func(http.Handler) http.Handler) {
r.Group(func(r chi.Router) {
r.Use(middlewares...)
r.Get("/reports/", h.List)
r.Post("/reports/", h.Create)
r.Put("/reports/{reportID}", h.Update)
r.Get("/reports/{reportID}", h.Read)
r.Delete("/reports/{reportID}", h.Delete)
r.Post("/reports/{reportID}/undelete", h.Undelete)
r.Post("/reports/run", h.RunFresh)
r.Post("/reports/{reportID}/run", h.Run)
})
}

184
system/rest/report.go Normal file
View File

@@ -0,0 +1,184 @@
package rest
import (
"context"
"github.com/cortezaproject/corteza-server/pkg/api"
"github.com/cortezaproject/corteza-server/pkg/filter"
"github.com/cortezaproject/corteza-server/pkg/report"
"github.com/cortezaproject/corteza-server/system/rest/request"
"github.com/cortezaproject/corteza-server/system/service"
"github.com/cortezaproject/corteza-server/system/types"
"github.com/pkg/errors"
)
var _ = errors.Wrap
type (
Report struct {
report reportService
ac reportAccessController
}
reportService interface {
LookupByID(ctx context.Context, ID uint64) (app *types.Report, err error)
Search(ctx context.Context, filter types.ReportFilter) (aa types.ReportSet, f types.ReportFilter, err error)
Create(ctx context.Context, new *types.Report) (app *types.Report, err error)
Update(ctx context.Context, upd *types.Report) (app *types.Report, err error)
Delete(ctx context.Context, ID uint64) (err error)
Undelete(ctx context.Context, ID uint64) (err error)
// @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)
}
reportAccessController interface {
CanGrant(context.Context) bool
CanUpdateReport(context.Context, *types.Report) bool
CanDeleteReport(context.Context, *types.Report) bool
CanRunReport(context.Context, *types.Report) bool
}
reportPayload struct {
*types.Report
CanGrant bool `json:"canGrant"`
CanUpdateReport bool `json:"canUpdateReport"`
CanDeleteReport bool `json:"canDeleteReport"`
CanRunReport bool `json:"canRunReport"`
}
reportSetPayload struct {
Filter types.ReportFilter `json:"filter"`
Set []*reportPayload `json:"set"`
}
reportFramePayload struct {
Frames []*report.Frame `json:"frames"`
}
)
func (Report) New() *Report {
return &Report{
report: service.DefaultReport,
ac: service.DefaultAccessControl,
}
}
func (ctrl *Report) List(ctx context.Context, r *request.ReportList) (interface{}, error) {
var (
err error
f = types.ReportFilter{
Handle: r.Handle,
Labels: r.Labels,
Deleted: filter.State(r.Deleted),
}
)
if f.Paging, err = filter.NewPaging(r.Limit, r.PageCursor); err != nil {
return nil, err
}
if f.Sorting, err = filter.NewSorting(r.Sort); err != nil {
return nil, err
}
set, filter, err := ctrl.report.Search(ctx, f)
return ctrl.makeFilterPayload(ctx, set, filter, err)
}
func (ctrl *Report) Create(ctx context.Context, r *request.ReportCreate) (interface{}, error) {
var (
err error
app = &types.Report{
Handle: r.Handle,
Meta: r.Meta,
Sources: r.Sources,
Projections: r.Projections,
Labels: r.Labels,
}
)
app, err = ctrl.report.Create(ctx, app)
return ctrl.makePayload(ctx, app, err)
}
func (ctrl *Report) Update(ctx context.Context, r *request.ReportUpdate) (interface{}, error) {
var (
err error
app = &types.Report{
ID: r.ReportID,
Handle: r.Handle,
Meta: r.Meta,
Sources: r.Sources,
Projections: r.Projections,
Labels: r.Labels,
}
)
app, err = ctrl.report.Update(ctx, app)
return ctrl.makePayload(ctx, app, err)
}
func (ctrl *Report) Read(ctx context.Context, r *request.ReportRead) (interface{}, error) {
app, err := ctrl.report.LookupByID(ctx, r.ReportID)
return ctrl.makePayload(ctx, app, err)
}
func (ctrl *Report) Delete(ctx context.Context, r *request.ReportDelete) (interface{}, error) {
return api.OK(), ctrl.report.Delete(ctx, r.ReportID)
}
func (ctrl *Report) Undelete(ctx context.Context, r *request.ReportUndelete) (interface{}, error) {
return api.OK(), ctrl.report.Undelete(ctx, r.ReportID)
}
func (ctrl *Report) Run(ctx context.Context, r *request.ReportRun) (interface{}, error) {
// @todo...
return nil, nil
}
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)
}
func (ctrl Report) makePayload(ctx context.Context, m *types.Report, err error) (*reportPayload, error) {
if err != nil || m == nil {
return nil, err
}
return &reportPayload{
Report: m,
CanGrant: ctrl.ac.CanGrant(ctx),
CanUpdateReport: ctrl.ac.CanUpdateReport(ctx, m),
CanDeleteReport: ctrl.ac.CanDeleteReport(ctx, m),
}, nil
}
func (ctrl Report) makeFilterPayload(ctx context.Context, nn types.ReportSet, f types.ReportFilter, err error) (*reportSetPayload, error) {
if err != nil {
return nil, err
}
msp := &reportSetPayload{Filter: f, Set: make([]*reportPayload, len(nn))}
for i := range nn {
msp.Set[i], _ = ctrl.makePayload(ctx, nn[i], nil)
}
return msp, nil
}
func (ctrl Report) makeReportFramePayload(ctx context.Context, ff []*report.Frame, err error) (*reportFramePayload, error) {
if err != nil || len(ff) == 0 {
return nil, err
}
return &reportFramePayload{
Frames: ff,
}, nil
}

View File

@@ -0,0 +1,766 @@
package request
// This file is auto-generated.
//
// Changes to this file may cause incorrect behavior and will be lost if
// the code is regenerated.
//
// Definitions file that controls how this file is generated:
//
import (
"encoding/json"
"fmt"
"github.com/cortezaproject/corteza-server/pkg/label"
"github.com/cortezaproject/corteza-server/pkg/payload"
"github.com/cortezaproject/corteza-server/pkg/report"
"github.com/cortezaproject/corteza-server/system/types"
"github.com/go-chi/chi"
"io"
"mime/multipart"
"net/http"
"strings"
)
// dummy vars to prevent
// unused imports complain
var (
_ = chi.URLParam
_ = multipart.ErrMessageTooLarge
_ = payload.ParseUint64s
_ = strings.ToLower
_ = io.EOF
_ = fmt.Errorf
_ = json.NewEncoder
)
type (
// Internal API interface
ReportList struct {
// Handle GET parameter
//
// Report handle
Handle string
// Deleted GET parameter
//
// Exclude (0, default), include (1) or return only (2) deleted reports
Deleted uint
// Labels GET parameter
//
// Labels
Labels map[string]string
// Limit GET parameter
//
// Limit
Limit uint
// PageCursor GET parameter
//
// Page cursor
PageCursor string
// Sort GET parameter
//
// Sort items
Sort string
}
ReportCreate struct {
// Handle POST parameter
//
// Client handle
Handle string
// Meta POST parameter
//
// Additional info
Meta *types.ReportMeta
// Sources POST parameter
//
// Report source definitions
Sources types.ReportDataSourceSet
// Projections POST parameter
//
// Report projections definition
Projections types.ReportProjectionSet
// Labels POST parameter
//
// Labels
Labels map[string]string
}
ReportUpdate struct {
// ReportID PATH parameter
//
// Report ID
ReportID uint64 `json:",string"`
// Handle POST parameter
//
// Client handle
Handle string
// Meta POST parameter
//
// Additional info
Meta *types.ReportMeta
// Sources POST parameter
//
// Report sources definition
Sources types.ReportDataSourceSet
// Projections POST parameter
//
// Report projections definition
Projections types.ReportProjectionSet
// Labels POST parameter
//
// Labels
Labels map[string]string
}
ReportRead struct {
// ReportID PATH parameter
//
// Report ID
ReportID uint64 `json:",string"`
}
ReportDelete struct {
// ReportID PATH parameter
//
// Report ID
ReportID uint64 `json:",string"`
}
ReportUndelete struct {
// ReportID PATH parameter
//
// Report ID
ReportID uint64 `json:",string"`
}
ReportRunFresh struct {
// Sources POST parameter
//
// Report steps definition
Sources types.ReportDataSourceSet
// Steps POST parameter
//
// Report steps definition
Steps report.StepDefinitionSet
// Frames POST parameter
//
// Report data frame definitions
Frames report.FrameDefinitionSet
}
ReportRun struct {
// ReportID PATH parameter
//
// Report ID
ReportID uint64 `json:",string"`
// Frames POST parameter
//
// Report data frame definitions
Frames report.FrameDefinitionSet
}
)
// NewReportList request
func NewReportList() *ReportList {
return &ReportList{}
}
// Auditable returns all auditable/loggable parameters
func (r ReportList) Auditable() map[string]interface{} {
return map[string]interface{}{
"handle": r.Handle,
"deleted": r.Deleted,
"labels": r.Labels,
"limit": r.Limit,
"pageCursor": r.PageCursor,
"sort": r.Sort,
}
}
// Auditable returns all auditable/loggable parameters
func (r ReportList) GetHandle() string {
return r.Handle
}
// Auditable returns all auditable/loggable parameters
func (r ReportList) GetDeleted() uint {
return r.Deleted
}
// Auditable returns all auditable/loggable parameters
func (r ReportList) GetLabels() map[string]string {
return r.Labels
}
// Auditable returns all auditable/loggable parameters
func (r ReportList) GetLimit() uint {
return r.Limit
}
// Auditable returns all auditable/loggable parameters
func (r ReportList) GetPageCursor() string {
return r.PageCursor
}
// Auditable returns all auditable/loggable parameters
func (r ReportList) GetSort() string {
return r.Sort
}
// Fill processes request and fills internal variables
func (r *ReportList) Fill(req *http.Request) (err error) {
{
// GET params
tmp := req.URL.Query()
if val, ok := tmp["handle"]; ok && len(val) > 0 {
r.Handle, err = val[0], nil
if err != nil {
return err
}
}
if val, ok := tmp["deleted"]; ok && len(val) > 0 {
r.Deleted, err = payload.ParseUint(val[0]), nil
if err != nil {
return err
}
}
if val, ok := tmp["labels[]"]; ok {
r.Labels, err = label.ParseStrings(val)
if err != nil {
return err
}
} else if val, ok := tmp["labels"]; ok {
r.Labels, err = label.ParseStrings(val)
if err != nil {
return err
}
}
if val, ok := tmp["limit"]; ok && len(val) > 0 {
r.Limit, err = payload.ParseUint(val[0]), nil
if err != nil {
return err
}
}
if val, ok := tmp["pageCursor"]; ok && len(val) > 0 {
r.PageCursor, err = val[0], nil
if err != nil {
return err
}
}
if val, ok := tmp["sort"]; ok && len(val) > 0 {
r.Sort, err = val[0], nil
if err != nil {
return err
}
}
}
return err
}
// NewReportCreate request
func NewReportCreate() *ReportCreate {
return &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,
"projections": r.Projections,
"labels": r.Labels,
}
}
// Auditable returns all auditable/loggable parameters
func (r ReportCreate) GetHandle() string {
return r.Handle
}
// Auditable returns all auditable/loggable parameters
func (r ReportCreate) GetMeta() *types.ReportMeta {
return r.Meta
}
// Auditable returns all auditable/loggable parameters
func (r ReportCreate) GetSources() types.ReportDataSourceSet {
return r.Sources
}
// Auditable returns all auditable/loggable parameters
func (r ReportCreate) GetProjections() types.ReportProjectionSet {
return r.Projections
}
// Auditable returns all auditable/loggable parameters
func (r ReportCreate) GetLabels() map[string]string {
return r.Labels
}
// Fill processes request and fills internal variables
func (r *ReportCreate) 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["handle"]; ok && len(val) > 0 {
r.Handle, err = val[0], nil
if err != nil {
return err
}
}
if val, ok := req.Form["meta[]"]; ok {
r.Meta, err = types.ParseReportMeta(val)
if err != nil {
return err
}
} else if val, ok := req.Form["meta"]; ok {
r.Meta, err = types.ParseReportMeta(val)
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 {
// return err
// }
//}
//if val, ok := req.Form["projections[]"]; ok && len(val) > 0 {
// r.Projections, err = types.ReportProjectionSet(val), nil
// if err != nil {
// return err
// }
//}
if val, ok := req.Form["labels[]"]; ok {
r.Labels, err = label.ParseStrings(val)
if err != nil {
return err
}
} else if val, ok := req.Form["labels"]; ok {
r.Labels, err = label.ParseStrings(val)
if err != nil {
return err
}
}
}
return err
}
// NewReportUpdate request
func NewReportUpdate() *ReportUpdate {
return &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,
"projections": r.Projections,
"labels": r.Labels,
}
}
// Auditable returns all auditable/loggable parameters
func (r ReportUpdate) GetReportID() uint64 {
return r.ReportID
}
// Auditable returns all auditable/loggable parameters
func (r ReportUpdate) GetHandle() string {
return r.Handle
}
// Auditable returns all auditable/loggable parameters
func (r ReportUpdate) GetMeta() *types.ReportMeta {
return r.Meta
}
// Auditable returns all auditable/loggable parameters
func (r ReportUpdate) GetSources() types.ReportDataSourceSet {
return r.Sources
}
// Auditable returns all auditable/loggable parameters
func (r ReportUpdate) GetProjections() types.ReportProjectionSet {
return r.Projections
}
// Auditable returns all auditable/loggable parameters
func (r ReportUpdate) GetLabels() map[string]string {
return r.Labels
}
// Fill processes request and fills internal variables
func (r *ReportUpdate) 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["handle"]; ok && len(val) > 0 {
r.Handle, err = val[0], nil
if err != nil {
return err
}
}
if val, ok := req.Form["meta[]"]; ok {
r.Meta, err = types.ParseReportMeta(val)
if err != nil {
return err
}
} else if val, ok := req.Form["meta"]; ok {
r.Meta, err = types.ParseReportMeta(val)
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 {
// return err
// }
//}
//if val, ok := req.Form["projections[]"]; ok && len(val) > 0 {
// r.Projections, err = types.ReportProjectionSet(val), nil
// if err != nil {
// return err
// }
//}
if val, ok := req.Form["labels[]"]; ok {
r.Labels, err = label.ParseStrings(val)
if err != nil {
return err
}
} else if val, ok := req.Form["labels"]; ok {
r.Labels, err = label.ParseStrings(val)
if err != nil {
return err
}
}
}
{
var val string
// path params
val = chi.URLParam(req, "reportID")
r.ReportID, err = payload.ParseUint64(val), nil
if err != nil {
return err
}
}
return err
}
// NewReportRead request
func NewReportRead() *ReportRead {
return &ReportRead{}
}
// Auditable returns all auditable/loggable parameters
func (r ReportRead) Auditable() map[string]interface{} {
return map[string]interface{}{
"reportID": r.ReportID,
}
}
// Auditable returns all auditable/loggable parameters
func (r ReportRead) GetReportID() uint64 {
return r.ReportID
}
// Fill processes request and fills internal variables
func (r *ReportRead) Fill(req *http.Request) (err error) {
{
var val string
// path params
val = chi.URLParam(req, "reportID")
r.ReportID, err = payload.ParseUint64(val), nil
if err != nil {
return err
}
}
return err
}
// NewReportDelete request
func NewReportDelete() *ReportDelete {
return &ReportDelete{}
}
// Auditable returns all auditable/loggable parameters
func (r ReportDelete) Auditable() map[string]interface{} {
return map[string]interface{}{
"reportID": r.ReportID,
}
}
// Auditable returns all auditable/loggable parameters
func (r ReportDelete) GetReportID() uint64 {
return r.ReportID
}
// Fill processes request and fills internal variables
func (r *ReportDelete) Fill(req *http.Request) (err error) {
{
var val string
// path params
val = chi.URLParam(req, "reportID")
r.ReportID, err = payload.ParseUint64(val), nil
if err != nil {
return err
}
}
return err
}
// NewReportUndelete request
func NewReportUndelete() *ReportUndelete {
return &ReportUndelete{}
}
// Auditable returns all auditable/loggable parameters
func (r ReportUndelete) Auditable() map[string]interface{} {
return map[string]interface{}{
"reportID": r.ReportID,
}
}
// Auditable returns all auditable/loggable parameters
func (r ReportUndelete) GetReportID() uint64 {
return r.ReportID
}
// Fill processes request and fills internal variables
func (r *ReportUndelete) Fill(req *http.Request) (err error) {
{
var val string
// path params
val = chi.URLParam(req, "reportID")
r.ReportID, err = payload.ParseUint64(val), nil
if err != nil {
return err
}
}
return err
}
// NewReportRunFresh request
func NewReportRunFresh() *ReportRunFresh {
return &ReportRunFresh{}
}
// Auditable returns all auditable/loggable parameters
func (r ReportRunFresh) Auditable() map[string]interface{} {
return map[string]interface{}{
"sources": r.Sources,
"steps": r.Steps,
"frames": r.Frames,
}
}
// Auditable returns all auditable/loggable parameters
func (r ReportRunFresh) GetSources() types.ReportDataSourceSet {
return r.Sources
}
// Auditable returns all auditable/loggable parameters
func (r ReportRunFresh) GetSteps() report.StepDefinitionSet {
return r.Steps
}
// Auditable returns all auditable/loggable parameters
func (r ReportRunFresh) GetFrames() report.FrameDefinitionSet {
return r.Frames
}
// Fill processes request and fills internal variables
func (r *ReportRunFresh) 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["frames[]"]; ok && len(val) > 0 {
// r.Frames, err = report.FrameDefinitionSet(val), nil
// if err != nil {
// return err
// }
//}
}
return err
}
// NewReportRun request
func NewReportRun() *ReportRun {
return &ReportRun{}
}
// Auditable returns all auditable/loggable parameters
func (r ReportRun) Auditable() map[string]interface{} {
return map[string]interface{}{
"reportID": r.ReportID,
"frames": r.Frames,
}
}
// Auditable returns all auditable/loggable parameters
func (r ReportRun) GetReportID() uint64 {
return r.ReportID
}
// Auditable returns all auditable/loggable parameters
func (r ReportRun) GetFrames() report.FrameDefinitionSet {
return r.Frames
}
// Fill processes request and fills internal variables
func (r *ReportRun) 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["frames[]"]; ok && len(val) > 0 {
// r.Frames, err = report.FrameDefinitionSet(val), nil
// if err != nil {
// return err
// }
//}
}
{
var val string
// path params
val = chi.URLParam(req, "reportID")
r.ReportID, err = payload.ParseUint64(val), nil
if err != nil {
return err
}
}
return err
}

View File

@@ -32,6 +32,7 @@ func MountRoutes(r chi.Router) {
handlers.NewPermissions(Permissions{}.New()).MountRoutes(r)
handlers.NewApplication(Application{}.New()).MountRoutes(r)
handlers.NewTemplate(Template{}.New()).MountRoutes(r)
handlers.NewReport(Report{}.New()).MountRoutes(r)
handlers.NewSettings(Settings{}.New()).MountRoutes(r)
handlers.NewStats(Stats{}.New()).MountRoutes(r)
handlers.NewReminder(Reminder{}.New()).MountRoutes(r)

View File

@@ -11,6 +11,7 @@ package service
// - system.apigw-route.yaml
// - system.application.yaml
// - system.auth-client.yaml
// - system.report.yaml
// - system.role.yaml
// - system.template.yaml
// - system.user.yaml
@@ -128,6 +129,26 @@ func (svc accessControl) List() (out []map[string]string) {
"any": types.AuthClientRbacResource(0),
"op": "authorize",
},
{
"type": types.ReportResourceType,
"any": types.ReportRbacResource(0),
"op": "read",
},
{
"type": types.ReportResourceType,
"any": types.ReportRbacResource(0),
"op": "update",
},
{
"type": types.ReportResourceType,
"any": types.ReportRbacResource(0),
"op": "delete",
},
{
"type": types.ReportResourceType,
"any": types.ReportRbacResource(0),
"op": "run",
},
{
"type": types.RoleResourceType,
"any": types.RoleRbacResource(0),
@@ -288,6 +309,16 @@ func (svc accessControl) List() (out []map[string]string) {
"any": types.ComponentRbacResource(),
"op": "templates.search",
},
{
"type": types.ComponentResourceType,
"any": types.ComponentRbacResource(),
"op": "report.create",
},
{
"type": types.ComponentResourceType,
"any": types.ComponentRbacResource(),
"op": "reports.search",
},
{
"type": types.ComponentResourceType,
"any": types.ComponentRbacResource(),
@@ -476,6 +507,34 @@ func (svc accessControl) CanAuthorizeAuthClient(ctx context.Context, r *types.Au
return svc.can(ctx, "authorize", r)
}
// CanReadReport checks if current user can read report
//
// This function is auto-generated
func (svc accessControl) CanReadReport(ctx context.Context, r *types.Report) bool {
return svc.can(ctx, "read", r)
}
// CanUpdateReport checks if current user can update report
//
// This function is auto-generated
func (svc accessControl) CanUpdateReport(ctx context.Context, r *types.Report) bool {
return svc.can(ctx, "update", r)
}
// CanDeleteReport checks if current user can delete report
//
// This function is auto-generated
func (svc accessControl) CanDeleteReport(ctx context.Context, r *types.Report) bool {
return svc.can(ctx, "delete", r)
}
// CanRunReport checks if current user can run report
//
// This function is auto-generated
func (svc accessControl) CanRunReport(ctx context.Context, r *types.Report) bool {
return svc.can(ctx, "run", r)
}
// CanReadRole checks if current user can read role
//
// This function is auto-generated
@@ -700,6 +759,20 @@ func (svc accessControl) CanSearchTemplates(ctx context.Context) bool {
return svc.can(ctx, "templates.search", &types.Component{})
}
// CanCreateReport checks if current user can create report
//
// This function is auto-generated
func (svc accessControl) CanCreateReport(ctx context.Context) bool {
return svc.can(ctx, "report.create", &types.Component{})
}
// CanSearchReports checks if current user can list, search or filter reports
//
// This function is auto-generated
func (svc accessControl) CanSearchReports(ctx context.Context) bool {
return svc.can(ctx, "reports.search", &types.Component{})
}
// CanAssignReminder checks if current user can assign reminders
//
// This function is auto-generated
@@ -762,6 +835,8 @@ func rbacResourceValidator(r string, oo ...string) error {
return rbacApplicationResourceValidator(r, oo...)
case types.AuthClientResourceType:
return rbacAuthClientResourceValidator(r, oo...)
case types.ReportResourceType:
return rbacReportResourceValidator(r, oo...)
case types.RoleResourceType:
return rbacRoleResourceValidator(r, oo...)
case types.TemplateResourceType:
@@ -805,6 +880,13 @@ func rbacResourceOperations(r string) map[string]bool {
"delete": true,
"authorize": true,
}
case types.ReportResourceType:
return map[string]bool{
"read": true,
"update": true,
"delete": true,
"run": true,
}
case types.RoleResourceType:
return map[string]bool{
"read": true,
@@ -848,6 +930,8 @@ func rbacResourceOperations(r string) map[string]bool {
"application.flag.global": true,
"template.create": true,
"templates.search": true,
"report.create": true,
"reports.search": true,
"reminder.assign": true,
"queue.create": true,
"queues.search": true,
@@ -1037,6 +1121,57 @@ func rbacAuthClientResourceValidator(r string, oo ...string) error {
return nil
}
// rbacReportResourceValidator checks validity of rbac resource and operations
//
// Can be called without operations to check for validity of resource string only
//
// This function is auto-generated
func rbacReportResourceValidator(r string, oo ...string) error {
defOps := rbacResourceOperations(r)
for _, o := range oo {
if !defOps[o] {
return fmt.Errorf("invalid operation '%s' for system Report resource", o)
}
}
if !strings.HasPrefix(r, types.ReportResourceType) {
// expecting resource to always include path
return fmt.Errorf("invalid resource type")
}
const sep = "/"
var (
specIdUsed = true
pp = strings.Split(strings.Trim(r[len(types.ReportResourceType):], sep), sep)
prc = []string{
"ID",
}
)
if len(pp) != len(prc) {
return fmt.Errorf("invalid resource path structure")
}
for i, p := range pp {
if p == "*" {
if !specIdUsed {
return fmt.Errorf("invalid resource path wildcard level (%d) for Report", i)
}
specIdUsed = false
continue
}
specIdUsed = true
if _, err := cast.ToUint64E(p); err != nil {
return fmt.Errorf("invalid reference for %s: '%s'", prc[i], p)
}
}
return nil
}
// rbacRoleResourceValidator checks validity of rbac resource and operations
//
// Can be called without operations to check for validity of resource string only

342
system/service/report.go Normal file
View File

@@ -0,0 +1,342 @@
package service
import (
"context"
"github.com/cortezaproject/corteza-server/pkg/actionlog"
"github.com/cortezaproject/corteza-server/pkg/label"
rep "github.com/cortezaproject/corteza-server/pkg/report"
"github.com/cortezaproject/corteza-server/store"
"github.com/cortezaproject/corteza-server/system/types"
)
type (
report struct {
ac reportAccessController
eventbus eventDispatcher
actionlog actionlog.Recorder
store store.Storer
}
reportAccessController interface {
CanSearchReports(context.Context) bool
CanCreateReport(context.Context) bool
CanReadReport(context.Context, *types.Report) bool
CanUpdateReport(context.Context, *types.Report) bool
CanDeleteReport(context.Context, *types.Report) bool
CanRunReport(context.Context, *types.Report) bool
}
)
var (
reporters = make(map[string]rep.DatasourceProvider)
)
// Report is a default report service initializer
func Report(s store.Storer, ac reportAccessController, al actionlog.Recorder, eb eventDispatcher) *report {
return &report{store: s, ac: ac, actionlog: al, eventbus: eb}
}
func (svc *report) RegisterReporter(key string, r rep.DatasourceProvider) {
reporters[key] = r
}
func (svc *report) LookupByID(ctx context.Context, ID uint64) (report *types.Report, err error) {
var (
aaProps = &reportActionProps{report: &types.Report{ID: ID}}
)
err = func() error {
if ID == 0 {
return ReportErrInvalidID()
}
if report, err = store.LookupReportByID(ctx, svc.store, ID); err != nil {
return ReportErrInvalidID().Wrap(err)
}
if !svc.ac.CanReadReport(ctx, report) {
return ReportErrNotAllowedToRead()
}
return nil
}()
return report, svc.recordAction(ctx, aaProps, ReportActionLookup, err)
}
func (svc *report) Search(ctx context.Context, rf types.ReportFilter) (rr types.ReportSet, f types.ReportFilter, err error) {
var (
aaProps = &reportActionProps{filter: &rf}
)
// For each fetched item, store backend will check if it is valid or not
rf.Check = func(res *types.Report) (bool, error) {
if !svc.ac.CanReadReport(ctx, res) {
return false, nil
}
return true, nil
}
err = func() error {
if !svc.ac.CanSearchReports(ctx) {
return ReportErrNotAllowedToSearch()
}
if len(rf.Labels) > 0 {
rf.LabeledIDs, err = label.Search(
ctx,
svc.store,
types.Report{}.LabelResourceKind(),
rf.Labels,
)
if err != nil {
return err
}
// labels specified but no labeled resources found
if len(rf.LabeledIDs) == 0 {
return nil
}
}
if rr, f, err = store.SearchReports(ctx, svc.store, rf); err != nil {
return err
}
if err = label.Load(ctx, svc.store, toLabeledReports(rr)...); err != nil {
return err
}
return nil
}()
return rr, f, svc.recordAction(ctx, aaProps, ReportActionSearch, err)
}
func (svc *report) Create(ctx context.Context, new *types.Report) (report *types.Report, err error) {
var (
aaProps = &reportActionProps{new: new}
)
err = func() (err error) {
if !svc.ac.CanCreateReport(ctx) {
return ReportErrNotAllowedToCreate()
}
// if err = svc.eventbus.WaitFor(ctx, event.ReportBeforeCreate(new, nil)); err != nil {
// return
// }
// Set new values after beforeCreate events are emitted
new.ID = nextID()
new.CreatedAt = *now()
if new.Meta == nil {
new.Meta = &types.ReportMeta{}
}
if err = store.CreateReport(ctx, svc.store, new); err != nil {
return
}
if err = label.Create(ctx, svc.store, new); err != nil {
return
}
report = new
// _ = svc.eventbus.WaitFor(ctx, event.ReportAfterCreate(new, nil))
return nil
}()
return report, svc.recordAction(ctx, aaProps, ReportActionCreate, err)
}
func (svc *report) Update(ctx context.Context, upd *types.Report) (report *types.Report, err error) {
var (
aaProps = &reportActionProps{update: upd}
)
err = func() (err error) {
if upd.ID == 0 {
return ReportErrInvalidID()
}
if report, err = store.LookupReportByID(ctx, svc.store, upd.ID); err != nil {
return
}
aaProps.setReport(report)
if !svc.ac.CanUpdateReport(ctx, report) {
return ReportErrNotAllowedToUpdate()
}
// if err = svc.eventbus.WaitFor(ctx, event.ReportBeforeUpdate(upd, report)); err != nil {
// return
// }
// Assign changed values after afterUpdate events are emitted
report.Handle = upd.Handle
report.Sources = upd.Sources
report.UpdatedAt = now()
if upd.Meta != nil {
report.Meta = upd.Meta
}
if err = store.UpdateReport(ctx, svc.store, report); err != nil {
return err
}
if label.Changed(report.Labels, upd.Labels) {
if err = label.Update(ctx, svc.store, upd); err != nil {
return
}
report.Labels = upd.Labels
}
// _ = svc.eventbus.WaitFor(ctx, event.ReportAfterUpdate(upd, report))
return nil
}()
return report, svc.recordAction(ctx, aaProps, ReportActionUpdate, err)
}
func (svc *report) Delete(ctx context.Context, ID uint64) (err error) {
var (
aaProps = &reportActionProps{}
report *types.Report
)
err = func() (err error) {
if ID == 0 {
return ReportErrInvalidID()
}
if report, err = store.LookupReportByID(ctx, svc.store, ID); err != nil {
return
}
aaProps.setReport(report)
if !svc.ac.CanDeleteReport(ctx, report) {
return ReportErrNotAllowedToDelete()
}
// if err = svc.eventbus.WaitFor(ctx, event.ReportBeforeDelete(nil, report)); err != nil {
// return
// }
report.DeletedAt = now()
if err = store.UpdateReport(ctx, svc.store, report); err != nil {
return
}
// _ = svc.eventbus.WaitFor(ctx, event.ReportAfterDelete(nil, report))
return nil
}()
return svc.recordAction(ctx, aaProps, ReportActionDelete, err)
}
func (svc *report) Undelete(ctx context.Context, ID uint64) (err error) {
var (
aaProps = &reportActionProps{}
report *types.Report
)
err = func() (err error) {
if ID == 0 {
return ReportErrInvalidID()
}
if report, err = store.LookupReportByID(ctx, svc.store, ID); err != nil {
return
}
aaProps.setReport(report)
if !svc.ac.CanDeleteReport(ctx, report) {
return ReportErrNotAllowedToUndelete()
}
// if err = svc.eventbus.WaitFor(ctx, event.ReportBeforeUndelete(nil, app)); err != nil {
// return
// }
report.DeletedAt = nil
if err = store.UpdateReport(ctx, svc.store, report); err != nil {
return
}
// _ = svc.eventbus.WaitFor(ctx, event.ReportAfterUndelete(nil, app))
return nil
}()
return svc.recordAction(ctx, aaProps, ReportActionUndelete, err)
}
func (svc *report) Run(ctx context.Context, ID uint64, dd rep.FrameDefinitionSet) (rr interface{}, err error) {
// @todo follow the RunFresh definition here
return nil, nil
}
func (svc *report) RunFresh(ctx context.Context, src types.ReportDataSourceSet, st rep.StepDefinitionSet, dd rep.FrameDefinitionSet) (out []*rep.Frame, err error) {
var (
aaProps = &reportActionProps{}
)
out = make([]*rep.Frame, 0, 4)
err = func() (err error) {
// if err = svc.eventbus.WaitFor(ctx, event.ReportBeforeUpdate(upd, report)); err != nil {
// return
// }
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
}
ff, err := model.Load(ctx, dd...)
if err != nil {
return err
}
out = append(out, ff...)
// _ = svc.eventbus.WaitFor(ctx, event.ReportAfterUpdate(upd, report))
// return nil
return nil
}()
return out, svc.recordAction(ctx, aaProps, ReportActionRun, err)
}
// toLabeledReports converts to []label.LabeledResource
//
// This function is auto-generated.
func toLabeledReports(set []*types.Report) []label.LabeledResource {
if len(set) == 0 {
return nil
}
ll := make([]label.LabeledResource, len(set))
for i := range set {
ll[i] = set[i]
}
return ll
}

780
system/service/report_actions.gen.go generated Normal file
View File

@@ -0,0 +1,780 @@
package service
// This file is auto-generated.
//
// Changes to this file may cause incorrect behavior and will be lost if
// the code is regenerated.
//
// Definitions file that controls how this file is generated:
// system/service/report_actions.yaml
import (
"context"
"fmt"
"github.com/cortezaproject/corteza-server/pkg/actionlog"
"github.com/cortezaproject/corteza-server/pkg/errors"
"github.com/cortezaproject/corteza-server/system/types"
"strings"
"time"
)
type (
reportActionProps struct {
report *types.Report
new *types.Report
update *types.Report
filter *types.ReportFilter
}
reportAction struct {
timestamp time.Time
resource string
action string
log string
severity actionlog.Severity
// prefix for error when action fails
errorMessage string
props *reportActionProps
}
reportLogMetaKey struct{}
reportPropsMetaKey struct{}
)
var (
// just a placeholder to cover template cases w/o fmt package use
_ = fmt.Println
)
// *********************************************************************************************************************
// *********************************************************************************************************************
// Props methods
// setReport updates reportActionProps's report
//
// Allows method chaining
//
// This function is auto-generated.
//
func (p *reportActionProps) setReport(report *types.Report) *reportActionProps {
p.report = report
return p
}
// setNew updates reportActionProps's new
//
// Allows method chaining
//
// This function is auto-generated.
//
func (p *reportActionProps) setNew(new *types.Report) *reportActionProps {
p.new = new
return p
}
// setUpdate updates reportActionProps's update
//
// Allows method chaining
//
// This function is auto-generated.
//
func (p *reportActionProps) setUpdate(update *types.Report) *reportActionProps {
p.update = update
return p
}
// setFilter updates reportActionProps's filter
//
// Allows method chaining
//
// This function is auto-generated.
//
func (p *reportActionProps) setFilter(filter *types.ReportFilter) *reportActionProps {
p.filter = filter
return p
}
// Serialize converts reportActionProps to actionlog.Meta
//
// This function is auto-generated.
//
func (p reportActionProps) Serialize() actionlog.Meta {
var (
m = make(actionlog.Meta)
)
if p.report != nil {
m.Set("report.handle", p.report.Handle, true)
m.Set("report.ID", p.report.ID, true)
}
if p.new != nil {
m.Set("new.handle", p.new.Handle, true)
m.Set("new.ID", p.new.ID, true)
}
if p.update != nil {
m.Set("update.handle", p.update.Handle, true)
m.Set("update.ID", p.update.ID, true)
}
if p.filter != nil {
m.Set("filter.handle", p.filter.Handle, true)
m.Set("filter.deleted", p.filter.Deleted, true)
m.Set("filter.sort", p.filter.Sort, true)
}
return m
}
// tr translates string and replaces meta value placeholder with values
//
// This function is auto-generated.
//
func (p reportActionProps) Format(in string, err error) string {
var (
pairs = []string{"{err}"}
// first non-empty string
fns = func(ii ...interface{}) string {
for _, i := range ii {
if s := fmt.Sprintf("%v", i); len(s) > 0 {
return s
}
}
return ""
}
)
if err != nil {
pairs = append(pairs, err.Error())
} else {
pairs = append(pairs, "nil")
}
if p.report != nil {
// replacement for "{report}" (in order how fields are defined)
pairs = append(
pairs,
"{report}",
fns(
p.report.Handle,
p.report.ID,
),
)
pairs = append(pairs, "{report.handle}", fns(p.report.Handle))
pairs = append(pairs, "{report.ID}", fns(p.report.ID))
}
if p.new != nil {
// replacement for "{new}" (in order how fields are defined)
pairs = append(
pairs,
"{new}",
fns(
p.new.Handle,
p.new.ID,
),
)
pairs = append(pairs, "{new.handle}", fns(p.new.Handle))
pairs = append(pairs, "{new.ID}", fns(p.new.ID))
}
if p.update != nil {
// replacement for "{update}" (in order how fields are defined)
pairs = append(
pairs,
"{update}",
fns(
p.update.Handle,
p.update.ID,
),
)
pairs = append(pairs, "{update.handle}", fns(p.update.Handle))
pairs = append(pairs, "{update.ID}", fns(p.update.ID))
}
if p.filter != nil {
// replacement for "{filter}" (in order how fields are defined)
pairs = append(
pairs,
"{filter}",
fns(
p.filter.Handle,
p.filter.Deleted,
p.filter.Sort,
),
)
pairs = append(pairs, "{filter.handle}", fns(p.filter.Handle))
pairs = append(pairs, "{filter.deleted}", fns(p.filter.Deleted))
pairs = append(pairs, "{filter.sort}", fns(p.filter.Sort))
}
return strings.NewReplacer(pairs...).Replace(in)
}
// *********************************************************************************************************************
// *********************************************************************************************************************
// Action methods
// String returns loggable description as string
//
// This function is auto-generated.
//
func (a *reportAction) String() string {
var props = &reportActionProps{}
if a.props != nil {
props = a.props
}
return props.Format(a.log, nil)
}
func (e *reportAction) ToAction() *actionlog.Action {
return &actionlog.Action{
Resource: e.resource,
Action: e.action,
Severity: e.severity,
Description: e.String(),
Meta: e.props.Serialize(),
}
}
// *********************************************************************************************************************
// *********************************************************************************************************************
// Action constructors
// ReportActionSearch returns "system:report.search" action
//
// This function is auto-generated.
//
func ReportActionSearch(props ...*reportActionProps) *reportAction {
a := &reportAction{
timestamp: time.Now(),
resource: "system:report",
action: "search",
log: "searched for reports",
severity: actionlog.Info,
}
if len(props) > 0 {
a.props = props[0]
}
return a
}
// ReportActionLookup returns "system:report.lookup" action
//
// This function is auto-generated.
//
func ReportActionLookup(props ...*reportActionProps) *reportAction {
a := &reportAction{
timestamp: time.Now(),
resource: "system:report",
action: "lookup",
log: "looked-up for a {report}",
severity: actionlog.Info,
}
if len(props) > 0 {
a.props = props[0]
}
return a
}
// ReportActionCreate returns "system:report.create" action
//
// This function is auto-generated.
//
func ReportActionCreate(props ...*reportActionProps) *reportAction {
a := &reportAction{
timestamp: time.Now(),
resource: "system:report",
action: "create",
log: "created {report}",
severity: actionlog.Notice,
}
if len(props) > 0 {
a.props = props[0]
}
return a
}
// ReportActionUpdate returns "system:report.update" action
//
// This function is auto-generated.
//
func ReportActionUpdate(props ...*reportActionProps) *reportAction {
a := &reportAction{
timestamp: time.Now(),
resource: "system:report",
action: "update",
log: "updated {report}",
severity: actionlog.Notice,
}
if len(props) > 0 {
a.props = props[0]
}
return a
}
// ReportActionDelete returns "system:report.delete" action
//
// This function is auto-generated.
//
func ReportActionDelete(props ...*reportActionProps) *reportAction {
a := &reportAction{
timestamp: time.Now(),
resource: "system:report",
action: "delete",
log: "deleted {report}",
severity: actionlog.Notice,
}
if len(props) > 0 {
a.props = props[0]
}
return a
}
// ReportActionUndelete returns "system:report.undelete" action
//
// This function is auto-generated.
//
func ReportActionUndelete(props ...*reportActionProps) *reportAction {
a := &reportAction{
timestamp: time.Now(),
resource: "system:report",
action: "undelete",
log: "undeleted {report}",
severity: actionlog.Notice,
}
if len(props) > 0 {
a.props = props[0]
}
return a
}
// ReportActionRun returns "system:report.run" action
//
// This function is auto-generated.
//
func ReportActionRun(props ...*reportActionProps) *reportAction {
a := &reportAction{
timestamp: time.Now(),
resource: "system:report",
action: "run",
log: "report ran",
severity: actionlog.Notice,
}
if len(props) > 0 {
a.props = props[0]
}
return a
}
// *********************************************************************************************************************
// *********************************************************************************************************************
// Error constructors
// ReportErrGeneric returns "system:report.generic" as *errors.Error
//
//
// This function is auto-generated.
//
func ReportErrGeneric(mm ...*reportActionProps) *errors.Error {
var p = &reportActionProps{}
if len(mm) > 0 {
p = mm[0]
}
var e = errors.New(
errors.KindInternal,
p.Format("failed to complete request due to internal error", nil),
errors.Meta("type", "generic"),
errors.Meta("resource", "system:report"),
// action log entry; no formatting, it will be applied inside recordAction fn.
errors.Meta(reportLogMetaKey{}, "{err}"),
errors.Meta(reportPropsMetaKey{}, p),
errors.StackSkip(1),
)
if len(mm) > 0 {
}
return e
}
// ReportErrNotFound returns "system:report.notFound" as *errors.Error
//
//
// This function is auto-generated.
//
func ReportErrNotFound(mm ...*reportActionProps) *errors.Error {
var p = &reportActionProps{}
if len(mm) > 0 {
p = mm[0]
}
var e = errors.New(
errors.KindInternal,
p.Format("report not found", nil),
errors.Meta("type", "notFound"),
errors.Meta("resource", "system:report"),
errors.Meta(reportPropsMetaKey{}, p),
errors.StackSkip(1),
)
if len(mm) > 0 {
}
return e
}
// ReportErrInvalidID returns "system:report.invalidID" as *errors.Error
//
//
// This function is auto-generated.
//
func ReportErrInvalidID(mm ...*reportActionProps) *errors.Error {
var p = &reportActionProps{}
if len(mm) > 0 {
p = mm[0]
}
var e = errors.New(
errors.KindInternal,
p.Format("invalid ID", nil),
errors.Meta("type", "invalidID"),
errors.Meta("resource", "system:report"),
errors.Meta(reportPropsMetaKey{}, p),
errors.StackSkip(1),
)
if len(mm) > 0 {
}
return e
}
// ReportErrNotAllowedToRead returns "system:report.notAllowedToRead" as *errors.Error
//
//
// This function is auto-generated.
//
func ReportErrNotAllowedToRead(mm ...*reportActionProps) *errors.Error {
var p = &reportActionProps{}
if len(mm) > 0 {
p = mm[0]
}
var e = errors.New(
errors.KindInternal,
p.Format("not allowed to read this report", nil),
errors.Meta("type", "notAllowedToRead"),
errors.Meta("resource", "system:report"),
// action log entry; no formatting, it will be applied inside recordAction fn.
errors.Meta(reportLogMetaKey{}, "failed to read {report}; insufficient permissions"),
errors.Meta(reportPropsMetaKey{}, p),
errors.StackSkip(1),
)
if len(mm) > 0 {
}
return e
}
// ReportErrNotAllowedToSearch returns "system:report.notAllowedToSearch" as *errors.Error
//
//
// This function is auto-generated.
//
func ReportErrNotAllowedToSearch(mm ...*reportActionProps) *errors.Error {
var p = &reportActionProps{}
if len(mm) > 0 {
p = mm[0]
}
var e = errors.New(
errors.KindInternal,
p.Format("not allowed to list or search reports", nil),
errors.Meta("type", "notAllowedToSearch"),
errors.Meta("resource", "system:report"),
// action log entry; no formatting, it will be applied inside recordAction fn.
errors.Meta(reportLogMetaKey{}, "failed to search for reports; insufficient permissions"),
errors.Meta(reportPropsMetaKey{}, p),
errors.StackSkip(1),
)
if len(mm) > 0 {
}
return e
}
// ReportErrNotAllowedToListReports returns "system:report.notAllowedToListReports" as *errors.Error
//
//
// This function is auto-generated.
//
func ReportErrNotAllowedToListReports(mm ...*reportActionProps) *errors.Error {
var p = &reportActionProps{}
if len(mm) > 0 {
p = mm[0]
}
var e = errors.New(
errors.KindInternal,
p.Format("not allowed to list reports", nil),
errors.Meta("type", "notAllowedToListReports"),
errors.Meta("resource", "system:report"),
// action log entry; no formatting, it will be applied inside recordAction fn.
errors.Meta(reportLogMetaKey{}, "failed to list report; insufficient permissions"),
errors.Meta(reportPropsMetaKey{}, p),
errors.StackSkip(1),
)
if len(mm) > 0 {
}
return e
}
// ReportErrNotAllowedToCreate returns "system:report.notAllowedToCreate" as *errors.Error
//
//
// This function is auto-generated.
//
func ReportErrNotAllowedToCreate(mm ...*reportActionProps) *errors.Error {
var p = &reportActionProps{}
if len(mm) > 0 {
p = mm[0]
}
var e = errors.New(
errors.KindInternal,
p.Format("not allowed to create reports", nil),
errors.Meta("type", "notAllowedToCreate"),
errors.Meta("resource", "system:report"),
// action log entry; no formatting, it will be applied inside recordAction fn.
errors.Meta(reportLogMetaKey{}, "failed to create report; insufficient permissions"),
errors.Meta(reportPropsMetaKey{}, p),
errors.StackSkip(1),
)
if len(mm) > 0 {
}
return e
}
// ReportErrNotAllowedToUpdate returns "system:report.notAllowedToUpdate" as *errors.Error
//
//
// This function is auto-generated.
//
func ReportErrNotAllowedToUpdate(mm ...*reportActionProps) *errors.Error {
var p = &reportActionProps{}
if len(mm) > 0 {
p = mm[0]
}
var e = errors.New(
errors.KindInternal,
p.Format("not allowed to update this report", nil),
errors.Meta("type", "notAllowedToUpdate"),
errors.Meta("resource", "system:report"),
// action log entry; no formatting, it will be applied inside recordAction fn.
errors.Meta(reportLogMetaKey{}, "failed to update {report}; insufficient permissions"),
errors.Meta(reportPropsMetaKey{}, p),
errors.StackSkip(1),
)
if len(mm) > 0 {
}
return e
}
// ReportErrNotAllowedToDelete returns "system:report.notAllowedToDelete" as *errors.Error
//
//
// This function is auto-generated.
//
func ReportErrNotAllowedToDelete(mm ...*reportActionProps) *errors.Error {
var p = &reportActionProps{}
if len(mm) > 0 {
p = mm[0]
}
var e = errors.New(
errors.KindInternal,
p.Format("not allowed to delete this report", nil),
errors.Meta("type", "notAllowedToDelete"),
errors.Meta("resource", "system:report"),
// action log entry; no formatting, it will be applied inside recordAction fn.
errors.Meta(reportLogMetaKey{}, "failed to delete {report}; insufficient permissions"),
errors.Meta(reportPropsMetaKey{}, p),
errors.StackSkip(1),
)
if len(mm) > 0 {
}
return e
}
// ReportErrNotAllowedToUndelete returns "system:report.notAllowedToUndelete" as *errors.Error
//
//
// This function is auto-generated.
//
func ReportErrNotAllowedToUndelete(mm ...*reportActionProps) *errors.Error {
var p = &reportActionProps{}
if len(mm) > 0 {
p = mm[0]
}
var e = errors.New(
errors.KindInternal,
p.Format("not allowed to undelete this report", nil),
errors.Meta("type", "notAllowedToUndelete"),
errors.Meta("resource", "system:report"),
// action log entry; no formatting, it will be applied inside recordAction fn.
errors.Meta(reportLogMetaKey{}, "failed to undelete {report}; insufficient permissions"),
errors.Meta(reportPropsMetaKey{}, p),
errors.StackSkip(1),
)
if len(mm) > 0 {
}
return e
}
// ReportErrNotAllowedToRun returns "system:report.notAllowedToRun" as *errors.Error
//
//
// This function is auto-generated.
//
func ReportErrNotAllowedToRun(mm ...*reportActionProps) *errors.Error {
var p = &reportActionProps{}
if len(mm) > 0 {
p = mm[0]
}
var e = errors.New(
errors.KindInternal,
p.Format("not allowed to run this report", nil),
errors.Meta("type", "notAllowedToRun"),
errors.Meta("resource", "system:report"),
// action log entry; no formatting, it will be applied inside recordAction fn.
errors.Meta(reportLogMetaKey{}, "failed to run {report}; insufficient permissions"),
errors.Meta(reportPropsMetaKey{}, p),
errors.StackSkip(1),
)
if len(mm) > 0 {
}
return e
}
// *********************************************************************************************************************
// *********************************************************************************************************************
// recordAction is a service helper function wraps function that can return error
//
// It will wrap unrecognized/internal errors with generic errors.
//
// This function is auto-generated.
//
func (svc report) recordAction(ctx context.Context, props *reportActionProps, actionFn func(...*reportActionProps) *reportAction, err error) error {
if svc.actionlog == nil || actionFn == nil {
// action log disabled or no action fn passed, return error as-is
return err
} else if err == nil {
// action completed w/o error, record it
svc.actionlog.Record(ctx, actionFn(props).ToAction())
return nil
}
a := actionFn(props).ToAction()
// Extracting error information and recording it as action
a.Error = err.Error()
switch c := err.(type) {
case *errors.Error:
m := c.Meta()
a.Error = err.Error()
a.Severity = actionlog.Severity(m.AsInt("severity"))
a.Description = props.Format(m.AsString(reportLogMetaKey{}), err)
if p, has := m[reportPropsMetaKey{}]; has {
a.Meta = p.(*reportActionProps).Serialize()
}
svc.actionlog.Record(ctx, a)
default:
svc.actionlog.Record(ctx, a)
}
// Original error is passed on
return err
}

View File

@@ -0,0 +1,92 @@
# List of loggable service actions
resource: system:report
service: report
# Default sensitivity for actions
defaultActionSeverity: notice
# default severity for errors
defaultErrorSeverity: error
import:
- github.com/cortezaproject/corteza-server/system/types
props:
- name: report
type: "*types.Report"
fields: [ handle, ID ]
- name: new
type: "*types.Report"
fields: [ handle, ID ]
- name: update
type: "*types.Report"
fields: [ handle, ID ]
- name: filter
type: "*types.ReportFilter"
fields: [ handle, deleted, sort ]
actions:
- action: search
log: "searched for reports"
severity: info
- action: lookup
log: "looked-up for a {report}"
severity: info
- action: create
log: "created {report}"
- action: update
log: "updated {report}"
- action: delete
log: "deleted {report}"
- action: undelete
log: "undeleted {report}"
- action: run
log: report ran
errors:
- error: notFound
message: "report not found"
severity: warning
- error: invalidID
message: "invalid ID"
severity: warning
- error: notAllowedToRead
message: "not allowed to read this report"
log: "failed to read {report}; insufficient permissions"
- error: notAllowedToSearch
message: "not allowed to list or search reports"
log: "failed to search for reports; insufficient permissions"
- error: notAllowedToListReports
message: "not allowed to list reports"
log: "failed to list report; insufficient permissions"
- error: notAllowedToCreate
message: "not allowed to create reports"
log: "failed to create report; insufficient permissions"
- error: notAllowedToUpdate
message: "not allowed to update this report"
log: "failed to update {report}; insufficient permissions"
- error: notAllowedToDelete
message: "not allowed to delete this report"
log: "failed to delete {report}; insufficient permissions"
- error: notAllowedToUndelete
message: "not allowed to undelete this report"
log: "failed to undelete {report}; insufficient permissions"
- error: notAllowedToRun
message: "not allowed to run this report"
log: "failed to run {report}; insufficient permissions"

View File

@@ -77,6 +77,7 @@ var (
DefaultQueue *queue
DefaultApigwRoute *apigwRoute
DefaultApigwFilter *apigwFilter
DefaultReport *report
DefaultStatistics *statistics
@@ -159,6 +160,7 @@ func Initialize(ctx context.Context, log *zap.Logger, s store.Storer, ws websock
hcd.Add(objstore.Healthcheck(DefaultObjectStore), "ObjectStore/System")
DefaultRenderer = Renderer(c.Template)
DefaultReport = Report(DefaultStore, DefaultAccessControl, DefaultActionlog, eventbus.Service())
DefaultAuthNotification = AuthNotification(CurrentSettings, DefaultRenderer, c.Auth)
DefaultAuth = Auth()
DefaultAuthClient = AuthClient(DefaultStore, DefaultAccessControl, DefaultActionlog, eventbus.Service(), c.Auth)

View File

@@ -32,6 +32,11 @@ func ParseAuthClientMeta(ss []string) (p *AuthClientMeta, err error) {
return p, parseStringsInput(ss, &p)
}
func ParseReportMeta(ss []string) (p *ReportMeta, err error) {
p = &ReportMeta{}
return p, parseStringsInput(ss, &p)
}
func ParseAuthClientSecurity(ss []string) (p *AuthClientSecurity, err error) {
p = &AuthClientSecurity{}
return p, parseStringsInput(ss, &p)

View File

@@ -11,6 +11,7 @@ package types
// - system.apigw-route.yaml
// - system.application.yaml
// - system.auth-client.yaml
// - system.report.yaml
// - system.role.yaml
// - system.template.yaml
// - system.user.yaml
@@ -33,6 +34,7 @@ const (
ApigwRouteResourceType = "corteza::system:apigw-route"
ApplicationResourceType = "corteza::system:application"
AuthClientResourceType = "corteza::system:auth-client"
ReportResourceType = "corteza::system:report"
RoleResourceType = "corteza::system:role"
TemplateResourceType = "corteza::system:template"
UserResourceType = "corteza::system:user"
@@ -163,6 +165,37 @@ func AuthClientRbacResourceTpl() string {
return "%s/%s"
}
// RbacResource returns string representation of RBAC resource for Report by calling ReportRbacResource fn
//
// RBAC resource is in the corteza::system:report/... format
//
// This function is auto-generated
func (r Report) RbacResource() string {
return ReportRbacResource(r.ID)
}
// ReportRbacResource returns string representation of RBAC resource for Report
//
// RBAC resource is in the corteza::system:report/... format
//
// This function is auto-generated
func ReportRbacResource(id uint64) string {
cpts := []interface{}{ReportResourceType}
if id != 0 {
cpts = append(cpts, strconv.FormatUint(id, 10))
} else {
cpts = append(cpts, "*")
}
return fmt.Sprintf(ReportRbacResourceTpl(), cpts...)
}
// @todo template
func ReportRbacResourceTpl() string {
return "%s/%s"
}
// RbacResource returns string representation of RBAC resource for Role by calling RoleRbacResource fn
//
// RBAC resource is in the corteza::system:role/... format

158
system/types/report.go Normal file
View File

@@ -0,0 +1,158 @@
package types
import (
"database/sql/driver"
"encoding/json"
"fmt"
"time"
"github.com/cortezaproject/corteza-server/pkg/filter"
"github.com/cortezaproject/corteza-server/pkg/report"
)
type (
Report struct {
ID uint64 `json:"reportID,string"`
Handle string `json:"handle"`
Meta *ReportMeta `json:"meta,omitempty"`
Sources ReportDataSourceSet `json:"sources"`
Projections ReportProjectionSet `json:"projections"`
// Report labels
Labels map[string]string `json:"labels,omitempty"`
OwnedBy uint64 `json:"ownedBy"`
CreatedBy uint64 `json:"createdBy"`
CreatedAt time.Time `json:"createdAt"`
UpdatedBy uint64 `json:"updatedBy,omitempty"`
UpdatedAt *time.Time `json:"updatedAt,omitempty"`
DeletedBy uint64 `json:"deletedBy,omitempty"`
DeletedAt *time.Time `json:"deletedAt,omitempty"`
}
ReportDataSource struct {
Name string `json:"name,omitempty"`
Groups []*ReportStepGroup
}
ReportDataSourceSet []*ReportDataSource
ReportStepGroup struct {
Name string `json:"name,omitempty"`
Steps report.StepDefinitionSet `json:"steps"`
}
ReportMeta struct {
Name string `json:"name"`
Description string `json:"description"`
}
ReportProjection struct {
Name string `json:"name"`
Key string `json:"key"`
Description string `json:"description"`
Kind string `json:"kind"`
Options map[string]interface{} `json:"options"`
}
ReportProjectionSet []*ReportProjection
ReportFilter struct {
ReportID []uint64 `json:"reportID"`
Handle string `json:"handle"`
Deleted filter.State `json:"deleted"`
LabeledIDs []uint64 `json:"-"`
Labels map[string]string `json:"labels,omitempty"`
// Check fn is called by store backend for each resource found function can
// modify the resource and return false if store should not return it
//
// Store then loads additional resources to satisfy the paging parameters
Check func(*Report) (bool, error) `json:"-"`
// Standard helpers for paging and sorting
filter.Sorting
filter.Paging
}
)
// @todo make better
func (ss ReportDataSourceSet) ModelSteps() report.StepDefinitionSet {
out := make(report.StepDefinitionSet, 0, 124)
for _, s := range ss {
for _, g := range s.Groups {
out = append(out, g.Steps...)
}
}
return out
}
// Store stuff
func (vv *ReportMeta) 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 = ReportMeta{}
case []uint8:
b := value.([]byte)
if err := json.Unmarshal(b, vv); err != nil {
return fmt.Errorf("cannot scan '%v' into ReportMeta: %w", string(b), err)
}
}
return nil
}
// Scan on ReportMeta gracefully handles conversion from NULL
func (vv *ReportMeta) Value() (driver.Value, error) {
if vv == nil {
return []byte("null"), nil
}
return json.Marshal(vv)
}
// Scan on ReportProjectionSet gracefully handles conversion from NULL
func (vv ReportProjectionSet) Value() (driver.Value, error) {
return json.Marshal(vv)
}
func (vv *ReportProjectionSet) 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 = ReportProjectionSet{}
case []uint8:
b := value.([]byte)
if err := json.Unmarshal(b, vv); err != nil {
return fmt.Errorf("cannot scan '%v' into ReportProjectionSet: %w", string(b), err)
}
}
return nil
}
// Scan on ReportDataSourceSet gracefully handles conversion from NULL
func (vv ReportDataSourceSet) Value() (driver.Value, error) {
return json.Marshal(vv)
}
func (vv *ReportDataSourceSet) 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 = ReportDataSourceSet{}
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
}

View File

@@ -56,6 +56,30 @@ func (m AuthClient) LabelResourceID() uint64 {
return m.ID
}
// SetLabel adds new label to label map
func (m *Report) SetLabel(key string, value string) {
if m.Labels == nil {
m.Labels = make(map[string]string)
}
m.Labels[key] = value
}
// GetLabels adds new label to label map
func (m Report) GetLabels() map[string]string {
return m.Labels
}
// GetLabels adds new label to label map
func (Report) LabelResourceKind() string {
return "report"
}
// GetLabels adds new label to label map
func (m Report) LabelResourceID() uint64 {
return m.ID
}
// SetLabel adds new label to label map
func (m *Role) SetLabel(key string, value string) {
if m.Labels == nil {

View File

@@ -60,6 +60,11 @@ type (
// This type is auto-generated.
ReminderSet []*Reminder
// ReportSet slice of Report
//
// This type is auto-generated.
ReportSet []*Report
// RoleSet slice of Role
//
// This type is auto-generated.
@@ -594,6 +599,62 @@ func (set ReminderSet) IDs() (IDs []uint64) {
return
}
// Walk iterates through every slice item and calls w(Report) err
//
// This function is auto-generated.
func (set ReportSet) Walk(w func(*Report) error) (err error) {
for i := range set {
if err = w(set[i]); err != nil {
return
}
}
return
}
// Filter iterates through every slice item, calls f(Report) (bool, err) and return filtered slice
//
// This function is auto-generated.
func (set ReportSet) Filter(f func(*Report) (bool, error)) (out ReportSet, err error) {
var ok bool
out = ReportSet{}
for i := range set {
if ok, err = f(set[i]); err != nil {
return
} else if ok {
out = append(out, set[i])
}
}
return
}
// FindByID finds items from slice by its ID property
//
// This function is auto-generated.
func (set ReportSet) FindByID(ID uint64) *Report {
for i := range set {
if set[i].ID == ID {
return set[i]
}
}
return nil
}
// IDs returns a slice of uint64s from all items in the set
//
// This function is auto-generated.
func (set ReportSet) IDs() (IDs []uint64) {
IDs = make([]uint64, len(set))
for i := range set {
IDs[i] = set[i].ID
}
return
}
// Walk iterates through every slice item and calls w(Role) err
//
// This function is auto-generated.

View File

@@ -846,6 +846,96 @@ func TestReminderSetIDs(t *testing.T) {
}
}
func TestReportSetWalk(t *testing.T) {
var (
value = make(ReportSet, 3)
req = require.New(t)
)
// check walk with no errors
{
err := value.Walk(func(*Report) error {
return nil
})
req.NoError(err)
}
// check walk with error
req.Error(value.Walk(func(*Report) error { return fmt.Errorf("walk error") }))
}
func TestReportSetFilter(t *testing.T) {
var (
value = make(ReportSet, 3)
req = require.New(t)
)
// filter nothing
{
set, err := value.Filter(func(*Report) (bool, error) {
return true, nil
})
req.NoError(err)
req.Equal(len(set), len(value))
}
// filter one item
{
found := false
set, err := value.Filter(func(*Report) (bool, error) {
if !found {
found = true
return found, nil
}
return false, nil
})
req.NoError(err)
req.Len(set, 1)
}
// filter error
{
_, err := value.Filter(func(*Report) (bool, error) {
return false, fmt.Errorf("filter error")
})
req.Error(err)
}
}
func TestReportSetIDs(t *testing.T) {
var (
value = make(ReportSet, 3)
req = require.New(t)
)
// construct objects
value[0] = new(Report)
value[1] = new(Report)
value[2] = new(Report)
// set ids
value[0].ID = 1
value[1].ID = 2
value[2].ID = 3
// Find existing
{
val := value.FindByID(2)
req.Equal(uint64(2), val.ID)
}
// Find non-existing
{
val := value.FindByID(4)
req.Nil(val)
}
// List IDs from set
{
val := value.IDs()
req.Equal(len(val), len(value))
}
}
func TestRoleSetWalk(t *testing.T) {
var (
value = make(RoleSet, 3)

View File

@@ -23,3 +23,5 @@ types:
labelResourceType: template
ApigwRoute: {}
ApigwFilter: {}
Report:
labelResourceType: report