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
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:
498
server/compose/automation/attachment_handler.gen.go
generated
Normal file
498
server/compose/automation/attachment_handler.gen.go
generated
Normal 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
|
||||
},
|
||||
}
|
||||
}
|
||||
227
server/compose/automation/attachment_handler.go
Normal file
227
server/compose/automation/attachment_handler.go
Normal 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
|
||||
}
|
||||
99
server/compose/automation/attachment_handler.yaml
Normal file
99
server/compose/automation/attachment_handler.yaml
Normal 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 }
|
||||
858
server/compose/automation/expr_types.gen.go
generated
Normal file
858
server/compose/automation/expr_types.gen.go
generated
Normal 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
|
||||
}
|
||||
}
|
||||
817
server/compose/automation/expr_types.go
Normal file
817
server/compose/automation/expr_types.go
Normal 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
|
||||
}
|
||||
64
server/compose/automation/expr_types.yaml
Normal file
64
server/compose/automation/expr_types.yaml
Normal 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 }
|
||||
749
server/compose/automation/expr_types_test.go
Normal file
749
server/compose/automation/expr_types_test.go
Normal 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
|
||||
}
|
||||
165
server/compose/automation/modules_handler.gen.go
generated
Normal file
165
server/compose/automation/modules_handler.gen.go
generated
Normal 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
|
||||
},
|
||||
}
|
||||
}
|
||||
83
server/compose/automation/modules_handler.go
Normal file
83
server/compose/automation/modules_handler.go
Normal 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")
|
||||
}
|
||||
39
server/compose/automation/modules_handler.yaml
Normal file
39
server/compose/automation/modules_handler.yaml
Normal 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
|
||||
134
server/compose/automation/namespaces_handler.gen.go
generated
Normal file
134
server/compose/automation/namespaces_handler.gen.go
generated
Normal 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
|
||||
},
|
||||
}
|
||||
}
|
||||
72
server/compose/automation/namespaces_handler.go
Normal file
72
server/compose/automation/namespaces_handler.go
Normal 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")
|
||||
}
|
||||
30
server/compose/automation/namespaces_handler.yaml
Normal file
30
server/compose/automation/namespaces_handler.yaml
Normal 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
|
||||
214
server/compose/automation/notification_handler.gen.go
generated
Normal file
214
server/compose/automation/notification_handler.gen.go
generated
Normal 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 = ¬ificationSendRecordArgs{
|
||||
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)
|
||||
},
|
||||
}
|
||||
}
|
||||
131
server/compose/automation/notification_handler.go
Normal file
131
server/compose/automation/notification_handler.go
Normal 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 := ¬ificationHandler{
|
||||
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
|
||||
}
|
||||
72
server/compose/automation/notification_handler.yaml
Normal file
72
server/compose/automation/notification_handler.yaml
Normal 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)
|
||||
1846
server/compose/automation/records_handler.gen.go
generated
Normal file
1846
server/compose/automation/records_handler.gen.go
generated
Normal file
File diff suppressed because it is too large
Load Diff
460
server/compose/automation/records_handler.go
Normal file
460
server/compose/automation/records_handler.go
Normal 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
|
||||
}
|
||||
300
server/compose/automation/records_handler.yaml
Normal file
300
server/compose/automation/records_handler.yaml
Normal 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
|
||||
Reference in New Issue
Block a user