Files
corteza/pkg/report/frame.go
T
Tomaž Jerman d65a767e94 Improve multivalue field support for report load step
Same behavour as with record lists where the module field delimiter
is used.
2022-06-22 14:35:21 +02:00

564 lines
11 KiB
Go

package report
import (
"bytes"
"encoding/json"
"fmt"
"strings"
"github.com/cortezaproject/corteza-server/pkg/errors"
"github.com/cortezaproject/corteza-server/pkg/expr"
"github.com/cortezaproject/corteza-server/pkg/filter"
"github.com/cortezaproject/corteza-server/pkg/qlng"
"github.com/spf13/cast"
)
type (
ColumnOptions interface {
Bool(key string) bool
}
Frame struct {
Name string `json:"name"`
Source string `json:"source"`
Ref string `json:"ref,omitempty"`
// RefValue is the common value between the two datasources
RefValue string `json:"refValue,omitempty"`
// RelColumn is what column in the local ds was used
RelColumn string `json:"relColumn,omitempty"`
// RelSource is the ds that this frame is related to
RelSource string `json:"relSource,omitempty"`
Columns FrameColumnSet `json:"columns"`
Rows FrameRowSet `json:"rows"`
Paging *filter.Paging `json:"paging"`
Sort filter.SortExprSet `json:"sort"`
Filter *Filter `json:"filter"`
// params to help us perform things in place
startIndex int
}
FrameRowSet []FrameRow
FrameRow []expr.TypedValue
frameCellCaster func(in interface{}) (expr.TypedValue, error)
FrameColumnSet []*FrameColumn
FrameColumn struct {
Name string `json:"name"`
Label string `json:"label"`
Kind string `json:"kind"`
Primary bool `json:"primary"`
Unique bool `json:"unique"`
System bool `json:"system"`
Options ColumnOptions `json:"-"`
Multivalue bool `json:"multivalue"`
MultivalueDelimiter string `json:"multivalueDelimiter"`
Caster frameCellCaster `json:"-" yaml:"-"`
}
FrameDefinitionSet []*FrameDefinition
FrameDefinition struct {
Name string `json:"name"`
Source string `json:"source"`
Ref string `json:"ref"`
Columns FrameColumnSet `json:"columns"`
Filter *Filter `json:"filter"`
Paging *filter.Paging `json:"paging"`
Sort filter.SortExprSet `json:"sort"`
}
FrameDescriptionSet []*FrameDescription
FrameDescription struct {
Source string `json:"source"`
Ref string `json:"ref,omitempty"`
Columns FrameColumnSet `json:"columns"`
// @todo size and other shape related bits
}
// Filter is a qlng.ASTNode wrapper to get some unmarshal/marshal features
Filter struct {
*qlng.ASTNode
Error string `json:"error,omitempty"`
}
)
func (f *Filter) Clone() *Filter {
if f == nil {
return nil
}
if f.ASTNode == nil {
return nil
}
return &Filter{
ASTNode: f.ASTNode.Clone(),
Error: f.Error,
}
}
func MakeColumnOfKind(k string) *FrameColumn {
return &FrameColumn{
Kind: k,
Caster: func(in interface{}) (expr.TypedValue, error) {
switch k {
case "Number":
return expr.NewFloat(in)
case "DateTime":
return expr.NewDateTime(in)
case "User",
"Record":
return expr.NewID(in)
case "Checkbox":
return expr.NewBoolean(in)
default:
return expr.NewString(in)
}
},
}
}
func (f *Filter) UnmarshalJSON(data []byte) (err error) {
var aux interface{}
if err = json.Unmarshal(data, &aux); err != nil {
return
}
p := qlng.NewParser()
// String expr. needs to be parsed to the AST
switch v := aux.(type) {
case string:
if v == "" {
return
}
f.ASTNode, err = p.Parse(v)
f.ASTNode.Raw = v
if err != nil {
f.Error = err.Error()
}
return nil
}
// special case for empty JSON
if bytes.Equal([]byte{'{', '}'}, data) {
return
}
// non-string is considered an AST and we parse that
if err = json.Unmarshal(data, &f.ASTNode); err != nil {
f.Error = err.Error()
return nil
}
// traverse the AST to parse any raw exprs.
if f.ASTNode == nil {
return nil
}
// A raw expression takes priority and replaces the original AST sub-tree
err = f.ASTNode.Traverse(func(n *qlng.ASTNode) (bool, *qlng.ASTNode, error) {
if n.Raw == "" {
return true, n, nil
}
aux, err := p.Parse(n.Raw)
if err != nil {
return false, n, err
}
aux.Raw = n.Raw
return false, aux, nil
})
if err != nil {
f.Error = err.Error()
} else {
f.Error = ""
}
return nil
}
// With guard element
func (f *Frame) WalkRowsG(cb func(i int, r FrameRow) error) (err error) {
err = f.WalkRows(cb)
if err != nil {
return err
}
return cb(f.Size(), nil)
}
func (f *Frame) WalkRows(cb func(i int, r FrameRow) error) (err error) {
for i := 0; i < f.Size(); i++ {
if err = cb(i, f.Rows[i]); err != nil {
return err
}
}
return nil
}
func (f *Frame) WalkRowsR(cb func(i int, r FrameRow) error) (err error) {
for i := f.Size(); i >= 0; i-- {
if err = cb(i, f.Rows[i]); err != nil {
return err
}
}
return nil
}
func (f *Frame) PeekRow(i int) FrameRow {
return f.Rows[i]
}
func (f *Frame) PeekRowSafe(i int) FrameRow {
if i >= f.Size() {
return nil
}
return f.Rows[i]
}
func (f *Frame) Size() int {
return len(f.Rows)
}
func (f *Frame) FirstRow() FrameRow {
if f.Size() == 0 {
return nil
}
return f.Rows[0]
}
func (f *Frame) LastRow() FrameRow {
if f.Size() == 0 {
return nil
}
return f.Rows[f.Size()-1]
}
func (f *Frame) LastLastRow() FrameRow {
if f.Size() == 0 {
return nil
}
return f.Rows[f.Size()-2]
}
// @todo nicer formatting and alignment
func (f *Frame) String() string {
if f == nil {
return "<NIL>"
}
out := fmt.Sprintf("n: %10s; src: %10s\n", f.Name, f.Source)
if f.Ref != "" {
out += fmt.Sprintf("ref: %10s; col: %10s\n; key: %10s\n", f.Ref, f.RelColumn, f.RefValue)
}
if f.RelSource != "" {
out += fmt.Sprintf("Rel source: %10s;\n", f.RelSource)
}
for _, c := range f.Columns {
out += fmt.Sprintf("%s<%s>, ", c.Name, c.Kind)
}
out = strings.TrimRight(out, " ,")
out += "\n"
f.WalkRows(func(i int, r FrameRow) error {
out += fmt.Sprintf("%d| ", i+1)
out += fmt.Sprintf("%s, ", r.String())
out = strings.TrimRight(out, ", ") + "\n"
return nil
})
if f.Paging != nil {
out += "\n"
out += fmt.Sprintf("< %s; =%s; > %s", f.Paging.PrevPage.String(), f.Paging.PageCursor.String(), f.Paging.NextPage.String())
}
if len(f.Sort) > 0 {
out += "\n"
out += f.Sort.String()
}
out += "\n"
out += fmt.Sprintf("ix %d; len %d", f.startIndex, f.Size())
return out
}
func (f *Frame) CollectCursorValues(r FrameRow, cc ...*filter.SortExpr) *filter.PagingCursor {
cursor := &filter.PagingCursor{LThen: filter.SortExprSet(cc).Reversed()}
for _, c := range cc {
var v interface{}
if r[f.Columns.Find(c.Column)] != nil {
v = r[f.Columns.Find(c.Column)].Get()
}
// the check for existence should be performed way in advanced so we won't bother here
cursor.Set(c.Column, v, c.Descending)
}
return cursor
}
func (f *Frame) CloneMeta() *Frame {
return &Frame{
Name: f.Name,
Source: f.Source,
Ref: f.Ref,
RefValue: f.RefValue,
RelColumn: f.RelColumn,
RelSource: f.RelSource,
Columns: f.Columns.Clone(),
Paging: f.Paging.Clone(),
Sort: f.Sort.Clone(),
Filter: f.Filter.Clone(),
}
}
func (cc FrameColumnSet) Clone() (out FrameColumnSet) {
out = make(FrameColumnSet, len(cc))
for i, c := range cc {
out[i] = &FrameColumn{
Name: c.Name,
Label: c.Label,
Kind: c.Kind,
Caster: c.Caster,
Primary: c.Primary,
Unique: c.Unique,
System: c.System,
}
}
return
}
func (cc FrameColumnSet) Find(name string) int {
for i, c := range cc {
if c.Name == name {
return i
}
}
return -1
}
func (cc FrameColumnSet) FirstPrimary() *FrameColumn {
for _, c := range cc {
if c.Primary {
return c
}
}
return nil
}
func (cc FrameColumnSet) String() string {
out := ""
for _, c := range cc {
out += fmt.Sprintf("%s<%s>, ", c.Name, c.Kind)
}
return strings.TrimRight(out, " ,")
}
// OmitSys returns the columns that are not system defined
func (cc FrameColumnSet) OmitSys() FrameColumnSet {
out := make(FrameColumnSet, 0, len(cc))
for _, c := range cc {
if !c.System {
out = append(out, c)
}
}
return out
}
// Receivers to conform to rdbms field matcher
func (c *FrameColumn) IsBoolean() bool {
return c.Kind == "Bool"
}
func (c *FrameColumn) IsNumeric() bool {
return c.Kind == "Number"
}
func (c *FrameColumn) IsDateTime() bool {
return c.Kind == "DateTime" && !c.IsTimeOnly() && !c.IsDateOnly()
}
func (c *FrameColumn) IsTimeOnly() bool {
return c.Options != nil && c.Options.Bool("onlyDate")
}
func (c *FrameColumn) IsDateOnly() bool {
return c.Options != nil && c.Options.Bool("onlyTime")
}
func (c *FrameColumn) IsRef() bool {
// @todo not quite right
return c.Kind == "Record"
}
func (r FrameRow) ToVars(cc FrameColumnSet) (vv *expr.Vars, err error) {
vv, _ = expr.NewVars(nil)
// The row
for i, c := range r {
if c == nil {
err = vv.AssignFieldValue(cc[i].Name, nil)
if err != nil {
return nil, err
}
} else {
err := vv.AssignFieldValue(cc[i].Name, c)
if err != nil {
return nil, err
}
}
}
return
}
func (f *FrameDefinition) Clone() (out *FrameDefinition) {
return &FrameDefinition{
Name: f.Name,
Source: f.Source,
Ref: f.Ref,
Columns: f.Columns.Clone(),
Paging: f.Paging.Clone(),
Sort: f.Sort.Clone(),
Filter: f.Filter.Clone(),
}
}
func (d FrameDefinition) Validate() error {
if d.Filter != nil && d.Filter.Error != "" {
return errors.InvalidData(d.Filter.Error)
}
return nil
}
func (dd FrameDefinitionSet) Find(name string) *FrameDefinition {
for _, d := range dd {
if d.Name == name {
return d
}
}
return nil
}
func (dd FrameDefinitionSet) FindBySourceRef(source, ref string) *FrameDefinition {
for _, d := range dd {
if d.Source == source && d.Ref == ref {
return d
}
}
return nil
}
func (row FrameRow) String() string {
out := ""
var s string
format := func(c expr.TypedValue) (out string) {
out, err := cast.ToStringE(c.Get())
if err != nil {
return fmt.Sprintf("[STRING CAST ERROR]%s", err.Error())
}
return out
}
for _, cell := range row {
if cell == nil {
out += "<N/A>, "
} else {
switch c := cell.(type) {
case *expr.Array:
aux := make([]string, 0, 3)
for _, c := range c.GetValue() {
aux = append(aux, format(c))
}
out = fmt.Sprintf("%s, %s", out, fmt.Sprintf("[%s]", strings.Join(aux, ", ")))
default:
s = format(cell)
out = fmt.Sprintf("%s, %s", out, s)
}
}
}
return strings.Trim(out, ", ")
}
func (a FrameRow) Compare(b FrameRow, cols ...int) int {
for _, col := range cols {
if cmp, _ := a[col].(expr.Comparable).Compare(b[col]); cmp != 0 {
return cmp
}
}
return 0
}
// AppendCell adds an extra value to the cell at index i
func (a FrameRow) AppendCell(i int, v expr.TypedValue) (err error) {
out := make([]expr.TypedValue, 0, 10)
handle := func(v expr.TypedValue) {
switch c := v.(type) {
case *expr.Array:
out = append(out, c.GetValue()...)
default:
out = append(out, v)
}
}
handle(a[i])
handle(v)
a[i], err = expr.NewArray(out)
return err
}
func (dd FrameDescriptionSet) FilterBySource(source string) FrameDescriptionSet {
out := make(FrameDescriptionSet, 0, len(dd))
for _, d := range dd {
if d.Source == source {
out = append(out, d)
}
}
return out
}
func (dd FrameDescriptionSet) FilterByRef(ref string) FrameDescriptionSet {
out := make(FrameDescriptionSet, 0, len(dd))
for _, d := range dd {
if d.Ref == ref {
out = append(out, d)
}
}
return out
}