Support attachment management in workflow
This commit is contained in:
461
compose/automation/attachment_handler.gen.go
generated
Normal file
461
compose/automation/attachment_handler.gen.go
generated
Normal file
@@ -0,0 +1,461 @@
|
||||
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 Compose record 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: "Compose record lookup",
|
||||
Description: "Find specific record by ID",
|
||||
},
|
||||
|
||||
Parameters: []*atypes.Param{
|
||||
{
|
||||
Name: "attachment",
|
||||
Types: []string{"ID"}, Required: true,
|
||||
},
|
||||
},
|
||||
|
||||
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
|
||||
|
||||
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"},
|
||||
},
|
||||
{
|
||||
Name: "resource",
|
||||
Types: []string{"ComposeRecord"}, Required: true,
|
||||
},
|
||||
{
|
||||
Name: "content",
|
||||
Types: []string{"String", "Reader", "Bytes"}, Required: true,
|
||||
},
|
||||
},
|
||||
|
||||
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"),
|
||||
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,
|
||||
},
|
||||
},
|
||||
|
||||
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,
|
||||
},
|
||||
},
|
||||
|
||||
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,
|
||||
},
|
||||
},
|
||||
|
||||
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
|
||||
},
|
||||
}
|
||||
}
|
||||
139
compose/automation/attachment_handler.go
Normal file
139
compose/automation/attachment_handler.go
Normal file
@@ -0,0 +1,139 @@
|
||||
package automation
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"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) (att *types.Attachment, err error)
|
||||
DeleteByID(ctx context.Context, namespaceID uint64, attachmentID uint64) error
|
||||
OpenOriginal(att *types.Attachment) (io.ReadSeeker, error)
|
||||
OpenPreview(att *types.Attachment) (io.ReadSeeker, 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
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
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.contentString))
|
||||
fh = bytes.NewReader(args.contentBytes)
|
||||
|
||||
case args.contentStream != nil:
|
||||
if rs, is := args.contentStream.(io.ReadSeeker); is {
|
||||
fh = rs
|
||||
}
|
||||
|
||||
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,
|
||||
)
|
||||
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")
|
||||
}
|
||||
87
compose/automation/attachment_handler.yaml
Normal file
87
compose/automation/attachment_handler.yaml
Normal file
@@ -0,0 +1,87 @@
|
||||
# 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: Compose record lookup
|
||||
description: Find specific record by ID
|
||||
labels:
|
||||
<<: *labels
|
||||
params:
|
||||
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:
|
||||
types:
|
||||
- { wf: String }
|
||||
|
||||
resource:
|
||||
required: true
|
||||
types:
|
||||
- { wf: ComposeRecord }
|
||||
|
||||
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:
|
||||
required: true
|
||||
types: [ { wf: ID } ]
|
||||
|
||||
openOriginal:
|
||||
meta:
|
||||
short: Open original attachment
|
||||
labels:
|
||||
<<: *labels
|
||||
original-attachment: "step"
|
||||
params:
|
||||
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:
|
||||
required: true
|
||||
types: [ { wf: ID }, { wf: Attachment } ]
|
||||
results:
|
||||
content: { wf: Reader }
|
||||
201
compose/automation/expr_types.gen.go
generated
201
compose/automation/expr_types.gen.go
generated
@@ -19,6 +19,207 @@ import (
|
||||
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
|
||||
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
|
||||
|
||||
@@ -509,3 +509,12 @@ func isNil(i interface{}) bool {
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -51,8 +51,14 @@ types:
|
||||
ComposeRecordValueErrorSet:
|
||||
as: '*types.RecordValueErrorSet'
|
||||
|
||||
#
|
||||
# Page:
|
||||
# as: '*types.Page'
|
||||
# Chart:
|
||||
# as: '*types.Chart'
|
||||
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 }
|
||||
|
||||
@@ -157,6 +157,7 @@ func Initialize(ctx context.Context, log *zap.Logger, s store.Storer, c Config)
|
||||
automation.ComposeModule{},
|
||||
automation.ComposeRecord{},
|
||||
automation.ComposeRecordValues{},
|
||||
automation.Attachment{},
|
||||
)
|
||||
|
||||
automation.RecordsHandler(
|
||||
@@ -177,6 +178,11 @@ func Initialize(ctx context.Context, log *zap.Logger, s store.Storer, c Config)
|
||||
DefaultNamespace,
|
||||
)
|
||||
|
||||
automation.AttachmentHandler(
|
||||
automationService.Registry(),
|
||||
DefaultAttachment,
|
||||
)
|
||||
|
||||
// Register reporters
|
||||
// @todo additional datasource providers; generate?
|
||||
systemService.DefaultReport.RegisterReporter("composeRecords", DefaultRecord)
|
||||
|
||||
1
tests/.gitignore
vendored
1
tests/.gitignore
vendored
@@ -1 +1,2 @@
|
||||
.env
|
||||
workflows/var
|
||||
|
||||
43
tests/workflows/attachment_management_test.go
Normal file
43
tests/workflows/attachment_management_test.go
Normal file
@@ -0,0 +1,43 @@
|
||||
package workflows
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"testing"
|
||||
|
||||
"github.com/cortezaproject/corteza-server/automation/types"
|
||||
cmpTypes "github.com/cortezaproject/corteza-server/compose/types"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func Test_attachment_management(t *testing.T) {
|
||||
var (
|
||||
ctx = bypassRBAC(context.Background())
|
||||
req = require.New(t)
|
||||
)
|
||||
|
||||
req.NoError(defStore.TruncateAttachments(ctx))
|
||||
|
||||
loadNewScenario(ctx, t)
|
||||
|
||||
var (
|
||||
aux = struct {
|
||||
Base64blackGif string
|
||||
LoadedAttBlackGif *cmpTypes.Attachment
|
||||
StoredAttBlackGif *cmpTypes.Attachment
|
||||
LoadedContent io.ReadSeeker
|
||||
}{}
|
||||
)
|
||||
|
||||
vars, _ := mustExecWorkflow(ctx, t, "attachments", types.WorkflowExecParams{})
|
||||
req.NoError(vars.Decode(&aux))
|
||||
|
||||
req.Equal(int(aux.LoadedAttBlackGif.Meta.Original.Size), len(aux.Base64blackGif))
|
||||
req.Equal(int(aux.StoredAttBlackGif.Meta.Original.Size), len(aux.Base64blackGif))
|
||||
|
||||
b, err := ioutil.ReadAll(aux.LoadedContent)
|
||||
req.NoError(err)
|
||||
req.Equal(aux.Base64blackGif, string(b))
|
||||
|
||||
}
|
||||
@@ -16,7 +16,6 @@ import (
|
||||
"github.com/cortezaproject/corteza-server/pkg/envoy/json"
|
||||
envoyStore "github.com/cortezaproject/corteza-server/pkg/envoy/store"
|
||||
"github.com/cortezaproject/corteza-server/pkg/envoy/yaml"
|
||||
"github.com/cortezaproject/corteza-server/pkg/errors"
|
||||
"github.com/cortezaproject/corteza-server/pkg/eventbus"
|
||||
"github.com/cortezaproject/corteza-server/pkg/expr"
|
||||
"github.com/cortezaproject/corteza-server/pkg/id"
|
||||
|
||||
9
tests/workflows/testdata/attachment_management/data_model.yaml
vendored
Normal file
9
tests/workflows/testdata/attachment_management/data_model.yaml
vendored
Normal file
@@ -0,0 +1,9 @@
|
||||
namespaces:
|
||||
ns1:
|
||||
name: Namespace#1
|
||||
|
||||
modules:
|
||||
mod1:
|
||||
name: Module#1
|
||||
fields:
|
||||
f1: { label: 'Field1' }
|
||||
58
tests/workflows/testdata/attachment_management/workflow.yaml
vendored
Normal file
58
tests/workflows/testdata/attachment_management/workflow.yaml
vendored
Normal file
@@ -0,0 +1,58 @@
|
||||
workflows:
|
||||
attachments:
|
||||
enabled: true
|
||||
trace: true
|
||||
triggers:
|
||||
- enabled: true
|
||||
stepID: 10
|
||||
|
||||
steps:
|
||||
- stepID: 10
|
||||
kind: expressions
|
||||
arguments:
|
||||
- { target: base64blackGif, value: "R0lGODlhAQABAIAAAAUEBAAAACwAAAAAAQABAAACAkQBADs=" }
|
||||
|
||||
- stepID: 11
|
||||
kind: function
|
||||
ref: composeRecordsNew
|
||||
arguments:
|
||||
- { target: module, type: Handle, value: "mod1" }
|
||||
- { target: namespace, type: Handle, value: "ns1" }
|
||||
results:
|
||||
- { target: attachable, expr: "record" }
|
||||
|
||||
|
||||
# Store attachment from given string
|
||||
- stepID: 20
|
||||
kind: function
|
||||
ref: attachmentCreate
|
||||
arguments:
|
||||
- { target: content, type: String, expr: "base64blackGif" }
|
||||
- { target: name, type: String, value: "black-from-base64-string.gif" }
|
||||
- { target: resource, type: ComposeRecord, expr: "attachable" }
|
||||
results:
|
||||
- { target: storedAttBlackGif, expr: "attachment" }
|
||||
|
||||
# Load attachment
|
||||
- stepID: 30
|
||||
kind: function
|
||||
ref: attachmentLookup
|
||||
arguments:
|
||||
- { target: attachment, type: ID, expr: "storedAttBlackGif.ID" }
|
||||
results:
|
||||
- { target: loadedAttBlackGif, expr: "attachment" }
|
||||
|
||||
# Open attachment
|
||||
- stepID: 31
|
||||
kind: function
|
||||
ref: attachmentOpenOriginal
|
||||
arguments:
|
||||
- { target: attachment, type: "Attachment", expr: "storedAttBlackGif" }
|
||||
results:
|
||||
- { target: loadedContent, expr: "content" }
|
||||
|
||||
paths:
|
||||
- { parentID: 10, childID: 11 }
|
||||
- { parentID: 11, childID: 20 }
|
||||
- { parentID: 20, childID: 30 }
|
||||
- { parentID: 30, childID: 31 }
|
||||
Reference in New Issue
Block a user