Some checks failed
SonarQube / SonarQube (push) Failing after 5m28s
E2e test on main branch push / server and client test (map[name:admin port:8080]) (push) Failing after 3m33s
Server and lib tests / Library test (js) (push) Failing after 1m36s
Server and lib tests / Library test (vue) (push) Successful in 2m24s
Server and lib tests / Web client test (admin) (push) Successful in 2m52s
Server and lib tests / Web client test (compose) (push) Successful in 3m23s
Server and lib tests / Web client test (discovery) (push) Successful in 2m49s
Server and lib tests / Web client test (one) (push) Successful in 2m52s
Server and lib tests / Web client test (privacy) (push) Successful in 2m44s
Server and lib tests / Web client test (reporter) (push) Successful in 2m56s
Server and lib tests / Web client test (workflow) (push) Successful in 3m0s
Server and lib tests / Server tests (push) Failing after 4m55s
E2e test on main branch push / notify-failure (push) Failing after 9s
Server and lib tests / Send matrix testing notification (push) Successful in 2s
Mark stale issues and pull requests / stale (push) Successful in 40s
742 lines
18 KiB
Go
742 lines
18 KiB
Go
// Package reporting implements utilities for working with system reports and
|
|
// DAL pipelines
|
|
//
|
|
// The package exists primarily for making the system service codebase smaller
|
|
// and prepare for reusability with compose record reports.
|
|
package reporting
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
|
|
"github.com/cortezaproject/corteza/server/pkg/dal"
|
|
"github.com/cortezaproject/corteza/server/pkg/filter"
|
|
"github.com/cortezaproject/corteza/server/system/types"
|
|
"github.com/spf13/cast"
|
|
)
|
|
|
|
type (
|
|
// run is a wrapper around the pipeline and corresponding
|
|
// output frame definitions
|
|
run struct {
|
|
Pipeline dal.Pipeline
|
|
Defs FrameDefinitionSet
|
|
}
|
|
|
|
modelFinder interface {
|
|
FindModel(dal.ModelRef) *dal.Model
|
|
}
|
|
|
|
// @todo not a fan of this anymore; consider reworking pkg/dal/service to
|
|
// provide a more sequential approach we can use.
|
|
dryRunner interface {
|
|
modelFinder
|
|
Dryrun(context.Context, dal.Pipeline) error
|
|
}
|
|
)
|
|
|
|
// Runs create a set of report runs based on step and frame definitions
|
|
func Runs(pr modelFinder, steps types.ReportStepSet, defs FrameDefinitionSet) (out []run, err error) {
|
|
// Prepare runs based on the provided definitions
|
|
//
|
|
// - If consecutive definitions point to the same source with the same name
|
|
// consider them to fall under the same workload (the link step)
|
|
// - else, one def per workload
|
|
auxDefs := make(FrameDefinitionSet, 0)
|
|
var aux run
|
|
for i, def := range defs {
|
|
if i == 0 {
|
|
auxDefs = append(auxDefs, def)
|
|
continue
|
|
}
|
|
|
|
// Definitions fall together
|
|
if def.Name == defs[i-1].Name && def.Source == defs[i-1].Source {
|
|
auxDefs = append(auxDefs, def)
|
|
continue
|
|
}
|
|
|
|
// Make run for the previous definition (exclude current!!)
|
|
aux, err = makeRun(pr, steps, auxDefs)
|
|
if err != nil {
|
|
return
|
|
}
|
|
out = append(out, aux)
|
|
|
|
// Prepare next definition batch including the current one
|
|
auxDefs = make(FrameDefinitionSet, 0)
|
|
auxDefs = append(auxDefs, def)
|
|
}
|
|
|
|
// Handle the ones (potentially) not covered by the above loop
|
|
if len(auxDefs) > 0 {
|
|
aux, err = makeRun(pr, steps, auxDefs)
|
|
if err != nil {
|
|
return
|
|
}
|
|
out = append(out, aux)
|
|
}
|
|
|
|
return
|
|
}
|
|
|
|
// Frames returns a set of Frame for the given workload & iterator combo
|
|
func Frames(ctx context.Context, iter dal.Iterator, r run) (ff []*Frame, err error) {
|
|
// Preprocessing on the workload's frame definitions; assure all
|
|
// columns/metdata are there to avoid nonesense later down the line
|
|
updateDefinitionColumns(r)
|
|
|
|
// @todo perhaps need to change; for now only this scenario introduces multiple
|
|
// frame defs per workload
|
|
if len(r.Defs) > 1 {
|
|
return stepLinkFrames(ctx, iter, r)
|
|
}
|
|
|
|
return stepFrames(ctx, iter, r)
|
|
}
|
|
|
|
// Describe returns a set of frame descriptions based on the given pipeline
|
|
func Describe(ctx context.Context, rr dryRunner, ss types.ReportStepSet, sources []string) (out []FrameDescription, err error) {
|
|
// Make a run for the whole thing
|
|
pp, err := makePipeline(rr, ss, nil)
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
var aux []FrameDescription
|
|
for _, src := range sources {
|
|
// Use the requested source as root
|
|
sub := pp.Slice(src)
|
|
err = rr.Dryrun(ctx, sub)
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
s := sub[0]
|
|
aux, err = describePipeline(s, src)
|
|
if err != nil {
|
|
return
|
|
}
|
|
out = append(out, aux...)
|
|
}
|
|
|
|
return
|
|
}
|
|
|
|
// stepLinkFrames is dedicated for the link step due to it's unique output
|
|
func stepLinkFrames(ctx context.Context, iter dal.Iterator, r run) (ff []*Frame, err error) {
|
|
defs := r.Defs
|
|
// @note this will only be called for the link step so it can freely panic if violated
|
|
defLink := r.Pipeline[0].(*dal.Link)
|
|
|
|
// Unpack frame definitions for the link
|
|
defLeft, defRight := unpackLinkDefs(defs, r.Pipeline)
|
|
|
|
// Init vars to keep track of the progress
|
|
// @note true is left, false is right
|
|
counters := make(map[bool]uint)
|
|
|
|
builders := make(map[bool]*reportFrameBuilder)
|
|
builders[true] = newReportFrameBuilder(defLeft)
|
|
builders[false] = newReportFrameBuilder(defRight)
|
|
builders[false].linked(defLink.On.Right, defLink.On.Left, defLink.RelLeft)
|
|
|
|
limits := make(map[bool]uint)
|
|
if defLeft.Paging != nil {
|
|
limits[true] = defLeft.Paging.Limit
|
|
}
|
|
if defRight.Paging != nil {
|
|
limits[false] = defRight.Paging.Limit
|
|
}
|
|
|
|
// Helper to determine if we need a next cursor
|
|
nextCursor := false
|
|
|
|
// Helpers for reading iterators
|
|
var (
|
|
ref string
|
|
row = &dal.Row{}
|
|
doingF = false
|
|
)
|
|
|
|
for iter.Next(ctx) {
|
|
if limits[true] > 0 && counters[true] >= limits[true] {
|
|
nextCursor = true
|
|
break
|
|
}
|
|
row.Reset()
|
|
|
|
_ = iter.Scan(row)
|
|
|
|
// Determine ref and which vars to use
|
|
aux, _ := row.GetValue(dal.LinkRefIdent, 0)
|
|
ref = cast.ToString(aux)
|
|
if ref == "" {
|
|
ref = defLeft.Ref
|
|
}
|
|
left := ref == defLeft.Ref
|
|
|
|
// When needed, flush the finished frames to the output
|
|
if left && doingF {
|
|
ff = append(ff, builders[false].done())
|
|
doingF = false
|
|
} else if !left {
|
|
doingF = true
|
|
}
|
|
|
|
builders[left].addRow(row)
|
|
counters[left]++
|
|
}
|
|
if err = iter.Err(); err != nil {
|
|
return
|
|
}
|
|
|
|
// If the loop ended before the limit cut it off, we need to finish the
|
|
// last right frame as it wasn't yet in the above loop
|
|
if !nextCursor {
|
|
if doingF {
|
|
ff = append(ff, builders[false].done())
|
|
}
|
|
}
|
|
|
|
// Apply paging cursor to the left frame
|
|
// @todo consider applying them to the right as well, for now, no
|
|
if nextCursor {
|
|
if builders[true].frame.Paging == nil {
|
|
builders[true].frame.Paging = &filter.Paging{}
|
|
}
|
|
builders[true].frame.Paging.NextPage, err = iter.ForwardCursor(row)
|
|
if err != nil {
|
|
return
|
|
}
|
|
}
|
|
|
|
// Complete the output with the left frame
|
|
// @note the left frame goes to the start and the right frames are in the same order
|
|
// as the related rows from the left frame.
|
|
return append([]*Frame{builders[true].done()}, ff...), nil
|
|
}
|
|
|
|
// unpackLinkDefs returns the left and the right frame definition disregarding
|
|
// the order of the definitions in the input
|
|
func unpackLinkDefs(defs FrameDefinitionSet, pp dal.Pipeline) (left, right *FrameDefinition) {
|
|
r := pp[0]
|
|
|
|
l := r.(*dal.Link)
|
|
|
|
find := func(defs FrameDefinitionSet, ref string) *FrameDefinition {
|
|
for _, def := range defs {
|
|
if def.Ref == ref {
|
|
return def
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
return find(defs, l.RelLeft), find(defs, l.RelRight)
|
|
}
|
|
|
|
// stepFrames is a generic iter to Frame converter
|
|
func stepFrames(ctx context.Context, iter dal.Iterator, r run) (ff []*Frame, err error) {
|
|
defs := r.Defs
|
|
|
|
// @note only the link step takes multiple defs and that one is not covered
|
|
// by this function
|
|
if len(defs) != 1 {
|
|
panic(fmt.Sprintf("impossible state: expecting one frame definition, got %d", len(defs)))
|
|
}
|
|
def := defs[0]
|
|
|
|
// Init vars to keep track of the progress
|
|
limit := uint(0)
|
|
counter := uint(0)
|
|
builder := newReportFrameBuilder(def)
|
|
if def.Paging != nil {
|
|
limit = def.Paging.Limit
|
|
}
|
|
|
|
// Helper to determine if we need a next cursor
|
|
nextCursor := false
|
|
|
|
// Helpers for reading iterators
|
|
row := &dal.Row{}
|
|
|
|
for iter.Next(ctx) {
|
|
if limit > 0 && counter >= limit {
|
|
nextCursor = true
|
|
break
|
|
}
|
|
row.Reset()
|
|
|
|
_ = iter.Scan(row)
|
|
builder.addRow(row)
|
|
counter++
|
|
}
|
|
if err = iter.Err(); err != nil {
|
|
return
|
|
}
|
|
|
|
// Apply paging cursor to the frame
|
|
if nextCursor {
|
|
if builder.frame.Paging == nil {
|
|
builder.frame.Paging = &filter.Paging{}
|
|
}
|
|
builder.frame.Paging.NextPage, err = iter.ForwardCursor(row)
|
|
if err != nil {
|
|
return
|
|
}
|
|
}
|
|
|
|
return append(ff, builder.done()), nil
|
|
}
|
|
|
|
func describePipeline(s dal.PipelineStep, src string) (out []FrameDescription, err error) {
|
|
aa := s.Attributes()
|
|
|
|
out = make([]FrameDescription, len(aa))
|
|
for i, a := range aa {
|
|
out[i].Source = src
|
|
out[i].Columns = mappingToFrameCols(a)
|
|
}
|
|
|
|
// @note this case is only possible for the link step; expand when/if needed
|
|
if len(out) == 2 {
|
|
l := s.(*dal.Link)
|
|
out[0].Ref = l.RelLeft
|
|
out[1].Ref = l.RelRight
|
|
}
|
|
|
|
return
|
|
}
|
|
|
|
func makeRun(pr modelFinder, ss types.ReportStepSet, defs FrameDefinitionSet) (out run, err error) {
|
|
var pp dal.Pipeline
|
|
pp, err = makePipeline(pr, ss, defs)
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
out.Defs = defs
|
|
out.Pipeline = pp.Slice(defs[0].Source)
|
|
return
|
|
}
|
|
|
|
// makePipeline converts the given report steps into the DAL pipeline
|
|
func makePipeline(mf modelFinder, ss types.ReportStepSet, defs FrameDefinitionSet) (pp dal.Pipeline, err error) {
|
|
for _, step := range ss {
|
|
switch {
|
|
case step.Load != nil:
|
|
aux, err := convStepLoad(mf, *step.Load, defs.FilterBySource(step.Load.Name))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
pp = append(pp, aux)
|
|
|
|
case step.Aggregate != nil:
|
|
aux, err := convStepAggregate(*step.Aggregate, defs.FilterBySource(step.Aggregate.Name))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
pp = append(pp, aux)
|
|
|
|
case step.Join != nil:
|
|
aux, err := convStepJoin(*step.Join, defs.FilterBySource(step.Join.Name))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
pp = append(pp, aux)
|
|
|
|
case step.Link != nil:
|
|
aux, err := convStepLink(*step.Link, defs.FilterBySource(step.Link.Name))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
pp = append(pp, aux)
|
|
|
|
default:
|
|
// this should never happen
|
|
panic(fmt.Errorf("unknown step type: %v", step.Kind))
|
|
}
|
|
}
|
|
|
|
return pp, pp.LinkSteps()
|
|
}
|
|
|
|
// mappingToFrameCols converts pipeline AttributeMapping to FrameColumnSet
|
|
func mappingToFrameCols(mm []dal.AttributeMapping) FrameColumnSet {
|
|
out := make(FrameColumnSet, 0, len(mm))
|
|
|
|
for _, m := range mm {
|
|
out = append(out, mappingToFrameCol(m))
|
|
}
|
|
|
|
return out
|
|
}
|
|
|
|
// @note current implementation a bit _rushed_ since I'll probably rethink
|
|
// how the pipeline handles attributes -- will revisit then.
|
|
func mappingToFrameCol(m dal.AttributeMapping) FrameColumn {
|
|
p := m.Properties()
|
|
|
|
const (
|
|
// Coppied around to reduce imports
|
|
emailLength = 254
|
|
urlLength = 2048
|
|
|
|
attachmentResType = "corteza::system:attachment"
|
|
userResType = "corteza::system:user"
|
|
moduleResType = "corteza::compose:module"
|
|
)
|
|
|
|
l := m.Properties().Label
|
|
if l == "" {
|
|
l = m.Identifier()
|
|
}
|
|
out := FrameColumn{
|
|
Name: m.Identifier(),
|
|
Label: l,
|
|
Kind: "String",
|
|
|
|
Primary: p.IsPrimary,
|
|
System: p.IsSystem,
|
|
Multivalue: p.IsMultivalue,
|
|
MultivalueDelimiter: p.MultivalueDelimiter,
|
|
}
|
|
if out.MultivalueDelimiter == "" {
|
|
out.MultivalueDelimiter = "\n"
|
|
}
|
|
|
|
switch t := p.Type.(type) {
|
|
case *dal.TypeBoolean:
|
|
out.Kind = "Boolean"
|
|
case *dal.TypeDate, *dal.TypeTime, *dal.TypeTimestamp:
|
|
out.Kind = "DateTime"
|
|
|
|
case *dal.TypeNumber:
|
|
out.Kind = "Number"
|
|
|
|
case *dal.TypeEnum:
|
|
out.Kind = "Select"
|
|
|
|
case *dal.TypeText:
|
|
// @note temporary solution; we should push some meta along with it
|
|
if t.Length == emailLength {
|
|
out.Kind = "Email"
|
|
} else if t.Length == urlLength {
|
|
out.Kind = "URL"
|
|
} else {
|
|
out.Kind = "String"
|
|
}
|
|
|
|
case *dal.TypeRef:
|
|
switch t.RefModel.ResourceType {
|
|
case moduleResType:
|
|
out.Kind = "Record"
|
|
case userResType:
|
|
out.Kind = "User"
|
|
case attachmentResType:
|
|
out.Kind = "File"
|
|
}
|
|
}
|
|
|
|
return out
|
|
}
|
|
|
|
// Report step -> DAL step conversion
|
|
|
|
// convStepLoad converts ReportStepLoad to dal.Datasource
|
|
func convStepLoad(pr modelFinder, step types.ReportStepLoad, defs FrameDefinitionSet) (out *dal.Datasource, err error) {
|
|
// Validation
|
|
if len(defs) > 1 {
|
|
err = fmt.Errorf("cannot convert load step: expecting at most one definition, got %d", len(defs))
|
|
return
|
|
}
|
|
|
|
// Get additional filtering
|
|
var extf filter.Filter
|
|
if len(defs) == 1 {
|
|
extf = filterFromDef(defs[0])
|
|
}
|
|
|
|
// Prepare model ref
|
|
mfr, err := makeModelRef(step)
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
// @todo refactor this out after we support other resources with potentially missing soft delete fields
|
|
f, err := dal.FilterFromExpr(step.Filter.Node()).
|
|
MergeFilters(filter.Generic(filter.WithStateConstraint("deletedAt", filter.StateExcluded)))
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
f, err = f.MergeFilters(extf)
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
// Make pipeline step
|
|
return &dal.Datasource{
|
|
Ident: step.Name,
|
|
Filter: f,
|
|
ModelRef: mfr,
|
|
OutAttributes: modelAttributes(pr, step, mfr),
|
|
}, nil
|
|
}
|
|
|
|
// convStepAggregate converts ReportStepAggregate to dal.Aggregate
|
|
func convStepAggregate(step types.ReportStepAggregate, defs FrameDefinitionSet) (out *dal.Aggregate, err error) {
|
|
// Validation
|
|
if len(defs) > 1 {
|
|
err = fmt.Errorf("cannot convert aggregate step: expecting at most one definition, got %d", len(defs))
|
|
return
|
|
}
|
|
|
|
// Get additional filtering
|
|
var extf filter.Filter
|
|
if len(defs) == 1 {
|
|
extf = filterFromDef(defs[0])
|
|
}
|
|
|
|
f, err := dal.FilterFromExpr(step.Filter.Node()).
|
|
MergeFilters(extf)
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
ggs := make([]dal.AggregateAttr, 0, len(step.Keys))
|
|
for _, c := range step.Keys {
|
|
ggs = append(ggs, dal.AggregateAttr{
|
|
Key: true,
|
|
Identifier: c.Name,
|
|
Label: c.Label,
|
|
Expression: c.Def.Node(),
|
|
})
|
|
}
|
|
|
|
vvs := make([]dal.AggregateAttr, 0, len(step.Columns))
|
|
for _, c := range step.Columns {
|
|
vvs = append(vvs, dal.AggregateAttr{
|
|
Identifier: c.Name,
|
|
Label: c.Label,
|
|
Expression: c.Def.Node(),
|
|
})
|
|
}
|
|
|
|
// Make pipeline step
|
|
out = &dal.Aggregate{
|
|
Ident: step.Name,
|
|
RelSource: step.Source,
|
|
Filter: f,
|
|
|
|
Group: ggs,
|
|
OutAttributes: vvs,
|
|
}
|
|
return
|
|
}
|
|
|
|
// convStepJoin converts ReportStepJoin to dal.Join
|
|
func convStepJoin(step types.ReportStepJoin, defs FrameDefinitionSet) (out *dal.Join, err error) {
|
|
// Validation
|
|
if len(defs) > 1 {
|
|
err = fmt.Errorf("cannot convert join step: expecting at most one definition, got %d", len(defs))
|
|
return
|
|
}
|
|
|
|
// Get additional filtering
|
|
var extf filter.Filter
|
|
if len(defs) == 1 {
|
|
extf = filterFromDef(defs[0])
|
|
}
|
|
|
|
f, err := dal.FilterFromExpr(step.Filter.Node()).
|
|
MergeFilters(extf)
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
// Make pipeline step
|
|
out = &dal.Join{
|
|
Ident: step.Name,
|
|
RelLeft: step.LocalSource,
|
|
RelRight: step.ForeignSource,
|
|
|
|
Filter: f,
|
|
|
|
On: dal.JoinPredicate{
|
|
Left: step.LocalColumn,
|
|
Right: step.ForeignColumn,
|
|
},
|
|
}
|
|
return
|
|
}
|
|
|
|
// convStepLink converts ReportStepLink to dal.Link
|
|
func convStepLink(step types.ReportStepLink, defs FrameDefinitionSet) (out *dal.Link, err error) {
|
|
// Validation
|
|
if len(defs) > 2 {
|
|
err = fmt.Errorf("cannot convert join step: expecting at most two definitions, got %d", len(defs))
|
|
return
|
|
}
|
|
|
|
// @todo additional filtering; will need to split the dal.Link filter into
|
|
// left and right for more control and clarity
|
|
|
|
var extf filter.Filter
|
|
if len(defs) > 0 {
|
|
extf = filterFromDef(defs[0])
|
|
}
|
|
|
|
// Get additional filtering
|
|
f, err := dal.FilterFromExpr(step.Filter.Node()).
|
|
MergeFilters(extf)
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
// @todo temporary constraint
|
|
if len(defs) == 2 {
|
|
d := defs[1]
|
|
if d.Filter != nil && d.Filter.Node() != nil || len(d.Sort) > 0 {
|
|
return nil, fmt.Errorf("temporary constraint: cannot apply sorting/filtering to foreign frame definition")
|
|
}
|
|
}
|
|
|
|
// Make pipeline step
|
|
out = &dal.Link{
|
|
Ident: step.Name,
|
|
RelLeft: step.LocalSource,
|
|
RelRight: step.ForeignSource,
|
|
|
|
On: dal.LinkPredicate{
|
|
Left: step.LocalColumn,
|
|
Right: step.ForeignColumn,
|
|
},
|
|
Filter: f,
|
|
}
|
|
return
|
|
}
|
|
|
|
// updateDefinitionColumns assures run's frame column completeness
|
|
func updateDefinitionColumns(r run) {
|
|
ppAttrs := r.Pipeline[0].Attributes()
|
|
for i, def := range r.Defs {
|
|
if len(def.Columns) > 0 {
|
|
continue
|
|
}
|
|
|
|
def.Columns = mappingToFrameCols(ppAttrs[i])
|
|
}
|
|
}
|
|
|
|
// makeModelRef returns the model ref based on the step load definition
|
|
// @todo should be expanded when we support models that are not compose modules
|
|
func makeModelRef(step types.ReportStepLoad) (out dal.ModelRef, err error) {
|
|
var (
|
|
connectionID uint64
|
|
moduleID, namespaceID uint64
|
|
module, namespace string
|
|
|
|
aux any
|
|
ok bool
|
|
)
|
|
|
|
if aux, ok = step.Definition["moduleID"]; ok {
|
|
moduleID = cast.ToUint64(aux)
|
|
} else if aux, ok = step.Definition["module"]; ok {
|
|
module = cast.ToString(aux)
|
|
} else {
|
|
err = fmt.Errorf("step definition is missing moduleID or module")
|
|
return
|
|
}
|
|
|
|
if aux, ok = step.Definition["namespaceID"]; ok {
|
|
namespaceID = cast.ToUint64(aux)
|
|
} else if aux, ok = step.Definition["namespace"]; ok {
|
|
namespace = cast.ToString(aux)
|
|
} else {
|
|
err = fmt.Errorf("step definition is missing namespaceID or namespace")
|
|
return
|
|
}
|
|
|
|
// Connection is optional, default is primary connection
|
|
if aux, ok = step.Definition["connectionID"]; ok {
|
|
connectionID = cast.ToUint64(aux)
|
|
}
|
|
|
|
out.ConnectionID = connectionID
|
|
out.Refs = make(map[string]any)
|
|
|
|
// Use only one of the two identifier variations with priority to ID
|
|
if moduleID > 0 {
|
|
out.Refs["moduleID"] = moduleID
|
|
} else {
|
|
out.Refs["module"] = module
|
|
}
|
|
|
|
if namespaceID > 0 {
|
|
out.Refs["namespaceID"] = namespaceID
|
|
} else {
|
|
out.Refs["namespace"] = namespace
|
|
}
|
|
|
|
return
|
|
}
|
|
|
|
// modelAttributes returns the attributes defined on the referenced model
|
|
func modelAttributes(pr modelFinder, step types.ReportStepLoad, mfr dal.ModelRef) []dal.AttributeMapping {
|
|
// All of the attributes
|
|
attrs, err := getModelAttrs(pr, mfr)
|
|
if err != nil {
|
|
return nil
|
|
}
|
|
|
|
return attrToMapping(attrs...)
|
|
}
|
|
|
|
func getModelAttrs(pr modelFinder, mfr dal.ModelRef) (attrs dal.AttributeSet, err error) {
|
|
m := pr.FindModel(mfr)
|
|
if m == nil {
|
|
return nil, fmt.Errorf("model not found: %v", mfr)
|
|
}
|
|
|
|
return m.Attributes, nil
|
|
}
|
|
|
|
func attrToMapping(aa ...*dal.Attribute) (out []dal.AttributeMapping) {
|
|
for _, a := range aa {
|
|
out = append(out, dal.SimpleAttr{
|
|
Ident: a.Ident,
|
|
Src: a.Ident,
|
|
Props: dal.MapProperties{
|
|
Label: a.Label,
|
|
Type: a.Type,
|
|
IsSystem: a.System,
|
|
Nullable: a.Type.IsNullable(),
|
|
IsPrimary: a.PrimaryKey,
|
|
// @todo add multi value delimiter; it's currently not set on the
|
|
// attribute so we might need to rethink this a bit
|
|
IsMultivalue: a.MultiValue,
|
|
},
|
|
})
|
|
}
|
|
return
|
|
}
|
|
|
|
func filterFromDef(def *FrameDefinition) (out filter.Filter) {
|
|
aux := filter.Generic(
|
|
filter.WithExpressionParsed(def.Filter.Node()),
|
|
filter.WithOrderBy(def.Sort),
|
|
)
|
|
|
|
if def.Paging != nil {
|
|
aux = aux.With(
|
|
filter.WithCursor(def.Paging.PageCursor),
|
|
filter.WithLimit(def.Paging.Limit),
|
|
)
|
|
}
|
|
|
|
return aux
|
|
}
|