snapshot of https://github.com/cortezaproject/corteza.git (2024.9.x, 2026-09-18; blobs > 131072B omitted)
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

This commit is contained in:
upstream-mirror
2026-09-18 01:22:23 +05:00
commit 7e385fc126
8391 changed files with 1766236 additions and 0 deletions

View File

@@ -0,0 +1,498 @@
package automation
// 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:
// compose/automation/attachment_handler.yaml
import (
"context"
atypes "github.com/cortezaproject/corteza/server/automation/types"
"github.com/cortezaproject/corteza/server/compose/types"
"github.com/cortezaproject/corteza/server/pkg/expr"
"github.com/cortezaproject/corteza/server/pkg/wfexec"
"io"
)
var _ wfexec.ExecResponse
type (
attachmentHandlerRegistry interface {
AddFunctions(ff ...*atypes.Function)
Type(ref string) expr.Type
}
)
func (h attachmentHandler) register() {
h.reg.AddFunctions(
h.Lookup(),
h.Create(),
h.Delete(),
h.OpenOriginal(),
h.OpenPreview(),
)
}
type (
attachmentLookupArgs struct {
hasAttachment bool
Attachment uint64
}
attachmentLookupResults struct {
Attachment *types.Attachment
}
)
// Lookup function Attachment lookup
//
// expects implementation of lookup function:
//
// func (h attachmentHandler) lookup(ctx context.Context, args *attachmentLookupArgs) (results *attachmentLookupResults, err error) {
// return
// }
func (h attachmentHandler) Lookup() *atypes.Function {
return &atypes.Function{
Ref: "attachmentLookup",
Kind: "function",
Labels: map[string]string{"attachment": "step,workflow"},
Meta: &atypes.FunctionMeta{
Short: "Attachment lookup",
Description: "Find specific attachment by ID",
},
Parameters: []*atypes.Param{
{
Name: "attachment",
Types: []string{"ID"}, Required: true,
Meta: &atypes.ParamMeta{
Label: "Attachment",
},
},
},
Results: []*atypes.Param{
{
Name: "attachment",
Types: []string{"Attachment"},
},
},
Handler: func(ctx context.Context, in *expr.Vars) (out *expr.Vars, err error) {
var (
args = &attachmentLookupArgs{
hasAttachment: in.Has("attachment"),
}
)
if err = in.Decode(args); err != nil {
return
}
var results *attachmentLookupResults
if results, err = h.lookup(ctx, args); err != nil {
return
}
out = &expr.Vars{}
{
// converting results.Attachment (*types.Attachment) to Attachment
var (
tval expr.TypedValue
)
if tval, err = h.reg.Type("Attachment").Cast(results.Attachment); err != nil {
return
} else if err = expr.Assign(out, "attachment", tval); err != nil {
return
}
}
return
},
}
}
type (
attachmentCreateArgs struct {
hasName bool
Name string
hasResource bool
Resource *types.Record
hasFieldName bool
FieldName string
hasContent bool
Content interface{}
contentString string
contentStream io.Reader
contentBytes []byte
}
attachmentCreateResults struct {
Attachment *types.Attachment
}
)
func (a attachmentCreateArgs) GetContent() (bool, string, io.Reader, []byte) {
return a.hasContent, a.contentString, a.contentStream, a.contentBytes
}
// Create function Create file and attach it to a resource
//
// expects implementation of create function:
//
// func (h attachmentHandler) create(ctx context.Context, args *attachmentCreateArgs) (results *attachmentCreateResults, err error) {
// return
// }
func (h attachmentHandler) Create() *atypes.Function {
return &atypes.Function{
Ref: "attachmentCreate",
Kind: "function",
Labels: map[string]string{"attachment": "step,workflow", "create": "step"},
Meta: &atypes.FunctionMeta{
Short: "Create file and attach it to a resource",
},
Parameters: []*atypes.Param{
{
Name: "name",
Types: []string{"String"},
Meta: &atypes.ParamMeta{
Label: "Name",
},
},
{
Name: "resource",
Types: []string{"ComposeRecord"}, Required: true,
Meta: &atypes.ParamMeta{
Label: "Resource",
},
},
{
Name: "fieldName",
Types: []string{"String"},
Meta: &atypes.ParamMeta{
Label: "Field name",
},
},
{
Name: "content",
Types: []string{"String", "Reader", "Bytes"}, Required: true,
Meta: &atypes.ParamMeta{
Label: "Content",
},
},
},
Results: []*atypes.Param{
{
Name: "attachment",
Types: []string{"Attachment"},
},
},
Handler: func(ctx context.Context, in *expr.Vars) (out *expr.Vars, err error) {
var (
args = &attachmentCreateArgs{
hasName: in.Has("name"),
hasResource: in.Has("resource"),
hasFieldName: in.Has("fieldName"),
hasContent: in.Has("content"),
}
)
if err = in.Decode(args); err != nil {
return
}
// Converting Content argument
if args.hasContent {
aux := expr.Must(expr.Select(in, "content"))
switch aux.Type() {
case h.reg.Type("String").Type():
args.contentString = aux.Get().(string)
case h.reg.Type("Reader").Type():
args.contentStream = aux.Get().(io.Reader)
case h.reg.Type("Bytes").Type():
args.contentBytes = aux.Get().([]byte)
}
}
var results *attachmentCreateResults
if results, err = h.create(ctx, args); err != nil {
return
}
out = &expr.Vars{}
{
// converting results.Attachment (*types.Attachment) to Attachment
var (
tval expr.TypedValue
)
if tval, err = h.reg.Type("Attachment").Cast(results.Attachment); err != nil {
return
} else if err = expr.Assign(out, "attachment", tval); err != nil {
return
}
}
return
},
}
}
type (
attachmentDeleteArgs struct {
hasAttachment bool
Attachment uint64
}
)
// Delete function Delete attachment
//
// expects implementation of delete function:
//
// func (h attachmentHandler) delete(ctx context.Context, args *attachmentDeleteArgs) (err error) {
// return
// }
func (h attachmentHandler) Delete() *atypes.Function {
return &atypes.Function{
Ref: "attachmentDelete",
Kind: "function",
Labels: map[string]string{"attachment": "step,workflow", "delete": "step"},
Meta: &atypes.FunctionMeta{
Short: "Delete attachment",
},
Parameters: []*atypes.Param{
{
Name: "attachment",
Types: []string{"ID"}, Required: true,
Meta: &atypes.ParamMeta{
Label: "Attachment",
},
},
},
Handler: func(ctx context.Context, in *expr.Vars) (out *expr.Vars, err error) {
var (
args = &attachmentDeleteArgs{
hasAttachment: in.Has("attachment"),
}
)
if err = in.Decode(args); err != nil {
return
}
return out, h.delete(ctx, args)
},
}
}
type (
attachmentOpenOriginalArgs struct {
hasAttachment bool
Attachment interface{}
attachmentID uint64
attachmentAttachment *types.Attachment
}
attachmentOpenOriginalResults struct {
Content io.Reader
}
)
func (a attachmentOpenOriginalArgs) GetAttachment() (bool, uint64, *types.Attachment) {
return a.hasAttachment, a.attachmentID, a.attachmentAttachment
}
// OpenOriginal function Open original attachment
//
// expects implementation of openOriginal function:
//
// func (h attachmentHandler) openOriginal(ctx context.Context, args *attachmentOpenOriginalArgs) (results *attachmentOpenOriginalResults, err error) {
// return
// }
func (h attachmentHandler) OpenOriginal() *atypes.Function {
return &atypes.Function{
Ref: "attachmentOpenOriginal",
Kind: "function",
Labels: map[string]string{"attachment": "step,workflow", "original-attachment": "step"},
Meta: &atypes.FunctionMeta{
Short: "Open original attachment",
},
Parameters: []*atypes.Param{
{
Name: "attachment",
Types: []string{"ID", "Attachment"}, Required: true,
Meta: &atypes.ParamMeta{
Label: "Attachment",
},
},
},
Results: []*atypes.Param{
{
Name: "content",
Types: []string{"Reader"},
},
},
Handler: func(ctx context.Context, in *expr.Vars) (out *expr.Vars, err error) {
var (
args = &attachmentOpenOriginalArgs{
hasAttachment: in.Has("attachment"),
}
)
if err = in.Decode(args); err != nil {
return
}
// Converting Attachment argument
if args.hasAttachment {
aux := expr.Must(expr.Select(in, "attachment"))
switch aux.Type() {
case h.reg.Type("ID").Type():
args.attachmentID = aux.Get().(uint64)
case h.reg.Type("Attachment").Type():
args.attachmentAttachment = aux.Get().(*types.Attachment)
}
}
var results *attachmentOpenOriginalResults
if results, err = h.openOriginal(ctx, args); err != nil {
return
}
out = &expr.Vars{}
{
// converting results.Content (io.Reader) to Reader
var (
tval expr.TypedValue
)
if tval, err = h.reg.Type("Reader").Cast(results.Content); err != nil {
return
} else if err = expr.Assign(out, "content", tval); err != nil {
return
}
}
return
},
}
}
type (
attachmentOpenPreviewArgs struct {
hasAttachment bool
Attachment interface{}
attachmentID uint64
attachmentAttachment *types.Attachment
}
attachmentOpenPreviewResults struct {
Content io.Reader
}
)
func (a attachmentOpenPreviewArgs) GetAttachment() (bool, uint64, *types.Attachment) {
return a.hasAttachment, a.attachmentID, a.attachmentAttachment
}
// OpenPreview function Open attachment preview
//
// expects implementation of openPreview function:
//
// func (h attachmentHandler) openPreview(ctx context.Context, args *attachmentOpenPreviewArgs) (results *attachmentOpenPreviewResults, err error) {
// return
// }
func (h attachmentHandler) OpenPreview() *atypes.Function {
return &atypes.Function{
Ref: "attachmentOpenPreview",
Kind: "function",
Labels: map[string]string{"attachment": "step,workflow", "preview-attachment": "step"},
Meta: &atypes.FunctionMeta{
Short: "Open attachment preview",
},
Parameters: []*atypes.Param{
{
Name: "attachment",
Types: []string{"ID", "Attachment"}, Required: true,
Meta: &atypes.ParamMeta{
Label: "Attachment",
},
},
},
Results: []*atypes.Param{
{
Name: "content",
Types: []string{"Reader"},
},
},
Handler: func(ctx context.Context, in *expr.Vars) (out *expr.Vars, err error) {
var (
args = &attachmentOpenPreviewArgs{
hasAttachment: in.Has("attachment"),
}
)
if err = in.Decode(args); err != nil {
return
}
// Converting Attachment argument
if args.hasAttachment {
aux := expr.Must(expr.Select(in, "attachment"))
switch aux.Type() {
case h.reg.Type("ID").Type():
args.attachmentID = aux.Get().(uint64)
case h.reg.Type("Attachment").Type():
args.attachmentAttachment = aux.Get().(*types.Attachment)
}
}
var results *attachmentOpenPreviewResults
if results, err = h.openPreview(ctx, args); err != nil {
return
}
out = &expr.Vars{}
{
// converting results.Content (io.Reader) to Reader
var (
tval expr.TypedValue
)
if tval, err = h.reg.Type("Reader").Cast(results.Content); err != nil {
return
} else if err = expr.Assign(out, "content", tval); err != nil {
return
}
}
return
},
}
}

View File

@@ -0,0 +1,227 @@
package automation
import (
"bufio"
"bytes"
"context"
"fmt"
"io"
"io/ioutil"
"os"
"strings"
"github.com/cortezaproject/corteza/server/compose/types"
)
type (
attachmentService interface {
FindByID(ctx context.Context, namespaceID, attachmentID uint64) (*types.Attachment, error)
CreateRecordAttachment(ctx context.Context, namespaceID uint64, name string, size int64, fh io.ReadSeeker, moduleID, recordID uint64, fieldName string) (att *types.Attachment, err error)
DeleteByID(ctx context.Context, namespaceID uint64, attachmentID uint64) error
OpenOriginal(att *types.Attachment) (io.ReadSeekCloser, error)
OpenPreview(att *types.Attachment) (io.ReadSeekCloser, error)
}
attachmentHandler struct {
reg modulesHandlerRegistry
svc attachmentService
}
attachmentLookup interface {
GetAttachment() (bool, uint64, *types.Attachment)
}
)
func AttachmentHandler(reg modulesHandlerRegistry, svc attachmentService) *attachmentHandler {
h := &attachmentHandler{
reg: reg,
svc: svc,
}
h.register()
return h
}
func (h attachmentHandler) lookup(ctx context.Context, args *attachmentLookupArgs) (results *attachmentLookupResults, err error) {
results = &attachmentLookupResults{}
results.Attachment, err = h.svc.FindByID(ctx, 0, args.Attachment)
return
}
func (h attachmentHandler) delete(ctx context.Context, args *attachmentDeleteArgs) error {
return h.svc.DeleteByID(ctx, 0, args.Attachment)
}
func (h attachmentHandler) openOriginal(ctx context.Context, args *attachmentOpenOriginalArgs) (*attachmentOpenOriginalResults, error) {
att, err := lookupAttachment(ctx, h.svc, args)
if err != nil {
return nil, err
}
r := &attachmentOpenOriginalResults{}
r.Content, err = h.svc.OpenOriginal(att)
if err != nil {
return nil, err
}
// @todo we need to call Close() when file is read (or at the end of the workflow)
// some kind of workflow-cleanup facility is needed
return r, nil
}
func (h attachmentHandler) openPreview(ctx context.Context, args *attachmentOpenPreviewArgs) (*attachmentOpenPreviewResults, error) {
att, err := lookupAttachment(ctx, h.svc, args)
if err != nil {
return nil, err
}
r := &attachmentOpenPreviewResults{}
r.Content, err = h.svc.OpenOriginal(att)
if err != nil {
return nil, err
}
// @todo we need to call Close() when file is read (or at the end of the workflow)
// some kind of workflow-cleanup facility is needed
return r, nil
}
func (h attachmentHandler) create(ctx context.Context, args *attachmentCreateArgs) (*attachmentCreateResults, error) {
var (
err error
att *types.Attachment
fh io.ReadSeeker
size int64
)
switch {
case len(args.contentBytes) > 0:
size = int64(len(args.contentBytes))
fh = bytes.NewReader(args.contentBytes)
case args.contentStream != nil:
if rs, is := args.contentStream.(io.ReadSeeker); is {
_, err = rs.Seek(0, 0)
if err != nil {
return nil, err
}
size, err = getReaderSize(rs)
if err != nil {
return nil, err
}
_, err = rs.Seek(0, 0)
if err != nil {
return nil, err
}
fh = rs
} else {
// In case we only got a reader...
//
// For future proofing, for handling larger attachment, we create a temp.
// file which we then use as a reader
// Preparations
tmpf, err := ioutil.TempFile("", "reader")
if err != nil {
return nil, err
}
defer tmpf.Close()
defer os.Remove(tmpf.Name())
// Writing content to file
w := bufio.NewWriter(tmpf)
r := args.contentStream
buf := make([]byte, 1024)
for {
// read
n, err := r.Read(buf)
if err != nil && err != io.EOF {
return nil, err
}
if n == 0 {
break
}
// on-the-fly size calculation
size += int64(n)
// write
if _, err := w.Write(buf[:n]); err != nil {
return nil, err
}
}
if err = w.Flush(); err != nil {
return nil, err
}
_, err = tmpf.Seek(0, 0)
if err != nil {
return nil, err
}
fh = tmpf
}
default:
fh = strings.NewReader(args.contentString)
size = int64(len(args.contentString))
}
switch {
case args.Resource != nil:
att, err = h.svc.CreateRecordAttachment(
ctx,
args.Resource.NamespaceID,
args.Name,
size,
fh,
args.Resource.ModuleID,
args.Resource.ID,
args.FieldName,
)
default:
return nil, fmt.Errorf("unknown resource")
}
if err != nil {
return nil, err
}
return &attachmentCreateResults{att}, nil
}
func lookupAttachment(ctx context.Context, svc attachmentService, args attachmentLookup) (*types.Attachment, error) {
_, ID, attachment := args.GetAttachment()
switch {
case attachment != nil:
return attachment, nil
case ID > 0:
return svc.FindByID(ctx, 0, ID)
}
return nil, fmt.Errorf("empty attachment lookup params")
}
func getReaderSize(r io.Reader) (size int64, err error) {
buf := make([]byte, 1024)
var n int
for {
// read a chunk
n, err = r.Read(buf)
if err != nil && err != io.EOF {
return 0, err
}
if n == 0 {
break
}
size += int64(n)
}
return size, nil
}

View File

@@ -0,0 +1,99 @@
# Note: we're intentionally not prefixing this with "compose"
# due to plans in the (near) future to refactor attachments
# from system & compose components into one single service
imports:
- github.com/cortezaproject/corteza/server/compose/types
- io
labels: &labels
attachment: "step,workflow"
functions:
lookup:
meta:
short: Attachment lookup
description: Find specific attachment by ID
labels:
<<: *labels
params:
attachment:
meta: { label: Attachment }
required: true
types: [ { wf: ID } ]
results:
attachment: { wf: Attachment }
create:
meta:
short: Create file and attach it to a resource
labels:
<<: *labels
create: "step"
params:
name:
meta: { label: Name }
types:
- { wf: String }
resource:
meta: { label: Resource }
required: true
types:
- { wf: ComposeRecord }
fieldName:
meta: { label: Field name }
types:
- { wf: String }
content:
meta: { label: Content }
required: true
types:
- { wf: String, suffix: String }
- { wf: Reader, suffix: Stream }
- { wf: Bytes, suffix: Bytes }
results:
attachment: { wf: Attachment }
delete:
meta:
short: Delete attachment
labels:
<<: *labels
delete: "step"
params:
attachment:
meta: { label: Attachment }
required: true
types: [ { wf: ID } ]
openOriginal:
meta:
short: Open original attachment
labels:
<<: *labels
original-attachment: "step"
params:
attachment:
meta: { label: Attachment }
required: true
types: [ { wf: ID }, { wf: Attachment } ]
results:
content: { wf: Reader }
openPreview:
meta:
short: Open attachment preview
labels:
<<: *labels
preview-attachment: "step"
params:
attachment:
meta: { label: Attachment }
required: true
types: [ { wf: ID }, { wf: Attachment } ]
results:
content: { wf: Reader }

View File

@@ -0,0 +1,858 @@
package automation
// 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:
// compose/automation/expr_types.yaml
import (
"context"
"fmt"
"github.com/cortezaproject/corteza/server/compose/types"
. "github.com/cortezaproject/corteza/server/pkg/expr"
"sync"
)
var _ = context.Background
var _ = fmt.Errorf
// Attachment is an expression type, wrapper for *types.Attachment type
type Attachment struct {
value *types.Attachment
mux sync.RWMutex
}
// NewAttachment creates new instance of Attachment expression type
func NewAttachment(val interface{}) (*Attachment, error) {
if c, err := CastToAttachment(val); err != nil {
return nil, fmt.Errorf("unable to create Attachment: %w", err)
} else {
return &Attachment{value: c}, nil
}
}
// Get return underlying value on Attachment
func (t *Attachment) Get() interface{} {
t.mux.RLock()
defer t.mux.RUnlock()
return t.value
}
// GetValue returns underlying value on Attachment
func (t *Attachment) GetValue() *types.Attachment {
t.mux.RLock()
defer t.mux.RUnlock()
return t.value
}
// Type return type name
func (Attachment) Type() string { return "Attachment" }
// Cast converts value to *types.Attachment
func (Attachment) Cast(val interface{}) (TypedValue, error) {
return NewAttachment(val)
}
// Assign new value to Attachment
//
// value is first passed through CastToAttachment
func (t *Attachment) Assign(val interface{}) error {
if c, err := CastToAttachment(val); err != nil {
return err
} else {
t.value = c
return nil
}
}
func (t *Attachment) AssignFieldValue(key string, val TypedValue) error {
t.mux.Lock()
defer t.mux.Unlock()
return assignToAttachment(t.value, key, val)
}
// SelectGVal implements gval.Selector requirements
//
// It allows gval lib to access Attachment's underlying value (*types.Attachment)
// and it's fields
func (t *Attachment) SelectGVal(ctx context.Context, k string) (interface{}, error) {
t.mux.RLock()
defer t.mux.RUnlock()
return attachmentGValSelector(t.value, k)
}
// Select is field accessor for *types.Attachment
//
// Similar to SelectGVal but returns typed values
func (t *Attachment) Select(k string) (TypedValue, error) {
t.mux.RLock()
defer t.mux.RUnlock()
return attachmentTypedValueSelector(t.value, k)
}
func (t *Attachment) Has(k string) bool {
t.mux.RLock()
defer t.mux.RUnlock()
switch k {
case "ID":
return true
case "kind":
return true
case "url":
return true
case "previewUrl":
return true
case "name":
return true
case "createdAt":
return true
case "updatedAt":
return true
case "deletedAt":
return true
}
return false
}
// attachmentGValSelector is field accessor for *types.Attachment
func attachmentGValSelector(res *types.Attachment, k string) (interface{}, error) {
if res == nil {
return nil, nil
}
switch k {
case "ID":
return res.ID, nil
case "kind":
return res.Kind, nil
case "url":
return res.Url, nil
case "previewUrl":
return res.PreviewUrl, nil
case "name":
return res.Name, nil
case "createdAt":
return res.CreatedAt, nil
case "updatedAt":
return res.UpdatedAt, nil
case "deletedAt":
return res.DeletedAt, nil
}
return nil, fmt.Errorf("unknown field '%s'", k)
}
// attachmentTypedValueSelector is field accessor for *types.Attachment
func attachmentTypedValueSelector(res *types.Attachment, k string) (TypedValue, error) {
if res == nil {
return nil, nil
}
switch k {
case "ID":
return NewID(res.ID)
case "kind":
return NewString(res.Kind)
case "url":
return NewHandle(res.Url)
case "previewUrl":
return NewHandle(res.PreviewUrl)
case "name":
return NewHandle(res.Name)
case "createdAt":
return NewDateTime(res.CreatedAt)
case "updatedAt":
return NewDateTime(res.UpdatedAt)
case "deletedAt":
return NewDateTime(res.DeletedAt)
}
return nil, fmt.Errorf("unknown field '%s'", k)
}
// assignToAttachment is field value setter for *types.Attachment
//
// Value types are passed by pointer, otherwise assigned values are lost
func assignToAttachment(res *types.Attachment, k string, val interface{}) error {
switch k {
case "ID":
return fmt.Errorf("field '%s' is read-only", k)
case "kind":
aux, err := CastToString(val)
if err != nil {
return err
}
res.Kind = aux
return nil
case "url":
aux, err := CastToHandle(val)
if err != nil {
return err
}
res.Url = aux
return nil
case "previewUrl":
aux, err := CastToHandle(val)
if err != nil {
return err
}
res.PreviewUrl = aux
return nil
case "name":
aux, err := CastToHandle(val)
if err != nil {
return err
}
res.Name = aux
return nil
case "createdAt":
return fmt.Errorf("field '%s' is read-only", k)
case "updatedAt":
return fmt.Errorf("field '%s' is read-only", k)
case "deletedAt":
return fmt.Errorf("field '%s' is read-only", k)
}
return fmt.Errorf("unknown field '%s'", k)
}
// ComposeModule is an expression type, wrapper for *types.Module type
type ComposeModule struct {
value *types.Module
mux sync.RWMutex
}
// NewComposeModule creates new instance of ComposeModule expression type
func NewComposeModule(val interface{}) (*ComposeModule, error) {
if c, err := CastToComposeModule(val); err != nil {
return nil, fmt.Errorf("unable to create ComposeModule: %w", err)
} else {
return &ComposeModule{value: c}, nil
}
}
// Get return underlying value on ComposeModule
func (t *ComposeModule) Get() interface{} {
t.mux.RLock()
defer t.mux.RUnlock()
return t.value
}
// GetValue returns underlying value on ComposeModule
func (t *ComposeModule) GetValue() *types.Module {
t.mux.RLock()
defer t.mux.RUnlock()
return t.value
}
// Type return type name
func (ComposeModule) Type() string { return "ComposeModule" }
// Cast converts value to *types.Module
func (ComposeModule) Cast(val interface{}) (TypedValue, error) {
return NewComposeModule(val)
}
// Assign new value to ComposeModule
//
// value is first passed through CastToComposeModule
func (t *ComposeModule) Assign(val interface{}) error {
if c, err := CastToComposeModule(val); err != nil {
return err
} else {
t.value = c
return nil
}
}
func (t *ComposeModule) AssignFieldValue(key string, val TypedValue) error {
t.mux.Lock()
defer t.mux.Unlock()
return assignToComposeModule(t.value, key, val)
}
// SelectGVal implements gval.Selector requirements
//
// It allows gval lib to access ComposeModule's underlying value (*types.Module)
// and it's fields
func (t *ComposeModule) SelectGVal(ctx context.Context, k string) (interface{}, error) {
t.mux.RLock()
defer t.mux.RUnlock()
return composeModuleGValSelector(t.value, k)
}
// Select is field accessor for *types.Module
//
// Similar to SelectGVal but returns typed values
func (t *ComposeModule) Select(k string) (TypedValue, error) {
t.mux.RLock()
defer t.mux.RUnlock()
return composeModuleTypedValueSelector(t.value, k)
}
func (t *ComposeModule) Has(k string) bool {
t.mux.RLock()
defer t.mux.RUnlock()
switch k {
case "ID", "moduleID":
return true
case "namespaceID":
return true
case "name":
return true
case "handle":
return true
case "labels":
return true
case "createdAt":
return true
case "updatedAt":
return true
case "deletedAt":
return true
}
return false
}
// composeModuleGValSelector is field accessor for *types.Module
func composeModuleGValSelector(res *types.Module, k string) (interface{}, error) {
if res == nil {
return nil, nil
}
switch k {
case "ID", "moduleID":
return res.ID, nil
case "namespaceID":
return res.NamespaceID, nil
case "name":
return res.Name, nil
case "handle":
return res.Handle, nil
case "labels":
return res.Labels, nil
case "createdAt":
return res.CreatedAt, nil
case "updatedAt":
return res.UpdatedAt, nil
case "deletedAt":
return res.DeletedAt, nil
}
return nil, fmt.Errorf("unknown field '%s'", k)
}
// composeModuleTypedValueSelector is field accessor for *types.Module
func composeModuleTypedValueSelector(res *types.Module, k string) (TypedValue, error) {
if res == nil {
return nil, nil
}
switch k {
case "ID", "moduleID":
return NewID(res.ID)
case "namespaceID":
return NewID(res.NamespaceID)
case "name":
return NewString(res.Name)
case "handle":
return NewHandle(res.Handle)
case "labels":
return NewLabelValue(res.Labels)
case "createdAt":
return NewDateTime(res.CreatedAt)
case "updatedAt":
return NewDateTime(res.UpdatedAt)
case "deletedAt":
return NewDateTime(res.DeletedAt)
}
return nil, fmt.Errorf("unknown field '%s'", k)
}
// assignToComposeModule is field value setter for *types.Module
//
// Value types are passed by pointer, otherwise assigned values are lost
func assignToComposeModule(res *types.Module, k string, val interface{}) error {
switch k {
case "ID", "moduleID":
return fmt.Errorf("field '%s' is read-only", k)
case "namespaceID":
return fmt.Errorf("field '%s' is read-only", k)
case "name":
aux, err := CastToString(val)
if err != nil {
return err
}
res.Name = aux
return nil
case "handle":
aux, err := CastToHandle(val)
if err != nil {
return err
}
res.Handle = aux
return nil
case "labels":
aux, err := CastToLabelValue(val)
if err != nil {
return err
}
res.Labels = aux
return nil
case "createdAt":
return fmt.Errorf("field '%s' is read-only", k)
case "updatedAt":
return fmt.Errorf("field '%s' is read-only", k)
case "deletedAt":
return fmt.Errorf("field '%s' is read-only", k)
}
return fmt.Errorf("unknown field '%s'", k)
}
// ComposeNamespace is an expression type, wrapper for *types.Namespace type
type ComposeNamespace struct {
value *types.Namespace
mux sync.RWMutex
}
// NewComposeNamespace creates new instance of ComposeNamespace expression type
func NewComposeNamespace(val interface{}) (*ComposeNamespace, error) {
if c, err := CastToComposeNamespace(val); err != nil {
return nil, fmt.Errorf("unable to create ComposeNamespace: %w", err)
} else {
return &ComposeNamespace{value: c}, nil
}
}
// Get return underlying value on ComposeNamespace
func (t *ComposeNamespace) Get() interface{} {
t.mux.RLock()
defer t.mux.RUnlock()
return t.value
}
// GetValue returns underlying value on ComposeNamespace
func (t *ComposeNamespace) GetValue() *types.Namespace {
t.mux.RLock()
defer t.mux.RUnlock()
return t.value
}
// Type return type name
func (ComposeNamespace) Type() string { return "ComposeNamespace" }
// Cast converts value to *types.Namespace
func (ComposeNamespace) Cast(val interface{}) (TypedValue, error) {
return NewComposeNamespace(val)
}
// Assign new value to ComposeNamespace
//
// value is first passed through CastToComposeNamespace
func (t *ComposeNamespace) Assign(val interface{}) error {
if c, err := CastToComposeNamespace(val); err != nil {
return err
} else {
t.value = c
return nil
}
}
func (t *ComposeNamespace) AssignFieldValue(key string, val TypedValue) error {
t.mux.Lock()
defer t.mux.Unlock()
return assignToComposeNamespace(t.value, key, val)
}
// SelectGVal implements gval.Selector requirements
//
// It allows gval lib to access ComposeNamespace's underlying value (*types.Namespace)
// and it's fields
func (t *ComposeNamespace) SelectGVal(ctx context.Context, k string) (interface{}, error) {
t.mux.RLock()
defer t.mux.RUnlock()
return composeNamespaceGValSelector(t.value, k)
}
// Select is field accessor for *types.Namespace
//
// Similar to SelectGVal but returns typed values
func (t *ComposeNamespace) Select(k string) (TypedValue, error) {
t.mux.RLock()
defer t.mux.RUnlock()
return composeNamespaceTypedValueSelector(t.value, k)
}
func (t *ComposeNamespace) Has(k string) bool {
t.mux.RLock()
defer t.mux.RUnlock()
switch k {
case "ID", "namespaceID":
return true
case "name":
return true
case "slug", "handle":
return true
case "labels":
return true
case "createdAt":
return true
case "updatedAt":
return true
case "deletedAt":
return true
}
return false
}
// composeNamespaceGValSelector is field accessor for *types.Namespace
func composeNamespaceGValSelector(res *types.Namespace, k string) (interface{}, error) {
if res == nil {
return nil, nil
}
switch k {
case "ID", "namespaceID":
return res.ID, nil
case "name":
return res.Name, nil
case "slug", "handle":
return res.Slug, nil
case "labels":
return res.Labels, nil
case "createdAt":
return res.CreatedAt, nil
case "updatedAt":
return res.UpdatedAt, nil
case "deletedAt":
return res.DeletedAt, nil
}
return nil, fmt.Errorf("unknown field '%s'", k)
}
// composeNamespaceTypedValueSelector is field accessor for *types.Namespace
func composeNamespaceTypedValueSelector(res *types.Namespace, k string) (TypedValue, error) {
if res == nil {
return nil, nil
}
switch k {
case "ID", "namespaceID":
return NewID(res.ID)
case "name":
return NewString(res.Name)
case "slug", "handle":
return NewHandle(res.Slug)
case "labels":
return NewLabelValue(res.Labels)
case "createdAt":
return NewDateTime(res.CreatedAt)
case "updatedAt":
return NewDateTime(res.UpdatedAt)
case "deletedAt":
return NewDateTime(res.DeletedAt)
}
return nil, fmt.Errorf("unknown field '%s'", k)
}
// assignToComposeNamespace is field value setter for *types.Namespace
//
// Value types are passed by pointer, otherwise assigned values are lost
func assignToComposeNamespace(res *types.Namespace, k string, val interface{}) error {
switch k {
case "ID", "namespaceID":
return fmt.Errorf("field '%s' is read-only", k)
case "name":
aux, err := CastToString(val)
if err != nil {
return err
}
res.Name = aux
return nil
case "slug", "handle":
aux, err := CastToHandle(val)
if err != nil {
return err
}
res.Slug = aux
return nil
case "labels":
aux, err := CastToLabelValue(val)
if err != nil {
return err
}
res.Labels = aux
return nil
case "createdAt":
return fmt.Errorf("field '%s' is read-only", k)
case "updatedAt":
return fmt.Errorf("field '%s' is read-only", k)
case "deletedAt":
return fmt.Errorf("field '%s' is read-only", k)
}
return fmt.Errorf("unknown field '%s'", k)
}
// ComposeRecord is an expression type, wrapper for *types.Record type
type ComposeRecord struct {
value *types.Record
mux sync.RWMutex
}
// NewComposeRecord creates new instance of ComposeRecord expression type
func NewComposeRecord(val interface{}) (*ComposeRecord, error) {
if c, err := CastToComposeRecord(val); err != nil {
return nil, fmt.Errorf("unable to create ComposeRecord: %w", err)
} else {
return &ComposeRecord{value: c}, nil
}
}
// Get return underlying value on ComposeRecord
func (t *ComposeRecord) Get() interface{} {
t.mux.RLock()
defer t.mux.RUnlock()
return t.value
}
// GetValue returns underlying value on ComposeRecord
func (t *ComposeRecord) GetValue() *types.Record {
t.mux.RLock()
defer t.mux.RUnlock()
return t.value
}
// Type return type name
func (ComposeRecord) Type() string { return "ComposeRecord" }
// Cast converts value to *types.Record
func (ComposeRecord) Cast(val interface{}) (TypedValue, error) {
return NewComposeRecord(val)
}
// Assign new value to ComposeRecord
//
// value is first passed through CastToComposeRecord
func (t *ComposeRecord) Assign(val interface{}) error {
if c, err := CastToComposeRecord(val); err != nil {
return err
} else {
t.value = c
return nil
}
}
func (t *ComposeRecord) Has(k string) bool {
t.mux.RLock()
defer t.mux.RUnlock()
switch k {
case "ID", "recordID":
return true
case "moduleID":
return true
case "namespaceID":
return true
case "values":
return true
case "meta":
return true
case "ownedBy":
return true
case "createdAt":
return true
case "createdBy":
return true
case "updatedAt":
return true
case "updatedBy":
return true
case "deletedAt":
return true
case "deletedBy":
return true
}
return false
}
// composeRecordGValSelector is field accessor for *types.Record
func composeRecordGValSelector(res *types.Record, k string) (interface{}, error) {
if res == nil {
return nil, nil
}
switch k {
case "ID", "recordID":
return res.ID, nil
case "moduleID":
return res.ModuleID, nil
case "namespaceID":
return res.NamespaceID, nil
case "values":
return res.Values, nil
case "meta":
return res.Meta, nil
case "ownedBy":
return res.OwnedBy, nil
case "createdAt":
return res.CreatedAt, nil
case "createdBy":
return res.CreatedBy, nil
case "updatedAt":
return res.UpdatedAt, nil
case "updatedBy":
return res.UpdatedBy, nil
case "deletedAt":
return res.DeletedAt, nil
case "deletedBy":
return res.DeletedBy, nil
}
return nil, fmt.Errorf("unknown field '%s'", k)
}
// composeRecordTypedValueSelector is field accessor for *types.Record
func composeRecordTypedValueSelector(res *types.Record, k string) (TypedValue, error) {
if res == nil {
return nil, nil
}
switch k {
case "ID", "recordID":
return NewID(res.ID)
case "moduleID":
return NewID(res.ModuleID)
case "namespaceID":
return NewID(res.NamespaceID)
case "values":
return NewComposeRecordValues(res.Values)
case "meta":
return NewMeta(res.Meta)
case "ownedBy":
return NewID(res.OwnedBy)
case "createdAt":
return NewDateTime(res.CreatedAt)
case "createdBy":
return NewID(res.CreatedBy)
case "updatedAt":
return NewDateTime(res.UpdatedAt)
case "updatedBy":
return NewID(res.UpdatedBy)
case "deletedAt":
return NewDateTime(res.DeletedAt)
case "deletedBy":
return NewID(res.DeletedBy)
}
return nil, fmt.Errorf("unknown field '%s'", k)
}
// assignToComposeRecord is field value setter for *types.Record
//
// Value types are passed by pointer, otherwise assigned values are lost
func assignToComposeRecord(res *types.Record, k string, val interface{}) error {
switch k {
case "ID", "recordID":
return fmt.Errorf("field '%s' is read-only", k)
case "moduleID":
return fmt.Errorf("field '%s' is read-only", k)
case "namespaceID":
return fmt.Errorf("field '%s' is read-only", k)
case "values":
aux, err := CastToComposeRecordValues(val)
if err != nil {
return err
}
res.Values = aux
return nil
case "meta":
aux, err := CastToMeta(val)
if err != nil {
return err
}
res.Meta = aux
return nil
case "ownedBy":
aux, err := CastToID(val)
if err != nil {
return err
}
res.OwnedBy = aux
return nil
case "createdAt":
return fmt.Errorf("field '%s' is read-only", k)
case "createdBy":
return fmt.Errorf("field '%s' is read-only", k)
case "updatedAt":
return fmt.Errorf("field '%s' is read-only", k)
case "updatedBy":
return fmt.Errorf("field '%s' is read-only", k)
case "deletedAt":
return fmt.Errorf("field '%s' is read-only", k)
case "deletedBy":
return fmt.Errorf("field '%s' is read-only", k)
}
return fmt.Errorf("unknown field '%s'", k)
}
// ComposeRecordValueErrorSet is an expression type, wrapper for *types.RecordValueErrorSet type
type ComposeRecordValueErrorSet struct {
value *types.RecordValueErrorSet
mux sync.RWMutex
}
// NewComposeRecordValueErrorSet creates new instance of ComposeRecordValueErrorSet expression type
func NewComposeRecordValueErrorSet(val interface{}) (*ComposeRecordValueErrorSet, error) {
if c, err := CastToComposeRecordValueErrorSet(val); err != nil {
return nil, fmt.Errorf("unable to create ComposeRecordValueErrorSet: %w", err)
} else {
return &ComposeRecordValueErrorSet{value: c}, nil
}
}
// Get return underlying value on ComposeRecordValueErrorSet
func (t *ComposeRecordValueErrorSet) Get() interface{} {
t.mux.RLock()
defer t.mux.RUnlock()
return t.value
}
// GetValue returns underlying value on ComposeRecordValueErrorSet
func (t *ComposeRecordValueErrorSet) GetValue() *types.RecordValueErrorSet {
t.mux.RLock()
defer t.mux.RUnlock()
return t.value
}
// Type return type name
func (ComposeRecordValueErrorSet) Type() string { return "ComposeRecordValueErrorSet" }
// Cast converts value to *types.RecordValueErrorSet
func (ComposeRecordValueErrorSet) Cast(val interface{}) (TypedValue, error) {
return NewComposeRecordValueErrorSet(val)
}
// Assign new value to ComposeRecordValueErrorSet
//
// value is first passed through CastToComposeRecordValueErrorSet
func (t *ComposeRecordValueErrorSet) Assign(val interface{}) error {
if c, err := CastToComposeRecordValueErrorSet(val); err != nil {
return err
} else {
t.value = c
return nil
}
}

View File

@@ -0,0 +1,817 @@
package automation
import (
"context"
"encoding/json"
"fmt"
"reflect"
"time"
"github.com/PaesslerAG/gval"
"github.com/cortezaproject/corteza/server/compose/types"
"github.com/cortezaproject/corteza/server/pkg/expr"
"github.com/cortezaproject/corteza/server/pkg/slice"
"github.com/spf13/cast"
)
func CastToComposeNamespace(val interface{}) (out *types.Namespace, err error) {
switch val := val.(type) {
case expr.Iterator:
out = &types.Namespace{}
return out, val.Each(func(k string, v expr.TypedValue) error {
return assignToComposeNamespace(out, k, v)
})
}
switch val := expr.UntypedValue(val).(type) {
case *types.Namespace:
return val, nil
case map[string]interface{}:
out = &types.Namespace{}
m, _ := json.Marshal(val)
_ = json.Unmarshal(m, out)
return
default:
return nil, fmt.Errorf("unable to cast type %T to %T", val, out)
}
}
func CastToComposeModule(val interface{}) (out *types.Module, err error) {
switch val := val.(type) {
case expr.Iterator:
out = &types.Module{}
return out, val.Each(func(k string, v expr.TypedValue) error {
return assignToComposeModule(out, k, v)
})
}
switch val := expr.UntypedValue(val).(type) {
case *types.Module:
return val, nil
case map[string]interface{}:
out = &types.Module{}
m, _ := json.Marshal(val)
_ = json.Unmarshal(m, out)
return
default:
return nil, fmt.Errorf("unable to cast type %T to %T", val, out)
}
}
func CastToComposeRecord(val interface{}) (out *types.Record, err error) {
switch val := val.(type) {
case expr.Iterator:
out = &types.Record{}
return out, val.Each(func(k string, v expr.TypedValue) error {
return assignToComposeRecord(out, k, v)
})
}
switch val := expr.UntypedValue(val).(type) {
case *types.Record:
if val == nil {
val = &types.Record{}
}
return val, nil
case map[string]interface{}:
out = &types.Record{}
m, _ := json.Marshal(val)
_ = json.Unmarshal(m, out)
return
default:
return nil, fmt.Errorf("unable to cast type %T to %T", val, out)
}
}
var _ expr.DeepFieldAssigner = &ComposeRecord{}
// AssignFieldValue implements expr.DeepFieldAssigner
//
// We need to reroute value assigning for record-value-sets because
// we loose the reference to record-value slice
func (t *ComposeRecord) AssignFieldValue(p expr.Pather, val expr.TypedValue) (err error) {
t.mux.Lock()
defer t.mux.Unlock()
switch p.Get() {
case "values":
err = p.Next()
if err != nil {
return
}
return assignToComposeRecordValues(t.value, p, val)
// case "labels":
// @todo deep setting labels
default:
return assignToComposeRecord(t.value, p.Get(), val)
}
}
var _ gval.Selector = &ComposeRecord{}
// SelectGVal implements gval.Selector requirements
//
// It allows gval lib to access Record's underlying value (*types.Record)
// and it's fields
func (t *ComposeRecord) SelectGVal(_ context.Context, k string) (interface{}, error) {
t.mux.RLock()
defer t.mux.RUnlock()
if t.value != nil && k == "values" {
if t.value.Values == nil {
t.value.Values = types.RecordValueSet{}
}
return &ComposeRecordValues{t.value}, nil
}
return composeRecordGValSelector(t.value, k)
}
// Select is field accessor for *types.ComposeRecord
//
// Similar to SelectGVal but returns typed values
func (t *ComposeRecord) Select(k string) (expr.TypedValue, error) {
t.mux.RLock()
defer t.mux.RUnlock()
if t.value != nil && k == "values" {
if t.value.Values == nil {
t.value.Values = types.RecordValueSet{}
}
return &ComposeRecordValues{value: t.value}, nil
}
return composeRecordTypedValueSelector(t.value, k)
}
type ComposeRecordValues struct{ value *types.Record }
func CastToComposeRecordValues(val interface{}) (out types.RecordValueSet, err error) {
out = types.RecordValueSet{}
switch val := val.(type) {
case expr.Iterator:
return out, val.Each(func(k string, v expr.TypedValue) error {
// try with slice of strings first:
if v.Type() == "String" {
out = out.Set(&types.RecordValue{Name: k, Value: v.Get().(string)})
return nil
}
if ss, err := cast.ToStringSliceE(expr.UntypedValue(v)); err == nil {
for i, v := range ss {
out = out.Set(&types.RecordValue{Name: k, Value: v, Place: uint(i)})
}
return nil
}
if str, err := expr.CastToString(v); err != nil {
return err
} else {
out = out.Set(&types.RecordValue{Name: k, Value: str})
return nil
}
})
}
switch val := expr.UntypedValue(val).(type) {
case *types.Record:
return val.Values, nil
case *types.RecordValue:
return types.RecordValueSet{val}, nil
case types.RecordValueSet:
return val, nil
case map[string]string:
for _, k := range slice.Keys(val) {
out = out.Set(&types.RecordValue{Name: k, Value: val[k]})
}
return
case map[string][]string:
for _, k := range slice.Keys(val) {
for i, v := range val[k] {
out = out.Set(&types.RecordValue{Name: k, Value: v, Place: uint(i)})
}
}
return
case map[string]interface{}:
for _, k := range slice.Keys(val) {
if isNil(val[k]) {
continue
}
var vv []interface{}
// covering a couple of typed slices
ref := reflect.ValueOf(val[k])
if ref.Kind() == reflect.Slice {
for i := 0; i < ref.Len(); i++ {
vv = append(vv, ref.Index(i).Interface())
}
} else {
vv = []interface{}{val[k]}
}
i := 0
for _, value := range vv {
switch v := value.(type) {
// a small exception for boolean fields, don't add false values and when its true cast to 1
case bool:
if !v {
continue
} else {
value = "1"
}
}
out = out.Set(&types.RecordValue{Name: k, Value: cast.ToString(value), Place: uint(i)})
// explicitly counting because of how booleans are handled
i++
}
}
return
default:
return nil, fmt.Errorf("unable to cast type %T to %T", val, out)
}
}
func (t *ComposeRecordValues) AssignFieldValue(p expr.Pather, val expr.TypedValue) error {
return assignToComposeRecordValues(t.value, p, val)
}
// SelectGVal implements gval.Selector requirements
//
// It allows gval lib to access Record's underlying value (*types.RecordValues)
// and it's fields
func (t *ComposeRecordValues) SelectGVal(_ context.Context, k string) (interface{}, error) {
return composeRecordValuesGValSelector(t.value, k)
}
// IsEmpty implements pkg/expr.empty requirements to be able to determine if
// the value is empty.
//
// This is needed cor cases when we are working with empty records, but are trying
// to access their values.
func (t *ComposeRecordValues) IsEmpty() bool {
return t == nil || t.value == nil || len(t.value.Values) == 0
}
// Select is field accessor for *types.Record
//
// Similar to SelectGVal but returns typed values
func (t *ComposeRecordValues) Select(k string) (expr.TypedValue, error) {
return composeRecordValuesTypedValueSelector(t.value, k)
}
func (t ComposeRecordValues) Has(k string) bool {
return t.value.Values.Get(k, 0) != nil
}
// recordGValSelector is field accessor for *types.RecordValueSet
func composeRecordValuesGValSelector(res *types.Record, k string) (interface{}, error) {
var (
vv = res.Values.FilterByName(k)
multiValueField bool
field *types.ModuleField
)
if mod := res.GetModule(); mod != nil {
field = mod.Fields.FindByName(k)
if field == nil {
return nil, fmt.Errorf("field '%s' does not exist on module %s", k, mod.Name)
}
multiValueField = field.Multi
}
switch {
case len(vv) == 0:
if field != nil && field.IsBoolean() {
return false, nil
}
return nil, nil
case len(vv) == 1 && !multiValueField:
return vv[0].Cast(field)
default:
out := make([]interface{}, 0, len(vv))
return out, vv.Walk(func(v *types.RecordValue) error {
i, err := v.Cast(field)
if err != nil {
return err
}
out = append(out, i)
return nil
})
}
}
// recordValuesTypedValueSelector is field accessor for *types.RecordValueSet
func composeRecordValuesTypedValueSelector(res *types.Record, k string) (expr.TypedValue, error) {
var (
vv = res.Values.FilterByName(k)
multiValueField bool
field *types.ModuleField
)
if mod := res.GetModule(); mod != nil {
field = mod.Fields.FindByName(k)
if field == nil {
return nil, fmt.Errorf("field '%s' does not exist on module %s", k, mod.Name)
}
multiValueField = field.Multi
}
switch {
case len(vv) == 0:
if field != nil && field.IsBoolean() {
return &expr.Boolean{}, nil
}
return nil, nil
case len(vv) == 1 && !multiValueField:
return recordValueToExprTypedValue(field, vv[0])
default:
return recordValueSetToExprArray(field, vv...)
}
}
// assignToRecordValuesSet is field value setter for *types.RecordValueSet
//
// We'll be using types.Record for the base (and not types.RecordValueSet)
func assignToComposeRecordValues(res *types.Record, p expr.Pather, val interface{}) (err error) {
if p == nil || !p.More() {
switch val := val.(type) {
case *ComposeRecordValues:
res.Values = val.value.Values
return
case *ComposeRecord:
*res = *val.value
return
}
switch val := expr.UntypedValue(val).(type) {
case types.RecordValueSet:
res.Values = val
return
case *types.Record:
*res = *val
return
}
return fmt.Errorf("empty path used for assigning record values")
}
var (
k = p.Get()
rv = &types.RecordValue{Name: k}
setSliceOfValues = func(vv []interface{}) error {
// Handle situation where array of values is assigned to a single (multi-value) field
// @todo this should use field context (when available) to determinate if we're actually
// setting array to a multi-value field
if !p.IsLast() {
// Tying to assign an array of values to a single value; that will not work
return fmt.Errorf("can not assign array of values to a single value in a record value set")
}
if ss, err := cast.ToStringSliceE(vv); err != nil {
return err
} else {
res.Values = res.Values.Replace(k, ss...)
}
return nil
}
)
// @todo this needs to be implemented properly
// we're just guessing here and putting out fires
switch utval := expr.UntypedValue(val).(type) {
case time.Time:
rv.Value = utval.Format(time.RFC3339)
case *time.Time:
if utval == nil {
rv.Value = ""
break
}
rv.Value = utval.Format(time.RFC3339)
case []string:
aux := make([]interface{}, len(utval))
for i := range utval {
aux[i] = utval[i]
}
return setSliceOfValues(aux)
case []expr.TypedValue: // expr.Array
aux := make([]interface{}, len(utval))
for i := range utval {
aux[i] = utval[i].Get()
}
return setSliceOfValues(aux)
case []interface{}: // expr.Any
return setSliceOfValues(utval)
default:
rv.Value, err = cast.ToStringE(utval)
}
if err != nil {
return
}
if !p.IsLast() {
err = p.Next()
if err != nil {
return
}
if rv.Place, err = cast.ToUintE(expr.UntypedValue(p.Get())); err != nil {
return fmt.Errorf("failed to decode record value place from '%s': %w", p.String(), err)
}
}
res.Values = res.Values.Set(rv)
return nil
}
// NewComposeRecordValues creates new instance of ComposeRecordValues expression type
func NewComposeRecordValues(val interface{}) (*ComposeRecordValues, error) {
if c, err := CastToComposeRecordValues(val); err != nil {
return nil, fmt.Errorf("unable to create ComposeRecordValues: %w", err)
} else {
return &ComposeRecordValues{value: &types.Record{Values: c}}, nil
}
}
// Return underlying value on ComposeRecordValues
func (t ComposeRecordValues) Get() interface{} { return t.value }
// Return underlying value on ComposeRecordValues
func (t ComposeRecordValues) GetValue() types.RecordValueSet { return t.value.Values }
// Return type name
func (ComposeRecordValues) Type() string { return "ComposeRecordValues" }
// Convert value to types.RecordValueSet
func (ComposeRecordValues) Cast(val interface{}) (expr.TypedValue, error) {
return NewComposeRecordValues(val)
}
// Assign new value to ComposeRecordValues
//
// value is first passed through CastToComposeRecordValues
func (t *ComposeRecordValues) Assign(val interface{}) error {
if c, err := CastToComposeRecordValues(val); err != nil {
return err
} else {
t.value.Values = c
return nil
}
}
func CastToComposeRecordValueErrorSet(val interface{}) (out *types.RecordValueErrorSet, err error) {
switch val := expr.UntypedValue(val).(type) {
case *types.RecordValueErrorSet:
return val, nil
default:
return nil, fmt.Errorf("unable to cast type %T to %T", val, out)
}
}
func recordValueToExprTypedValue(field *types.ModuleField, rv *types.RecordValue) (expr.TypedValue, error) {
if v, err := rv.Cast(field); err != nil {
return nil, err
} else {
return expr.Typify(v)
}
}
func recordValueSetToExprArray(field *types.ModuleField, vv ...*types.RecordValue) (arr *expr.Array, err error) {
var (
tv expr.TypedValue
)
arr = &expr.Array{}
for _, v := range vv {
tv, err = recordValueToExprTypedValue(field, v)
if err != nil {
return
}
arr.Push(tv)
}
return
}
func isNil(i interface{}) bool {
if i == nil {
return true
}
switch reflect.TypeOf(i).Kind() {
case reflect.Slice, reflect.Array, reflect.Ptr, reflect.Map:
return reflect.ValueOf(i).IsNil()
}
return false
}
func CastToAttachment(val interface{}) (out *types.Attachment, err error) {
switch val := expr.UntypedValue(val).(type) {
case *types.Attachment:
return val, nil
default:
return nil, fmt.Errorf("unable to cast type %T to %T", val, out)
}
}
func EmptyComposeRecordValues() *ComposeRecordValues {
return &ComposeRecordValues{value: &types.Record{Values: types.RecordValueSet{}}}
}
func (t *ComposeRecordValues) Each(fn func(k string, v expr.TypedValue) error) (err error) {
if t.IsEmpty() {
return
}
// Helper to check if field is a multi value or not; defaulting to false
isMulti := func(name string) (multi bool) {
m := t.value.GetModule()
if m == nil {
return
}
if m.Fields == nil {
return
}
f := m.Fields.FindByName(name)
if f == nil {
return
}
return f.Multi
}
// RecordValues can have multi value fields which need to be presented as an Array
lastKey := ""
values := []expr.TypedValue{}
// Processes the current set of values keeping multi values in mind
processChunk := func() (err error) {
if isMulti(lastKey) {
ar := &expr.Array{}
for _, x := range values {
ar.Push(x)
}
if err = fn(lastKey, ar); err != nil {
return
}
} else {
for _, v := range values {
if err = fn(lastKey, v); err != nil {
return
}
}
}
return
}
for _, vv := range t.GetValue() {
key := vv.Name
if len(key) == 0 {
continue
}
// Reset placement since it's not valid on non multi value fields
if !isMulti(key) {
vv.Place = 0
}
// Mismatch, process prev values
if key != lastKey {
err = processChunk()
if err != nil {
return
}
values = values[0:0]
lastKey = key
}
var val expr.TypedValue
val, err = expr.Typify(vv)
values = append(values, val)
}
err = processChunk()
if err != nil {
return
}
return
}
// Merge combines the given ComposeRecordValues into ComposeRecordValues
// NOTE: It will return CLONE of the original ComposeRecordValues, if it's called without any parameters
func (t *ComposeRecordValues) Merge(nn ...expr.Iterator) (out expr.TypedValue, err error) {
rv := EmptyComposeRecordValues()
if t.value == nil {
t.value = &types.Record{}
}
rv.value.ID = t.value.ID
rv.value.ModuleID = t.value.ModuleID
rv.value.SetModule(t.value.GetModule())
rv.value.NamespaceID = t.value.NamespaceID
nn = append([]expr.Iterator{t}, nn...)
procValue := func(v expr.TypedValue) (err error) {
var (
rVal types.RecordValue
bb []byte
)
// @todo implement casting of RecordValue from TypedValue
bb, err = json.Marshal(v.Get())
if err != nil {
return err
}
err = json.Unmarshal(bb, &rVal)
if err != nil {
return err
}
rr := rv.GetValue().Set(&rVal)
err = rv.Assign(rr)
if err != nil {
return err
}
return
}
for _, i := range nn {
err = i.Each(func(k string, v expr.TypedValue) error {
if v.Type() == "Array" {
// This holds since it's an array
for _, v := range (v.(*expr.Array)).Get().([]expr.TypedValue) {
err = procValue(v)
if err != nil {
return err
}
}
} else {
err = procValue(v)
if err != nil {
return err
}
}
return err
})
if err != nil {
return
}
}
return rv, nil
}
func (t *ComposeRecordValues) Filter(keys ...string) (out expr.TypedValue, err error) {
if t.IsEmpty() {
return
}
// get cloned ComposeRecordValues
out, err = t.Merge()
if err != nil {
return
}
rv := out.(*ComposeRecordValues)
if !rv.IsEmpty() {
rv.value.Values = types.RecordValueSet{}
}
keyMap := make(map[string]string)
for _, k := range keys {
keyMap[k] = k
}
// Push the only with values with matching Name
for _, val := range t.GetValue() {
_, ok := keyMap[val.Name]
if ok {
rr := rv.GetValue().Set(val.Clone())
err = rv.Assign(rr)
if err != nil {
return out, err
}
}
}
return rv, nil
}
func (t *ComposeRecordValues) Delete(keys ...string) (out expr.TypedValue, err error) {
if t.IsEmpty() {
return
}
// get cloned ComposeRecordValues
out, err = t.Merge()
if err != nil {
return
}
rv := out.(*ComposeRecordValues)
if !rv.IsEmpty() {
rv.value.Values = types.RecordValueSet{}
}
keyMap := make(map[string]string)
for _, k := range keys {
keyMap[k] = k
}
// Push the only with values with non-matching Name
for _, val := range t.GetValue() {
_, ok := keyMap[val.Name]
if !ok {
rr := rv.GetValue().Set(val.Clone())
err = rv.Assign(rr)
if err != nil {
return out, err
}
}
}
return rv, nil
}
func (v *Attachment) Clone() (expr.TypedValue, error) {
att := *v.value
aux, err := NewAttachment(&att)
return aux, err
}
func (v *ComposeModule) Clone() (expr.TypedValue, error) {
aux, err := NewComposeModule(v.value.Clone())
return aux, err
}
func (v *ComposeNamespace) Clone() (expr.TypedValue, error) {
aux, err := NewComposeNamespace(v.value.Clone())
return aux, err
}
func (v *ComposeRecord) Clone() (expr.TypedValue, error) {
aux, err := NewComposeRecord(v.value.Clone())
return aux, err
}
func (v *ComposeRecordValues) Clone() (expr.TypedValue, error) {
aux, err := NewComposeRecordValues(v.value.Clone())
return aux, err
}
func (v *ComposeRecordValueErrorSet) Clone() (expr.TypedValue, error) {
if v.value == nil {
return nil, nil
}
errs := types.RecordValueErrorSet{
Set: make([]types.RecordValueError, len(v.value.Set)),
}
for i, v := range v.value.Set {
errs.Set[i] = v
}
aux, err := NewComposeRecordValueErrorSet(&errs)
return aux, err
}

View File

@@ -0,0 +1,64 @@
package: automation
imports:
- github.com/cortezaproject/corteza/server/compose/types
types:
ComposeNamespace:
as: '*types.Namespace'
struct:
- { name: 'ID', exprType: 'ID', goType: 'uint64', mode: ro, alias: 'namespaceID' }
- { name: 'name', exprType: 'String', goType: 'string' }
- { name: 'slug', exprType: 'Handle', goType: 'string', alias: 'handle'}
- { name: 'labels', exprType: 'LabelValue', goType: 'map[string]labelTypes.LabelValue' }
- { name: 'createdAt', exprType: 'DateTime', goType: 'time.Time', mode: ro }
- { name: 'updatedAt', exprType: 'DateTime', goType: '*time.Time', mode: ro }
- { name: 'deletedAt', exprType: 'DateTime', goType: '*time.Time', mode: ro }
ComposeModule:
as: '*types.Module'
struct:
- { name: 'ID', exprType: 'ID', goType: 'uint64', mode: ro, alias: 'moduleID' }
- { name: 'namespaceID', exprType: 'ID', goType: 'uint64', mode: ro }
- { name: 'name', exprType: 'String', goType: 'string' }
- { name: 'handle', exprType: 'Handle', goType: 'string' }
- { name: 'labels', exprType: 'LabelValue', goType: 'map[string]labelTypes.LabelValue' }
- { name: 'createdAt', exprType: 'DateTime', goType: 'time.Time', mode: ro }
- { name: 'updatedAt', exprType: 'DateTime', goType: '*time.Time', mode: ro }
- { name: 'deletedAt', exprType: 'DateTime', goType: '*time.Time', mode: ro }
ComposeRecord:
as: '*types.Record'
struct:
- { name: 'ID', exprType: 'ID', goType: 'uint64', mode: ro, alias: 'recordID' }
- { name: 'moduleID', exprType: 'ID', goType: 'uint64', mode: ro }
- { name: 'namespaceID', exprType: 'ID', goType: 'uint64', mode: ro }
- { name: 'values', exprType: 'ComposeRecordValues', goType: 'map[string]interface{}' }
- { name: 'meta', exprType: 'Meta', goType: 'map[string]any' }
- { name: 'ownedBy', exprType: 'ID', goType: 'uint64' }
- { name: 'createdAt', exprType: 'DateTime', goType: 'time.Time', mode: ro }
- { name: 'createdBy', exprType: 'ID', goType: 'uint64', mode: ro }
- { name: 'updatedAt', exprType: 'DateTime', goType: '*time.Time', mode: ro }
- { name: 'updatedBy', exprType: 'ID', goType: 'uint64', mode: ro }
- { name: 'deletedAt', exprType: 'DateTime', goType: '*time.Time', mode: ro }
- { name: 'deletedBy', exprType: 'ID', goType: 'uint64', mode: ro }
customSelector: true
customGValSelector: true
customFieldAssigner: true
ComposeRecordValues:
customType: true
as: 'types.RecordValueSet'
ComposeRecordValueErrorSet:
as: '*types.RecordValueErrorSet'
Attachment:
as: '*types.Attachment'
struct:
- { name: 'ID', exprType: 'ID', goType: 'uint64', mode: ro}
- { name: 'kind', exprType: 'String', goType: 'string' }
- { name: 'url', exprType: 'Handle', goType: 'string' }
- { name: 'previewUrl', exprType: 'Handle', goType: 'string' }
- { name: 'name', exprType: 'Handle', goType: 'string' }
- { name: 'createdAt', exprType: 'DateTime', goType: 'time.Time', mode: ro }
- { name: 'updatedAt', exprType: 'DateTime', goType: '*time.Time', mode: ro }
- { name: 'deletedAt', exprType: 'DateTime', goType: '*time.Time', mode: ro }

View File

@@ -0,0 +1,749 @@
package automation
import (
"context"
"fmt"
"testing"
"time"
aTypes "github.com/cortezaproject/corteza/server/automation/types"
"github.com/cortezaproject/corteza/server/compose/types"
"github.com/cortezaproject/corteza/server/pkg/expr"
"github.com/stretchr/testify/require"
)
func TestSetRecordValuesWithPath(t *testing.T) {
t.Run("some basic aerobics", func(t *testing.T) {
var (
r = require.New(t)
rvs = &ComposeRecordValues{&types.Record{}}
)
r.NoError(expr.Assign(rvs, "field1", expr.Must(expr.NewString("a"))))
r.NoError(expr.Assign(rvs, "field1.1", expr.Must(expr.NewString("a"))))
r.True(rvs.value.Values.Has("field1", 0))
r.True(rvs.value.Values.Has("field1", 1))
})
}
func TestRecordFieldValuesAccess(t *testing.T) {
var (
err error
v expr.TypedValue
mod = &types.Module{Fields: types.ModuleFieldSet{
&types.ModuleField{Name: "s1", Multi: false, Kind: "String"},
&types.ModuleField{Name: "m1", Multi: true, Kind: "String"},
&types.ModuleField{Name: "m2", Multi: true, Kind: "String"},
&types.ModuleField{Name: "s2", Multi: false, Kind: "String"},
&types.ModuleField{Name: "b0", Multi: false, Kind: "Bool"},
&types.ModuleField{Name: "b1", Multi: false, Kind: "Bool"},
&types.ModuleField{Name: "n1", Multi: false, Kind: "Number"},
&types.ModuleField{Name: "n2", Multi: false, Kind: "Number"},
&types.ModuleField{Name: "n3", Multi: false, Kind: "Number"},
&types.ModuleField{Name: "d1", Multi: false, Kind: "DateTime"},
&types.ModuleField{Name: "d2", Multi: false, Kind: "DateTime"},
&types.ModuleField{Name: "ref1", Multi: false, Kind: "Record"},
&types.ModuleField{Name: "ref2", Multi: false, Kind: "Record"},
}}
rawValues = types.RecordValueSet{
&types.RecordValue{Name: "s1", Value: "sVal1"},
&types.RecordValue{Name: "m1", Value: "mVal1.0", Place: 0},
&types.RecordValue{Name: "m1", Value: "mVal1.1", Place: 1},
&types.RecordValue{Name: "m1", Value: "mVal1.2", Place: 2},
&types.RecordValue{Name: "m2", Value: "mVal2.0", Place: 0},
&types.RecordValue{Name: "b1", Value: "1", Place: 0},
&types.RecordValue{Name: "n2", Value: "0", Place: 0},
&types.RecordValue{Name: "n3", Value: "2", Place: 0},
&types.RecordValue{Name: "ref2", Value: "", Ref: 2, Place: 0},
&types.RecordValue{Name: "d1", Value: "", Place: 0},
&types.RecordValue{Name: "d2", Value: "1999-09-09 09:09:09.000000009 +0000 UTC", Place: 0},
}
raw = &types.Record{Values: rawValues}
scope, _ = expr.NewVars(map[string]interface{}{
"rec": &ComposeRecord{value: raw},
// nis is only for testing, gval does not recognise nil (or null) as a keyword!
"nil": nil,
// typed value but empty (this happens when you assign next value in expression but do not set a value to it
"initRec": &ComposeRecord{},
// same as &ComposeRecord{value: &types.Record{}},
"validRecZero": &ComposeRecord{value: &types.Record{ID: 0, Values: types.RecordValueSet{}}},
"validRecValidID": &ComposeRecord{value: &types.Record{ID: 42, Values: types.RecordValueSet{}}},
// "record" (not really) set to nil
"fooRec": nil,
// record with id and value set (this was) fixme
"record": &ComposeRecord{value: &types.Record{ID: 99, Values: rawValues}},
})
)
// @todo see note above regarding back-ref to record
raw.SetModule(mod)
t.Run("via typed value", func(t *testing.T) {
tcc := []struct {
expects interface{}
path string
}{
{"sVal1", "rec.values.s1"},
{"mVal1.0", "rec.values.m1.0"},
{"mVal1.1", "rec.values.m1.1"},
{"mVal2.0", "rec.values.m2.0"},
// expecting valid value (false) even when boolean fields are not set
{false, "rec.values.b0"},
{true, "rec.values.b1"},
{uint64(2), "rec.values.ref2"},
{raw, "rec.values"},
}
for _, tc := range tcc {
t.Run(tc.path, func(t *testing.T) {
var (
req = require.New(t)
)
v, err = expr.Select(scope, tc.path)
req.NoError(err)
req.Equal(tc.expects, v.Get())
})
}
})
t.Run("copy record's values via gval selector", func(t *testing.T) {
var (
req = require.New(t)
output *expr.Vars
inputRec = &types.Record{ID: 99, Values: rawValues}
cloneRec = &types.Record{}
)
// IMPORTANT! set module to record; otherwise values will not be copied properly
inputRec.SetModule(mod)
cloneRec.SetModule(mod)
e, err := aTypes.NewExpr(
"recordClone.values",
ComposeRecordValues{}.Type(),
`record.values`)
req.NoError(err)
err = e.SetType(func(s string) (expr.Type, error) {
return ComposeRecordValues{}, nil
})
req.NoError(err)
evaluable, err := expr.NewParser().Parse(e.GetExpr())
req.NoError(err)
e.SetEval(evaluable)
input, _ := expr.NewVars(map[string]expr.TypedValue{
"record": &ComposeRecord{value: inputRec},
"recordClone": &ComposeRecord{value: cloneRec},
})
output, err = (aTypes.ExprSet{e}).Eval(context.Background(), input)
req.NoError(err)
rc, err := output.Select("recordClone")
req.NoError(err)
req.NotNil(rc.Get())
req.Equal(rawValues, rc.Get().(*types.Record).Values)
})
t.Run("via gval selector", func(t *testing.T) {
tcc := []struct {
test interface{}
expr string
}{
{false, `nil`},
{true, `rec`},
{true, `rec.values`},
// not set, so false
{fmt.Errorf("failed to select 'notSetRec' on *expr.Vars: no such key 'notSetRec'"), `notSetRec`},
// set but nil
{false, `fooRec`},
// set, initialized
{true, `initRec`},
// set, initialized, but recordID is empty
{false, `initRec.recordID`},
// set, initialized, but recordID (ID is alias) is empty
{false, `initRec.ID`},
// set, initialized, but values are empty
{false, `initRec.values`},
{true, `validRecZero`},
{false, `validRecZero.recordID`},
{false, `validRecZero.values`},
{true, `validRecValidID`},
{true, `validRecValidID.recordID`},
{false, `validRecValidID.values`},
// but we can not access the values below...
{fmt.Errorf("unknown parameter initRec.values.foo"), `initRec.values.foo`},
// interaction with set values
{true, `rec.values.s1 == "sVal1"`},
{false, `rec.values.s1 == "sVal2"`},
{true, `rec.values.s1`},
{true, `rec.values.s1 != "foo"`},
// interaction with unset (= nil) values
{true, `rec.values.s2 != "foo"`},
{false, `rec.values.s2 == "foo"`},
{true, `!rec.values.s2`},
{false, `rec.values.s2`},
// multival
{true, `rec.values.m1[0] == "mVal1.0"`},
{true, `rec.values.m1[1] == "mVal1.1"`},
{true, `rec.values.m2[0] == "mVal2.0"`},
// booleans
{true, `!rec.values.b0`},
{false, `rec.values.b0`},
{true, `rec.values.b1`},
{false, `!rec.values.b1`},
// numbers
{false, `rec.values.n1`},
{false, `rec.values.n2`},
{true, `rec.values.n3`},
{true, `rec.values.n1 == 0`},
{true, `rec.values.n2 == 0`},
{false, `rec.values.n3 == 0`},
{false, `rec.values.n1 == 2`},
{false, `rec.values.n2 == 2`},
{true, `rec.values.n3 == 2`},
// {true, `rec.values.n1 < 3`}, // invalid op <nil> < 3
{true, `rec.values.n2 < 3`},
{true, `rec.values.n3 < 3`},
// {false, `rec.values.n1 > 1`}, // invalid op <nil> > 3
{false, `rec.values.n2 > 2`},
{false, `rec.values.n3 > 2`},
{true, `rec.values.ref1 != 2`},
{true, `rec.values.ref2 == 2`},
{true, `rec.values.ref2 == "2"`},
{true, `isNil(nil)`},
{true, `isNil(rec.values.d1)`},
{true, `rec.values.n3 ? true : false`},
{false, `rec.values.d1 ? true : false`},
{true, `rec.values.d2 ? true : false`},
{true, `rec.values.d1 ? true : false || true `},
{false, `rec.values.d1 ? true : false && true `},
}
for _, tc := range tcc {
t.Run(tc.expr, func(t *testing.T) {
var (
req = require.New(t)
parser = expr.NewParser()
eval, err = parser.Parse(tc.expr)
)
req.NoError(err)
test, err := eval.Test(context.Background(), scope)
switch tct := tc.test.(type) {
case error:
req.EqualError(err, tct.Error())
case bool:
req.NoError(err)
req.Equal(tc.test, test)
default:
panic("unexpected test case type")
}
})
}
})
}
func TestAssignToComposeRecordValues(t *testing.T) {
t.Run("assign simple", func(t *testing.T) {
var (
req = require.New(t)
target = &types.Record{Values: types.RecordValueSet{}}
)
req.NoError(assignToComposeRecordValues(target, initPather(req, expr.Path("a")), "b"))
req.Len(target.Values, 1)
req.True(target.Values.Has("a", 0))
req.NoError(assignToComposeRecordValues(target, initPather(req, expr.Path("a[1]")), "b"))
req.Len(target.Values, 2)
req.True(target.Values.Has("a", 0))
req.True(target.Values.Has("a", 1))
})
t.Run("assign rvs", func(t *testing.T) {
var (
req = require.New(t)
target = &types.Record{Values: types.RecordValueSet{}}
)
req.NoError(assignToComposeRecordValues(target, nil, types.RecordValueSet{{}}))
req.Len(target.Values, 1)
})
t.Run("assign record", func(t *testing.T) {
var (
req = require.New(t)
target = &types.Record{Values: types.RecordValueSet{}}
)
req.NoError(assignToComposeRecordValues(target, nil, &types.Record{Values: types.RecordValueSet{{}}}))
req.Len(target.Values, 1)
})
t.Run("overwrite rvs", func(t *testing.T) {
var (
req = require.New(t)
target = &types.Record{Values: types.RecordValueSet{{Name: "a"}}}
)
req.NoError(assignToComposeRecordValues(target, nil, types.RecordValueSet{{Name: "b"}}))
req.Len(target.Values, 1)
req.False(target.Values.Has("a", 0))
req.True(target.Values.Has("b", 0))
})
t.Run("assign multiple values", func(t *testing.T) {
var (
req = require.New(t)
target = &types.Record{Values: types.RecordValueSet{}}
)
req.Error(assignToComposeRecordValues(target, initPather(req, expr.Path("a[2]")), expr.Must(expr.NewAny([]interface{}{"1", "2"}))))
req.Len(target.Values, 0)
req.NoError(assignToComposeRecordValues(target, initPather(req, expr.Path("a")), expr.Must(expr.NewAny([]interface{}{"1", "2"}))))
req.Len(target.Values, 2)
req.NoError(assignToComposeRecordValues(target, initPather(req, expr.Path("a")), expr.Must(expr.NewAny([]string{"1", "2"}))))
req.Len(target.Values, 2)
})
t.Run("overwrite multiple values", func(t *testing.T) {
var (
req = require.New(t)
target = &types.Record{Values: types.RecordValueSet{
&types.RecordValue{Name: "mval", Value: "203221389712893", Place: 0},
&types.RecordValue{Name: "mval", Value: "203221389712893", Place: 1},
}}
scope, _ = expr.NewVars(map[string]interface{}{
"rec": expr.Must(NewComposeRecord(target)),
})
ee = aTypes.ExprSet{
&aTypes.Expr{Target: "rec.values.mval", Type: "Array", Expr: "[1,2,3,4]"},
}
)
// convert all types
req.NoError(ee[0].SetType(func(_ string) (expr.Type, error) { return expr.Array{}, nil }))
// init expression handler
evaluable, err := expr.NewParser().Parse(ee[0].GetExpr())
req.NoError(err)
ee[0].SetEval(evaluable)
_, err = ee.Eval(context.Background(), scope)
req.NoError(err)
req.Len(target.Values, 4)
req.Equal(target.Values.Get("mval", 0).Value, "1")
req.Equal(target.Values.Get("mval", 1).Value, "2")
req.Equal(target.Values.Get("mval", 2).Value, "3")
req.Equal(target.Values.Get("mval", 3).Value, "4")
})
}
func TestCastToComposeRecordValues(t *testing.T) {
var (
commonRVS = types.RecordValueSet{
&types.RecordValue{Name: "bools", Value: "1"},
&types.RecordValue{Name: "interfaces", Value: "val"},
&types.RecordValue{Name: "interfaces", Value: "val2", Place: 1},
&types.RecordValue{Name: "string", Value: "val"},
&types.RecordValue{Name: "strings", Value: "val"},
&types.RecordValue{Name: "strings", Value: "val2", Place: 1},
// false booleans will not be set and that is ok!
// &types.RecordValue{Name: "bools", Value: "false", Place: 1}
}
tt = time.Date(1999, 9, 9, 9, 9, 9, 9, time.UTC)
nilSlice []int
nilUntypedMap map[string]interface{}
cases = []struct {
name string
in interface{}
out types.RecordValueSet
err error
}{
{
in: map[string]interface{}{
"string": "val",
"interfaces": []interface{}{"val", "val2"},
"strings": []string{"val", "val2"},
"bools": []bool{true, false},
"nilSlice": nilSlice,
"nilUntypedMap": nilUntypedMap,
},
// warning! false booleans will not be set and that is ok!
out: commonRVS,
},
{
in: map[string][]string{
"strings": {"val", "val2"},
},
out: types.RecordValueSet{
&types.RecordValue{Name: "strings", Value: "val"},
&types.RecordValue{Name: "strings", Value: "val2", Place: 1},
},
},
{
in: map[string]string{
"string": "val",
},
out: types.RecordValueSet{
&types.RecordValue{Name: "string", Value: "val"},
},
},
{
in: &types.RecordValue{Name: "string", Value: "val"},
out: types.RecordValueSet{
&types.RecordValue{Name: "string", Value: "val"},
},
},
{
in: &types.RecordValue{Name: "datetime", Value: tt.String()},
out: types.RecordValueSet{
&types.RecordValue{Name: "datetime", Value: tt.String()},
},
},
{
in: commonRVS,
out: commonRVS,
},
{
in: &types.Record{Values: commonRVS},
out: commonRVS,
},
{
in: 42,
err: fmt.Errorf("unable to cast type int to types.RecordValueSet"),
},
}
)
for _, c := range cases {
if c.name == "" && c.in != nil {
c.name = fmt.Sprintf("%T", c.in)
}
t.Run(c.name, func(t *testing.T) {
var (
req = require.New(t)
)
if c.in == nil {
t.Skip()
}
if out, err := CastToComposeRecordValues(c.in); c.err != nil {
req.EqualError(err, c.err.Error())
} else {
req.EqualValues(c.out, out)
}
})
}
}
func TestIsNil(t *testing.T) {
var (
nilSlice []int
nilUntypedSlice []interface{}
nilPtr *int
nilStringMap map[string]string
nilUntypedMap map[string]interface{}
cases = []struct {
name string
input interface{}
expected bool
}{
{
"nilSlice",
nilSlice,
true,
},
{
"nilUntypedSlice",
nilUntypedSlice,
true,
},
{
"nilPtr",
nilPtr,
true,
},
{
"nilStringMap",
nilStringMap,
true,
},
{
"nilUntypedMap",
nilUntypedMap,
true,
},
}
)
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
var (
req = require.New(t)
)
req.Equal(c.expected, isNil(c.input))
})
}
}
func TestGvalStringFunctionParamsIntCasting(t *testing.T) {
var (
ctx = context.Background()
req = require.New(t)
scope = &expr.Vars{}
)
t.Run("substring", func(t *testing.T) {
eval, err := expr.NewParser().Parse(`substring("foobar", 1, -1)`)
req.NoError(err)
res, err := eval.Eval(ctx, scope)
req.NoError(err)
req.Equal("oobar", res.(string))
})
t.Run("replace", func(t *testing.T) {
eval, err := expr.NewParser().Parse(`replace(" foo foo foo", "foo", "bar", 1)`)
req.NoError(err)
res, err := eval.Eval(ctx, scope)
req.NoError(err)
req.Equal(" bar foo foo", res.(string))
})
t.Run("split", func(t *testing.T) {
eval, err := expr.NewParser().Parse(`split("foo-bar", "-")`)
req.NoError(err)
res, err := eval.Eval(ctx, scope)
req.NoError(err)
req.Equal([]string{"foo", "bar"}, res.([]string))
})
t.Run("has", func(t *testing.T) {
eval, err := expr.NewParser().Parse(`has([1,2], 2)`)
req.NoError(err)
res, err := eval.Eval(ctx, scope)
req.NoError(err)
req.Equal(true, res)
})
t.Run("repeat", func(t *testing.T) {
eval, err := expr.NewParser().Parse(`repeat("! ", 3)`)
req.NoError(err)
res, err := eval.Eval(ctx, scope)
req.NoError(err)
req.Equal("! ! ! ", res.(string))
})
t.Run("shorten", func(t *testing.T) {
eval, err := expr.NewParser().Parse(`shorten("This is a whole sentence", "word", 4)`)
req.NoError(err)
res, err := eval.Eval(ctx, scope)
req.NoError(err)
req.Equal("This is a whole …", res.(string))
})
}
func TestRecordValues_Merge(t *testing.T) {
var (
req = require.New(t)
rv = &ComposeRecordValues{}
foo = &ComposeRecordValues{
value: &types.Record{
Values: []*types.RecordValue{
{
Name: "k1",
Value: "testValue1",
},
{
Name: "k2",
Value: "testValue2",
},
},
},
}
bar = &ComposeRecordValues{
value: &types.Record{
Values: []*types.RecordValue{
{
Name: "k3",
Value: "testValue3",
},
},
},
}
expected = &types.Record{
Values: []*types.RecordValue{
{
Name: "k1",
Value: "testValue1",
},
{
Name: "k2",
Value: "testValue2",
},
{
Name: "k3",
Value: "testValue3",
},
},
}
)
out, err := rv.Merge(foo, bar)
req.NoError(err)
req.Equal(expected, out.Get())
}
func TestRecordValues_Omit(t *testing.T) {
var (
req = require.New(t)
rv = ComposeRecordValues{
value: &types.Record{
Values: []*types.RecordValue{
{
Name: "k1",
Value: "testValue1",
},
{
Name: "k2",
Value: "testValue2",
},
{
Name: "k3",
Value: "testValue3",
},
},
},
}
expected = &types.Record{
Values: []*types.RecordValue{
{
Name: "k2",
Value: "testValue2",
},
},
}
)
out, err := rv.Delete("k1", "k3")
req.NoError(err)
req.Equal(expected, out.Get())
}
// goos: darwin
// goarch: arm64
// pkg: github.com/cortezaproject/corteza/server/compose/automation
// BenchmarkAssignToComposeRecordValues-12 22131764 53.22 ns/op 96 B/op 1 allocs/op
// PASS
func BenchmarkAssignToComposeRecordValues(b *testing.B) {
var (
req = require.New(b)
target = &types.Record{Values: types.RecordValueSet{}}
p = initPather(req, expr.Path("a"))
)
b.ResetTimer()
for i := 0; i < b.N; i++ {
assignToComposeRecordValues(target, p, "b")
}
}
// goos: darwin
// goarch: arm64
// pkg: github.com/cortezaproject/corteza/server/compose/automation
// BenchmarkRecordFieldValuesAccess-12 1947148 593.6 ns/op 544 B/op 19 allocs/op
// PASS
func BenchmarkRecordFieldValuesAccess(b *testing.B) {
var (
mod = &types.Module{Fields: types.ModuleFieldSet{
&types.ModuleField{Name: "s1", Multi: true, Kind: "String"},
}}
rawValues = types.RecordValueSet{
&types.RecordValue{Name: "s1", Value: "sVal1.0", Place: 0},
&types.RecordValue{Name: "s1", Value: "sVal1.1", Place: 1},
&types.RecordValue{Name: "s1", Value: "sVal1.2", Place: 2},
}
raw = &types.Record{Values: rawValues}
scope, _ = expr.NewVars(map[string]interface{}{
"rec": &ComposeRecord{value: raw},
})
)
// @todo see note above regarding back-ref to record
raw.SetModule(mod)
b.ResetTimer()
for n := 0; n < b.N; n++ {
expr.Select(scope, "rec.values.s1[1]")
}
}
func initPather(req *require.Assertions, p expr.Pather) (out expr.Pather) {
err := p.Next()
req.NoError(err)
return p
}

View File

@@ -0,0 +1,165 @@
package automation
// 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:
// compose/automation/modules_handler.yaml
import (
"context"
atypes "github.com/cortezaproject/corteza/server/automation/types"
"github.com/cortezaproject/corteza/server/compose/types"
"github.com/cortezaproject/corteza/server/pkg/expr"
"github.com/cortezaproject/corteza/server/pkg/wfexec"
)
var _ wfexec.ExecResponse
type (
modulesHandlerRegistry interface {
AddFunctions(ff ...*atypes.Function)
Type(ref string) expr.Type
}
)
func (h modulesHandler) register() {
h.reg.AddFunctions(
h.Lookup(),
)
}
type (
modulesLookupArgs struct {
hasModule bool
Module interface{}
moduleID uint64
moduleHandle string
moduleRes *types.Module
hasNamespace bool
Namespace interface{}
namespaceID uint64
namespaceHandle string
namespaceRes *types.Namespace
}
modulesLookupResults struct {
Module *types.Module
}
)
func (a modulesLookupArgs) GetModule() (bool, uint64, string, *types.Module) {
return a.hasModule, a.moduleID, a.moduleHandle, a.moduleRes
}
func (a modulesLookupArgs) GetNamespace() (bool, uint64, string, *types.Namespace) {
return a.hasNamespace, a.namespaceID, a.namespaceHandle, a.namespaceRes
}
// Lookup function Compose module lookup
//
// expects implementation of lookup function:
//
// func (h modulesHandler) lookup(ctx context.Context, args *modulesLookupArgs) (results *modulesLookupResults, err error) {
// return
// }
func (h modulesHandler) Lookup() *atypes.Function {
return &atypes.Function{
Ref: "composeModulesLookup",
Kind: "function",
Labels: map[string]string{"compose": "step,workflow", "lookup": "step", "module": "step,workflow"},
Meta: &atypes.FunctionMeta{
Short: "Compose module lookup",
Description: "Find specific module by ID or handle",
},
Parameters: []*atypes.Param{
{
Name: "module",
Types: []string{"ID", "Handle", "ComposeModule"}, Required: true,
Meta: &atypes.ParamMeta{
Label: "Module",
},
},
{
Name: "namespace",
Types: []string{"ID", "Handle", "ComposeNamespace"}, Required: true,
Meta: &atypes.ParamMeta{
Label: "Namespace",
},
},
},
Results: []*atypes.Param{
{
Name: "module",
Types: []string{"ComposeModule"},
},
},
Handler: func(ctx context.Context, in *expr.Vars) (out *expr.Vars, err error) {
var (
args = &modulesLookupArgs{
hasModule: in.Has("module"),
hasNamespace: in.Has("namespace"),
}
)
if err = in.Decode(args); err != nil {
return
}
// Converting Module argument
if args.hasModule {
aux := expr.Must(expr.Select(in, "module"))
switch aux.Type() {
case h.reg.Type("ID").Type():
args.moduleID = aux.Get().(uint64)
case h.reg.Type("Handle").Type():
args.moduleHandle = aux.Get().(string)
case h.reg.Type("ComposeModule").Type():
args.moduleRes = aux.Get().(*types.Module)
}
}
// Converting Namespace argument
if args.hasNamespace {
aux := expr.Must(expr.Select(in, "namespace"))
switch aux.Type() {
case h.reg.Type("ID").Type():
args.namespaceID = aux.Get().(uint64)
case h.reg.Type("Handle").Type():
args.namespaceHandle = aux.Get().(string)
case h.reg.Type("ComposeNamespace").Type():
args.namespaceRes = aux.Get().(*types.Namespace)
}
}
var results *modulesLookupResults
if results, err = h.lookup(ctx, args); err != nil {
return
}
out = &expr.Vars{}
{
// converting results.Module (*types.Module) to ComposeModule
var (
tval expr.TypedValue
)
if tval, err = h.reg.Type("ComposeModule").Cast(results.Module); err != nil {
return
} else if err = expr.Assign(out, "module", tval); err != nil {
return
}
}
return
},
}
}

View File

@@ -0,0 +1,83 @@
package automation
import (
"context"
"fmt"
"github.com/cortezaproject/corteza/server/compose/types"
)
type (
moduleService interface {
FindByID(ctx context.Context, namespaceID, moduleID uint64) (*types.Module, error)
FindByHandle(ctx context.Context, namespaceID uint64, handle string) (*types.Module, error)
Find(ctx context.Context, filter types.ModuleFilter) (set types.ModuleSet, f types.ModuleFilter, err error)
Create(ctx context.Context, module *types.Module) (*types.Module, error)
Update(ctx context.Context, module *types.Module) (*types.Module, error)
DeleteByID(ctx context.Context, namespaceID uint64, moduleID uint64) error
}
modulesHandler struct {
reg modulesHandlerRegistry
ns namespaceService
mod moduleService
}
moduleLookup interface {
GetModule() (bool, uint64, string, *types.Module)
}
)
func ModulesHandler(reg modulesHandlerRegistry, ns namespaceService, mod moduleService) *modulesHandler {
h := &modulesHandler{
reg: reg,
ns: ns,
mod: mod,
}
h.register()
return h
}
func (h modulesHandler) lookup(ctx context.Context, args *modulesLookupArgs) (results *modulesLookupResults, err error) {
results = &modulesLookupResults{}
results.Module, err = lookupModule(ctx, h.ns, h.mod, args)
return
}
func getModuleID(ctx context.Context, nsSvc namespaceService, modSvc moduleService, args moduleLookup) (namespaceID uint64, moduleID uint64, err error) {
namespaceID, err = getNamespaceID(ctx, nsSvc, args.(namespaceLookup))
if err != nil {
return
}
var mod *types.Module
if _, moduleID, _, _ = args.GetModule(); moduleID > 0 {
return
} else if mod, err = lookupModule(ctx, nsSvc, modSvc, args); err != nil {
return
} else {
return namespaceID, mod.ID, nil
}
}
func lookupModule(ctx context.Context, nsSvc namespaceService, modSvc moduleService, args moduleLookup) (*types.Module, error) {
namespaceID, err := getNamespaceID(ctx, nsSvc, args.(namespaceLookup))
if err != nil {
return nil, fmt.Errorf("could not load namespace: %w", err)
}
_, ID, handle, module := args.GetModule()
switch {
case module != nil:
return module, nil
case ID > 0:
return modSvc.FindByID(ctx, namespaceID, ID)
case len(handle) > 0:
return modSvc.FindByHandle(ctx, namespaceID, handle)
}
return nil, fmt.Errorf("empty module lookup params")
}

View File

@@ -0,0 +1,39 @@
prefix: compose
imports:
- github.com/cortezaproject/corteza/server/compose/types
params:
moduleLookup: &moduleLookup
meta: { label: Module }
required: true
types:
- { wf: ID, }
- { wf: Handle, }
- { wf: ComposeModule, suffix: res }
namespaceLookup: &namespaceLookup
meta: { label: Namespace }
required: true
types:
- { wf: ID, }
- { wf: Handle, }
- { wf: ComposeNamespace, suffix: res }
rvModule: &rvModule
wf: ComposeModule
functions:
lookup:
meta:
short: Compose module lookup
description: Find specific module by ID or handle
params:
module: *moduleLookup
namespace: *namespaceLookup
labels:
lookup: "step"
module: "step,workflow"
compose: "step,workflow"
results:
module: *rvModule

View File

@@ -0,0 +1,134 @@
package automation
// 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:
// compose/automation/namespaces_handler.yaml
import (
"context"
atypes "github.com/cortezaproject/corteza/server/automation/types"
"github.com/cortezaproject/corteza/server/compose/types"
"github.com/cortezaproject/corteza/server/pkg/expr"
"github.com/cortezaproject/corteza/server/pkg/wfexec"
)
var _ wfexec.ExecResponse
type (
namespacesHandlerRegistry interface {
AddFunctions(ff ...*atypes.Function)
Type(ref string) expr.Type
}
)
func (h namespacesHandler) register() {
h.reg.AddFunctions(
h.Lookup(),
)
}
type (
namespacesLookupArgs struct {
hasNamespace bool
Namespace interface{}
namespaceID uint64
namespaceHandle string
namespaceRes *types.Namespace
}
namespacesLookupResults struct {
Namespace *types.Namespace
}
)
func (a namespacesLookupArgs) GetNamespace() (bool, uint64, string, *types.Namespace) {
return a.hasNamespace, a.namespaceID, a.namespaceHandle, a.namespaceRes
}
// Lookup function Compose namespace lookup
//
// expects implementation of lookup function:
//
// func (h namespacesHandler) lookup(ctx context.Context, args *namespacesLookupArgs) (results *namespacesLookupResults, err error) {
// return
// }
func (h namespacesHandler) Lookup() *atypes.Function {
return &atypes.Function{
Ref: "composeNamespacesLookup",
Kind: "function",
Labels: map[string]string{"compose": "step,workflow", "lookup": "step", "namespace": "step,workflow"},
Meta: &atypes.FunctionMeta{
Short: "Compose namespace lookup",
Description: "Find specific namespace by ID or handle",
},
Parameters: []*atypes.Param{
{
Name: "namespace",
Types: []string{"ID", "Handle", "ComposeNamespace"}, Required: true,
Meta: &atypes.ParamMeta{
Label: "Namespace",
},
},
},
Results: []*atypes.Param{
{
Name: "namespace",
Types: []string{"ComposeNamespace"},
},
},
Handler: func(ctx context.Context, in *expr.Vars) (out *expr.Vars, err error) {
var (
args = &namespacesLookupArgs{
hasNamespace: in.Has("namespace"),
}
)
if err = in.Decode(args); err != nil {
return
}
// Converting Namespace argument
if args.hasNamespace {
aux := expr.Must(expr.Select(in, "namespace"))
switch aux.Type() {
case h.reg.Type("ID").Type():
args.namespaceID = aux.Get().(uint64)
case h.reg.Type("Handle").Type():
args.namespaceHandle = aux.Get().(string)
case h.reg.Type("ComposeNamespace").Type():
args.namespaceRes = aux.Get().(*types.Namespace)
}
}
var results *namespacesLookupResults
if results, err = h.lookup(ctx, args); err != nil {
return
}
out = &expr.Vars{}
{
// converting results.Namespace (*types.Namespace) to ComposeNamespace
var (
tval expr.TypedValue
)
if tval, err = h.reg.Type("ComposeNamespace").Cast(results.Namespace); err != nil {
return
} else if err = expr.Assign(out, "namespace", tval); err != nil {
return
}
}
return
},
}
}

View File

@@ -0,0 +1,72 @@
package automation
import (
"context"
"fmt"
"github.com/cortezaproject/corteza/server/compose/types"
)
type (
namespaceService interface {
FindByID(ctx context.Context, namespaceID uint64) (*types.Namespace, error)
FindByHandle(ctx context.Context, handle string) (*types.Namespace, error)
Find(ctx context.Context, filter types.NamespaceFilter) (set types.NamespaceSet, f types.NamespaceFilter, err error)
Create(ctx context.Context, namespace *types.Namespace) (*types.Namespace, error)
Update(ctx context.Context, namespace *types.Namespace) (*types.Namespace, error)
DeleteByID(ctx context.Context, namespaceID uint64) error
}
namespacesHandler struct {
reg namespacesHandlerRegistry
ns namespaceService
}
namespaceLookup interface {
GetNamespace() (bool, uint64, string, *types.Namespace)
}
)
func NamespacesHandler(reg namespacesHandlerRegistry, ns namespaceService) *namespacesHandler {
h := &namespacesHandler{
reg: reg,
ns: ns,
}
h.register()
return h
}
func (h namespacesHandler) lookup(ctx context.Context, args *namespacesLookupArgs) (results *namespacesLookupResults, err error) {
results = &namespacesLookupResults{}
results.Namespace, err = lookupNamespace(ctx, h.ns, args)
return
}
func getNamespaceID(ctx context.Context, svc namespaceService, args namespaceLookup) (uint64, error) {
_, ID, _, _ := args.GetNamespace()
if ID > 0 {
return ID, nil
} else if ns, err := lookupNamespace(ctx, svc, args); err != nil {
return 0, err
} else {
return ns.ID, nil
}
}
func lookupNamespace(ctx context.Context, svc namespaceService, args namespaceLookup) (*types.Namespace, error) {
_, ID, handle, namespace := args.GetNamespace()
switch {
case namespace != nil:
return namespace, nil
case ID > 0:
return svc.FindByID(ctx, ID)
case len(handle) > 0:
return svc.FindByHandle(ctx, handle)
}
return nil, fmt.Errorf("empty namespace lookup params")
}

View File

@@ -0,0 +1,30 @@
prefix: compose
imports:
- github.com/cortezaproject/corteza/server/compose/types
params:
namespaceLookup: &namespaceLookup
meta: { label: Namespace }
required: true
types:
- { wf: ID, }
- { wf: Handle, }
- { wf: ComposeNamespace, suffix: res }
rvNamespace: &rvNamespace
wf: ComposeNamespace
functions:
lookup:
meta:
short: Compose namespace lookup
description: Find specific namespace by ID or handle
labels:
lookup: "step"
namespace: "step,workflow"
compose: "step,workflow"
params:
namespace: *namespaceLookup
results:
namespace: *rvNamespace

View File

@@ -0,0 +1,214 @@
package automation
// 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:
// compose/automation/notification_handler.yaml
import (
"context"
atypes "github.com/cortezaproject/corteza/server/automation/types"
"github.com/cortezaproject/corteza/server/pkg/expr"
"github.com/cortezaproject/corteza/server/pkg/wfexec"
)
var _ wfexec.ExecResponse
type (
notificationHandlerRegistry interface {
AddFunctions(ff ...*atypes.Function)
Type(ref string) expr.Type
}
)
func (h notificationHandler) register() {
h.reg.AddFunctions(
h.SendRecord(),
)
}
type (
notificationSendRecordArgs struct {
hasRecipient bool
Recipient interface{}
recipientID uint64
recipientHandle string
recipientEmail string
hasTitle bool
Title string
hasDescription bool
Description string
hasModule bool
Module interface{}
moduleID uint64
moduleHandle string
hasNamespace bool
Namespace interface{}
namespaceID uint64
namespaceHandle string
hasRecord bool
Record uint64
hasOpenMode bool
OpenMode string
hasEdit bool
Edit bool
}
)
func (a notificationSendRecordArgs) GetRecipient() (bool, uint64, string, string) {
return a.hasRecipient, a.recipientID, a.recipientHandle, a.recipientEmail
}
func (a notificationSendRecordArgs) GetModule() (bool, uint64, string) {
return a.hasModule, a.moduleID, a.moduleHandle
}
func (a notificationSendRecordArgs) GetNamespace() (bool, uint64, string) {
return a.hasNamespace, a.namespaceID, a.namespaceHandle
}
// SendRecord function Send record notification
//
// expects implementation of sendRecord function:
//
// func (h notificationHandler) sendRecord(ctx context.Context, args *notificationSendRecordArgs) (err error) {
// return
// }
func (h notificationHandler) SendRecord() *atypes.Function {
return &atypes.Function{
Ref: "notificationSendRecord",
Kind: "function",
Labels: map[string]string(nil),
Meta: &atypes.FunctionMeta{
Short: "Send record notification",
Description: "Sends a notification that links to a specific record",
},
Parameters: []*atypes.Param{
{
Name: "recipient",
Types: []string{"ID", "Handle", "String"}, Required: true,
Meta: &atypes.ParamMeta{
Label: "Recipient",
},
},
{
Name: "title",
Types: []string{"String"}, Required: true,
Meta: &atypes.ParamMeta{
Label: "Title",
},
},
{
Name: "description",
Types: []string{"String"},
Meta: &atypes.ParamMeta{
Label: "Description",
Visual: map[string]interface{}{"input": map[string]interface{}{"type": "richtext"}},
},
},
{
Name: "module",
Types: []string{"ID", "Handle"}, Required: true,
Meta: &atypes.ParamMeta{
Label: "Module",
},
},
{
Name: "namespace",
Types: []string{"ID", "Handle"}, Required: true,
Meta: &atypes.ParamMeta{
Label: "Namespace",
},
},
{
Name: "record",
Types: []string{"ID"},
Meta: &atypes.ParamMeta{
Label: "Record",
},
},
{
Name: "openMode",
Types: []string{"String"},
Meta: &atypes.ParamMeta{
Label: "Open mode",
Visual: map[string]interface{}{"input": map[string]interface{}{"default": "modal", "properties": map[string]interface{}{"options": []interface{}{map[string]interface{}{"text": "Open link in the same tab", "value": "sameTab"}, map[string]interface{}{"text": "Open link in a new tab", "value": "newTab"}, map[string]interface{}{"text": "Open in a modal", "value": "modal"}}}, "type": "select"}},
},
},
{
Name: "edit",
Types: []string{"Boolean"},
Meta: &atypes.ParamMeta{
Label: "Edit",
},
},
},
Handler: func(ctx context.Context, in *expr.Vars) (out *expr.Vars, err error) {
var (
args = &notificationSendRecordArgs{
hasRecipient: in.Has("recipient"),
hasTitle: in.Has("title"),
hasDescription: in.Has("description"),
hasModule: in.Has("module"),
hasNamespace: in.Has("namespace"),
hasRecord: in.Has("record"),
hasOpenMode: in.Has("openMode"),
hasEdit: in.Has("edit"),
}
)
if err = in.Decode(args); err != nil {
return
}
// Converting Recipient argument
if args.hasRecipient {
aux := expr.Must(expr.Select(in, "recipient"))
switch aux.Type() {
case h.reg.Type("ID").Type():
args.recipientID = aux.Get().(uint64)
case h.reg.Type("Handle").Type():
args.recipientHandle = aux.Get().(string)
case h.reg.Type("String").Type():
args.recipientEmail = aux.Get().(string)
}
}
// Converting Module argument
if args.hasModule {
aux := expr.Must(expr.Select(in, "module"))
switch aux.Type() {
case h.reg.Type("ID").Type():
args.moduleID = aux.Get().(uint64)
case h.reg.Type("Handle").Type():
args.moduleHandle = aux.Get().(string)
}
}
// Converting Namespace argument
if args.hasNamespace {
aux := expr.Must(expr.Select(in, "namespace"))
switch aux.Type() {
case h.reg.Type("ID").Type():
args.namespaceID = aux.Get().(uint64)
case h.reg.Type("Handle").Type():
args.namespaceHandle = aux.Get().(string)
}
}
return out, h.sendRecord(ctx, args)
},
}
}

View File

@@ -0,0 +1,131 @@
package automation
import (
"context"
"fmt"
composeTypes "github.com/cortezaproject/corteza/server/compose/types"
systemTypes "github.com/cortezaproject/corteza/server/system/types"
"go.uber.org/zap"
)
type (
notificationHandler struct {
reg notificationHandlerRegistry
svc notificationService
uSvc notificationUserService
nSvc notificationNamespaceService
mSvc notificationModuleService
logger *zap.Logger
}
notificationService interface {
Create(context.Context, *systemTypes.Notification) (*systemTypes.Notification, error)
}
// Interface for user service that we need for recipient lookups
notificationUserService interface {
FindByID(ctx context.Context, userID uint64) (*systemTypes.User, error)
FindByHandle(ctx context.Context, handle string) (*systemTypes.User, error)
FindByEmail(ctx context.Context, email string) (*systemTypes.User, error)
}
notificationNamespaceService interface {
FindByHandle(ctx context.Context, handle string) (*composeTypes.Namespace, error)
}
notificationModuleService interface {
FindByHandle(ctx context.Context, namespaceID uint64, handle string) (*composeTypes.Module, error)
}
)
// NotificationHandler initializes the notification handler
func NotificationHandler(reg notificationHandlerRegistry, ntfSvc notificationService, uSvc notificationUserService, nSvc notificationNamespaceService, mSvc notificationModuleService, log *zap.Logger) *notificationHandler {
h := &notificationHandler{
reg: reg,
svc: ntfSvc,
uSvc: uSvc,
nSvc: nSvc,
mSvc: mSvc,
logger: log.Named("notification"),
}
h.register()
return h
}
// sendRecord creates and sends a record notification
func (h notificationHandler) sendRecord(ctx context.Context, args *notificationSendRecordArgs) error {
// Get the recipient user ID from the input parameter
var recipientID uint64
// Check if we have a direct user object
if args.recipientID > 0 {
// Direct ID passed
recipientID = args.recipientID
} else {
// Need to look up the user
var user *systemTypes.User
var err error
if args.recipientHandle != "" {
user, err = h.uSvc.FindByHandle(ctx, args.recipientHandle)
} else if args.recipientEmail != "" {
user, err = h.uSvc.FindByEmail(ctx, args.recipientEmail)
} else {
return fmt.Errorf("invalid recipient: unable to determine user ID")
}
if err != nil {
return err
}
if user == nil {
return fmt.Errorf("recipient not found")
}
recipientID = user.ID
}
if args.namespaceHandle != "" {
namespace, err := h.nSvc.FindByHandle(ctx, args.namespaceHandle)
if err != nil {
return err
}
args.namespaceID = namespace.ID
}
if args.moduleHandle != "" {
module, err := h.mSvc.FindByHandle(ctx, args.namespaceID, args.moduleHandle)
if err != nil {
return err
}
args.moduleID = module.ID
}
// Create notification config for a record notification
config := systemTypes.NotificationConfig{
Record: systemTypes.RecordNotificationConfig{
Title: args.Title,
Description: args.Description,
ModuleID: args.moduleID,
NamespaceID: args.namespaceID,
RecordID: args.Record,
OpenMode: systemTypes.OpenModeType(args.OpenMode),
Edit: args.Edit,
},
}
// Create a record notification
ntf := &systemTypes.Notification{
Kind: systemTypes.NotificationKindRecord,
Config: config,
Recipient: recipientID,
}
// Create the notification
_, err := h.svc.Create(ctx, ntf)
return err
}

View File

@@ -0,0 +1,72 @@
name: notification
snippets:
lookupUser: &lookupUser
meta: { label: Recipient }
required: true
types:
- { wf: ID }
- { wf: Handle }
- { wf: String, suffix: email }
functions:
sendRecord:
meta:
short: Send record notification
description: Sends a notification that links to a specific record
params:
recipient: *lookupUser
title:
meta: { label: Title }
types:
- { wf: String }
required: true
description: The title of the record notification
description:
types:
- { wf: String }
required: false
description: Optional description for the record notification
meta: { label: Description, visual: { input: { type: richtext } } }
module:
meta: { label: Module }
required: true
types:
- { wf: ID }
- { wf: Handle }
description: The module ID of the record
namespace:
meta: { label: Namespace }
required: true
types:
- { wf: ID }
- { wf: Handle }
description: The namespace ID of the record
record:
meta: { label: Record }
required: false
types:
- { wf: ID }
description: The record ID to link to
openMode:
types:
- { wf: String }
required: false
description: How to open the record - "modal", "newTab", or "sameTab" (default)
meta:
label: Open mode
visual:
input:
type: select
properties:
options:
- { value: 'sameTab', text: 'Open link in the same tab' }
- { value: 'newTab', text: 'Open link in a new tab' }
- { value: 'modal', text: 'Open in a modal' }
default: 'modal'
edit:
meta: { label: Edit }
types:
- { wf: Boolean }
required: false
description: Whether to open the record in edit mode (default is false)

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,460 @@
package automation
import (
"context"
"encoding/json"
"errors"
"fmt"
"regexp"
"strings"
"time"
"github.com/cortezaproject/corteza/server/compose/types"
. "github.com/cortezaproject/corteza/server/pkg/expr"
"github.com/cortezaproject/corteza/server/pkg/filter"
"github.com/cortezaproject/corteza/server/pkg/wfexec"
)
type (
recordService interface {
FindByID(ctx context.Context, namespaceID, moduleID, recordID uint64) (*types.Record, *types.RecordValueErrorSet, error)
Find(ctx context.Context, filter types.RecordFilter) (set types.RecordSet, f types.RecordFilter, err error)
Create(ctx context.Context, record *types.Record) (*types.Record, *types.RecordValueErrorSet, error)
Update(ctx context.Context, record *types.Record) (*types.Record, *types.RecordValueErrorSet, error)
Bulk(ctx context.Context, skipFailed bool, oo ...*types.RecordBulkOperation) ([]types.RecordBulkOperationResult, error)
Report(ctx context.Context, namespaceID, moduleID uint64, metrics, dimensions, filter string) (out any, err error)
Validate(ctx context.Context, rec *types.Record) error
DeleteByID(ctx context.Context, namespaceID, moduleID uint64, recordID ...uint64) error
}
recordsHandler struct {
reg recordsHandlerRegistry
ns namespaceService
mod moduleService
rec recordService
}
recordSetIterator struct {
// Item buffer, current item pointer, and total items traversed
ptr uint
buffer types.RecordSet
total uint
// When filter limit is set, this constraints it
iterLimit uint
useIterLimit bool
// Item loader for additional chunks
filter types.RecordFilter
loader func() error
}
recordLookup interface {
GetRecord() (bool, uint64, *types.Record)
}
)
func RecordsHandler(reg recordsHandlerRegistry, ns namespaceService, mod moduleService, rec recordService) *recordsHandler {
h := &recordsHandler{
reg: reg,
ns: ns,
mod: mod,
rec: rec,
}
h.register()
return h
}
func (h recordsHandler) lookup(ctx context.Context, args *recordsLookupArgs) (results *recordsLookupResults, err error) {
results = &recordsLookupResults{}
results.Record, err = h.lookupRecord(ctx, args)
return
}
func (h recordsHandler) search(ctx context.Context, args *recordsSearchArgs) (results *recordsSearchResults, err error) {
results = &recordsSearchResults{}
var (
f = types.RecordFilter{
Query: args.Query,
Meta: args.Meta,
Deleted: filter.State(args.Deleted),
}
)
if ns, mod, err := h.loadCombo(ctx, args); err != nil {
return nil, err
} else {
f.ModuleID = mod.ID
f.NamespaceID = ns.ID
}
if args.hasSort {
if err = f.Sort.Set(args.Sort); err != nil {
return
}
}
if args.hasPageCursor {
if args.PageCursor != "" {
f.PageCursor = &filter.PagingCursor{}
if err = f.PageCursor.UnmarshalJSON([]byte(args.PageCursor)); err != nil {
return
}
}
}
f.IncTotal = args.IncTotal
f.IncPageNavigation = args.IncPageNavigation
if args.hasMeta {
f.Meta = args.Meta
}
if args.hasLimit {
f.Limit = uint(args.Limit)
}
var auxf types.RecordFilter
results.Records, auxf, err = h.rec.Find(ctx, f)
results.Total = uint64(auxf.Total)
if auxf.NextPage != nil {
results.NextPage = auxf.NextPage.Encode()
}
if auxf.PrevPage != nil {
results.PrevPage = auxf.PrevPage.Encode()
}
// Always assure at least empty cursor when requesting page nav.
if auxf.PageNavigation == nil && auxf.IncPageNavigation {
results.PageNavigation = append(results.PageNavigation, nil)
}
for _, pn := range auxf.PageNavigation {
if pn == nil {
continue
}
// first page is null -- no cursor
if pn.Cursor == nil {
results.PageNavigation = append(results.PageNavigation, nil)
} else {
results.PageNavigation = append(results.PageNavigation, Must(NewString(pn.Cursor.Encode())))
}
}
return
}
func (h recordsHandler) first(ctx context.Context, args *recordsFirstArgs) (results *recordsFirstResults, err error) {
r, err := h.fetchEdge(ctx, args, true)
if err != nil {
return nil, err
}
return &recordsFirstResults{
Record: r,
}, nil
}
func (h recordsHandler) last(ctx context.Context, args *recordsLastArgs) (results *recordsLastResults, err error) {
r, err := h.fetchEdge(ctx, args, false)
if err != nil {
return nil, err
}
return &recordsLastResults{
Record: r,
}, nil
}
func (h recordsHandler) each(ctx context.Context, args *recordsEachArgs) (out wfexec.IteratorHandler, err error) {
var (
i = &recordSetIterator{}
f = types.RecordFilter{
Query: args.Query,
Meta: args.Meta,
Deleted: filter.State(args.Deleted),
}
)
if ns, mod, err := h.loadCombo(ctx, args); err != nil {
return nil, err
} else {
f.ModuleID = mod.ID
f.NamespaceID = ns.ID
}
if args.hasSort {
if err = f.Sort.Set(args.Sort); err != nil {
return nil, err
}
}
if args.hasPageCursor {
if args.PageCursor != "" {
f.NextPage = &filter.PagingCursor{}
if err = f.NextPage.UnmarshalJSON([]byte(args.PageCursor)); err != nil {
return
}
}
}
f.IncTotal = args.IncTotal
f.IncPageNavigation = args.IncPageNavigation
if args.hasMeta {
f.Meta = args.Meta
}
if args.hasLimit {
i.useIterLimit = true
i.iterLimit = uint(args.Limit)
f.Limit = uint(args.Limit)
if args.Limit > uint64(wfexec.MaxIteratorBufferSize) {
f.Limit = wfexec.MaxIteratorBufferSize
}
i.iterLimit = uint(args.Limit)
} else {
f.Limit = wfexec.MaxIteratorBufferSize
}
i.filter = f
i.loader = func() (err error) {
// Edgecase
if i.filter.PageCursor != nil && i.filter.NextPage == nil {
return
}
i.total += i.ptr
i.ptr = 0
i.filter.PageCursor = i.filter.NextPage
i.filter.NextPage = nil
i.buffer, i.filter, err = h.rec.Find(ctx, i.filter)
return
}
// Initial load
return i, i.loader()
}
func (h recordsHandler) validate(ctx context.Context, args *recordsValidateArgs) (*recordsValidateResults, error) {
results := &recordsValidateResults{Valid: true}
if err := h.rec.Validate(ctx, args.Record); err != nil {
results.Valid = false
//if rves, is := err.(*types.RecordValueErrorSet); is {
// results.Errors = rves
//} else {
// return nil, err
//}
}
return results, nil
}
func (h recordsHandler) new(ctx context.Context, args *recordsNewArgs) (*recordsNewResults, error) {
results := &recordsNewResults{}
namespace, module, err := h.loadCombo(ctx, args)
if err != nil {
return nil, err
}
results.Record = &types.Record{
ModuleID: module.ID,
NamespaceID: namespace.ID,
}
results.Record.SetModule(module)
return results, nil
}
func (h recordsHandler) create(ctx context.Context, args *recordsCreateArgs) (results *recordsCreateResults, err error) {
results = &recordsCreateResults{}
results.Record, err = wrapRecordValueErrorSet(h.rec.Create(ctx, args.Record))
return
}
func (h recordsHandler) update(ctx context.Context, args *recordsUpdateArgs) (results *recordsUpdateResults, err error) {
results = &recordsUpdateResults{}
results.Record, err = wrapRecordValueErrorSet(h.rec.Update(ctx, args.Record))
return
}
func (h recordsHandler) delete(ctx context.Context, args *recordsDeleteArgs) error {
if rec, err := h.lookupRecord(ctx, args); err != nil {
return err
} else {
return h.rec.DeleteByID(ctx, rec.NamespaceID, rec.ModuleID, rec.ID)
}
}
func (h recordsHandler) report(ctx context.Context, args *recordsReportArgs) (*recordsReportResults, error) {
r := &recordsReportResults{}
ns, mod, err := h.loadCombo(ctx, args)
if err != nil {
return nil, err
}
r.Report, err = h.rec.Report(ctx, ns.ID, mod.ID, args.Metrics, args.Dimensons, args.Filter)
if err != nil {
return nil, err
}
return r, nil
}
func (h recordsHandler) lookupRecord(ctx context.Context, args recordLookup) (record *types.Record, err error) {
var (
namespace *types.Namespace
module *types.Module
recordID uint64
)
if _, recordID, record = args.GetRecord(); record != nil {
return
}
if namespace, module, err = h.loadCombo(ctx, args); err != nil {
return
}
record, _, err = h.rec.FindByID(ctx, namespace.ID, module.ID, recordID)
return
}
func (h recordsHandler) loadCombo(ctx context.Context, args interface{}) (namespace *types.Namespace, module *types.Module, err error) {
if lkp, is := args.(namespaceLookup); is {
if namespace, err = lookupNamespace(ctx, h.ns, lkp); err != nil {
err = fmt.Errorf("could not load namespace: %w", err)
return
}
} else {
err = fmt.Errorf("could not extract namespace lookup arguments")
return
}
if lkp, is := args.(moduleLookup); is {
if module, err = lookupModule(ctx, h.ns, h.mod, lkp); err != nil {
err = fmt.Errorf("could not load module: %w", err)
return
}
} else {
err = fmt.Errorf("could not extract module lookup arguments")
return
}
return
}
func (h recordsHandler) fetchEdge(ctx context.Context, args interface{}, first bool) (*types.Record, error) {
f := types.RecordFilter{}
if first {
f.Sort.Set("createdAt ASC")
} else {
f.Sort.Set("createdAt DESC")
}
f.Limit = 1
if ns, mod, err := h.loadCombo(ctx, args); err != nil {
return nil, err
} else {
f.ModuleID = mod.ID
f.NamespaceID = ns.ID
}
rr, _, err := h.rec.Find(ctx, f)
if err != nil {
return nil, err
}
if len(rr) == 0 {
return nil, fmt.Errorf("could not fetch records: no records found")
}
return rr[0], nil
}
func (i *recordSetIterator) More(context.Context, *Vars) (bool, error) {
return wfexec.GenericResourceNextCheck(i.useIterLimit, i.ptr, uint(len(i.buffer)), i.total, i.iterLimit, i.filter.NextPage != nil), nil
}
func (i *recordSetIterator) Start(context.Context, *Vars) error { i.ptr = 0; return nil }
func (i *recordSetIterator) Next(context.Context, *Vars) (out *Vars, err error) {
if len(i.buffer)-int(i.ptr) <= 0 {
if err = i.loader(); err != nil {
panic(err)
}
}
out = &Vars{}
out.Set("record", Must(NewComposeRecord(i.buffer[i.ptr])))
out.Set("index", Must(NewInteger(i.total+i.ptr)))
out.Set("total", Must(NewInteger(i.filter.Total)))
i.ptr++
return out, nil
}
func wrapRecordValueErrorSet(rr *types.Record, res *types.RecordValueErrorSet, ee error) (r *types.Record, err error) {
var (
ss string
j []byte
e error
)
r = rr
err = ee
if res = types.IsRecordValueErrorSet(err); res == nil {
return
}
if !res.Safe() {
return rr, err
}
for _, vv := range res.Set {
if j, e = json.Marshal(vv.Meta); e != nil {
continue
}
ss = fmt.Sprintf("%s, %s (%s)", ss, strings.ToLower(vv.Message), j)
}
re := regexp.MustCompile("^,\\s")
ss = re.ReplaceAllString(ss, "")
return rr, errors.New(ss)
}
func (h recordsHandler) clone(ctx context.Context, args *recordsCloneArgs) (*recordsCloneResults, error) {
results := &recordsCloneResults{}
rec, err := h.lookupRecord(ctx, args)
if err != nil {
return nil, err
}
results.Record = rec.Clone()
results.Record.ID = 0
results.Record.CreatedAt = time.Now()
results.Record.UpdatedAt = nil
results.Record.DeletedAt = nil
results.Record.CreatedBy = 0
results.Record.UpdatedBy = 0
results.Record.DeletedBy = 0
return results, nil
}

View File

@@ -0,0 +1,300 @@
prefix: compose
imports:
- github.com/cortezaproject/corteza/server/compose/types
params:
moduleLookup: &moduleLookup
required: true
types:
- { wf: ID, }
- { wf: Handle, }
- { wf: ComposeModule, suffix: res }
meta:
label: Module
description: |-
Even with unique record ID across all modules, module needs to be known
before doing any record operations. Mainly because records of different
modules can be located in different stores.
namespaceLookup: &namespaceLookup
meta: { label: Namespace }
required: true
types:
- { wf: ID, }
- { wf: Handle, }
- { wf: ComposeNamespace, suffix: res }
recordLookup: &recordLookup
meta: { label: Record }
required: true
types:
- { wf: ID, }
- { wf: ComposeRecord, suffix: res }
record: &record
meta: { label: Record }
required: true
types:
- { wf: ComposeRecord, }
rvRecord: &rvRecord
wf: ComposeRecord
rvTotal: &rvTotal
wf: UnsignedInteger
meta:
label: Total records found
description: |-
Total items that satisfy given conditions.
Needs to be explicitly requested with incTotal argument
rvIndex: &rvIndex
wf: UnsignedInteger
meta:
label: Iteration counter
description: |-
Zero-based number iteration counter
rvPageCursor: &rvPageCursor
wf: String
rvPageNavigation: &rvPageNavigation
wf: Array
filterParams: &filterParams
namespace: *namespaceLookup
module: *moduleLookup
query:
meta: { label: Query }
types:
- { wf: String }
meta:
meta: { label: Meta }
types:
- { wf: Meta }
deleted:
meta: { label: Deleted }
types:
- { wf: UnsignedInteger }
sort:
meta: { label: Sort }
types:
- { wf: String }
limit:
meta: { label: Limit }
types:
- { wf: UnsignedInteger }
incTotal:
meta: { label: Include total }
types:
- { wf: Boolean }
incPageNavigation:
meta: { label: Include page navigation }
types:
- { wf: Boolean }
pageCursor:
meta: { label: Page cursor }
types:
- { wf: String }
edgeParams: &edgeParams
namespace: *namespaceLookup
module: *moduleLookup
labels: &labels
record: "step,workflow"
compose: "step,workflow"
functions:
lookup:
meta:
short: Compose record lookup
description: Find specific record by ID
labels:
<<: *labels
params:
namespace: *namespaceLookup
module: *moduleLookup
record: *recordLookup
results:
record: *rvRecord
search:
meta:
short: Compose records search
labels:
<<: *labels
params: *filterParams
results:
records:
<<: *rvRecord
isArray: true
total: *rvTotal
nextPage: *rvPageCursor
prevPage: *rvPageCursor
pageNavigation: *rvPageNavigation
first:
meta:
short: Compose record lookup (oldest)
labels:
<<: *labels
params: *edgeParams
results:
record: *rvRecord
last:
meta:
short: Compose record lookup (newest)
labels:
<<: *labels
params: *edgeParams
results:
record: *rvRecord
each:
kind: iterator
meta:
short: Compose records
labels:
<<: *labels
params: *filterParams
results:
record: *rvRecord
index: *rvIndex
total: *rvTotal
new:
meta:
short: Compose record maker
description: Creates new compose record instance without saving it
labels:
<<: *labels
params:
namespace: *namespaceLookup
module: *moduleLookup
results:
record: *rvRecord
validate:
meta:
short: Compose record validator
labels:
<<: *labels
params:
record: *record
results:
valid:
wf: Boolean
meta:
label: Set to true when record is valid
create:
meta:
short: Compose record create
labels:
<<: *labels
create: "step"
params:
record:
<<: *record
meta: { label: Record }
required: true
results:
record: *rvRecord
update:
meta:
short: Compose record update
labels:
<<: *labels
update: "step"
params:
record:
<<: *record
meta: { label: Record }
required: true
results:
record: *rvRecord
delete:
meta:
short: Compose record delete
labels:
<<: *labels
delete: "step"
params:
module:
<<: *moduleLookup
required: false
namespace:
<<: *namespaceLookup
meta: { label: Namespace }
required: false
record: *recordLookup
# undelete:
# meta:
# short: Recovers deleted record by ID
# params:
# record: *recordLookup
report:
meta:
short: Report
description: Compose records report
labels:
<<: *labels
params:
namespace: *namespaceLookup
module: *moduleLookup
metrics:
required: true
types: [ { wf: String } ]
meta:
label: Metrics for records report
description: |-
List of comma delimited expressions with aggregation functions (count, sum, min, avg)
dimensons:
required: true
types: [ { wf: String } ]
meta:
label: Dimensons for records report
description: |-
List of comma delimited dimension (i.e.: group by) expressions or fields
filter:
required: true
types: [ { wf: String } ]
meta:
label: Filter for records report
description: |-
Filter in CortezaQL format
results:
report:
wf: Any
meta:
label: Complex structure holding complete records report
description: |-
Example of a result value
[]map[string]interface{}{
{"count": 3, "dimension_0": 1, "metric_0": 3},
{"count": 2, "dimension_0": 2, "metric_0": 5},
{"count": 1, "dimension_0": nil, "metric_0": nil},
}
clone:
meta:
short: Compose record cloner
description: Creates a copy of an existing record
labels:
<<: *labels
params:
namespace: *namespaceLookup
module: *moduleLookup
record: *recordLookup
results:
record: *rvRecord