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/rest/attachment.go b/compose/rest/attachment.go index f6883a989..b516723b2 100644 --- a/compose/rest/attachment.go +++ b/compose/rest/attachment.go @@ -53,7 +53,7 @@ func (ctrl Attachment) List(ctx context.Context, r *request.AttachmentList) (int FieldName: r.FieldName, } - set, filter, err := ctrl.attachment.With(ctx).Find(f) + set, filter, err := ctrl.attachment.Find(ctx, f) return ctrl.makeFilterPayload(ctx, set, filter, err) } @@ -62,7 +62,7 @@ func (ctrl Attachment) Read(ctx context.Context, r *request.AttachmentRead) (int return nil, errors.New("Unauthorized") } - a, err := ctrl.attachment.With(ctx).FindByID(r.NamespaceID, r.AttachmentID) + a, err := ctrl.attachment.FindByID(ctx, r.NamespaceID, r.AttachmentID) return makeAttachmentPayload(ctx, a, err) } @@ -71,12 +71,12 @@ func (ctrl Attachment) Delete(ctx context.Context, r *request.AttachmentDelete) return nil, errors.New("Unauthorized") } - _, err := ctrl.attachment.With(ctx).FindByID(r.NamespaceID, r.AttachmentID) + _, err := ctrl.attachment.FindByID(ctx, r.NamespaceID, r.AttachmentID) if err != nil { return nil, err } - return api.OK(), ctrl.attachment.With(ctx).DeleteByID(r.NamespaceID, r.AttachmentID) + return api.OK(), ctrl.attachment.DeleteByID(ctx, r.NamespaceID, r.AttachmentID) } func (ctrl Attachment) Original(ctx context.Context, r *request.AttachmentOriginal) (interface{}, error) { @@ -117,7 +117,7 @@ func (ctrl Attachment) isAccessible(namespaceID, attachmentID, userID uint64, si func (ctrl Attachment) serve(ctx context.Context, namespaceID, attachmentID uint64, preview, download bool) (interface{}, error) { return func(w http.ResponseWriter, req *http.Request) { - att, err := ctrl.attachment.With(ctx).FindByID(namespaceID, attachmentID) + att, err := ctrl.attachment.FindByID(ctx, namespaceID, attachmentID) if err != nil { // Simplify error handling for now w.WriteHeader(http.StatusNotFound) diff --git a/compose/rest/namespace.go b/compose/rest/namespace.go index b2a854ce5..bc902b7a5 100644 --- a/compose/rest/namespace.go +++ b/compose/rest/namespace.go @@ -158,7 +158,7 @@ func (ctrl Namespace) Upload(ctx context.Context, r *request.NamespaceUpload) (i } defer file.Close() - a, err := ctrl.attachment.With(ctx).CreateNamespaceAttachment( + a, err := ctrl.attachment.CreateNamespaceAttachment( ctx, r.Upload.Filename, r.Upload.Size, diff --git a/compose/rest/page.go b/compose/rest/page.go index e361b8b9d..addc51c7c 100644 --- a/compose/rest/page.go +++ b/compose/rest/page.go @@ -180,7 +180,8 @@ func (ctrl *Page) Upload(ctx context.Context, r *request.PageUpload) (interface{ defer file.Close() - a, err := ctrl.attachment.With(ctx).CreatePageAttachment( + a, err := ctrl.attachment.CreatePageAttachment( + ctx, r.NamespaceID, r.Upload.Filename, r.Upload.Size, diff --git a/compose/rest/record.go b/compose/rest/record.go index 55a0e75ee..edd91f967 100644 --- a/compose/rest/record.go +++ b/compose/rest/record.go @@ -266,14 +266,14 @@ func (ctrl *Record) Upload(ctx context.Context, r *request.RecordUpload) (interf defer file.Close() - a, err := ctrl.attachment.With(ctx).CreateRecordAttachment( + a, err := ctrl.attachment.CreateRecordAttachment( + ctx, r.NamespaceID, r.Upload.Filename, r.Upload.Size, file, r.ModuleID, r.RecordID, - r.FieldName, ) return makeAttachmentPayload(ctx, a, err) diff --git a/compose/service/attachment.go b/compose/service/attachment.go index 660bcf965..fe76de400 100644 --- a/compose/service/attachment.go +++ b/compose/service/attachment.go @@ -27,7 +27,6 @@ const ( type ( attachment struct { - ctx context.Context actionlog actionlog.Recorder objects objstore.Store ac attachmentAccessController @@ -46,38 +45,26 @@ type ( } AttachmentService interface { - With(ctx context.Context) AttachmentService - - FindByID(namespaceID, attachmentID uint64) (*types.Attachment, error) - Find(filter types.AttachmentFilter) (types.AttachmentSet, types.AttachmentFilter, error) - CreatePageAttachment(namespaceID uint64, name string, size int64, fh io.ReadSeeker, pageID uint64) (*types.Attachment, error) - CreateRecordAttachment(namespaceID uint64, name string, size int64, fh io.ReadSeeker, moduleID, recordID uint64, fieldName string) (*types.Attachment, error) + FindByID(ctx context.Context, namespaceID, attachmentID uint64) (*types.Attachment, error) + Find(ctx context.Context, filter types.AttachmentFilter) (types.AttachmentSet, types.AttachmentFilter, error) + CreatePageAttachment(ctx context.Context, namespaceID uint64, name string, size int64, fh io.ReadSeeker, pageID uint64) (*types.Attachment, error) + CreateRecordAttachment(ctx context.Context, namespaceID uint64, name string, size int64, fh io.ReadSeeker, moduleID, recordID uint64) (*types.Attachment, error) CreateNamespaceAttachment(ctx context.Context, name string, size int64, fh io.ReadSeeker) (*types.Attachment, error) OpenOriginal(att *types.Attachment) (io.ReadSeeker, error) OpenPreview(att *types.Attachment) (io.ReadSeeker, error) - DeleteByID(namespaceID, attachmentID uint64) error + DeleteByID(ctx context.Context, namespaceID, attachmentID uint64) error } ) -func Attachment(store objstore.Store) AttachmentService { - return (&attachment{ +func Attachment(store objstore.Store) *attachment { + return &attachment{ objects: store, ac: DefaultAccessControl, store: DefaultStore, - }).With(context.Background()) -} - -func (svc attachment) With(ctx context.Context) AttachmentService { - return &attachment{ - ctx: ctx, - actionlog: DefaultActionlog, - ac: svc.ac, - objects: svc.objects, - store: svc.store, } } -func (svc attachment) Find(filter types.AttachmentFilter) (set types.AttachmentSet, f types.AttachmentFilter, err error) { +func (svc attachment) Find(ctx context.Context, filter types.AttachmentFilter) (set types.AttachmentSet, f types.AttachmentFilter, err error) { var ( aProps = &attachmentActionProps{filter: &filter} ) @@ -88,38 +75,38 @@ func (svc attachment) Find(filter types.AttachmentFilter) (set types.AttachmentS } if filter.PageID > 0 { - aProps.namespace, aProps.page, err = loadPage(svc.ctx, svc.store, filter.NamespaceID, filter.PageID) + aProps.namespace, aProps.page, err = loadPage(ctx, svc.store, filter.NamespaceID, filter.PageID) if err != nil { return err - } else if svc.ac.CanReadPage(svc.ctx, aProps.page) { + } else if svc.ac.CanReadPage(ctx, aProps.page) { return AttachmentErrNotAllowedToReadPage() } } if filter.RecordID > 0 { - aProps.namespace, aProps.module, aProps.record, err = loadRecordCombo(svc.ctx, svc.store, filter.NamespaceID, filter.ModuleID, filter.RecordID) + aProps.namespace, aProps.module, aProps.record, err = loadRecordCombo(ctx, svc.store, filter.NamespaceID, filter.ModuleID, filter.RecordID) if err != nil { return err - } else if svc.ac.CanReadRecord(svc.ctx, aProps.record) { + } else if svc.ac.CanReadRecord(ctx, aProps.record) { return AttachmentErrNotAllowedToReadRecord() } } else if filter.ModuleID > 0 { - aProps.namespace, aProps.module, err = loadModuleWithNamespace(svc.ctx, svc.store, filter.NamespaceID, filter.ModuleID) + aProps.namespace, aProps.module, err = loadModuleWithNamespace(ctx, svc.store, filter.NamespaceID, filter.ModuleID) if err != nil { return err - } else if svc.ac.CanReadRecord(svc.ctx, aProps.record) { + } else if svc.ac.CanReadRecord(ctx, aProps.record) { return AttachmentErrNotAllowedToReadRecord() } } - set, f, err = store.SearchComposeAttachments(svc.ctx, svc.store, f) + set, f, err = store.SearchComposeAttachments(ctx, svc.store, f) return err }() - return set, f, svc.recordAction(svc.ctx, aProps, AttachmentActionSearch, err) + return set, f, svc.recordAction(ctx, aProps, AttachmentActionSearch, err) } -func (svc attachment) FindByID(namespaceID, attachmentID uint64) (att *types.Attachment, err error) { +func (svc attachment) FindByID(ctx context.Context, namespaceID, attachmentID uint64) (att *types.Attachment, err error) { var ( aProps = &attachmentActionProps{} ) @@ -129,7 +116,7 @@ func (svc attachment) FindByID(namespaceID, attachmentID uint64) (att *types.Att return AttachmentErrInvalidID() } - if att, err = store.LookupComposeAttachmentByID(svc.ctx, svc.store, attachmentID); err != nil { + if att, err = store.LookupComposeAttachmentByID(ctx, svc.store, attachmentID); err != nil { return err } @@ -137,16 +124,16 @@ func (svc attachment) FindByID(namespaceID, attachmentID uint64) (att *types.Att return nil }() - return att, svc.recordAction(svc.ctx, aProps, AttachmentActionLookup, err) + return att, svc.recordAction(ctx, aProps, AttachmentActionLookup, err) } -func (svc attachment) DeleteByID(namespaceID, attachmentID uint64) (err error) { +func (svc attachment) DeleteByID(ctx context.Context, namespaceID, attachmentID uint64) (err error) { var ( att *types.Attachment aProps = &attachmentActionProps{attachment: &types.Attachment{ID: attachmentID}} ) - err = store.Tx(svc.ctx, svc.store, func(ctx context.Context, s store.Storer) (err error) { + err = store.Tx(ctx, svc.store, func(ctx context.Context, s store.Storer) (err error) { if attachmentID == 0 { return AttachmentErrInvalidID() } @@ -161,7 +148,7 @@ func (svc attachment) DeleteByID(namespaceID, attachmentID uint64) (err error) { return store.UpdateComposeAttachment(ctx, s, att) }) - return svc.recordAction(svc.ctx, aProps, AttachmentActionDelete, err) + return svc.recordAction(ctx, aProps, AttachmentActionDelete, err) } //func (svc attachment) findNamespaceByID(namespaceID uint64) (ns *types.Namespace, err error) { @@ -174,7 +161,7 @@ func (svc attachment) DeleteByID(namespaceID, attachmentID uint64) (err error) { // return nil, AttachmentErrNamespaceNotFound() // } // -// if !svc.ac.CanReadNamespace(svc.ctx, ns) { +// if !svc.ac.CanReadNamespace(ctx, ns) { // return nil, AttachmentErrNotAllowedToReadNamespace() // } // @@ -191,7 +178,7 @@ func (svc attachment) DeleteByID(namespaceID, attachmentID uint64) (err error) { // return nil, AttachmentErrPageNotFound() // } // -// if !svc.ac.CanReadPage(svc.ctx, p) { +// if !svc.ac.CanReadPage(ctx, p) { // return nil, AttachmentErrNotAllowedToReadPage() // } // @@ -208,7 +195,7 @@ func (svc attachment) DeleteByID(namespaceID, attachmentID uint64) (err error) { // return nil, AttachmentErrModuleNotFound() // } // -// if !svc.ac.CanReadModule(svc.ctx, m) { +// if !svc.ac.CanReadModule(ctx, m) { // return nil, AttachmentErrNotAllowedToReadModule() // } // @@ -225,7 +212,7 @@ func (svc attachment) DeleteByID(namespaceID, attachmentID uint64) (err error) { // return nil, AttachmentErrRecordNotFound() // } // -// if !svc.ac.CanReadRecord(svc.ctx, m) { +// if !svc.ac.CanReadRecord(ctx, m) { // return nil, AttachmentErrNotAllowedToReadRecord() // } // @@ -248,7 +235,7 @@ func (svc attachment) OpenPreview(att *types.Attachment) (io.ReadSeeker, error) return svc.objects.Open(att.PreviewUrl) } -func (svc attachment) CreatePageAttachment(namespaceID uint64, name string, size int64, fh io.ReadSeeker, pageID uint64) (att *types.Attachment, err error) { +func (svc attachment) CreatePageAttachment(ctx context.Context, namespaceID uint64, name string, size int64, fh io.ReadSeeker, pageID uint64) (att *types.Attachment, err error) { var ( ns *types.Namespace p *types.Page @@ -260,7 +247,7 @@ func (svc attachment) CreatePageAttachment(namespaceID uint64, name string, size ) err = func() error { - ns, p, err = loadPage(svc.ctx, svc.store, namespaceID, pageID) + ns, p, err = loadPage(ctx, svc.store, namespaceID, pageID) if err != nil { return err } @@ -268,7 +255,7 @@ func (svc attachment) CreatePageAttachment(namespaceID uint64, name string, size aProps.setNamespace(ns) aProps.setPage(p) - if !svc.ac.CanUpdatePage(svc.ctx, p) { + if !svc.ac.CanUpdatePage(ctx, p) { return AttachmentErrNotAllowedToUpdatePage() } @@ -278,14 +265,14 @@ func (svc attachment) CreatePageAttachment(namespaceID uint64, name string, size Kind: types.PageAttachment, } - return svc.create(name, size, fh, att) + return svc.create(ctx, name, size, fh, att) }() - return att, svc.recordAction(svc.ctx, aProps, AttachmentActionCreate, err) + return att, svc.recordAction(ctx, aProps, AttachmentActionCreate, err) } -func (svc attachment) CreateRecordAttachment(namespaceID uint64, name string, size int64, fh io.ReadSeeker, moduleID, recordID uint64, fieldName string) (att *types.Attachment, err error) { +func (svc attachment) CreateRecordAttachment(ctx context.Context, namespaceID uint64, name string, size int64, fh io.ReadSeeker, moduleID, recordID uint64) (att *types.Attachment, err error) { var ( ns *types.Namespace m *types.Module @@ -298,7 +285,7 @@ func (svc attachment) CreateRecordAttachment(namespaceID uint64, name string, si } ) - err = store.Tx(svc.ctx, svc.store, func(ctx context.Context, s store.Storer) (err error) { + err = store.Tx(ctx, svc.store, func(ctx context.Context, s store.Storer) (err error) { ns, m, err = loadModuleWithNamespace(ctx, s, namespaceID, moduleID) if err != nil { return err @@ -339,10 +326,10 @@ func (svc attachment) CreateRecordAttachment(namespaceID uint64, name string, si Kind: types.RecordAttachment, } - return svc.create(name, size, fh, att) + return svc.create(ctx, name, size, fh, att) }) - return att, svc.recordAction(svc.ctx, aProps, AttachmentActionCreate, err) + return att, svc.recordAction(ctx, aProps, AttachmentActionCreate, err) } func (svc attachment) CreateNamespaceAttachment(ctx context.Context, name string, size int64, fh io.ReadSeeker) (att *types.Attachment, err error) { @@ -361,13 +348,13 @@ func (svc attachment) CreateNamespaceAttachment(ctx context.Context, name string Kind: types.NamespaceAttachment, } - return svc.create(name, size, fh, att) + return svc.create(ctx, name, size, fh, att) }) return att, svc.recordAction(ctx, aProps, AttachmentActionCreate, err) } -func (svc attachment) create(name string, size int64, fh io.ReadSeeker, att *types.Attachment) (err error) { +func (svc attachment) create(ctx context.Context, name string, size int64, fh io.ReadSeeker, att *types.Attachment) (err error) { var ( aProps = &attachmentActionProps{} ) @@ -377,7 +364,7 @@ func (svc attachment) create(name string, size int64, fh io.ReadSeeker, att *typ att.CreatedAt = *now() if att.OwnerID == 0 { - att.OwnerID = auth.GetIdentityFromContext(svc.ctx).Identity() + att.OwnerID = auth.GetIdentityFromContext(ctx).Identity() } if svc.objects == nil { @@ -412,7 +399,7 @@ func (svc attachment) create(name string, size int64, fh io.ReadSeeker, att *typ return AttachmentErrFailedToProcessImage(aProps).Wrap(err) } - if err = store.CreateComposeAttachment(svc.ctx, svc.store, att); err != nil { + if err = store.CreateComposeAttachment(ctx, svc.store, att); err != nil { return } 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/pkg/codegen/afunc.go b/pkg/codegen/afunc.go index cf5c438b3..5bad27190 100644 --- a/pkg/codegen/afunc.go +++ b/pkg/codegen/afunc.go @@ -3,12 +3,13 @@ package codegen import ( "encoding/json" "fmt" - . "github.com/cortezaproject/corteza-server/pkg/y7s" - "gopkg.in/yaml.v3" "os" "path" "strings" "text/template" + + . "github.com/cortezaproject/corteza-server/pkg/y7s" + "gopkg.in/yaml.v3" ) type ( @@ -164,6 +165,7 @@ func expandAutomationFunctionTypes(ff []*aFuncDefs, tt []*exprTypesDef) { for _, t := range p.Types { if ti[t.WorkflowType] == nil { fmt.Printf("%s/%s(): unknown type %q used for param %q\n", f.Prefix, fn.Name, t.WorkflowType, p.Name) + continue } if t.GoType == "" { @@ -182,6 +184,7 @@ func expandAutomationFunctionTypes(ff []*aFuncDefs, tt []*exprTypesDef) { for _, r := range fn.Results { if ti[r.WorkflowType] == nil { fmt.Printf("%s/%s(): unknown type %q used for result %q\n", f.Prefix, fn.Name, r.WorkflowType, r.Name) + continue } if r.GoType == "" { 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..77b686663 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" @@ -146,10 +145,6 @@ func mustExecWorkflow(ctx context.Context, t *testing.T, name string, p autTypes } } - if unw := errors.Unwrap(err); unw != nil { - err = unw - } - t.Fatalf("could not exec %q: %v", name, err) } 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 }