From b9f96d920a7678ff3d06c3bb8ba2cf2ffc11db8f Mon Sep 17 00:00:00 2001 From: Denis Arh Date: Mon, 29 Nov 2021 21:11:15 +0100 Subject: [PATCH] Support attachment management in workflow --- compose/automation/attachment_handler.gen.go | 461 ++++++++++++++++++ compose/automation/attachment_handler.go | 139 ++++++ compose/automation/attachment_handler.yaml | 87 ++++ compose/automation/expr_types.gen.go | 201 ++++++++ compose/automation/expr_types.go | 9 + compose/automation/expr_types.yaml | 16 +- compose/service/service.go | 6 + tests/.gitignore | 1 + tests/workflows/attachment_management_test.go | 43 ++ tests/workflows/main_test.go | 1 - .../attachment_management/data_model.yaml | 9 + .../attachment_management/workflow.yaml | 58 +++ 12 files changed, 1025 insertions(+), 6 deletions(-) create mode 100644 compose/automation/attachment_handler.gen.go create mode 100644 compose/automation/attachment_handler.go create mode 100644 compose/automation/attachment_handler.yaml create mode 100644 tests/workflows/attachment_management_test.go create mode 100644 tests/workflows/testdata/attachment_management/data_model.yaml create mode 100644 tests/workflows/testdata/attachment_management/workflow.yaml diff --git a/compose/automation/attachment_handler.gen.go b/compose/automation/attachment_handler.gen.go new file mode 100644 index 000000000..d1d61cc55 --- /dev/null +++ b/compose/automation/attachment_handler.gen.go @@ -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 + }, + } +} diff --git a/compose/automation/attachment_handler.go b/compose/automation/attachment_handler.go new file mode 100644 index 000000000..b8635a176 --- /dev/null +++ b/compose/automation/attachment_handler.go @@ -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") +} diff --git a/compose/automation/attachment_handler.yaml b/compose/automation/attachment_handler.yaml new file mode 100644 index 000000000..2f3dbdce8 --- /dev/null +++ b/compose/automation/attachment_handler.yaml @@ -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 } diff --git a/compose/automation/expr_types.gen.go b/compose/automation/expr_types.gen.go index b8225d66a..f601f6c74 100644 --- a/compose/automation/expr_types.gen.go +++ b/compose/automation/expr_types.gen.go @@ -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 diff --git a/compose/automation/expr_types.go b/compose/automation/expr_types.go index 026294f26..3ef25a702 100644 --- a/compose/automation/expr_types.go +++ b/compose/automation/expr_types.go @@ -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) + } +} diff --git a/compose/automation/expr_types.yaml b/compose/automation/expr_types.yaml index c286fbb29..8dc4b7706 100644 --- a/compose/automation/expr_types.yaml +++ b/compose/automation/expr_types.yaml @@ -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 } diff --git a/compose/service/service.go b/compose/service/service.go index 75fb20e80..afffde8cf 100644 --- a/compose/service/service.go +++ b/compose/service/service.go @@ -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) diff --git a/tests/.gitignore b/tests/.gitignore index 4c49bd78f..69222c2fe 100644 --- a/tests/.gitignore +++ b/tests/.gitignore @@ -1 +1,2 @@ .env +workflows/var diff --git a/tests/workflows/attachment_management_test.go b/tests/workflows/attachment_management_test.go new file mode 100644 index 000000000..f5054e2a3 --- /dev/null +++ b/tests/workflows/attachment_management_test.go @@ -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)) + +} diff --git a/tests/workflows/main_test.go b/tests/workflows/main_test.go index cfa7bab9c..84d51b419 100644 --- a/tests/workflows/main_test.go +++ b/tests/workflows/main_test.go @@ -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" diff --git a/tests/workflows/testdata/attachment_management/data_model.yaml b/tests/workflows/testdata/attachment_management/data_model.yaml new file mode 100644 index 000000000..dc4556ab3 --- /dev/null +++ b/tests/workflows/testdata/attachment_management/data_model.yaml @@ -0,0 +1,9 @@ +namespaces: + ns1: + name: Namespace#1 + +modules: + mod1: + name: Module#1 + fields: + f1: { label: 'Field1' } diff --git a/tests/workflows/testdata/attachment_management/workflow.yaml b/tests/workflows/testdata/attachment_management/workflow.yaml new file mode 100644 index 000000000..ad4c5c2c4 --- /dev/null +++ b/tests/workflows/testdata/attachment_management/workflow.yaml @@ -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 }