snapshot of https://github.com/cortezaproject/corteza.git (2024.9.x, 2026-09-18; blobs > 131072B omitted)
Some checks failed
SonarQube / SonarQube (push) Failing after 5m28s
E2e test on main branch push / server and client test (map[name:admin port:8080]) (push) Failing after 3m33s
Server and lib tests / Library test (js) (push) Failing after 1m36s
Server and lib tests / Library test (vue) (push) Successful in 2m24s
Server and lib tests / Web client test (admin) (push) Successful in 2m52s
Server and lib tests / Web client test (compose) (push) Successful in 3m23s
Server and lib tests / Web client test (discovery) (push) Successful in 2m49s
Server and lib tests / Web client test (one) (push) Successful in 2m52s
Server and lib tests / Web client test (privacy) (push) Successful in 2m44s
Server and lib tests / Web client test (reporter) (push) Successful in 2m56s
Server and lib tests / Web client test (workflow) (push) Successful in 3m0s
Server and lib tests / Server tests (push) Failing after 4m55s
E2e test on main branch push / notify-failure (push) Failing after 9s
Server and lib tests / Send matrix testing notification (push) Successful in 2s
Mark stale issues and pull requests / stale (push) Successful in 40s

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

View File

@@ -0,0 +1,86 @@
package compose
import (
"github.com/cortezaproject/corteza/server/codegen/schema"
)
attachment: {
features: {
labels: false
}
model: {
ident: "compose_attachment"
attributes: {
id: schema.IdField
namespace_id: {
ident: "namespaceID",
goType: "uint64",
storeIdent: "rel_namespace"
dal: { type: "Ref", refModelResType: "corteza::compose:namespace" }
}
owner_id: {
sortable: true,
goType: "uint64",
storeIdent: "rel_owner",
ident: "ownerID"
dal: { type: "Ref", refModelResType: "corteza::system:user" }
}
kind: {
sortable: true
dal: {}
}
url: {
dal: {}
}
preview_url: {
dal: {}
}
name: {
sortable: true
dal: {}
}
meta: {
goType: "types.AttachmentMeta"
dal: { type: "JSON", defaultEmptyObject: true }
omitSetter: true
omitGetter: true
}
created_at: schema.SortableTimestampNowField
updated_at: schema.SortableTimestampNilField
deleted_at: schema.SortableTimestampNilField
}
indexes: {
"primary": { attribute: "id" }
"namespace": { attribute: "namespace_id" },
}
}
filter: {
struct: {
kind: {}
namespace_id: { goType: "uint64", ident: "namespaceID" }
page_id: { goType: "uint64", ident: "pageID" }
record_id: { goType: "uint64", ident: "recordID" }
module_id: { goType: "uint64", ident: "moduleID" }
field_name: { }
}
byValue: ["kind", "namespace_id"]
}
envoy: {
omit: true
}
store: {
ident: "composeAttachment"
api: {
lookups: [
{ fields: ["id"] },
]
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

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

146
server/compose/chart.cue Normal file
View File

@@ -0,0 +1,146 @@
package compose
import (
"github.com/cortezaproject/corteza/server/codegen/schema"
)
chart: {
parents: [
{handle: "namespace"},
]
model: {
defaultSetter: true
ident: "compose_chart"
attributes: {
id: schema.IdField & {
envoy: {
yaml: {
identKeyEncode: "chartID"
}
}
}
handle: schema.HandleField
namespace_id: {
ident: "namespaceID",
goType: "uint64",
storeIdent: "rel_namespace"
dal: { type: "Ref", refModelResType: "corteza::compose:namespace" }
envoy: {
yaml: {
identKeyAlias: ["namespace", "namespace_id", "ns"]
}
}
}
name: {
sortable: true
dal: {}
}
config: {
goType: "types.ChartConfig"
dal: {}
omitSetter: true
omitGetter: true
envoy: {
yaml: {
customDecoder: true
customEncoder: true
}
}
}
created_at: schema.SortableTimestampNowField
updated_at: schema.SortableTimestampNilField
deleted_at: schema.SortableTimestampNilField
}
indexes: {
"primary": { attribute: "id" }
"namespace": { attribute: "namespace_id" },
"unique_handle": {
fields: [{ attribute: "handle", modifiers: ["LOWERCASE"] }, { attribute: "namespace_id" }]
predicate: "handle != '' AND deleted_at IS NULL"
}
}
indexes: {
"primary": { attribute: "id" }
}
}
filter: {
struct: {
chart_id: { goType: "[]uint64", ident: "chartID", storeIdent: "id" }
namespace_id: { goType: "uint64", ident: "namespaceID", storeIdent: "rel_namespace" }
handle: { goType: "string" }
name: { goType: "string" }
deleted: { goType: "filter.State", storeIdent: "deleted_at" }
}
query: ["handle", "name"]
byValue: ["handle", "chart_id", "namespace_id"]
byNilState: ["deleted"]
}
envoy: {
scoped: true
yaml: {
supportMappedInput: true
mappedField: "Handle"
identKeyAlias: ["charts", "chrt"]
}
store: {
extendedRefDecoder: true
}
}
rbac: {
operations: {
"read": {}
"update": {}
"delete": {}
}
}
locale: {
extended: true
keys: {
reportsYaxisLabel: {
path: ["yAxis", "label"]
customHandler: true
}
reportsMetricLabel: {
path: ["metrics", {part: "metricID", var: true}, "label"]
customHandler: true
}
reportsDimensionStepLabel: {
path: ["dimensions", {part: "dimensionID", var: true}, "meta", "steps", {part: "stepID", var: true}, "label"]
customHandler: true
}
}
}
store: {
ident: "composeChart"
api: {
lookups: [
{
fields: ["id"]
description: """
searches for compose chart by ID
It returns compose chart even if deleted
"""
}, {
fields: ["namespace_id", "handle"]
nullConstraint: ["deleted_at"]
description: """
searches for compose chart by handle (case-insensitive)
"""
},
]
}
}
}

View File

@@ -0,0 +1,25 @@
package commands
import (
"context"
"github.com/spf13/cobra"
)
type (
serviceInitializer interface {
InitServices(ctx context.Context) error
}
)
func Base(ctx context.Context, app serviceInitializer) (cmd *cobra.Command) {
cmd = &cobra.Command{
Use: "compose",
Aliases: []string{"cmp"},
}
cmd.AddCommand(
Records(ctx, app),
)
return
}

View File

@@ -0,0 +1,128 @@
package commands
import (
"context"
"fmt"
"time"
"github.com/brianvoe/gofakeit/v6"
"github.com/cortezaproject/corteza/server/compose/service"
"github.com/cortezaproject/corteza/server/compose/types"
"github.com/cortezaproject/corteza/server/pkg/auth"
"github.com/cortezaproject/corteza/server/pkg/cli"
"github.com/spf13/cobra"
)
func Records(ctx context.Context, app serviceInitializer) (cmd *cobra.Command) {
cmd = &cobra.Command{
Use: "records",
Aliases: []string{"rec", "record"},
}
cmd.AddCommand(RecordsSynthetic(ctx, app))
return
}
func RecordsSynthetic(ctx context.Context, app serviceInitializer) *cobra.Command {
var (
total uint
faker = gofakeit.NewCrypto()
namespace string
module string
synth = &cobra.Command{
Use: "synthetic",
Aliases: []string{"synth"},
PersistentPreRunE: func(cmd *cobra.Command, args []string) (err error) {
if err = app.InitServices(ctx); err != nil {
return
}
if err = service.DefaultModule.ReloadDALModels(ctx); err != nil {
return
}
return
},
}
gen = &cobra.Command{
Use: "generate",
Aliases: []string{"gen"},
Short: "Create synthetic records",
Args: cobra.MaximumNArgs(0),
Run: func(cmd *cobra.Command, args []string) {
if len(namespace) == 0 || len(module) == 0 {
cli.HandleError(fmt.Errorf("specifiy ID and handle for both, module and namespace"))
}
ctx = auth.SetIdentityToContext(ctx, auth.ServiceUser())
ns, mod, err := resolveModule(ctx, service.DefaultNamespace, service.DefaultModule, namespace, module)
cli.HandleError(err)
_ = faker
_ = ns
_ = mod
_ = err
cmd.Printf("Generating %d compose records (module: %s) ...", total, mod.Name)
bm := time.Now()
cli.HandleError(service.DefaultRecord.CreateSynthetic(ctx, faker, mod, total))
cmd.Printf("done in %s", time.Since(bm).Round(time.Millisecond))
cmd.Println()
},
}
rem = &cobra.Command{
Use: "remove",
Aliases: []string{"rm", "d", "delete", "del"},
Short: "Remove synthetic records",
Args: cobra.MaximumNArgs(0),
Run: func(cmd *cobra.Command, args []string) {
if len(namespace) == 0 || len(module) == 0 {
cli.HandleError(fmt.Errorf("specifiy ID and handle for both, module and namespace"))
}
ctx = auth.SetIdentityToContext(ctx, auth.ServiceUser())
ns, mod, err := resolveModule(ctx, service.DefaultNamespace, service.DefaultModule, namespace, module)
cli.HandleError(err)
_ = ns
_ = mod
_ = err
cmd.Printf("Removing all synthetic compose records (module: %s) ...", mod.Name)
bm := time.Now()
cli.HandleError(service.DefaultRecord.RemoveSynthetic(ctx, mod))
cmd.Printf("done in %s", time.Since(bm).Round(time.Millisecond))
cmd.Println()
},
}
)
for _, cmd := range []*cobra.Command{gen, rem} {
cmd.Flags().StringVarP(&namespace, "namespace", "n", "", "namespace IS or handle for recode generation")
cmd.Flags().StringVarP(&module, "module", "m", "", "module IS or handle for recode generation")
}
gen.Flags().UintVarP(&total, "total", "t", 1, "Number of synthetic records generated")
synth.AddCommand(gen, rem)
return synth
}
func resolveModule(ctx context.Context, nsSvc service.NamespaceService, modSvc service.ModuleService, nsIdent, modIdent string) (ns *types.Namespace, mod *types.Module, err error) {
if ns, err = nsSvc.FindByAny(ctx, nsIdent); err != nil {
return
}
mod, err = modSvc.FindByAny(ctx, ns.ID, modIdent)
return
}

View File

@@ -0,0 +1,30 @@
package compose
import (
"github.com/cortezaproject/corteza/server/codegen/schema"
)
component: schema.#component & {
handle: "compose"
resources: {
"attachment": attachment
"chart": chart
"module": module
"module-field": moduleField
"namespace": namespace
"page": page
"page-layout": pageLayout
"record": record
"record-revision": record_revision
}
rbac: operations: {
"settings.read": description: "Read settings"
"settings.manage": description: "Manage settings"
"namespace.create": description: "Create namespace"
"namespaces.search": description: "List, search or filter namespaces"
"resource-translations.manage": description: "List, search, create, or update resource translations"
}
}

View File

@@ -0,0 +1,630 @@
package dalutils
import (
"context"
"fmt"
"math"
"github.com/cortezaproject/corteza/server/compose/types"
"github.com/cortezaproject/corteza/server/pkg/dal"
"github.com/cortezaproject/corteza/server/pkg/filter"
"github.com/spf13/cast"
)
type (
creator interface {
Create(ctx context.Context, m dal.ModelRef, operations dal.OperationSet, vv ...dal.ValueGetter) error
}
updater interface {
Update(ctx context.Context, m dal.ModelRef, operations dal.OperationSet, rr ...dal.ValueGetter) (err error)
}
searcher interface {
Search(ctx context.Context, m dal.ModelRef, operations dal.OperationSet, f filter.Filter) (dal.Iterator, error)
}
lookuper interface {
Lookup(ctx context.Context, m dal.ModelRef, operations dal.OperationSet, lookup dal.ValueGetter, dst dal.ValueSetter) (err error)
}
deleter interface {
Delete(ctx context.Context, m dal.ModelRef, operations dal.OperationSet, pkv ...dal.ValueGetter) (err error)
}
counter interface {
Count(ctx context.Context, m dal.ModelRef, operations dal.OperationSet, f filter.Filter) (uint, error)
}
)
// ComposeRecordsList iterates over results and collects all available records
func ComposeRecordsList(ctx context.Context, s searcher, mod *types.Module, filter types.RecordFilter) (set types.RecordSet, outFilter types.RecordFilter, err error) {
iter, err := prepIterator(ctx, s, mod, filter)
if err != nil {
return
}
set, _, outFilter, err = drainIterator(ctx, iter, mod, filter)
return
}
func ComposeRecordsListN(ctx context.Context, s searcher, mod *types.Module, filter types.RecordFilter) (set types.RecordSet, summaries map[string]types.RecordSummary, outFilter types.RecordFilter, err error) {
iter, err := prepIterator(ctx, s, mod, filter)
if err != nil {
return
}
set, summaries, outFilter, err = drainIterator(ctx, iter, mod, filter)
return
}
func ComposeRecordsIterator(ctx context.Context, s searcher, mod *types.Module, filter types.RecordFilter) (iter dal.Iterator, outFilter types.RecordFilter, err error) {
iter, err = prepIterator(ctx, s, mod, filter)
if err != nil {
return
}
outFilter = filter
outFilter.Paging = *filter.Paging.Clone()
return
}
func ComposeRecordsFind(ctx context.Context, l lookuper, mod *types.Module, recordID uint64) (out *types.Record, err error) {
out = prepareRecordTarget(mod)
err = l.Lookup(ctx, mod.ModelRef(), recLookupOperations(mod), dal.PKValues{"id": recordID}, out)
if err != nil {
return
}
return
}
func ComposeRecordsCount(ctx context.Context, c counter, mod *types.Module, filter types.RecordFilter) (cnt uint, err error) {
constraints := map[string][]interface{}{
"namespaceID": {mod.NamespaceID},
}
if filter.ModuleID != 0 {
constraints["moduleID"] = []interface{}{filter.ModuleID}
}
dalFilter := filter.ToConstraintedFilter(constraints)
return c.Count(ctx, mod.ModelRef(), recLookupOperations(mod), dalFilter)
}
func ComposeRecordCreate(ctx context.Context, c creator, mod *types.Module, records ...*types.Record) (err error) {
return c.Create(ctx, mod.ModelRef(), recCreateOperations(mod), recToGetters(records...)...)
}
func ComposeRecordUpdate(ctx context.Context, u updater, mod *types.Module, records ...*types.Record) (err error) {
return u.Update(ctx, mod.ModelRef(), recUpdateOperations(mod), recToGetters(records...)...)
}
func ComposeRecordSoftDelete(ctx context.Context, u updater, mod *types.Module, records ...*types.Record) (err error) {
return u.Update(ctx, mod.ModelRef(), recUpdateOperations(mod), recToGetters(records...)...)
}
func ComposeRecordUndelete(ctx context.Context, u updater, mod *types.Module, records ...*types.Record) (err error) {
return u.Update(ctx, mod.ModelRef(), recUpdateOperations(mod), recToGetters(records...)...)
}
func ComposeRecordDelete(ctx context.Context, d deleter, mod *types.Module, records ...*types.Record) (err error) {
return d.Delete(ctx, mod.ModelRef(), recDeleteOperations(mod), recToGetters(records...)...)
}
func WalkIterator(ctx context.Context, iter dal.Iterator, mod *types.Module, f func(r *types.Record) error) (err error) {
for iter.Next(ctx) {
r := prepareRecordTarget(mod)
if err = iter.Scan(r); err != nil {
return
}
if err = f(r); err != nil {
return
}
}
return iter.Err()
}
// // // // // // // // // // // // // // // // // // // // // // // // //
// Utils
func prepFilter(filter types.RecordFilter, mod *types.Module) filter.Filter {
return filter.ToConstraintedFilter(mod.Config.DAL.Constraints)
}
func prepIterator(ctx context.Context, dal searcher, mod *types.Module, filter types.RecordFilter) (iter dal.Iterator, err error) {
dalFilter := prepFilter(filter, mod)
iter, err = dal.Search(ctx, mod.ModelRef(), recSearchOperations(mod, filter), dalFilter)
return
}
// drains iterator and collects all records
//
// Collection of records is done with respect to check function and limit constraint on record filter
// For any other filter constraint we assume that underlying DAL took care of it
func drainIterator(ctx context.Context, iter dal.Iterator, mod *types.Module, f types.RecordFilter) (set types.RecordSet, summaries map[string]types.RecordSummary, outFilter types.RecordFilter, err error) {
// close iterator after we've drained it
defer iter.Close()
if f.PageCursor != nil {
if f.IncPageNavigation || f.IncTotal {
err = fmt.Errorf("not allowed to fetch page navigation or total item count with page cursor")
return
}
}
var (
ok bool
fetched uint
filtered uint
lastRecord *types.Record
)
// Get the requested number of record
if f.Limit > 0 {
set = make(types.RecordSet, 0, f.Limit)
} else {
set = make(types.RecordSet, 0, 1000)
}
for f.Limit == 0 || uint(len(set)) < f.Limit {
var firstRecord *types.Record
// reset counters every drain
fetched = 0
filtered = 0
add := make(types.RecordSet, 0, 12)
err = WalkIterator(ctx, iter, mod, func(r *types.Record) error {
lastRecord = r
if firstRecord == nil {
firstRecord = r
}
// check fetched record
if f.Check != nil {
if ok, err = f.Check(r); err != nil {
return err
} else if !ok {
filtered++
return nil
}
}
fetched++
add = append(add, r)
return err
})
// if an error occurred inside Next()/WalkIterator,
// we need to stop draining
if err != nil {
return
}
// If it's reverse, we need to add extra fetches to the start
if f.PageCursor != nil && f.PageCursor.ROrder {
set = append(add, set...)
} else {
set = append(set, add...)
}
total := fetched + filtered
if total == 0 || f.Limit == 0 {
// do not re-fetch if:
// 1) nothing was fetch in the previous run
// 2) there was no limit (everything was fetched)
// 3) there are less total (fetched and filtered) items then value of limit
break
}
// Fetch more records
setLen := uint(len(set))
if total > 0 && setLen < f.Limit {
fetchMore := f.Limit - setLen
var crsrRec *types.Record
// request more items
if f.PageCursor == nil || !f.PageCursor.ROrder {
crsrRec = lastRecord
} else {
crsrRec = firstRecord
}
if err = iter.More(fetchMore, crsrRec); err != nil {
return
}
}
}
// Get the page nav/total/next-prev cursors
nav, auxSm, err := generatePageNavigation(ctx, iter, mod, f, set)
if err != nil {
return
}
summaries = auxSm
// Make out filter
outFilter = f
outFilter.Paging = nav.Paging
outFilter.Sorting = nav.Sorting
return
}
// generatePageNavigation generates page navigation for a given record set using an iterator and filter limit.
// If the limit is not defined, the page navigation will consist of only one page without a cursor.
// If the limit is defined and is greater than the total number of records in the set,
// the page navigation will consist of only one page without a cursor.
// If the limit is defined and is less than the total number of records in the set,
// the page navigation will have multiple pages with cursor(s) based on the total number of records and the provided limit.
// @todo revisit and clean up this function properly
func generatePageNavigation(ctx context.Context, iter dal.Iterator, mod *types.Module, p types.RecordFilter, set types.RecordSet) (out types.RecordFilter, summaries map[string]types.RecordSummary, err error) {
const (
howMuchMore = 1000
)
var (
ok bool
total = uint(len(set))
setLen = len(set)
counter = 0
first *types.Record
last *types.Record
page filter.Page
pageNavigation = []*filter.Page{
{
Page: 1,
Count: 0,
Cursor: nil,
},
}
// generatePage generates pageNavigation for given record set
generatePage = func(last *types.Record) (err error) {
if !p.IncPageNavigation || p.Limit == 0 || len(pageNavigation) == 0 {
return
}
lastNavPageNo := len(pageNavigation) - 1
nextPage, err := iter.ForwardCursor(last)
if err != nil {
return
}
if total < p.Limit {
pageNavigation[lastNavPageNo].Count = total
}
// prepare page
if total != 0 && (total%p.Limit) == 0 {
pageNavigation[lastNavPageNo].Count = p.Limit
page = filter.Page{
Page: uint(len(pageNavigation) + 1),
Count: p.Limit,
Cursor: nextPage,
}
}
expectedItemCountUpToPage := uint(lastNavPageNo+1) * p.Limit
if p.Limit == 1 {
expectedItemCountUpToPage = uint(lastNavPageNo) * p.Limit
}
if expectedItemCountUpToPage < total {
// push page when limit is matched with the previous page item size
if pageNavigation[lastNavPageNo].Count == p.Limit {
pageNavigation = append(pageNavigation, &filter.Page{
Page: page.Page,
Count: total % p.Limit,
Cursor: page.Cursor, // prev cursor
})
}
}
return
}
recordChecker = func(i dal.Iterator) (ok bool, err error) {
if p.Check == nil {
return true, err
}
rc := prepareRecordTarget(mod)
err = i.Scan(rc)
if err != nil {
return
}
return p.Check(rc)
}
)
if len(p.Summaries) > 0 {
summaries = make(map[string]types.RecordSummary, len(p.Summaries))
}
if setLen == 0 {
return
}
existing := make(map[any]struct{}, 24)
looped := false
procSummary := func(r *types.Record) (err error) {
for _, smDef := range p.Summaries {
// Get record value
var vv []any
vv, err = r.GetValues(smDef.Field)
if err != nil {
return
}
bit := summaries[fmt.Sprintf("%s %s", smDef.Name, smDef.Field)]
// This will be constant so we're good
bit.Name = smDef.Name
// Skip empty
if len(vv) == 0 {
bit.EmptyCount++
summaries[fmt.Sprintf("%s %s", smDef.Name, smDef.Field)] = bit
continue
}
bit.NotEmptyCount++
for _, v := range vv {
bit.Count++
if _, ok := existing[v]; !ok {
existing[v] = struct{}{}
bit.UniqueCount++
}
}
switch smDef.Name {
case "min":
for _, v := range vv {
if !looped {
bit.Min = cast.ToFloat64(v)
} else {
bit.Min = math.Min(bit.Min, cast.ToFloat64(v))
}
}
case "max":
for _, v := range vv {
if !looped {
bit.Max = cast.ToFloat64(v)
} else {
bit.Max = math.Max(bit.Max, cast.ToFloat64(v))
}
}
case "avg":
for _, v := range vv {
bit.Sum += cast.ToFloat64(v)
}
case "sum":
for _, v := range vv {
bit.Sum += cast.ToFloat64(v)
}
case "earliest":
for _, v := range vv {
if bit.Earliest.IsZero() {
bit.Earliest = cast.ToTime(v)
} else {
aux := cast.ToTime(v)
if !aux.IsZero() && aux.Before(bit.Earliest) {
bit.Earliest = aux
}
}
}
case "latest":
for _, v := range vv {
if bit.Latest.IsZero() {
bit.Latest = cast.ToTime(v)
} else {
aux := cast.ToTime(v)
if !aux.IsZero() && aux.After(bit.Latest) {
bit.Latest = aux
}
}
}
}
looped = true
summaries[fmt.Sprintf("%s %s", smDef.Name, smDef.Field)] = bit
}
return
}
// Firstly sort out the current things
if len(p.Summaries) > 0 {
for _, r := range set {
err = procSummary(r)
if err != nil {
return
}
}
}
first = set[0]
last = set[setLen-1]
// Limit
out.Limit = p.Limit
// Sorting
out.Sort = dal.IteratorSorting(iter)
// No need to generate prev/next cursor
// if limit is not defined and set is empty
if p.Limit > 0 && len(set) > 0 {
// PrevPage
if p.PageCursor != nil {
out.PrevPage, err = dal.PreLoadCursor(ctx, iter, 100, true, first, recordChecker)
if err != nil {
return
}
}
// NextPage
out.NextPage, err = dal.PreLoadCursor(ctx, iter, 100, false, last, recordChecker)
if err != nil {
return
}
}
if p.IncTotal || p.IncPageNavigation || len(p.Summaries) > 0 {
// For the first page nav
err = generatePage(last)
if err != nil {
return
}
for counter == 0 || counter < howMuchMore {
counter++
interLoop := 0
if err = iter.More(howMuchMore, last); err != nil {
return
}
err = WalkIterator(ctx, iter, mod, func(rec *types.Record) error {
// check fetched record
if p.Check != nil {
if ok, err = p.Check(rec); err != nil {
return err
} else if !ok {
return nil
}
}
err = procSummary(rec)
if err != nil {
return err
}
interLoop++
total++
last = rec
return generatePage(rec)
})
if err != nil {
return
}
if interLoop < howMuchMore {
break
}
}
}
// Total
if p.IncTotal {
out.Total = total
}
// Page navigation
if p.IncPageNavigation {
// Ensure that the last page count is correct if it's not equal to the limit.
lastPageCount := pageNavigation[len(pageNavigation)-1].Count
if lastPageCount > 0 && lastPageCount != p.Limit && lastPageCount != total%p.Limit {
pageNavigation[len(pageNavigation)-1].Count = total % p.Limit
}
if p.Limit == 1 {
pageNavigation = pageNavigation[:len(pageNavigation)-1]
}
out.PageNavigation = pageNavigation
}
// Do averages
if len(p.Summaries) > 0 {
for n, s := range summaries {
if s.Name != "avg" {
continue
}
s.Avg = s.Sum / float64(s.Count)
summaries[n] = s
}
}
return
}
func prepareRecordTarget(module *types.Module) *types.Record {
// so we can avoid some code later involving (non)partitioned modules :seenoevil:
r := &types.Record{
ModuleID: module.ID,
NamespaceID: module.NamespaceID,
Values: make(types.RecordValueSet, 0, len(module.Fields)),
}
r.SetModule(module)
return r
}
func recToGetters(rr ...*types.Record) (out []dal.ValueGetter) {
out = make([]dal.ValueGetter, len(rr))
for i := range rr {
out[i] = rr[i]
}
return
}
func recCreateOperations(m *types.Module) (out dal.OperationSet) {
return dal.CreateOperations()
}
func recUpdateOperations(m *types.Module) (out dal.OperationSet) {
return dal.UpdateOperations()
}
func recDeleteOperations(m *types.Module) (out dal.OperationSet) {
return dal.DeleteOperations()
}
func recFilterOperations(f types.RecordFilter) (out dal.OperationSet) {
if f.PageCursor != nil {
out = append(out, dal.Paging)
}
if f.IncPageNavigation {
out = append(out, dal.Paging)
}
if f.Sort != nil {
out = append(out, dal.Sorting)
}
return
}
func recSearchOperations(m *types.Module, f types.RecordFilter) (out dal.OperationSet) {
return dal.SearchOperations().
Union(recFilterOperations(f))
}
func recLookupOperations(m *types.Module) (out dal.OperationSet) {
return dal.LookupOperations()
}

View File

@@ -0,0 +1,208 @@
package envoy
import (
"context"
"encoding/csv"
"fmt"
"io"
"strings"
"time"
"github.com/cortezaproject/corteza/server/pkg/envoyx"
"github.com/cortezaproject/corteza/server/pkg/envoyx/datasource"
"github.com/spf13/cast"
)
type (
CsvEncoder struct{}
)
func (e CsvEncoder) Encode(ctx context.Context, p envoyx.EncodeParams, rt string, nodes envoyx.NodeSet, tt envoyx.Traverser) (err error) {
w, err := e.getWriter(p)
if err != nil {
return
}
cw := csv.NewWriter(w)
switch rt {
case ComposeRecordDatasourceAuxType:
_, err = e.encodeRecordDatasources(ctx, cw, p, nodes, tt)
if err != nil {
return
}
}
cw.Flush()
return
}
func (e CsvEncoder) encodeRecordDatasources(ctx context.Context, writer *csv.Writer, p envoyx.EncodeParams, nodes envoyx.NodeSet, tt envoyx.Traverser) (out any, err error) {
for _, n := range nodes {
_, err = e.encodeRecordDatasource(ctx, writer, p, n, tt)
if err != nil {
return
}
}
return
}
func (e CsvEncoder) encodeRecordDatasource(ctx context.Context, writer *csv.Writer, p envoyx.EncodeParams, node *envoyx.Node, tt envoyx.Traverser) (_ any, err error) {
rds := node.Datasource.(*RecordDatasource)
resolved := map[string]string{}
var loc *time.Location
loc, err = resolveTimezone(p)
if err != nil {
return
}
header := make([]string, 0, 4)
for _, m := range p.FieldMapping {
if strings.ToLower(m.Field) != "id" {
header = append(header, m.Field)
}
}
header = append([]string{"ID"}, header...)
mvDelimiter := ";"
wrapBrackets := false
if p.Params["multiValueDelimiter"] == nil {
wrapBrackets = false
mvDelimiter = ";"
}
switch cast.ToString(p.Params["multiValueDelimiter"]) {
case ",":
mvDelimiter = ","
wrapBrackets = false
case ";":
mvDelimiter = ";"
wrapBrackets = false
case "|":
mvDelimiter = "|"
wrapBrackets = false
case "[,]":
mvDelimiter = ","
wrapBrackets = true
case "[;]":
mvDelimiter = ";"
wrapBrackets = true
case "[|]":
mvDelimiter = "|"
wrapBrackets = true
}
getValue := func(cache datasource.RawRecord, h string) string {
v := cache[h]
if len(v.Values) == 0 {
return ""
}
// Encode as mf if the OG field is multi value OR we're on the resolved record column
src, ok := resolved[h]
isDt := rds.datetimeFields[h] || (ok && rds.datetimeFields[src])
if !rds.multivalues[h] && !(ok && rds.multivalues[src]) {
vv := v.Values[0]
if isDt {
vv = formatInTimezone(vv, loc)
}
return vv
}
auxv := make([]string, 0, len(v.Values))
for _, vv := range v.Values {
if isDt {
vv = formatInTimezone(vv, loc)
}
if strings.Contains(vv, mvDelimiter) {
auxv = append(auxv, fmt.Sprintf("\"%s\"", vv))
} else {
auxv = append(auxv, vv)
}
}
out := strings.Join(auxv, mvDelimiter)
if wrapBrackets {
out = fmt.Sprintf("[%s]", out)
}
return out
}
// Pre-build resolved field set from datasource so header is correct even
// when the first row has no value for a resolved field.
resolvedFieldSet := make(map[string]bool)
for _, f := range rds.ResolvedFields() {
resolvedFieldSet[f] = true
}
row := make([]string, 0, 4)
var (
more bool
hWritten bool
)
for {
cache := make(datasource.RawRecord)
_, more, err = rds.Next(ctx, cache)
if err != nil || !more {
return
}
if len(header) == 0 {
for k := range cache {
header = append(header, k)
}
}
if !hWritten {
hWritten = true
// Splice in "X ID" columns after each resolved field
newHeader := make([]string, 0, len(header)+len(resolvedFieldSet))
for _, h := range header {
newHeader = append(newHeader, h)
if resolvedFieldSet[h] {
idCol := fmt.Sprintf("%s ID", h)
newHeader = append(newHeader, idCol)
resolved[idCol] = h
}
}
header = newHeader
err = writer.Write(header)
if err != nil {
return
}
}
for _, h := range header {
row = append(row, getValue(cache, h))
}
err = writer.Write(row)
if err != nil {
return
}
row = nil
}
}
func (e CsvEncoder) getWriter(p envoyx.EncodeParams) (out io.Writer, err error) {
aux, ok := p.Params[paramsKeyWriter]
if ok {
out, ok = aux.(io.Writer)
if ok {
return
}
}
err = fmt.Errorf("csv encoder expects a writer conforming to io.Writer interface")
return
}

View File

@@ -0,0 +1,153 @@
package envoy
import (
"context"
"encoding/json"
"fmt"
"io"
"strings"
"time"
"github.com/cortezaproject/corteza/server/pkg/envoyx"
"github.com/cortezaproject/corteza/server/pkg/envoyx/datasource"
"github.com/cortezaproject/corteza/server/pkg/j7s"
)
type (
JsonlEncoder struct{}
)
func (e JsonlEncoder) Encode(ctx context.Context, p envoyx.EncodeParams, rt string, nodes envoyx.NodeSet, tt envoyx.Traverser) (err error) {
w, err := e.getWriter(p)
if err != nil {
return
}
jw := json.NewEncoder(w)
switch rt {
case ComposeRecordDatasourceAuxType:
_, err = e.encodeRecordDatasources(ctx, jw, p, nodes, tt)
if err != nil {
return
}
}
return
}
func (e JsonlEncoder) encodeRecordDatasources(ctx context.Context, writer *json.Encoder, p envoyx.EncodeParams, nodes envoyx.NodeSet, tt envoyx.Traverser) (out any, err error) {
for _, n := range nodes {
_, err = e.encodeRecordDatasource(ctx, writer, p, n, tt)
if err != nil {
return
}
}
return
}
func (e JsonlEncoder) encodeRecordDatasource(ctx context.Context, writer *json.Encoder, p envoyx.EncodeParams, node *envoyx.Node, tt envoyx.Traverser) (_ any, err error) {
rds := node.Datasource.(*RecordDatasource)
var loc *time.Location
loc, err = resolveTimezone(p)
if err != nil {
return
}
header := make([]string, 0, 4)
hasID := false
for _, m := range p.FieldMapping {
header = append(header, m.Field)
hasID = hasID || strings.ToLower(m.Field) == "id"
}
if !hasID {
header = append([]string{"ID"}, header...)
}
var more bool
for {
out := make(datasource.RawRecord)
_, more, err = rds.Next(ctx, out)
if err != nil || !more {
break
}
if len(header) == 0 {
for k := range out {
header = append(header, k)
}
}
row, _ := j7s.MakeMap()
for _, h := range header {
proc := func(h string) {
v := out[h]
if len(v.Values) == 0 {
return
}
srcKey := strings.TrimSuffix(h, " value")
isDt := rds.datetimeFields[h] || rds.datetimeFields[srcKey]
if !rds.multivalues[h] {
vv := v.Values[0]
if isDt {
vv = formatInTimezone(vv, loc)
}
row, err = j7s.AddMap(row, h, vv)
if err != nil {
return
}
} else {
auxv, _ := j7s.MakeSeq()
for _, vv := range v.Values {
if isDt {
vv = formatInTimezone(vv, loc)
}
auxv, err = j7s.AddSeq(auxv, vv)
if err != nil {
return
}
}
row, err = j7s.AddMap(row, h, auxv)
if err != nil {
return
}
}
}
proc(h)
if _, ok := out[fmt.Sprintf("%s value", h)]; ok {
proc(fmt.Sprintf("%s value", h))
}
}
err = writer.Encode(row)
if err != nil {
return
}
}
return
}
func (e JsonlEncoder) getWriter(p envoyx.EncodeParams) (out io.Writer, err error) {
aux, ok := p.Params[paramsKeyWriter]
if ok {
out, ok = aux.(io.Writer)
if ok {
return
}
}
err = fmt.Errorf("jsonl encoder expects a writer conforming to io.Writer interface")
return
}

View File

@@ -0,0 +1,653 @@
package envoy
import (
"context"
"fmt"
"strconv"
"strings"
"github.com/cortezaproject/corteza/server/compose/dalutils"
"github.com/cortezaproject/corteza/server/compose/types"
"github.com/cortezaproject/corteza/server/pkg/dal"
"github.com/cortezaproject/corteza/server/pkg/envoyx"
"github.com/cortezaproject/corteza/server/pkg/envoyx/datasource"
"github.com/cortezaproject/corteza/server/pkg/errors"
"github.com/cortezaproject/corteza/server/pkg/filter"
"github.com/cortezaproject/corteza/server/pkg/logger"
"github.com/cortezaproject/corteza/server/store"
systemTypes "github.com/cortezaproject/corteza/server/system/types"
"github.com/modern-go/reflect2"
"github.com/spf13/cast"
"go.uber.org/zap"
)
type (
// RecordDatasource provides a mechanism for you to access large
// record datasets optimally
RecordDatasource struct {
Mapping envoyx.DatasourceMapping
Provider envoyx.Provider
multivalues map[string]bool
datetimeFields map[string]bool
CheckExisting func(ctx context.Context, ref ...[]string) ([]uint64, error)
currentIndex int
// Index to map from ref to ID
// @todo we might need to flush these to the disc in case a huge dataset is passed in
refToID map[string]uint64
// @todo might be worth putting both into one map; not sure how much space we'd save up
existingIDs map[uint64]bool
}
// iteratorProvider is a wrapper around the dal.Iterator to conform to the
// envoy.Provider interface
iteratorProvider struct {
iter dal.Iterator
// Items related to ref resolution
// @todo can be removed when reworked
resolveRefs bool
includeRefID bool
relMods map[string]refModWrap
dal dal.FullService
// Access control for field-level permissions
ac recordValueAccessController
mod *types.Module
readableFields map[string]bool
rows []datasource.RawRecord
buffIndex int
done bool
// User ref resolution
store store.Storer
userFields map[string]bool
}
refModWrap struct {
modLvl1 *types.Module
labelLvl1 string
modLvl2 *types.Module
labelLvl2 string
}
// recordValueAccessController interface for checking field-level read permissions
recordValueAccessController interface {
CanReadRecordValueOnModuleField(context.Context, *types.ModuleField) bool
}
)
const (
bufferPullChunkSize = int(100)
)
func mkIteratorProvider(ctx context.Context, ac recordValueAccessController, s store.Storer, dl dal.FullService, iter dal.Iterator, mod *types.Module, resolveRefs bool, includeRefID bool) (out *iteratorProvider, err error) {
out = &iteratorProvider{
iter: iter,
dal: dl,
resolveRefs: resolveRefs,
includeRefID: includeRefID,
ac: ac,
mod: mod,
}
// Pre-compute readable fields based on access control permissions
// This avoids checking permissions on every record iteration
if !reflect2.IsNil(ac) {
out.readableFields = make(map[string]bool)
for _, f := range mod.Fields {
// Ensure NamespaceID is set on the field (it may not be loaded from store)
// RBAC checks require the full resource path including namespace
if f.NamespaceID == 0 {
f.NamespaceID = mod.NamespaceID
}
out.readableFields[f.Name] = ac.CanReadRecordValueOnModuleField(ctx, f)
}
}
refMods := make(map[string]refModWrap)
userFields := make(map[string]bool)
for _, name := range []string{"createdBy", "updatedBy", "ownedBy", "deletedBy"} {
userFields[name] = true
}
for _, f := range mod.Fields {
switch f.Kind {
case "Record":
refMods[f.Name], err = mkRecordRefWrap(ctx, s, f)
if err != nil {
return
}
case "User":
userFields[f.Name] = true
}
}
out.relMods = refMods
out.store = s
out.userFields = userFields
return
}
// ResolvedFields returns field names that will have a companion "X ID" column when includeRefID is enabled.
func (ip *iteratorProvider) ResolvedFields() []string {
if !ip.resolveRefs || !ip.includeRefID {
return nil
}
out := make([]string, 0, len(ip.userFields)+len(ip.relMods))
for f := range ip.userFields {
out = append(out, f)
}
for f := range ip.relMods {
out = append(out, f)
}
return out
}
// ResolvedFields returns field names that will have a companion "X value" column.
func (rd *RecordDatasource) ResolvedFields() []string {
if ip, ok := rd.Provider.(*iteratorProvider); ok {
return ip.ResolvedFields()
}
return nil
}
func (rd *RecordDatasource) SetProvider(s envoyx.Provider) bool {
if rd.Mapping.SourceIdent != s.Ident() {
return false
}
rd.Provider = s
return true
}
func (rd *RecordDatasource) Next(ctx context.Context, out datasource.RawRecord) (ident []string, more bool, err error) {
rowCache := make(datasource.RawRecord)
more, err = rd.Provider.Next(ctx, rowCache)
if err != nil || !more {
return
}
rd.applyMapping(rowCache, out)
if len(rd.Mapping.KeyField) == 0 {
ident = append(ident, strconv.FormatInt(int64(rd.currentIndex), 10))
} else {
for _, k := range rd.Mapping.KeyField {
ident = append(ident, strings.Join(rowCache[k].Values, ","))
}
}
rd.currentIndex++
return
}
func (rd *iteratorProvider) SetConfigs(map[string]any) error {
return nil
}
func (rd *RecordDatasource) Reset(ctx context.Context) (err error) {
rd.currentIndex = 0
return rd.Provider.Reset(ctx)
}
func (rd *RecordDatasource) applyMapping(in, out datasource.RawRecord) {
if len(rd.Mapping.Mapping.Map) == 0 {
if !rd.Mapping.Defaultable {
return
}
for k, v := range in {
out[k] = v
}
return
}
if rd.Mapping.Defaultable {
rd.applyMappingWithDefaults(in, out)
} else {
rd.applyMappingWoDefaults(in, out)
}
}
func (rd *RecordDatasource) applyMappingWithDefaults(in, out datasource.RawRecord) {
maps := make(map[string]envoyx.MapEntry)
for k, v := range rd.Mapping.Mapping.Map {
maps[k] = v
}
for k, v := range in {
if m, ok := maps[k]; ok {
if m.Skip {
continue
}
out[m.Field] = v
} else {
out[k] = v
}
}
}
func (rd *RecordDatasource) applyMappingWoDefaults(in, out datasource.RawRecord) {
for _, m := range rd.Mapping.Mapping.Map {
if m.Skip {
continue
}
out[m.Field] = in[m.Column]
}
}
func (rd *RecordDatasource) ResolveRef(ref ...any) (out uint64, err error) {
idents, err := cast.ToStringSliceE(ref)
if err != nil {
return
}
for i, ident := range idents {
idents[i] = strings.Replace(ident, "-", "_", -1)
}
out = rd.refToID[strings.Join(idents, "-")]
return
}
func (rd *RecordDatasource) ResolveRefS(ref ...string) (out uint64, err error) {
aux := make([]any, len(ref))
for i, r := range ref {
aux[i] = r
}
return rd.ResolveRef(aux...)
}
// @todo this should be replaced by some smarter structure
func (rd *RecordDatasource) AddRef(id uint64, idents ...string) {
for i, ident := range idents {
idents[i] = strings.Replace(ident, "-", "_", -1)
}
rd.refToID[strings.Join(idents, "-")] = id
}
func (ip *iteratorProvider) Next(ctx context.Context, out datasource.RawRecord) (more bool, err error) {
if ip.resolveRefs {
return ip.nextResolved(ctx, out)
}
return ip.next(ctx, out)
}
func (ip *iteratorProvider) next(ctx context.Context, out datasource.RawRecord) (more bool, err error) {
rowCache := make(datasource.RawRecord)
if !ip.iter.Next(ctx) {
return false, ip.iter.Err()
}
err = ip.iter.Scan(rowCache)
if err != nil {
return
}
// Apply field-level access control filtering
ip.filterUnreadableFields(rowCache)
for k, v := range rowCache {
out[k] = v
}
return true, nil
}
func (ip *iteratorProvider) nextResolved(ctx context.Context, out datasource.RawRecord) (more bool, err error) {
if ip.done && ip.buffIndex >= len(ip.rows) {
return false, nil
}
if ip.buffIndex == len(ip.rows) {
// pull chunk
ip.rows = make([]datasource.RawRecord, 0)
ip.buffIndex = 0
for i := 0; i < bufferPullChunkSize; i++ {
rowCache := make(datasource.RawRecord)
if !ip.iter.Next(ctx) {
ip.done = true
err := ip.iter.Err()
if err != nil {
return false, err
}
break
}
err = ip.iter.Scan(rowCache)
if err != nil {
return
}
ip.rows = append(ip.rows, rowCache)
}
if len(ip.rows) == 0 {
return false, nil
}
// resolve stuff
err = ip.resolveReferences(ctx, ip.dal)
if err != nil {
return
}
err = ip.resolveUsers(ctx, ip.store)
if err != nil {
return
}
}
rowCache := ip.rows[ip.buffIndex]
ip.buffIndex++
// Apply field-level access control filtering
ip.filterUnreadableFields(rowCache)
for k, v := range rowCache {
out[k] = v
}
return true, nil
}
func (ip *iteratorProvider) resolveReferences(ctx context.Context, ds dal.FullService) (err error) {
// @todo I'll need to chunk these up for to reduce DB query count...
for i, cacheRecord := range ip.rows {
for refField, refWrap := range ip.relMods {
value := cacheRecord[refField]
if len(value.Values) == 0 {
continue
}
aux := []string{}
for _, v := range value.Values {
aux = append(aux, fmt.Sprintf("recordID=%s", v))
}
var relRecords types.RecordSet
resLab := refWrap.labelLvl1
qq := fmt.Sprintf("(%s)", strings.Join(aux, " OR "))
relRecords, _, err = dalutils.ComposeRecordsList(ctx, ds, refWrap.modLvl1, types.RecordFilter{
Query: qq,
Paging: filter.Paging{
Limit: uint(len(aux)),
},
})
if err != nil {
return err
}
// lvl 2 nesting; current max lvl
if refWrap.modLvl2 != nil {
resLab = refWrap.labelLvl2
// Iterate related records and collect lvl 2 identifiers
aux := []string{}
for _, rec := range relRecords {
for _, v := range rec.Values.FilterByName(refWrap.labelLvl1) {
aux = append(aux, fmt.Sprintf("recordID=%s", v.Value))
}
}
qq := fmt.Sprintf("(%s)", strings.Join(aux, " OR "))
relRecords, _, err = dalutils.ComposeRecordsList(ctx, ds, refWrap.modLvl2, types.RecordFilter{
Query: qq,
Paging: filter.Paging{
Limit: uint(len(aux)),
},
})
if err != nil {
return err
}
}
// Build a map from recordID to resolved label for ordered assignment
labelByID := make(map[string]string, len(relRecords))
for _, rec := range relRecords {
if rec.Values == nil {
continue
}
v := rec.Values.Get(resLab, 0)
if v == nil {
continue
}
labelByID[strconv.FormatUint(rec.ID, 10)] = v.Value
}
for j, origID := range value.Values {
label, ok := labelByID[origID]
if !ok {
continue
}
if ip.includeRefID {
cacheRecord.SetValue(fmt.Sprintf("%s ID", refField), uint(j), origID)
}
cacheRecord.SetValue(refField, uint(j), label)
}
ip.rows[i] = cacheRecord
}
}
return
}
// @todo consider omitting these from the interface since they're not always needed
func (ip *iteratorProvider) Reset(ctx context.Context) (err error) {
return
}
// @todo consider omitting these from the interface since they're not always needed
func (ip *iteratorProvider) Ident() (out string) {
return
}
// @todo consider omitting these from the interface since they're not always needed
func (ip *iteratorProvider) SetIdent(string) {
}
func mkRecordRefWrap(ctx context.Context, s store.Storer, f *types.ModuleField) (wrap refModWrap, err error) {
relModID := f.Options.UInt64("moduleID")
if relModID == 0 {
return
}
var relMod *types.Module
relMod, err = s.LookupComposeModuleByID(ctx, relModID)
if err != nil {
// Referenced module may have been deleted or made inaccessible since the
// field was configured. Skip ref resolution for this field instead of
// failing the entire export — the column is still emitted, just unresolved.
if errors.IsNotFound(err) {
logger.Default().Warn(
"skipping record ref resolution: referenced module not found",
zap.String("field", f.Name),
zap.Uint64("moduleID", relModID),
)
err = nil
}
return
}
relMod.Fields, _, err = s.SearchComposeModuleFields(ctx, types.ModuleFieldFilter{
ModuleID: []uint64{relMod.ID},
})
if err != nil {
return
}
wrap = refModWrap{
modLvl1: relMod,
labelLvl1: f.Options.String("labelField"),
}
if f.Options.String("recordLabelField") != "" {
// Two-level label resolution: the labelField on the related module must
// itself be a Record field pointing to another module. If that chain is
// broken (labelField renamed/removed, target module missing), skip the
// nested resolution instead of aborting the export.
labelFieldName := f.Options.String("labelField")
nestedRef := wrap.modLvl1.Fields.FindByName(labelFieldName)
if nestedRef == nil {
logger.Default().Warn(
"skipping nested record ref resolution: labelField not found on related module",
zap.String("field", f.Name),
zap.String("labelField", labelFieldName),
zap.Uint64("relatedModuleID", relMod.ID),
)
return
}
nestedModID := nestedRef.Options.UInt64("moduleID")
if nestedModID == 0 {
logger.Default().Warn(
"skipping nested record ref resolution: labelField has no moduleID option",
zap.String("field", f.Name),
zap.String("labelField", labelFieldName),
)
return
}
wrap.modLvl2, err = s.LookupComposeModuleByID(ctx, nestedModID)
if err != nil {
if errors.IsNotFound(err) {
logger.Default().Warn(
"skipping nested record ref resolution: nested module not found",
zap.String("field", f.Name),
zap.Uint64("nestedModuleID", nestedModID),
)
err = nil
}
return
}
wrap.labelLvl2 = f.Options.String("recordLabelField")
}
return
}
func (ip *iteratorProvider) resolveUsers(ctx context.Context, s store.Storer) (err error) {
if len(ip.userFields) == 0 || s == nil {
return
}
// Collect unique user IDs across the current chunk
seen := make(map[string]bool)
for _, row := range ip.rows {
for fieldName := range ip.userFields {
for _, val := range row[fieldName].Values {
if val != "" && val != "0" {
seen[val] = true
}
}
}
}
if len(seen) == 0 {
return
}
ids := make([]string, 0, len(seen))
for id := range seen {
ids = append(ids, id)
}
uu, _, err := store.SearchUsers(ctx, s, systemTypes.UserFilter{
UserID: ids,
Paging: filter.Paging{Limit: 0},
Deleted: filter.StateInclusive,
Suspended: filter.StateInclusive,
})
if err != nil {
return
}
labels := make(map[string]string, len(uu))
for _, u := range uu {
labels[strconv.FormatUint(u.ID, 10)] = userLabel(u)
}
for i, row := range ip.rows {
for fieldName := range ip.userFields {
v := row[fieldName]
if len(v.Values) == 0 {
continue
}
for j, val := range v.Values {
if val == "0" || val == "" {
row.SetValue(fieldName, uint(j), "")
continue
}
if label, ok := labels[val]; ok {
if ip.includeRefID {
row.SetValue(fmt.Sprintf("%s ID", fieldName), uint(j), val)
}
row.SetValue(fieldName, uint(j), label)
}
}
}
ip.rows[i] = row
}
return
}
func userLabel(u *systemTypes.User) string {
if u.Name != "" {
return u.Name
}
if u.Email != "" {
return u.Email
}
if u.Handle != "" {
return u.Handle
}
return strconv.FormatUint(u.ID, 10)
}
// filterUnreadableFields removes values the given user does not have access to
func (ip *iteratorProvider) filterUnreadableFields(rowCache datasource.RawRecord) {
if reflect2.IsNil(ip.ac) {
return
}
for fieldName := range rowCache {
// Skip system fields (ID, createdAt, etc.) - these are always readable
// System fields are lowercase and don't have associated module field definitions
if _, hasField := ip.readableFields[fieldName]; !hasField {
// Field not in module definition, likely a system field - keep it
continue
}
// Check if user can read this field
if !ip.readableFields[fieldName] {
delete(rowCache, fieldName)
}
}
}

View File

@@ -0,0 +1,117 @@
package envoy
import (
"context"
"github.com/cortezaproject/corteza/server/compose/dalutils"
"github.com/cortezaproject/corteza/server/compose/types"
"github.com/cortezaproject/corteza/server/pkg/dal"
"github.com/cortezaproject/corteza/server/pkg/envoyx"
"github.com/spf13/cast"
)
type (
// recordGetter is a utility struct to resolve record references from
// different parts of the system such as the dep graph and the database
recordGetter struct {
// Bits to get data from the dep graph
relDatasource *RecordDatasource
// Bits to get data from the database
dalSvc dal.FullService
relModule types.Module
baseFilter types.RecordFilter
// resolved refs, including misses; imports resolve the same handful
// of refs over and over so this saves a query per value
cache map[string]uint64
}
)
func makeRecordGetter(dalSvc dal.FullService, tt envoyx.Traverser, n *envoyx.Node, modRef, dsRef envoyx.Ref) (g *recordGetter) {
g = &recordGetter{
dalSvc: dalSvc,
cache: make(map[string]uint64),
}
// Resolve from dep graph
auxDs := tt.ParentForRef(n, dsRef)
if auxDs != nil {
g.relDatasource = auxDs.Datasource.(*RecordDatasource)
}
// Resolve from the database
mod := g.getRefMod(tt, n, modRef)
if mod != nil {
g.baseFilter = types.RecordFilter{
ModuleID: mod.ID,
NamespaceID: mod.NamespaceID,
}
}
return
}
// resolve resolves the provided reference into a record ID; 0 if can't be resolved
func (g *recordGetter) resolve(ctx context.Context, ref any) (out uint64, err error) {
key := cast.ToString(ref)
if out, ok := g.cache[key]; ok {
return out, nil
}
defer func() {
if err == nil {
g.cache[key] = out
}
}()
// Try to get from datasource
if g.relDatasource != nil {
out, err = g.getDS(ref)
if err != nil {
return
}
}
if out > 0 {
return
}
// Fallback to the store
out, err = g.getDB(ctx, ref)
if err != nil {
return
}
if out > 0 {
return
}
return
}
func (g *recordGetter) getDS(ref any) (out uint64, err error) {
return g.relDatasource.ResolveRef(ref)
}
func (g *recordGetter) getDB(ctx context.Context, ref any) (out uint64, err error) {
// @note the old version only resolved IDs so that's what we're doing here also
// @todo consider expanding this
id, err := cast.ToUint64E(ref)
if err != nil {
return 0, nil
}
aux, err := dalutils.ComposeRecordsFind(ctx, g.dalSvc, &g.relModule, id)
if err != nil {
return
}
out = aux.ID
return
}
func (g *recordGetter) getRefMod(tt envoyx.Traverser, n *envoyx.Node, ref envoyx.Ref) (refMod *types.Module) {
aux := tt.ParentForRef(n, ref)
if aux == nil {
return nil
}
return aux.Resource.(*types.Module)
}

907
server/compose/envoy/store_decode.gen.go generated Normal file
View File

@@ -0,0 +1,907 @@
package envoy
// This file is auto-generated.
//
// Changes to this file may cause incorrect behavior and will be lost if
// the code is regenerated.
//
import (
"context"
"fmt"
"strings"
"github.com/cortezaproject/corteza/server/compose/types"
"github.com/cortezaproject/corteza/server/pkg/dal"
"github.com/cortezaproject/corteza/server/pkg/envoyx"
"github.com/cortezaproject/corteza/server/pkg/id"
"github.com/cortezaproject/corteza/server/store"
"github.com/pkg/errors"
)
type (
// StoreDecoder is responsible for generating Envoy nodes from already stored
// resources which can then be managed by Envoy and imported via an encoder.
StoreDecoder struct{}
filterWrap struct {
rt string
f envoyx.ResourceFilter
}
)
const (
paramsKeyStorer = "storer"
paramsKeyDAL = "dal"
)
var (
// @todo temporary fix to make unused pkg/id not throw errors
_ = id.Next
)
// Decode returns a set of envoy nodes based on the provided params
//
// StoreDecoder expects the DecodeParam of `storer` and `dal` which conform
// to the store.Storer and dal.FullService interfaces.
func (d StoreDecoder) Decode(ctx context.Context, p envoyx.DecodeParams) (out envoyx.NodeSet, err error) {
// @todo we can optionally not require them based on what we're doing
s, err := d.getStorer(p)
if err != nil {
return
}
dl, err := d.getDal(p)
if err != nil {
return
}
return d.decode(ctx, s, dl, p)
}
func (d StoreDecoder) decode(ctx context.Context, s store.Storer, dl dal.FullService, p envoyx.DecodeParams) (out envoyx.NodeSet, err error) {
// Preprocessing and basic filtering (to omit what this decoder can't handle)
wrappedFilters := d.prepFilters(p.Filter)
// Get all scoped nodes
scopedNodes, err := d.getScopeNodes(ctx, s, dl, wrappedFilters)
if err != nil {
return
}
// Get all reference nodes
refNodes, refRefs, err := d.getReferenceNodes(ctx, s, dl, wrappedFilters)
if err != nil {
return
}
// Process filters to get the envoy nodes
err = func() (err error) {
var aux envoyx.NodeSet
for i, wf := range wrappedFilters {
switch wf.rt {
case types.ChartResourceType:
aux, err = d.decodeChart(ctx, s, dl, d.makeChartFilter(scopedNodes[i], refNodes[i], wf.f))
if err != nil {
return
}
for _, a := range aux {
a.Identifiers = a.Identifiers.Merge(wf.f.Identifiers)
a.References = envoyx.MergeRefs(a.References, refRefs[i])
}
out = append(out, aux...)
case types.ModuleResourceType:
aux, err = d.decodeModule(ctx, s, dl, d.makeModuleFilter(scopedNodes[i], refNodes[i], wf.f))
if err != nil {
return
}
for _, a := range aux {
a.Identifiers = a.Identifiers.Merge(wf.f.Identifiers)
a.References = envoyx.MergeRefs(a.References, refRefs[i])
}
out = append(out, aux...)
case types.ModuleFieldResourceType:
aux, err = d.decodeModuleField(ctx, s, dl, d.makeModuleFieldFilter(scopedNodes[i], refNodes[i], wf.f))
if err != nil {
return
}
for _, a := range aux {
a.Identifiers = a.Identifiers.Merge(wf.f.Identifiers)
a.References = envoyx.MergeRefs(a.References, refRefs[i])
}
out = append(out, aux...)
case types.NamespaceResourceType:
aux, err = d.decodeNamespace(ctx, s, dl, d.makeNamespaceFilter(scopedNodes[i], refNodes[i], wf.f))
if err != nil {
return
}
for _, a := range aux {
a.Identifiers = a.Identifiers.Merge(wf.f.Identifiers)
a.References = envoyx.MergeRefs(a.References, refRefs[i])
}
out = append(out, aux...)
case types.PageResourceType:
aux, err = d.decodePage(ctx, s, dl, d.makePageFilter(scopedNodes[i], refNodes[i], wf.f))
if err != nil {
return
}
for _, a := range aux {
a.Identifiers = a.Identifiers.Merge(wf.f.Identifiers)
a.References = envoyx.MergeRefs(a.References, refRefs[i])
}
out = append(out, aux...)
case types.PageLayoutResourceType:
aux, err = d.decodePageLayout(ctx, s, dl, d.makePageLayoutFilter(scopedNodes[i], refNodes[i], wf.f))
if err != nil {
return
}
for _, a := range aux {
a.Identifiers = a.Identifiers.Merge(wf.f.Identifiers)
a.References = envoyx.MergeRefs(a.References, refRefs[i])
}
out = append(out, aux...)
default:
aux, err = d.extendDecoder(ctx, s, dl, p, wf.rt, refNodes[i], wf.f)
if err != nil {
return
}
for _, a := range aux {
a.Identifiers = a.Identifiers.Merge(wf.f.Identifiers)
a.References = envoyx.MergeRefs(a.References, refRefs[i])
}
out = append(out, aux...)
}
}
return
}()
if err != nil {
err = errors.Wrap(err, "failed to decode filters")
return
}
return
}
// // // // // // // // // // // // // // // // // // // // // // // // //
// Functions for resource chart
// // // // // // // // // // // // // // // // // // // // // // // // //
func (d StoreDecoder) decodeChart(ctx context.Context, s store.Storer, dl dal.FullService, f types.ChartFilter) (out envoyx.NodeSet, err error) {
// @todo this might need to be improved.
// Currently, no resource is vast enough to pose a problem.
rr, _, err := store.SearchComposeCharts(ctx, s, f)
if err != nil {
return
}
for _, r := range rr {
var n *envoyx.Node
n, err = ChartToEnvoyNode(r)
if err != nil {
return
}
out = append(out, n)
}
return
}
func ChartToEnvoyNode(r *types.Chart) (node *envoyx.Node, err error) {
// Identifiers
ii := envoyx.MakeIdentifiers(
r.Handle,
r.ID,
)
// Handle references
// Omit any non-defined values
refs := map[string]envoyx.Ref{}
if r.NamespaceID > 0 {
refs["NamespaceID"] = envoyx.Ref{
ResourceType: "corteza::compose:namespace",
Identifiers: envoyx.MakeIdentifiers(r.NamespaceID),
}
}
refs = envoyx.MergeRefs(refs, decodeChartRefs(r))
var scope envoyx.Scope
scope = envoyx.Scope{
ResourceType: refs["NamespaceID"].ResourceType,
Identifiers: refs["NamespaceID"].Identifiers,
}
for k, ref := range refs {
// @todo temporary solution to not needlessly scope resources.
// Optimally, this would be selectively handled by codegen.
if !strings.HasPrefix(ref.ResourceType, "corteza::compose") {
continue
}
ref.Scope = scope
refs[k] = ref
}
node = &envoyx.Node{
Resource: r,
ResourceType: types.ChartResourceType,
Identifiers: ii,
References: refs,
Scope: scope,
}
return
}
func (d StoreDecoder) makeChartFilter(scope *envoyx.Node, refs map[string]*envoyx.Node, auxf envoyx.ResourceFilter) (out types.ChartFilter) {
out.Limit = auxf.Limit
ids, hh := auxf.Identifiers.Idents()
_ = ids
_ = hh
out.ChartID = id.Strings(ids...)
if len(hh) > 0 {
out.Handle = hh[0]
}
// Refs
var (
ar *envoyx.Node
ok bool
)
_ = ar
_ = ok
ar, ok = refs["NamespaceID"]
if ok {
out.NamespaceID = ar.Resource.GetID()
}
return
}
// // // // // // // // // // // // // // // // // // // // // // // // //
// Functions for resource module
// // // // // // // // // // // // // // // // // // // // // // // // //
func (d StoreDecoder) decodeModule(ctx context.Context, s store.Storer, dl dal.FullService, f types.ModuleFilter) (out envoyx.NodeSet, err error) {
// @todo this might need to be improved.
// Currently, no resource is vast enough to pose a problem.
rr, _, err := store.SearchComposeModules(ctx, s, f)
if err != nil {
return
}
for _, r := range rr {
var n *envoyx.Node
n, err = ModuleToEnvoyNode(r)
if err != nil {
return
}
out = append(out, n)
}
aux, err := d.extendedModuleDecoder(ctx, s, dl, f, out)
if err != nil {
return
}
out = append(out, aux...)
return
}
func ModuleToEnvoyNode(r *types.Module) (node *envoyx.Node, err error) {
// Identifiers
ii := envoyx.MakeIdentifiers(
r.Handle,
r.ID,
)
// Handle references
// Omit any non-defined values
refs := map[string]envoyx.Ref{}
if r.NamespaceID > 0 {
refs["NamespaceID"] = envoyx.Ref{
ResourceType: "corteza::compose:namespace",
Identifiers: envoyx.MakeIdentifiers(r.NamespaceID),
}
}
var scope envoyx.Scope
scope = envoyx.Scope{
ResourceType: refs["NamespaceID"].ResourceType,
Identifiers: refs["NamespaceID"].Identifiers,
}
for k, ref := range refs {
// @todo temporary solution to not needlessly scope resources.
// Optimally, this would be selectively handled by codegen.
if !strings.HasPrefix(ref.ResourceType, "corteza::compose") {
continue
}
ref.Scope = scope
refs[k] = ref
}
node = &envoyx.Node{
Resource: r,
ResourceType: types.ModuleResourceType,
Identifiers: ii,
References: refs,
Scope: scope,
}
return
}
func (d StoreDecoder) makeModuleFilter(scope *envoyx.Node, refs map[string]*envoyx.Node, auxf envoyx.ResourceFilter) (out types.ModuleFilter) {
out.Limit = auxf.Limit
ids, hh := auxf.Identifiers.Idents()
_ = ids
_ = hh
out.ModuleID = id.Strings(ids...)
if len(hh) > 0 {
out.Handle = hh[0]
}
// Refs
var (
ar *envoyx.Node
ok bool
)
_ = ar
_ = ok
ar, ok = refs["NamespaceID"]
if ok {
out.NamespaceID = ar.Resource.GetID()
}
out = d.extendModuleFilter(scope, refs, auxf, out)
return
}
// // // // // // // // // // // // // // // // // // // // // // // // //
// Functions for resource moduleField
// // // // // // // // // // // // // // // // // // // // // // // // //
func (d StoreDecoder) decodeModuleField(ctx context.Context, s store.Storer, dl dal.FullService, f types.ModuleFieldFilter) (out envoyx.NodeSet, err error) {
// @todo this might need to be improved.
// Currently, no resource is vast enough to pose a problem.
rr, _, err := store.SearchComposeModuleFields(ctx, s, f)
if err != nil {
return
}
for _, r := range rr {
var n *envoyx.Node
n, err = ModuleFieldToEnvoyNode(r)
if err != nil {
return
}
out = append(out, n)
}
return
}
func ModuleFieldToEnvoyNode(r *types.ModuleField) (node *envoyx.Node, err error) {
// Identifiers
ii := envoyx.MakeIdentifiers(
r.ID,
r.Name,
)
// Handle references
// Omit any non-defined values
refs := map[string]envoyx.Ref{}
if r.ModuleID > 0 {
refs["ModuleID"] = envoyx.Ref{
ResourceType: "corteza::compose:module",
Identifiers: envoyx.MakeIdentifiers(r.ModuleID),
}
}
refs = envoyx.MergeRefs(refs, decodeModuleFieldRefs(r))
var scope envoyx.Scope
scope = envoyx.Scope{
ResourceType: refs["NamespaceID"].ResourceType,
Identifiers: refs["NamespaceID"].Identifiers,
}
for k, ref := range refs {
// @todo temporary solution to not needlessly scope resources.
// Optimally, this would be selectively handled by codegen.
if !strings.HasPrefix(ref.ResourceType, "corteza::compose") {
continue
}
ref.Scope = scope
refs[k] = ref
}
node = &envoyx.Node{
Resource: r,
ResourceType: types.ModuleFieldResourceType,
Identifiers: ii,
References: refs,
Scope: scope,
}
return
}
// Resource should define a custom filter builder
// // // // // // // // // // // // // // // // // // // // // // // // //
// Functions for resource namespace
// // // // // // // // // // // // // // // // // // // // // // // // //
func (d StoreDecoder) decodeNamespace(ctx context.Context, s store.Storer, dl dal.FullService, f types.NamespaceFilter) (out envoyx.NodeSet, err error) {
// @todo this might need to be improved.
// Currently, no resource is vast enough to pose a problem.
rr, _, err := store.SearchComposeNamespaces(ctx, s, f)
if err != nil {
return
}
for _, r := range rr {
var n *envoyx.Node
n, err = NamespaceToEnvoyNode(r)
if err != nil {
return
}
out = append(out, n)
}
return
}
func NamespaceToEnvoyNode(r *types.Namespace) (node *envoyx.Node, err error) {
// Identifiers
ii := envoyx.MakeIdentifiers(
r.ID,
r.Slug,
)
// Handle references
// Omit any non-defined values
refs := map[string]envoyx.Ref{}
var scope envoyx.Scope
scope = envoyx.Scope{
ResourceType: types.NamespaceResourceType,
Identifiers: ii,
}
node = &envoyx.Node{
Resource: r,
ResourceType: types.NamespaceResourceType,
Identifiers: ii,
References: refs,
Scope: scope,
}
return
}
func (d StoreDecoder) makeNamespaceFilter(scope *envoyx.Node, refs map[string]*envoyx.Node, auxf envoyx.ResourceFilter) (out types.NamespaceFilter) {
out.Limit = auxf.Limit
ids, hh := auxf.Identifiers.Idents()
_ = ids
_ = hh
out.NamespaceID = id.Strings(ids...)
if len(hh) > 0 {
out.Slug = hh[0]
}
// Refs
var (
ar *envoyx.Node
ok bool
)
_ = ar
_ = ok
out = d.extendNamespaceFilter(scope, refs, auxf, out)
return
}
// // // // // // // // // // // // // // // // // // // // // // // // //
// Functions for resource page
// // // // // // // // // // // // // // // // // // // // // // // // //
func (d StoreDecoder) decodePage(ctx context.Context, s store.Storer, dl dal.FullService, f types.PageFilter) (out envoyx.NodeSet, err error) {
// @todo this might need to be improved.
// Currently, no resource is vast enough to pose a problem.
rr, _, err := store.SearchComposePages(ctx, s, f)
if err != nil {
return
}
for _, r := range rr {
var n *envoyx.Node
n, err = PageToEnvoyNode(r)
if err != nil {
return
}
out = append(out, n)
}
return
}
func PageToEnvoyNode(r *types.Page) (node *envoyx.Node, err error) {
// Identifiers
ii := envoyx.MakeIdentifiers(
r.Handle,
r.ID,
)
// Handle references
// Omit any non-defined values
refs := map[string]envoyx.Ref{}
if r.ModuleID > 0 {
refs["ModuleID"] = envoyx.Ref{
ResourceType: "corteza::compose:module",
Identifiers: envoyx.MakeIdentifiers(r.ModuleID),
}
}
if r.NamespaceID > 0 {
refs["NamespaceID"] = envoyx.Ref{
ResourceType: "corteza::compose:namespace",
Identifiers: envoyx.MakeIdentifiers(r.NamespaceID),
}
}
if r.SelfID > 0 {
refs["SelfID"] = envoyx.Ref{
ResourceType: "corteza::compose:page",
Identifiers: envoyx.MakeIdentifiers(r.SelfID),
}
}
refs = envoyx.MergeRefs(refs, decodePageRefs(r))
var scope envoyx.Scope
scope = envoyx.Scope{
ResourceType: refs["NamespaceID"].ResourceType,
Identifiers: refs["NamespaceID"].Identifiers,
}
for k, ref := range refs {
// @todo temporary solution to not needlessly scope resources.
// Optimally, this would be selectively handled by codegen.
if !strings.HasPrefix(ref.ResourceType, "corteza::compose") {
continue
}
ref.Scope = scope
refs[k] = ref
}
node = &envoyx.Node{
Resource: r,
ResourceType: types.PageResourceType,
Identifiers: ii,
References: refs,
Scope: scope,
}
return
}
func (d StoreDecoder) makePageFilter(scope *envoyx.Node, refs map[string]*envoyx.Node, auxf envoyx.ResourceFilter) (out types.PageFilter) {
out.Limit = auxf.Limit
ids, hh := auxf.Identifiers.Idents()
_ = ids
_ = hh
out.PageID = id.Strings(ids...)
if len(hh) > 0 {
out.Handle = hh[0]
}
// Refs
var (
ar *envoyx.Node
ok bool
)
_ = ar
_ = ok
ar, ok = refs["ModuleID"]
if ok {
out.ModuleID = ar.Resource.GetID()
}
ar, ok = refs["NamespaceID"]
if ok {
out.NamespaceID = ar.Resource.GetID()
}
ar, ok = refs["SelfID"]
if ok {
out.ParentID = ar.Resource.GetID()
}
out = d.extendPageFilter(scope, refs, auxf, out)
return
}
// // // // // // // // // // // // // // // // // // // // // // // // //
// Functions for resource pageLayout
// // // // // // // // // // // // // // // // // // // // // // // // //
func (d StoreDecoder) decodePageLayout(ctx context.Context, s store.Storer, dl dal.FullService, f types.PageLayoutFilter) (out envoyx.NodeSet, err error) {
// @todo this might need to be improved.
// Currently, no resource is vast enough to pose a problem.
rr, _, err := store.SearchComposePageLayouts(ctx, s, f)
if err != nil {
return
}
for _, r := range rr {
var n *envoyx.Node
n, err = PageLayoutToEnvoyNode(r)
if err != nil {
return
}
out = append(out, n)
}
return
}
func PageLayoutToEnvoyNode(r *types.PageLayout) (node *envoyx.Node, err error) {
// Identifiers
ii := envoyx.MakeIdentifiers(
r.Handle,
r.ID,
)
// Handle references
// Omit any non-defined values
refs := map[string]envoyx.Ref{}
if r.NamespaceID > 0 {
refs["NamespaceID"] = envoyx.Ref{
ResourceType: "corteza::compose:namespace",
Identifiers: envoyx.MakeIdentifiers(r.NamespaceID),
}
}
if r.OwnedBy > 0 {
refs["OwnedBy"] = envoyx.Ref{
ResourceType: "corteza::system:user",
Identifiers: envoyx.MakeIdentifiers(r.OwnedBy),
}
}
if r.PageID > 0 {
refs["PageID"] = envoyx.Ref{
ResourceType: "corteza::compose:page",
Identifiers: envoyx.MakeIdentifiers(r.PageID),
}
}
if r.ParentID > 0 {
refs["ParentID"] = envoyx.Ref{
ResourceType: "corteza::compose:page-layout",
Identifiers: envoyx.MakeIdentifiers(r.ParentID),
}
}
var scope envoyx.Scope
scope = envoyx.Scope{
ResourceType: refs["NamespaceID"].ResourceType,
Identifiers: refs["NamespaceID"].Identifiers,
}
for k, ref := range refs {
// @todo temporary solution to not needlessly scope resources.
// Optimally, this would be selectively handled by codegen.
if !strings.HasPrefix(ref.ResourceType, "corteza::compose") {
continue
}
ref.Scope = scope
refs[k] = ref
}
node = &envoyx.Node{
Resource: r,
ResourceType: types.PageLayoutResourceType,
Identifiers: ii,
References: refs,
Scope: scope,
}
return
}
func (d StoreDecoder) makePageLayoutFilter(scope *envoyx.Node, refs map[string]*envoyx.Node, auxf envoyx.ResourceFilter) (out types.PageLayoutFilter) {
out.Limit = auxf.Limit
ids, hh := auxf.Identifiers.Idents()
_ = ids
_ = hh
out.PageLayoutID = id.Strings(ids...)
if len(hh) > 0 {
out.Handle = hh[0]
}
// Refs
var (
ar *envoyx.Node
ok bool
)
_ = ar
_ = ok
ar, ok = refs["NamespaceID"]
if ok {
out.NamespaceID = ar.Resource.GetID()
}
ar, ok = refs["PageID"]
if ok {
out.PageID = ar.Resource.GetID()
}
ar, ok = refs["ParentID"]
if ok {
out.ParentID = ar.Resource.GetID()
}
return
}
// // // // // // // // // // // // // // // // // // // // // // // // //
// Utilities
// // // // // // // // // // // // // // // // // // // // // // // // //
func (d StoreDecoder) getStorer(p envoyx.DecodeParams) (s store.Storer, err error) {
aux, ok := p.Params[paramsKeyStorer]
if ok {
s, ok = aux.(store.Storer)
if ok {
return
}
}
err = errors.Errorf("store decoder expects a storer conforming to store.Storer interface")
return
}
func (d StoreDecoder) getDal(p envoyx.DecodeParams) (dl dal.FullService, err error) {
aux, ok := p.Params[paramsKeyDAL]
if ok {
dl, ok = aux.(dal.FullService)
if ok {
return
}
}
err = errors.Errorf("store decoder expects a DAL conforming to dal.FullService interface")
return
}
func (d StoreDecoder) prepFilters(ff map[string]envoyx.ResourceFilter) (out []filterWrap) {
out = make([]filterWrap, 0, len(ff))
for rt, f := range ff {
// Handle resources that don't belong to this decoder
if !strings.HasPrefix(rt, "corteza::compose") {
continue
}
out = append(out, filterWrap{rt: rt, f: f})
}
return
}
func (d StoreDecoder) getScopeNodes(ctx context.Context, s store.Storer, dl dal.FullService, ff []filterWrap) (scopes envoyx.NodeSet, err error) {
// Get all requested scopes
scopes = make(envoyx.NodeSet, len(ff))
err = func() (err error) {
for i, fw := range ff {
if fw.f.Scope.ResourceType == "" {
continue
}
// For now the scope can only point to namespace so this will do
var nn envoyx.NodeSet
nn, err = d.decodeNamespace(ctx, s, dl, d.makeNamespaceFilter(nil, nil, envoyx.ResourceFilter{Identifiers: fw.f.Scope.Identifiers}))
if err != nil {
return
}
if len(nn) > 1 {
err = fmt.Errorf("ambiguous scope %v: matches multiple resources", fw.f.Scope)
return
}
if len(nn) == 0 {
err = fmt.Errorf("invalid scope %v: resource not found", fw.f)
return
}
scopes[i] = nn[0]
}
return
}()
if err != nil {
err = errors.Wrap(err, "failed to decode node scopes")
return
}
return
}
// getReferenceNodes returns all of the nodes referenced by the nodes defined by the filters
//
// The nodes are provided as a slice (the same order as the filters) and as a map for easier lookups.
func (d StoreDecoder) getReferenceNodes(ctx context.Context, s store.Storer, dl dal.FullService, ff []filterWrap) (nodes []map[string]*envoyx.Node, refs []map[string]envoyx.Ref, err error) {
nodes = make([]map[string]*envoyx.Node, len(ff))
refs = make([]map[string]envoyx.Ref, len(ff))
err = func() (err error) {
for i, a := range ff {
if len(a.f.Refs) == 0 {
continue
}
auxr := make(map[string]*envoyx.Node, len(a.f.Refs))
auxa := make(map[string]envoyx.Ref)
for field, ref := range a.f.Refs {
f := ref.ResourceFilter()
aux, err := d.decode(ctx, s, dl, envoyx.DecodeParams{
Type: envoyx.DecodeTypeStore,
Filter: f,
})
if err != nil {
return err
}
// @todo consider changing this.
// Currently it's required because the .decode may return some
// nested nodes as well.
// Consider a flag or a new function.
aux = envoyx.NodesForResourceType(ref.ResourceType, aux...)
if len(aux) == 0 {
return fmt.Errorf("invalid reference %v", ref)
}
if len(aux) > 1 {
return fmt.Errorf("ambiguous reference: too many resources returned %v", a.f)
}
auxr[field] = aux[0]
auxa[field] = aux[0].ToRef()
}
nodes[i] = auxr
refs[i] = auxa
}
return
}()
if err != nil {
err = errors.Wrap(err, "failed to decode node references")
return
}
return
}

View File

@@ -0,0 +1,301 @@
package envoy
import (
"context"
"fmt"
"github.com/cortezaproject/corteza/server/compose/dalutils"
"github.com/cortezaproject/corteza/server/compose/service"
"github.com/cortezaproject/corteza/server/compose/types"
"github.com/cortezaproject/corteza/server/pkg/dal"
"github.com/cortezaproject/corteza/server/pkg/envoyx"
"github.com/cortezaproject/corteza/server/pkg/filter"
"github.com/cortezaproject/corteza/server/pkg/id"
"github.com/cortezaproject/corteza/server/pkg/rbac"
"github.com/cortezaproject/corteza/server/store"
"github.com/spf13/cast"
)
func (d StoreDecoder) extendNamespaceFilter(scope *envoyx.Node, refs map[string]*envoyx.Node, auxf envoyx.ResourceFilter, base types.NamespaceFilter) (out types.NamespaceFilter) {
out = base
if scope == nil {
return
}
if scope.ResourceType == "" {
return
}
// Overwrite it
out.NamespaceID = id.Strings(scope.Resource.GetID())
return
}
func (d StoreDecoder) extendModuleFilter(scope *envoyx.Node, refs map[string]*envoyx.Node, auxf envoyx.ResourceFilter, base types.ModuleFilter) (out types.ModuleFilter) {
out = base
if scope == nil {
return
}
if scope.ResourceType == "" {
return
}
// Overwrite it
out.NamespaceID = scope.Resource.GetID()
return
}
func (d StoreDecoder) extendPageFilter(scope *envoyx.Node, refs map[string]*envoyx.Node, auxf envoyx.ResourceFilter, base types.PageFilter) (out types.PageFilter) {
out = base
if scope == nil {
return
}
if scope.ResourceType == "" {
return
}
// Overwrite it
out.NamespaceID = scope.Resource.GetID()
return
}
func (d StoreDecoder) makeModuleFieldFilter(scope *envoyx.Node, refs map[string]*envoyx.Node, auxf envoyx.ResourceFilter) (out types.ModuleFieldFilter) {
out.Limit = auxf.Limit
ids, hh := auxf.Identifiers.Idents()
_ = ids
_ = hh
// Refs
var (
ar *envoyx.Node
ok bool
)
_ = ar
_ = ok
// ar, ok = refs["ModuleID"]
// if ok {
// out.ModuleID = ar.Resource.GetID()
// }
return
}
func (d StoreDecoder) extendedModuleDecoder(ctx context.Context, s store.Storer, dl dal.FullService, f types.ModuleFilter, base envoyx.NodeSet) (out envoyx.NodeSet, err error) {
var ff envoyx.NodeSet
for _, b := range base {
mod := b.Resource.(*types.Module)
// Get all of the related module fields, append them to the output and
// the original module (so other code can have access to the related fields)
ff, err = d.decodeModuleField(ctx, s, dl, types.ModuleFieldFilter{ModuleID: []uint64{mod.ID}})
if err != nil {
return
}
for _, f := range ff {
f.Scope = b.Scope
f.References = envoyx.MergeRefs(f.References, b.References, map[string]envoyx.Ref{
"ModuleID": b.ToRef(),
})
for k, ref := range f.References {
ref.Scope = b.Scope
f.References[k] = ref
}
mod.Fields = append(mod.Fields, f.Resource.(*types.ModuleField))
}
out = append(out, ff...)
}
return
}
func decodeChartRefs(c *types.Chart) (refs map[string]envoyx.Ref) {
refs = make(map[string]envoyx.Ref, len(c.Config.Reports))
for i, r := range c.Config.Reports {
if r.ModuleID == 0 {
continue
}
refs[fmt.Sprintf("Config.Reports.%d.ModuleID", i)] = envoyx.Ref{
ResourceType: types.ModuleResourceType,
Identifiers: envoyx.MakeIdentifiers(r.ModuleID),
}
}
return
}
func decodeModuleFieldRefs(c *types.ModuleField) (refs map[string]envoyx.Ref) {
refs = make(map[string]envoyx.Ref, 1)
refs["NamespaceID"] = envoyx.Ref{
ResourceType: types.NamespaceResourceType,
Identifiers: envoyx.MakeIdentifiers(c.NamespaceID),
}
id := c.Options.UInt64("moduleID")
if id == 0 {
return
}
refs["Options.ModuleID"] = envoyx.Ref{
ResourceType: types.ModuleResourceType,
Identifiers: envoyx.MakeIdentifiers(id),
}
return
}
func decodePageRefs(p *types.Page) (refs map[string]envoyx.Ref) {
refs = make(map[string]envoyx.Ref, len(p.Blocks)/2)
for index, b := range p.Blocks {
switch b.Kind {
case "RecordList":
refs = envoyx.MergeRefs(refs, getPageBlockRecordListRefs(b, index))
case "Automation":
refs = envoyx.MergeRefs(refs, getPageBlockAutomationRefs(b, index))
case "RecordOrganizer":
refs = envoyx.MergeRefs(refs, getPageBlockRecordOrganizerRefs(b, index))
case "Chart":
refs = envoyx.MergeRefs(refs, getPageBlockChartRefs(b, index))
case "Calendar":
refs = envoyx.MergeRefs(refs, getPageBlockCalendarRefs(b, index))
case "Metric":
refs = envoyx.MergeRefs(refs, getPageBlockMetricRefs(b, index))
case "Comment":
refs = envoyx.MergeRefs(refs, getPageBlockCommentRefs(b, index))
case "Progress":
refs = envoyx.MergeRefs(refs, getPageBlockProgressRefs(b, index))
}
}
return
}
func (d StoreDecoder) extendDecoder(ctx context.Context, s store.Storer, dl dal.FullService, p envoyx.DecodeParams, rt string, refs map[string]*envoyx.Node, rf envoyx.ResourceFilter) (out envoyx.NodeSet, err error) {
switch rt {
// @todo consider hooking into the regular record resource type as well
case ComposeRecordDatasourceAuxType:
return d.decodeRecordDatasource(ctx, s, dl, p, refs, rf)
}
return
}
func (d StoreDecoder) decodeRecordDatasource(ctx context.Context, s store.Storer, dl dal.FullService, p envoyx.DecodeParams, refs map[string]*envoyx.Node, rf envoyx.ResourceFilter) (out envoyx.NodeSet, err error) {
var (
ok bool
module *types.Module
moduleNode *envoyx.Node
namespace *types.Namespace
namespaceNode *envoyx.Node
)
// Get the refs
namespaceNode, ok = refs["NamespaceID"]
if !ok {
err = fmt.Errorf("missing NamespaceID reference")
return
}
namespace = namespaceNode.Resource.(*types.Namespace)
moduleNode, ok = refs["ModuleID"]
if !ok {
err = fmt.Errorf("missing ModuleID reference")
return
}
module = moduleNode.Resource.(*types.Module)
// Get the iterator
iter, _, err := dalutils.ComposeRecordsIterator(ctx, dl, module, types.RecordFilter{
Query: rf.Query,
ModuleID: module.ID,
NamespaceID: namespace.ID,
Paging: filter.Paging{
Limit: rf.Limit,
},
})
if err != nil {
return
}
mv := make(map[string]bool)
dt := make(map[string]bool)
for _, f := range module.Fields {
if f.Multi {
mv[f.Name] = true
}
if f.Kind == "DateTime" {
dt[f.Name] = true
}
}
for _, n := range []string{"createdAt", "updatedAt", "deletedAt"} {
dt[n] = true
}
rds := &RecordDatasource{
Provider: &iteratorProvider{iter: iter},
multivalues: mv,
datetimeFields: dt,
refToID: make(map[string]uint64),
existingIDs: make(map[uint64]bool),
// @todo consider providing defaults from the outside
Mapping: envoyx.DatasourceMapping{
KeyField: []string{"id"},
Defaultable: true,
},
}
// Get access controller to enforce field-level read permissions
ac := service.AccessControl(s)
if rbac.Global() == nil {
ac = nil
}
rds.Provider, err = mkIteratorProvider(ctx, ac, s, dl, iter, module, cast.ToBool(p.Params["resolveRefs"]), cast.ToBool(p.Params["includeRefID"]))
if err != nil {
return
}
rr := map[string]envoyx.Ref{
"NamespaceID": namespaceNode.ToRef(),
"ModuleID": moduleNode.ToRef(),
}
out = append(out, &envoyx.Node{
Datasource: rds,
ResourceType: ComposeRecordDatasourceAuxType,
Identifiers: envoyx.MakeIdentifiers(module.ID, module.Handle),
References: rr,
Scope: moduleNode.Scope,
})
return
}

1552
server/compose/envoy/store_encode.gen.go generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,233 @@
package envoy
import (
"context"
"time"
"github.com/cortezaproject/corteza/server/compose/service"
"github.com/cortezaproject/corteza/server/compose/types"
"github.com/cortezaproject/corteza/server/pkg/dal"
"github.com/cortezaproject/corteza/server/pkg/envoyx"
"github.com/cortezaproject/corteza/server/store"
"github.com/pkg/errors"
)
func (e StoreEncoder) encode(ctx context.Context, p envoyx.EncodeParams, s store.Storer, rt string, nn envoyx.NodeSet, tree envoyx.Traverser) (err error) {
dl, err := e.grabDal(p)
if err != nil {
return
}
switch rt {
case ComposeRecordDatasourceAuxType:
err = e.encodeRecordDatasources(ctx, p, s, dl, nn, tree)
if err != nil {
return
}
}
return
}
func (e StoreEncoder) setChartDefaults(res *types.Chart) (err error) {
if res.CreatedAt.IsZero() {
res.CreatedAt = time.Now()
}
return
}
func (e StoreEncoder) validateChart(*types.Chart) (err error) {
return
}
func (e StoreEncoder) setModuleDefaults(res *types.Module) (err error) {
if res.CreatedAt.IsZero() {
res.CreatedAt = time.Now()
}
return
}
func (e StoreEncoder) validateModule(*types.Module) (err error) {
return
}
func (e StoreEncoder) setModuleFieldDefaults(res *types.ModuleField) (err error) {
if res.CreatedAt.IsZero() {
res.CreatedAt = time.Now()
}
if res.Kind == "" {
res.Kind = "String"
}
// Update validator ID
maxValidatorID := uint64(0)
for _, v := range res.Expressions.Validators {
if v.ValidatorID > maxValidatorID {
maxValidatorID = v.ValidatorID
}
}
for _, v := range res.Expressions.Validators {
if v.ValidatorID == 0 {
v.ValidatorID = maxValidatorID + 1
maxValidatorID++
}
}
return
}
func (e StoreEncoder) sanitizeModuleFieldBeforeSave(f *types.ModuleField) (err error) {
delete(f.Options, "module")
return
}
func (e StoreEncoder) validateModuleField(*types.ModuleField) (err error) {
return
}
func (e StoreEncoder) setNamespaceDefaults(res *types.Namespace) (err error) {
if res.CreatedAt.IsZero() {
res.CreatedAt = time.Now()
}
return
}
func (e StoreEncoder) validateNamespace(*types.Namespace) (err error) {
return
}
func (e StoreEncoder) setPageDefaults(res *types.Page) (err error) {
if res.CreatedAt.IsZero() {
res.CreatedAt = time.Now()
}
if res.Title == "" {
res.Title = res.Handle
}
// Update pageblock ID
maxPageBlockID := uint64(0)
for _, b := range res.Blocks {
if b.BlockID > maxPageBlockID {
maxPageBlockID = b.BlockID
}
}
for i, b := range res.Blocks {
if b.BlockID == 0 {
b.BlockID = maxPageBlockID + 1
maxPageBlockID++
res.Blocks[i] = b
}
}
return
}
func (e StoreEncoder) validatePage(*types.Page) (err error) {
return
}
func (e StoreEncoder) setPageLayoutDefaults(res *types.PageLayout) (err error) {
if res.CreatedAt.IsZero() {
res.CreatedAt = time.Now()
}
// @note these pageblocks reference the ones define on the page so nothing
// to do here.
return
}
func (e StoreEncoder) validatePageLayout(*types.PageLayout) (err error) {
return
}
func (e StoreEncoder) setRecordDefaults(*types.Record) (err error) {
return
}
func (e StoreEncoder) validateRecord(*types.Record) (err error) {
return
}
func (e StoreEncoder) prepare(ctx context.Context, p envoyx.EncodeParams, s store.Storer, rt string, nn envoyx.NodeSet) (err error) {
dl, err := e.grabDal(p)
if err != nil {
return
}
switch rt {
case ComposeRecordDatasourceAuxType:
return e.prepareRecordDatasource(ctx, p, s, dl, nn)
}
return
}
func (e StoreEncoder) encodeModuleExtend(ctx context.Context, p envoyx.EncodeParams, s store.Storer, n *envoyx.Node, nested envoyx.NodeSet, tree envoyx.Traverser) (err error) {
// Push fields under mod
mod := n.Resource.(*types.Module)
for _, n := range nested {
if n.ResourceType != types.ModuleFieldResourceType {
continue
}
mod.Fields = append(mod.Fields, n.Resource.(*types.ModuleField))
}
// Register to DAL
dl, err := e.grabDal(p)
if err != nil {
return
}
nsNode := tree.ParentForRef(n, n.References["NamespaceID"])
ns := nsNode.Resource.(*types.Namespace)
// @todo get connection and things from there
models, err := service.ModulesToModelSet(dl, ns, mod)
if err != nil {
return err
}
// @note there is only one model so this is ok
_, err = dl.ReplaceModel(ctx, nil, models[0])
return
}
func (e StoreEncoder) postModulesEncode(ctx context.Context, p envoyx.EncodeParams, s store.Storer, tree envoyx.Traverser, nn envoyx.NodeSet) (err error) {
dl, err := e.grabDal(p)
if err != nil {
return
}
for _, n := range nn {
cc := tree.ChildrenForResourceType(n, ComposeRecordDatasourceAuxType)
err = e.encodeRecordDatasources(ctx, p, s, dl, cc, tree)
if err != nil {
return
}
}
return
}
func (e *StoreEncoder) grabDal(p envoyx.EncodeParams) (dl dal.FullService, err error) {
auxs, ok := p.Params[paramsKeyDAL]
if !ok {
err = errors.Errorf("store encoder expects a dal conforming to dal.FullService interface")
return
}
dl, ok = auxs.(dal.FullService)
if !ok {
err = errors.Errorf("store encoder expects a dal conforming to dal.FullService interface")
return
}
return
}

View File

@@ -0,0 +1,420 @@
package envoy
import (
"context"
"fmt"
"strings"
"time"
"github.com/cortezaproject/corteza/server/compose/dalutils"
"github.com/cortezaproject/corteza/server/compose/service"
"github.com/cortezaproject/corteza/server/compose/service/values"
"github.com/cortezaproject/corteza/server/compose/types"
"github.com/cortezaproject/corteza/server/pkg/auth"
"github.com/cortezaproject/corteza/server/pkg/dal"
"github.com/cortezaproject/corteza/server/pkg/envoyx"
"github.com/cortezaproject/corteza/server/pkg/envoyx/datasource"
"github.com/cortezaproject/corteza/server/pkg/id"
"github.com/cortezaproject/corteza/server/store"
systemEnvoy "github.com/cortezaproject/corteza/server/system/envoy"
"github.com/spf13/cast"
)
const (
recordBatchMaxChunk = 100
)
var (
rvSanitizer = values.Sanitizer()
rvValidator = values.Validator()
rvFormatter = values.Formatter()
)
func (e StoreEncoder) prepareRecordDatasource(ctx context.Context, p envoyx.EncodeParams, s store.Storer, dl dal.FullService, nn envoyx.NodeSet) (err error) {
// @todo match existing records; for now use just the ID like V1
for _, n := range nn {
if n.Datasource == nil {
panic("unexpected state: cannot call prepareRecordDatasource with nodes without a defined Datasource")
}
ds, ok := n.Datasource.(*RecordDatasource)
if !ok {
panic("unexpected datasource type: node expecting type of RecordDatasource")
}
err = e.prepareRecords(ctx, p, s, dl, ds, nn)
if err != nil {
return
}
err = ds.Reset(ctx)
if err != nil {
return
}
}
return
}
func (e StoreEncoder) prepareRecords(ctx context.Context, p envoyx.EncodeParams, s store.Storer, dl dal.FullService, ds *RecordDatasource, nn envoyx.NodeSet) (err error) {
var (
aux = make(datasource.RawRecord)
more bool
ident []string
)
ds.refToID = make(map[string]uint64)
ds.existingIDs = make(map[uint64]bool)
// Just so we don't need to do a branch later down the line
cex := ds.CheckExisting
if cex == nil || len(ds.Mapping.KeyField) == 0 {
cex = func(ctx context.Context, ref ...[]string) ([]uint64, error) {
return make([]uint64, len(ref)), nil
}
}
for {
ident, more, err = ds.Next(ctx, aux)
if err != nil || !more {
return
}
// @todo we'll need to batch these up
existing, err := cex(ctx, ident)
if err != nil {
return err
}
if existing[0] != 0 {
ds.existingIDs[existing[0]] = true
}
if existing[0] == 0 {
existing[0] = id.Next()
}
ds.AddRef(existing[0], ident...)
// Construct a simple record for basic validation/preprocessing
rec := types.Record{}
for k, vv := range aux {
for i, v := range vv.Values {
// Ignore errors at this point since some values can't yet be properly casted
rec.SetValue(k, uint(i), v)
}
}
// @note defaults and validation will have to happen again when encoding
// since we won't persist it.
// Consider supporting updating the datasource's data.
err = e.setRecordDefaults(&rec)
if err != nil {
return err
}
err = e.validateRecord(&rec)
if err != nil {
return err
}
}
}
func (e StoreEncoder) encodeRecordDatasources(ctx context.Context, p envoyx.EncodeParams, s store.Storer, dl dal.FullService, nn envoyx.NodeSet, tree envoyx.Traverser) (err error) {
for _, n := range nn {
err = e.encodeRecordDatasource(ctx, p, s, dl, n, tree)
if err != nil {
return
}
}
return
}
func (e StoreEncoder) encodeRecordDatasource(ctx context.Context, p envoyx.EncodeParams, s store.Storer, dl dal.FullService, n *envoyx.Node, tree envoyx.Traverser) (err error) {
var (
auxRec = make(datasource.RawRecord)
more bool
ident []string
nsNode *envoyx.Node
ns *types.Namespace
modNode *envoyx.Node
mod *types.Module
// This was already validated so we can blindly cast
ds = n.Datasource.(*RecordDatasource)
)
// Get the parent namespace
nsNode = tree.ParentForRef(n, n.References["NamespaceID"])
if nsNode == nil {
err = fmt.Errorf("cannot encode record datasource: missing reference for NamespaceID")
return
}
ns = nsNode.Resource.(*types.Namespace)
// Get the parent module
modNode = tree.ParentForRef(n, n.References["ModuleID"])
if modNode == nil {
err = fmt.Errorf("cannot encode record datasource: missing reference for ModuleID")
return
}
mod = modNode.Resource.(*types.Module)
// Prepare getters for related resources
recordGetters := e.makeRecordGetters(dl, tree, n)
userGetters, sysUserFields := e.makeUserGetters(s, dl, tree, mod, n)
maykr := e.recordMaker(ns, mod, recordGetters, userGetters, sysUserFields)
var (
rec types.Record
creates types.RecordSet
updates types.RecordSet
rve *types.RecordValueErrorSet
)
for {
ident, more, err = ds.Next(ctx, auxRec)
if err != nil {
return
}
if !more {
break
}
err = func() (err error) {
rec, err = maykr(ctx, auxRec)
if err != nil {
return
}
// Do these at the end so they can't be overwritten
rec.NamespaceID = ns.ID
rec.ModuleID = mod.ID
rec.CreatedAt = time.Now()
rec.OwnedBy = service.CalcRecordOwner(0, rec.OwnedBy, p.Encoder.DefaultUserID)
rec.CreatedBy = e.getCreatedBy(ctx, rec)
rec.ID, err = ds.ResolveRefS(ident...)
if err != nil {
return err
}
// Standard record processing
rec.Values.SetUpdatedFlag(true)
//
rve = service.RecordValueUpdateOpCheck(ctx, nil, mod, rec.Values)
if !rve.IsValid() {
return rve
}
//
rve = service.RecordPreparer(ctx, s, rvSanitizer, rvValidator, rvFormatter, mod, &rec)
if !rve.IsValid() {
return rve
}
ax := rec
if ds.existingIDs[rec.ID] {
updates = append(updates, &ax)
} else {
creates = append(creates, &ax)
}
if len(creates) > recordBatchMaxChunk {
err = dalutils.ComposeRecordCreate(ctx, dl, mod, creates...)
if err != nil {
return
}
creates = make(types.RecordSet, 0, recordBatchMaxChunk/2)
}
if len(updates) > recordBatchMaxChunk {
err = dalutils.ComposeRecordUpdate(ctx, dl, mod, updates...)
if err != nil {
return
}
updates = make(types.RecordSet, 0, recordBatchMaxChunk/2)
}
return
}()
if p.Defer != nil {
p.Defer()
}
if err != nil {
if p.DeferNok != nil {
err = p.DeferNok(err)
}
if err != nil {
return
}
} else if p.DeferOk != nil {
p.DeferOk()
}
}
if len(creates) > 0 {
err = dalutils.ComposeRecordCreate(ctx, dl, mod, creates...)
}
if len(updates) > 0 {
err = dalutils.ComposeRecordUpdate(ctx, dl, mod, updates...)
}
return
}
func (e StoreEncoder) getCreatedBy(ctx context.Context, rec types.Record) uint64 {
if rec.CreatedBy > 0 {
return rec.CreatedBy
}
idt := auth.GetIdentityFromContext(ctx)
if idt == nil {
return 0
}
return idt.Identity()
}
// maceRecordGetters returns a map of getters where the key is the field name
func (e StoreEncoder) makeRecordGetters(dl dal.FullService, tree envoyx.Traverser, n *envoyx.Node) (getters map[string]*recordGetter) {
var (
modIndex = make(map[string]envoyx.Ref)
dsIndex = make(map[string]envoyx.Ref)
)
// First pass collects all the references
for k, v := range n.References {
if k == "NamespaceID" || k == "ModuleID" {
continue
}
// Encoded as <fieldName>.<refKind>
pp := strings.Split(k, ".")
f := pp[0]
kind := pp[1]
switch kind {
case "module":
modIndex[f] = v
case "datasource":
dsIndex[f] = v
}
}
// - second pass makes the getters
getters = make(map[string]*recordGetter)
for k := range modIndex {
getters[k] = makeRecordGetter(dl, tree, n, modIndex[k], dsIndex[k])
}
return
}
// sysUserRefFields are the record system user fields in the spellings
// types.Record.SetValue understands
var sysUserRefFields = []string{
"ownedBy", "OwnedBy", "owned_by",
"createdBy", "CreatedBy", "created_by",
"updatedBy", "UpdatedBy", "updated_by",
"deletedBy", "DeletedBy", "deleted_by",
}
func (e StoreEncoder) makeUserGetters(s store.Storer, dl dal.FullService, tree envoyx.Traverser, mod *types.Module, n *envoyx.Node) (getters map[string]*systemEnvoy.UserGetter, sysFields map[string]bool) {
userGetter := systemEnvoy.MakeUserGetter(s, tree)
getters = make(map[string]*systemEnvoy.UserGetter, len(sysUserRefFields))
sysFields = make(map[string]bool, len(sysUserRefFields))
for _, name := range sysUserRefFields {
getters[name] = userGetter
sysFields[name] = true
}
// Module fields take precedence over the system field spellings; only
// User kind fields resolve through the getter
for _, f := range mod.Fields {
if f.Kind == "User" {
getters[f.Name] = userGetter
} else {
delete(getters, f.Name)
}
delete(sysFields, f.Name)
}
return
}
func (e StoreEncoder) recordMaker(ns *types.Namespace, mod *types.Module, recordGetters map[string]*recordGetter, userGetters map[string]*systemEnvoy.UserGetter, sysUserFields map[string]bool) func(ctx context.Context, auxRec datasource.RawRecord) (r types.Record, err error) {
return func(ctx context.Context, auxRec datasource.RawRecord) (rec types.Record, err error) {
// Iterate mapified record values and populate the provided values
var auxvv types.RecordValueSet
for k, vv := range auxRec {
auxvv = nil
if rg := recordGetters[k]; rg != nil {
auxvv, err = resolveRefs(ctx, rg.resolve, k, vv.Values)
} else if ug := userGetters[k]; ug != nil {
vals := vv.Values
// system fields hold a single user; the first value wins
if sysUserFields[k] && len(vals) > 1 {
vals = vals[:1]
}
auxvv, err = resolveRefs(ctx, ug.Resolve, k, vals)
} else {
// Default is regular values
for i, v := range vv.Values {
err = rec.SetValue(k, uint(i), v)
if err != nil {
return
}
}
continue
}
if err != nil {
return
}
for _, auxv := range auxvv {
err = rec.SetValue(k, auxv.Place, auxv.Ref)
if err != nil {
return
}
}
}
return
}
}
// resolveRefs resolves reference values (user or record identifiers) into IDs
func resolveRefs(ctx context.Context, resolve func(context.Context, any) (uint64, error), k string, vals []string) (out types.RecordValueSet, err error) {
var (
ref uint64
place uint
)
for _, v := range vals {
// mini-csv decoding can produce empty entries; nothing to resolve
if strings.TrimSpace(v) == "" {
continue
}
ref, err = resolve(ctx, v)
if err != nil {
return
}
out = append(out, &types.RecordValue{
Name: k,
Place: place,
Ref: ref,
Value: cast.ToString(ref),
})
place++
}
return
}

View File

@@ -0,0 +1,50 @@
package envoy
import (
"fmt"
"time"
"github.com/cortezaproject/corteza/server/pkg/envoyx"
"github.com/spf13/cast"
)
// resolveTimezone reads the "timezone" param and returns the loaded location.
// Returns a nil location when no timezone is set. An unresolvable timezone is
// reported as an error so the export fails loudly instead of silently skipping
// the conversion.
func resolveTimezone(p envoyx.EncodeParams) (*time.Location, error) {
tz := cast.ToString(p.Params["timezone"])
if tz == "" {
return nil, nil
}
loc, err := time.LoadLocation(tz)
if err != nil {
return nil, fmt.Errorf("invalid export timezone %q: %w", tz, err)
}
return loc, nil
}
// formatInTimezone reparses v as a datetime and reformats it in loc.
// On parse failure returns v unchanged so values are never dropped.
func formatInTimezone(v string, loc *time.Location) string {
if loc == nil || v == "" {
return v
}
layouts := []string{time.RFC3339Nano, time.RFC3339, "2006-01-02 15:04:05.999999999 -0700 MST", "2006-01-02 15:04:05 -0700 MST", "2006-01-02T15:04:05Z"}
for _, ly := range layouts {
if t, err := time.Parse(ly, v); err == nil {
return t.In(loc).Format(time.RFC3339)
}
}
// Fall back to zone-less layouts; assume values are stored in UTC.
naiveLayouts := []string{"2006-01-02T15:04:05.999999999", "2006-01-02T15:04:05", "2006-01-02 15:04:05.999999999", "2006-01-02 15:04:05"}
for _, ly := range naiveLayouts {
if t, err := time.ParseInLocation(ly, v, time.UTC); err == nil {
return t.In(loc).Format(time.RFC3339)
}
}
return v
}

2571
server/compose/envoy/yaml_decode.gen.go generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,492 @@
package envoy
import (
"fmt"
"strings"
automationTypes "github.com/cortezaproject/corteza/server/automation/types"
"github.com/cortezaproject/corteza/server/compose/types"
"github.com/cortezaproject/corteza/server/pkg/envoyx"
"github.com/cortezaproject/corteza/server/pkg/y7s"
"github.com/modern-go/reflect2"
"gopkg.in/yaml.v3"
)
const (
ComposeRecordDatasourceAuxType = "corteza::compose:record-datasource"
)
func (d *auxYamlDoc) unmarshalYAML(k string, n *yaml.Node) (out envoyx.NodeSet, err error) { return }
func (d *auxYamlDoc) unmarshalChartConfigNode(r *types.Chart, n *yaml.Node) (refs map[string]envoyx.Ref, idents envoyx.Identifiers, err error) {
err = y7s.EachMap(n, func(k, v *yaml.Node) error {
if k.Value != "reports" {
return nil
}
if y7s.IsSeq(v) {
var (
auxRefs = make(map[string]envoyx.Ref)
auxIdents envoyx.Identifiers
i = -1
)
err = y7s.EachSeq(v, func(c *yaml.Node) error {
i++
auxRefs, auxIdents, err = d.unmarshalChartConfigReportNode(r, c, i)
refs = envoyx.MergeRefs(refs, auxRefs)
idents = idents.Merge(auxIdents)
return err
})
if err != nil {
return err
}
} else {
refs, idents, err = d.unmarshalChartConfigReportNode(r, v, 0)
return err
}
return nil
})
return
}
func (d *auxYamlDoc) unmarshalChartConfigReportNode(r *types.Chart, n *yaml.Node, index int) (refs map[string]envoyx.Ref, idents envoyx.Identifiers, err error) {
err = y7s.EachMap(n, func(k, v *yaml.Node) error {
switch strings.ToLower(k.Value) {
case "module", "mod", "moduleid", "module_id":
var auxi any
y7s.DecodeScalar(v, "moduleID", &auxi)
refs = map[string]envoyx.Ref{
fmt.Sprintf("Config.Reports.%d.ModuleID", index): {
ResourceType: types.ModuleResourceType,
Identifiers: envoyx.MakeIdentifiers(auxi),
},
}
}
return nil
})
return
}
func (d *auxYamlDoc) unmarshalPageBlocksNode(r *types.Page, n *yaml.Node) (refs map[string]envoyx.Ref, idents envoyx.Identifiers, err error) {
refs = map[string]envoyx.Ref{}
for index, b := range r.Blocks {
switch b.Kind {
case "RecordList":
refs = envoyx.MergeRefs(refs, getPageBlockRecordListRefs(b, index))
case "Automation":
refs = envoyx.MergeRefs(refs, getPageBlockAutomationRefs(b, index))
case "RecordOrganizer":
refs = envoyx.MergeRefs(refs, getPageBlockRecordOrganizerRefs(b, index))
case "Chart":
refs = envoyx.MergeRefs(refs, getPageBlockChartRefs(b, index))
case "Calendar":
refs = envoyx.MergeRefs(refs, getPageBlockCalendarRefs(b, index))
case "Metric":
refs = envoyx.MergeRefs(refs, getPageBlockMetricRefs(b, index))
case "Comment":
refs = envoyx.MergeRefs(refs, getPageBlockCommentRefs(b, index))
case "Progress":
refs = envoyx.MergeRefs(refs, getPageBlockProgressRefs(b, index))
}
}
return
}
func getPageBlockRecordListRefs(b types.PageBlock, index int) (refs map[string]envoyx.Ref) {
refs = make(map[string]envoyx.Ref)
id := optString(b.Options, "module", "moduleID")
if id == "" || id == "0" {
return
}
refs[fmt.Sprintf("Blocks.%d.Options.ModuleID", index)] = envoyx.Ref{
ResourceType: types.ModuleResourceType,
Identifiers: envoyx.MakeIdentifiers(id),
}
return
}
func getPageBlockChartRefs(b types.PageBlock, index int) (refs map[string]envoyx.Ref) {
refs = make(map[string]envoyx.Ref)
id := optString(b.Options, "chart", "chartID")
if id == "" || id == "0" {
return
}
refs[fmt.Sprintf("Blocks.%d.Options.ChartID", index)] = envoyx.Ref{
ResourceType: types.ChartResourceType,
Identifiers: envoyx.MakeIdentifiers(id),
}
return
}
func getPageBlockCalendarRefs(b types.PageBlock, index int) (refs map[string]envoyx.Ref) {
refs = make(map[string]envoyx.Ref)
ff, _ := b.Options["feeds"].([]interface{})
for j, f := range ff {
feed, _ := f.(map[string]interface{})
opt, _ := (feed["options"]).(map[string]interface{})
id := optString(opt, "module", "moduleID")
if id == "" || id == "0" {
return
}
refs[fmt.Sprintf("Blocks.%d.Options.feeds.%d.ModuleID", index, j)] = envoyx.Ref{
ResourceType: types.ModuleResourceType,
Identifiers: envoyx.MakeIdentifiers(id),
}
}
return
}
func getPageBlockMetricRefs(b types.PageBlock, index int) (refs map[string]envoyx.Ref) {
refs = make(map[string]envoyx.Ref)
mm, _ := b.Options["metrics"].([]interface{})
for j, m := range mm {
mops, _ := m.(map[string]interface{})
id := optString(mops, "module", "moduleID")
if id == "" || id == "0" {
return
}
refs[fmt.Sprintf("Blocks.%d.Options.metrics.%d.ModuleID", index, j)] = envoyx.Ref{
ResourceType: types.ModuleResourceType,
Identifiers: envoyx.MakeIdentifiers(id),
}
}
return
}
func getPageBlockCommentRefs(b types.PageBlock, index int) (refs map[string]envoyx.Ref) {
// Same difference
return getPageBlockRecordListRefs(b, index)
}
func getPageBlockProgressRefs(b types.PageBlock, index int) (refs map[string]envoyx.Ref) {
refs = make(map[string]envoyx.Ref)
var aux *envoyx.Ref
aux = getPageBlockProgressValueRefs(b.Options["minValue"])
if aux != nil {
refs[fmt.Sprintf("Blocks.%d.Options.minValue.ModuleID", index)] = *aux
}
aux = getPageBlockProgressValueRefs(b.Options["maxValue"])
if aux != nil {
refs[fmt.Sprintf("Blocks.%d.Options.maxValue.ModuleID", index)] = *aux
}
aux = getPageBlockProgressValueRefs(b.Options["value"])
if aux != nil {
refs[fmt.Sprintf("Blocks.%d.Options.value.ModuleID", index)] = *aux
}
return
}
func getPageBlockProgressValueRefs(val any) (ref *envoyx.Ref) {
if reflect2.IsNil(val) {
return
}
opt, ok := val.(map[string]interface{})
if !ok {
return
}
id := optString(opt, "module", "moduleID")
if id == "" || id == "0" {
return
}
return &envoyx.Ref{
ResourceType: types.ModuleResourceType,
Identifiers: envoyx.MakeIdentifiers(id),
}
}
func getPageBlockRecordOrganizerRefs(b types.PageBlock, index int) (refs map[string]envoyx.Ref) {
// Same difference
return getPageBlockRecordListRefs(b, index)
}
func getPageBlockAutomationRefs(b types.PageBlock, index int) (refs map[string]envoyx.Ref) {
refs = make(map[string]envoyx.Ref)
bb, _ := b.Options["buttons"].([]interface{})
for buttonIx, b := range bb {
button, _ := b.(map[string]interface{})
id := optString(button, "workflow", "workflowID")
if id == "" || id == "0" {
return
}
refs[fmt.Sprintf("Blocks.%d.Options.buttons.%d.WorkflowID", index, buttonIx)] = envoyx.Ref{
ResourceType: automationTypes.WorkflowResourceType,
Identifiers: envoyx.MakeIdentifiers(id),
}
}
return
}
func optString(opt map[string]interface{}, kk ...string) string {
for _, k := range kk {
if vr, has := opt[k]; has {
v, _ := vr.(string)
return v
}
}
return ""
}
func (d *auxYamlDoc) unmarshalModuleFieldOptionsNode(r *types.ModuleField, n *yaml.Node) (refs map[string]envoyx.Ref, idents envoyx.Identifiers, err error) {
refs = make(map[string]envoyx.Ref)
err = y7s.EachMap(n, func(k, v *yaml.Node) error {
switch strings.ToLower(k.Value) {
case "module", "module_id", "moduleid":
var aux any
err = y7s.DecodeScalar(v, "moduleID", &aux)
if err != nil {
return err
}
refs["Options.ModuleID"] = envoyx.Ref{
ResourceType: types.ModuleResourceType,
Identifiers: envoyx.MakeIdentifiers(aux),
}
default:
return nil
}
return nil
})
return
}
func (d *auxYamlDoc) unmarshalModuleFieldDefaultValueNode(r *types.ModuleField, n *yaml.Node) (refs map[string]envoyx.Ref, idents envoyx.Identifiers, err error) {
var rvs = types.RecordValueSet{}
switch n.Kind {
case yaml.ScalarNode:
rvs = rvs.Set(&types.RecordValue{Value: n.Value})
case yaml.SequenceNode:
_ = y7s.EachSeq(n, func(v *yaml.Node) error {
rvs = rvs.Set(&types.RecordValue{Value: n.Value, Place: uint(len(rvs))})
return nil
})
}
r.DefaultValue = rvs
return
}
func (d *auxYamlDoc) unmarshalModuleFieldExpressionsNode(r *types.ModuleField, n *yaml.Node) (refs map[string]envoyx.Ref, idents envoyx.Identifiers, err error) {
err = y7s.EachMap(n, func(k *yaml.Node, v *yaml.Node) error {
switch k.Value {
case "sanitizer":
var aux string
err = y7s.DecodeScalar(v, "sanitizer", &aux)
if err != nil {
return err
}
r.Expressions.Sanitizers = append(r.Expressions.Sanitizers, aux)
case "sanitizers":
return y7s.EachSeq(v, func(san *yaml.Node) error {
r.Expressions.Sanitizers = append(r.Expressions.Sanitizers, san.Value)
return nil
})
case "validator":
var aux types.ModuleFieldValidator
err = v.Decode(&aux)
if err != nil {
return err
}
r.Expressions.Validators = append(r.Expressions.Validators, aux)
case "validators":
return y7s.Each(v, func(k *yaml.Node, v *yaml.Node) error {
var aux types.ModuleFieldValidator
if y7s.IsKind(v, yaml.MappingNode) {
err = v.Decode(&aux)
if err != nil {
return err
}
} else {
aux.Test = k.Value
aux.Error = v.Value
}
r.Expressions.Validators = append(r.Expressions.Validators, aux)
return nil
})
case "formatter":
r.Expressions.Formatters = append(r.Expressions.Formatters, v.Value)
return nil
case "formatters":
return y7s.EachSeq(v, func(san *yaml.Node) error {
r.Expressions.Formatters = append(r.Expressions.Formatters, san.Value)
return nil
})
}
return nil
})
return
}
func (d *auxYamlDoc) postProcessNestedModuleNodes(nn envoyx.NodeSet) (out envoyx.NodeSet, err error) {
// Get all references from all module fields
refs := make(map[string]envoyx.Ref)
for _, n := range nn {
if n.ResourceType != types.ModuleFieldResourceType {
continue
}
if len(n.References) == 0 {
continue
}
r, ok := n.References["Options.ModuleID"]
if !ok {
continue
}
refs[fmt.Sprintf("%s.module", n.Identifiers.FriendlyIdentifier())] = r
refs[fmt.Sprintf("%s.datasource", n.Identifiers.FriendlyIdentifier())] = envoyx.Ref{
ResourceType: ComposeRecordDatasourceAuxType,
Identifiers: r.Identifiers,
Scope: r.Scope,
Optional: true,
}
}
// Update datasources with references
for _, n := range nn {
if n.Datasource == nil {
continue
}
n.References = envoyx.MergeRefs(n.References, refs)
}
out = nn
return
}
func (d *auxYamlDoc) unmarshalPagesExtendedNode(dctx documentContext, n *yaml.Node, meta ...*yaml.Node) (out envoyx.NodeSet, err error) {
return d.unmarshalPageNode(dctx, n, meta...)
}
func (d *auxYamlDoc) unmarshalSourceExtendedNode(dctx documentContext, n *yaml.Node, meta ...*yaml.Node) (out envoyx.NodeSet, err error) {
var r envoyx.DatasourceMapping
// @todo we're omitting errors because there will be a bunch due to invalid
// resource field types. This might be a bit unstable as other errors may
// also get ignored.
//
// A potential fix would be to firstly unmarshal into an any, check errors
// and then unmarshal into the resource while omitting errors.
n.Decode(&r)
err = y7s.EachMap(n, func(k, n *yaml.Node) error {
var auxNodeValue any
_ = auxNodeValue
switch strings.ToLower(k.Value) {
case "origin", "from":
err = y7s.DecodeScalar(n, "origin", &r.SourceIdent)
if err != nil {
return err
}
case "key", "index", "pk":
if !y7s.IsKind(n, yaml.SequenceNode) {
r.KeyField = []string{n.Value}
} else {
r.KeyField = make([]string, 0, 3)
y7s.EachSeq(n, func(n *yaml.Node) error {
r.KeyField = append(r.KeyField, n.Value)
return nil
})
}
case "map":
return n.Decode(&r.Mapping)
}
return nil
})
// @todo for now we only support record datasources; extend when needed
auxN := &envoyx.Node{
Datasource: &RecordDatasource{
Mapping: r,
},
ResourceType: ComposeRecordDatasourceAuxType,
Identifiers: dctx.parentIdent,
}
auxN.References, auxN.Scope = d.procMappingRefs(r.References)
out = append(out, auxN)
return
}
func (d *auxYamlDoc) procMappingRefs(in map[string]string) (out map[string]envoyx.Ref, scope envoyx.Scope) {
out = make(map[string]envoyx.Ref)
nsII := envoyx.MakeIdentifiers(in["namespace"])
if len(nsII.Slice) > 0 {
scope = envoyx.Scope{
ResourceType: types.NamespaceResourceType,
Identifiers: nsII,
}
}
if len(nsII.Slice) > 0 {
out["NamespaceID"] = envoyx.Ref{
ResourceType: types.NamespaceResourceType,
Identifiers: nsII,
Scope: scope,
}
}
modII := envoyx.MakeIdentifiers(in["module"])
if len(modII.Slice) > 0 {
out["ModuleID"] = envoyx.Ref{
ResourceType: types.ModuleResourceType,
Identifiers: envoyx.MakeIdentifiers(in["module"]),
Scope: scope,
}
}
return
}

721
server/compose/envoy/yaml_encode.gen.go generated Normal file
View File

@@ -0,0 +1,721 @@
package envoy
// This file is auto-generated.
//
// Changes to this file may cause incorrect behavior and will be lost if
// the code is regenerated.
//
import (
"context"
"io"
"time"
"github.com/cortezaproject/corteza/server/compose/types"
"github.com/cortezaproject/corteza/server/pkg/envoyx"
"github.com/cortezaproject/corteza/server/pkg/y7s"
"github.com/pkg/errors"
"gopkg.in/yaml.v3"
)
type (
// YamlEncoder is responsible for encoding Corteza resources into
// a YAML supported format
YamlEncoder struct{}
)
const (
paramsKeyWriter = "writer"
)
// Encode encodes the given Corteza resources into some YAML supported format
//
// Encoding should not do any additional processing apart from matching with
// dependencies and runtime validation
//
// Preparation runs validation, default value initialization, matching with
// already existing instances, ...
//
// The prepare function receives a set of nodes grouped by the resource type.
// This enables some batching optimization and simplifications when it comes to
// matching with existing resources.
//
// Prepare does not receive any placeholder nodes which are used solely
// for dependency resolution.
func (e YamlEncoder) Encode(ctx context.Context, p envoyx.EncodeParams, rt string, nodes envoyx.NodeSet, tt envoyx.Traverser) (err error) {
var (
out *yaml.Node
aux *yaml.Node
)
_ = aux
w, err := e.getWriter(p)
if err != nil {
return
}
switch rt {
case types.ChartResourceType:
aux, err = e.encodeCharts(ctx, p, nodes, tt)
if err != nil {
return
}
// Root level resources are always encoded as a map
out, err = y7s.AddMap(out, "chart", aux)
if err != nil {
return
}
case types.ModuleResourceType:
aux, err = e.encodeModules(ctx, p, nodes, tt)
if err != nil {
return
}
// Root level resources are always encoded as a map
out, err = y7s.AddMap(out, "module", aux)
if err != nil {
return
}
case types.ModuleFieldResourceType:
aux, err = e.encodeModuleFields(ctx, p, nodes, tt)
if err != nil {
return
}
// Root level resources are always encoded as a map
out, err = y7s.AddMap(out, "moduleField", aux)
if err != nil {
return
}
case types.NamespaceResourceType:
aux, err = e.encodeNamespaces(ctx, p, nodes, tt)
if err != nil {
return
}
// Root level resources are always encoded as a map
out, err = y7s.AddMap(out, "namespace", aux)
if err != nil {
return
}
case types.PageResourceType:
aux, err = e.encodePages(ctx, p, nodes, tt)
if err != nil {
return
}
// Root level resources are always encoded as a map
out, err = y7s.AddMap(out, "page", aux)
if err != nil {
return
}
case types.PageLayoutResourceType:
aux, err = e.encodePageLayouts(ctx, p, nodes, tt)
if err != nil {
return
}
// Root level resources are always encoded as a map
out, err = y7s.AddMap(out, "pageLayout", aux)
if err != nil {
return
}
default:
out, err = e.encode(ctx, out, p, rt, nodes, tt)
if err != nil {
return
}
}
// Don't output nil values since that will produce broken yaml docs
if out == nil {
return
}
return yaml.NewEncoder(w).Encode(out)
}
// // // // // // // // // // // // // // // // // // // // // // // // //
// Functions for resource chart
// // // // // // // // // // // // // // // // // // // // // // // // //
func (e YamlEncoder) encodeCharts(ctx context.Context, p envoyx.EncodeParams, nodes envoyx.NodeSet, tt envoyx.Traverser) (out *yaml.Node, err error) {
var aux *yaml.Node
for _, n := range nodes {
aux, err = e.encodeChart(ctx, p, n, tt)
if err != nil {
return
}
out, err = y7s.AddSeq(out, aux)
if err != nil {
return
}
}
return
}
// encodeChart focuses on the specific resource invoked by the Encode method
func (e YamlEncoder) encodeChart(ctx context.Context, p envoyx.EncodeParams, node *envoyx.Node, tt envoyx.Traverser) (out *yaml.Node, err error) {
res := node.Resource.(*types.Chart)
// Pre-compute some map values so we can omit error checking when encoding yaml nodes
auxConfig, err := e.encodeChartConfigC(ctx, p, tt, node, res, res.Config)
if err != nil {
return
}
auxCreatedAt, err := e.encodeTimestamp(p, res.CreatedAt)
if err != nil {
return
}
auxDeletedAt, err := e.encodeTimestampNil(p, res.DeletedAt)
if err != nil {
return
}
auxNamespaceID, err := e.encodeRef(p, res.NamespaceID, "NamespaceID", node, tt)
if err != nil {
return
}
auxUpdatedAt, err := e.encodeTimestampNil(p, res.UpdatedAt)
if err != nil {
return
}
out, err = y7s.AddMap(out,
"config", auxConfig,
"createdAt", auxCreatedAt,
"deletedAt", auxDeletedAt,
"handle", res.Handle,
"chartID", res.ID,
"name", res.Name,
"namespaceID", auxNamespaceID,
"updatedAt", auxUpdatedAt,
)
if err != nil {
return
}
// Handle nested resources
var aux *yaml.Node
_ = aux
return
}
// // // // // // // // // // // // // // // // // // // // // // // // //
// Functions for resource module
// // // // // // // // // // // // // // // // // // // // // // // // //
func (e YamlEncoder) encodeModules(ctx context.Context, p envoyx.EncodeParams, nodes envoyx.NodeSet, tt envoyx.Traverser) (out *yaml.Node, err error) {
var aux *yaml.Node
for _, n := range nodes {
aux, err = e.encodeModule(ctx, p, n, tt)
if err != nil {
return
}
out, err = y7s.AddSeq(out, aux)
if err != nil {
return
}
}
return
}
// encodeModule focuses on the specific resource invoked by the Encode method
func (e YamlEncoder) encodeModule(ctx context.Context, p envoyx.EncodeParams, node *envoyx.Node, tt envoyx.Traverser) (out *yaml.Node, err error) {
res := node.Resource.(*types.Module)
// Pre-compute some map values so we can omit error checking when encoding yaml nodes
auxCreatedAt, err := e.encodeTimestamp(p, res.CreatedAt)
if err != nil {
return
}
auxDeletedAt, err := e.encodeTimestampNil(p, res.DeletedAt)
if err != nil {
return
}
auxNamespaceID, err := e.encodeRef(p, res.NamespaceID, "NamespaceID", node, tt)
if err != nil {
return
}
auxUpdatedAt, err := e.encodeTimestampNil(p, res.UpdatedAt)
if err != nil {
return
}
out, err = y7s.AddMap(out,
"config", res.Config,
"createdAt", auxCreatedAt,
"deletedAt", auxDeletedAt,
"handle", res.Handle,
"moduleID", res.ID,
"meta", res.Meta,
"name", res.Name,
"namespaceID", auxNamespaceID,
"updatedAt", auxUpdatedAt,
)
if err != nil {
return
}
// Handle nested resources
var aux *yaml.Node
_ = aux
// When processing module fields, we need to filter out the ones that
// don't belong to this module
//
// @todo offload this to dependency resolution; this is a hack
children := tt.ChildrenForResourceType(node, types.ModuleFieldResourceType)
var proc envoyx.NodeSet
selfRef := node.ToRef()
for _, c := range children {
if c.References["ModuleID"].Equals(selfRef) {
proc = append(proc, c)
}
}
aux, err = e.encodeModuleFields(ctx, p, proc, tt)
if err != nil {
return
}
out, err = y7s.AddMap(out,
"moduleField", aux,
)
if err != nil {
return
}
return
}
// // // // // // // // // // // // // // // // // // // // // // // // //
// Functions for resource moduleField
// // // // // // // // // // // // // // // // // // // // // // // // //
func (e YamlEncoder) encodeModuleFields(ctx context.Context, p envoyx.EncodeParams, nodes envoyx.NodeSet, tt envoyx.Traverser) (out *yaml.Node, err error) {
var aux *yaml.Node
for _, n := range nodes {
aux, err = e.encodeModuleField(ctx, p, n, tt)
if err != nil {
return
}
out, err = y7s.AddSeq(out, aux)
if err != nil {
return
}
}
return
}
// encodeModuleField focuses on the specific resource invoked by the Encode method
func (e YamlEncoder) encodeModuleField(ctx context.Context, p envoyx.EncodeParams, node *envoyx.Node, tt envoyx.Traverser) (out *yaml.Node, err error) {
res := node.Resource.(*types.ModuleField)
// Pre-compute some map values so we can omit error checking when encoding yaml nodes
auxCreatedAt, err := e.encodeTimestamp(p, res.CreatedAt)
if err != nil {
return
}
auxDeletedAt, err := e.encodeTimestampNil(p, res.DeletedAt)
if err != nil {
return
}
auxModuleID, err := e.encodeRef(p, res.ModuleID, "ModuleID", node, tt)
if err != nil {
return
}
auxOptions, err := e.encodeModuleFieldOptionsC(ctx, p, tt, node, res, res.Options)
if err != nil {
return
}
auxUpdatedAt, err := e.encodeTimestampNil(p, res.UpdatedAt)
if err != nil {
return
}
out, err = y7s.AddMap(out,
"config", res.Config,
"createdAt", auxCreatedAt,
"defaultValue", res.DefaultValue,
"deletedAt", auxDeletedAt,
"expressions", res.Expressions,
"id", res.ID,
"kind", res.Kind,
"label", res.Label,
"moduleID", auxModuleID,
"multi", res.Multi,
"name", res.Name,
"options", auxOptions,
"place", res.Place,
"required", res.Required,
"updatedAt", auxUpdatedAt,
)
if err != nil {
return
}
// Handle nested resources
var aux *yaml.Node
_ = aux
return
}
// // // // // // // // // // // // // // // // // // // // // // // // //
// Functions for resource namespace
// // // // // // // // // // // // // // // // // // // // // // // // //
func (e YamlEncoder) encodeNamespaces(ctx context.Context, p envoyx.EncodeParams, nodes envoyx.NodeSet, tt envoyx.Traverser) (out *yaml.Node, err error) {
var aux *yaml.Node
for _, n := range nodes {
aux, err = e.encodeNamespace(ctx, p, n, tt)
if err != nil {
return
}
out, err = y7s.AddSeq(out, aux)
if err != nil {
return
}
}
return
}
// encodeNamespace focuses on the specific resource invoked by the Encode method
func (e YamlEncoder) encodeNamespace(ctx context.Context, p envoyx.EncodeParams, node *envoyx.Node, tt envoyx.Traverser) (out *yaml.Node, err error) {
res := node.Resource.(*types.Namespace)
// Pre-compute some map values so we can omit error checking when encoding yaml nodes
auxCreatedAt, err := e.encodeTimestamp(p, res.CreatedAt)
if err != nil {
return
}
auxDeletedAt, err := e.encodeTimestampNil(p, res.DeletedAt)
if err != nil {
return
}
auxUpdatedAt, err := e.encodeTimestampNil(p, res.UpdatedAt)
if err != nil {
return
}
out, err = y7s.AddMap(out,
"createdAt", auxCreatedAt,
"deletedAt", auxDeletedAt,
"enabled", res.Enabled,
"id", res.ID,
"meta", res.Meta,
"name", res.Name,
"slug", res.Slug,
"updatedAt", auxUpdatedAt,
)
if err != nil {
return
}
// Handle nested resources
var aux *yaml.Node
_ = aux
aux, err = e.encodeCharts(ctx, p, tt.ChildrenForResourceType(node, types.ChartResourceType), tt)
if err != nil {
return
}
out, err = y7s.AddMap(out,
"chart", aux,
)
if err != nil {
return
}
aux, err = e.encodeModules(ctx, p, tt.ChildrenForResourceType(node, types.ModuleResourceType), tt)
if err != nil {
return
}
out, err = y7s.AddMap(out,
"module", aux,
)
if err != nil {
return
}
aux, err = e.encodePages(ctx, p, tt.ChildrenForResourceType(node, types.PageResourceType), tt)
if err != nil {
return
}
out, err = y7s.AddMap(out,
"page", aux,
)
if err != nil {
return
}
return
}
// // // // // // // // // // // // // // // // // // // // // // // // //
// Functions for resource page
// // // // // // // // // // // // // // // // // // // // // // // // //
func (e YamlEncoder) encodePages(ctx context.Context, p envoyx.EncodeParams, nodes envoyx.NodeSet, tt envoyx.Traverser) (out *yaml.Node, err error) {
var aux *yaml.Node
for _, n := range nodes {
aux, err = e.encodePage(ctx, p, n, tt)
if err != nil {
return
}
out, err = y7s.AddSeq(out, aux)
if err != nil {
return
}
}
return
}
// encodePage focuses on the specific resource invoked by the Encode method
func (e YamlEncoder) encodePage(ctx context.Context, p envoyx.EncodeParams, node *envoyx.Node, tt envoyx.Traverser) (out *yaml.Node, err error) {
res := node.Resource.(*types.Page)
// Pre-compute some map values so we can omit error checking when encoding yaml nodes
auxBlocks, err := e.encodePageBlocksC(ctx, p, tt, node, res, res.Blocks)
if err != nil {
return
}
auxCreatedAt, err := e.encodeTimestamp(p, res.CreatedAt)
if err != nil {
return
}
auxDeletedAt, err := e.encodeTimestampNil(p, res.DeletedAt)
if err != nil {
return
}
auxModuleID, err := e.encodeRef(p, res.ModuleID, "ModuleID", node, tt)
if err != nil {
return
}
auxNamespaceID, err := e.encodeRef(p, res.NamespaceID, "NamespaceID", node, tt)
if err != nil {
return
}
auxSelfID, err := e.encodeRef(p, res.SelfID, "SelfID", node, tt)
if err != nil {
return
}
auxUpdatedAt, err := e.encodeTimestampNil(p, res.UpdatedAt)
if err != nil {
return
}
out, err = y7s.AddMap(out,
"blocks", auxBlocks,
"children", res.Children,
"config", res.Config,
"createdAt", auxCreatedAt,
"deletedAt", auxDeletedAt,
"description", res.Description,
"handle", res.Handle,
"id", res.ID,
"meta", res.Meta,
"moduleID", auxModuleID,
"namespaceID", auxNamespaceID,
"selfID", auxSelfID,
"title", res.Title,
"updatedAt", auxUpdatedAt,
"visible", res.Visible,
"weight", res.Weight,
)
if err != nil {
return
}
// Handle nested resources
var aux *yaml.Node
_ = aux
aux, err = e.encodePageLayouts(ctx, p, tt.ChildrenForResourceType(node, types.PageLayoutResourceType), tt)
if err != nil {
return
}
out, err = y7s.AddMap(out,
"pageLayout", aux,
)
if err != nil {
return
}
return
}
// // // // // // // // // // // // // // // // // // // // // // // // //
// Functions for resource pageLayout
// // // // // // // // // // // // // // // // // // // // // // // // //
func (e YamlEncoder) encodePageLayouts(ctx context.Context, p envoyx.EncodeParams, nodes envoyx.NodeSet, tt envoyx.Traverser) (out *yaml.Node, err error) {
var aux *yaml.Node
for _, n := range nodes {
aux, err = e.encodePageLayout(ctx, p, n, tt)
if err != nil {
return
}
out, err = y7s.AddSeq(out, aux)
if err != nil {
return
}
}
return
}
// encodePageLayout focuses on the specific resource invoked by the Encode method
func (e YamlEncoder) encodePageLayout(ctx context.Context, p envoyx.EncodeParams, node *envoyx.Node, tt envoyx.Traverser) (out *yaml.Node, err error) {
res := node.Resource.(*types.PageLayout)
// Pre-compute some map values so we can omit error checking when encoding yaml nodes
auxCreatedAt, err := e.encodeTimestamp(p, res.CreatedAt)
if err != nil {
return
}
auxDeletedAt, err := e.encodeTimestampNil(p, res.DeletedAt)
if err != nil {
return
}
auxNamespaceID, err := e.encodeRef(p, res.NamespaceID, "NamespaceID", node, tt)
if err != nil {
return
}
auxOwnedBy, err := e.encodeRef(p, res.OwnedBy, "OwnedBy", node, tt)
if err != nil {
return
}
auxPageID, err := e.encodeRef(p, res.PageID, "PageID", node, tt)
if err != nil {
return
}
auxParentID, err := e.encodeRef(p, res.ParentID, "ParentID", node, tt)
if err != nil {
return
}
auxUpdatedAt, err := e.encodeTimestampNil(p, res.UpdatedAt)
if err != nil {
return
}
out, err = y7s.AddMap(out,
"blocks", res.Blocks,
"config", res.Config,
"createdAt", auxCreatedAt,
"deletedAt", auxDeletedAt,
"handle", res.Handle,
"id", res.ID,
"meta", res.Meta,
"namespaceID", auxNamespaceID,
"ownedBy", auxOwnedBy,
"pageID", auxPageID,
"parentID", auxParentID,
"updatedAt", auxUpdatedAt,
"weight", res.Weight,
)
if err != nil {
return
}
// Handle nested resources
var aux *yaml.Node
_ = aux
return
}
// // // // // // // // // // // // // // // // // // // // // // // // //
// Encoding utils
// // // // // // // // // // // // // // // // // // // // // // // // //
func (e YamlEncoder) encodeTimestamp(p envoyx.EncodeParams, t time.Time) (any, error) {
if t.IsZero() {
return nil, nil
}
tz := p.Encoder.PreferredTimezone
if tz != "" {
tzL, err := time.LoadLocation(tz)
if err != nil {
return nil, err
}
t = t.In(tzL)
}
ly := p.Encoder.PreferredTimeLayout
if ly == "" {
ly = time.RFC3339
}
return t.Format(ly), nil
}
func (e YamlEncoder) encodeTimestampNil(p envoyx.EncodeParams, t *time.Time) (any, error) {
if t == nil {
return nil, nil
}
return e.encodeTimestamp(p, *t)
}
func (e YamlEncoder) encodeRef(p envoyx.EncodeParams, id uint64, field string, node *envoyx.Node, tt envoyx.Traverser) (any, error) {
parent := tt.ParentForRef(node, node.References[field])
// @todo should we panic instead?
// for now gracefully fallback to the ID
if parent == nil {
return id, nil
}
return parent.Identifiers.FriendlyIdentifier(), nil
}
// // // // // // // // // // // // // // // // // // // // // // // // //
// Utility functions
// // // // // // // // // // // // // // // // // // // // // // // // //
func (e YamlEncoder) getWriter(p envoyx.EncodeParams) (out io.Writer, err error) {
aux, ok := p.Params[paramsKeyWriter]
if ok {
out, ok = aux.(io.Writer)
if ok {
return
}
}
// @todo consider adding support for managing files from a location
err = errors.Errorf("YAML encoder expects a writer conforming to io.Writer interface")
return
}
func safeParentIdentifier(tt envoyx.Traverser, n *envoyx.Node, ref envoyx.Ref) (out string) {
aux := tt.ParentForRef(n, ref)
if aux == nil {
return ref.Identifiers.FriendlyIdentifier()
}
return aux.Identifiers.FriendlyIdentifier()
}

View File

@@ -0,0 +1,238 @@
package envoy
import (
"context"
"fmt"
"github.com/cortezaproject/corteza/server/compose/types"
"github.com/cortezaproject/corteza/server/pkg/envoyx"
"github.com/cortezaproject/corteza/server/pkg/y7s"
"github.com/modern-go/reflect2"
"gopkg.in/yaml.v3"
)
func (e YamlEncoder) encode(ctx context.Context, base *yaml.Node, p envoyx.EncodeParams, rt string, nodes envoyx.NodeSet, tt envoyx.Traverser) (out *yaml.Node, err error) {
return
}
func (e YamlEncoder) encodeChartConfigC(ctx context.Context, p envoyx.EncodeParams, tt envoyx.Traverser, n *envoyx.Node, chart *types.Chart, cfg types.ChartConfig) (_ any, err error) {
reports, _ := y7s.MakeSeq()
for i, r := range cfg.Reports {
modRef, ok := n.References[fmt.Sprintf("Config.Reports.%d.ModuleID", i)]
if !ok {
continue
}
r, err := y7s.MakeMap(
"filter", r.Filter,
"module", safeParentIdentifier(tt, n, modRef),
"metrics", r.Metrics,
"dimensions", r.Dimensions,
"yAxis", r.YAxis,
)
if err != nil {
return nil, err
}
reports, err = y7s.AddSeq(reports, r)
if err != nil {
return nil, err
}
}
return y7s.MakeMap(
"reports", reports,
"colorScheme", cfg.ColorScheme,
)
}
func (e YamlEncoder) encodeModuleFieldsC(ctx context.Context, p envoyx.EncodeParams, tt envoyx.Traverser, n *envoyx.Node, mod *types.Module, fields types.ModuleFieldSet) (_ any, err error) {
fn := tt.ChildrenForResourceType(n, types.ModuleFieldResourceType)
out, err := e.encodeModuleFields(ctx, p, fn, tt)
return out, err
}
func (e YamlEncoder) encodeModuleFieldOptionsC(ctx context.Context, p envoyx.EncodeParams, tt envoyx.Traverser, n *envoyx.Node, f *types.ModuleField, opt types.ModuleFieldOptions) (_ any, err error) {
if opt == nil {
opt = make(types.ModuleFieldOptions)
}
switch f.Kind {
case "Record":
modRef := n.References["Options.ModuleID"]
opt["module"] = safeParentIdentifier(tt, n, modRef)
delete(opt, "moduleID")
case "User":
aux := make([]string, 0, 2)
for i := range opt.Strings("roles") {
roleRef := n.References[fmt.Sprintf("Options.RoleID.%d", i)]
aux = append(aux, safeParentIdentifier(tt, n, roleRef))
}
opt["roles"] = aux
delete(opt, "role")
delete(opt, "roleID")
}
nopt, _ := y7s.MakeMap()
for k, v := range opt {
nopt, err = y7s.AddMap(nopt, k, v)
if err != nil {
return nil, err
}
}
return nopt, nil
}
func (e YamlEncoder) encodePageBlocksC(ctx context.Context, p envoyx.EncodeParams, tt envoyx.Traverser, n *envoyx.Node, pg *types.Page, bb types.PageBlocks) (_ any, err error) {
out, _ := y7s.MakeSeq()
var aux any
for i, b := range pg.Blocks {
aux, err = e.encodePageBlockC(ctx, p, tt, n, pg, i, b)
if err != nil {
return
}
out, err = y7s.AddSeq(out, aux)
if err != nil {
return
}
}
return out, nil
}
func (e YamlEncoder) encodePageBlockC(ctx context.Context, p envoyx.EncodeParams, tt envoyx.Traverser, n *envoyx.Node, pg *types.Page, index int, b types.PageBlock) (_ any, err error) {
switch b.Kind {
case "RecordList":
b = e.cleanupPageblockRecordList(b)
modRef := n.References[fmt.Sprintf("Blocks.%d.Options.ModuleID", index)]
b.Options["module"] = safeParentIdentifier(tt, n, modRef)
delete(b.Options, "moduleID")
break
case "RecordOrganizer":
modRef := n.References[fmt.Sprintf("Blocks.%d.Options.ModuleID", index)]
b.Options["module"] = safeParentIdentifier(tt, n, modRef)
delete(b.Options, "moduleID")
break
case "Chart":
chrRef := n.References[fmt.Sprintf("Blocks.%d.Options.ChartID", index)]
b.Options["chart"] = safeParentIdentifier(tt, n, chrRef)
delete(b.Options, "chartID")
break
case "Calendar":
ff, _ := b.Options["feeds"].([]interface{})
for i, f := range ff {
feed, _ := f.(map[string]interface{})
fOpts, _ := (feed["options"]).(map[string]interface{})
modRef := n.References[fmt.Sprintf("Blocks.%d.Options.feeds.%d.ModuleID", index, i)]
fOpts["module"] = safeParentIdentifier(tt, n, modRef)
delete(fOpts, "moduleID")
}
break
case "Automation":
bb, _ := b.Options["buttons"].([]interface{})
for i, b := range bb {
button, _ := b.(map[string]interface{})
if _, has := button["workflowID"]; !has {
continue
}
wfRef := n.References[fmt.Sprintf("Blocks.%d.Options.buttons.%d.WorkflowID", index, i)]
button["workflow"] = safeParentIdentifier(tt, n, wfRef)
delete(button, "workflowID")
i++
}
break
case "Metric":
mm, _ := b.Options["metrics"].([]interface{})
for i, m := range mm {
modRef := n.References[fmt.Sprintf("Blocks.%d.Options.metrics.%d.ModuleID", index, i)]
mops, _ := m.(map[string]interface{})
mops["module"] = safeParentIdentifier(tt, n, modRef)
delete(mops, "moduleID")
}
break
case "Comment":
modRef := n.References[fmt.Sprintf("Blocks.%d.Options.ModuleID", index)]
b.Options["module"] = safeParentIdentifier(tt, n, modRef)
delete(b.Options, "moduleID")
break
case "Progress":
err = e.encodeProgressPageblockVal("minValue", index, n, tt, &b)
if err != nil {
return
}
err = e.encodeProgressPageblockVal("maxValue", index, n, tt, &b)
if err != nil {
return
}
err = e.encodeProgressPageblockVal("value", index, n, tt, &b)
if err != nil {
return
}
break
}
return b, nil
}
func (e YamlEncoder) encodeProgressPageblockVal(k string, index int, n *envoyx.Node, tt envoyx.Traverser, b *types.PageBlock) (err error) {
if reflect2.IsNil(b.Options[k]) {
return
}
modRef := n.References[fmt.Sprintf("Blocks.%d.Options.%s.ModuleID", index, k)]
opt := b.Options[k].(map[string]any)
opt["moduleID"] = safeParentIdentifier(tt, n, modRef)
delete(opt, "moduleID")
return
}
func (e YamlEncoder) cleanupPageblockRecordList(b types.PageBlock) (out types.PageBlock) {
out = b
rawFF, has := out.Options["fields"]
if !has {
return
}
ff, ok := rawFF.([]interface{})
if !ok {
return
}
retFF := make([]interface{}, 0, len(ff))
for _, rawF := range ff {
switch c := rawF.(type) {
case string:
retFF = append(retFF, map[string]interface{}{"name": c})
case map[string]interface{}, map[string]string:
retFF = append(retFF, c)
default:
retFF = append(retFF, rawF)
}
}
out.Options["fields"] = retFF
return b
}

26
server/compose/model/init.gen.go generated Normal file
View File

@@ -0,0 +1,26 @@
package model
// This file is auto-generated.
//
// Changes to this file may cause incorrect behavior and will be lost if
// the code is regenerated.
//
import (
"context"
"github.com/cortezaproject/corteza/server/pkg/dal"
)
type (
modelReplacer interface {
ReplaceModel(ctx context.Context, model *dal.Model) (err error)
}
)
var (
models []*dal.Model
)
func Models() dal.ModelSet {
return models
}

1203
server/compose/model/models.gen.go generated Normal file

File diff suppressed because it is too large Load Diff

172
server/compose/module.cue Normal file
View File

@@ -0,0 +1,172 @@
package compose
import (
"github.com/cortezaproject/corteza/server/codegen/schema"
)
module: {
handle: "module"
parents: [
{handle: "namespace"},
]
model: {
ident: "compose_module"
attributes: {
id: schema.IdField & {
envoy: {
yaml: {
identKeyEncode: "moduleID"
}
}
}
namespace_id: {
ident: "namespaceID",
goType: "uint64",
storeIdent: "rel_namespace"
dal: { type: "Ref", refModelResType: "corteza::compose:namespace" }
envoy: {
yaml: {
identKeyAlias: ["namespace", "namespace_id", "ns", "ns_id"]
}
}
}
handle: schema.HandleField
name: {
sortable: true
dal: {}
}
meta: {
goType: "rawJson"
dal: { type: "JSON", defaultEmptyObject: true }
omitSetter: true
omitGetter: true
}
config: {
goType: "types.ModuleConfig"
dal: { type: "JSON", defaultEmptyObject: true }
omitSetter: true
omitGetter: true
}
fields: {
goType: "types.ModuleFieldSet",
store: false
omitSetter: true
omitGetter: true
envoy: {
yaml: {
omitEncoder: true
}
}
}
created_at: schema.SortableTimestampNowField
updated_at: schema.SortableTimestampNilField
deleted_at: schema.SortableTimestampNilField
}
indexes: {
"primary": { attribute: "id" }
"namespace": { attribute: "namespace_id" },
"unique_handle": {
fields: [{ attribute: "handle", modifiers: ["LOWERCASE"] }, { attribute: "namespace_id" }]
predicate: "handle != '' AND deleted_at IS NULL"
}
}
}
filter: {
struct: {
module_id: { goType: "[]uint64", ident: "moduleID", storeIdent: "id" }
namespace_id: { goType: "uint64", ident: "namespaceID", storeIdent: "rel_namespace" }
handle: { goType: "string" }
name: { goType: "string" }
deleted: { goType: "filter.State", storeIdent: "deleted_at" }
}
query: ["handle", "name"]
byValue: ["handle", "module_id", "namespace_id"]
byNilState: ["deleted"]
}
envoy: {
scoped: true
yaml: {
supportMappedInput: true
mappedField: "Handle"
identKeyAlias: ["modules", "mod"]
extendedResourcePostProcess: true
extendedResourceDecoders: [{
ident: "source"
expIdent: "Source"
// @deprecated records is what the old version used
identKeys: ["source", "datasource", "records"]
supportMappedInput: false
}]
}
store: {
postSetEncoder: true
extendedEncoder: true
extendedFilterBuilder: true
extendedDecoder: true
}
}
rbac: {
operations: {
"read": {}
"update": {}
"delete": {}
"record.create": description: "Create record"
"owned-record.create": description: "Create record with custom owner"
"records.search": description: "List, search or filter records"
}
}
store: {
ident: "composeModule"
api: {
lookups: [
{
fields: ["namespace_id", "handle"]
constraintCheck: true
nullConstraint: ["deleted_at"]
description: """
searches for compose module by handle (case-insensitive)
"""
}, {
fields: ["namespace_id", "name"]
nullConstraint: ["deleted_at"]
description: """
searches for compose module by name (case-insensitive)
"""
}, {
fields: ["id"]
description: """
searches for compose module by ID
It returns compose module even if deleted
"""
},
]
}
}
locale: {
extended: true
keys: {
"name": {}
}
}
//locale:
// resource:
// references: [ namespace, ID ]
//
// extended: true
// keys:
// - name
}

View File

@@ -0,0 +1,212 @@
package compose
import (
"github.com/cortezaproject/corteza/server/codegen/schema"
)
moduleField: {
parents: [
{handle: "namespace"},
{handle: "module"},
]
model: {
defaultSetter: true
ident: "compose_module_field"
attributes: {
id: schema.IdField & {
envoy: {
identifier: true
}
}
module_id: {
ident: "moduleID",
goType: "uint64",
storeIdent: "rel_module"
dal: { type: "Ref", refModelResType: "corteza::compose:module" }
}
place: {
sortable: true,
goType: "int"
dal: { type: "Number", meta: { "rdbms:type": "integer" } }
}
kind: {
sortable: true,
goType: "string"
dal: {}
}
options: {
goType: "types.ModuleFieldOptions"
dal: { type: "JSON", defaultEmptyObject: true }
omitSetter: true
omitGetter: true
envoy: {
yaml: {
customDecoder: true
customEncoder: true
}
}
}
name: {
sortable: true
dal: {}
} & {
envoy: {
identifier: true
}
}
label: {
sortable: true
dal: {}
}
config: {
goType: "types.ModuleFieldConfig"
dal: { type: "JSON", defaultEmptyObject: true }
omitSetter: true
omitGetter: true
}
required: {
goType: "bool",
storeIdent: "is_required"
dal: { type: "Boolean" }
}
multi: {
goType: "bool",
storeIdent: "is_multi"
dal: { type: "Boolean" }
}
default_value: {
goType: "types.RecordValueSet"
dal: { type: "JSON", defaultEmptyObject: true }
omitSetter: true
omitGetter: true
envoy: {
yaml: {
customDecoder: true
}
}
}
expressions: {
goType: "types.ModuleFieldExpr"
dal: { type: "JSON", defaultEmptyObject: true }
omitSetter: true
omitGetter: true
envoy: {
yaml: {
customDecoder: true
}
}
}
created_at: schema.SortableTimestampNowField
updated_at: schema.SortableTimestampNilField
deleted_at: schema.SortableTimestampNilField
}
indexes: {
"primary": { attribute: "id" }
"module": { attribute: "module_id" },
"unique_name": {
fields: [{ attribute: "name", modifiers: ["LOWERCASE"] }, { attribute: "module_id" }]
predicate: "name != '' AND deleted_at IS NULL"
}
}
}
filter: {
struct: {
module_id: { goType: "[]uint64", ident: "moduleID", storeIdent: "rel_module" }
deleted: { goType: "filter.State", storeIdent: "deleted_at" }
}
byNilState: ["deleted"]
byValue: ["module_id"]
}
features: {
labels: false
paging: false
sorting: false
checkFn: false
}
envoy: {
scoped: true
yaml: {
supportMappedInput: true
mappedField: "Name"
identKeyAlias: ["module_fields", "modulefields", "fields"]
}
store: {
handleField: ""
customFilterBuilder: true
extendedRefDecoder: true
sanitizeBeforeSave: true
}
}
rbac: {
operations: {
"record.value.read": description: "Read field value on records"
"record.value.update": description: "Update field value on records"
}
}
locale: {
skipSvc: true
keys: {
label: {}
descriptionView: {
path: ["meta", "description", "view"]
customHandler: true
}
descriptionEdit: {
path: ["meta", "description", "edit"]
customHandler: true
}
hintView: {
path: ["meta", "hint", "view"]
customHandler: true
}
hintEdit: {
path: ["meta", "hint", "edit"]
customHandler: true
}
validatorError: {
path: ["expression", "validator", {part: "validatorID", var: true}, "error"]
customHandler: true
}
optionsOptionTexts: {
path: ["meta", "options", {part: "value", var: true}, "text"]
customHandler: true
}
optionsBoolLabels: {
path: ["meta", "bool", {part: "value", var: true}, "label"]
customHandler: true
}
}
}
store: {
ident: "composeModuleField"
api: {
lookups: [
{
fields: ["module_id", "name"]
constraintCheck: true
nullConstraint: ["deleted_at"]
description: """
searches for compose module field by name (case-insensitive)
"""
}, {
fields: ["id"]
description: """
searches for compose module field by ID
"""
}
]
}
}
}

View File

@@ -0,0 +1,128 @@
package compose
import (
"github.com/cortezaproject/corteza/server/codegen/schema"
)
namespace: {
model: {
ident: "compose_namespace"
attributes: {
id: schema.IdField
slug: {
sortable: true,
goType: "string"
dal: {}
envoy: {
identifier: true
}
}
enabled: {
goType: "bool"
dal: { type: "Boolean" }
}
meta: {
goType: "types.NamespaceMeta"
dal: { type: "JSON", defaultEmptyObject: true }
omitSetter: true
omitGetter: true
}
name: {
sortable: true
dal: {}
}
created_at: schema.SortableTimestampNowField
updated_at: schema.SortableTimestampNilField
deleted_at: schema.SortableTimestampNilField
}
indexes: {
"primary": { attribute: "id" }
"unique_handle": {
fields: [{ attribute: "slug", modifiers: ["LOWERCASE"] }]
predicate: "slug != '' AND deleted_at IS NULL"
}
}
}
filter: {
struct: {
namespace_id: { goType: "[]uint64", ident: "namespaceID", storeIdent: "id" }
slug: { goType: "string" }
name: { goType: "string" }
deleted: { goType: "filter.State", storeIdent: "deleted_at" }
}
query: ["name", "slug"]
byValue: ["namespace_id", "name", "slug"]
byNilState: ["deleted"]
}
envoy: {
scoped: true
yaml: {
supportMappedInput: true
mappedField: "Slug"
identKeyAlias: ["namespaces", "ns"]
}
store: {
handleField: "Slug"
extendedFilterBuilder: true
}
}
rbac: {
operations: {
"read": {}
"update": {}
"delete": {}
"export": description: "Access to export the entire namespace"
"manage": description: "Access to namespace admin panel"
"module.create": description: "Create module on namespace"
"modules.search": description: "List, search or filter module on namespace"
"modules.export": description: "Export modules on namespace"
"chart.create": description: "Create chart on namespace"
"charts.search": description: "List, search or filter chart on namespace"
"charts.export": description: "Export charts on namespace"
"page.create": description: "Create page on namespace"
"pages.search": description: "List, search or filter pages on namespace"
}
}
locale: {
keys: {
name: {}
metaSubtitle: {
path: ["meta", "subtitle"]
}
metaDescription: {
path: ["meta", "description"]
}
}
}
store: {
ident: "composeNamespace"
api: {
lookups: [
{
fields: ["slug"]
constraintCheck: true
nullConstraint: ["deleted_at"]
description: """
searches for namespace by slug (case-insensitive)
"""
}, {
fields: ["id"]
description: """
searches for compose namespace by ID
It returns compose namespace even if deleted
"""
},
]
}
}
}

238
server/compose/page.cue Normal file
View File

@@ -0,0 +1,238 @@
package compose
import (
"github.com/cortezaproject/corteza/server/codegen/schema"
)
page: {
parents: [
{handle: "namespace"},
]
model: {
defaultSetter: true
ident: "compose_page"
attributes: {
id: schema.IdField
title: {
goType: "string",
sortable: true
dal: {}
}
handle: schema.HandleField
self_id: {
ident: "selfID",
goType: "uint64",
dal: { type: "Ref", refModelResType: "corteza::compose:page" }
sortable: true
envoy: {
store: {
filterRefField: "ParentID"
}
yaml: {
identKeyAlias: ["parent"]
}
}
}
module_id: {
ident: "moduleID",
goType: "uint64",
storeIdent: "rel_module"
dal: { type: "Ref", refModelResType: "corteza::compose:module" }
envoy: {
yaml: {
identKeyAlias: ["module"]
}
}
}
namespace_id: {
ident: "namespaceID",
goType: "uint64",
storeIdent: "rel_namespace"
dal: { type: "Ref", refModelResType: "corteza::compose:namespace" }
envoy: {
yaml: {
identKeyAlias: ["namespace"]
}
}
}
meta: {
goType: "types.PageMeta"
dal: { type: "JSON", defaultEmptyObject: true }
omitSetter: true
omitGetter: true
}
config: {
goType: "types.PageConfig"
dal: { type: "JSON", defaultEmptyObject: true }
omitSetter: true
omitGetter: true
}
blocks: {
goType: "types.PageBlocks"
dal: { type: "JSON", defaultEmptyObject: true }
omitSetter: true
omitGetter: true
envoy: {
yaml: {
customDecoder: true
customEncoder: true
}
}
}
children: {
goType: "types.PageSet", store: false
omitSetter: true
omitGetter: true
}
visible: {
goType: "bool"
dal: { type: "Boolean", default: true }
}
weight: {
goType: "int", sortable: true
dal: { type: "Number", default: 0, meta: { "rdbms:type": "integer" } }
envoy: {
yaml: {
identKeyAlias: ["order"]
}
}
}
description: {
goType: "string"
dal: {}
}
created_at: schema.SortableTimestampNowField
updated_at: schema.SortableTimestampNilField
deleted_at: schema.SortableTimestampNilField
}
indexes: {
"primary": { attribute: "id" }
"namespace": { attribute: "namespace_id" },
"module": { attribute: "module_id" },
"self_id": { attribute: "self_id" },
"unique_handle": {
fields: [{ attribute: "handle", modifiers: ["LOWERCASE"] }, { attribute: "namespace_id" }]
predicate: "handle != '' AND deleted_at IS NULL"
}
}
}
filter: {
struct: {
page_id: { goType: "[]uint64", ident: "pageID", storeIdent: "id" }
namespace_id: { goType: "uint64", ident: "namespaceID", storeIdent: "rel_namespace" }
parent_id: { goType: "uint64", ident: "parentID" }
module_id: { goType: "uint64", ident: "moduleID", storeIdent: "rel_module" }
root: { goType: "bool" }
handle: { goType: "string" }
title: { goType: "string" }
deleted: { goType: "filter.State", storeIdent: "deleted_at" }
}
query: ["handle", "title", "description"]
byValue: ["page_id", "handle", "namespace_id", "module_id"]
byNilState: ["deleted"]
}
envoy: {
scoped: true
yaml: {
supportMappedInput: true
mappedField: "Handle"
identKeyAlias: ["pages", "pg"]
extendedResourceDecoders: [{
ident: "pages"
expIdent: "Pages"
identKeys: ["children", "pages"]
supportMappedInput: true
mappedField: "Handle"
}]
extendedResourceRefIdent: "SelfID"
}
store: {
extendedFilterBuilder: true
extendedRefDecoder: true
}
}
rbac: {
operations: {
"read": {}
"update": {}
"delete": {}
"page-layout.create": description: "Create page layout on namespace"
"page-layouts.search": description: "List, search or filter page layouts on namespace"
}
}
locale: {
extended: true
keys: {
title: {}
description: {}
blockTitle: {
path: ["pageBlock", {part: "blockID", var: true}, "title"]
customHandler: true
}
blockDescription: {
path: ["pageBlock", {part: "blockID", var: true}, "description"]
customHandler: true
}
blockAutomationButtonLabel: {
path: ["pageBlock", {part: "blockID", var: true}, "button", {part: "buttonID", var: true}, "label"]
customHandler: true
}
blockContentBody: {
path: ["pageBlock", {part: "blockID", var: true}, "content", "body"]
customHandler: true
}
}
}
store: {
ident: "composePage"
api: {
lookups: [
{
fields: ["namespace_id", "handle"]
nullConstraint: ["deleted_at"]
description: """
searches for page by handle (case-insensitive)
"""
}, {
fields: ["namespace_id", "module_id"]
nullConstraint: ["deleted_at"]
description: """
searches for page by moduleID
"""
}, {
fields: ["id"]
description: """
searches for compose page by ID
It returns compose page even if deleted
"""
},
]
functions: [
{
expIdent: "ReorderComposePages"
args: [
{ ident: "namespace_id", goType: "uint64" },
{ ident: "parent_id", goType: "uint64" },
{ ident: "page_ids", goType: "[]uint64" }
]
}
]
}
}
}

View File

@@ -0,0 +1,215 @@
package compose
import (
"github.com/cortezaproject/corteza/server/codegen/schema"
)
pageLayout: {
parents: [
{handle: "namespace"},
{handle: "page"},
]
model: {
ident: "compose_page_layout"
defaultGetter: true
defaultSetter: true
attributes: {
id: schema.IdField
handle: schema.HandleField
page_id: {
ident: "pageID",
goType: "uint64",
dal: { type: "Ref", refModelResType: "corteza::compose:page" }
sortable: true
envoy: {
yaml: {
identKeyAlias: ["page"]
}
}
}
parent_id: {
ident: "parentID",
goType: "uint64",
dal: { type: "Ref", refModelResType: "corteza::compose:page-layout" }
sortable: true
envoy: {
yaml: {
identKeyAlias: ["parent"]
}
}
}
namespace_id: {
ident: "namespaceID",
goType: "uint64",
storeIdent: "rel_namespace"
dal: { type: "Ref", refModelResType: "corteza::compose:namespace" }
envoy: {
yaml: {
identKeyAlias: ["namespace"]
}
}
}
weight: {
goType: "int", sortable: true
dal: { type: "Number", default: 0, meta: { "rdbms:type": "integer" } }
}
meta: {
goType: "types.PageLayoutMeta"
dal: { type: "JSON", defaultEmptyObject: true }
omitSetter: true
omitGetter: true
}
config: {
goType: "types.PageLayoutConfig"
dal: { type: "JSON", defaultEmptyObject: true }
omitSetter: true
omitGetter: true
}
blocks: {
goType: "types.PageLayoutBlocks"
dal: { type: "JSON", defaultEmptyObject: true }
omitSetter: true
omitGetter: true
}
owned_by: schema.AttributeUserRef
created_at: schema.SortableTimestampNowField
updated_at: schema.SortableTimestampNilField
deleted_at: schema.SortableTimestampNilField
}
indexes: {
"primary": { attribute: "id" }
"namespace": { attribute: "namespace_id" },
"page_id": { attribute: "page_id" },
"parent_id": { attribute: "parent_id" },
"unique_handle": {
fields: [{ attribute: "handle", modifiers: ["LOWERCASE"] }, { attribute: "page_id" }, { attribute: "namespace_id" }]
predicate: "handle != '' AND deleted_at IS NULL"
}
}
}
filter: {
struct: {
page_layout_id: { goType: "[]uint64", ident: "pageLayoutID", storeIdent: "id" }
namespace_id: { goType: "uint64", ident: "namespaceID", storeIdent: "rel_namespace" }
page_id: { goType: "uint64", ident: "pageID", storeIdent: "page_id" }
parent_id: { goType: "uint64", ident: "parentID", storeIdent: "parent_id" }
default: { goType: "bool", ident: "default" }
handle: { goType: "string" }
deleted: { goType: "filter.State", storeIdent: "deleted_at" }
}
query: ["handle"]
byValue: ["handle", "parent_id", "namespace_id", "page_id", "page_layout_id"]
byNilState: ["deleted"]
}
envoy: {
scoped: true
yaml: {
supportMappedInput: true
mappedField: "Handle"
identKeyAlias: ["page_layouts", "pagelayouts", "layouts"]
}
store: {
}
}
rbac: {
operations: {
"read": {}
"update": {}
"delete": {}
}
}
locale: {
extended: true
keys: {
title: {
path: ["meta", "title"]
}
description: {
path: ["meta", "description"]
}
recordToolbarButtonNewLabel: {
path: ["config", "buttons", "new", "label"]
customHandler: true
}
recordToolbarButtonEditLabel: {
path: ["config", "buttons", "edit", "label"]
customHandler: true
}
recordToolbarButtonSubmitLabel: {
path: ["config", "buttons", "submit", "label"]
customHandler: true
}
recordToolbarButtonDeleteLabel: {
path: ["config", "buttons", "delete", "label"]
customHandler: true
}
recordToolbarButtonCloneLabel: {
path: ["config", "buttons", "clone", "label"]
customHandler: true
}
recordToolbarButtonBackLabel: {
path: ["config", "buttons", "back", "label"]
customHandler: true
}
actionLabel: {
path: ["config", "actions", {part: "actionID", var: true}, "meta", "label"]
customHandler: true
}
}
}
store: {
ident: "composePageLayout"
api: {
lookups: [
{
fields: ["namespace_id", "handle"]
nullConstraint: ["deleted_at"]
description: """
searches for page layour by handle (case-insensitive)
"""
}, {
fields: ["namespace_id", "page_id", "handle"]
nullConstraint: ["deleted_at"]
description: """
searches for page layour by handle (case-insensitive)
"""
}, {
fields: ["id"]
description: """
searches for compose page layour by ID
It returns compose page layour even if deleted
"""
},
]
functions: [
{
expIdent: "ReorderComposePageLayouts"
args: [
{ ident: "namespace_id", goType: "uint64" },
{ ident: "page_id", goType: "uint64" },
{ ident: "page_layout_ids", goType: "[]uint64" }
]
}
]
}
}
}

106
server/compose/record.cue Normal file
View File

@@ -0,0 +1,106 @@
package compose
import (
"github.com/cortezaproject/corteza/server/codegen/schema"
)
record: {
parents: [
{handle: "namespace"},
{handle: "module"},
]
model: {
ident: "compose_record"
defaultSetter: true
defaultGetter: true
attributes: {
id: schema.IdField
revision: {
goType: "int"
dal: { type: "Number", meta: { "rdbms:type": "integer" }, default: 0 }
}
module_id: {
ident: "moduleID",
goType: "uint64",
storeIdent: "rel_module"
dal: { type: "Ref", refModelResType: "corteza::compose:module" }
}
module: {
goType: "*types.Module",
store: false
omitSetter: true
omitGetter: true
}
values: {
goType: "types.RecordValueSet",
dal: { type: "JSON", defaultEmptyObject: true }
omitSetter: true
omitGetter: true
}
meta: {
goType: "map[string]any",
dal: { type: "JSON", defaultEmptyObject: true }
}
namespace_id: {
ident: "namespaceID",
goType: "uint64",
storeIdent: "rel_namespace"
dal: { type: "Ref", refModelResType: "corteza::compose:namespace" }
}
created_at: schema.SortableTimestampNowField
updated_at: schema.SortableTimestampNilField
deleted_at: schema.SortableTimestampNilField
owned_by: schema.AttributeUserRef & {
identAlias: ["ownedBy", "OwnedBy", "owned_by"]
}
created_by: schema.AttributeUserRef & {
identAlias: ["createdBy", "CreatedBy", "created_by"]
}
updated_by: schema.AttributeUserRef & {
identAlias: ["updatedBy", "UpdatedBy", "updated_by"]
}
deleted_by: schema.AttributeUserRef & {
identAlias: ["deletedBy", "DeletedBy", "deleted_by"]
}
}
indexes: {
"primary": { attribute: "id" }
"idx_compose_record_base": {
attributes: ["module_id", "namespace_id"]
predicate: "deleted_at IS NULL"
}
}
}
// @todo tmp
envoy: {
omit: true
}
filter: {
struct: {
module_id: { goType: "uint64" }
namespace_id: { goType: "uint64" }
query: { goType: "string" }
deleted: { goType: "filter.State", storeIdent: "deleted_at" }
}
byNilState: ["deleted"]
}
rbac: {
operations: {
"read": {}
"update": {}
"delete": {}
"undelete": {}
"owner.manage": {}
"revisions.search": {}
}
}
}

View File

@@ -0,0 +1,45 @@
package compose
import (
"github.com/cortezaproject/corteza/server/codegen/schema"
)
record_revision: {
model: {
ident: "compose_record_revisions"
omitGetterSetter: true
attributes: {
id: schema.IdField
timestamp: schema.SortableTimestampField & { storeIdent: "ts" }
rel_resource: {
ident: "resourceID",
goType: "uint64",
dal: { type: "ID" }
}
revision: {
goType: "uint"
dal: { type: "Number", meta: { "rdbms:type": "integer" } }
}
operation: {
dal: {}
}
rel_user: schema.AttributeUserRef
delta: {
goType: "types.RecordValueSet",
dal: { type: "JSON", defaultEmptyObject: true }
}
comment: {
dal: {}
}
}
indexes: {
"primary": { attribute: "id" }
}
}
envoy: {
omit: true
}
}

1783
server/compose/rest.yaml Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,199 @@
package rest
import (
"context"
"fmt"
"github.com/cortezaproject/corteza/server/pkg/errors"
"io"
"net/http"
"net/url"
"github.com/cortezaproject/corteza/server/compose/rest/request"
"github.com/cortezaproject/corteza/server/compose/service"
"github.com/cortezaproject/corteza/server/compose/types"
"github.com/cortezaproject/corteza/server/pkg/api"
"github.com/cortezaproject/corteza/server/pkg/auth"
)
type (
attachmentPayload struct {
*types.Attachment
}
attachmentSetPayload struct {
Filter types.AttachmentFilter `json:"filter"`
Set []*attachmentPayload `json:"set"`
}
Attachment struct {
attachment service.AttachmentService
}
)
func (Attachment) New() *Attachment {
return &Attachment{
attachment: service.DefaultAttachment,
}
}
// Attachments returns list of all files attached to records
func (ctrl Attachment) List(ctx context.Context, r *request.AttachmentList) (interface{}, error) {
if !auth.GetIdentityFromContext(ctx).Valid() {
return nil, errors.Unauthorized("cannot list attachments")
}
f := types.AttachmentFilter{
NamespaceID: r.NamespaceID,
Kind: r.Kind,
ModuleID: r.ModuleID,
RecordID: r.RecordID,
FieldName: r.FieldName,
}
set, filter, err := ctrl.attachment.Find(ctx, f)
return ctrl.makeFilterPayload(ctx, set, filter, err)
}
func (ctrl Attachment) Read(ctx context.Context, r *request.AttachmentRead) (interface{}, error) {
if !auth.GetIdentityFromContext(ctx).Valid() {
return nil, errors.Unauthorized("cannot read attachment")
}
a, err := ctrl.attachment.FindByID(ctx, r.NamespaceID, r.AttachmentID)
return makeAttachmentPayload(ctx, a, err)
}
func (ctrl Attachment) Delete(ctx context.Context, r *request.AttachmentDelete) (interface{}, error) {
if !auth.GetIdentityFromContext(ctx).Valid() {
return nil, errors.Unauthorized("cannot delete attachment")
}
_, err := ctrl.attachment.FindByID(ctx, r.NamespaceID, r.AttachmentID)
if err != nil {
return nil, err
}
return api.OK(), ctrl.attachment.DeleteByID(ctx, r.NamespaceID, r.AttachmentID)
}
func (ctrl Attachment) Original(ctx context.Context, r *request.AttachmentOriginal) (interface{}, error) {
if err := ctrl.isAccessible(r.Kind, r.NamespaceID, r.AttachmentID, r.UserID, r.Sign); err != nil {
return nil, err
}
return ctrl.serve(ctx, r.NamespaceID, r.AttachmentID, false, r.Download)
}
func (ctrl Attachment) Preview(ctx context.Context, r *request.AttachmentPreview) (interface{}, error) {
if err := ctrl.isAccessible(r.Kind, r.NamespaceID, r.AttachmentID, r.UserID, r.Sign); err != nil {
return nil, err
}
return ctrl.serve(ctx, r.NamespaceID, r.AttachmentID, true, false)
}
func (ctrl Attachment) isAccessible(kind string, namespaceID, attachmentID, userID uint64, signature string) error {
if kind == types.PageAttachment || kind == types.IconAttachment || kind == types.NamespaceAttachment {
// Public Attachments
return nil
}
if signature == "" {
return errors.Unauthorized("missing signature")
}
if userID == 0 {
return errors.InvalidData("missing or invalid user ID")
}
if attachmentID == 0 {
return errors.InvalidData("missing or invalid attachment ID")
}
if !auth.DefaultSigner.Verify(signature, userID, namespaceID, attachmentID) {
return errors.InvalidData("missing or invalid signature")
}
return nil
}
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.FindByID(ctx, namespaceID, attachmentID)
if err != nil {
// Simplify error handling for now
w.WriteHeader(http.StatusNotFound)
return
}
var fh io.ReadSeekCloser
if preview {
fh, err = ctrl.attachment.OpenPreview(att)
} else {
fh, err = ctrl.attachment.OpenOriginal(att)
}
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
defer fh.Close()
name := url.QueryEscape(att.Name)
if download {
w.Header().Add("Content-Disposition", "attachment; filename="+name)
} else {
w.Header().Add("Content-Disposition", "inline; filename="+name)
w.Header().Add("Content-Security-Policy", "default-src 'none'; style-src 'unsafe-inline'; sandbox")
}
http.ServeContent(w, req, name, att.CreatedAt, fh)
}, nil
}
func (ctrl Attachment) makeFilterPayload(ctx context.Context, aa types.AttachmentSet, f types.AttachmentFilter, err error) (*attachmentSetPayload, error) {
if err != nil {
return nil, err
}
asp := &attachmentSetPayload{Filter: f, Set: make([]*attachmentPayload, len(aa))}
for i := range aa {
asp.Set[i], _ = makeAttachmentPayload(ctx, aa[i], nil)
}
return asp, nil
}
func makeAttachmentPayload(ctx context.Context, a *types.Attachment, err error) (*attachmentPayload, error) {
if err != nil || a == nil {
return nil, err
}
var (
userID = auth.GetIdentityFromContext(ctx).Identity()
signParams = fmt.Sprintf("?sign=%s&userID=%d", auth.DefaultSigner.Sign(userID, a.NamespaceID, a.ID), userID)
preview string
baseURL = fmt.Sprintf("/namespace/%d/attachment/%s/%d/", a.NamespaceID, a.Kind, a.ID)
)
if a.Meta.Preview != nil {
var ext = a.Meta.Preview.Extension
if ext == "" {
ext = "jpg"
}
preview = baseURL + fmt.Sprintf("preview.%s", ext) + signParams
}
ap := &attachmentPayload{a}
ap.Url = baseURL + fmt.Sprintf("original/%s", url.PathEscape(a.Name)) + signParams
ap.PreviewUrl = preview
return ap, nil
}

View File

@@ -0,0 +1,49 @@
package rest
import (
"context"
"github.com/cortezaproject/corteza/server/pkg/api"
"github.com/cortezaproject/corteza/server/compose/rest/request"
"github.com/cortezaproject/corteza/server/compose/service/event"
"github.com/cortezaproject/corteza/server/pkg/corredor"
)
type (
Automation struct{}
)
func (Automation) New() *Automation {
return &Automation{}
}
func (ctrl *Automation) List(ctx context.Context, r *request.AutomationList) (interface{}, error) {
return corredor.GenericListHandler(
ctx,
corredor.Service(),
corredor.Filter{
ResourceTypePrefixes: r.ResourceTypePrefixes,
ExcludeInvalid: r.ExcludeInvalid,
ResourceTypes: r.ResourceTypes,
EventTypes: r.EventTypes,
ExcludeServerScripts: r.ExcludeServerScripts,
ExcludeClientScripts: r.ExcludeClientScripts,
},
"compose",
)
}
func (ctrl *Automation) Bundle(ctx context.Context, r *request.AutomationBundle) (interface{}, error) {
return corredor.GenericBundleHandler(
ctx,
corredor.Service(),
r.Bundle,
r.Type,
r.Ext,
)
}
func (ctrl *Automation) TriggerScript(ctx context.Context, r *request.AutomationTriggerScript) (interface{}, error) {
return api.OK(), corredor.Service().Exec(ctx, r.Script, corredor.ExtendScriptArgs(event.ComposeOnManual(), r.Args))
}

View File

@@ -0,0 +1,173 @@
package rest
import (
"context"
"github.com/cortezaproject/corteza/server/compose/rest/request"
"github.com/cortezaproject/corteza/server/compose/service"
"github.com/cortezaproject/corteza/server/compose/types"
"github.com/cortezaproject/corteza/server/pkg/api"
"github.com/cortezaproject/corteza/server/pkg/filter"
)
type (
chartPayload struct {
*types.Chart
CanGrant bool `json:"canGrant"`
CanUpdateChart bool `json:"canUpdateChart"`
CanDeleteChart bool `json:"canDeleteChart"`
}
chartSetPayload struct {
Filter types.ChartFilter `json:"filter"`
Set []*chartPayload `json:"set"`
}
Chart struct {
chart interface {
FindByID(ctx context.Context, namespaceID, chartID uint64) (*types.Chart, error)
FindByHandle(ctx context.Context, namespaceID uint64, handle string) (*types.Chart, error)
Find(ctx context.Context, filter types.ChartFilter) (set types.ChartSet, f types.ChartFilter, err error)
Create(ctx context.Context, chart *types.Chart) (*types.Chart, error)
Update(ctx context.Context, chart *types.Chart) (*types.Chart, error)
DeleteByID(ctx context.Context, namespaceID, chartID uint64) error
}
locale service.ResourceTranslationsManagerService
ac chartAccessController
}
chartAccessController interface {
CanGrant(context.Context) bool
CanUpdateChart(context.Context, *types.Chart) bool
CanDeleteChart(context.Context, *types.Chart) bool
}
)
func (Chart) New() *Chart {
return &Chart{
chart: service.DefaultChart,
locale: service.DefaultResourceTranslation,
ac: service.DefaultAccessControl,
}
}
func (ctrl Chart) List(ctx context.Context, r *request.ChartList) (interface{}, error) {
var (
err error
f = types.ChartFilter{
NamespaceID: r.NamespaceID,
Handle: r.Handle,
Query: r.Query,
Labels: r.Labels,
}
)
if f.Paging, err = filter.NewPaging(r.Limit, r.PageCursor); err != nil {
return nil, err
}
f.IncTotal = r.IncTotal
if f.Sorting, err = filter.NewSorting(r.Sort); err != nil {
return nil, err
}
set, filter, err := ctrl.chart.Find(ctx, f)
return ctrl.makeFilterPayload(ctx, set, filter, err)
}
func (ctrl Chart) Create(ctx context.Context, r *request.ChartCreate) (interface{}, error) {
var err error
mod := &types.Chart{
NamespaceID: r.NamespaceID,
Name: r.Name,
Handle: r.Handle,
Labels: r.Labels,
}
if len(r.Config) > 2 {
if err = r.Config.Unmarshal(&mod.Config); err != nil {
return nil, err
}
}
mod, err = ctrl.chart.Create(ctx, mod)
return ctrl.makePayload(ctx, mod, err)
}
func (ctrl Chart) Read(ctx context.Context, r *request.ChartRead) (interface{}, error) {
mod, err := ctrl.chart.FindByID(ctx, r.NamespaceID, r.ChartID)
return ctrl.makePayload(ctx, mod, err)
}
func (ctrl Chart) Update(ctx context.Context, r *request.ChartUpdate) (interface{}, error) {
var (
err error
mod = &types.Chart{
ID: r.ChartID,
Name: r.Name,
Handle: r.Handle,
NamespaceID: r.NamespaceID,
UpdatedAt: r.UpdatedAt,
Labels: r.Labels,
}
)
if len(r.Config) > 2 {
if err = r.Config.Unmarshal(&mod.Config); err != nil {
return nil, err
}
}
mod, err = ctrl.chart.Update(ctx, mod)
return ctrl.makePayload(ctx, mod, err)
}
func (ctrl Chart) Delete(ctx context.Context, r *request.ChartDelete) (interface{}, error) {
_, err := ctrl.chart.FindByID(ctx, r.NamespaceID, r.ChartID)
if err != nil {
return nil, err
}
return api.OK(), ctrl.chart.DeleteByID(ctx, r.NamespaceID, r.ChartID)
}
func (ctrl Chart) ListTranslations(ctx context.Context, r *request.ChartListTranslations) (interface{}, error) {
return ctrl.locale.Chart(ctx, r.NamespaceID, r.ChartID)
}
func (ctrl Chart) UpdateTranslations(ctx context.Context, r *request.ChartUpdateTranslations) (interface{}, error) {
return api.OK(), ctrl.locale.Upsert(ctx, r.Translations)
}
func (ctrl Chart) makePayload(ctx context.Context, c *types.Chart, err error) (*chartPayload, error) {
if err != nil || c == nil {
return nil, err
}
return &chartPayload{
Chart: c,
CanGrant: ctrl.ac.CanGrant(ctx),
CanUpdateChart: ctrl.ac.CanUpdateChart(ctx, c),
CanDeleteChart: ctrl.ac.CanDeleteChart(ctx, c),
}, nil
}
func (ctrl Chart) makeFilterPayload(ctx context.Context, nn types.ChartSet, f types.ChartFilter, err error) (*chartSetPayload, error) {
if err != nil {
return nil, err
}
modp := &chartSetPayload{Filter: f, Set: make([]*chartPayload, len(nn))}
for i := range nn {
modp.Set[i], _ = ctrl.makePayload(ctx, nn[i], nil)
}
return modp, nil
}

View File

@@ -0,0 +1,137 @@
package rest
import (
"context"
"github.com/cortezaproject/corteza/server/compose/rest/request"
"github.com/cortezaproject/corteza/server/compose/service"
"github.com/cortezaproject/corteza/server/compose/types"
"github.com/cortezaproject/corteza/server/pkg/filter"
"github.com/cortezaproject/corteza/server/pkg/payload"
)
type (
sensitiveRecordsSetPayload struct {
Set []*sensitiveDataPayload `json:"set"`
}
sensitiveDataPayload struct {
NamespaceID uint64 `json:"namespaceID,string"`
Namespace string `json:"namespace"`
ModuleID uint64 `json:"moduleID,string"`
Module string `json:"module"`
Records []sensitiveRecords `json:"records"`
}
sensitiveRecords struct {
RecordID uint64 `json:"recordID,string"`
Values []map[string]any `json:"values"`
}
privacyModuleSetPayload struct {
Filter types.PrivacyModuleFilter `json:"filter"`
Set []*types.PrivacyModule `json:"set"`
}
privateDataFinder interface {
SearchSensitive(ctx context.Context) (set []types.SensitiveRecordSet, err error)
}
DataPrivacy struct {
record privateDataFinder
module service.ModuleService
namespace service.NamespaceService
privacy service.DataPrivacyService
}
)
func (DataPrivacy) New() *DataPrivacy {
return &DataPrivacy{
record: service.DefaultRecord,
module: service.DefaultModule,
namespace: service.DefaultNamespace,
privacy: service.DefaultDataPrivacy,
}
}
func (ctrl *DataPrivacy) RecordList(ctx context.Context, r *request.DataPrivacyRecordList) (out interface{}, err error) {
// If we're requesting only specific connections, prepare filter params here
reqConns := make(map[uint64]bool)
hasReqConns := len(r.ConnectionID) > 0
for _, connectionID := range payload.ParseUint64s(r.ConnectionID) {
reqConns[connectionID] = true
}
// Collect sensitive records
ss, err := ctrl.record.SearchSensitive(ctx)
if err != nil {
return
}
outSet := sensitiveRecordsSetPayload{
Set: make([]*sensitiveDataPayload, 0, 10),
}
for _, s := range ss {
// Skip the ones we don't want
if hasReqConns && !reqConns[s.Module.Config.DAL.ConnectionID] {
continue
}
// Build the payload
payload := &sensitiveDataPayload{
NamespaceID: s.Namespace.ID,
Namespace: s.Namespace.Name,
ModuleID: s.Module.ID,
Module: s.Module.Name,
Records: make([]sensitiveRecords, 0, len(s.Records)),
}
for _, a := range s.Records {
if len(a.Values) == 0 {
continue
}
payload.Records = append(payload.Records, sensitiveRecords{
RecordID: a.RecordID,
Values: a.Values,
})
}
outSet.Set = append(outSet.Set, payload)
}
return outSet, nil
}
func (ctrl *DataPrivacy) ModuleList(ctx context.Context, r *request.DataPrivacyModuleList) (out interface{}, err error) {
var (
f = types.PrivacyModuleFilter{
ConnectionID: r.ConnectionID,
}
)
if f.Paging, err = filter.NewPaging(r.Limit, r.PageCursor); err != nil {
return nil, err
}
if f.Sorting, err = filter.NewSorting(r.Sort); err != nil {
return nil, err
}
set, f, err := ctrl.privacy.FindModules(ctx, f)
return ctrl.makeFilterPayload(ctx, set, f, err)
}
func (ctrl DataPrivacy) makeFilterPayload(_ context.Context, mm types.PrivacyModuleSet, f types.PrivacyModuleFilter, err error) (*privacyModuleSetPayload, error) {
if err != nil {
return nil, err
}
if len(mm) == 0 {
mm = make([]*types.PrivacyModule, 0)
}
return &privacyModuleSetPayload{Filter: f, Set: mm}, nil
}

View File

@@ -0,0 +1,133 @@
package handlers
// 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:
//
import (
"context"
"github.com/cortezaproject/corteza/server/compose/rest/request"
"github.com/cortezaproject/corteza/server/pkg/api"
"github.com/go-chi/chi/v5"
"net/http"
)
type (
// Internal API interface
AttachmentAPI interface {
List(context.Context, *request.AttachmentList) (interface{}, error)
Read(context.Context, *request.AttachmentRead) (interface{}, error)
Delete(context.Context, *request.AttachmentDelete) (interface{}, error)
Original(context.Context, *request.AttachmentOriginal) (interface{}, error)
Preview(context.Context, *request.AttachmentPreview) (interface{}, error)
}
// HTTP API interface
Attachment struct {
List func(http.ResponseWriter, *http.Request)
Read func(http.ResponseWriter, *http.Request)
Delete func(http.ResponseWriter, *http.Request)
Original func(http.ResponseWriter, *http.Request)
Preview func(http.ResponseWriter, *http.Request)
}
)
func NewAttachment(h AttachmentAPI) *Attachment {
return &Attachment{
List: func(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close()
params := request.NewAttachmentList()
if err := params.Fill(r); err != nil {
api.Send(w, r, err)
return
}
value, err := h.List(r.Context(), params)
if err != nil {
api.Send(w, r, err)
return
}
api.Send(w, r, value)
},
Read: func(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close()
params := request.NewAttachmentRead()
if err := params.Fill(r); err != nil {
api.Send(w, r, err)
return
}
value, err := h.Read(r.Context(), params)
if err != nil {
api.Send(w, r, err)
return
}
api.Send(w, r, value)
},
Delete: func(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close()
params := request.NewAttachmentDelete()
if err := params.Fill(r); err != nil {
api.Send(w, r, err)
return
}
value, err := h.Delete(r.Context(), params)
if err != nil {
api.Send(w, r, err)
return
}
api.Send(w, r, value)
},
Original: func(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close()
params := request.NewAttachmentOriginal()
if err := params.Fill(r); err != nil {
api.Send(w, r, err)
return
}
value, err := h.Original(r.Context(), params)
if err != nil {
api.Send(w, r, err)
return
}
api.Send(w, r, value)
},
Preview: func(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close()
params := request.NewAttachmentPreview()
if err := params.Fill(r); err != nil {
api.Send(w, r, err)
return
}
value, err := h.Preview(r.Context(), params)
if err != nil {
api.Send(w, r, err)
return
}
api.Send(w, r, value)
},
}
}
func (h Attachment) MountRoutes(r chi.Router, middlewares ...func(http.Handler) http.Handler) {
r.Group(func(r chi.Router) {
r.Use(middlewares...)
r.Get("/namespace/{namespaceID}/attachment/{kind}/", h.List)
r.Get("/namespace/{namespaceID}/attachment/{kind}/{attachmentID}", h.Read)
r.Delete("/namespace/{namespaceID}/attachment/{kind}/{attachmentID}", h.Delete)
r.Get("/namespace/{namespaceID}/attachment/{kind}/{attachmentID}/original/{name}", h.Original)
r.Get("/namespace/{namespaceID}/attachment/{kind}/{attachmentID}/preview.{ext}", h.Preview)
})
}

View File

@@ -0,0 +1,95 @@
package handlers
// 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:
//
import (
"context"
"github.com/cortezaproject/corteza/server/compose/rest/request"
"github.com/cortezaproject/corteza/server/pkg/api"
"github.com/go-chi/chi/v5"
"net/http"
)
type (
// Internal API interface
AutomationAPI interface {
List(context.Context, *request.AutomationList) (interface{}, error)
Bundle(context.Context, *request.AutomationBundle) (interface{}, error)
TriggerScript(context.Context, *request.AutomationTriggerScript) (interface{}, error)
}
// HTTP API interface
Automation struct {
List func(http.ResponseWriter, *http.Request)
Bundle func(http.ResponseWriter, *http.Request)
TriggerScript func(http.ResponseWriter, *http.Request)
}
)
func NewAutomation(h AutomationAPI) *Automation {
return &Automation{
List: func(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close()
params := request.NewAutomationList()
if err := params.Fill(r); err != nil {
api.Send(w, r, err)
return
}
value, err := h.List(r.Context(), params)
if err != nil {
api.Send(w, r, err)
return
}
api.Send(w, r, value)
},
Bundle: func(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close()
params := request.NewAutomationBundle()
if err := params.Fill(r); err != nil {
api.Send(w, r, err)
return
}
value, err := h.Bundle(r.Context(), params)
if err != nil {
api.Send(w, r, err)
return
}
api.Send(w, r, value)
},
TriggerScript: func(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close()
params := request.NewAutomationTriggerScript()
if err := params.Fill(r); err != nil {
api.Send(w, r, err)
return
}
value, err := h.TriggerScript(r.Context(), params)
if err != nil {
api.Send(w, r, err)
return
}
api.Send(w, r, value)
},
}
}
func (h Automation) MountRoutes(r chi.Router, middlewares ...func(http.Handler) http.Handler) {
r.Group(func(r chi.Router) {
r.Use(middlewares...)
r.Get("/automation/", h.List)
r.Get("/automation/{bundle}-{type}.{ext}", h.Bundle)
r.Post("/automation/trigger", h.TriggerScript)
})
}

View File

@@ -0,0 +1,171 @@
package handlers
// 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:
//
import (
"context"
"github.com/cortezaproject/corteza/server/compose/rest/request"
"github.com/cortezaproject/corteza/server/pkg/api"
"github.com/go-chi/chi/v5"
"net/http"
)
type (
// Internal API interface
ChartAPI interface {
List(context.Context, *request.ChartList) (interface{}, error)
Create(context.Context, *request.ChartCreate) (interface{}, error)
Read(context.Context, *request.ChartRead) (interface{}, error)
Update(context.Context, *request.ChartUpdate) (interface{}, error)
Delete(context.Context, *request.ChartDelete) (interface{}, error)
ListTranslations(context.Context, *request.ChartListTranslations) (interface{}, error)
UpdateTranslations(context.Context, *request.ChartUpdateTranslations) (interface{}, error)
}
// HTTP API interface
Chart struct {
List func(http.ResponseWriter, *http.Request)
Create func(http.ResponseWriter, *http.Request)
Read func(http.ResponseWriter, *http.Request)
Update func(http.ResponseWriter, *http.Request)
Delete func(http.ResponseWriter, *http.Request)
ListTranslations func(http.ResponseWriter, *http.Request)
UpdateTranslations func(http.ResponseWriter, *http.Request)
}
)
func NewChart(h ChartAPI) *Chart {
return &Chart{
List: func(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close()
params := request.NewChartList()
if err := params.Fill(r); err != nil {
api.Send(w, r, err)
return
}
value, err := h.List(r.Context(), params)
if err != nil {
api.Send(w, r, err)
return
}
api.Send(w, r, value)
},
Create: func(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close()
params := request.NewChartCreate()
if err := params.Fill(r); err != nil {
api.Send(w, r, err)
return
}
value, err := h.Create(r.Context(), params)
if err != nil {
api.Send(w, r, err)
return
}
api.Send(w, r, value)
},
Read: func(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close()
params := request.NewChartRead()
if err := params.Fill(r); err != nil {
api.Send(w, r, err)
return
}
value, err := h.Read(r.Context(), params)
if err != nil {
api.Send(w, r, err)
return
}
api.Send(w, r, value)
},
Update: func(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close()
params := request.NewChartUpdate()
if err := params.Fill(r); err != nil {
api.Send(w, r, err)
return
}
value, err := h.Update(r.Context(), params)
if err != nil {
api.Send(w, r, err)
return
}
api.Send(w, r, value)
},
Delete: func(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close()
params := request.NewChartDelete()
if err := params.Fill(r); err != nil {
api.Send(w, r, err)
return
}
value, err := h.Delete(r.Context(), params)
if err != nil {
api.Send(w, r, err)
return
}
api.Send(w, r, value)
},
ListTranslations: func(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close()
params := request.NewChartListTranslations()
if err := params.Fill(r); err != nil {
api.Send(w, r, err)
return
}
value, err := h.ListTranslations(r.Context(), params)
if err != nil {
api.Send(w, r, err)
return
}
api.Send(w, r, value)
},
UpdateTranslations: func(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close()
params := request.NewChartUpdateTranslations()
if err := params.Fill(r); err != nil {
api.Send(w, r, err)
return
}
value, err := h.UpdateTranslations(r.Context(), params)
if err != nil {
api.Send(w, r, err)
return
}
api.Send(w, r, value)
},
}
}
func (h Chart) MountRoutes(r chi.Router, middlewares ...func(http.Handler) http.Handler) {
r.Group(func(r chi.Router) {
r.Use(middlewares...)
r.Get("/namespace/{namespaceID}/chart/", h.List)
r.Post("/namespace/{namespaceID}/chart/", h.Create)
r.Get("/namespace/{namespaceID}/chart/{chartID}", h.Read)
r.Post("/namespace/{namespaceID}/chart/{chartID}", h.Update)
r.Delete("/namespace/{namespaceID}/chart/{chartID}", h.Delete)
r.Get("/namespace/{namespaceID}/chart/{chartID}/translation", h.ListTranslations)
r.Patch("/namespace/{namespaceID}/chart/{chartID}/translation", h.UpdateTranslations)
})
}

View File

@@ -0,0 +1,76 @@
package handlers
// 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:
//
import (
"context"
"github.com/cortezaproject/corteza/server/compose/rest/request"
"github.com/cortezaproject/corteza/server/pkg/api"
"github.com/go-chi/chi/v5"
"net/http"
)
type (
// Internal API interface
DataPrivacyAPI interface {
RecordList(context.Context, *request.DataPrivacyRecordList) (interface{}, error)
ModuleList(context.Context, *request.DataPrivacyModuleList) (interface{}, error)
}
// HTTP API interface
DataPrivacy struct {
RecordList func(http.ResponseWriter, *http.Request)
ModuleList func(http.ResponseWriter, *http.Request)
}
)
func NewDataPrivacy(h DataPrivacyAPI) *DataPrivacy {
return &DataPrivacy{
RecordList: func(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close()
params := request.NewDataPrivacyRecordList()
if err := params.Fill(r); err != nil {
api.Send(w, r, err)
return
}
value, err := h.RecordList(r.Context(), params)
if err != nil {
api.Send(w, r, err)
return
}
api.Send(w, r, value)
},
ModuleList: func(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close()
params := request.NewDataPrivacyModuleList()
if err := params.Fill(r); err != nil {
api.Send(w, r, err)
return
}
value, err := h.ModuleList(r.Context(), params)
if err != nil {
api.Send(w, r, err)
return
}
api.Send(w, r, value)
},
}
}
func (h DataPrivacy) MountRoutes(r chi.Router, middlewares ...func(http.Handler) http.Handler) {
r.Group(func(r chi.Router) {
r.Use(middlewares...)
r.Get("/data-privacy/record", h.RecordList)
r.Get("/data-privacy/module", h.ModuleList)
})
}

View File

@@ -0,0 +1,95 @@
package handlers
// 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:
//
import (
"context"
"github.com/cortezaproject/corteza/server/compose/rest/request"
"github.com/cortezaproject/corteza/server/pkg/api"
"github.com/go-chi/chi/v5"
"net/http"
)
type (
// Internal API interface
IconAPI interface {
List(context.Context, *request.IconList) (interface{}, error)
Upload(context.Context, *request.IconUpload) (interface{}, error)
Delete(context.Context, *request.IconDelete) (interface{}, error)
}
// HTTP API interface
Icon struct {
List func(http.ResponseWriter, *http.Request)
Upload func(http.ResponseWriter, *http.Request)
Delete func(http.ResponseWriter, *http.Request)
}
)
func NewIcon(h IconAPI) *Icon {
return &Icon{
List: func(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close()
params := request.NewIconList()
if err := params.Fill(r); err != nil {
api.Send(w, r, err)
return
}
value, err := h.List(r.Context(), params)
if err != nil {
api.Send(w, r, err)
return
}
api.Send(w, r, value)
},
Upload: func(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close()
params := request.NewIconUpload()
if err := params.Fill(r); err != nil {
api.Send(w, r, err)
return
}
value, err := h.Upload(r.Context(), params)
if err != nil {
api.Send(w, r, err)
return
}
api.Send(w, r, value)
},
Delete: func(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close()
params := request.NewIconDelete()
if err := params.Fill(r); err != nil {
api.Send(w, r, err)
return
}
value, err := h.Delete(r.Context(), params)
if err != nil {
api.Send(w, r, err)
return
}
api.Send(w, r, value)
},
}
}
func (h Icon) MountRoutes(r chi.Router, middlewares ...func(http.Handler) http.Handler) {
r.Group(func(r chi.Router) {
r.Use(middlewares...)
r.Get("/icon/", h.List)
r.Post("/icon/", h.Upload)
r.Delete("/icon/{iconID}", h.Delete)
})
}

View File

@@ -0,0 +1,190 @@
package handlers
// 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:
//
import (
"context"
"github.com/cortezaproject/corteza/server/compose/rest/request"
"github.com/cortezaproject/corteza/server/pkg/api"
"github.com/go-chi/chi/v5"
"net/http"
)
type (
// Internal API interface
ModuleAPI interface {
List(context.Context, *request.ModuleList) (interface{}, error)
Create(context.Context, *request.ModuleCreate) (interface{}, error)
Read(context.Context, *request.ModuleRead) (interface{}, error)
Update(context.Context, *request.ModuleUpdate) (interface{}, error)
Delete(context.Context, *request.ModuleDelete) (interface{}, error)
TriggerScript(context.Context, *request.ModuleTriggerScript) (interface{}, error)
ListTranslations(context.Context, *request.ModuleListTranslations) (interface{}, error)
UpdateTranslations(context.Context, *request.ModuleUpdateTranslations) (interface{}, error)
}
// HTTP API interface
Module struct {
List func(http.ResponseWriter, *http.Request)
Create func(http.ResponseWriter, *http.Request)
Read func(http.ResponseWriter, *http.Request)
Update func(http.ResponseWriter, *http.Request)
Delete func(http.ResponseWriter, *http.Request)
TriggerScript func(http.ResponseWriter, *http.Request)
ListTranslations func(http.ResponseWriter, *http.Request)
UpdateTranslations func(http.ResponseWriter, *http.Request)
}
)
func NewModule(h ModuleAPI) *Module {
return &Module{
List: func(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close()
params := request.NewModuleList()
if err := params.Fill(r); err != nil {
api.Send(w, r, err)
return
}
value, err := h.List(r.Context(), params)
if err != nil {
api.Send(w, r, err)
return
}
api.Send(w, r, value)
},
Create: func(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close()
params := request.NewModuleCreate()
if err := params.Fill(r); err != nil {
api.Send(w, r, err)
return
}
value, err := h.Create(r.Context(), params)
if err != nil {
api.Send(w, r, err)
return
}
api.Send(w, r, value)
},
Read: func(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close()
params := request.NewModuleRead()
if err := params.Fill(r); err != nil {
api.Send(w, r, err)
return
}
value, err := h.Read(r.Context(), params)
if err != nil {
api.Send(w, r, err)
return
}
api.Send(w, r, value)
},
Update: func(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close()
params := request.NewModuleUpdate()
if err := params.Fill(r); err != nil {
api.Send(w, r, err)
return
}
value, err := h.Update(r.Context(), params)
if err != nil {
api.Send(w, r, err)
return
}
api.Send(w, r, value)
},
Delete: func(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close()
params := request.NewModuleDelete()
if err := params.Fill(r); err != nil {
api.Send(w, r, err)
return
}
value, err := h.Delete(r.Context(), params)
if err != nil {
api.Send(w, r, err)
return
}
api.Send(w, r, value)
},
TriggerScript: func(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close()
params := request.NewModuleTriggerScript()
if err := params.Fill(r); err != nil {
api.Send(w, r, err)
return
}
value, err := h.TriggerScript(r.Context(), params)
if err != nil {
api.Send(w, r, err)
return
}
api.Send(w, r, value)
},
ListTranslations: func(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close()
params := request.NewModuleListTranslations()
if err := params.Fill(r); err != nil {
api.Send(w, r, err)
return
}
value, err := h.ListTranslations(r.Context(), params)
if err != nil {
api.Send(w, r, err)
return
}
api.Send(w, r, value)
},
UpdateTranslations: func(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close()
params := request.NewModuleUpdateTranslations()
if err := params.Fill(r); err != nil {
api.Send(w, r, err)
return
}
value, err := h.UpdateTranslations(r.Context(), params)
if err != nil {
api.Send(w, r, err)
return
}
api.Send(w, r, value)
},
}
}
func (h Module) MountRoutes(r chi.Router, middlewares ...func(http.Handler) http.Handler) {
r.Group(func(r chi.Router) {
r.Use(middlewares...)
r.Get("/namespace/{namespaceID}/module/", h.List)
r.Post("/namespace/{namespaceID}/module/", h.Create)
r.Get("/namespace/{namespaceID}/module/{moduleID}", h.Read)
r.Post("/namespace/{namespaceID}/module/{moduleID}", h.Update)
r.Delete("/namespace/{namespaceID}/module/{moduleID}", h.Delete)
r.Post("/namespace/{namespaceID}/module/{moduleID}/trigger", h.TriggerScript)
r.Get("/namespace/{namespaceID}/module/{moduleID}/translation", h.ListTranslations)
r.Patch("/namespace/{namespaceID}/module/{moduleID}/translation", h.UpdateTranslations)
})
}

View File

@@ -0,0 +1,285 @@
package handlers
// 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:
//
import (
"context"
"github.com/cortezaproject/corteza/server/compose/rest/request"
"github.com/cortezaproject/corteza/server/pkg/api"
"github.com/go-chi/chi/v5"
"net/http"
)
type (
// Internal API interface
NamespaceAPI interface {
List(context.Context, *request.NamespaceList) (interface{}, error)
Create(context.Context, *request.NamespaceCreate) (interface{}, error)
Read(context.Context, *request.NamespaceRead) (interface{}, error)
Update(context.Context, *request.NamespaceUpdate) (interface{}, error)
Delete(context.Context, *request.NamespaceDelete) (interface{}, error)
Upload(context.Context, *request.NamespaceUpload) (interface{}, error)
Clone(context.Context, *request.NamespaceClone) (interface{}, error)
Export(context.Context, *request.NamespaceExport) (interface{}, error)
ImportInit(context.Context, *request.NamespaceImportInit) (interface{}, error)
ImportRun(context.Context, *request.NamespaceImportRun) (interface{}, error)
TriggerScript(context.Context, *request.NamespaceTriggerScript) (interface{}, error)
ListTranslations(context.Context, *request.NamespaceListTranslations) (interface{}, error)
UpdateTranslations(context.Context, *request.NamespaceUpdateTranslations) (interface{}, error)
}
// HTTP API interface
Namespace struct {
List func(http.ResponseWriter, *http.Request)
Create func(http.ResponseWriter, *http.Request)
Read func(http.ResponseWriter, *http.Request)
Update func(http.ResponseWriter, *http.Request)
Delete func(http.ResponseWriter, *http.Request)
Upload func(http.ResponseWriter, *http.Request)
Clone func(http.ResponseWriter, *http.Request)
Export func(http.ResponseWriter, *http.Request)
ImportInit func(http.ResponseWriter, *http.Request)
ImportRun func(http.ResponseWriter, *http.Request)
TriggerScript func(http.ResponseWriter, *http.Request)
ListTranslations func(http.ResponseWriter, *http.Request)
UpdateTranslations func(http.ResponseWriter, *http.Request)
}
)
func NewNamespace(h NamespaceAPI) *Namespace {
return &Namespace{
List: func(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close()
params := request.NewNamespaceList()
if err := params.Fill(r); err != nil {
api.Send(w, r, err)
return
}
value, err := h.List(r.Context(), params)
if err != nil {
api.Send(w, r, err)
return
}
api.Send(w, r, value)
},
Create: func(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close()
params := request.NewNamespaceCreate()
if err := params.Fill(r); err != nil {
api.Send(w, r, err)
return
}
value, err := h.Create(r.Context(), params)
if err != nil {
api.Send(w, r, err)
return
}
api.Send(w, r, value)
},
Read: func(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close()
params := request.NewNamespaceRead()
if err := params.Fill(r); err != nil {
api.Send(w, r, err)
return
}
value, err := h.Read(r.Context(), params)
if err != nil {
api.Send(w, r, err)
return
}
api.Send(w, r, value)
},
Update: func(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close()
params := request.NewNamespaceUpdate()
if err := params.Fill(r); err != nil {
api.Send(w, r, err)
return
}
value, err := h.Update(r.Context(), params)
if err != nil {
api.Send(w, r, err)
return
}
api.Send(w, r, value)
},
Delete: func(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close()
params := request.NewNamespaceDelete()
if err := params.Fill(r); err != nil {
api.Send(w, r, err)
return
}
value, err := h.Delete(r.Context(), params)
if err != nil {
api.Send(w, r, err)
return
}
api.Send(w, r, value)
},
Upload: func(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close()
params := request.NewNamespaceUpload()
if err := params.Fill(r); err != nil {
api.Send(w, r, err)
return
}
value, err := h.Upload(r.Context(), params)
if err != nil {
api.Send(w, r, err)
return
}
api.Send(w, r, value)
},
Clone: func(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close()
params := request.NewNamespaceClone()
if err := params.Fill(r); err != nil {
api.Send(w, r, err)
return
}
value, err := h.Clone(r.Context(), params)
if err != nil {
api.Send(w, r, err)
return
}
api.Send(w, r, value)
},
Export: func(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close()
params := request.NewNamespaceExport()
if err := params.Fill(r); err != nil {
api.Send(w, r, err)
return
}
value, err := h.Export(r.Context(), params)
if err != nil {
api.Send(w, r, err)
return
}
api.Send(w, r, value)
},
ImportInit: func(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close()
params := request.NewNamespaceImportInit()
if err := params.Fill(r); err != nil {
api.Send(w, r, err)
return
}
value, err := h.ImportInit(r.Context(), params)
if err != nil {
api.Send(w, r, err)
return
}
api.Send(w, r, value)
},
ImportRun: func(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close()
params := request.NewNamespaceImportRun()
if err := params.Fill(r); err != nil {
api.Send(w, r, err)
return
}
value, err := h.ImportRun(r.Context(), params)
if err != nil {
api.Send(w, r, err)
return
}
api.Send(w, r, value)
},
TriggerScript: func(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close()
params := request.NewNamespaceTriggerScript()
if err := params.Fill(r); err != nil {
api.Send(w, r, err)
return
}
value, err := h.TriggerScript(r.Context(), params)
if err != nil {
api.Send(w, r, err)
return
}
api.Send(w, r, value)
},
ListTranslations: func(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close()
params := request.NewNamespaceListTranslations()
if err := params.Fill(r); err != nil {
api.Send(w, r, err)
return
}
value, err := h.ListTranslations(r.Context(), params)
if err != nil {
api.Send(w, r, err)
return
}
api.Send(w, r, value)
},
UpdateTranslations: func(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close()
params := request.NewNamespaceUpdateTranslations()
if err := params.Fill(r); err != nil {
api.Send(w, r, err)
return
}
value, err := h.UpdateTranslations(r.Context(), params)
if err != nil {
api.Send(w, r, err)
return
}
api.Send(w, r, value)
},
}
}
func (h Namespace) MountRoutes(r chi.Router, middlewares ...func(http.Handler) http.Handler) {
r.Group(func(r chi.Router) {
r.Use(middlewares...)
r.Get("/namespace/", h.List)
r.Post("/namespace/", h.Create)
r.Get("/namespace/{namespaceID}", h.Read)
r.Post("/namespace/{namespaceID}", h.Update)
r.Delete("/namespace/{namespaceID}", h.Delete)
r.Post("/namespace/upload", h.Upload)
r.Post("/namespace/{namespaceID}/clone", h.Clone)
r.Get("/namespace/{namespaceID}/export/{filename}.zip", h.Export)
r.Post("/namespace/import", h.ImportInit)
r.Post("/namespace/import/{sessionID}", h.ImportRun)
r.Post("/namespace/{namespaceID}/trigger", h.TriggerScript)
r.Get("/namespace/{namespaceID}/translation", h.ListTranslations)
r.Patch("/namespace/{namespaceID}/translation", h.UpdateTranslations)
})
}

View File

@@ -0,0 +1,57 @@
package handlers
// 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:
//
import (
"context"
"github.com/cortezaproject/corteza/server/compose/rest/request"
"github.com/cortezaproject/corteza/server/pkg/api"
"github.com/go-chi/chi/v5"
"net/http"
)
type (
// Internal API interface
NotificationAPI interface {
EmailSend(context.Context, *request.NotificationEmailSend) (interface{}, error)
}
// HTTP API interface
Notification struct {
EmailSend func(http.ResponseWriter, *http.Request)
}
)
func NewNotification(h NotificationAPI) *Notification {
return &Notification{
EmailSend: func(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close()
params := request.NewNotificationEmailSend()
if err := params.Fill(r); err != nil {
api.Send(w, r, err)
return
}
value, err := h.EmailSend(r.Context(), params)
if err != nil {
api.Send(w, r, err)
return
}
api.Send(w, r, value)
},
}
}
func (h Notification) MountRoutes(r chi.Router, middlewares ...func(http.Handler) http.Handler) {
r.Group(func(r chi.Router) {
r.Use(middlewares...)
r.Post("/notification/email", h.EmailSend)
})
}

View File

@@ -0,0 +1,266 @@
package handlers
// 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:
//
import (
"context"
"github.com/cortezaproject/corteza/server/compose/rest/request"
"github.com/cortezaproject/corteza/server/pkg/api"
"github.com/go-chi/chi/v5"
"net/http"
)
type (
// Internal API interface
PageAPI interface {
List(context.Context, *request.PageList) (interface{}, error)
Create(context.Context, *request.PageCreate) (interface{}, error)
Read(context.Context, *request.PageRead) (interface{}, error)
Tree(context.Context, *request.PageTree) (interface{}, error)
Update(context.Context, *request.PageUpdate) (interface{}, error)
Reorder(context.Context, *request.PageReorder) (interface{}, error)
Delete(context.Context, *request.PageDelete) (interface{}, error)
Upload(context.Context, *request.PageUpload) (interface{}, error)
TriggerScript(context.Context, *request.PageTriggerScript) (interface{}, error)
ListTranslations(context.Context, *request.PageListTranslations) (interface{}, error)
UpdateTranslations(context.Context, *request.PageUpdateTranslations) (interface{}, error)
UpdateIcon(context.Context, *request.PageUpdateIcon) (interface{}, error)
}
// HTTP API interface
Page struct {
List func(http.ResponseWriter, *http.Request)
Create func(http.ResponseWriter, *http.Request)
Read func(http.ResponseWriter, *http.Request)
Tree func(http.ResponseWriter, *http.Request)
Update func(http.ResponseWriter, *http.Request)
Reorder func(http.ResponseWriter, *http.Request)
Delete func(http.ResponseWriter, *http.Request)
Upload func(http.ResponseWriter, *http.Request)
TriggerScript func(http.ResponseWriter, *http.Request)
ListTranslations func(http.ResponseWriter, *http.Request)
UpdateTranslations func(http.ResponseWriter, *http.Request)
UpdateIcon func(http.ResponseWriter, *http.Request)
}
)
func NewPage(h PageAPI) *Page {
return &Page{
List: func(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close()
params := request.NewPageList()
if err := params.Fill(r); err != nil {
api.Send(w, r, err)
return
}
value, err := h.List(r.Context(), params)
if err != nil {
api.Send(w, r, err)
return
}
api.Send(w, r, value)
},
Create: func(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close()
params := request.NewPageCreate()
if err := params.Fill(r); err != nil {
api.Send(w, r, err)
return
}
value, err := h.Create(r.Context(), params)
if err != nil {
api.Send(w, r, err)
return
}
api.Send(w, r, value)
},
Read: func(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close()
params := request.NewPageRead()
if err := params.Fill(r); err != nil {
api.Send(w, r, err)
return
}
value, err := h.Read(r.Context(), params)
if err != nil {
api.Send(w, r, err)
return
}
api.Send(w, r, value)
},
Tree: func(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close()
params := request.NewPageTree()
if err := params.Fill(r); err != nil {
api.Send(w, r, err)
return
}
value, err := h.Tree(r.Context(), params)
if err != nil {
api.Send(w, r, err)
return
}
api.Send(w, r, value)
},
Update: func(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close()
params := request.NewPageUpdate()
if err := params.Fill(r); err != nil {
api.Send(w, r, err)
return
}
value, err := h.Update(r.Context(), params)
if err != nil {
api.Send(w, r, err)
return
}
api.Send(w, r, value)
},
Reorder: func(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close()
params := request.NewPageReorder()
if err := params.Fill(r); err != nil {
api.Send(w, r, err)
return
}
value, err := h.Reorder(r.Context(), params)
if err != nil {
api.Send(w, r, err)
return
}
api.Send(w, r, value)
},
Delete: func(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close()
params := request.NewPageDelete()
if err := params.Fill(r); err != nil {
api.Send(w, r, err)
return
}
value, err := h.Delete(r.Context(), params)
if err != nil {
api.Send(w, r, err)
return
}
api.Send(w, r, value)
},
Upload: func(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close()
params := request.NewPageUpload()
if err := params.Fill(r); err != nil {
api.Send(w, r, err)
return
}
value, err := h.Upload(r.Context(), params)
if err != nil {
api.Send(w, r, err)
return
}
api.Send(w, r, value)
},
TriggerScript: func(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close()
params := request.NewPageTriggerScript()
if err := params.Fill(r); err != nil {
api.Send(w, r, err)
return
}
value, err := h.TriggerScript(r.Context(), params)
if err != nil {
api.Send(w, r, err)
return
}
api.Send(w, r, value)
},
ListTranslations: func(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close()
params := request.NewPageListTranslations()
if err := params.Fill(r); err != nil {
api.Send(w, r, err)
return
}
value, err := h.ListTranslations(r.Context(), params)
if err != nil {
api.Send(w, r, err)
return
}
api.Send(w, r, value)
},
UpdateTranslations: func(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close()
params := request.NewPageUpdateTranslations()
if err := params.Fill(r); err != nil {
api.Send(w, r, err)
return
}
value, err := h.UpdateTranslations(r.Context(), params)
if err != nil {
api.Send(w, r, err)
return
}
api.Send(w, r, value)
},
UpdateIcon: func(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close()
params := request.NewPageUpdateIcon()
if err := params.Fill(r); err != nil {
api.Send(w, r, err)
return
}
value, err := h.UpdateIcon(r.Context(), params)
if err != nil {
api.Send(w, r, err)
return
}
api.Send(w, r, value)
},
}
}
func (h Page) MountRoutes(r chi.Router, middlewares ...func(http.Handler) http.Handler) {
r.Group(func(r chi.Router) {
r.Use(middlewares...)
r.Get("/namespace/{namespaceID}/page/", h.List)
r.Post("/namespace/{namespaceID}/page/", h.Create)
r.Get("/namespace/{namespaceID}/page/{pageID}", h.Read)
r.Get("/namespace/{namespaceID}/page/tree", h.Tree)
r.Post("/namespace/{namespaceID}/page/{pageID}", h.Update)
r.Post("/namespace/{namespaceID}/page/{selfID}/reorder", h.Reorder)
r.Delete("/namespace/{namespaceID}/page/{pageID}", h.Delete)
r.Post("/namespace/{namespaceID}/page/{pageID}/attachment", h.Upload)
r.Post("/namespace/{namespaceID}/page/{pageID}/trigger", h.TriggerScript)
r.Get("/namespace/{namespaceID}/page/{pageID}/translation", h.ListTranslations)
r.Patch("/namespace/{namespaceID}/page/{pageID}/translation", h.UpdateTranslations)
r.Patch("/namespace/{namespaceID}/page/{pageID}/icon", h.UpdateIcon)
})
}

View File

@@ -0,0 +1,228 @@
package handlers
// 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:
//
import (
"context"
"github.com/cortezaproject/corteza/server/compose/rest/request"
"github.com/cortezaproject/corteza/server/pkg/api"
"github.com/go-chi/chi/v5"
"net/http"
)
type (
// Internal API interface
PageLayoutAPI interface {
ListNamespace(context.Context, *request.PageLayoutListNamespace) (interface{}, error)
List(context.Context, *request.PageLayoutList) (interface{}, error)
Create(context.Context, *request.PageLayoutCreate) (interface{}, error)
Read(context.Context, *request.PageLayoutRead) (interface{}, error)
Update(context.Context, *request.PageLayoutUpdate) (interface{}, error)
Reorder(context.Context, *request.PageLayoutReorder) (interface{}, error)
Delete(context.Context, *request.PageLayoutDelete) (interface{}, error)
Undelete(context.Context, *request.PageLayoutUndelete) (interface{}, error)
ListTranslations(context.Context, *request.PageLayoutListTranslations) (interface{}, error)
UpdateTranslations(context.Context, *request.PageLayoutUpdateTranslations) (interface{}, error)
}
// HTTP API interface
PageLayout struct {
ListNamespace func(http.ResponseWriter, *http.Request)
List func(http.ResponseWriter, *http.Request)
Create func(http.ResponseWriter, *http.Request)
Read func(http.ResponseWriter, *http.Request)
Update func(http.ResponseWriter, *http.Request)
Reorder func(http.ResponseWriter, *http.Request)
Delete func(http.ResponseWriter, *http.Request)
Undelete func(http.ResponseWriter, *http.Request)
ListTranslations func(http.ResponseWriter, *http.Request)
UpdateTranslations func(http.ResponseWriter, *http.Request)
}
)
func NewPageLayout(h PageLayoutAPI) *PageLayout {
return &PageLayout{
ListNamespace: func(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close()
params := request.NewPageLayoutListNamespace()
if err := params.Fill(r); err != nil {
api.Send(w, r, err)
return
}
value, err := h.ListNamespace(r.Context(), params)
if err != nil {
api.Send(w, r, err)
return
}
api.Send(w, r, value)
},
List: func(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close()
params := request.NewPageLayoutList()
if err := params.Fill(r); err != nil {
api.Send(w, r, err)
return
}
value, err := h.List(r.Context(), params)
if err != nil {
api.Send(w, r, err)
return
}
api.Send(w, r, value)
},
Create: func(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close()
params := request.NewPageLayoutCreate()
if err := params.Fill(r); err != nil {
api.Send(w, r, err)
return
}
value, err := h.Create(r.Context(), params)
if err != nil {
api.Send(w, r, err)
return
}
api.Send(w, r, value)
},
Read: func(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close()
params := request.NewPageLayoutRead()
if err := params.Fill(r); err != nil {
api.Send(w, r, err)
return
}
value, err := h.Read(r.Context(), params)
if err != nil {
api.Send(w, r, err)
return
}
api.Send(w, r, value)
},
Update: func(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close()
params := request.NewPageLayoutUpdate()
if err := params.Fill(r); err != nil {
api.Send(w, r, err)
return
}
value, err := h.Update(r.Context(), params)
if err != nil {
api.Send(w, r, err)
return
}
api.Send(w, r, value)
},
Reorder: func(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close()
params := request.NewPageLayoutReorder()
if err := params.Fill(r); err != nil {
api.Send(w, r, err)
return
}
value, err := h.Reorder(r.Context(), params)
if err != nil {
api.Send(w, r, err)
return
}
api.Send(w, r, value)
},
Delete: func(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close()
params := request.NewPageLayoutDelete()
if err := params.Fill(r); err != nil {
api.Send(w, r, err)
return
}
value, err := h.Delete(r.Context(), params)
if err != nil {
api.Send(w, r, err)
return
}
api.Send(w, r, value)
},
Undelete: func(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close()
params := request.NewPageLayoutUndelete()
if err := params.Fill(r); err != nil {
api.Send(w, r, err)
return
}
value, err := h.Undelete(r.Context(), params)
if err != nil {
api.Send(w, r, err)
return
}
api.Send(w, r, value)
},
ListTranslations: func(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close()
params := request.NewPageLayoutListTranslations()
if err := params.Fill(r); err != nil {
api.Send(w, r, err)
return
}
value, err := h.ListTranslations(r.Context(), params)
if err != nil {
api.Send(w, r, err)
return
}
api.Send(w, r, value)
},
UpdateTranslations: func(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close()
params := request.NewPageLayoutUpdateTranslations()
if err := params.Fill(r); err != nil {
api.Send(w, r, err)
return
}
value, err := h.UpdateTranslations(r.Context(), params)
if err != nil {
api.Send(w, r, err)
return
}
api.Send(w, r, value)
},
}
}
func (h PageLayout) MountRoutes(r chi.Router, middlewares ...func(http.Handler) http.Handler) {
r.Group(func(r chi.Router) {
r.Use(middlewares...)
r.Get("/namespace/{namespaceID}/page-layout", h.ListNamespace)
r.Get("/namespace/{namespaceID}/page/{pageID}/layout/", h.List)
r.Post("/namespace/{namespaceID}/page/{pageID}/layout/", h.Create)
r.Get("/namespace/{namespaceID}/page/{pageID}/layout/{pageLayoutID}", h.Read)
r.Post("/namespace/{namespaceID}/page/{pageID}/layout/{pageLayoutID}", h.Update)
r.Post("/namespace/{namespaceID}/page/{pageID}/layout/reorder", h.Reorder)
r.Delete("/namespace/{namespaceID}/page/{pageID}/layout/{pageLayoutID}", h.Delete)
r.Post("/namespace/{namespaceID}/page/{pageID}/layout/{pageLayoutID}/undelete", h.Undelete)
r.Get("/namespace/{namespaceID}/page/{pageID}/layout/{pageLayoutID}/translation", h.ListTranslations)
r.Patch("/namespace/{namespaceID}/page/{pageID}/layout/{pageLayoutID}/translation", h.UpdateTranslations)
})
}

View File

@@ -0,0 +1,152 @@
package handlers
// 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:
//
import (
"context"
"github.com/cortezaproject/corteza/server/compose/rest/request"
"github.com/cortezaproject/corteza/server/pkg/api"
"github.com/go-chi/chi/v5"
"net/http"
)
type (
// Internal API interface
PermissionsAPI interface {
List(context.Context, *request.PermissionsList) (interface{}, error)
Effective(context.Context, *request.PermissionsEffective) (interface{}, error)
Trace(context.Context, *request.PermissionsTrace) (interface{}, error)
Read(context.Context, *request.PermissionsRead) (interface{}, error)
Delete(context.Context, *request.PermissionsDelete) (interface{}, error)
Update(context.Context, *request.PermissionsUpdate) (interface{}, error)
}
// HTTP API interface
Permissions struct {
List func(http.ResponseWriter, *http.Request)
Effective func(http.ResponseWriter, *http.Request)
Trace func(http.ResponseWriter, *http.Request)
Read func(http.ResponseWriter, *http.Request)
Delete func(http.ResponseWriter, *http.Request)
Update func(http.ResponseWriter, *http.Request)
}
)
func NewPermissions(h PermissionsAPI) *Permissions {
return &Permissions{
List: func(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close()
params := request.NewPermissionsList()
if err := params.Fill(r); err != nil {
api.Send(w, r, err)
return
}
value, err := h.List(r.Context(), params)
if err != nil {
api.Send(w, r, err)
return
}
api.Send(w, r, value)
},
Effective: func(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close()
params := request.NewPermissionsEffective()
if err := params.Fill(r); err != nil {
api.Send(w, r, err)
return
}
value, err := h.Effective(r.Context(), params)
if err != nil {
api.Send(w, r, err)
return
}
api.Send(w, r, value)
},
Trace: func(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close()
params := request.NewPermissionsTrace()
if err := params.Fill(r); err != nil {
api.Send(w, r, err)
return
}
value, err := h.Trace(r.Context(), params)
if err != nil {
api.Send(w, r, err)
return
}
api.Send(w, r, value)
},
Read: func(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close()
params := request.NewPermissionsRead()
if err := params.Fill(r); err != nil {
api.Send(w, r, err)
return
}
value, err := h.Read(r.Context(), params)
if err != nil {
api.Send(w, r, err)
return
}
api.Send(w, r, value)
},
Delete: func(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close()
params := request.NewPermissionsDelete()
if err := params.Fill(r); err != nil {
api.Send(w, r, err)
return
}
value, err := h.Delete(r.Context(), params)
if err != nil {
api.Send(w, r, err)
return
}
api.Send(w, r, value)
},
Update: func(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close()
params := request.NewPermissionsUpdate()
if err := params.Fill(r); err != nil {
api.Send(w, r, err)
return
}
value, err := h.Update(r.Context(), params)
if err != nil {
api.Send(w, r, err)
return
}
api.Send(w, r, value)
},
}
}
func (h Permissions) MountRoutes(r chi.Router, middlewares ...func(http.Handler) http.Handler) {
r.Group(func(r chi.Router) {
r.Use(middlewares...)
r.Get("/permissions/", h.List)
r.Get("/permissions/effective", h.Effective)
r.Get("/permissions/trace", h.Trace)
r.Get("/permissions/{roleID}/rules", h.Read)
r.Delete("/permissions/{roleID}/rules", h.Delete)
r.Patch("/permissions/{roleID}/rules", h.Update)
})
}

View File

@@ -0,0 +1,437 @@
package handlers
// 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:
//
import (
"context"
"github.com/cortezaproject/corteza/server/compose/rest/request"
"github.com/cortezaproject/corteza/server/pkg/api"
"github.com/go-chi/chi/v5"
"net/http"
)
type (
// Internal API interface
RecordAPI interface {
Report(context.Context, *request.RecordReport) (interface{}, error)
List(context.Context, *request.RecordList) (interface{}, error)
ImportInit(context.Context, *request.RecordImportInit) (interface{}, error)
ImportRun(context.Context, *request.RecordImportRun) (interface{}, error)
ImportProgress(context.Context, *request.RecordImportProgress) (interface{}, error)
ExportInit(context.Context, *request.RecordExportInit) (interface{}, error)
ExportPull(context.Context, *request.RecordExportPull) (interface{}, error)
Export(context.Context, *request.RecordExport) (interface{}, error)
Exec(context.Context, *request.RecordExec) (interface{}, error)
Create(context.Context, *request.RecordCreate) (interface{}, error)
Read(context.Context, *request.RecordRead) (interface{}, error)
Update(context.Context, *request.RecordUpdate) (interface{}, error)
Patch(context.Context, *request.RecordPatch) (interface{}, error)
BulkDelete(context.Context, *request.RecordBulkDelete) (interface{}, error)
Delete(context.Context, *request.RecordDelete) (interface{}, error)
Undelete(context.Context, *request.RecordUndelete) (interface{}, error)
BulkUndelete(context.Context, *request.RecordBulkUndelete) (interface{}, error)
Upload(context.Context, *request.RecordUpload) (interface{}, error)
TriggerScript(context.Context, *request.RecordTriggerScript) (interface{}, error)
TriggerScriptOnList(context.Context, *request.RecordTriggerScriptOnList) (interface{}, error)
Revisions(context.Context, *request.RecordRevisions) (interface{}, error)
}
// HTTP API interface
Record struct {
Report func(http.ResponseWriter, *http.Request)
List func(http.ResponseWriter, *http.Request)
ImportInit func(http.ResponseWriter, *http.Request)
ImportRun func(http.ResponseWriter, *http.Request)
ImportProgress func(http.ResponseWriter, *http.Request)
ExportInit func(http.ResponseWriter, *http.Request)
ExportPull func(http.ResponseWriter, *http.Request)
Export func(http.ResponseWriter, *http.Request)
Exec func(http.ResponseWriter, *http.Request)
Create func(http.ResponseWriter, *http.Request)
Read func(http.ResponseWriter, *http.Request)
Update func(http.ResponseWriter, *http.Request)
Patch func(http.ResponseWriter, *http.Request)
BulkDelete func(http.ResponseWriter, *http.Request)
Delete func(http.ResponseWriter, *http.Request)
Undelete func(http.ResponseWriter, *http.Request)
BulkUndelete func(http.ResponseWriter, *http.Request)
Upload func(http.ResponseWriter, *http.Request)
TriggerScript func(http.ResponseWriter, *http.Request)
TriggerScriptOnList func(http.ResponseWriter, *http.Request)
Revisions func(http.ResponseWriter, *http.Request)
}
)
func NewRecord(h RecordAPI) *Record {
return &Record{
Report: func(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close()
params := request.NewRecordReport()
if err := params.Fill(r); err != nil {
api.Send(w, r, err)
return
}
value, err := h.Report(r.Context(), params)
if err != nil {
api.Send(w, r, err)
return
}
api.Send(w, r, value)
},
List: func(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close()
params := request.NewRecordList()
if err := params.Fill(r); err != nil {
api.Send(w, r, err)
return
}
value, err := h.List(r.Context(), params)
if err != nil {
api.Send(w, r, err)
return
}
api.Send(w, r, value)
},
ImportInit: func(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close()
params := request.NewRecordImportInit()
if err := params.Fill(r); err != nil {
api.Send(w, r, err)
return
}
value, err := h.ImportInit(r.Context(), params)
if err != nil {
api.Send(w, r, err)
return
}
api.Send(w, r, value)
},
ImportRun: func(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close()
params := request.NewRecordImportRun()
if err := params.Fill(r); err != nil {
api.Send(w, r, err)
return
}
value, err := h.ImportRun(r.Context(), params)
if err != nil {
api.Send(w, r, err)
return
}
api.Send(w, r, value)
},
ImportProgress: func(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close()
params := request.NewRecordImportProgress()
if err := params.Fill(r); err != nil {
api.Send(w, r, err)
return
}
value, err := h.ImportProgress(r.Context(), params)
if err != nil {
api.Send(w, r, err)
return
}
api.Send(w, r, value)
},
ExportInit: func(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close()
params := request.NewRecordExportInit()
if err := params.Fill(r); err != nil {
api.Send(w, r, err)
return
}
value, err := h.ExportInit(r.Context(), params)
if err != nil {
api.Send(w, r, err)
return
}
api.Send(w, r, value)
},
ExportPull: func(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close()
params := request.NewRecordExportPull()
if err := params.Fill(r); err != nil {
api.Send(w, r, err)
return
}
value, err := h.ExportPull(r.Context(), params)
if err != nil {
api.Send(w, r, err)
return
}
api.Send(w, r, value)
},
Export: func(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close()
params := request.NewRecordExport()
if err := params.Fill(r); err != nil {
api.Send(w, r, err)
return
}
value, err := h.Export(r.Context(), params)
if err != nil {
api.Send(w, r, err)
return
}
api.Send(w, r, value)
},
Exec: func(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close()
params := request.NewRecordExec()
if err := params.Fill(r); err != nil {
api.Send(w, r, err)
return
}
value, err := h.Exec(r.Context(), params)
if err != nil {
api.Send(w, r, err)
return
}
api.Send(w, r, value)
},
Create: func(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close()
params := request.NewRecordCreate()
if err := params.Fill(r); err != nil {
api.Send(w, r, err)
return
}
value, err := h.Create(r.Context(), params)
if err != nil {
api.Send(w, r, err)
return
}
api.Send(w, r, value)
},
Read: func(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close()
params := request.NewRecordRead()
if err := params.Fill(r); err != nil {
api.Send(w, r, err)
return
}
value, err := h.Read(r.Context(), params)
if err != nil {
api.Send(w, r, err)
return
}
api.Send(w, r, value)
},
Update: func(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close()
params := request.NewRecordUpdate()
if err := params.Fill(r); err != nil {
api.Send(w, r, err)
return
}
value, err := h.Update(r.Context(), params)
if err != nil {
api.Send(w, r, err)
return
}
api.Send(w, r, value)
},
Patch: func(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close()
params := request.NewRecordPatch()
if err := params.Fill(r); err != nil {
api.Send(w, r, err)
return
}
value, err := h.Patch(r.Context(), params)
if err != nil {
api.Send(w, r, err)
return
}
api.Send(w, r, value)
},
BulkDelete: func(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close()
params := request.NewRecordBulkDelete()
if err := params.Fill(r); err != nil {
api.Send(w, r, err)
return
}
value, err := h.BulkDelete(r.Context(), params)
if err != nil {
api.Send(w, r, err)
return
}
api.Send(w, r, value)
},
Delete: func(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close()
params := request.NewRecordDelete()
if err := params.Fill(r); err != nil {
api.Send(w, r, err)
return
}
value, err := h.Delete(r.Context(), params)
if err != nil {
api.Send(w, r, err)
return
}
api.Send(w, r, value)
},
Undelete: func(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close()
params := request.NewRecordUndelete()
if err := params.Fill(r); err != nil {
api.Send(w, r, err)
return
}
value, err := h.Undelete(r.Context(), params)
if err != nil {
api.Send(w, r, err)
return
}
api.Send(w, r, value)
},
BulkUndelete: func(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close()
params := request.NewRecordBulkUndelete()
if err := params.Fill(r); err != nil {
api.Send(w, r, err)
return
}
value, err := h.BulkUndelete(r.Context(), params)
if err != nil {
api.Send(w, r, err)
return
}
api.Send(w, r, value)
},
Upload: func(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close()
params := request.NewRecordUpload()
if err := params.Fill(r); err != nil {
api.Send(w, r, err)
return
}
value, err := h.Upload(r.Context(), params)
if err != nil {
api.Send(w, r, err)
return
}
api.Send(w, r, value)
},
TriggerScript: func(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close()
params := request.NewRecordTriggerScript()
if err := params.Fill(r); err != nil {
api.Send(w, r, err)
return
}
value, err := h.TriggerScript(r.Context(), params)
if err != nil {
api.Send(w, r, err)
return
}
api.Send(w, r, value)
},
TriggerScriptOnList: func(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close()
params := request.NewRecordTriggerScriptOnList()
if err := params.Fill(r); err != nil {
api.Send(w, r, err)
return
}
value, err := h.TriggerScriptOnList(r.Context(), params)
if err != nil {
api.Send(w, r, err)
return
}
api.Send(w, r, value)
},
Revisions: func(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close()
params := request.NewRecordRevisions()
if err := params.Fill(r); err != nil {
api.Send(w, r, err)
return
}
value, err := h.Revisions(r.Context(), params)
if err != nil {
api.Send(w, r, err)
return
}
api.Send(w, r, value)
},
}
}
func (h Record) MountRoutes(r chi.Router, middlewares ...func(http.Handler) http.Handler) {
r.Group(func(r chi.Router) {
r.Use(middlewares...)
r.Get("/namespace/{namespaceID}/module/{moduleID}/record/report", h.Report)
r.Get("/namespace/{namespaceID}/module/{moduleID}/record/", h.List)
r.Post("/namespace/{namespaceID}/module/{moduleID}/record/import", h.ImportInit)
r.Patch("/namespace/{namespaceID}/module/{moduleID}/record/import/{sessionID}", h.ImportRun)
r.Get("/namespace/{namespaceID}/module/{moduleID}/record/import/{sessionID}", h.ImportProgress)
r.Post("/namespace/{namespaceID}/module/{moduleID}/record/export", h.ExportInit)
r.Get("/namespace/{namespaceID}/module/{moduleID}/record/export/{sessionID}/{filename}.{ext}", h.ExportPull)
r.Get("/namespace/{namespaceID}/module/{moduleID}/record/export{filename}.{ext}", h.Export)
r.Post("/namespace/{namespaceID}/module/{moduleID}/record/exec/{procedure}", h.Exec)
r.Post("/namespace/{namespaceID}/module/{moduleID}/record/", h.Create)
r.Get("/namespace/{namespaceID}/module/{moduleID}/record/{recordID}", h.Read)
r.Post("/namespace/{namespaceID}/module/{moduleID}/record/{recordID}", h.Update)
r.Patch("/namespace/{namespaceID}/module/{moduleID}/record/", h.Patch)
r.Delete("/namespace/{namespaceID}/module/{moduleID}/record/", h.BulkDelete)
r.Delete("/namespace/{namespaceID}/module/{moduleID}/record/{recordID}", h.Delete)
r.Post("/namespace/{namespaceID}/module/{moduleID}/record/{recordID}/undelete", h.Undelete)
r.Patch("/namespace/{namespaceID}/module/{moduleID}/record/undelete", h.BulkUndelete)
r.Post("/namespace/{namespaceID}/module/{moduleID}/record/attachment", h.Upload)
r.Post("/namespace/{namespaceID}/module/{moduleID}/record/{recordID}/trigger", h.TriggerScript)
r.Post("/namespace/{namespaceID}/module/{moduleID}/record/trigger", h.TriggerScriptOnList)
r.Get("/namespace/{namespaceID}/module/{moduleID}/record/{recordID}/revisions", h.Revisions)
})
}

126
server/compose/rest/icon.go Normal file
View File

@@ -0,0 +1,126 @@
package rest
import (
"context"
"github.com/cortezaproject/corteza/server/pkg/api"
"github.com/cortezaproject/corteza/server/pkg/auth"
"github.com/cortezaproject/corteza/server/pkg/errors"
"mime/multipart"
"github.com/cortezaproject/corteza/server/compose/rest/request"
"github.com/cortezaproject/corteza/server/compose/service"
"github.com/cortezaproject/corteza/server/compose/types"
"github.com/cortezaproject/corteza/server/pkg/filter"
)
type (
iconPayload struct {
*attachmentPayload
}
iconSetPayload struct {
Filter types.IconFilter `json:"filter"`
Set []*iconPayload `json:"set"`
}
Icon struct {
locale service.ResourceTranslationsManagerService
attachment service.AttachmentService
ac iconAccessController
}
iconAccessController interface {
CanGrant(context.Context) bool
}
)
func (Icon) New() *Icon {
return &Icon{
locale: service.DefaultResourceTranslation,
attachment: service.DefaultAttachment,
ac: service.DefaultAccessControl,
}
}
func (ctrl *Icon) List(ctx context.Context, r *request.IconList) (interface{}, error) {
var (
err error
f = types.AttachmentFilter{
Kind: types.IconAttachment,
}
set types.AttachmentSet
)
if f.Paging, err = filter.NewPaging(r.Limit, r.PageCursor); err != nil {
return nil, err
}
if f.Sorting, err = filter.NewSorting(r.Sort); err != nil {
return nil, err
}
//Get only the undeleted icons
f.Deleted = filter.StateExcluded
set, f, err = ctrl.attachment.Find(ctx, f)
return ctrl.makeIconFilterPayload(ctx, set, f, err)
}
func (ctrl *Icon) Upload(ctx context.Context, r *request.IconUpload) (interface{}, error) {
file, err := r.Icon.Open()
if err != nil {
return nil, err
}
defer func(file multipart.File) {
err = file.Close()
if err != nil {
return
}
}(file)
a, err := ctrl.attachment.CreateIconAttachment(
ctx,
r.Icon.Filename,
r.Icon.Size,
file,
)
return makeAttachmentPayload(ctx, a, err)
}
func (ctrl *Icon) makeIconFilterPayload(ctx context.Context, nn types.AttachmentSet, f types.AttachmentFilter, err error) (*iconSetPayload, error) {
if err != nil {
return nil, err
}
var (
a *attachmentPayload
ff types.IconFilter
)
ff.Paging = f.Paging
ff.Sorting = f.Sorting
res := &iconSetPayload{Filter: ff, Set: make([]*iconPayload, len(nn))}
for i := range nn {
a, _ = makeAttachmentPayload(ctx, nn[i], nil)
res.Set[i] = &iconPayload{a}
}
return res, nil
}
func (ctrl *Icon) Delete(ctx context.Context, r *request.IconDelete) (interface{}, error) {
if !auth.GetIdentityFromContext(ctx).Valid() {
return nil, errors.Unauthorized("cannot delete icon")
}
_, err := ctrl.attachment.FindByID(ctx, 0, r.IconID)
if err != nil {
return nil, err
}
return api.OK(), ctrl.attachment.DeleteByID(ctx, 0, r.IconID)
}

View File

@@ -0,0 +1,227 @@
package rest
import (
"context"
"github.com/cortezaproject/corteza/server/compose/rest/request"
"github.com/cortezaproject/corteza/server/compose/service"
"github.com/cortezaproject/corteza/server/compose/service/event"
"github.com/cortezaproject/corteza/server/compose/types"
"github.com/cortezaproject/corteza/server/pkg/api"
"github.com/cortezaproject/corteza/server/pkg/corredor"
"github.com/cortezaproject/corteza/server/pkg/filter"
)
type (
moduleSetPayload struct {
Filter types.ModuleFilter `json:"filter"`
Set []*modulePayload `json:"set"`
}
modulePayload struct {
*types.Module
Fields []*moduleFieldPayload `json:"fields"`
CanGrant bool `json:"canGrant"`
CanUpdateModule bool `json:"canUpdateModule"`
CanDeleteModule bool `json:"canDeleteModule"`
CanCreateRecord bool `json:"canCreateRecord"`
CanCreateOwnedRecord bool `json:"canCreateOwnedRecord"`
}
moduleFieldPayload struct {
*types.ModuleField
CanReadRecordValue bool `json:"canReadRecordValue"`
CanUpdateRecordValue bool `json:"canUpdateRecordValue"`
}
Module struct {
module service.ModuleService
locale service.ResourceTranslationsManagerService
namespace service.NamespaceService
ac moduleAccessController
}
moduleAccessController interface {
CanGrant(context.Context) bool
CanUpdateModule(context.Context, *types.Module) bool
CanDeleteModule(context.Context, *types.Module) bool
CanCreateRecordOnModule(context.Context, *types.Module) bool
CanCreateOwnedRecordOnModule(context.Context, *types.Module) bool
CanReadRecord(context.Context, *types.Record) bool
CanReadRecordValueOnModuleField(context.Context, *types.ModuleField) bool
CanUpdateRecordValueOnModuleField(context.Context, *types.ModuleField) bool
}
)
func (Module) New() *Module {
return &Module{
module: service.DefaultModule,
namespace: service.DefaultNamespace,
ac: service.DefaultAccessControl,
locale: service.DefaultResourceTranslation,
}
}
func (ctrl *Module) List(ctx context.Context, r *request.ModuleList) (interface{}, error) {
var (
err error
f = types.ModuleFilter{
NamespaceID: r.NamespaceID,
Query: r.Query,
Name: r.Name,
Handle: r.Handle,
Labels: r.Labels,
}
)
if f.Paging, err = filter.NewPaging(r.Limit, r.PageCursor); err != nil {
return nil, err
}
f.IncTotal = r.IncTotal
if f.Sorting, err = filter.NewSorting(r.Sort); err != nil {
return nil, err
}
set, filter, err := ctrl.module.Find(ctx, f)
return ctrl.makeFilterPayload(ctx, set, filter, err)
}
func (ctrl *Module) Read(ctx context.Context, r *request.ModuleRead) (interface{}, error) {
mod, err := ctrl.module.FindByID(ctx, r.NamespaceID, r.ModuleID)
return ctrl.makePayload(ctx, mod, err)
}
func (ctrl *Module) ListTranslations(ctx context.Context, r *request.ModuleListTranslations) (interface{}, error) {
return ctrl.locale.Module(ctx, r.NamespaceID, r.ModuleID)
}
func (ctrl *Module) UpdateTranslations(ctx context.Context, r *request.ModuleUpdateTranslations) (interface{}, error) {
return api.OK(), ctrl.locale.Upsert(ctx, r.Translations)
}
func (ctrl *Module) Create(ctx context.Context, r *request.ModuleCreate) (interface{}, error) {
var (
err error
mod = &types.Module{
NamespaceID: r.NamespaceID,
Config: r.Config,
Name: r.Name,
Handle: r.Handle,
Fields: r.Fields,
Meta: r.Meta,
Labels: r.Labels,
}
)
mod, err = ctrl.module.Create(ctx, mod)
return ctrl.makePayload(ctx, mod, err)
}
func (ctrl *Module) Update(ctx context.Context, r *request.ModuleUpdate) (interface{}, error) {
var (
err error
mod = &types.Module{
ID: r.ModuleID,
NamespaceID: r.NamespaceID,
Config: r.Config,
Name: r.Name,
Handle: r.Handle,
Fields: r.Fields,
Meta: r.Meta,
Labels: r.Labels,
UpdatedAt: r.UpdatedAt,
}
)
mod, err = ctrl.module.Update(ctx, mod)
return ctrl.makePayload(ctx, mod, err)
}
func (ctrl *Module) Delete(ctx context.Context, r *request.ModuleDelete) (interface{}, error) {
_, err := ctrl.module.FindByID(ctx, r.NamespaceID, r.ModuleID)
if err != nil {
return nil, err
}
return api.OK(), ctrl.module.DeleteByID(ctx, r.NamespaceID, r.ModuleID)
}
func (ctrl *Module) TriggerScript(ctx context.Context, r *request.ModuleTriggerScript) (rsp interface{}, err error) {
var (
module *types.Module
namespace *types.Namespace
)
if module, err = ctrl.module.FindByID(ctx, r.NamespaceID, r.ModuleID); err != nil {
return
}
if namespace, err = ctrl.namespace.FindByID(ctx, r.NamespaceID); err != nil {
return
}
// @todo implement same behaviour as we have on record - module+oldModule
err = corredor.Service().Exec(ctx, r.Script, corredor.ExtendScriptArgs(event.ModuleOnManual(module, module, namespace), r.Args))
return ctrl.makePayload(ctx, module, err)
}
func (ctrl Module) makePayload(ctx context.Context, m *types.Module, err error) (*modulePayload, error) {
if err != nil || m == nil {
return nil, err
}
mfp, err := ctrl.makeFieldsPayload(ctx, m)
if err != nil {
return nil, err
}
return &modulePayload{
Module: m,
Fields: mfp,
CanGrant: ctrl.ac.CanGrant(ctx),
CanUpdateModule: ctrl.ac.CanUpdateModule(ctx, m),
CanDeleteModule: ctrl.ac.CanDeleteModule(ctx, m),
CanCreateRecord: ctrl.ac.CanCreateRecordOnModule(ctx, m),
CanCreateOwnedRecord: ctrl.ac.CanCreateOwnedRecordOnModule(ctx, m),
}, nil
}
func (ctrl Module) makeFieldsPayload(ctx context.Context, m *types.Module) (out []*moduleFieldPayload, err error) {
out = make([]*moduleFieldPayload, len(m.Fields))
for i, f := range m.Fields {
out[i] = &moduleFieldPayload{
ModuleField: f,
CanReadRecordValue: ctrl.ac.CanReadRecordValueOnModuleField(ctx, f),
CanUpdateRecordValue: ctrl.ac.CanUpdateRecordValueOnModuleField(ctx, f),
}
}
return
}
func (ctrl Module) makeFilterPayload(ctx context.Context, nn types.ModuleSet, f types.ModuleFilter, err error) (*moduleSetPayload, error) {
if err != nil {
return nil, err
}
msp := &moduleSetPayload{Filter: f, Set: make([]*modulePayload, len(nn))}
for i := range nn {
msp.Set[i], _ = ctrl.makePayload(ctx, nn[i], nil)
}
return msp, nil
}

View File

@@ -0,0 +1,600 @@
package rest
import (
"archive/zip"
"bytes"
"context"
"fmt"
"io"
"net/http"
"time"
automationTypes "github.com/cortezaproject/corteza/server/automation/types"
composeEnvoy "github.com/cortezaproject/corteza/server/compose/envoy"
"github.com/cortezaproject/corteza/server/compose/rest/request"
"github.com/cortezaproject/corteza/server/compose/service"
"github.com/cortezaproject/corteza/server/compose/service/event"
"github.com/cortezaproject/corteza/server/compose/types"
"github.com/cortezaproject/corteza/server/pkg/api"
"github.com/cortezaproject/corteza/server/pkg/corredor"
"github.com/cortezaproject/corteza/server/pkg/envoyx"
"github.com/cortezaproject/corteza/server/pkg/filter"
"github.com/cortezaproject/corteza/server/pkg/locale"
"github.com/cortezaproject/corteza/server/pkg/rbac"
systemEnvoy "github.com/cortezaproject/corteza/server/system/envoy"
systemService "github.com/cortezaproject/corteza/server/system/service"
systemTypes "github.com/cortezaproject/corteza/server/system/types"
)
type (
namespacePayload struct {
*types.Namespace
CanGrant bool `json:"canGrant"`
CanExportNamespace bool `json:"canExportNamespace"`
CanUpdateNamespace bool `json:"canUpdateNamespace"`
CanDeleteNamespace bool `json:"canDeleteNamespace"`
CanManageNamespace bool `json:"canManageNamespace"`
CanCreateModule bool `json:"canCreateModule"`
CanExportModules bool `json:"canExportModules"`
CanCreateChart bool `json:"canCreateChart"`
CanExportCharts bool `json:"canExportCharts"`
CanCreatePage bool `json:"canCreatePage"`
}
namespaceSetPayload struct {
Filter types.NamespaceFilter `json:"filter"`
Set []*namespacePayload `json:"set"`
}
pageFinder interface {
Find(ctx context.Context, filter types.PageFilter) (set types.PageSet, f types.PageFilter, err error)
}
pageLayoutFinder interface {
Find(ctx context.Context, filter types.PageLayoutFilter) (set types.PageLayoutSet, f types.PageLayoutFilter, err error)
}
chartFinder interface {
Find(ctx context.Context, filter types.ChartFilter) (set types.ChartSet, f types.ChartFilter, err error)
}
Namespace struct {
namespace service.NamespaceService
module service.ModuleService
page pageFinder
pageLayout pageLayoutFinder
chart chartFinder
locale service.ResourceTranslationsManagerService
attachment service.AttachmentService
role systemService.RoleService
ac namespaceAccessController
}
namespaceAccessController interface {
CanGrant(context.Context) bool
CanExportNamespace(context.Context, *types.Namespace) bool
CanUpdateNamespace(context.Context, *types.Namespace) bool
CanDeleteNamespace(context.Context, *types.Namespace) bool
CanManageNamespace(context.Context, *types.Namespace) bool
CanCreateModuleOnNamespace(context.Context, *types.Namespace) bool
CanExportModulesOnNamespace(context.Context, *types.Namespace) bool
CanCreateChartOnNamespace(context.Context, *types.Namespace) bool
CanExportChartsOnNamespace(context.Context, *types.Namespace) bool
CanCreatePageOnNamespace(context.Context, *types.Namespace) bool
}
)
func (Namespace) New() *Namespace {
return &Namespace{
namespace: service.DefaultNamespace,
module: service.DefaultModule,
page: service.DefaultPage,
pageLayout: service.DefaultPageLayout,
chart: service.DefaultChart,
locale: service.DefaultResourceTranslation,
role: systemService.DefaultRole,
attachment: service.DefaultAttachment,
ac: service.DefaultAccessControl,
}
}
func (ctrl Namespace) List(ctx context.Context, r *request.NamespaceList) (interface{}, error) {
var (
err error
f = types.NamespaceFilter{
Query: r.Query,
Slug: r.Slug,
Labels: r.Labels,
}
)
if f.Paging, err = filter.NewPaging(r.Limit, r.PageCursor); err != nil {
return nil, err
}
f.IncTotal = r.IncTotal
if f.Sorting, err = filter.NewSorting(r.Sort); err != nil {
return nil, err
}
set, filter, err := ctrl.namespace.Find(ctx, f)
return ctrl.makeFilterPayload(ctx, set, filter, err)
}
func (ctrl Namespace) Create(ctx context.Context, r *request.NamespaceCreate) (interface{}, error) {
var (
err error
ns = &types.Namespace{
Name: r.Name,
Slug: r.Slug,
Enabled: r.Enabled,
Labels: r.Labels,
}
)
if err = r.Meta.Unmarshal(&ns.Meta); err != nil {
return nil, err
}
ns, err = ctrl.namespace.Create(ctx, ns)
return ctrl.makePayload(ctx, ns, err)
}
func (ctrl Namespace) Read(ctx context.Context, r *request.NamespaceRead) (interface{}, error) {
ns, err := ctrl.namespace.FindByID(ctx, r.NamespaceID)
return ctrl.makePayload(ctx, ns, err)
}
func (ctrl Namespace) ListTranslations(ctx context.Context, r *request.NamespaceListTranslations) (interface{}, error) {
return ctrl.locale.Namespace(ctx, r.NamespaceID)
}
func (ctrl Namespace) UpdateTranslations(ctx context.Context, r *request.NamespaceUpdateTranslations) (interface{}, error) {
return api.OK(), ctrl.locale.Upsert(ctx, r.Translations)
}
func (ctrl Namespace) Update(ctx context.Context, r *request.NamespaceUpdate) (interface{}, error) {
var (
err error
ns = &types.Namespace{
ID: r.NamespaceID,
Name: r.Name,
Slug: r.Slug,
Enabled: r.Enabled,
Labels: r.Labels,
UpdatedAt: r.UpdatedAt,
}
)
if err = r.Meta.Unmarshal(&ns.Meta); err != nil {
return nil, err
}
ns, err = ctrl.namespace.Update(ctx, ns)
return ctrl.makePayload(ctx, ns, err)
}
func (ctrl Namespace) Delete(ctx context.Context, r *request.NamespaceDelete) (interface{}, error) {
_, err := ctrl.namespace.FindByID(ctx, r.NamespaceID)
if err != nil {
return nil, err
}
return api.OK(), ctrl.namespace.DeleteByID(ctx, r.NamespaceID)
}
func (ctrl Namespace) Upload(ctx context.Context, r *request.NamespaceUpload) (interface{}, error) {
file, err := r.Upload.Open()
if err != nil {
return nil, err
}
defer file.Close()
a, err := ctrl.attachment.CreateNamespaceAttachment(
ctx,
r.Upload.Filename,
r.Upload.Size,
file,
)
if err != nil {
return nil, err
}
return makeAttachmentPayload(ctx, a, err)
}
func (ctrl Namespace) Clone(ctx context.Context, r *request.NamespaceClone) (interface{}, error) {
dup := &types.Namespace{
Name: r.Name,
Slug: r.Slug,
}
// @todo temporary workaround cause Envoy requires some identifiable thing
if dup.Slug == "" {
dup.Slug = fmt.Sprintf("cl_%d", r.NamespaceID)
}
nodes, err := ctrl.gatherNodes(ctx, r.NamespaceID)
if err != nil {
return nil, err
}
decoder := func() (envoyx.NodeSet, error) {
return nodes, nil
}
ns, err := ctrl.namespace.Clone(ctx, r.NamespaceID, dup, decoder)
if err != nil {
return nil, err
}
// @todo temporary workaround cause Envoy requires some identifiable thing
if r.Slug == "" {
ns.Slug = ""
ns, err = ctrl.namespace.Update(ctx, ns)
if err != nil {
return nil, err
}
}
return ctrl.makePayload(ctx, ns, err)
}
func (ctrl Namespace) Export(ctx context.Context, r *request.NamespaceExport) (out interface{}, err error) {
nodes, err := ctrl.gatherNodes(ctx, r.NamespaceID)
if err != nil {
return
}
p := envoyx.EncodeParams{
Type: envoyx.EncodeTypeIo,
Params: map[string]any{},
}
evsvc := envoyx.Global()
gg, err := evsvc.Bake(ctx, p, nil, nodes...)
if err != nil {
return
}
// Archive encoded resources
buf := bytes.NewBuffer(nil)
zw := zip.NewWriter(buf)
f, err := zw.Create(fmt.Sprintf("%s.yaml", r.Filename))
if err != nil {
return
}
p.Params["writer"] = f
err = evsvc.Encode(ctx, p, gg)
if err != nil {
return
}
err = zw.Close()
if err != nil {
return
}
return ctrl.serveExport(ctx, fmt.Sprintf("%s.zip", r.Filename), bytes.NewReader(buf.Bytes()), nil)
}
func (ctrl Namespace) ImportInit(ctx context.Context, r *request.NamespaceImportInit) (interface{}, error) {
f, err := r.Upload.Open()
if err != nil {
return nil, err
}
defer f.Close()
return ctrl.namespace.ImportInit(ctx, f, r.Upload.Size)
// return ctrl.namespace.ImportInit(ctx, f, r.Upload.Header.Get("content-type"), r.Upload.Size)
}
func (ctrl Namespace) ImportRun(ctx context.Context, r *request.NamespaceImportRun) (interface{}, error) {
var (
dup = &types.Namespace{
Name: r.Name,
Slug: r.Slug,
}
)
// @todo temporary workaround cause Envoy requires some identifiable thing
if dup.Slug == "" {
dup.Slug = fmt.Sprintf("cl_%d", r.SessionID)
}
ns, err := ctrl.namespace.ImportRun(ctx, r.SessionID, dup)
if err != nil {
return nil, err
}
// @todo temporary workaround cause Envoy requires some identifiable thing
if r.Slug == "" {
ns.Slug = ""
ns, err = ctrl.namespace.Update(ctx, ns)
if err != nil {
return nil, err
}
}
return ctrl.makePayload(ctx, ns, err)
}
func (ctrl Namespace) serveExport(ctx context.Context, fn string, archive io.ReadSeeker, err error) (interface{}, error) {
if err != nil {
return nil, err
}
return func(w http.ResponseWriter, req *http.Request) {
w.Header().Add("Content-Disposition", "attachment; filename="+fn)
http.ServeContent(w, req, fn, time.Now(), archive)
}, nil
}
func (ctrl *Namespace) TriggerScript(ctx context.Context, r *request.NamespaceTriggerScript) (rsp interface{}, err error) {
var (
namespace *types.Namespace
)
if namespace, err = ctrl.namespace.FindByID(ctx, r.NamespaceID); err != nil {
return
}
err = corredor.Service().Exec(ctx, r.Script, corredor.ExtendScriptArgs(event.NamespaceOnManual(namespace, nil), r.Args))
return ctrl.makePayload(ctx, namespace, err)
}
func (ctrl Namespace) makePayload(ctx context.Context, ns *types.Namespace, err error) (*namespacePayload, error) {
if err != nil || ns == nil {
return nil, err
}
return &namespacePayload{
Namespace: ns,
CanGrant: ctrl.ac.CanGrant(ctx),
CanExportNamespace: ctrl.ac.CanExportNamespace(ctx, ns),
CanUpdateNamespace: ctrl.ac.CanUpdateNamespace(ctx, ns),
CanDeleteNamespace: ctrl.ac.CanDeleteNamespace(ctx, ns),
CanManageNamespace: ctrl.ac.CanManageNamespace(ctx, ns),
CanCreateModule: ctrl.ac.CanCreateModuleOnNamespace(ctx, ns),
CanExportModules: ctrl.ac.CanExportModulesOnNamespace(ctx, ns),
CanCreateChart: ctrl.ac.CanCreateChartOnNamespace(ctx, ns),
CanExportCharts: ctrl.ac.CanExportChartsOnNamespace(ctx, ns),
CanCreatePage: ctrl.ac.CanCreatePageOnNamespace(ctx, ns),
}, nil
}
func (ctrl Namespace) makeFilterPayload(ctx context.Context, nn types.NamespaceSet, f types.NamespaceFilter, err error) (*namespaceSetPayload, error) {
if err != nil {
return nil, err
}
nsp := &namespaceSetPayload{Filter: f, Set: make([]*namespacePayload, len(nn))}
for i := range nn {
nsp.Set[i], _ = ctrl.makePayload(ctx, nn[i], nil)
}
return nsp, nil
}
func (ctrl Namespace) gatherNodes(ctx context.Context, namespaceID uint64) (resources envoyx.NodeSet, err error) {
var (
nsII envoyx.Identifiers
aux envoyx.NodeSet
)
// Prepare resources
aux, nsII, err = ctrl.exportCompose(ctx, namespaceID)
if err != nil {
return
}
resources = append(resources, aux...)
// Tweak exported resources
resources = ctrl.tweakExport(ctx, resources, nsII)
// Role placeholders for RBAC
aux, err = ctrl.preparePlaceholders(ctx)
if err != nil {
return
}
resources = append(resources, aux...)
// RBAC
aux, err = ctrl.exportRBAC(ctx, resources)
if err != nil {
return
}
resources = append(resources, aux...)
// Translations
aux, err = ctrl.exportResourceTranslations(ctx, resources)
if err != nil {
return
}
resources = append(resources, aux...)
return
}
func (ctrl Namespace) exportCompose(ctx context.Context, namespaceID uint64) (resources envoyx.NodeSet, nsII envoyx.Identifiers, err error) {
// - namespace
n, err := ctrl.namespace.FindByID(ctx, namespaceID)
if err != nil {
return
}
// @todo this isn't ok, will do for now
if !ctrl.ac.CanExportNamespace(ctx, n) {
err = fmt.Errorf("not allowed to export namespace %s", n.Name)
return
}
nsNode, err := composeEnvoy.NamespaceToEnvoyNode(n)
if err != nil {
return
}
nsII = nsNode.Identifiers
resources = append(resources, nsNode)
// - modules
mm, _, err := ctrl.module.Find(ctx, types.ModuleFilter{NamespaceID: n.ID})
if err != nil {
return
}
for _, m := range mm {
var aux *envoyx.Node
aux, err = composeEnvoy.ModuleToEnvoyNode(m)
if err != nil {
return
}
resources = append(resources, aux)
for _, f := range m.Fields {
aux, err = composeEnvoy.ModuleFieldToEnvoyNode(f)
if err != nil {
return
}
resources = append(resources, aux)
}
}
// - pages
pp, _, err := ctrl.page.Find(ctx, types.PageFilter{NamespaceID: n.ID})
if err != nil {
return
}
for _, p := range pp {
var aux *envoyx.Node
aux, err = composeEnvoy.PageToEnvoyNode(p)
if err != nil {
return
}
resources = append(resources, aux)
}
// - page layouts
ll, _, err := ctrl.pageLayout.Find(ctx, types.PageLayoutFilter{NamespaceID: n.ID})
if err != nil {
return
}
for _, l := range ll {
var aux *envoyx.Node
aux, err = composeEnvoy.PageLayoutToEnvoyNode(l)
if err != nil {
return
}
resources = append(resources, aux)
}
// - charts
cc, _, err := ctrl.chart.Find(ctx, types.ChartFilter{NamespaceID: n.ID})
if err != nil {
return
}
for _, c := range cc {
var aux *envoyx.Node
aux, err = composeEnvoy.ChartToEnvoyNode(c)
if err != nil {
return
}
resources = append(resources, aux)
}
return
}
func (ctrl Namespace) exportRBAC(ctx context.Context, base envoyx.NodeSet) (resources envoyx.NodeSet, err error) {
// Prepare RBAC Rules
rawRules := rbac.Global().Rules()
resources, err = envoyx.RBACRulesForNodes(rawRules, base...)
if err != nil {
return
}
return
}
func (ctrl Namespace) exportResourceTranslations(ctx context.Context, base envoyx.NodeSet) (resources envoyx.NodeSet, err error) {
var (
lsvc = locale.Global()
tags = lsvc.Tags()
translations = make([]*locale.ResourceTranslation, 0, 128)
resKeyTrans map[string]map[string]*locale.ResourceTranslation
)
for _, t := range tags {
resKeyTrans, err = lsvc.LoadResourceTranslations(ctx, t)
if err != nil {
return
}
for _, keyTrans := range resKeyTrans {
for _, trans := range keyTrans {
translations = append(translations, trans)
}
}
}
resources, err = envoyx.ResourceTranslationsForNodes(systemTypes.FromLocale(translations), base...)
return
}
func (ctrl Namespace) tweakExport(ctx context.Context, nodes envoyx.NodeSet, nsII envoyx.Identifiers) envoyx.NodeSet {
nsRef := envoyx.Ref{
ResourceType: types.NamespaceResourceType,
Identifiers: nsII,
Scope: envoyx.Scope{
ResourceType: types.NamespaceResourceType,
Identifiers: nsII,
},
}
nsNode := envoyx.NodeForRef(nsRef, nodes...)
// - remove logo and icon references as attachments are not exported by default
// @todo code in attachment exporting, most likely when we do attachment handling rework
ns := nsNode.Resource.(*types.Namespace)
ns.Meta.Icon = ""
ns.Meta.IconID = 0
ns.Meta.Logo = ""
ns.Meta.LogoID = 0
ns.Meta.LogoEnabled = false
// - prune resources we won't preserve
pref := envoyx.Ref{
ResourceType: automationTypes.WorkflowResourceType,
}
for _, n := range nodes {
n.Prune(pref)
}
return nodes
}
func (ctrl Namespace) preparePlaceholders(ctx context.Context) (resources envoyx.NodeSet, err error) {
rr, _, err := ctrl.role.Find(ctx, systemTypes.RoleFilter{})
if err != nil {
return
}
var aux *envoyx.Node
for _, role := range rr {
aux, err = systemEnvoy.RoleToEnvoyNode(role)
if err != nil {
return
}
aux.Placeholder = true
resources = append(resources, aux)
}
return
}

View File

@@ -0,0 +1,62 @@
package rest
import (
"context"
"fmt"
"github.com/cortezaproject/corteza/server/compose/rest/request"
"github.com/cortezaproject/corteza/server/compose/service"
"github.com/cortezaproject/corteza/server/compose/types"
)
type (
Notification struct {
svc notificationService
}
contentPayload struct {
Plain string `json:"plain"`
HTML string `json:"html"`
}
notificationService interface {
SendEmail(context.Context, *types.EmailNotification) error
}
)
func (Notification) New() *Notification {
return &Notification{
svc: service.DefaultNotification,
}
}
// EmailSend assembles Email Message and pushes message to notification service
func (ctrl *Notification) EmailSend(ctx context.Context, r *request.NotificationEmailSend) (interface{}, error) {
ntf := &types.EmailNotification{
To: r.To,
Cc: r.Cc,
ReplyTo: r.ReplyTo,
Subject: r.Subject,
RemoteAttachments: r.RemoteAttachments,
}
var cp = contentPayload{}
if err := r.Content.Unmarshal(&cp); err != nil {
return false, fmt.Errorf("could not unmarshal content: %w", err)
} else {
if len(cp.HTML) > 0 {
ntf.ContentHTML = cp.HTML
}
if len(cp.Plain) > 0 {
ntf.ContentPlain = cp.Plain
}
}
if err := ctrl.svc.SendEmail(ctx, ntf); err != nil {
return false, err
} else {
return true, nil
}
}

338
server/compose/rest/page.go Normal file
View File

@@ -0,0 +1,338 @@
package rest
import (
"context"
"strings"
"github.com/cortezaproject/corteza/server/compose/rest/request"
"github.com/cortezaproject/corteza/server/compose/service"
"github.com/cortezaproject/corteza/server/compose/service/event"
"github.com/cortezaproject/corteza/server/compose/types"
"github.com/cortezaproject/corteza/server/pkg/api"
"github.com/cortezaproject/corteza/server/pkg/corredor"
"github.com/cortezaproject/corteza/server/pkg/filter"
"github.com/cortezaproject/corteza/server/pkg/payload"
)
type (
pagePayload struct {
*types.Page
Children []*pagePayload `json:"children,omitempty"`
CanGrant bool `json:"canGrant"`
CanUpdatePage bool `json:"canUpdatePage"`
CanDeletePage bool `json:"canDeletePage"`
}
pageSetPayload struct {
Filter types.PageFilter `json:"filter"`
Set []*pagePayload `json:"set"`
}
pageIconPayload struct {
*types.PageConfigIcon
}
Page struct {
page interface {
FindByID(ctx context.Context, namespaceID, pageID uint64) (*types.Page, error)
FindByHandle(ctx context.Context, namespaceID uint64, handle string) (*types.Page, error)
FindByPageID(ctx context.Context, namespaceID, pageID uint64) (*types.Page, error)
FindBySelfID(ctx context.Context, namespaceID, selfID uint64) (pages types.PageSet, f types.PageFilter, err error)
Find(ctx context.Context, filter types.PageFilter) (set types.PageSet, f types.PageFilter, err error)
Tree(ctx context.Context, namespaceID uint64) (pages types.PageSet, err error)
Create(ctx context.Context, page *types.Page) (*types.Page, error)
Update(ctx context.Context, page *types.Page) (*types.Page, error)
DeleteByID(ctx context.Context, namespaceID, pageID uint64, pds types.PageChildrenDeleteStrategy) error
UpdateIcon(ctx context.Context, namespaceID, pageID uint64, icon *types.PageConfigIcon) (out *types.PageConfigIcon, err error)
Reorder(ctx context.Context, namespaceID, selfID uint64, pageIDs []uint64) error
}
locale service.ResourceTranslationsManagerService
namespace service.NamespaceService
attachment service.AttachmentService
ac pageAccessController
}
pageAccessController interface {
CanGrant(context.Context) bool
CanUpdatePage(context.Context, *types.Page) bool
CanDeletePage(context.Context, *types.Page) bool
}
)
func (Page) New() *Page {
return &Page{
page: service.DefaultPage,
locale: service.DefaultResourceTranslation,
namespace: service.DefaultNamespace,
attachment: service.DefaultAttachment,
ac: service.DefaultAccessControl,
}
}
func (ctrl *Page) List(ctx context.Context, r *request.PageList) (interface{}, error) {
var (
err error
f = types.PageFilter{
NamespaceID: r.NamespaceID,
ModuleID: r.ModuleID,
ParentID: r.SelfID,
Labels: r.Labels,
Handle: r.Handle,
Query: r.Query,
}
)
if f.Paging, err = filter.NewPaging(r.Limit, r.PageCursor); err != nil {
return nil, err
}
if f.Sorting, err = filter.NewSorting(r.Sort); err != nil {
return nil, err
}
set, filter, err := ctrl.page.Find(ctx, f)
return ctrl.makeFilterPayload(ctx, set, filter, err)
}
func (ctrl *Page) Tree(ctx context.Context, r *request.PageTree) (interface{}, error) {
tree, err := ctrl.page.Tree(ctx, r.NamespaceID)
return ctrl.makeTreePayload(ctx, tree, err)
}
func (ctrl *Page) Create(ctx context.Context, r *request.PageCreate) (interface{}, error) {
var (
err error
mod = &types.Page{
NamespaceID: r.NamespaceID,
SelfID: r.SelfID,
ModuleID: r.ModuleID,
Title: r.Title,
Handle: r.Handle,
Description: r.Description,
Visible: r.Visible,
Weight: r.Weight,
Labels: r.Labels,
Meta: r.Meta,
}
)
if len(r.Config) > 2 {
if err = r.Config.Unmarshal(&mod.Config); err != nil {
return nil, err
}
}
if len(r.Blocks) > 2 {
if err = r.Blocks.Unmarshal(&mod.Blocks); err != nil {
return nil, err
}
}
mod, err = ctrl.page.Create(ctx, mod)
return ctrl.makePayload(ctx, mod, err)
}
func (ctrl *Page) Read(ctx context.Context, r *request.PageRead) (interface{}, error) {
mod, err := ctrl.page.FindByID(ctx, r.NamespaceID, r.PageID)
return ctrl.makePayload(ctx, mod, err)
}
func (ctrl *Page) ListTranslations(ctx context.Context, r *request.PageListTranslations) (interface{}, error) {
return ctrl.locale.Page(ctx, r.NamespaceID, r.PageID)
}
func (ctrl *Page) UpdateTranslations(ctx context.Context, r *request.PageUpdateTranslations) (interface{}, error) {
return api.OK(), ctrl.locale.Upsert(ctx, r.Translations)
}
func (ctrl *Page) Reorder(ctx context.Context, r *request.PageReorder) (interface{}, error) {
return api.OK(), ctrl.page.Reorder(ctx, r.NamespaceID, r.SelfID, payload.ParseUint64s(r.PageIDs))
}
func (ctrl *Page) Update(ctx context.Context, r *request.PageUpdate) (interface{}, error) {
var (
err error
mod = &types.Page{
NamespaceID: r.NamespaceID,
ID: r.PageID,
SelfID: r.SelfID,
ModuleID: r.ModuleID,
Title: r.Title,
Handle: r.Handle,
Description: r.Description,
Visible: r.Visible,
Weight: r.Weight,
Labels: r.Labels,
Meta: r.Meta,
UpdatedAt: r.UpdatedAt,
}
)
if len(r.Config) > 2 {
// Process config if it was included in the request
// if not, do not assume that config has been removed!
if err = r.Config.Unmarshal(&mod.Config); err != nil {
return nil, err
}
}
if len(r.Blocks) > 2 {
// Process blocks if they were included in the request
// if not, do not assume that blocks were removed!
if err = r.Blocks.Unmarshal(&mod.Blocks); err != nil {
return nil, err
}
}
mod, err = ctrl.page.Update(ctx, mod)
return ctrl.makePayload(ctx, mod, err)
}
func (ctrl *Page) Delete(ctx context.Context, r *request.PageDelete) (interface{}, error) {
var strategy types.PageChildrenDeleteStrategy
switch aux := types.PageChildrenDeleteStrategy(r.Strategy); aux {
case types.PageChildrenOnDeleteForce,
types.PageChildrenOnDeleteRebase,
types.PageChildrenOnDeleteCascade:
strategy = aux
default:
strategy = types.PageChildrenOnDeleteAbort
}
return api.OK(), ctrl.page.DeleteByID(ctx, r.NamespaceID, r.PageID, strategy)
}
func (ctrl *Page) Upload(ctx context.Context, r *request.PageUpload) (interface{}, error) {
file, err := r.Upload.Open()
if err != nil {
return nil, err
}
defer file.Close()
a, err := ctrl.attachment.CreatePageAttachment(
ctx,
r.NamespaceID,
r.Upload.Filename,
r.Upload.Size,
file,
r.PageID,
)
return makeAttachmentPayload(ctx, a, err)
}
func (ctrl *Page) TriggerScript(ctx context.Context, r *request.PageTriggerScript) (rsp interface{}, err error) {
var (
page *types.Page
namespace *types.Namespace
)
if page, err = ctrl.page.FindByID(ctx, r.NamespaceID, r.PageID); err != nil {
return
}
if namespace, err = ctrl.namespace.FindByID(ctx, r.NamespaceID); err != nil {
return
}
// @todo implement same behaviour as we have on record - page+oldPage
err = corredor.Service().Exec(ctx, r.Script, corredor.ExtendScriptArgs(event.PageOnManual(page, page, namespace, nil), r.Args))
return ctrl.makePayload(ctx, page, err)
}
func (ctrl *Page) UpdateIcon(ctx context.Context, r *request.PageUpdateIcon) (interface{}, error) {
var (
err error
icon = &types.PageConfigIcon{
Type: r.Type,
Src: r.Source,
Style: func() map[string]string {
style := make(map[string]string, len(r.Style))
for k, v := range r.Style {
if v.Val != "" {
style[k] = v.Val
} else if len(v.Values) > 0 {
style[k] = strings.Join(v.Values, ",")
}
}
return style
}(),
}
)
icon, err = ctrl.page.UpdateIcon(ctx, r.NamespaceID, r.PageID, icon)
return ctrl.makeIconPayload(ctx, icon, err)
}
func (ctrl Page) makePayload(ctx context.Context, c *types.Page, err error) (*pagePayload, error) {
if err != nil || c == nil {
return nil, err
}
return &pagePayload{
Page: c,
CanGrant: ctrl.ac.CanGrant(ctx),
CanUpdatePage: ctrl.ac.CanUpdatePage(ctx, c),
CanDeletePage: ctrl.ac.CanDeletePage(ctx, c),
}, nil
}
func (ctrl Page) makeIconPayload(_ context.Context, i *types.PageConfigIcon, err error) (*pageIconPayload, error) {
if err != nil || i == nil {
return nil, err
}
return &pageIconPayload{
PageConfigIcon: i,
}, nil
}
func (ctrl Page) makeTreePayload(ctx context.Context, pp types.PageSet, err error) ([]*pagePayload, error) {
if err != nil {
return nil, err
}
set := make([]*pagePayload, len(pp))
for i := range pp {
set[i], err = ctrl.makePayload(ctx, pp[i], nil)
if err != nil {
return nil, err
}
if len(pp[i].Children) > 0 {
set[i].Children, err = ctrl.makeTreePayload(ctx, pp[i].Children, nil)
if err != nil {
return nil, err
}
}
}
return set, nil
}
func (ctrl Page) makeFilterPayload(ctx context.Context, nn types.PageSet, f types.PageFilter, err error) (*pageSetPayload, error) {
if err != nil {
return nil, err
}
modp := &pageSetPayload{Filter: f, Set: make([]*pagePayload, len(nn))}
for i := range nn {
modp.Set[i], _ = ctrl.makePayload(ctx, nn[i], nil)
}
return modp, nil
}

View File

@@ -0,0 +1,224 @@
package rest
import (
"context"
"github.com/cortezaproject/corteza/server/compose/rest/request"
"github.com/cortezaproject/corteza/server/compose/service"
"github.com/cortezaproject/corteza/server/compose/types"
"github.com/cortezaproject/corteza/server/pkg/api"
"github.com/cortezaproject/corteza/server/pkg/filter"
"github.com/cortezaproject/corteza/server/pkg/payload"
)
type (
pageLayoutPayload struct {
*types.PageLayout
// CanGrant bool `json:"canGrant"`
// CanUpdatePageLayout bool `json:"canUpdatePageLayout"`
// CanDeletePageLayout bool `json:"canDeletePageLayout"`
}
pageLayoutSetPayload struct {
Filter types.PageLayoutFilter `json:"filter"`
Set []*pageLayoutPayload `json:"set"`
}
PageLayout struct {
pageLayout interface {
FindByID(ctx context.Context, namespaceID, pageLayoutID uint64) (*types.PageLayout, error)
FindByHandle(ctx context.Context, namespaceID uint64, handle string) (*types.PageLayout, error)
FindByPageLayoutID(ctx context.Context, namespaceID, pageLayoutID uint64) (*types.PageLayout, error)
Find(ctx context.Context, filter types.PageLayoutFilter) (set types.PageLayoutSet, f types.PageLayoutFilter, err error)
Create(ctx context.Context, pageLayout *types.PageLayout) (*types.PageLayout, error)
Reorder(ctx context.Context, namespaceID uint64, pageID uint64, pageLayoutIDs []uint64) error
Update(ctx context.Context, pageLayout *types.PageLayout) (*types.PageLayout, error)
DeleteByID(ctx context.Context, namespaceID, pageID, pageLayoutID uint64) error
UndeleteByID(ctx context.Context, namespaceID, pageID, pageLayoutID uint64) error
}
locale service.ResourceTranslationsManagerService
namespace service.NamespaceService
ac pageLayoutAccessController
}
pageLayoutAccessController interface {
// @todo
}
)
func (PageLayout) New() *PageLayout {
return &PageLayout{
pageLayout: service.DefaultPageLayout,
locale: service.DefaultResourceTranslation,
namespace: service.DefaultNamespace,
ac: service.DefaultAccessControl,
}
}
func (ctrl *PageLayout) List(ctx context.Context, r *request.PageLayoutList) (interface{}, error) {
var (
err error
f = types.PageLayoutFilter{
NamespaceID: r.NamespaceID,
PageID: r.PageID,
Labels: r.Labels,
Handle: r.Handle,
Query: r.Query,
}
)
if f.Paging, err = filter.NewPaging(r.Limit, r.PageCursor); err != nil {
return nil, err
}
if f.Sorting, err = filter.NewSorting(r.Sort); err != nil {
return nil, err
}
set, filter, err := ctrl.pageLayout.Find(ctx, f)
return ctrl.makeFilterPayload(ctx, set, filter, err)
}
func (ctrl *PageLayout) ListNamespace(ctx context.Context, r *request.PageLayoutListNamespace) (interface{}, error) {
var (
err error
f = types.PageLayoutFilter{
NamespaceID: r.NamespaceID,
Labels: r.Labels,
Handle: r.Handle,
Query: r.Query,
}
)
if f.Paging, err = filter.NewPaging(r.Limit, r.PageCursor); err != nil {
return nil, err
}
if f.Sorting, err = filter.NewSorting(r.Sort); err != nil {
return nil, err
}
set, filter, err := ctrl.pageLayout.Find(ctx, f)
return ctrl.makeFilterPayload(ctx, set, filter, err)
}
func (ctrl *PageLayout) Create(ctx context.Context, r *request.PageLayoutCreate) (interface{}, error) {
var (
err error
layout = &types.PageLayout{
PageID: r.PageID,
ParentID: r.ParentID,
Weight: r.Weight,
NamespaceID: r.NamespaceID,
Handle: r.Handle,
Meta: r.Meta,
Labels: r.Labels,
OwnedBy: r.OwnedBy,
}
)
if len(r.Config) > 2 {
if err = r.Config.Unmarshal(&layout.Config); err != nil {
return nil, err
}
}
if len(r.Blocks) > 2 {
if err = r.Blocks.Unmarshal(&layout.Blocks); err != nil {
return nil, err
}
}
layout, err = ctrl.pageLayout.Create(ctx, layout)
return ctrl.makePayload(ctx, layout, err)
}
func (ctrl *PageLayout) Read(ctx context.Context, r *request.PageLayoutRead) (interface{}, error) {
mod, err := ctrl.pageLayout.FindByID(ctx, r.NamespaceID, r.PageLayoutID)
return ctrl.makePayload(ctx, mod, err)
}
func (ctrl *PageLayout) ListTranslations(ctx context.Context, r *request.PageLayoutListTranslations) (interface{}, error) {
return ctrl.locale.PageLayout(ctx, r.NamespaceID, r.PageID, r.PageLayoutID)
}
func (ctrl *PageLayout) UpdateTranslations(ctx context.Context, r *request.PageLayoutUpdateTranslations) (interface{}, error) {
return api.OK(), ctrl.locale.Upsert(ctx, r.Translations)
}
func (ctrl *PageLayout) Reorder(ctx context.Context, r *request.PageLayoutReorder) (interface{}, error) {
return api.OK(), ctrl.pageLayout.Reorder(ctx, r.NamespaceID, r.PageID, payload.ParseUint64s(r.PageIDs))
}
func (ctrl *PageLayout) Update(ctx context.Context, r *request.PageLayoutUpdate) (interface{}, error) {
var (
err error
mod = &types.PageLayout{
ID: r.PageLayoutID,
PageID: r.PageID,
Weight: r.Weight,
ParentID: r.ParentID,
NamespaceID: r.NamespaceID,
Handle: r.Handle,
Meta: r.Meta,
Labels: r.Labels,
OwnedBy: r.OwnedBy,
UpdatedAt: r.UpdatedAt,
}
)
if len(r.Config) > 2 {
// Process config if it was included in the request
// if not, do not assume that config has been removed!
if err = r.Config.Unmarshal(&mod.Config); err != nil {
return nil, err
}
}
if len(r.Blocks) > 2 {
// Process blocks if they were included in the request
// if not, do not assume that blocks were removed!
if err = r.Blocks.Unmarshal(&mod.Blocks); err != nil {
return nil, err
}
}
mod, err = ctrl.pageLayout.Update(ctx, mod)
return ctrl.makePayload(ctx, mod, err)
}
func (ctrl *PageLayout) Delete(ctx context.Context, r *request.PageLayoutDelete) (interface{}, error) {
return api.OK(), ctrl.pageLayout.DeleteByID(ctx, r.NamespaceID, r.PageID, r.PageLayoutID)
}
func (ctrl *PageLayout) Undelete(ctx context.Context, r *request.PageLayoutUndelete) (interface{}, error) {
return api.OK(), ctrl.pageLayout.UndeleteByID(ctx, r.NamespaceID, r.PageID, r.PageLayoutID)
}
func (ctrl PageLayout) makePayload(ctx context.Context, c *types.PageLayout, err error) (*pageLayoutPayload, error) {
if err != nil || c == nil {
return nil, err
}
return &pageLayoutPayload{
PageLayout: c,
}, nil
}
func (ctrl PageLayout) makeFilterPayload(ctx context.Context, nn types.PageLayoutSet, f types.PageLayoutFilter, err error) (*pageLayoutSetPayload, error) {
if err != nil {
return nil, err
}
modp := &pageLayoutSetPayload{Filter: f, Set: make([]*pageLayoutPayload, len(nn))}
for i := range nn {
modp.Set[i], _ = ctrl.makePayload(ctx, nn[i], nil)
}
return modp, nil
}

View File

@@ -0,0 +1,69 @@
package rest
import (
"context"
"github.com/cortezaproject/corteza/server/compose/rest/request"
"github.com/cortezaproject/corteza/server/compose/service"
"github.com/cortezaproject/corteza/server/compose/types"
"github.com/cortezaproject/corteza/server/pkg/api"
"github.com/cortezaproject/corteza/server/pkg/rbac"
)
type (
Permissions struct {
ac permissionsAccessController
}
permissionsAccessController interface {
Effective(context.Context, ...rbac.Resource) rbac.EffectiveSet
Trace(context.Context, uint64, []uint64, ...string) ([]*rbac.Trace, error)
List() []map[string]string
FindRulesByRoleID(context.Context, uint64) (rbac.RuleSet, error)
FindRules(ctx context.Context, roleID uint64, rr ...string) (rbac.RuleSet, error)
Grant(ctx context.Context, rr ...*rbac.Rule) error
}
)
func (Permissions) New() *Permissions {
return &Permissions{
ac: service.DefaultAccessControl,
}
}
func (ctrl Permissions) Effective(ctx context.Context, r *request.PermissionsEffective) (interface{}, error) {
return ctrl.ac.Effective(ctx, types.Component{}), nil
}
func (ctrl Permissions) Trace(ctx context.Context, r *request.PermissionsTrace) (interface{}, error) {
return ctrl.ac.Trace(ctx, r.UserID, r.RoleID, r.Resource...)
}
func (ctrl Permissions) List(ctx context.Context, r *request.PermissionsList) (interface{}, error) {
return ctrl.ac.List(), nil
}
func (ctrl Permissions) Read(ctx context.Context, r *request.PermissionsRead) (interface{}, error) {
return ctrl.ac.FindRules(ctx, r.RoleID, r.Resource...)
}
func (ctrl Permissions) Delete(ctx context.Context, r *request.PermissionsDelete) (interface{}, error) {
rr, err := ctrl.ac.FindRulesByRoleID(ctx, r.RoleID)
if err != nil {
return nil, err
}
for _, r := range rr {
r.Access = rbac.Inherit
}
return api.OK(), ctrl.ac.Grant(ctx, rr...)
}
func (ctrl Permissions) Update(ctx context.Context, r *request.PermissionsUpdate) (interface{}, error) {
for _, rule := range r.Rules {
// Make sure everything is properly set
rule.RoleID = r.RoleID
}
return api.OK(), ctrl.ac.Grant(ctx, r.Rules...)
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,751 @@
package request
// 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:
//
import (
"encoding/json"
"fmt"
"github.com/cortezaproject/corteza/server/pkg/payload"
"github.com/go-chi/chi/v5"
"io"
"mime/multipart"
"net/http"
"strings"
)
// dummy vars to prevent
// unused imports complain
var (
_ = chi.URLParam
_ = multipart.ErrMessageTooLarge
_ = payload.ParseUint64s
_ = strings.ToLower
_ = io.EOF
_ = fmt.Errorf
_ = json.NewEncoder
)
type (
// Internal API interface
AttachmentList struct {
// Kind PATH parameter
//
// Attachment kind
Kind string
// NamespaceID PATH parameter
//
// Namespace ID
NamespaceID uint64 `json:",string"`
// Sign GET parameter
//
// Signature
Sign string
// UserID GET parameter
//
// User ID
UserID uint64 `json:",string"`
// PageID GET parameter
//
// Filter attachments by page ID
PageID uint64 `json:",string"`
// ModuleID GET parameter
//
// Filter attachments by module ID
ModuleID uint64 `json:",string"`
// RecordID GET parameter
//
// Filter attachments by record ID
RecordID uint64 `json:",string"`
// FieldName GET parameter
//
// Filter attachments by field name
FieldName string
// Limit GET parameter
//
// Limit
Limit uint
// PageCursor GET parameter
//
// Page cursor
PageCursor string
}
AttachmentRead struct {
// Kind PATH parameter
//
// Attachment kind
Kind string
// NamespaceID PATH parameter
//
// Namespace ID
NamespaceID uint64 `json:",string"`
// AttachmentID PATH parameter
//
// Attachment ID
AttachmentID uint64 `json:",string"`
// Sign GET parameter
//
// Signature
Sign string
// UserID GET parameter
//
// User ID
UserID uint64 `json:",string"`
}
AttachmentDelete struct {
// Kind PATH parameter
//
// Attachment kind
Kind string
// NamespaceID PATH parameter
//
// Namespace ID
NamespaceID uint64 `json:",string"`
// AttachmentID PATH parameter
//
// Attachment ID
AttachmentID uint64 `json:",string"`
// Sign GET parameter
//
// Signature
Sign string
// UserID GET parameter
//
// User ID
UserID uint64 `json:",string"`
}
AttachmentOriginal struct {
// Kind PATH parameter
//
// Attachment kind
Kind string
// NamespaceID PATH parameter
//
// Namespace ID
NamespaceID uint64 `json:",string"`
// AttachmentID PATH parameter
//
// Attachment ID
AttachmentID uint64 `json:",string"`
// Name PATH parameter
//
// File name
Name string
// Sign GET parameter
//
// Signature
Sign string
// UserID GET parameter
//
// User ID
UserID uint64 `json:",string"`
// Download GET parameter
//
// Force file download
Download bool
}
AttachmentPreview struct {
// Kind PATH parameter
//
// Attachment kind
Kind string
// NamespaceID PATH parameter
//
// Namespace ID
NamespaceID uint64 `json:",string"`
// AttachmentID PATH parameter
//
// Attachment ID
AttachmentID uint64 `json:",string"`
// Ext PATH parameter
//
// Preview extension/format
Ext string
// Sign GET parameter
//
// Signature
Sign string
// UserID GET parameter
//
// User ID
UserID uint64 `json:",string"`
}
)
// NewAttachmentList request
func NewAttachmentList() *AttachmentList {
return &AttachmentList{}
}
// Auditable returns all auditable/loggable parameters
func (r AttachmentList) Auditable() map[string]interface{} {
return map[string]interface{}{
"kind": r.Kind,
"namespaceID": r.NamespaceID,
"sign": r.Sign,
"userID": r.UserID,
"pageID": r.PageID,
"moduleID": r.ModuleID,
"recordID": r.RecordID,
"fieldName": r.FieldName,
"limit": r.Limit,
"pageCursor": r.PageCursor,
}
}
// Auditable returns all auditable/loggable parameters
func (r AttachmentList) GetKind() string {
return r.Kind
}
// Auditable returns all auditable/loggable parameters
func (r AttachmentList) GetNamespaceID() uint64 {
return r.NamespaceID
}
// Auditable returns all auditable/loggable parameters
func (r AttachmentList) GetSign() string {
return r.Sign
}
// Auditable returns all auditable/loggable parameters
func (r AttachmentList) GetUserID() uint64 {
return r.UserID
}
// Auditable returns all auditable/loggable parameters
func (r AttachmentList) GetPageID() uint64 {
return r.PageID
}
// Auditable returns all auditable/loggable parameters
func (r AttachmentList) GetModuleID() uint64 {
return r.ModuleID
}
// Auditable returns all auditable/loggable parameters
func (r AttachmentList) GetRecordID() uint64 {
return r.RecordID
}
// Auditable returns all auditable/loggable parameters
func (r AttachmentList) GetFieldName() string {
return r.FieldName
}
// Auditable returns all auditable/loggable parameters
func (r AttachmentList) GetLimit() uint {
return r.Limit
}
// Auditable returns all auditable/loggable parameters
func (r AttachmentList) GetPageCursor() string {
return r.PageCursor
}
// Fill processes request and fills internal variables
func (r *AttachmentList) Fill(req *http.Request) (err error) {
{
// GET params
tmp := req.URL.Query()
if val, ok := tmp["sign"]; ok && len(val) > 0 {
r.Sign, err = val[0], nil
if err != nil {
return err
}
}
if val, ok := tmp["userID"]; ok && len(val) > 0 {
r.UserID, err = payload.ParseUint64(val[0]), nil
if err != nil {
return err
}
}
if val, ok := tmp["pageID"]; ok && len(val) > 0 {
r.PageID, err = payload.ParseUint64(val[0]), nil
if err != nil {
return err
}
}
if val, ok := tmp["moduleID"]; ok && len(val) > 0 {
r.ModuleID, err = payload.ParseUint64(val[0]), nil
if err != nil {
return err
}
}
if val, ok := tmp["recordID"]; ok && len(val) > 0 {
r.RecordID, err = payload.ParseUint64(val[0]), nil
if err != nil {
return err
}
}
if val, ok := tmp["fieldName"]; ok && len(val) > 0 {
r.FieldName, err = val[0], nil
if err != nil {
return err
}
}
if val, ok := tmp["limit"]; ok && len(val) > 0 {
r.Limit, err = payload.ParseUint(val[0]), nil
if err != nil {
return err
}
}
if val, ok := tmp["pageCursor"]; ok && len(val) > 0 {
r.PageCursor, err = val[0], nil
if err != nil {
return err
}
}
}
{
var val string
// path params
val = chi.URLParam(req, "kind")
r.Kind, err = val, nil
if err != nil {
return err
}
val = chi.URLParam(req, "namespaceID")
r.NamespaceID, err = payload.ParseUint64(val), nil
if err != nil {
return err
}
}
return err
}
// NewAttachmentRead request
func NewAttachmentRead() *AttachmentRead {
return &AttachmentRead{}
}
// Auditable returns all auditable/loggable parameters
func (r AttachmentRead) Auditable() map[string]interface{} {
return map[string]interface{}{
"kind": r.Kind,
"namespaceID": r.NamespaceID,
"attachmentID": r.AttachmentID,
"sign": r.Sign,
"userID": r.UserID,
}
}
// Auditable returns all auditable/loggable parameters
func (r AttachmentRead) GetKind() string {
return r.Kind
}
// Auditable returns all auditable/loggable parameters
func (r AttachmentRead) GetNamespaceID() uint64 {
return r.NamespaceID
}
// Auditable returns all auditable/loggable parameters
func (r AttachmentRead) GetAttachmentID() uint64 {
return r.AttachmentID
}
// Auditable returns all auditable/loggable parameters
func (r AttachmentRead) GetSign() string {
return r.Sign
}
// Auditable returns all auditable/loggable parameters
func (r AttachmentRead) GetUserID() uint64 {
return r.UserID
}
// Fill processes request and fills internal variables
func (r *AttachmentRead) Fill(req *http.Request) (err error) {
{
// GET params
tmp := req.URL.Query()
if val, ok := tmp["sign"]; ok && len(val) > 0 {
r.Sign, err = val[0], nil
if err != nil {
return err
}
}
if val, ok := tmp["userID"]; ok && len(val) > 0 {
r.UserID, err = payload.ParseUint64(val[0]), nil
if err != nil {
return err
}
}
}
{
var val string
// path params
val = chi.URLParam(req, "kind")
r.Kind, err = val, nil
if err != nil {
return err
}
val = chi.URLParam(req, "namespaceID")
r.NamespaceID, err = payload.ParseUint64(val), nil
if err != nil {
return err
}
val = chi.URLParam(req, "attachmentID")
r.AttachmentID, err = payload.ParseUint64(val), nil
if err != nil {
return err
}
}
return err
}
// NewAttachmentDelete request
func NewAttachmentDelete() *AttachmentDelete {
return &AttachmentDelete{}
}
// Auditable returns all auditable/loggable parameters
func (r AttachmentDelete) Auditable() map[string]interface{} {
return map[string]interface{}{
"kind": r.Kind,
"namespaceID": r.NamespaceID,
"attachmentID": r.AttachmentID,
"sign": r.Sign,
"userID": r.UserID,
}
}
// Auditable returns all auditable/loggable parameters
func (r AttachmentDelete) GetKind() string {
return r.Kind
}
// Auditable returns all auditable/loggable parameters
func (r AttachmentDelete) GetNamespaceID() uint64 {
return r.NamespaceID
}
// Auditable returns all auditable/loggable parameters
func (r AttachmentDelete) GetAttachmentID() uint64 {
return r.AttachmentID
}
// Auditable returns all auditable/loggable parameters
func (r AttachmentDelete) GetSign() string {
return r.Sign
}
// Auditable returns all auditable/loggable parameters
func (r AttachmentDelete) GetUserID() uint64 {
return r.UserID
}
// Fill processes request and fills internal variables
func (r *AttachmentDelete) Fill(req *http.Request) (err error) {
{
// GET params
tmp := req.URL.Query()
if val, ok := tmp["sign"]; ok && len(val) > 0 {
r.Sign, err = val[0], nil
if err != nil {
return err
}
}
if val, ok := tmp["userID"]; ok && len(val) > 0 {
r.UserID, err = payload.ParseUint64(val[0]), nil
if err != nil {
return err
}
}
}
{
var val string
// path params
val = chi.URLParam(req, "kind")
r.Kind, err = val, nil
if err != nil {
return err
}
val = chi.URLParam(req, "namespaceID")
r.NamespaceID, err = payload.ParseUint64(val), nil
if err != nil {
return err
}
val = chi.URLParam(req, "attachmentID")
r.AttachmentID, err = payload.ParseUint64(val), nil
if err != nil {
return err
}
}
return err
}
// NewAttachmentOriginal request
func NewAttachmentOriginal() *AttachmentOriginal {
return &AttachmentOriginal{}
}
// Auditable returns all auditable/loggable parameters
func (r AttachmentOriginal) Auditable() map[string]interface{} {
return map[string]interface{}{
"kind": r.Kind,
"namespaceID": r.NamespaceID,
"attachmentID": r.AttachmentID,
"name": r.Name,
"sign": r.Sign,
"userID": r.UserID,
"download": r.Download,
}
}
// Auditable returns all auditable/loggable parameters
func (r AttachmentOriginal) GetKind() string {
return r.Kind
}
// Auditable returns all auditable/loggable parameters
func (r AttachmentOriginal) GetNamespaceID() uint64 {
return r.NamespaceID
}
// Auditable returns all auditable/loggable parameters
func (r AttachmentOriginal) GetAttachmentID() uint64 {
return r.AttachmentID
}
// Auditable returns all auditable/loggable parameters
func (r AttachmentOriginal) GetName() string {
return r.Name
}
// Auditable returns all auditable/loggable parameters
func (r AttachmentOriginal) GetSign() string {
return r.Sign
}
// Auditable returns all auditable/loggable parameters
func (r AttachmentOriginal) GetUserID() uint64 {
return r.UserID
}
// Auditable returns all auditable/loggable parameters
func (r AttachmentOriginal) GetDownload() bool {
return r.Download
}
// Fill processes request and fills internal variables
func (r *AttachmentOriginal) Fill(req *http.Request) (err error) {
{
// GET params
tmp := req.URL.Query()
if val, ok := tmp["sign"]; ok && len(val) > 0 {
r.Sign, err = val[0], nil
if err != nil {
return err
}
}
if val, ok := tmp["userID"]; ok && len(val) > 0 {
r.UserID, err = payload.ParseUint64(val[0]), nil
if err != nil {
return err
}
}
if val, ok := tmp["download"]; ok && len(val) > 0 {
r.Download, err = payload.ParseBool(val[0]), nil
if err != nil {
return err
}
}
}
{
var val string
// path params
val = chi.URLParam(req, "kind")
r.Kind, err = val, nil
if err != nil {
return err
}
val = chi.URLParam(req, "namespaceID")
r.NamespaceID, err = payload.ParseUint64(val), nil
if err != nil {
return err
}
val = chi.URLParam(req, "attachmentID")
r.AttachmentID, err = payload.ParseUint64(val), nil
if err != nil {
return err
}
val = chi.URLParam(req, "name")
r.Name, err = val, nil
if err != nil {
return err
}
}
return err
}
// NewAttachmentPreview request
func NewAttachmentPreview() *AttachmentPreview {
return &AttachmentPreview{}
}
// Auditable returns all auditable/loggable parameters
func (r AttachmentPreview) Auditable() map[string]interface{} {
return map[string]interface{}{
"kind": r.Kind,
"namespaceID": r.NamespaceID,
"attachmentID": r.AttachmentID,
"ext": r.Ext,
"sign": r.Sign,
"userID": r.UserID,
}
}
// Auditable returns all auditable/loggable parameters
func (r AttachmentPreview) GetKind() string {
return r.Kind
}
// Auditable returns all auditable/loggable parameters
func (r AttachmentPreview) GetNamespaceID() uint64 {
return r.NamespaceID
}
// Auditable returns all auditable/loggable parameters
func (r AttachmentPreview) GetAttachmentID() uint64 {
return r.AttachmentID
}
// Auditable returns all auditable/loggable parameters
func (r AttachmentPreview) GetExt() string {
return r.Ext
}
// Auditable returns all auditable/loggable parameters
func (r AttachmentPreview) GetSign() string {
return r.Sign
}
// Auditable returns all auditable/loggable parameters
func (r AttachmentPreview) GetUserID() uint64 {
return r.UserID
}
// Fill processes request and fills internal variables
func (r *AttachmentPreview) Fill(req *http.Request) (err error) {
{
// GET params
tmp := req.URL.Query()
if val, ok := tmp["sign"]; ok && len(val) > 0 {
r.Sign, err = val[0], nil
if err != nil {
return err
}
}
if val, ok := tmp["userID"]; ok && len(val) > 0 {
r.UserID, err = payload.ParseUint64(val[0]), nil
if err != nil {
return err
}
}
}
{
var val string
// path params
val = chi.URLParam(req, "kind")
r.Kind, err = val, nil
if err != nil {
return err
}
val = chi.URLParam(req, "namespaceID")
r.NamespaceID, err = payload.ParseUint64(val), nil
if err != nil {
return err
}
val = chi.URLParam(req, "attachmentID")
r.AttachmentID, err = payload.ParseUint64(val), nil
if err != nil {
return err
}
val = chi.URLParam(req, "ext")
r.Ext, err = val, nil
if err != nil {
return err
}
}
return err
}

View File

@@ -0,0 +1,360 @@
package request
// 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:
//
import (
"encoding/json"
"fmt"
"github.com/cortezaproject/corteza/server/pkg/payload"
"github.com/go-chi/chi/v5"
"io"
"mime/multipart"
"net/http"
"strings"
)
// dummy vars to prevent
// unused imports complain
var (
_ = chi.URLParam
_ = multipart.ErrMessageTooLarge
_ = payload.ParseUint64s
_ = strings.ToLower
_ = io.EOF
_ = fmt.Errorf
_ = json.NewEncoder
)
type (
// Internal API interface
AutomationList struct {
// ResourceTypePrefixes GET parameter
//
// Filter by resource prefix
ResourceTypePrefixes []string
// ResourceTypes GET parameter
//
// Filter by resource type
ResourceTypes []string
// EventTypes GET parameter
//
// Filter by event type
EventTypes []string
// ExcludeInvalid GET parameter
//
// Exclude scripts that cannot be used (errors)
ExcludeInvalid bool
// ExcludeClientScripts GET parameter
//
// Do not include client scripts
ExcludeClientScripts bool
// ExcludeServerScripts GET parameter
//
// Do not include server scripts
ExcludeServerScripts bool
}
AutomationBundle struct {
// Bundle PATH parameter
//
// Name of the bundle
Bundle string
// Type PATH parameter
//
// Bundle type
Type string
// Ext PATH parameter
//
// Bundle extension
Ext string
}
AutomationTriggerScript struct {
// Script POST parameter
//
// Script to execute
Script string
// Args POST parameter
//
// Arguments to pass to the script
Args map[string]interface{}
}
)
// NewAutomationList request
func NewAutomationList() *AutomationList {
return &AutomationList{}
}
// Auditable returns all auditable/loggable parameters
func (r AutomationList) Auditable() map[string]interface{} {
return map[string]interface{}{
"resourceTypePrefixes": r.ResourceTypePrefixes,
"resourceTypes": r.ResourceTypes,
"eventTypes": r.EventTypes,
"excludeInvalid": r.ExcludeInvalid,
"excludeClientScripts": r.ExcludeClientScripts,
"excludeServerScripts": r.ExcludeServerScripts,
}
}
// Auditable returns all auditable/loggable parameters
func (r AutomationList) GetResourceTypePrefixes() []string {
return r.ResourceTypePrefixes
}
// Auditable returns all auditable/loggable parameters
func (r AutomationList) GetResourceTypes() []string {
return r.ResourceTypes
}
// Auditable returns all auditable/loggable parameters
func (r AutomationList) GetEventTypes() []string {
return r.EventTypes
}
// Auditable returns all auditable/loggable parameters
func (r AutomationList) GetExcludeInvalid() bool {
return r.ExcludeInvalid
}
// Auditable returns all auditable/loggable parameters
func (r AutomationList) GetExcludeClientScripts() bool {
return r.ExcludeClientScripts
}
// Auditable returns all auditable/loggable parameters
func (r AutomationList) GetExcludeServerScripts() bool {
return r.ExcludeServerScripts
}
// Fill processes request and fills internal variables
func (r *AutomationList) Fill(req *http.Request) (err error) {
{
// GET params
tmp := req.URL.Query()
if val, ok := tmp["resourceTypePrefixes[]"]; ok {
r.ResourceTypePrefixes, err = val, nil
if err != nil {
return err
}
} else if val, ok := tmp["resourceTypePrefixes"]; ok {
r.ResourceTypePrefixes, err = val, nil
if err != nil {
return err
}
}
if val, ok := tmp["resourceTypes[]"]; ok {
r.ResourceTypes, err = val, nil
if err != nil {
return err
}
} else if val, ok := tmp["resourceTypes"]; ok {
r.ResourceTypes, err = val, nil
if err != nil {
return err
}
}
if val, ok := tmp["eventTypes[]"]; ok {
r.EventTypes, err = val, nil
if err != nil {
return err
}
} else if val, ok := tmp["eventTypes"]; ok {
r.EventTypes, err = val, nil
if err != nil {
return err
}
}
if val, ok := tmp["excludeInvalid"]; ok && len(val) > 0 {
r.ExcludeInvalid, err = payload.ParseBool(val[0]), nil
if err != nil {
return err
}
}
if val, ok := tmp["excludeClientScripts"]; ok && len(val) > 0 {
r.ExcludeClientScripts, err = payload.ParseBool(val[0]), nil
if err != nil {
return err
}
}
if val, ok := tmp["excludeServerScripts"]; ok && len(val) > 0 {
r.ExcludeServerScripts, err = payload.ParseBool(val[0]), nil
if err != nil {
return err
}
}
}
return err
}
// NewAutomationBundle request
func NewAutomationBundle() *AutomationBundle {
return &AutomationBundle{}
}
// Auditable returns all auditable/loggable parameters
func (r AutomationBundle) Auditable() map[string]interface{} {
return map[string]interface{}{
"bundle": r.Bundle,
"type": r.Type,
"ext": r.Ext,
}
}
// Auditable returns all auditable/loggable parameters
func (r AutomationBundle) GetBundle() string {
return r.Bundle
}
// Auditable returns all auditable/loggable parameters
func (r AutomationBundle) GetType() string {
return r.Type
}
// Auditable returns all auditable/loggable parameters
func (r AutomationBundle) GetExt() string {
return r.Ext
}
// Fill processes request and fills internal variables
func (r *AutomationBundle) Fill(req *http.Request) (err error) {
{
var val string
// path params
val = chi.URLParam(req, "bundle")
r.Bundle, err = val, nil
if err != nil {
return err
}
val = chi.URLParam(req, "type")
r.Type, err = val, nil
if err != nil {
return err
}
val = chi.URLParam(req, "ext")
r.Ext, err = val, nil
if err != nil {
return err
}
}
return err
}
// NewAutomationTriggerScript request
func NewAutomationTriggerScript() *AutomationTriggerScript {
return &AutomationTriggerScript{}
}
// Auditable returns all auditable/loggable parameters
func (r AutomationTriggerScript) Auditable() map[string]interface{} {
return map[string]interface{}{
"script": r.Script,
"args": r.Args,
}
}
// Auditable returns all auditable/loggable parameters
func (r AutomationTriggerScript) GetScript() string {
return r.Script
}
// Auditable returns all auditable/loggable parameters
func (r AutomationTriggerScript) GetArgs() map[string]interface{} {
return r.Args
}
// Fill processes request and fills internal variables
func (r *AutomationTriggerScript) Fill(req *http.Request) (err error) {
if strings.HasPrefix(strings.ToLower(req.Header.Get("content-type")), "application/json") {
err = json.NewDecoder(req.Body).Decode(r)
switch {
case err == io.EOF:
err = nil
case err != nil:
return fmt.Errorf("error parsing http request body: %w", err)
}
}
{
// Caching 32MB to memory, the rest to disk
if err = req.ParseMultipartForm(32 << 20); err != nil && err != http.ErrNotMultipart {
return err
} else if err == nil {
// Multipart params
if val, ok := req.MultipartForm.Value["script"]; ok && len(val) > 0 {
r.Script, err = val[0], nil
if err != nil {
return err
}
}
if val, ok := req.MultipartForm.Value["args[]"]; ok {
r.Args, err = parseMapStringInterface(val)
if err != nil {
return err
}
} else if val, ok := req.MultipartForm.Value["args"]; ok {
r.Args, err = parseMapStringInterface(val)
if err != nil {
return err
}
}
}
}
{
if err = req.ParseForm(); err != nil {
return err
}
// POST params
if val, ok := req.Form["script"]; ok && len(val) > 0 {
r.Script, err = val[0], nil
if err != nil {
return err
}
}
if val, ok := req.Form["args[]"]; ok {
r.Args, err = parseMapStringInterface(val)
if err != nil {
return err
}
} else if val, ok := req.Form["args"]; ok {
r.Args, err = parseMapStringInterface(val)
if err != nil {
return err
}
}
}
return err
}

View File

@@ -0,0 +1,897 @@
package request
// 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:
//
import (
"encoding/json"
"fmt"
"github.com/cortezaproject/corteza/server/pkg/label"
labelTypes "github.com/cortezaproject/corteza/server/pkg/label/types"
"github.com/cortezaproject/corteza/server/pkg/locale"
"github.com/cortezaproject/corteza/server/pkg/payload"
"github.com/go-chi/chi/v5"
sqlxTypes "github.com/jmoiron/sqlx/types"
"io"
"mime/multipart"
"net/http"
"strings"
"time"
)
// dummy vars to prevent
// unused imports complain
var (
_ = chi.URLParam
_ = multipart.ErrMessageTooLarge
_ = payload.ParseUint64s
_ = strings.ToLower
_ = io.EOF
_ = fmt.Errorf
_ = json.NewEncoder
)
type (
// Internal API interface
ChartList struct {
// NamespaceID PATH parameter
//
// Namespace ID
NamespaceID uint64 `json:",string"`
// Query GET parameter
//
// Search query to match against charts
Query string
// Handle GET parameter
//
// Search charts by handle
Handle string
// Labels GET parameter
//
// Labels
Labels map[string]labelTypes.LabelValue
// Limit GET parameter
//
// Limit
Limit uint
// IncTotal GET parameter
//
// Include total counter
IncTotal bool
// PageCursor GET parameter
//
// Page cursor
PageCursor string
// Sort GET parameter
//
// Sort items
Sort string
}
ChartCreate struct {
// NamespaceID PATH parameter
//
// Namespace ID
NamespaceID uint64 `json:",string"`
// Config POST parameter
//
// Chart JSON
Config sqlxTypes.JSONText
// Name POST parameter
//
// Chart name
Name string
// Handle POST parameter
//
// Chart handle
Handle string
// Labels POST parameter
//
// Labels
Labels map[string]labelTypes.LabelValue
}
ChartRead struct {
// NamespaceID PATH parameter
//
// Namespace ID
NamespaceID uint64 `json:",string"`
// ChartID PATH parameter
//
// Chart ID
ChartID uint64 `json:",string"`
}
ChartUpdate struct {
// NamespaceID PATH parameter
//
// Namespace ID
NamespaceID uint64 `json:",string"`
// ChartID PATH parameter
//
// Chart ID
ChartID uint64 `json:",string"`
// Config POST parameter
//
// Chart JSON
Config sqlxTypes.JSONText
// Name POST parameter
//
// Chart name
Name string
// Handle POST parameter
//
// Chart handle
Handle string
// Labels POST parameter
//
// Labels
Labels map[string]labelTypes.LabelValue
// UpdatedAt POST parameter
//
// Last update (or creation) date
UpdatedAt *time.Time
}
ChartDelete struct {
// NamespaceID PATH parameter
//
// Namespace ID
NamespaceID uint64 `json:",string"`
// ChartID PATH parameter
//
// Chart ID
ChartID uint64 `json:",string"`
}
ChartListTranslations struct {
// NamespaceID PATH parameter
//
// Namespace ID
NamespaceID uint64 `json:",string"`
// ChartID PATH parameter
//
// ID
ChartID uint64 `json:",string"`
}
ChartUpdateTranslations struct {
// NamespaceID PATH parameter
//
// Namespace ID
NamespaceID uint64 `json:",string"`
// ChartID PATH parameter
//
// ID
ChartID uint64 `json:",string"`
// Translations POST parameter
//
// Resource translation to upsert
Translations locale.ResourceTranslationSet
}
)
// NewChartList request
func NewChartList() *ChartList {
return &ChartList{}
}
// Auditable returns all auditable/loggable parameters
func (r ChartList) Auditable() map[string]interface{} {
return map[string]interface{}{
"namespaceID": r.NamespaceID,
"query": r.Query,
"handle": r.Handle,
"labels": r.Labels,
"limit": r.Limit,
"incTotal": r.IncTotal,
"pageCursor": r.PageCursor,
"sort": r.Sort,
}
}
// Auditable returns all auditable/loggable parameters
func (r ChartList) GetNamespaceID() uint64 {
return r.NamespaceID
}
// Auditable returns all auditable/loggable parameters
func (r ChartList) GetQuery() string {
return r.Query
}
// Auditable returns all auditable/loggable parameters
func (r ChartList) GetHandle() string {
return r.Handle
}
// Auditable returns all auditable/loggable parameters
func (r ChartList) GetLabels() map[string]labelTypes.LabelValue {
return r.Labels
}
// Auditable returns all auditable/loggable parameters
func (r ChartList) GetLimit() uint {
return r.Limit
}
// Auditable returns all auditable/loggable parameters
func (r ChartList) GetIncTotal() bool {
return r.IncTotal
}
// Auditable returns all auditable/loggable parameters
func (r ChartList) GetPageCursor() string {
return r.PageCursor
}
// Auditable returns all auditable/loggable parameters
func (r ChartList) GetSort() string {
return r.Sort
}
// Fill processes request and fills internal variables
func (r *ChartList) Fill(req *http.Request) (err error) {
{
// GET params
tmp := req.URL.Query()
if val, ok := tmp["query"]; ok && len(val) > 0 {
r.Query, err = val[0], nil
if err != nil {
return err
}
}
if val, ok := tmp["handle"]; ok && len(val) > 0 {
r.Handle, err = val[0], nil
if err != nil {
return err
}
}
if val, ok := tmp["labels[]"]; ok {
r.Labels, err = label.ParseStrings(val)
if err != nil {
return err
}
} else if val, ok := tmp["labels"]; ok {
r.Labels, err = label.ParseStrings(val)
if err != nil {
return err
}
}
if val, ok := tmp["limit"]; ok && len(val) > 0 {
r.Limit, err = payload.ParseUint(val[0]), nil
if err != nil {
return err
}
}
if val, ok := tmp["incTotal"]; ok && len(val) > 0 {
r.IncTotal, err = payload.ParseBool(val[0]), nil
if err != nil {
return err
}
}
if val, ok := tmp["pageCursor"]; ok && len(val) > 0 {
r.PageCursor, err = val[0], nil
if err != nil {
return err
}
}
if val, ok := tmp["sort"]; ok && len(val) > 0 {
r.Sort, err = val[0], nil
if err != nil {
return err
}
}
}
{
var val string
// path params
val = chi.URLParam(req, "namespaceID")
r.NamespaceID, err = payload.ParseUint64(val), nil
if err != nil {
return err
}
}
return err
}
// NewChartCreate request
func NewChartCreate() *ChartCreate {
return &ChartCreate{}
}
// Auditable returns all auditable/loggable parameters
func (r ChartCreate) Auditable() map[string]interface{} {
return map[string]interface{}{
"namespaceID": r.NamespaceID,
"config": r.Config,
"name": r.Name,
"handle": r.Handle,
"labels": r.Labels,
}
}
// Auditable returns all auditable/loggable parameters
func (r ChartCreate) GetNamespaceID() uint64 {
return r.NamespaceID
}
// Auditable returns all auditable/loggable parameters
func (r ChartCreate) GetConfig() sqlxTypes.JSONText {
return r.Config
}
// Auditable returns all auditable/loggable parameters
func (r ChartCreate) GetName() string {
return r.Name
}
// Auditable returns all auditable/loggable parameters
func (r ChartCreate) GetHandle() string {
return r.Handle
}
// Auditable returns all auditable/loggable parameters
func (r ChartCreate) GetLabels() map[string]labelTypes.LabelValue {
return r.Labels
}
// Fill processes request and fills internal variables
func (r *ChartCreate) Fill(req *http.Request) (err error) {
if strings.HasPrefix(strings.ToLower(req.Header.Get("content-type")), "application/json") {
err = json.NewDecoder(req.Body).Decode(r)
switch {
case err == io.EOF:
err = nil
case err != nil:
return fmt.Errorf("error parsing http request body: %w", err)
}
}
{
// Caching 32MB to memory, the rest to disk
if err = req.ParseMultipartForm(32 << 20); err != nil && err != http.ErrNotMultipart {
return err
} else if err == nil {
// Multipart params
if val, ok := req.MultipartForm.Value["config"]; ok && len(val) > 0 {
r.Config, err = payload.ParseJSONTextWithErr(val[0])
if err != nil {
return err
}
}
if val, ok := req.MultipartForm.Value["name"]; ok && len(val) > 0 {
r.Name, err = val[0], nil
if err != nil {
return err
}
}
if val, ok := req.MultipartForm.Value["handle"]; ok && len(val) > 0 {
r.Handle, err = val[0], nil
if err != nil {
return err
}
}
if val, ok := req.MultipartForm.Value["labels[]"]; ok {
r.Labels, err = label.ParseStrings(val)
if err != nil {
return err
}
} else if val, ok := req.MultipartForm.Value["labels"]; ok {
r.Labels, err = label.ParseStrings(val)
if err != nil {
return err
}
}
}
}
{
if err = req.ParseForm(); err != nil {
return err
}
// POST params
if val, ok := req.Form["config"]; ok && len(val) > 0 {
r.Config, err = payload.ParseJSONTextWithErr(val[0])
if err != nil {
return err
}
}
if val, ok := req.Form["name"]; ok && len(val) > 0 {
r.Name, err = val[0], nil
if err != nil {
return err
}
}
if val, ok := req.Form["handle"]; ok && len(val) > 0 {
r.Handle, err = val[0], nil
if err != nil {
return err
}
}
if val, ok := req.Form["labels[]"]; ok {
r.Labels, err = label.ParseStrings(val)
if err != nil {
return err
}
} else if val, ok := req.Form["labels"]; ok {
r.Labels, err = label.ParseStrings(val)
if err != nil {
return err
}
}
}
{
var val string
// path params
val = chi.URLParam(req, "namespaceID")
r.NamespaceID, err = payload.ParseUint64(val), nil
if err != nil {
return err
}
}
return err
}
// NewChartRead request
func NewChartRead() *ChartRead {
return &ChartRead{}
}
// Auditable returns all auditable/loggable parameters
func (r ChartRead) Auditable() map[string]interface{} {
return map[string]interface{}{
"namespaceID": r.NamespaceID,
"chartID": r.ChartID,
}
}
// Auditable returns all auditable/loggable parameters
func (r ChartRead) GetNamespaceID() uint64 {
return r.NamespaceID
}
// Auditable returns all auditable/loggable parameters
func (r ChartRead) GetChartID() uint64 {
return r.ChartID
}
// Fill processes request and fills internal variables
func (r *ChartRead) Fill(req *http.Request) (err error) {
{
var val string
// path params
val = chi.URLParam(req, "namespaceID")
r.NamespaceID, err = payload.ParseUint64(val), nil
if err != nil {
return err
}
val = chi.URLParam(req, "chartID")
r.ChartID, err = payload.ParseUint64(val), nil
if err != nil {
return err
}
}
return err
}
// NewChartUpdate request
func NewChartUpdate() *ChartUpdate {
return &ChartUpdate{}
}
// Auditable returns all auditable/loggable parameters
func (r ChartUpdate) Auditable() map[string]interface{} {
return map[string]interface{}{
"namespaceID": r.NamespaceID,
"chartID": r.ChartID,
"config": r.Config,
"name": r.Name,
"handle": r.Handle,
"labels": r.Labels,
"updatedAt": r.UpdatedAt,
}
}
// Auditable returns all auditable/loggable parameters
func (r ChartUpdate) GetNamespaceID() uint64 {
return r.NamespaceID
}
// Auditable returns all auditable/loggable parameters
func (r ChartUpdate) GetChartID() uint64 {
return r.ChartID
}
// Auditable returns all auditable/loggable parameters
func (r ChartUpdate) GetConfig() sqlxTypes.JSONText {
return r.Config
}
// Auditable returns all auditable/loggable parameters
func (r ChartUpdate) GetName() string {
return r.Name
}
// Auditable returns all auditable/loggable parameters
func (r ChartUpdate) GetHandle() string {
return r.Handle
}
// Auditable returns all auditable/loggable parameters
func (r ChartUpdate) GetLabels() map[string]labelTypes.LabelValue {
return r.Labels
}
// Auditable returns all auditable/loggable parameters
func (r ChartUpdate) GetUpdatedAt() *time.Time {
return r.UpdatedAt
}
// Fill processes request and fills internal variables
func (r *ChartUpdate) Fill(req *http.Request) (err error) {
if strings.HasPrefix(strings.ToLower(req.Header.Get("content-type")), "application/json") {
err = json.NewDecoder(req.Body).Decode(r)
switch {
case err == io.EOF:
err = nil
case err != nil:
return fmt.Errorf("error parsing http request body: %w", err)
}
}
{
// Caching 32MB to memory, the rest to disk
if err = req.ParseMultipartForm(32 << 20); err != nil && err != http.ErrNotMultipart {
return err
} else if err == nil {
// Multipart params
if val, ok := req.MultipartForm.Value["config"]; ok && len(val) > 0 {
r.Config, err = payload.ParseJSONTextWithErr(val[0])
if err != nil {
return err
}
}
if val, ok := req.MultipartForm.Value["name"]; ok && len(val) > 0 {
r.Name, err = val[0], nil
if err != nil {
return err
}
}
if val, ok := req.MultipartForm.Value["handle"]; ok && len(val) > 0 {
r.Handle, err = val[0], nil
if err != nil {
return err
}
}
if val, ok := req.MultipartForm.Value["labels[]"]; ok {
r.Labels, err = label.ParseStrings(val)
if err != nil {
return err
}
} else if val, ok := req.MultipartForm.Value["labels"]; ok {
r.Labels, err = label.ParseStrings(val)
if err != nil {
return err
}
}
if val, ok := req.MultipartForm.Value["updatedAt"]; ok && len(val) > 0 {
r.UpdatedAt, err = payload.ParseISODatePtrWithErr(val[0])
if err != nil {
return err
}
}
}
}
{
if err = req.ParseForm(); err != nil {
return err
}
// POST params
if val, ok := req.Form["config"]; ok && len(val) > 0 {
r.Config, err = payload.ParseJSONTextWithErr(val[0])
if err != nil {
return err
}
}
if val, ok := req.Form["name"]; ok && len(val) > 0 {
r.Name, err = val[0], nil
if err != nil {
return err
}
}
if val, ok := req.Form["handle"]; ok && len(val) > 0 {
r.Handle, err = val[0], nil
if err != nil {
return err
}
}
if val, ok := req.Form["labels[]"]; ok {
r.Labels, err = label.ParseStrings(val)
if err != nil {
return err
}
} else if val, ok := req.Form["labels"]; ok {
r.Labels, err = label.ParseStrings(val)
if err != nil {
return err
}
}
if val, ok := req.Form["updatedAt"]; ok && len(val) > 0 {
r.UpdatedAt, err = payload.ParseISODatePtrWithErr(val[0])
if err != nil {
return err
}
}
}
{
var val string
// path params
val = chi.URLParam(req, "namespaceID")
r.NamespaceID, err = payload.ParseUint64(val), nil
if err != nil {
return err
}
val = chi.URLParam(req, "chartID")
r.ChartID, err = payload.ParseUint64(val), nil
if err != nil {
return err
}
}
return err
}
// NewChartDelete request
func NewChartDelete() *ChartDelete {
return &ChartDelete{}
}
// Auditable returns all auditable/loggable parameters
func (r ChartDelete) Auditable() map[string]interface{} {
return map[string]interface{}{
"namespaceID": r.NamespaceID,
"chartID": r.ChartID,
}
}
// Auditable returns all auditable/loggable parameters
func (r ChartDelete) GetNamespaceID() uint64 {
return r.NamespaceID
}
// Auditable returns all auditable/loggable parameters
func (r ChartDelete) GetChartID() uint64 {
return r.ChartID
}
// Fill processes request and fills internal variables
func (r *ChartDelete) Fill(req *http.Request) (err error) {
{
var val string
// path params
val = chi.URLParam(req, "namespaceID")
r.NamespaceID, err = payload.ParseUint64(val), nil
if err != nil {
return err
}
val = chi.URLParam(req, "chartID")
r.ChartID, err = payload.ParseUint64(val), nil
if err != nil {
return err
}
}
return err
}
// NewChartListTranslations request
func NewChartListTranslations() *ChartListTranslations {
return &ChartListTranslations{}
}
// Auditable returns all auditable/loggable parameters
func (r ChartListTranslations) Auditable() map[string]interface{} {
return map[string]interface{}{
"namespaceID": r.NamespaceID,
"chartID": r.ChartID,
}
}
// Auditable returns all auditable/loggable parameters
func (r ChartListTranslations) GetNamespaceID() uint64 {
return r.NamespaceID
}
// Auditable returns all auditable/loggable parameters
func (r ChartListTranslations) GetChartID() uint64 {
return r.ChartID
}
// Fill processes request and fills internal variables
func (r *ChartListTranslations) Fill(req *http.Request) (err error) {
{
var val string
// path params
val = chi.URLParam(req, "namespaceID")
r.NamespaceID, err = payload.ParseUint64(val), nil
if err != nil {
return err
}
val = chi.URLParam(req, "chartID")
r.ChartID, err = payload.ParseUint64(val), nil
if err != nil {
return err
}
}
return err
}
// NewChartUpdateTranslations request
func NewChartUpdateTranslations() *ChartUpdateTranslations {
return &ChartUpdateTranslations{}
}
// Auditable returns all auditable/loggable parameters
func (r ChartUpdateTranslations) Auditable() map[string]interface{} {
return map[string]interface{}{
"namespaceID": r.NamespaceID,
"chartID": r.ChartID,
"translations": r.Translations,
}
}
// Auditable returns all auditable/loggable parameters
func (r ChartUpdateTranslations) GetNamespaceID() uint64 {
return r.NamespaceID
}
// Auditable returns all auditable/loggable parameters
func (r ChartUpdateTranslations) GetChartID() uint64 {
return r.ChartID
}
// Auditable returns all auditable/loggable parameters
func (r ChartUpdateTranslations) GetTranslations() locale.ResourceTranslationSet {
return r.Translations
}
// Fill processes request and fills internal variables
func (r *ChartUpdateTranslations) Fill(req *http.Request) (err error) {
if strings.HasPrefix(strings.ToLower(req.Header.Get("content-type")), "application/json") {
err = json.NewDecoder(req.Body).Decode(r)
switch {
case err == io.EOF:
err = nil
case err != nil:
return fmt.Errorf("error parsing http request body: %w", err)
}
}
{
// Caching 32MB to memory, the rest to disk
if err = req.ParseMultipartForm(32 << 20); err != nil && err != http.ErrNotMultipart {
return err
} else if err == nil {
// Multipart params
}
}
{
if err = req.ParseForm(); err != nil {
return err
}
// POST params
//if val, ok := req.Form["translations[]"]; ok && len(val) > 0 {
// r.Translations, err = locale.ResourceTranslationSet(val), nil
// if err != nil {
// return err
// }
//}
}
{
var val string
// path params
val = chi.URLParam(req, "namespaceID")
r.NamespaceID, err = payload.ParseUint64(val), nil
if err != nil {
return err
}
val = chi.URLParam(req, "chartID")
r.ChartID, err = payload.ParseUint64(val), nil
if err != nil {
return err
}
}
return err
}

View File

@@ -0,0 +1,197 @@
package request
// 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:
//
import (
"encoding/json"
"fmt"
"github.com/cortezaproject/corteza/server/pkg/payload"
"github.com/go-chi/chi/v5"
"io"
"mime/multipart"
"net/http"
"strings"
)
// dummy vars to prevent
// unused imports complain
var (
_ = chi.URLParam
_ = multipart.ErrMessageTooLarge
_ = payload.ParseUint64s
_ = strings.ToLower
_ = io.EOF
_ = fmt.Errorf
_ = json.NewEncoder
)
type (
// Internal API interface
DataPrivacyRecordList struct {
// SensitivityLevelID GET parameter
//
// Sensitivity Level ID
SensitivityLevelID uint64 `json:",string"`
// ConnectionID GET parameter
//
// Filter by connection ID
ConnectionID []string
}
DataPrivacyModuleList struct {
// ConnectionID GET parameter
//
// Filter by connection ID
ConnectionID []string
// Limit GET parameter
//
// Limit
Limit uint
// PageCursor GET parameter
//
// Page cursor
PageCursor string
// Sort GET parameter
//
// Sort items
Sort string
}
)
// NewDataPrivacyRecordList request
func NewDataPrivacyRecordList() *DataPrivacyRecordList {
return &DataPrivacyRecordList{}
}
// Auditable returns all auditable/loggable parameters
func (r DataPrivacyRecordList) Auditable() map[string]interface{} {
return map[string]interface{}{
"sensitivityLevelID": r.SensitivityLevelID,
"connectionID": r.ConnectionID,
}
}
// Auditable returns all auditable/loggable parameters
func (r DataPrivacyRecordList) GetSensitivityLevelID() uint64 {
return r.SensitivityLevelID
}
// Auditable returns all auditable/loggable parameters
func (r DataPrivacyRecordList) GetConnectionID() []string {
return r.ConnectionID
}
// Fill processes request and fills internal variables
func (r *DataPrivacyRecordList) Fill(req *http.Request) (err error) {
{
// GET params
tmp := req.URL.Query()
if val, ok := tmp["sensitivityLevelID"]; ok && len(val) > 0 {
r.SensitivityLevelID, err = payload.ParseUint64(val[0]), nil
if err != nil {
return err
}
}
if val, ok := tmp["connectionID[]"]; ok {
r.ConnectionID, err = val, nil
if err != nil {
return err
}
} else if val, ok := tmp["connectionID"]; ok {
r.ConnectionID, err = val, nil
if err != nil {
return err
}
}
}
return err
}
// NewDataPrivacyModuleList request
func NewDataPrivacyModuleList() *DataPrivacyModuleList {
return &DataPrivacyModuleList{}
}
// Auditable returns all auditable/loggable parameters
func (r DataPrivacyModuleList) Auditable() map[string]interface{} {
return map[string]interface{}{
"connectionID": r.ConnectionID,
"limit": r.Limit,
"pageCursor": r.PageCursor,
"sort": r.Sort,
}
}
// Auditable returns all auditable/loggable parameters
func (r DataPrivacyModuleList) GetConnectionID() []string {
return r.ConnectionID
}
// Auditable returns all auditable/loggable parameters
func (r DataPrivacyModuleList) GetLimit() uint {
return r.Limit
}
// Auditable returns all auditable/loggable parameters
func (r DataPrivacyModuleList) GetPageCursor() string {
return r.PageCursor
}
// Auditable returns all auditable/loggable parameters
func (r DataPrivacyModuleList) GetSort() string {
return r.Sort
}
// Fill processes request and fills internal variables
func (r *DataPrivacyModuleList) Fill(req *http.Request) (err error) {
{
// GET params
tmp := req.URL.Query()
if val, ok := tmp["connectionID[]"]; ok {
r.ConnectionID, err = val, nil
if err != nil {
return err
}
} else if val, ok := tmp["connectionID"]; ok {
r.ConnectionID, err = val, nil
if err != nil {
return err
}
}
if val, ok := tmp["limit"]; ok && len(val) > 0 {
r.Limit, err = payload.ParseUint(val[0]), nil
if err != nil {
return err
}
}
if val, ok := tmp["pageCursor"]; ok && len(val) > 0 {
r.PageCursor, err = val[0], nil
if err != nil {
return err
}
}
if val, ok := tmp["sort"]; ok && len(val) > 0 {
r.Sort, err = val[0], nil
if err != nil {
return err
}
}
}
return err
}

View File

@@ -0,0 +1,235 @@
package request
// 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:
//
import (
"encoding/json"
"fmt"
"github.com/cortezaproject/corteza/server/pkg/payload"
"github.com/go-chi/chi/v5"
"io"
"mime/multipart"
"net/http"
"strings"
)
// dummy vars to prevent
// unused imports complain
var (
_ = chi.URLParam
_ = multipart.ErrMessageTooLarge
_ = payload.ParseUint64s
_ = strings.ToLower
_ = io.EOF
_ = fmt.Errorf
_ = json.NewEncoder
)
type (
// Internal API interface
IconList struct {
// Limit GET parameter
//
// Limit
Limit uint
// IncTotal GET parameter
//
// Include total counter
IncTotal bool
// PageCursor GET parameter
//
// Page cursor
PageCursor string
// Sort GET parameter
//
// Sort items
Sort string
}
IconUpload struct {
// Icon POST parameter
//
// Icon to upload
Icon *multipart.FileHeader
}
IconDelete struct {
// IconID PATH parameter
//
// Icon ID
IconID uint64 `json:",string"`
}
)
// NewIconList request
func NewIconList() *IconList {
return &IconList{}
}
// Auditable returns all auditable/loggable parameters
func (r IconList) Auditable() map[string]interface{} {
return map[string]interface{}{
"limit": r.Limit,
"incTotal": r.IncTotal,
"pageCursor": r.PageCursor,
"sort": r.Sort,
}
}
// Auditable returns all auditable/loggable parameters
func (r IconList) GetLimit() uint {
return r.Limit
}
// Auditable returns all auditable/loggable parameters
func (r IconList) GetIncTotal() bool {
return r.IncTotal
}
// Auditable returns all auditable/loggable parameters
func (r IconList) GetPageCursor() string {
return r.PageCursor
}
// Auditable returns all auditable/loggable parameters
func (r IconList) GetSort() string {
return r.Sort
}
// Fill processes request and fills internal variables
func (r *IconList) Fill(req *http.Request) (err error) {
{
// GET params
tmp := req.URL.Query()
if val, ok := tmp["limit"]; ok && len(val) > 0 {
r.Limit, err = payload.ParseUint(val[0]), nil
if err != nil {
return err
}
}
if val, ok := tmp["incTotal"]; ok && len(val) > 0 {
r.IncTotal, err = payload.ParseBool(val[0]), nil
if err != nil {
return err
}
}
if val, ok := tmp["pageCursor"]; ok && len(val) > 0 {
r.PageCursor, err = val[0], nil
if err != nil {
return err
}
}
if val, ok := tmp["sort"]; ok && len(val) > 0 {
r.Sort, err = val[0], nil
if err != nil {
return err
}
}
}
return err
}
// NewIconUpload request
func NewIconUpload() *IconUpload {
return &IconUpload{}
}
// Auditable returns all auditable/loggable parameters
func (r IconUpload) Auditable() map[string]interface{} {
return map[string]interface{}{
"icon": r.Icon,
}
}
// Auditable returns all auditable/loggable parameters
func (r IconUpload) GetIcon() *multipart.FileHeader {
return r.Icon
}
// Fill processes request and fills internal variables
func (r *IconUpload) Fill(req *http.Request) (err error) {
if strings.HasPrefix(strings.ToLower(req.Header.Get("content-type")), "application/json") {
err = json.NewDecoder(req.Body).Decode(r)
switch {
case err == io.EOF:
err = nil
case err != nil:
return fmt.Errorf("error parsing http request body: %w", err)
}
}
{
// Caching 32MB to memory, the rest to disk
if err = req.ParseMultipartForm(32 << 20); err != nil && err != http.ErrNotMultipart {
return err
} else if err == nil {
// Multipart params
// Ignoring icon as its handled in the POST params section
}
}
{
if err = req.ParseForm(); err != nil {
return err
}
// POST params
if _, r.Icon, err = req.FormFile("icon"); err != nil {
return fmt.Errorf("error processing uploaded file: %w", err)
}
}
return err
}
// NewIconDelete request
func NewIconDelete() *IconDelete {
return &IconDelete{}
}
// Auditable returns all auditable/loggable parameters
func (r IconDelete) Auditable() map[string]interface{} {
return map[string]interface{}{
"iconID": r.IconID,
}
}
// Auditable returns all auditable/loggable parameters
func (r IconDelete) GetIconID() uint64 {
return r.IconID
}
// Fill processes request and fills internal variables
func (r *IconDelete) Fill(req *http.Request) (err error) {
{
var val string
// path params
val = chi.URLParam(req, "iconID")
r.IconID, err = payload.ParseUint64(val), nil
if err != nil {
return err
}
}
return err
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,213 @@
package request
// 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:
//
import (
"encoding/json"
"fmt"
"github.com/cortezaproject/corteza/server/pkg/payload"
"github.com/go-chi/chi/v5"
sqlxTypes "github.com/jmoiron/sqlx/types"
"io"
"mime/multipart"
"net/http"
"strings"
)
// dummy vars to prevent
// unused imports complain
var (
_ = chi.URLParam
_ = multipart.ErrMessageTooLarge
_ = payload.ParseUint64s
_ = strings.ToLower
_ = io.EOF
_ = fmt.Errorf
_ = json.NewEncoder
)
type (
// Internal API interface
NotificationEmailSend struct {
// To POST parameter
//
// Email addresses
To []string
// Cc POST parameter
//
// Email addresses
Cc []string
// ReplyTo POST parameter
//
// Email address in reply-to field
ReplyTo string
// Subject POST parameter
//
// Email subject
Subject string
// Content POST parameter
//
// Message content
Content sqlxTypes.JSONText
// RemoteAttachments POST parameter
//
// Remote files to attach to the email
RemoteAttachments []string
}
)
// NewNotificationEmailSend request
func NewNotificationEmailSend() *NotificationEmailSend {
return &NotificationEmailSend{}
}
// Auditable returns all auditable/loggable parameters
func (r NotificationEmailSend) Auditable() map[string]interface{} {
return map[string]interface{}{
"to": r.To,
"cc": r.Cc,
"replyTo": r.ReplyTo,
"subject": r.Subject,
"content": r.Content,
"remoteAttachments": r.RemoteAttachments,
}
}
// Auditable returns all auditable/loggable parameters
func (r NotificationEmailSend) GetTo() []string {
return r.To
}
// Auditable returns all auditable/loggable parameters
func (r NotificationEmailSend) GetCc() []string {
return r.Cc
}
// Auditable returns all auditable/loggable parameters
func (r NotificationEmailSend) GetReplyTo() string {
return r.ReplyTo
}
// Auditable returns all auditable/loggable parameters
func (r NotificationEmailSend) GetSubject() string {
return r.Subject
}
// Auditable returns all auditable/loggable parameters
func (r NotificationEmailSend) GetContent() sqlxTypes.JSONText {
return r.Content
}
// Auditable returns all auditable/loggable parameters
func (r NotificationEmailSend) GetRemoteAttachments() []string {
return r.RemoteAttachments
}
// Fill processes request and fills internal variables
func (r *NotificationEmailSend) Fill(req *http.Request) (err error) {
if strings.HasPrefix(strings.ToLower(req.Header.Get("content-type")), "application/json") {
err = json.NewDecoder(req.Body).Decode(r)
switch {
case err == io.EOF:
err = nil
case err != nil:
return fmt.Errorf("error parsing http request body: %w", err)
}
}
{
// Caching 32MB to memory, the rest to disk
if err = req.ParseMultipartForm(32 << 20); err != nil && err != http.ErrNotMultipart {
return err
} else if err == nil {
// Multipart params
if val, ok := req.MultipartForm.Value["replyTo"]; ok && len(val) > 0 {
r.ReplyTo, err = val[0], nil
if err != nil {
return err
}
}
if val, ok := req.MultipartForm.Value["subject"]; ok && len(val) > 0 {
r.Subject, err = val[0], nil
if err != nil {
return err
}
}
if val, ok := req.MultipartForm.Value["content"]; ok && len(val) > 0 {
r.Content, err = payload.ParseJSONTextWithErr(val[0])
if err != nil {
return err
}
}
}
}
{
if err = req.ParseForm(); err != nil {
return err
}
// POST params
//if val, ok := req.Form["to[]"]; ok && len(val) > 0 {
// r.To, err = val, nil
// if err != nil {
// return err
// }
//}
//if val, ok := req.Form["cc[]"]; ok && len(val) > 0 {
// r.Cc, err = val, nil
// if err != nil {
// return err
// }
//}
if val, ok := req.Form["replyTo"]; ok && len(val) > 0 {
r.ReplyTo, err = val[0], nil
if err != nil {
return err
}
}
if val, ok := req.Form["subject"]; ok && len(val) > 0 {
r.Subject, err = val[0], nil
if err != nil {
return err
}
}
if val, ok := req.Form["content"]; ok && len(val) > 0 {
r.Content, err = payload.ParseJSONTextWithErr(val[0])
if err != nil {
return err
}
}
//if val, ok := req.Form["remoteAttachments[]"]; ok && len(val) > 0 {
// r.RemoteAttachments, err = val, nil
// if err != nil {
// return err
// }
//}
}
return err
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,384 @@
package request
// 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:
//
import (
"encoding/json"
"fmt"
"github.com/cortezaproject/corteza/server/pkg/payload"
"github.com/cortezaproject/corteza/server/pkg/rbac"
"github.com/go-chi/chi/v5"
"io"
"mime/multipart"
"net/http"
"strings"
)
// dummy vars to prevent
// unused imports complain
var (
_ = chi.URLParam
_ = multipart.ErrMessageTooLarge
_ = payload.ParseUint64s
_ = strings.ToLower
_ = io.EOF
_ = fmt.Errorf
_ = json.NewEncoder
)
type (
// Internal API interface
PermissionsList struct {
}
PermissionsEffective struct {
// Resource GET parameter
//
// Show only rules for a specific resource
Resource string
}
PermissionsTrace struct {
// Resource GET parameter
//
// Show only rules for a specific resource
Resource []string
// UserID GET parameter
//
//
UserID uint64 `json:",string"`
// RoleID GET parameter
//
//
RoleID []uint64
}
PermissionsRead struct {
// RoleID PATH parameter
//
// Role ID
RoleID uint64 `json:",string"`
// Resource GET parameter
//
// Show only rules for a specific resource
Resource []string
}
PermissionsDelete struct {
// RoleID PATH parameter
//
// Role ID
RoleID uint64 `json:",string"`
}
PermissionsUpdate struct {
// RoleID PATH parameter
//
// Role ID
RoleID uint64 `json:",string"`
// Rules POST parameter
//
// List of permission rules to set
Rules rbac.RuleSet
}
)
// NewPermissionsList request
func NewPermissionsList() *PermissionsList {
return &PermissionsList{}
}
// Auditable returns all auditable/loggable parameters
func (r PermissionsList) Auditable() map[string]interface{} {
return map[string]interface{}{}
}
// Fill processes request and fills internal variables
func (r *PermissionsList) Fill(req *http.Request) (err error) {
return err
}
// NewPermissionsEffective request
func NewPermissionsEffective() *PermissionsEffective {
return &PermissionsEffective{}
}
// Auditable returns all auditable/loggable parameters
func (r PermissionsEffective) Auditable() map[string]interface{} {
return map[string]interface{}{
"resource": r.Resource,
}
}
// Auditable returns all auditable/loggable parameters
func (r PermissionsEffective) GetResource() string {
return r.Resource
}
// Fill processes request and fills internal variables
func (r *PermissionsEffective) Fill(req *http.Request) (err error) {
{
// GET params
tmp := req.URL.Query()
if val, ok := tmp["resource"]; ok && len(val) > 0 {
r.Resource, err = val[0], nil
if err != nil {
return err
}
}
}
return err
}
// NewPermissionsTrace request
func NewPermissionsTrace() *PermissionsTrace {
return &PermissionsTrace{}
}
// Auditable returns all auditable/loggable parameters
func (r PermissionsTrace) Auditable() map[string]interface{} {
return map[string]interface{}{
"resource": r.Resource,
"userID": r.UserID,
"roleID": r.RoleID,
}
}
// Auditable returns all auditable/loggable parameters
func (r PermissionsTrace) GetResource() []string {
return r.Resource
}
// Auditable returns all auditable/loggable parameters
func (r PermissionsTrace) GetUserID() uint64 {
return r.UserID
}
// Auditable returns all auditable/loggable parameters
func (r PermissionsTrace) GetRoleID() []uint64 {
return r.RoleID
}
// Fill processes request and fills internal variables
func (r *PermissionsTrace) Fill(req *http.Request) (err error) {
{
// GET params
tmp := req.URL.Query()
if val, ok := tmp["resource[]"]; ok {
r.Resource, err = val, nil
if err != nil {
return err
}
} else if val, ok := tmp["resource"]; ok {
r.Resource, err = val, nil
if err != nil {
return err
}
}
if val, ok := tmp["userID"]; ok && len(val) > 0 {
r.UserID, err = payload.ParseUint64(val[0]), nil
if err != nil {
return err
}
}
if val, ok := tmp["roleID[]"]; ok {
r.RoleID, err = payload.ParseUint64s(val), nil
if err != nil {
return err
}
} else if val, ok := tmp["roleID"]; ok {
r.RoleID, err = payload.ParseUint64s(val), nil
if err != nil {
return err
}
}
}
return err
}
// NewPermissionsRead request
func NewPermissionsRead() *PermissionsRead {
return &PermissionsRead{}
}
// Auditable returns all auditable/loggable parameters
func (r PermissionsRead) Auditable() map[string]interface{} {
return map[string]interface{}{
"roleID": r.RoleID,
"resource": r.Resource,
}
}
// Auditable returns all auditable/loggable parameters
func (r PermissionsRead) GetRoleID() uint64 {
return r.RoleID
}
// Auditable returns all auditable/loggable parameters
func (r PermissionsRead) GetResource() []string {
return r.Resource
}
// Fill processes request and fills internal variables
func (r *PermissionsRead) Fill(req *http.Request) (err error) {
{
// GET params
tmp := req.URL.Query()
if val, ok := tmp["resource[]"]; ok {
r.Resource, err = val, nil
if err != nil {
return err
}
} else if val, ok := tmp["resource"]; ok {
r.Resource, err = val, nil
if err != nil {
return err
}
}
}
{
var val string
// path params
val = chi.URLParam(req, "roleID")
r.RoleID, err = payload.ParseUint64(val), nil
if err != nil {
return err
}
}
return err
}
// NewPermissionsDelete request
func NewPermissionsDelete() *PermissionsDelete {
return &PermissionsDelete{}
}
// Auditable returns all auditable/loggable parameters
func (r PermissionsDelete) Auditable() map[string]interface{} {
return map[string]interface{}{
"roleID": r.RoleID,
}
}
// Auditable returns all auditable/loggable parameters
func (r PermissionsDelete) GetRoleID() uint64 {
return r.RoleID
}
// Fill processes request and fills internal variables
func (r *PermissionsDelete) Fill(req *http.Request) (err error) {
{
var val string
// path params
val = chi.URLParam(req, "roleID")
r.RoleID, err = payload.ParseUint64(val), nil
if err != nil {
return err
}
}
return err
}
// NewPermissionsUpdate request
func NewPermissionsUpdate() *PermissionsUpdate {
return &PermissionsUpdate{}
}
// Auditable returns all auditable/loggable parameters
func (r PermissionsUpdate) Auditable() map[string]interface{} {
return map[string]interface{}{
"roleID": r.RoleID,
"rules": r.Rules,
}
}
// Auditable returns all auditable/loggable parameters
func (r PermissionsUpdate) GetRoleID() uint64 {
return r.RoleID
}
// Auditable returns all auditable/loggable parameters
func (r PermissionsUpdate) GetRules() rbac.RuleSet {
return r.Rules
}
// Fill processes request and fills internal variables
func (r *PermissionsUpdate) Fill(req *http.Request) (err error) {
if strings.HasPrefix(strings.ToLower(req.Header.Get("content-type")), "application/json") {
err = json.NewDecoder(req.Body).Decode(r)
switch {
case err == io.EOF:
err = nil
case err != nil:
return fmt.Errorf("error parsing http request body: %w", err)
}
}
{
// Caching 32MB to memory, the rest to disk
if err = req.ParseMultipartForm(32 << 20); err != nil && err != http.ErrNotMultipart {
return err
} else if err == nil {
// Multipart params
}
}
{
if err = req.ParseForm(); err != nil {
return err
}
// POST params
//if val, ok := req.Form["rules[]"]; ok && len(val) > 0 {
// r.Rules, err = rbac.RuleSet(val), nil
// if err != nil {
// return err
// }
//}
}
{
var val string
// path params
val = chi.URLParam(req, "roleID")
r.RoleID, err = payload.ParseUint64(val), nil
if err != nil {
return err
}
}
return err
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,44 @@
package request
//lint:file-ignore U1000 Ignore unused code, part of request pkg toolset
import (
"encoding/json"
"strconv"
"strings"
)
type (
ProcedureArgs []ProcedureArg
ProcedureArg struct {
Name string `json:"name"`
Value string `json:"value"`
}
)
func (args ProcedureArgs) GetUint64(name string) uint64 {
u, _ := strconv.ParseUint(args.Get(name), 10, 64)
return u
}
func (args ProcedureArgs) Get(name string) string {
name = strings.ToLower(name)
for _, arg := range args {
if strings.ToLower(arg.Name) == name {
return arg.Value
}
}
return ""
}
func parseMapStringInterface(ss []string) (map[string]interface{}, error) {
if len(ss) == 0 {
return nil, nil
}
out := make(map[string]interface{})
return out, json.Unmarshal([]byte(ss[0]), out)
}

View File

@@ -0,0 +1,49 @@
package rest
import (
"github.com/go-chi/chi/v5"
"github.com/cortezaproject/corteza/server/compose/rest/handlers"
"github.com/cortezaproject/corteza/server/pkg/auth"
)
func MountRoutes() func(r chi.Router) {
return func(r chi.Router) {
var (
namespace = Namespace{}.New()
module = Module{}.New()
record = Record{}.New()
page = Page{}.New()
pageIcon = Icon{}.New()
pageLayout = PageLayout{}.New()
chart = Chart{}.New()
notification = Notification{}.New()
attachment = Attachment{}.New()
automation = Automation{}.New()
dataPrivacy = DataPrivacy{}.New()
)
// Initialize handlers & controllers.
r.Group(func(r chi.Router) {
// Use alternative handlers that support file serving
handlers.NewAttachment(attachment).MountRoutes(r)
})
// Protect all _private_ routes
r.Group(func(r chi.Router) {
r.Use(auth.HttpTokenValidator("api"))
handlers.NewPermissions(Permissions{}.New()).MountRoutes(r)
handlers.NewNamespace(namespace).MountRoutes(r)
handlers.NewPage(page).MountRoutes(r)
handlers.NewIcon(pageIcon).MountRoutes(r)
handlers.NewPageLayout(pageLayout).MountRoutes(r)
handlers.NewAutomation(automation).MountRoutes(r)
handlers.NewModule(module).MountRoutes(r)
handlers.NewRecord(record).MountRoutes(r)
handlers.NewChart(chart).MountRoutes(r)
handlers.NewNotification(notification).MountRoutes(r)
handlers.NewDataPrivacy(dataPrivacy).MountRoutes(r)
})
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,107 @@
package service
import (
"context"
"fmt"
"testing"
"github.com/cortezaproject/corteza/server/compose/types"
"github.com/cortezaproject/corteza/server/pkg/rbac"
"github.com/stretchr/testify/require"
)
func TestAccessControl_ResourceLoader(t *testing.T) {
svc := accessControl{}
// Has wildcard resources
testCases := []struct {
resource string
expected rbac.Resource
err error
}{
{
resource: "corteza::compose:chart/1/*",
expected: rbac.NewResource(types.ChartRbacResource(1, 0)),
err: nil,
},
{
resource: "corteza::compose:chart/*/*",
expected: rbac.NewResource(types.ChartRbacResource(0, 0)),
err: nil,
},
{
resource: "corteza::compose:module/3/*",
expected: rbac.NewResource(types.ModuleRbacResource(3, 0)),
err: nil,
},
{
resource: "corteza::compose:module/*/*",
expected: rbac.NewResource(types.ModuleRbacResource(0, 0)),
err: nil,
},
{
resource: "corteza::compose:module-field/5/*/*",
expected: rbac.NewResource(types.ModuleFieldRbacResource(5, 0, 0)),
err: nil,
},
{
resource: "corteza::compose:module-field/*/*/*",
expected: rbac.NewResource(types.ModuleFieldRbacResource(0, 0, 0)),
err: nil,
},
{
resource: "corteza::compose:namespace/*",
expected: rbac.NewResource(types.NamespaceRbacResource(0)),
err: nil,
},
{
resource: "corteza::compose:page/9/*",
expected: rbac.NewResource(types.PageRbacResource(9, 0)),
err: nil,
},
{
resource: "corteza::compose:page/*/*",
expected: rbac.NewResource(types.PageRbacResource(0, 0)),
err: nil,
},
{
resource: "corteza::compose:page-layout/11/*/*",
expected: rbac.NewResource(types.PageLayoutRbacResource(11, 0, 0)),
err: nil,
},
{
resource: "corteza::compose:page-layout/*/*/*",
expected: rbac.NewResource(types.PageLayoutRbacResource(0, 0, 0)),
err: nil,
},
{
resource: "corteza::compose:record/14/*/*",
expected: rbac.NewResource(types.RecordRbacResource(14, 0, 0)),
err: nil,
},
{
resource: "corteza::compose:record/*/*/*",
expected: rbac.NewResource(types.RecordRbacResource(0, 0, 0)),
err: nil,
},
{
resource: "corteza::compose",
expected: &types.Component{},
err: nil,
},
{
resource: "unknown_resource_type:17",
expected: nil,
err: fmt.Errorf("unknown resource type %q", "unknown_resource_type:17"),
},
}
for _, tc := range testCases {
t.Run(tc.resource, func(t *testing.T) {
res, err := svc.resourceLoader(context.Background(), tc.resource)
require.Equal(t, tc.expected, res)
require.Equal(t, tc.err, err)
})
}
}

View File

@@ -0,0 +1,284 @@
package service
// 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/service/access_control_actions.yaml
import (
"context"
"fmt"
"github.com/cortezaproject/corteza/server/pkg/actionlog"
"github.com/cortezaproject/corteza/server/pkg/errors"
"github.com/cortezaproject/corteza/server/pkg/locale"
"github.com/cortezaproject/corteza/server/pkg/rbac"
"strings"
"time"
)
type (
accessControlActionProps struct {
rule *rbac.Rule
}
accessControlAction struct {
timestamp time.Time
resource string
action string
log string
severity actionlog.Severity
// prefix for error when action fails
errorMessage string
props *accessControlActionProps
}
accessControlLogMetaKey struct{}
accessControlPropsMetaKey struct{}
)
var (
// just a placeholder to cover template cases w/o fmt package use
_ = fmt.Println
)
// *********************************************************************************************************************
// *********************************************************************************************************************
// Props methods
// setRule updates accessControlActionProps's rule
//
// This function is auto-generated.
func (p *accessControlActionProps) setRule(rule *rbac.Rule) *accessControlActionProps {
p.rule = rule
return p
}
// Serialize converts accessControlActionProps to actionlog.Meta
//
// This function is auto-generated.
func (p accessControlActionProps) Serialize() actionlog.Meta {
var (
m = make(actionlog.Meta)
)
if p.rule != nil {
m.Set("rule.operation", p.rule.Operation, true)
m.Set("rule.roleID", p.rule.RoleID, true)
m.Set("rule.access", p.rule.Access, true)
m.Set("rule.resource", p.rule.Resource, true)
}
return m
}
// tr translates string and replaces meta value placeholder with values
//
// This function is auto-generated.
func (p accessControlActionProps) Format(in string, err error) string {
var (
pairs = []string{"{{err}}"}
// first non-empty string
fns = func(ii ...interface{}) string {
for _, i := range ii {
if s := fmt.Sprintf("%v", i); len(s) > 0 {
return s
}
}
return ""
}
)
if err != nil {
pairs = append(pairs, err.Error())
} else {
pairs = append(pairs, "nil")
}
if p.rule != nil {
// replacement for "{{rule}}" (in order how fields are defined)
pairs = append(
pairs,
"{{rule}}",
fns(
p.rule.Operation,
p.rule.RoleID,
p.rule.Access,
p.rule.Resource,
),
)
pairs = append(pairs, "{{rule.operation}}", fns(p.rule.Operation))
pairs = append(pairs, "{{rule.roleID}}", fns(p.rule.RoleID))
pairs = append(pairs, "{{rule.access}}", fns(p.rule.Access))
pairs = append(pairs, "{{rule.resource}}", fns(p.rule.Resource))
}
return strings.NewReplacer(pairs...).Replace(in)
}
// *********************************************************************************************************************
// *********************************************************************************************************************
// Action methods
// String returns loggable description as string
//
// This function is auto-generated.
func (a *accessControlAction) String() string {
var props = &accessControlActionProps{}
if a.props != nil {
props = a.props
}
return props.Format(a.log, nil)
}
func (e *accessControlAction) ToAction() *actionlog.Action {
return &actionlog.Action{
Resource: e.resource,
Action: e.action,
Severity: e.severity,
Description: e.String(),
Meta: e.props.Serialize(),
}
}
// *********************************************************************************************************************
// *********************************************************************************************************************
// Action constructors
// AccessControlActionGrant returns "compose:access_control.grant" action
//
// This function is auto-generated.
func AccessControlActionGrant(props ...*accessControlActionProps) *accessControlAction {
a := &accessControlAction{
timestamp: time.Now(),
resource: "compose:access_control",
action: "grant",
log: "grant",
severity: actionlog.Notice,
}
if len(props) > 0 {
a.props = props[0]
}
return a
}
// *********************************************************************************************************************
// *********************************************************************************************************************
// Error constructors
// AccessControlErrGeneric returns "compose:access_control.generic" as *errors.Error
//
// This function is auto-generated.
func AccessControlErrGeneric(mm ...*accessControlActionProps) *errors.Error {
var p = &accessControlActionProps{}
if len(mm) > 0 {
p = mm[0]
}
var e = errors.New(
errors.KindInternal,
p.Format("failed to complete request due to internal error", nil),
errors.Meta("type", "generic"),
errors.Meta("resource", "compose:access_control"),
// action log entry; no formatting, it will be applied inside recordAction fn.
errors.Meta(accessControlLogMetaKey{}, "{err}"),
errors.Meta(accessControlPropsMetaKey{}, p),
// translation namespace & key
errors.Meta(locale.ErrorMetaNamespace{}, "compose"),
errors.Meta(locale.ErrorMetaKey{}, "access-control.errors.generic"),
errors.StackSkip(1),
)
if len(mm) > 0 {
}
return e
}
// AccessControlErrNotAllowedToSetPermissions returns "compose:access_control.notAllowedToSetPermissions" as *errors.Error
//
// This function is auto-generated.
func AccessControlErrNotAllowedToSetPermissions(mm ...*accessControlActionProps) *errors.Error {
var p = &accessControlActionProps{}
if len(mm) > 0 {
p = mm[0]
}
var e = errors.New(
errors.KindInternal,
p.Format("not allowed to set permissions", nil),
errors.Meta("type", "notAllowedToSetPermissions"),
errors.Meta("resource", "compose:access_control"),
errors.Meta(accessControlPropsMetaKey{}, p),
// translation namespace & key
errors.Meta(locale.ErrorMetaNamespace{}, "compose"),
errors.Meta(locale.ErrorMetaKey{}, "access-control.errors.notAllowedToSetPermissions"),
errors.StackSkip(1),
)
if len(mm) > 0 {
}
return e
}
// *********************************************************************************************************************
// *********************************************************************************************************************
// recordAction is a service helper function wraps function that can return error
//
// It will wrap unrecognized/internal errors with generic errors.
//
// This function is auto-generated.
func (svc accessControl) recordAction(ctx context.Context, props *accessControlActionProps, actionFn func(...*accessControlActionProps) *accessControlAction, err error) error {
if svc.actionlog == nil || actionFn == nil {
// action log disabled or no action fn passed, return error as-is
return err
} else if err == nil {
// action completed w/o error, record it
svc.actionlog.Record(ctx, actionFn(props).ToAction())
return nil
}
a := actionFn(props).ToAction()
// Extracting error information and recording it as action
a.Error = err.Error()
switch c := err.(type) {
case *errors.Error:
m := c.Meta()
a.Error = err.Error()
a.Severity = actionlog.Severity(m.AsInt("severity"))
a.Description = props.Format(m.AsString(accessControlLogMetaKey{}), err)
if p, has := m[accessControlPropsMetaKey{}]; has {
a.Meta = p.(*accessControlActionProps).Serialize()
}
svc.actionlog.Record(ctx, a)
default:
svc.actionlog.Record(ctx, a)
}
// Original error is passed on
return err
}

View File

@@ -0,0 +1,25 @@
# List of security/audit events and errors that we need to log
resource: compose:access_control
service: accessControl
# Default sensitivity for actions
defaultActionSeverity: notice
# default severity for errors
defaultErrorSeverity: alert
import:
- github.com/cortezaproject/corteza/server/pkg/rbac
props:
- name: rule
type: "*rbac.Rule"
fields: [ operation, roleID, access, resource ]
actions:
- action: grant
errors:
- error: notAllowedToSetPermissions
message: "not allowed to set permissions"

View File

@@ -0,0 +1,717 @@
package service
import (
"bytes"
"context"
"image"
"image/gif"
"io"
"path"
"regexp"
"strings"
"github.com/cortezaproject/corteza/server/compose/dalutils"
"github.com/cortezaproject/corteza/server/compose/types"
"github.com/cortezaproject/corteza/server/pkg/actionlog"
"github.com/cortezaproject/corteza/server/pkg/auth"
"github.com/cortezaproject/corteza/server/pkg/errors"
"github.com/cortezaproject/corteza/server/pkg/objstore"
"github.com/cortezaproject/corteza/server/store"
systemService "github.com/cortezaproject/corteza/server/system/service"
"github.com/disintegration/imaging"
"github.com/edwvee/exiffix"
"github.com/gabriel-vasile/mimetype"
"github.com/mat/besticon/ico"
)
const (
attachmentPreviewMaxWidth = 320
attachmentPreviewMaxHeight = 180
// using base 10, it will be less confusing for the non-techie users
megabyte = 1_000_000
)
var (
reMimeType = regexp.MustCompile(`\w+/[-.\w]+(?:\+[-.\w]+)?`)
)
type (
attachment struct {
actionlog actionlog.Recorder
objects objstore.Store
ac attachmentAccessController
store store.Storer
dal dalDater
}
attachmentAccessController interface {
CanReadNamespace(context.Context, *types.Namespace) bool
CanCreateNamespace(context.Context) bool
CanReadModule(context.Context, *types.Module) bool
CanReadPage(context.Context, *types.Page) bool
CanUpdatePage(context.Context, *types.Page) bool
CanReadRecord(context.Context, *types.Record) bool
CanUpdateRecord(context.Context, *types.Record) bool
CanCreateRecordOnModule(context.Context, *types.Module) bool
}
AttachmentService interface {
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)
CreateIconAttachment(ctx context.Context, name string, size int64, fh io.ReadSeeker) (*types.Attachment, error)
CreateRecordAttachment(ctx context.Context, namespaceID uint64, name string, size int64, fh io.ReadSeeker, moduleID, recordID uint64, fieldName string) (*types.Attachment, error)
CreateNamespaceAttachment(ctx context.Context, name string, size int64, fh io.ReadSeeker) (*types.Attachment, error)
OpenOriginal(att *types.Attachment) (io.ReadSeekCloser, error)
OpenPreview(att *types.Attachment) (io.ReadSeekCloser, error)
DeleteByID(ctx context.Context, namespaceID, attachmentID uint64) error
}
)
func Attachment(store objstore.Store, dal dalDater) *attachment {
return &attachment{
objects: store,
ac: DefaultAccessControl,
store: DefaultStore,
dal: dal,
}
}
func (svc attachment) Find(ctx context.Context, filter types.AttachmentFilter) (set types.AttachmentSet, f types.AttachmentFilter, err error) {
var (
aProps = &attachmentActionProps{filter: &filter}
)
err = func() error {
if filter.NamespaceID == 0 && filter.Kind != types.IconAttachment {
return AttachmentErrInvalidNamespaceID()
}
if filter.PageID > 0 {
aProps.namespace, aProps.page, err = loadPageCombo(ctx, svc.store, filter.NamespaceID, filter.PageID)
if err != nil {
return err
} else if svc.ac.CanReadPage(ctx, aProps.page) {
return AttachmentErrNotAllowedToReadPage()
}
}
if filter.RecordID > 0 {
aProps.namespace, aProps.module, aProps.record, err = loadRecordCombo(ctx, svc.store, svc.dal, filter.NamespaceID, filter.ModuleID, filter.RecordID)
if err != nil {
return err
} else if svc.ac.CanReadRecord(ctx, aProps.record) {
return AttachmentErrNotAllowedToReadRecord()
}
} else if filter.ModuleID > 0 {
aProps.namespace, aProps.module, err = loadModuleCombo(ctx, svc.store, filter.NamespaceID, filter.ModuleID)
if err != nil {
return err
} else if svc.ac.CanReadRecord(ctx, aProps.record) {
return AttachmentErrNotAllowedToReadRecord()
}
}
set, f, err = store.SearchComposeAttachments(ctx, svc.store, filter)
return err
}()
return set, f, svc.recordAction(ctx, aProps, AttachmentActionSearch, err)
}
func (svc attachment) FindByID(ctx context.Context, namespaceID, attachmentID uint64) (att *types.Attachment, err error) {
var (
aProps = &attachmentActionProps{}
)
err = func() error {
if attachmentID == 0 {
return AttachmentErrInvalidID()
}
if att, err = store.LookupComposeAttachmentByID(ctx, svc.store, attachmentID); err != nil {
return err
}
aProps.setAttachment(att)
return nil
}()
return att, svc.recordAction(ctx, aProps, AttachmentActionLookup, err)
}
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(ctx, svc.store, func(ctx context.Context, s store.Storer) (err error) {
if attachmentID == 0 {
return AttachmentErrInvalidID()
}
if att, err = store.LookupComposeAttachmentByID(ctx, s, attachmentID); err != nil {
return err
}
aProps.setAttachment(att)
att.DeletedAt = now()
return store.UpdateComposeAttachment(ctx, s, att)
})
return svc.recordAction(ctx, aProps, AttachmentActionDelete, err)
}
// func (svc attachment) findNamespaceByID(namespaceID uint64) (ns *types.Namespace, err error) {
// if namespaceID == 0 {
// return nil, AttachmentErrInvalidNamespaceID()
// }
//
// ns, err = svc.namespaceRepo.FindByID(namespaceID)
// if repository.ErrNamespaceNotFound.Eq(err) {
// return nil, AttachmentErrNamespaceNotFound()
// }
//
// if !svc.ac.CanReadNamespace(ctx, ns) {
// return nil, AttachmentErrNotAllowedToReadNamespace()
// }
//
// return ns, nil
// }
//
// func (svc attachment) findPageByID(namespaceID, pageID uint64) (p *types.Page, err error) {
// if pageID == 0 {
// return nil, AttachmentErrInvalidPageID()
// }
//
// p, err = svc.pageRepo.FindByID(namespaceID, pageID)
// if repository.ErrPageNotFound.Eq(err) {
// return nil, AttachmentErrPageNotFound()
// }
//
// if !svc.ac.CanReadPage(ctx, p) {
// return nil, AttachmentErrNotAllowedToReadPage()
// }
//
// return p, nil
// }
//
// func (svc attachment) findModuleByID(namespaceID, moduleID uint64) (m *types.Module, err error) {
// if moduleID == 0 {
// return nil, AttachmentErrInvalidModuleID()
// }
//
// m, err = svc.moduleRepo.FindByID(namespaceID, moduleID)
// if repository.ErrModuleNotFound.Eq(err) {
// return nil, AttachmentErrModuleNotFound()
// }
//
// if !svc.ac.CanReadModule(ctx, m) {
// return nil, AttachmentErrNotAllowedToReadModule()
// }
//
// return m, nil
// }
//
// func (svc attachment) findRecordByID(m *types.Module, recordID uint64) (r *types.Record, err error) {
// if recordID == 0 {
// return nil, AttachmentErrInvalidRecordID()
// }
//
// r, err = svc.recordRepo.FindByID(m.NamespaceID, recordID)
// if repository.ErrRecordNotFound.Eq(err) {
// return nil, AttachmentErrRecordNotFound()
// }
//
// if !svc.ac.CanReadRecord(ctx, m) {
// return nil, AttachmentErrNotAllowedToReadRecord()
// }
//
// return r, nil
// }
func (svc attachment) OpenOriginal(att *types.Attachment) (io.ReadSeekCloser, error) {
if len(att.Url) == 0 {
return nil, nil
}
return svc.objects.Open(att.Url)
}
func (svc attachment) OpenPreview(att *types.Attachment) (io.ReadSeekCloser, error) {
if len(att.PreviewUrl) == 0 {
return nil, nil
}
return svc.objects.Open(att.PreviewUrl)
}
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
aProps = &attachmentActionProps{
namespace: &types.Namespace{ID: namespaceID},
page: &types.Page{ID: pageID},
}
)
err = store.Tx(ctx, svc.store, func(ctx context.Context, s store.Storer) (err error) {
if size == 0 {
return AttachmentErrNotAllowedToCreateEmptyAttachment()
}
ns, p, err = loadPageCombo(ctx, s, namespaceID, pageID)
if err != nil {
return err
}
aProps.setNamespace(ns)
aProps.setPage(p)
if !svc.ac.CanUpdatePage(ctx, p) {
return AttachmentErrNotAllowedToUpdatePage()
}
{
// Verify size and type of the uploaded page attachment
// Max size & allowed mime-types are pulled from the current settings
var (
maxSize = int64(systemService.CurrentSettings.Compose.Page.Attachments.MaxSize) * megabyte
allowedTypes = systemService.CurrentSettings.Compose.Page.Attachments.Mimetypes
)
if err = svc.verifySizeAndMimetype(fh, size, maxSize, allowedTypes); err != nil {
return err
}
}
att = &types.Attachment{
NamespaceID: namespaceID,
Name: strings.TrimSpace(name),
Kind: types.PageAttachment,
}
return svc.create(ctx, s, name, size, fh, att)
})
return att, svc.recordAction(ctx, aProps, AttachmentActionCreate, err)
}
func (svc attachment) CreateIconAttachment(ctx context.Context, name string, size int64, fh io.ReadSeeker) (att *types.Attachment, err error) {
var (
aProps = &attachmentActionProps{}
)
err = store.Tx(ctx, svc.store, func(ctx context.Context, s store.Storer) (err error) {
if size == 0 {
return AttachmentErrNotAllowedToCreateEmptyAttachment()
}
{
// Verify size and type of the uploaded page attachment
// Max size & allowed mime-types are pulled from the current settings
var (
maxSize = int64(systemService.CurrentSettings.Compose.Icon.Attachments.MaxSize) * megabyte
allowedTypes = systemService.CurrentSettings.Compose.Icon.Attachments.Mimetypes
)
if err = svc.verifySizeAndMimetype(fh, size, maxSize, allowedTypes); err != nil {
return err
}
}
att = &types.Attachment{
Name: strings.TrimSpace(name),
Kind: types.IconAttachment,
}
return svc.create(ctx, s, name, size, fh, att)
})
return att, svc.recordAction(ctx, aProps, AttachmentActionCreate, err)
}
func (svc attachment) verifySizeAndMimetype(fh io.ReadSeeker, size, maxSize int64, allowedTypes []string) (err error) {
// Verify size and type of the uploaded page attachment
// Max size & allowed mime-types are pulled from the current settings
var (
mimeType *mimetype.MIME
)
if maxSize > 0 && maxSize < size {
return AttachmentErrTooLarge().Apply(
errors.Meta("size", size),
errors.Meta("maxSize", maxSize),
)
}
if mimeType, err = svc.extractMimetype(fh); err != nil {
return err
} else if !svc.checkMimeType(mimeType, allowedTypes...) {
return AttachmentErrNotAllowedToUploadThisType()
}
return
}
func (svc attachment) CreateRecordAttachment(ctx context.Context, namespaceID uint64, name string, size int64, fh io.ReadSeeker, moduleID, recordID uint64, fieldName string) (att *types.Attachment, err error) {
var (
ns *types.Namespace
m *types.Module
r *types.Record
aProps = &attachmentActionProps{
namespace: &types.Namespace{ID: namespaceID},
module: &types.Module{ID: moduleID},
record: &types.Record{ID: recordID},
}
)
err = store.Tx(ctx, svc.store, func(ctx context.Context, s store.Storer) (err error) {
if size == 0 {
return AttachmentErrNotAllowedToCreateEmptyAttachment()
}
ns, m, err = loadModuleCombo(ctx, s, namespaceID, moduleID)
if err != nil {
return err
}
aProps.setNamespace(ns)
aProps.setModule(m)
if recordID > 0 {
// Uploading to existing record
//
// To allow upload (attachment creation) user must have permissions to
// alter that record
r, err = dalutils.ComposeRecordsFind(ctx, svc.dal, m, recordID)
if err != nil {
return err
}
aProps.setRecord(r)
if !svc.ac.CanUpdateRecord(ctx, r) {
return AttachmentErrNotAllowedToUpdateRecord()
}
} else {
// Uploading to non-existing record
//
// To allow upload (attachment creation) user must have permissions to
// create records
if !svc.ac.CanCreateRecordOnModule(ctx, m) {
return AttachmentErrNotAllowedToCreateRecords()
}
}
{
// Verify size and type of the uploaded record attachment
// Max size & allowed mime-types are pulled from the current settings
var (
maxSize = int64(systemService.CurrentSettings.Compose.Record.Attachments.MaxSize) * megabyte
allowedTypes = systemService.CurrentSettings.Compose.Record.Attachments.Mimetypes
mimeType *mimetype.MIME
)
f := m.Fields.FindByName(fieldName)
if f == nil || f.Kind != "File" {
return AttachmentErrInvalidModuleField().Apply(
errors.Meta("fieldName", fieldName),
)
}
if aux := f.Options.Int64("maxSize"); aux > 0 {
maxSize = aux * megabyte
}
if aux := f.Options.String("mimetypes"); len(aux) > 0 {
allowedTypes = strings.Split(aux, ",")
}
if maxSize > 0 && maxSize < size {
return AttachmentErrTooLarge().Apply(
errors.Meta("size", size),
errors.Meta("maxSize", maxSize),
)
}
if mimeType, err = svc.extractMimetype(fh); err != nil {
return err
} else if !svc.checkMimeType(mimeType, allowedTypes...) {
return AttachmentErrNotAllowedToUploadThisType().Apply(errors.Meta("mimetype", mimeType))
}
}
att = &types.Attachment{
NamespaceID: namespaceID,
Name: strings.TrimSpace(name),
Kind: types.RecordAttachment,
}
return svc.create(ctx, s, name, size, fh, att)
})
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) {
var (
aProps = &attachmentActionProps{}
)
err = store.Tx(ctx, svc.store, func(ctx context.Context, s store.Storer) (err error) {
if size == 0 {
return AttachmentErrNotAllowedToCreateEmptyAttachment()
}
if !svc.ac.CanCreateNamespace(ctx) {
return AttachmentErrNotAllowedToUpdateNamespace()
}
{
// Verify file size and mimetype
var (
maxSize = int64(systemService.CurrentSettings.Compose.Namespace.Attachments.MaxSize) * megabyte
allowedTypes = systemService.CurrentSettings.Compose.Namespace.Attachments.Mimetypes
)
if err = svc.verifySizeAndMimetype(fh, size, maxSize, allowedTypes); err != nil {
return err
}
}
att = &types.Attachment{
Name: strings.TrimSpace(name),
Kind: types.NamespaceAttachment,
}
// @todo limit upload on image/* only!
return svc.create(ctx, s, name, size, fh, att)
})
return att, svc.recordAction(ctx, aProps, AttachmentActionCreate, err)
}
func (svc attachment) create(ctx context.Context, s store.ComposeAttachments, name string, size int64, fh io.ReadSeeker, att *types.Attachment) (err error) {
var (
aProps = &attachmentActionProps{}
)
// preset attachment ID because we need ref for storage
att.ID = nextID()
att.CreatedAt = *now()
if att.OwnerID == 0 {
att.OwnerID = auth.GetIdentityFromContext(ctx).Identity()
}
if svc.objects == nil {
return errors.Internal("cannot create attachment: store handler not set")
}
if size == 0 {
return AttachmentErrNotAllowedToCreateEmptyAttachment()
}
aProps.setName(name)
aProps.setSize(size)
// Extract extension but make sure path.Ext is not confused by any leading/trailing dots
att.Meta.Original.Extension = strings.Trim(path.Ext(strings.Trim(name, ".")), ".")
att.Meta.Original.Size = size
if att.Meta.Original.Mimetype, err = svc.extractMimetypeS(fh); err != nil {
return AttachmentErrFailedToExtractMimeType(aProps).Wrap(err)
}
att.Url = svc.objects.Original(att.ID, att.Meta.Original.Extension)
aProps.setUrl(att.Url)
if err = svc.objects.Save(att.Url, fh); err != nil {
return AttachmentErrFailedToStoreFile(aProps).Wrap(err)
}
// Process image: extract width, height, make preview
err = svc.processImage(fh, att)
if err != nil {
return AttachmentErrFailedToProcessImage(aProps).Wrap(err)
}
if err = store.CreateComposeAttachment(ctx, s, att); err != nil {
return
}
return nil
}
func (svc attachment) extractMimetype(file io.ReadSeeker) (mType *mimetype.MIME, err error) {
if _, err = file.Seek(0, 0); err != nil {
return
}
// Make sure we rewind when we're done
defer func(file io.ReadSeeker, offset int64, whence int) {
_, _ = file.Seek(offset, whence)
}(file, 0, 0)
var mime *mimetype.MIME
if mime, err = mimetype.DetectReader(file); err != nil {
return
}
return mime, nil
}
func (svc attachment) extractMimetypeS(file io.ReadSeeker) (mType string, err error) {
aux, err := svc.extractMimetype(file)
if err != nil {
return
}
return aux.String(), nil
}
func (svc attachment) processImage(original io.ReadSeeker, att *types.Attachment) (err error) {
if !strings.HasPrefix(att.Meta.Original.Mimetype, "image/") {
// Only supporting previews from images (for now)
return
}
if att.Meta.Original.Mimetype == "image/svg+xml" {
// SVG is a vector format, skip raster image processing/preview generation
return
}
const (
iconMimetype = "image/x-icon"
iconExtension = "ico"
)
var (
preview image.Image
opts []imaging.EncodeOption
format imaging.Format
previewFormat imaging.Format
animated bool
f2m = map[imaging.Format]string{
imaging.JPEG: "image/jpeg",
imaging.GIF: "image/gif",
}
f2e = map[imaging.Format]string{
imaging.JPEG: "jpg",
imaging.GIF: "gif",
}
isIcon = att.Meta.Original.Mimetype == iconMimetype
)
if _, err = original.Seek(0, 0); err != nil {
return
}
if isIcon {
preview, err = ico.Decode(original)
if err != nil {
return errors.Internal("Could not decode ico config").Wrap(err)
}
} else {
if format, err = imaging.FormatFromExtension(att.Meta.Original.Extension); err != nil {
return errors.Internal("could not get format from extension '%s'", att.Meta.Original.Extension).Wrap(err)
}
previewFormat = format
if imaging.JPEG == format {
// Rotate image if needed
// if preview, _, err = exiffix.Decode(original); err != nil {
// return fmt.Errorf("Could not decode EXIF from JPEG", err)
// }
preview, _, _ = exiffix.Decode(original)
}
if imaging.GIF == format {
// Decode all and check loops & delay to determine if GIF is animated or not
if cfg, err := gif.DecodeAll(original); err == nil {
animated = cfg.LoopCount > 0 || len(cfg.Delay) > 1
// Use first image for the preview
preview = cfg.Image[0]
} else {
return errors.Internal("Could not decode gif config").Wrap(err)
}
} else {
// Use GIF preview for GIFs and JPEG for everything else!
previewFormat = imaging.JPEG
// Store with a bit lower quality
opts = append(opts, imaging.JPEGQuality(85))
}
}
// In case of JPEG we decode the image and rotate it beforehand
// other cases are handled here
if preview == nil {
if preview, err = imaging.Decode(original); err != nil {
return errors.Internal("Could not decode original image").Wrap(err)
}
}
var width, height = preview.Bounds().Max.X, preview.Bounds().Max.Y
att.SetOriginalImageMeta(width, height, animated)
if width > attachmentPreviewMaxWidth && width > height {
// Landscape does not fit
preview = imaging.Resize(preview, attachmentPreviewMaxWidth, 0, imaging.Lanczos)
} else if height > attachmentPreviewMaxHeight {
// Height does not fit
preview = imaging.Resize(preview, 0, attachmentPreviewMaxHeight, imaging.Lanczos)
}
// Get dimensions from the preview
width, height = preview.Bounds().Max.X, preview.Bounds().Max.Y
var buf = &bytes.Buffer{}
if err = imaging.Encode(buf, preview, previewFormat, opts...); err != nil {
return
}
meta := att.SetPreviewImageMeta(width, height, false)
meta.Size = int64(buf.Len())
if isIcon {
meta.Mimetype = iconMimetype
meta.Extension = iconExtension
} else {
meta.Mimetype = f2m[previewFormat]
meta.Extension = f2e[previewFormat]
}
// Can and how we make a preview of this attachment?
att.PreviewUrl = svc.objects.Preview(att.ID, meta.Extension)
return svc.objects.Save(att.PreviewUrl, buf)
}
func (attachment) checkMimeType(test *mimetype.MIME, vv ...string) bool {
if len(vv) == 0 {
// return true if there are no type constraints to check against
return true
}
for _, v := range vv {
if test.Is(v) {
return true
}
}
return false
}
var _ AttachmentService = &attachment{}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,163 @@
# List of loggable service actions
resource: compose:attachment
service: attachment
# Default sensitivity for actions
defaultActionSeverity: notice
# default severity for errors
defaultErrorSeverity: alert
import:
- github.com/cortezaproject/corteza/server/compose/types
props:
- name: size
type: int64
- name: name
- name: mimetype
- name: url
- name: attachment
type: "*types.Attachment"
fields: [ name, kind, url, previewUrl, meta, ownerID, ID, namespaceID ]
- name: filter
type: "*types.AttachmentFilter"
fields: [ filter, kind, sort ]
- name: namespace
type: "*types.Namespace"
fields: [ name, slug, ID ]
- name: record
type: "*types.Record"
fields: [ ID, moduleID, namespaceID ]
- name: page
type: "*types.Page"
fields: [ handle, title, ID ]
- name: module
type: "*types.Module"
fields: [ handle, name, ID ]
actions:
- action: search
log: "searched for attachments"
severity: info
- action: lookup
log: "looked-up for a {{attachment}}"
severity: info
- action: create
log: "created {{attachment}}"
- action: delete
log: "deleted {{attachment}}"
errors:
- error: notFound
message: "attachment not found"
severity: warning
- error: namespaceNotFound
message: "namespace not found"
severity: warning
- error: moduleNotFound
message: "module not found"
severity: warning
- error: pageNotFound
message: "page not found"
severity: warning
- error: recordNotFound
message: "record not found"
severity: warning
- error: invalidID
message: "invalid ID"
severity: warning
- error: invalidNamespaceID
message: "invalid namespace ID"
severity: warning
- error: invalidModuleID
message: "invalid module ID"
severity: warning
- error: invalidModuleField
message: "invalid module field"
severity: warning
- error: invalidPageID
message: "invalid page ID"
severity: warning
- error: invalidRecordID
message: "invalid record ID"
severity: warning
- error: tooLarge
message: "file too large"
log: "could not upload this file, file size too large"
- error: notAllowedToUploadThisType
message: "file type not allowed"
log: "could not upload this file, file type not allowed"
- error: notAllowedToListAttachments
message: "not allowed to list attachments"
log: "could not list attachments; insufficient permissions"
- error: notAllowedToCreate
message: "not allowed to create attachments"
log: "could not create attachments; insufficient permissions"
- error: notAllowedToCreateEmptyAttachment
message: "not allowed to create empty attachments"
log: "failed to create attachment; empty file"
- error: failedToExtractMimeType
message: "could not extract mime type"
- error: failedToStoreFile
message: "could not extract store file"
- error: failedToProcessImage
message: "could not process image"
- error: notAllowedToRead
message: "not allowed to read this module"
log: "could not delete {{module}}; insufficient permissions"
- error: notAllowedToSearch
message: "not allowed to search or list modules"
log: "could not search or list modules; insufficient permissions"
- error: notAllowedToReadNamespace
message: "not allowed to read this namespace"
log: "could not delete {{namespace}}; insufficient permissions"
- error: notAllowedToReadPage
message: "not allowed to read this page"
log: "could not read {{page}}; insufficient permissions"
- error: notAllowedToReadRecord
message: "not allowed to read this record"
log: "could not read {{record}}; insufficient permissions"
- error: notAllowedToUpdatePage
message: "not allowed to update this page"
log: "could not update {{page}}; insufficient permissions"
- error: notAllowedToCreateRecords
message: "not allowed to create records"
log: "could not create records; insufficient permissions"
- error: notAllowedToUpdateRecord
message: "not allowed to update this record"
log: "could not update {{record}}; insufficient permissions"
- error: notAllowedToUpdateNamespace
message: "not allowed to update this namespace"
log: "could not update {{namespace}}; insufficient permissions"

View File

@@ -0,0 +1,447 @@
package service
import (
"context"
"reflect"
"strconv"
"github.com/cortezaproject/corteza/server/compose/types"
"github.com/cortezaproject/corteza/server/pkg/actionlog"
"github.com/cortezaproject/corteza/server/pkg/errors"
"github.com/cortezaproject/corteza/server/pkg/handle"
"github.com/cortezaproject/corteza/server/pkg/id"
"github.com/cortezaproject/corteza/server/pkg/label"
"github.com/cortezaproject/corteza/server/pkg/locale"
"github.com/cortezaproject/corteza/server/store"
)
type (
chart struct {
actionlog actionlog.Recorder
ac chartAccessController
store store.Storer
locale ResourceTranslationsManagerService
}
chartAccessController interface {
CanManageResourceTranslations(ctx context.Context) bool
CanSearchChartsOnNamespace(context.Context, *types.Namespace) bool
CanReadNamespace(context.Context, *types.Namespace) bool
CanCreateChartOnNamespace(context.Context, *types.Namespace) bool
CanReadChart(context.Context, *types.Chart) bool
CanUpdateChart(context.Context, *types.Chart) bool
CanDeleteChart(context.Context, *types.Chart) bool
}
chartUpdateHandler func(ctx context.Context, ns *types.Namespace, c *types.Chart) (chartChanges, error)
chartChanges uint8
)
const (
chartUnchanged chartChanges = 0
chartChanged chartChanges = 1
chartLabelsChanged chartChanges = 2
)
func Chart() *chart {
return &chart{
ac: DefaultAccessControl,
actionlog: DefaultActionlog,
store: DefaultStore,
locale: DefaultResourceTranslation,
}
}
func (svc chart) Find(ctx context.Context, filter types.ChartFilter) (set types.ChartSet, f types.ChartFilter, err error) {
var (
aProps = &chartActionProps{filter: &filter}
ns *types.Namespace
)
// For each fetched item, store backend will check if it is valid or not
filter.Check = func(res *types.Chart) (bool, error) {
if !svc.ac.CanReadChart(ctx, res) {
return false, nil
}
return true, nil
}
err = func() error {
ns, err = loadNamespace(ctx, svc.store, filter.NamespaceID)
if err != nil {
return err
}
aProps.setNamespace(ns)
if !svc.ac.CanSearchChartsOnNamespace(ctx, ns) {
return ChartErrNotAllowedToSearch()
}
if len(filter.Labels) > 0 {
filter.LabeledIDs, err = label.Search(
ctx,
svc.store,
types.Chart{}.LabelResourceKind(),
filter.Labels,
id.Uints(filter.ChartID...)...,
)
if err != nil {
return err
}
// labels specified but no labeled resources found
if len(filter.LabeledIDs) == 0 {
return nil
}
}
if set, f, err = store.SearchComposeCharts(ctx, svc.store, filter); err != nil {
return err
}
if err = label.Load(ctx, svc.store, toLabeledCharts(set)...); err != nil {
return err
}
set.Walk(func(c *types.Chart) error {
svc.proc(ctx, c)
return nil
})
return nil
}()
return set, f, svc.recordAction(ctx, aProps, ChartActionSearch, err)
}
func (svc chart) FindByID(ctx context.Context, namespaceID, chartID uint64) (c *types.Chart, err error) {
return svc.lookup(ctx, namespaceID, func(aProps *chartActionProps) (*types.Chart, error) {
return loadChart(ctx, svc.store, namespaceID, chartID)
})
}
func (svc chart) FindByHandle(ctx context.Context, namespaceID uint64, h string) (c *types.Chart, err error) {
return svc.lookup(ctx, namespaceID, func(aProps *chartActionProps) (*types.Chart, error) {
if !handle.IsValid(h) {
return nil, ChartErrInvalidHandle()
}
aProps.chart.Handle = h
return store.LookupComposeChartByNamespaceIDHandle(ctx, svc.store, namespaceID, h)
})
}
func (svc chart) proc(ctx context.Context, c *types.Chart) {
if svc.locale == nil || svc.locale.Locale() == nil {
return
}
tag := locale.GetAcceptLanguageFromContext(ctx)
c.DecodeTranslations(svc.locale.Locale().ResourceTranslations(tag, c.ResourceTranslation()))
}
func (svc chart) Create(ctx context.Context, new *types.Chart) (*types.Chart, error) {
var (
err error
ns *types.Namespace
aProps = &chartActionProps{chart: new}
)
err = store.Tx(ctx, svc.store, func(ctx context.Context, s store.Storer) (err error) {
if !handle.IsValid(new.Handle) {
return ChartErrInvalidHandle()
}
if ns, err = loadNamespace(ctx, s, new.NamespaceID); err != nil {
return err
}
aProps.setNamespace(ns)
if !svc.ac.CanCreateChartOnNamespace(ctx, ns) {
return ChartErrNotAllowedToCreate()
}
if err = svc.uniqueCheck(ctx, new); err != nil {
return err
}
new.ID = nextID()
new.CreatedAt = *now()
new.UpdatedAt = nil
new.DeletedAt = nil
// generate config element IDs
new.Config.GenerateIDs(nextID)
aProps.setChanged(new)
if err = store.CreateComposeChart(ctx, s, new); err != nil {
return err
}
if err = updateTranslations(ctx, svc.ac, svc.locale, new.EncodeTranslations()...); err != nil {
return
}
if err = label.Create(ctx, s, new); err != nil {
return
}
return nil
})
return new, svc.recordAction(ctx, aProps, ChartActionCreate, err)
}
func (svc chart) Update(ctx context.Context, upd *types.Chart) (c *types.Chart, err error) {
return svc.updater(ctx, upd.NamespaceID, upd.ID, ChartActionUpdate, svc.handleUpdate(ctx, upd))
}
func (svc chart) DeleteByID(ctx context.Context, namespaceID, chartID uint64) error {
return trim1st(svc.updater(ctx, namespaceID, chartID, ChartActionDelete, svc.handleDelete))
}
func (svc chart) UndeleteByID(ctx context.Context, namespaceID, chartID uint64) error {
return trim1st(svc.updater(ctx, namespaceID, chartID, ChartActionUndelete, svc.handleUndelete))
}
// lookup fn() orchestrates chart lookup, namespace preload and check
func (svc chart) lookup(ctx context.Context, namespaceID uint64, lookup func(*chartActionProps) (*types.Chart, error)) (c *types.Chart, err error) {
var aProps = &chartActionProps{chart: &types.Chart{NamespaceID: namespaceID}}
err = func() error {
if ns, err := loadNamespace(ctx, svc.store, namespaceID); err != nil {
return err
} else {
aProps.setNamespace(ns)
}
if c, err = lookup(aProps); errors.IsNotFound(err) {
return ChartErrNotFound()
} else if err != nil {
return err
}
aProps.setChart(c)
if !svc.ac.CanReadChart(ctx, c) {
return ChartErrNotAllowedToRead()
}
svc.proc(ctx, c)
if err = label.Load(ctx, svc.store, c); err != nil {
return err
}
return nil
}()
return c, svc.recordAction(ctx, aProps, ChartActionLookup, err)
}
func (svc chart) updater(ctx context.Context, namespaceID, chartID uint64, action func(...*chartActionProps) *chartAction, fn chartUpdateHandler) (*types.Chart, error) {
var (
changes chartChanges
ns *types.Namespace
c *types.Chart
aProps = &chartActionProps{chart: &types.Chart{ID: chartID, NamespaceID: namespaceID}}
err error
)
err = store.Tx(ctx, svc.store, func(ctx context.Context, s store.Storer) (err error) {
ns, c, err = loadChartCombo(ctx, s, namespaceID, chartID)
if err != nil {
return
}
if err = label.Load(ctx, svc.store, c); err != nil {
return err
}
aProps.setNamespace(ns)
aProps.setChanged(c)
if changes, err = fn(ctx, ns, c); err != nil {
return err
}
// generate config element IDs if missing
c.Config.GenerateIDs(nextID)
if changes&chartChanged > 0 {
if err = store.UpdateComposeChart(ctx, s, c); err != nil {
return err
}
}
if err = updateTranslations(ctx, svc.ac, svc.locale, c.EncodeTranslations()...); err != nil {
return
}
if changes&chartLabelsChanged > 0 {
if err = label.Update(ctx, s, c); err != nil {
return
}
}
return nil
})
return c, svc.recordAction(ctx, aProps, action, err)
}
func (svc chart) uniqueCheck(ctx context.Context, c *types.Chart) (err error) {
if c.Handle != "" {
if e, _ := store.LookupComposeChartByNamespaceIDHandle(ctx, svc.store, c.NamespaceID, c.Handle); e != nil && e.ID != c.ID {
return ChartErrHandleNotUnique()
}
}
return nil
}
func (svc chart) handleUpdate(ctx context.Context, upd *types.Chart) chartUpdateHandler {
return func(ctx context.Context, ns *types.Namespace, res *types.Chart) (changes chartChanges, err error) {
if isStale(upd.UpdatedAt, res.UpdatedAt, res.CreatedAt) {
return chartUnchanged, ChartErrStaleData()
}
if upd.Handle != res.Handle && !handle.IsValid(upd.Handle) {
return chartUnchanged, ChartErrInvalidHandle()
}
if err := svc.uniqueCheck(ctx, upd); err != nil {
return chartUnchanged, err
}
if !svc.ac.CanUpdateChart(ctx, res) {
return chartUnchanged, ChartErrNotAllowedToUpdate()
}
if res.Name != upd.Name {
changes |= chartChanged
res.Name = upd.Name
}
if res.Handle != upd.Handle {
changes |= chartChanged
res.Handle = upd.Handle
}
if !reflect.DeepEqual(upd.Config, res.Config) {
changes |= chartChanged
res.Config = upd.Config
}
// Assure ReportIDs
for i, r := range res.Config.Reports {
if r.ReportID == 0 {
r.ReportID = nextID()
res.Config.Reports[i] = r
changes |= chartChanged
}
// Ensure chart report metric IDs
for j, m := range r.Metrics {
if val, ok := m["metricID"]; !ok || val == 0 {
m["metricID"] = strconv.FormatUint(nextID(), 10)
res.Config.Reports[i].Metrics[j] = m
changes |= chartChanged
}
}
}
if changes&chartChanged > 0 {
res.UpdatedAt = now()
}
if upd.Labels != nil {
if label.Changed(res.Labels, upd.Labels) {
changes |= chartLabelsChanged
res.Labels = upd.Labels
}
}
return
}
}
func (svc chart) handleDelete(ctx context.Context, ns *types.Namespace, c *types.Chart) (chartChanges, error) {
if !svc.ac.CanDeleteChart(ctx, c) {
return chartUnchanged, ChartErrNotAllowedToDelete()
}
if c.DeletedAt != nil {
// chart already deleted
return chartUnchanged, nil
}
c.DeletedAt = now()
return chartChanged, nil
}
func (svc chart) handleUndelete(ctx context.Context, ns *types.Namespace, c *types.Chart) (chartChanges, error) {
if !svc.ac.CanDeleteChart(ctx, c) {
return chartUnchanged, ChartErrNotAllowedToUndelete()
}
if c.DeletedAt == nil {
// chart not deleted
return chartUnchanged, nil
}
c.DeletedAt = nil
return chartChanged, nil
}
func loadChartCombo(ctx context.Context, s interface {
store.ComposeCharts
store.ComposeNamespaces
}, namespaceID, chartID uint64) (ns *types.Namespace, c *types.Chart, err error) {
ns, err = loadNamespace(ctx, s, namespaceID)
if err != nil {
return
}
c, err = loadChart(ctx, s, namespaceID, chartID)
return
}
func loadChart(ctx context.Context, s store.ComposeCharts, namespaceID, chartID uint64) (res *types.Chart, err error) {
if chartID == 0 || namespaceID == 0 {
return nil, ChartErrInvalidID()
}
if res, err = store.LookupComposeChartByID(ctx, s, chartID); errors.IsNotFound(err) {
err = ChartErrNotFound()
}
if err == nil && namespaceID != res.NamespaceID {
// Make sure chart belongs to the right namespace
return nil, ChartErrNotFound()
}
return
}
// toLabeledCharts converts to []label.LabeledResource
//
// This function is auto-generated.
func toLabeledCharts(set []*types.Chart) []label.LabeledResource {
if len(set) == 0 {
return nil
}
ll := make([]label.LabeledResource, len(set))
for i := range set {
ll[i] = set[i]
}
return ll
}

View File

@@ -0,0 +1,964 @@
package service
// 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/service/chart_actions.yaml
import (
"context"
"fmt"
"github.com/cortezaproject/corteza/server/compose/types"
"github.com/cortezaproject/corteza/server/pkg/actionlog"
"github.com/cortezaproject/corteza/server/pkg/errors"
"github.com/cortezaproject/corteza/server/pkg/locale"
"strings"
"time"
)
type (
chartActionProps struct {
chart *types.Chart
changed *types.Chart
filter *types.ChartFilter
namespace *types.Namespace
}
chartAction struct {
timestamp time.Time
resource string
action string
log string
severity actionlog.Severity
// prefix for error when action fails
errorMessage string
props *chartActionProps
}
chartLogMetaKey struct{}
chartPropsMetaKey struct{}
)
var (
// just a placeholder to cover template cases w/o fmt package use
_ = fmt.Println
)
// *********************************************************************************************************************
// *********************************************************************************************************************
// Props methods
// setChart updates chartActionProps's chart
//
// This function is auto-generated.
func (p *chartActionProps) setChart(chart *types.Chart) *chartActionProps {
p.chart = chart
return p
}
// setChanged updates chartActionProps's changed
//
// This function is auto-generated.
func (p *chartActionProps) setChanged(changed *types.Chart) *chartActionProps {
p.changed = changed
return p
}
// setFilter updates chartActionProps's filter
//
// This function is auto-generated.
func (p *chartActionProps) setFilter(filter *types.ChartFilter) *chartActionProps {
p.filter = filter
return p
}
// setNamespace updates chartActionProps's namespace
//
// This function is auto-generated.
func (p *chartActionProps) setNamespace(namespace *types.Namespace) *chartActionProps {
p.namespace = namespace
return p
}
// Serialize converts chartActionProps to actionlog.Meta
//
// This function is auto-generated.
func (p chartActionProps) Serialize() actionlog.Meta {
var (
m = make(actionlog.Meta)
)
if p.chart != nil {
m.Set("chart.name", p.chart.Name, true)
m.Set("chart.handle", p.chart.Handle, true)
m.Set("chart.ID", p.chart.ID, true)
m.Set("chart.namespaceID", p.chart.NamespaceID, true)
}
if p.changed != nil {
m.Set("changed.name", p.changed.Name, true)
m.Set("changed.handle", p.changed.Handle, true)
m.Set("changed.ID", p.changed.ID, true)
m.Set("changed.namespaceID", p.changed.NamespaceID, true)
m.Set("changed.config", p.changed.Config, true)
}
if p.filter != nil {
m.Set("filter.query", p.filter.Query, true)
m.Set("filter.handle", p.filter.Handle, true)
m.Set("filter.namespaceID", p.filter.NamespaceID, true)
m.Set("filter.sort", p.filter.Sort, true)
m.Set("filter.limit", p.filter.Limit, true)
}
if p.namespace != nil {
m.Set("namespace.name", p.namespace.Name, true)
m.Set("namespace.slug", p.namespace.Slug, true)
m.Set("namespace.ID", p.namespace.ID, true)
}
return m
}
// tr translates string and replaces meta value placeholder with values
//
// This function is auto-generated.
func (p chartActionProps) Format(in string, err error) string {
var (
pairs = []string{"{{err}}"}
// first non-empty string
fns = func(ii ...interface{}) string {
for _, i := range ii {
if s := fmt.Sprintf("%v", i); len(s) > 0 {
return s
}
}
return ""
}
)
if err != nil {
pairs = append(pairs, err.Error())
} else {
pairs = append(pairs, "nil")
}
if p.chart != nil {
// replacement for "{{chart}}" (in order how fields are defined)
pairs = append(
pairs,
"{{chart}}",
fns(
p.chart.Name,
p.chart.Handle,
p.chart.ID,
p.chart.NamespaceID,
),
)
pairs = append(pairs, "{{chart.name}}", fns(p.chart.Name))
pairs = append(pairs, "{{chart.handle}}", fns(p.chart.Handle))
pairs = append(pairs, "{{chart.ID}}", fns(p.chart.ID))
pairs = append(pairs, "{{chart.namespaceID}}", fns(p.chart.NamespaceID))
}
if p.changed != nil {
// replacement for "{{changed}}" (in order how fields are defined)
pairs = append(
pairs,
"{{changed}}",
fns(
p.changed.Name,
p.changed.Handle,
p.changed.ID,
p.changed.NamespaceID,
p.changed.Config,
),
)
pairs = append(pairs, "{{changed.name}}", fns(p.changed.Name))
pairs = append(pairs, "{{changed.handle}}", fns(p.changed.Handle))
pairs = append(pairs, "{{changed.ID}}", fns(p.changed.ID))
pairs = append(pairs, "{{changed.namespaceID}}", fns(p.changed.NamespaceID))
pairs = append(pairs, "{{changed.config}}", fns(p.changed.Config))
}
if p.filter != nil {
// replacement for "{{filter}}" (in order how fields are defined)
pairs = append(
pairs,
"{{filter}}",
fns(
p.filter.Query,
p.filter.Handle,
p.filter.NamespaceID,
p.filter.Sort,
p.filter.Limit,
),
)
pairs = append(pairs, "{{filter.query}}", fns(p.filter.Query))
pairs = append(pairs, "{{filter.handle}}", fns(p.filter.Handle))
pairs = append(pairs, "{{filter.namespaceID}}", fns(p.filter.NamespaceID))
pairs = append(pairs, "{{filter.sort}}", fns(p.filter.Sort))
pairs = append(pairs, "{{filter.limit}}", fns(p.filter.Limit))
}
if p.namespace != nil {
// replacement for "{{namespace}}" (in order how fields are defined)
pairs = append(
pairs,
"{{namespace}}",
fns(
p.namespace.Name,
p.namespace.Slug,
p.namespace.ID,
),
)
pairs = append(pairs, "{{namespace.name}}", fns(p.namespace.Name))
pairs = append(pairs, "{{namespace.slug}}", fns(p.namespace.Slug))
pairs = append(pairs, "{{namespace.ID}}", fns(p.namespace.ID))
}
return strings.NewReplacer(pairs...).Replace(in)
}
// *********************************************************************************************************************
// *********************************************************************************************************************
// Action methods
// String returns loggable description as string
//
// This function is auto-generated.
func (a *chartAction) String() string {
var props = &chartActionProps{}
if a.props != nil {
props = a.props
}
return props.Format(a.log, nil)
}
func (e *chartAction) ToAction() *actionlog.Action {
return &actionlog.Action{
Resource: e.resource,
Action: e.action,
Severity: e.severity,
Description: e.String(),
Meta: e.props.Serialize(),
}
}
// *********************************************************************************************************************
// *********************************************************************************************************************
// Action constructors
// ChartActionSearch returns "compose:chart.search" action
//
// This function is auto-generated.
func ChartActionSearch(props ...*chartActionProps) *chartAction {
a := &chartAction{
timestamp: time.Now(),
resource: "compose:chart",
action: "search",
log: "searched for charts",
severity: actionlog.Info,
}
if len(props) > 0 {
a.props = props[0]
}
return a
}
// ChartActionLookup returns "compose:chart.lookup" action
//
// This function is auto-generated.
func ChartActionLookup(props ...*chartActionProps) *chartAction {
a := &chartAction{
timestamp: time.Now(),
resource: "compose:chart",
action: "lookup",
log: "looked-up for a {{chart}}",
severity: actionlog.Info,
}
if len(props) > 0 {
a.props = props[0]
}
return a
}
// ChartActionCreate returns "compose:chart.create" action
//
// This function is auto-generated.
func ChartActionCreate(props ...*chartActionProps) *chartAction {
a := &chartAction{
timestamp: time.Now(),
resource: "compose:chart",
action: "create",
log: "created {{chart}}",
severity: actionlog.Notice,
}
if len(props) > 0 {
a.props = props[0]
}
return a
}
// ChartActionUpdate returns "compose:chart.update" action
//
// This function is auto-generated.
func ChartActionUpdate(props ...*chartActionProps) *chartAction {
a := &chartAction{
timestamp: time.Now(),
resource: "compose:chart",
action: "update",
log: "updated {{chart}}",
severity: actionlog.Notice,
}
if len(props) > 0 {
a.props = props[0]
}
return a
}
// ChartActionDelete returns "compose:chart.delete" action
//
// This function is auto-generated.
func ChartActionDelete(props ...*chartActionProps) *chartAction {
a := &chartAction{
timestamp: time.Now(),
resource: "compose:chart",
action: "delete",
log: "deleted {{chart}}",
severity: actionlog.Notice,
}
if len(props) > 0 {
a.props = props[0]
}
return a
}
// ChartActionUndelete returns "compose:chart.undelete" action
//
// This function is auto-generated.
func ChartActionUndelete(props ...*chartActionProps) *chartAction {
a := &chartAction{
timestamp: time.Now(),
resource: "compose:chart",
action: "undelete",
log: "undeleted {{chart}}",
severity: actionlog.Notice,
}
if len(props) > 0 {
a.props = props[0]
}
return a
}
// ChartActionReorder returns "compose:chart.reorder" action
//
// This function is auto-generated.
func ChartActionReorder(props ...*chartActionProps) *chartAction {
a := &chartAction{
timestamp: time.Now(),
resource: "compose:chart",
action: "reorder",
log: "reordered {{chart}}",
severity: actionlog.Notice,
}
if len(props) > 0 {
a.props = props[0]
}
return a
}
// *********************************************************************************************************************
// *********************************************************************************************************************
// Error constructors
// ChartErrGeneric returns "compose:chart.generic" as *errors.Error
//
// This function is auto-generated.
func ChartErrGeneric(mm ...*chartActionProps) *errors.Error {
var p = &chartActionProps{}
if len(mm) > 0 {
p = mm[0]
}
var e = errors.New(
errors.KindInternal,
p.Format("failed to complete request due to internal error", nil),
errors.Meta("type", "generic"),
errors.Meta("resource", "compose:chart"),
// action log entry; no formatting, it will be applied inside recordAction fn.
errors.Meta(chartLogMetaKey{}, "{err}"),
errors.Meta(chartPropsMetaKey{}, p),
// translation namespace & key
errors.Meta(locale.ErrorMetaNamespace{}, "compose"),
errors.Meta(locale.ErrorMetaKey{}, "chart.errors.generic"),
errors.StackSkip(1),
)
if len(mm) > 0 {
}
return e
}
// ChartErrNotFound returns "compose:chart.notFound" as *errors.Error
//
// This function is auto-generated.
func ChartErrNotFound(mm ...*chartActionProps) *errors.Error {
var p = &chartActionProps{}
if len(mm) > 0 {
p = mm[0]
}
var e = errors.New(
errors.KindInternal,
p.Format("chart does not exist", nil),
errors.Meta("type", "notFound"),
errors.Meta("resource", "compose:chart"),
errors.Meta(chartPropsMetaKey{}, p),
// translation namespace & key
errors.Meta(locale.ErrorMetaNamespace{}, "compose"),
errors.Meta(locale.ErrorMetaKey{}, "chart.errors.notFound"),
errors.StackSkip(1),
)
if len(mm) > 0 {
}
return e
}
// ChartErrNamespaceNotFound returns "compose:chart.namespaceNotFound" as *errors.Error
//
// This function is auto-generated.
func ChartErrNamespaceNotFound(mm ...*chartActionProps) *errors.Error {
var p = &chartActionProps{}
if len(mm) > 0 {
p = mm[0]
}
var e = errors.New(
errors.KindInternal,
p.Format("namespace does not exist", nil),
errors.Meta("type", "namespaceNotFound"),
errors.Meta("resource", "compose:chart"),
errors.Meta(chartPropsMetaKey{}, p),
// translation namespace & key
errors.Meta(locale.ErrorMetaNamespace{}, "compose"),
errors.Meta(locale.ErrorMetaKey{}, "chart.errors.namespaceNotFound"),
errors.StackSkip(1),
)
if len(mm) > 0 {
}
return e
}
// ChartErrModuleNotFound returns "compose:chart.moduleNotFound" as *errors.Error
//
// This function is auto-generated.
func ChartErrModuleNotFound(mm ...*chartActionProps) *errors.Error {
var p = &chartActionProps{}
if len(mm) > 0 {
p = mm[0]
}
var e = errors.New(
errors.KindInternal,
p.Format("module does not exist", nil),
errors.Meta("type", "moduleNotFound"),
errors.Meta("resource", "compose:chart"),
errors.Meta(chartPropsMetaKey{}, p),
// translation namespace & key
errors.Meta(locale.ErrorMetaNamespace{}, "compose"),
errors.Meta(locale.ErrorMetaKey{}, "chart.errors.moduleNotFound"),
errors.StackSkip(1),
)
if len(mm) > 0 {
}
return e
}
// ChartErrInvalidID returns "compose:chart.invalidID" as *errors.Error
//
// This function is auto-generated.
func ChartErrInvalidID(mm ...*chartActionProps) *errors.Error {
var p = &chartActionProps{}
if len(mm) > 0 {
p = mm[0]
}
var e = errors.New(
errors.KindInternal,
p.Format("invalid ID", nil),
errors.Meta("type", "invalidID"),
errors.Meta("resource", "compose:chart"),
errors.Meta(chartPropsMetaKey{}, p),
// translation namespace & key
errors.Meta(locale.ErrorMetaNamespace{}, "compose"),
errors.Meta(locale.ErrorMetaKey{}, "chart.errors.invalidID"),
errors.StackSkip(1),
)
if len(mm) > 0 {
}
return e
}
// ChartErrInvalidHandle returns "compose:chart.invalidHandle" as *errors.Error
//
// This function is auto-generated.
func ChartErrInvalidHandle(mm ...*chartActionProps) *errors.Error {
var p = &chartActionProps{}
if len(mm) > 0 {
p = mm[0]
}
var e = errors.New(
errors.KindInternal,
p.Format("invalid handle", nil),
errors.Meta("type", "invalidHandle"),
errors.Meta("resource", "compose:chart"),
errors.Meta(chartPropsMetaKey{}, p),
// translation namespace & key
errors.Meta(locale.ErrorMetaNamespace{}, "compose"),
errors.Meta(locale.ErrorMetaKey{}, "chart.errors.invalidHandle"),
errors.StackSkip(1),
)
if len(mm) > 0 {
}
return e
}
// ChartErrHandleNotUnique returns "compose:chart.handleNotUnique" as *errors.Error
//
// This function is auto-generated.
func ChartErrHandleNotUnique(mm ...*chartActionProps) *errors.Error {
var p = &chartActionProps{}
if len(mm) > 0 {
p = mm[0]
}
var e = errors.New(
errors.KindInternal,
p.Format("handle not unique", nil),
errors.Meta("type", "handleNotUnique"),
errors.Meta("resource", "compose:chart"),
// action log entry; no formatting, it will be applied inside recordAction fn.
errors.Meta(chartLogMetaKey{}, "used duplicate handle ({{chart.handle}}) for chart"),
errors.Meta(chartPropsMetaKey{}, p),
// translation namespace & key
errors.Meta(locale.ErrorMetaNamespace{}, "compose"),
errors.Meta(locale.ErrorMetaKey{}, "chart.errors.handleNotUnique"),
errors.StackSkip(1),
)
if len(mm) > 0 {
}
return e
}
// ChartErrStaleData returns "compose:chart.staleData" as *errors.Error
//
// This function is auto-generated.
func ChartErrStaleData(mm ...*chartActionProps) *errors.Error {
var p = &chartActionProps{}
if len(mm) > 0 {
p = mm[0]
}
var e = errors.New(
errors.KindInternal,
p.Format("chart was modified by someone else or by a workflow after you've opened it. Please refresh to see the latest updated version", nil),
errors.Meta("type", "staleData"),
errors.Meta("resource", "compose:chart"),
errors.Meta(chartPropsMetaKey{}, p),
// translation namespace & key
errors.Meta(locale.ErrorMetaNamespace{}, "compose"),
errors.Meta(locale.ErrorMetaKey{}, "chart.errors.staleData"),
errors.StackSkip(1),
)
if len(mm) > 0 {
}
return e
}
// ChartErrInvalidNamespaceID returns "compose:chart.invalidNamespaceID" as *errors.Error
//
// This function is auto-generated.
func ChartErrInvalidNamespaceID(mm ...*chartActionProps) *errors.Error {
var p = &chartActionProps{}
if len(mm) > 0 {
p = mm[0]
}
var e = errors.New(
errors.KindInternal,
p.Format("invalid or missing namespace ID", nil),
errors.Meta("type", "invalidNamespaceID"),
errors.Meta("resource", "compose:chart"),
errors.Meta(chartPropsMetaKey{}, p),
// translation namespace & key
errors.Meta(locale.ErrorMetaNamespace{}, "compose"),
errors.Meta(locale.ErrorMetaKey{}, "chart.errors.invalidNamespaceID"),
errors.StackSkip(1),
)
if len(mm) > 0 {
}
return e
}
// ChartErrNotAllowedToRead returns "compose:chart.notAllowedToRead" as *errors.Error
//
// This function is auto-generated.
func ChartErrNotAllowedToRead(mm ...*chartActionProps) *errors.Error {
var p = &chartActionProps{}
if len(mm) > 0 {
p = mm[0]
}
var e = errors.New(
errors.KindInternal,
p.Format("not allowed to read this chart", nil),
errors.Meta("type", "notAllowedToRead"),
errors.Meta("resource", "compose:chart"),
// action log entry; no formatting, it will be applied inside recordAction fn.
errors.Meta(chartLogMetaKey{}, "could not read {{chart}}; insufficient permissions"),
errors.Meta(chartPropsMetaKey{}, p),
// translation namespace & key
errors.Meta(locale.ErrorMetaNamespace{}, "compose"),
errors.Meta(locale.ErrorMetaKey{}, "chart.errors.notAllowedToRead"),
errors.StackSkip(1),
)
if len(mm) > 0 {
}
return e
}
// ChartErrNotAllowedToSearch returns "compose:chart.notAllowedToSearch" as *errors.Error
//
// This function is auto-generated.
func ChartErrNotAllowedToSearch(mm ...*chartActionProps) *errors.Error {
var p = &chartActionProps{}
if len(mm) > 0 {
p = mm[0]
}
var e = errors.New(
errors.KindInternal,
p.Format("not allowed to search or list charts", nil),
errors.Meta("type", "notAllowedToSearch"),
errors.Meta("resource", "compose:chart"),
// action log entry; no formatting, it will be applied inside recordAction fn.
errors.Meta(chartLogMetaKey{}, "could not search or list charts; insufficient permissions"),
errors.Meta(chartPropsMetaKey{}, p),
// translation namespace & key
errors.Meta(locale.ErrorMetaNamespace{}, "compose"),
errors.Meta(locale.ErrorMetaKey{}, "chart.errors.notAllowedToSearch"),
errors.StackSkip(1),
)
if len(mm) > 0 {
}
return e
}
// ChartErrNotAllowedToReadNamespace returns "compose:chart.notAllowedToReadNamespace" as *errors.Error
//
// This function is auto-generated.
func ChartErrNotAllowedToReadNamespace(mm ...*chartActionProps) *errors.Error {
var p = &chartActionProps{}
if len(mm) > 0 {
p = mm[0]
}
var e = errors.New(
errors.KindInternal,
p.Format("not allowed to read this namespace", nil),
errors.Meta("type", "notAllowedToReadNamespace"),
errors.Meta("resource", "compose:chart"),
// action log entry; no formatting, it will be applied inside recordAction fn.
errors.Meta(chartLogMetaKey{}, "could not read namespace {{namespace}}; insufficient permissions"),
errors.Meta(chartPropsMetaKey{}, p),
// translation namespace & key
errors.Meta(locale.ErrorMetaNamespace{}, "compose"),
errors.Meta(locale.ErrorMetaKey{}, "chart.errors.notAllowedToReadNamespace"),
errors.StackSkip(1),
)
if len(mm) > 0 {
}
return e
}
// ChartErrNotAllowedToCreate returns "compose:chart.notAllowedToCreate" as *errors.Error
//
// This function is auto-generated.
func ChartErrNotAllowedToCreate(mm ...*chartActionProps) *errors.Error {
var p = &chartActionProps{}
if len(mm) > 0 {
p = mm[0]
}
var e = errors.New(
errors.KindInternal,
p.Format("not allowed to create charts", nil),
errors.Meta("type", "notAllowedToCreate"),
errors.Meta("resource", "compose:chart"),
// action log entry; no formatting, it will be applied inside recordAction fn.
errors.Meta(chartLogMetaKey{}, "could not create charts; insufficient permissions"),
errors.Meta(chartPropsMetaKey{}, p),
// translation namespace & key
errors.Meta(locale.ErrorMetaNamespace{}, "compose"),
errors.Meta(locale.ErrorMetaKey{}, "chart.errors.notAllowedToCreate"),
errors.StackSkip(1),
)
if len(mm) > 0 {
}
return e
}
// ChartErrNotAllowedToUpdate returns "compose:chart.notAllowedToUpdate" as *errors.Error
//
// This function is auto-generated.
func ChartErrNotAllowedToUpdate(mm ...*chartActionProps) *errors.Error {
var p = &chartActionProps{}
if len(mm) > 0 {
p = mm[0]
}
var e = errors.New(
errors.KindInternal,
p.Format("not allowed to update this chart", nil),
errors.Meta("type", "notAllowedToUpdate"),
errors.Meta("resource", "compose:chart"),
// action log entry; no formatting, it will be applied inside recordAction fn.
errors.Meta(chartLogMetaKey{}, "could not update {{chart}}; insufficient permissions"),
errors.Meta(chartPropsMetaKey{}, p),
// translation namespace & key
errors.Meta(locale.ErrorMetaNamespace{}, "compose"),
errors.Meta(locale.ErrorMetaKey{}, "chart.errors.notAllowedToUpdate"),
errors.StackSkip(1),
)
if len(mm) > 0 {
}
return e
}
// ChartErrNotAllowedToDelete returns "compose:chart.notAllowedToDelete" as *errors.Error
//
// This function is auto-generated.
func ChartErrNotAllowedToDelete(mm ...*chartActionProps) *errors.Error {
var p = &chartActionProps{}
if len(mm) > 0 {
p = mm[0]
}
var e = errors.New(
errors.KindInternal,
p.Format("not allowed to delete this chart", nil),
errors.Meta("type", "notAllowedToDelete"),
errors.Meta("resource", "compose:chart"),
// action log entry; no formatting, it will be applied inside recordAction fn.
errors.Meta(chartLogMetaKey{}, "could not delete {{chart}}; insufficient permissions"),
errors.Meta(chartPropsMetaKey{}, p),
// translation namespace & key
errors.Meta(locale.ErrorMetaNamespace{}, "compose"),
errors.Meta(locale.ErrorMetaKey{}, "chart.errors.notAllowedToDelete"),
errors.StackSkip(1),
)
if len(mm) > 0 {
}
return e
}
// ChartErrNotAllowedToUndelete returns "compose:chart.notAllowedToUndelete" as *errors.Error
//
// This function is auto-generated.
func ChartErrNotAllowedToUndelete(mm ...*chartActionProps) *errors.Error {
var p = &chartActionProps{}
if len(mm) > 0 {
p = mm[0]
}
var e = errors.New(
errors.KindInternal,
p.Format("not allowed to undelete this chart", nil),
errors.Meta("type", "notAllowedToUndelete"),
errors.Meta("resource", "compose:chart"),
// action log entry; no formatting, it will be applied inside recordAction fn.
errors.Meta(chartLogMetaKey{}, "could not undelete {{chart}}; insufficient permissions"),
errors.Meta(chartPropsMetaKey{}, p),
// translation namespace & key
errors.Meta(locale.ErrorMetaNamespace{}, "compose"),
errors.Meta(locale.ErrorMetaKey{}, "chart.errors.notAllowedToUndelete"),
errors.StackSkip(1),
)
if len(mm) > 0 {
}
return e
}
// *********************************************************************************************************************
// *********************************************************************************************************************
// recordAction is a service helper function wraps function that can return error
//
// It will wrap unrecognized/internal errors with generic errors.
//
// This function is auto-generated.
func (svc chart) recordAction(ctx context.Context, props *chartActionProps, actionFn func(...*chartActionProps) *chartAction, err error) error {
if svc.actionlog == nil || actionFn == nil {
// action log disabled or no action fn passed, return error as-is
return err
} else if err == nil {
// action completed w/o error, record it
svc.actionlog.Record(ctx, actionFn(props).ToAction())
return nil
}
a := actionFn(props).ToAction()
// Extracting error information and recording it as action
a.Error = err.Error()
switch c := err.(type) {
case *errors.Error:
m := c.Meta()
a.Error = err.Error()
a.Severity = actionlog.Severity(m.AsInt("severity"))
a.Description = props.Format(m.AsString(chartLogMetaKey{}), err)
if p, has := m[chartPropsMetaKey{}]; has {
a.Meta = p.(*chartActionProps).Serialize()
}
svc.actionlog.Record(ctx, a)
default:
svc.actionlog.Record(ctx, a)
}
// Original error is passed on
return err
}

View File

@@ -0,0 +1,113 @@
# List of loggable service actions
resource: compose:chart
service: chart
# Default sensitivity for actions
defaultActionSeverity: notice
# default severity for errors
defaultErrorSeverity: error
import:
- github.com/cortezaproject/corteza/server/compose/types
props:
- name: chart
type: "*types.Chart"
fields: [ name, handle, ID, namespaceID ]
- name: changed
type: "*types.Chart"
fields: [ name, handle, ID, namespaceID, config ]
- name: filter
type: "*types.ChartFilter"
fields: [ query, handle, namespaceID, sort, limit ]
- name: namespace
type: "*types.Namespace"
fields: [ name, slug, ID ]
actions:
- action: search
log: "searched for charts"
severity: info
- action: lookup
log: "looked-up for a {{chart}}"
severity: info
- action: create
log: "created {{chart}}"
- action: update
log: "updated {{chart}}"
- action: delete
log: "deleted {{chart}}"
- action: undelete
log: "undeleted {{chart}}"
- action: reorder
log: "reordered {{chart}}"
errors:
- error: notFound
message: "chart does not exist"
severity: warning
- error: namespaceNotFound
message: "namespace does not exist"
severity: warning
- error: moduleNotFound
message: "module does not exist"
severity: warning
- error: invalidID
message: "invalid ID"
severity: warning
- error: invalidHandle
message: "invalid handle"
severity: warning
- error: handleNotUnique
message: "handle not unique"
log: "used duplicate handle ({{chart.handle}}) for chart"
severity: warning
- error: staleData
message: "chart was modified by someone else or by a workflow after you've opened it. Please refresh to see the latest updated version"
severity: warning
- error: invalidNamespaceID
message: "invalid or missing namespace ID"
severity: warning
- error: notAllowedToRead
message: "not allowed to read this chart"
log: "could not read {{chart}}; insufficient permissions"
- error: notAllowedToSearch
message: "not allowed to search or list charts"
log: "could not search or list charts; insufficient permissions"
- error: notAllowedToReadNamespace
message: "not allowed to read this namespace"
log: "could not read namespace {{namespace}}; insufficient permissions"
- error: notAllowedToCreate
message: "not allowed to create charts"
log: "could not create charts; insufficient permissions"
- error: notAllowedToUpdate
message: "not allowed to update this chart"
log: "could not update {{chart}}; insufficient permissions"
- error: notAllowedToDelete
message: "not allowed to delete this chart"
log: "could not delete {{chart}}; insufficient permissions"
- error: notAllowedToUndelete
message: "not allowed to undelete this chart"
log: "could not undelete {{chart}}; insufficient permissions"

View File

@@ -0,0 +1,100 @@
package service
import (
"context"
"testing"
"github.com/cortezaproject/corteza/server/pkg/rbac"
"github.com/cortezaproject/corteza/server/store/adapters/rdbms/drivers/sqlite"
"github.com/cortezaproject/corteza/server/compose/types"
"github.com/cortezaproject/corteza/server/pkg/errors"
"github.com/cortezaproject/corteza/server/store"
"github.com/stretchr/testify/require"
"go.uber.org/zap"
)
func TestCharts(t *testing.T) {
var (
ctx = context.Background()
s, err = sqlite.ConnectInMemory(ctx)
namespaceID = nextID()
ns *types.Namespace
)
if err != nil {
t.Fatalf("failed to init sqlite in-memory db: %v", err)
}
if err = store.Upgrade(ctx, zap.NewNop(), s); err != nil {
t.Fatalf("failed to upgrade store: %v", err)
}
if err = s.TruncateComposeNamespaces(ctx); err != nil {
t.Fatalf("failed to truncate compose namespaces: %v", err)
}
if err = s.TruncateComposeCharts(ctx); err != nil {
t.Fatalf("failed to truncate compose charts: %v", err)
}
ns = &types.Namespace{Name: "testing", ID: namespaceID, CreatedAt: *now()}
if err = store.CreateComposeNamespace(ctx, s, ns); err != nil {
t.Fatalf("failed to seed namespaces: %v", err)
}
t.Run("crud", func(t *testing.T) {
req := require.New(t)
svc := &chart{
store: s,
ac: &accessControl{rbac: &rbac.ServiceAllowAll{}},
}
res, err := svc.Create(ctx, &types.Chart{Name: "My first chart", NamespaceID: namespaceID})
req.NoError(unwrapChartInternal(err))
req.NotNil(res)
res, err = svc.FindByID(ctx, namespaceID, res.ID)
req.NoError(unwrapChartInternal(err))
req.NotNil(res)
res, err = svc.FindByHandle(ctx, namespaceID, res.Handle)
req.NoError(unwrapChartInternal(err))
req.NotNil(res)
res.Name = "Changed"
res, err = svc.Update(ctx, res)
req.NoError(unwrapChartInternal(err))
req.NotNil(res)
req.NotNil(res.UpdatedAt)
req.Equal(res.Name, "Changed")
res, err = svc.FindByID(ctx, namespaceID, res.ID)
req.NoError(unwrapChartInternal(err))
req.NotNil(res)
req.Equal(res.Name, "Changed")
err = svc.DeleteByID(ctx, namespaceID, res.ID)
req.NoError(unwrapChartInternal(err))
req.NotNil(res)
// this works because we're allowed to do everything
res, err = svc.FindByID(ctx, namespaceID, res.ID)
req.NoError(unwrapChartInternal(err))
req.NotNil(res)
req.NotNil(res.DeletedAt)
})
}
func unwrapChartInternal(err error) error {
g := ChartErrGeneric()
for {
if errors.Is(err, g) {
err = errors.Unwrap(err)
continue
}
return err
}
}

View File

@@ -0,0 +1,21 @@
package service
import (
"context"
"github.com/cortezaproject/corteza/server/pkg/dal"
"github.com/cortezaproject/corteza/server/pkg/filter"
)
type (
dalDater interface {
Create(ctx context.Context, m dal.ModelRef, operations dal.OperationSet, vv ...dal.ValueGetter) error
Update(ctx context.Context, m dal.ModelRef, operations dal.OperationSet, rr ...dal.ValueGetter) (err error)
Search(ctx context.Context, m dal.ModelRef, operations dal.OperationSet, f filter.Filter) (dal.Iterator, error)
Count(ctx context.Context, m dal.ModelRef, operations dal.OperationSet, f filter.Filter) (uint, error)
Run(ctx context.Context, pp dal.Pipeline) (dal.Iterator, error)
Lookup(ctx context.Context, m dal.ModelRef, operations dal.OperationSet, lookup dal.ValueGetter, dst dal.ValueSetter) (err error)
Delete(ctx context.Context, m dal.ModelRef, operations dal.OperationSet, pkv ...dal.ValueGetter) (err error)
Truncate(ctx context.Context, m dal.ModelRef, operations dal.OperationSet) (err error)
}
)

View File

@@ -0,0 +1,84 @@
package service
import (
"context"
"github.com/cortezaproject/corteza/server/compose/types"
"github.com/cortezaproject/corteza/server/pkg/locale"
sysService "github.com/cortezaproject/corteza/server/system/service"
sysTypes "github.com/cortezaproject/corteza/server/system/types"
)
type (
dataPrivacy struct {
ns NamespaceService
m ModuleService
dalConn dalConnectionService
locale ResourceTranslationsManagerService
}
dalConnectionService interface {
FindByID(ctx context.Context, ID uint64) (q *sysTypes.DalConnection, err error)
}
DataPrivacyService interface {
FindModules(ctx context.Context, filter types.PrivacyModuleFilter) (types.PrivacyModuleSet, types.PrivacyModuleFilter, error)
}
)
func DataPrivacy() *dataPrivacy {
return &dataPrivacy{
ns: DefaultNamespace,
m: DefaultModule,
dalConn: sysService.DefaultDalConnection,
locale: DefaultResourceTranslation,
}
}
func (svc dataPrivacy) FindModules(ctx context.Context, filter types.PrivacyModuleFilter) (out types.PrivacyModuleSet, f types.PrivacyModuleFilter, err error) {
var (
modules []types.PrivacyModule
cc = make(map[uint64]*sysTypes.DalConnection, 0)
)
namespaces, _, err := svc.ns.Find(ctx, types.NamespaceFilter{})
if err != nil {
return
}
for _, n := range namespaces {
tag := locale.GetAcceptLanguageFromContext(ctx)
n.DecodeTranslations(svc.locale.Locale().ResourceTranslations(tag, n.ResourceTranslation()))
modules, f, err = svc.m.SearchSensitive(ctx, types.PrivacyModuleFilter{
NamespaceID: n.ID,
ConnectionID: filter.ConnectionID,
})
if err != nil {
return
}
if len(modules) == 0 {
continue
}
for _, m := range modules {
connID := m.ConnectionID
if val, ok := cc[connID]; ok {
m.Connection = val
} else {
m.Connection, err = svc.dalConn.FindByID(ctx, connID)
if err != nil {
cc[connID] = m.Connection
}
}
m.Namespace = types.PrivacyNamespaceMeta{
ID: n.ID,
Slug: n.Slug,
Name: n.Name,
}
out = append(out, &m)
}
}
return out, f, nil
}

Some files were not shown because too many files have changed in this diff Show More