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

174
server/pkg/envoy/builder.go Normal file
View File

@@ -0,0 +1,174 @@
package envoy
import (
"context"
"fmt"
"io"
"github.com/cortezaproject/corteza/server/pkg/envoy/resource"
)
type (
builder struct {
pp []Preparer
reportMissingRefs bool
}
Stream struct {
Source io.Reader
Resource string
Identifier string
}
Preparer interface {
Prepare(ctx context.Context, ee ...*ResourceState) error
}
// Encoder encodes all resources provided by the Rc until nil is passed
//
// Encoding errors are passed via Ec.
Encoder interface {
Encode(ctx context.Context, p Provider) error
}
// Streamer provides a set of streams of encoded documents
Streamer interface {
Stream() []*Stream
}
PrepareEncoder interface {
Preparer
Encoder
}
PrepareEncodeStreamer interface {
Preparer
Encoder
Streamer
}
)
var (
BuilderErrUnresolvedReferences = fmt.Errorf("builder error: unresolved references")
)
func NewBuilder(pp ...Preparer) *builder {
return &builder{
pp: pp,
}
}
func NewSafeBuilder(pp ...Preparer) *builder {
return &builder{
pp: pp,
reportMissingRefs: true,
}
}
// Build builds the graph that is used for structured data processing
//
// Outline:
// 1. Build an initial graph so that we can do some structured preprocessing.
// 2. Preprocess the resources based on the initial graph. The initial graph
// should remain unchanged. Preprocessing can request additional references and
// constraints.
// 3. Build a final graph based on the preprocessing modifications.
func (b *builder) Build(ctx context.Context, rr ...resource.Interface) (*graph, error) {
var err error
g := b.buildGraph(rr)
if len(g.missingRefs) > 0 {
return g, BuilderErrUnresolvedReferences
}
// Do any dep. related preprocessing
var state *ResourceState
err = func() error {
for {
state, err = g.NextInverted(ctx)
if err != nil {
return err
} else if state == nil {
return nil
}
for _, p := range b.pp {
err = p.Prepare(ctx, state)
if err != nil {
return err
}
}
}
}()
if err != nil {
return nil, err
}
g = b.buildGraph(rr)
if len(g.missingRefs) > 0 {
return g, BuilderErrUnresolvedReferences
}
return g, nil
}
func (b *builder) buildGraph(rr []resource.Interface) *graph {
g := newGraph()
// Prepare nodes for all resources
nn := make(nodeSet, 0, len(rr))
for _, r := range rr {
nn = nn.add(newNode(r))
}
// Index all resources for nicer lookups
nIndex := make(nodeIndex)
nIndex.Add(nn...)
// Build the graph
for _, cNode := range nn {
refs := cNode.res.Refs()
// Attempt to connect all available nodes
for _, ref := range refs {
// Handle wildcard references
if ref.IsWildcard() {
nn := nIndex.GetResourceType(ref.ResourceType)
// Connect the nodes
for _, n := range nn {
g.addChild(cNode, n)
g.addParent(n, cNode)
}
} else {
// Handle regular references
rNode := nIndex.GetRef(ref)
if rNode == nil {
if !resource.IgnoreDepResolution(ref) {
cNode.missing = append(cNode.missing, ref)
}
continue
}
// The resource is available; we can connect the two
g.addChild(cNode, rNode)
g.addParent(rNode, cNode)
}
}
g.addNode(cNode)
}
if b.reportMissingRefs {
missing := make(resource.RefSet, 0, 10)
for _, n := range g.nn {
missing = append(missing, n.missing...)
}
if len(missing) > 0 {
g.missingRefs = missing
}
}
return g
}

View File

@@ -0,0 +1,186 @@
package envoy
import (
"context"
"testing"
"github.com/cortezaproject/corteza/server/pkg/envoy/resource"
"github.com/stretchr/testify/require"
)
type (
testResource struct {
resType string
identifiers resource.Identifiers
refs resource.RefSet
ph bool
}
)
func (t *testResource) Identifiers() resource.Identifiers {
return t.identifiers
}
func (t *testResource) ResourceType() string {
return t.resType
}
func (t *testResource) Refs() resource.RefSet {
return t.refs
}
func (t *testResource) MarkPlaceholder() {
t.ph = true
}
func (t *testResource) Placeholder() bool {
return t.ph
}
func (r *testResource) Resource() interface{} {
return nil
}
func (t *testResource) ReID(ii resource.Identifiers) {
// no need for implementation
panic("not implemented")
}
func (t *testResource) ReRef(old resource.RefSet, new resource.RefSet) {
// no need for implementation
panic("not implemented")
}
func TestGraphBuilder_Rel(t *testing.T) {
req := require.New(t)
ctx := context.Background()
t.Run("single, simple node; a", func(t *testing.T) {
bl := NewBuilder()
rr := []resource.Interface{
&testResource{
resType: "test:resource:1:",
identifiers: resource.Identifiers{"id1"},
refs: nil,
},
}
g, err := bl.Build(ctx, rr...)
g.invert()
req.NoError(err)
req.Len(g.nodes(), 1)
a := g.nodes()[0]
req.Empty(g.childNodes(a))
req.Empty(g.parentNodes(a))
})
t.Run("simple node link; a -> b", func(t *testing.T) {
bl := NewBuilder()
rr := []resource.Interface{
&testResource{
resType: "test:resource:1:",
identifiers: resource.Identifiers{"id1"},
refs: resource.RefSet{&resource.Ref{ResourceType: "test:resource:1:", Identifiers: resource.Identifiers{"id2"}}},
},
&testResource{
resType: "test:resource:1:",
identifiers: resource.Identifiers{"id2"},
refs: nil,
},
}
g, err := bl.Build(ctx, rr...)
req.NoError(err)
req.Len(g.nodes(), 2)
a := g.resIndex[rr[0]]
b := g.resIndex[rr[1]]
req.Len(g.childNodes(a), 1)
req.Equal(b, g.childNodes(a)[0])
req.Empty(g.parentNodes(a))
req.Len(g.parentNodes(b), 1)
req.Equal(a, g.parentNodes(b)[0])
})
t.Run("cyclic node link; a -> b -> a", func(t *testing.T) {
bl := NewBuilder()
rr := []resource.Interface{
&testResource{
resType: "test:resource:1:",
identifiers: resource.Identifiers{"id1"},
refs: resource.RefSet{&resource.Ref{ResourceType: "test:resource:1:", Identifiers: resource.Identifiers{"id2"}}},
},
&testResource{
resType: "test:resource:1:",
identifiers: resource.Identifiers{"id2"},
refs: resource.RefSet{&resource.Ref{ResourceType: "test:resource:1:", Identifiers: resource.Identifiers{"id1"}}},
},
}
g, err := bl.Build(ctx, rr...)
g.invert()
req.NoError(err)
req.Len(g.nodes(), 2)
a := g.nodes()[0]
b := g.nodes()[1]
req.Len(g.childNodes(a), 1)
req.Equal(b, g.childNodes(a)[0])
req.Len(g.parentNodes(a), 1)
req.Equal(b, g.parentNodes(a)[0])
req.Len(b.cc, 1)
req.Equal(a, b.cc[0])
req.Len(b.pp, 1)
req.Equal(a, b.pp[0])
})
t.Run("node with missing dep; a -> nill", func(t *testing.T) {
bl := NewBuilder()
rr := []resource.Interface{
&testResource{
resType: "test:resource:1:",
identifiers: resource.Identifiers{"id1"},
refs: resource.RefSet{&resource.Ref{ResourceType: "test:resource:1:", Identifiers: resource.Identifiers{"id2"}}},
},
}
g, err := bl.Build(ctx, rr...)
g.invert()
req.NoError(err)
req.Len(g.nodes(), 1)
a := g.nodes()[0]
req.Len(g.childNodes(a), 0)
req.Len(g.parentNodes(a), 0)
})
t.Run("self-cycle; a -> a", func(t *testing.T) {
bl := NewBuilder()
rr := []resource.Interface{
&testResource{
resType: "test:resource:1:",
identifiers: resource.Identifiers{"id1"},
refs: resource.RefSet{&resource.Ref{ResourceType: "test:resource:1:", Identifiers: resource.Identifiers{"id1"}}},
},
}
g, err := bl.Build(ctx, rr...)
g.invert()
req.NoError(err)
req.Len(g.nodes(), 1)
a := g.nodes()[0]
req.Len(g.childNodes(a), 1)
req.Equal(g.childNodes(a)[0], a)
req.Len(g.parentNodes(a), 1)
req.Equal(g.parentNodes(a)[0], a)
})
}

View File

@@ -0,0 +1,162 @@
package csv
import (
"context"
"encoding/csv"
"fmt"
"io"
"github.com/cortezaproject/corteza/server/compose/types"
"github.com/cortezaproject/corteza/server/pkg/envoy"
"github.com/cortezaproject/corteza/server/pkg/envoy/resource"
)
type (
bulkComposeRecordEncoder struct {
encoderConfig *EncoderConfig
res *resource.ComposeRecord
refNs string
refMod string
relMod *types.Module
}
)
func bulkComposeRecordEncoderFromResource(rec *resource.ComposeRecord, cfg *EncoderConfig) *bulkComposeRecordEncoder {
return &bulkComposeRecordEncoder{
encoderConfig: cfg,
res: rec,
}
}
// Prepare prepares the composeRecord to be encoded
//
// Any validation, additional constraining should be performed here.
func (n *bulkComposeRecordEncoder) Prepare(ctx context.Context, state *envoy.ResourceState) (err error) {
_, ok := state.Res.(*resource.ComposeRecord)
if !ok {
return encoderErrInvalidResource(types.RecordResourceType, state.Res.ResourceType())
}
return nil
}
// Encode encodes the composeRecord to the document
//
// Encode is allowed to do some data manipulation, but no resource constraints
// should be changed.
func (n *bulkComposeRecordEncoder) Encode(ctx context.Context, w io.Writer, state *envoy.ResourceState) (err error) {
enc := csv.NewWriter(w)
all := len(n.encoderConfig.Fields) == 0
// Generate header & cell index
hh := make([]string, 0, 100)
hh = append(hh, "id")
pickLast := func(v resource.ComposeRecordRawValue) string {
if v.IsMulti {
if len(v.Values) > 0 {
return v.Values[len(v.Values)-1]
}
return ""
}
return v.Value
}
fx := make(map[string]int)
// 1 sys fields
sysFields := []string{"ownedBy", "createdAt", "createdBy", "updatedAt", "updatedBy", "deletedAt", "deletedBy"}
for _, sf := range sysFields {
if all || n.encoderConfig.Fields[sf] {
fx[sf] = len(hh)
hh = append(hh, sf)
}
}
// 2 modlue fields
for _, f := range n.res.RelMod.Fields {
if all || n.encoderConfig.Fields[f.Name] {
fx[f.Name] = len(hh)
hh = append(hh, f.Name)
}
}
enc.Write(hh)
err = n.res.Walker(func(r *resource.ComposeRecordRaw) error {
row := make([]string, len(hh))
var err error
row[0] = r.ID
if r.Us != nil {
if r.Us.OwnedBy != nil && (all || n.encoderConfig.Fields["ownedBy"]) {
row[fx["ownedBy"]], err = n.res.UserFlakes.GetByStamp(r.Us.OwnedBy).Model()
}
if r.Us.CreatedBy != nil && (all || n.encoderConfig.Fields["createdBy"]) {
row[fx["createdBy"]], err = n.res.UserFlakes.GetByStamp(r.Us.CreatedBy).Model()
}
if r.Us.UpdatedBy != nil && (all || n.encoderConfig.Fields["updatedBy"]) {
row[fx["updatedBy"]], err = n.res.UserFlakes.GetByStamp(r.Us.UpdatedBy).Model()
}
if r.Us.DeletedBy != nil && (all || n.encoderConfig.Fields["deletedBy"]) {
row[fx["deletedBy"]], err = n.res.UserFlakes.GetByStamp(r.Us.DeletedBy).Model()
}
}
if r.Ts != nil {
r.Ts, err = r.Ts.Model(n.encoderConfig.TimeLayout, n.encoderConfig.Timezone)
if err != nil {
return err
}
if r.Ts.CreatedAt != nil && (all || n.encoderConfig.Fields["createdAt"]) {
row[fx["createdAt"]] = r.Ts.CreatedAt.S
}
if r.Ts.UpdatedAt != nil && (all || n.encoderConfig.Fields["updatedAt"]) {
row[fx["updatedAt"]] = r.Ts.UpdatedAt.S
}
if r.Ts.DeletedAt != nil && (all || n.encoderConfig.Fields["deletedAt"]) {
row[fx["deletedAt"]] = r.Ts.DeletedAt.S
}
}
if err != nil {
return err
}
for k, v := range r.Values {
// Skip fields we don't want
if !all && !n.encoderConfig.Fields[k] {
continue
}
cell, has := fx[k]
if !has {
return fmt.Errorf("unknown cell %s", k)
}
f := n.res.RelMod.Fields.FindByName(k)
if f == nil {
return fmt.Errorf("field %s not found", k)
}
if f.Kind == "User" {
row[cell], err = n.res.UserFlakes.GetByKey(pickLast(v)).Model()
if err != nil {
return err
}
} else {
row[cell] = pickLast(v)
}
}
return enc.Write(row)
})
if err != nil {
return err
}
enc.Flush()
return nil
}

View File

@@ -0,0 +1,123 @@
package csv
import (
"context"
"encoding/csv"
"io"
"strings"
"github.com/cortezaproject/corteza/server/pkg/envoy"
"github.com/cortezaproject/corteza/server/pkg/envoy/resource"
"github.com/gabriel-vasile/mimetype"
)
type (
// wrapper struct for csv related methods
decoder struct{}
// csv decoder wrapper for additional bits
reader struct {
header []string
count uint64
cache []map[string]string
cacheIndex int
}
)
// Decoder initializes and returns a fresh CSV decoder
func Decoder() *decoder {
return &decoder{}
}
// CanDecodeFile determines if the file can be determined by this decoder
func (y *decoder) CanDecodeFile(f io.Reader) bool {
m, err := mimetype.DetectReader(f)
if err != nil {
return false
}
return y.CanDecodeExt(m.Extension())
}
func (y *decoder) CanDecodeMime(m string) bool {
return m == "text/csv"
}
func (y *decoder) CanDecodeExt(ext string) bool {
pt := strings.Split(ext, ".")
return strings.TrimSpace(pt[len(pt)-1]) == "csv"
}
// Decode decodes the given io.Reader into a generic resource dataset
func (c *decoder) Decode(ctx context.Context, r io.Reader, do *envoy.DecoderOpts) ([]resource.Interface, error) {
cr := &reader{
cacheIndex: 0,
cache: make([]map[string]string, 0, 1000),
}
err := cr.prepare(r)
if err != nil {
return nil, err
}
return []resource.Interface{resource.NewResourceDataset(do.Name, cr)}, nil
}
// The prepare step caches all of the rows into a cache array that will be used
// to access already visited CSV rows.
//
// @todo implement FS cache file for larger files
func (cr *reader) prepare(r io.Reader) (err error) {
cReader := csv.NewReader(r)
// Header
cr.header, err = cReader.Read()
if err != nil {
return err
}
for {
aux := make(map[string]string)
rr, err := cReader.Read()
if err == io.EOF {
return nil
} else if err != nil {
return err
}
// Entry count
cr.count++
// Cache entry
for i, h := range cr.header {
aux[h] = rr[i]
}
cr.cache = append(cr.cache, aux)
}
}
// Fields returns every available field in this dataset
func (cr *reader) Fields() []string {
return cr.header
}
func (cr *reader) Reset() error {
cr.cacheIndex = 0
return nil
}
// Next returns the field: value mapping for the next row
func (cr *reader) Next() (map[string]string, error) {
if cr.cacheIndex >= len(cr.cache) {
return nil, nil
}
mr := cr.cache[cr.cacheIndex]
cr.cacheIndex++
return mr, nil
}
func (cr *reader) Count() uint64 {
return cr.count
}

View File

@@ -0,0 +1,58 @@
package csv
import (
"context"
"fmt"
"os"
"testing"
"github.com/cortezaproject/corteza/server/pkg/envoy"
"github.com/cortezaproject/corteza/server/pkg/envoy/resource"
"github.com/stretchr/testify/require"
)
func parseDocument(ctx context.Context, name string) (*resource.ResourceDataset, error) {
path := fmt.Sprintf("testdata/%s.csv", name)
f, err := os.Open(path)
if err != nil {
return nil, err
}
defer f.Close()
cd := Decoder()
ii, err := cd.Decode(ctx, f, &envoy.DecoderOpts{
Name: name,
Path: path,
})
if err != nil {
return nil, err
}
rd, _ := ii[0].(*resource.ResourceDataset)
return rd, nil
}
func TestDecoder(t *testing.T) {
ctx := context.Background()
t.Run("doc 1", func(t *testing.T) {
req := require.New(t)
ds, err := parseDocument(ctx, "records_1")
req.NoError(err)
req.NotNil(ds)
req.Equal(resource.MakeIdentifiers("records_1"), ds.Identifiers())
req.Equal([]string{"id", "c1", "c2", "c3"}, ds.P.Fields())
req.Equal(uint64(3), ds.P.Count())
for i := 0; i < 3; i++ {
n, err := ds.P.Next()
req.NoError(err)
req.NotNil(n)
req.Len(n, 4)
}
n, err := ds.P.Next()
req.Nil(n)
req.Nil(err)
})
}

View File

@@ -0,0 +1,163 @@
package csv
import (
"bytes"
"context"
"fmt"
"io"
"strings"
"github.com/cortezaproject/corteza/server/pkg/envoy"
"github.com/cortezaproject/corteza/server/pkg/envoy/resource"
)
type (
bulkRecordEncoder struct {
cfg *EncoderConfig
resState map[resource.Interface]*encoderState
}
encoderState struct {
res resourceState
source io.ReadWriter
resourceType string
Scope string
identifier string
}
// EncoderConfig allows us to configure the resource encoding process
EncoderConfig struct {
// Timezone defines what timezone should be used when encoding timestamps
//
// If not defined, UTC is used
Timezone string
// TimeLayout defines how to format the encoded timestamp
//
// If not defined, RFC3339 is used (this one - 2006-01-02T15:04:05Z07:00)
TimeLayout string
// Fields specifies what fields we wish to include in the export
Fields map[string]bool
}
// resourceState holds some intermedia values to help with encoding
resourceState interface {
Prepare(ctx context.Context, state *envoy.ResourceState) (err error)
Encode(ctx context.Context, w io.Writer, state *envoy.ResourceState) (err error)
}
)
var (
ErrUnknownResource = fmt.Errorf("unknown resource")
ErrResourceStateUndefined = fmt.Errorf("undefined resource state")
)
func NewBulkRecordEncoder(cfg *EncoderConfig) envoy.PrepareEncodeStreamer {
if cfg == nil {
cfg = &EncoderConfig{}
}
return &bulkRecordEncoder{
cfg: cfg,
resState: make(map[resource.Interface]*encoderState),
}
}
// Prepare prepares the encoder for the given set of resources
//
// It initializes and prepares the resource state for each provided resource
func (se *bulkRecordEncoder) Prepare(ctx context.Context, ee ...*envoy.ResourceState) (err error) {
f := func(rs resourceState, es *envoy.ResourceState) error {
err = rs.Prepare(ctx, es)
if err != nil {
return err
}
se.resState[es.Res] = &encoderState{
res: rs,
source: &bytes.Buffer{},
resourceType: es.Res.ResourceType(),
identifier: es.Res.Identifiers().First(),
}
return nil
}
for _, e := range ee {
switch res := e.Res.(type) {
// @todo other resources; we'll only do records for now
case *resource.ComposeRecord:
err = f(bulkComposeRecordEncoderFromResource(res, se.cfg), e)
default:
err = ErrUnknownResource
}
if err != nil {
return se.WrapError("prepare", e.Res, err)
}
}
return nil
}
// Encode encodes the resources into a series of csv documents
func (se *bulkRecordEncoder) Encode(ctx context.Context, p envoy.Provider) error {
var e *envoy.ResourceState
var err error
// Encode the resources into document structs
for {
e, err = p.NextInverted(ctx)
if err != nil {
return err
}
if e == nil {
break
}
state := se.resState[e.Res]
if state == nil {
err = ErrResourceStateUndefined
} else {
err = state.res.Encode(ctx, state.source, e)
}
if err != nil {
return se.WrapError("encode: build doc", e.Res, err)
}
}
for _, s := range se.resState {
s.res = nil
}
return nil
}
func (se *bulkRecordEncoder) Stream() []*envoy.Stream {
ss := make([]*envoy.Stream, 0, 20)
for _, s := range se.resState {
ss = append(ss, &envoy.Stream{
Resource: s.resourceType,
Identifier: s.identifier,
Source: s.source,
})
}
return ss
}
// WrapError wraps errors related to csv encoding
//
// Always wrap your errors.
func (se *bulkRecordEncoder) WrapError(act string, res resource.Interface, err error) error {
rt := strings.Join(strings.Split(strings.TrimSpace(strings.TrimRight(res.ResourceType(), ":")), ":"), " ")
return fmt.Errorf("csv encoder %s %s %v: %s", act, rt, res.Identifiers().StringSlice(), err)
}
func encoderErrInvalidResource(exp, got string) error {
return fmt.Errorf("invalid resource type: expecting %s, got %s", exp, got)
}

View File

@@ -0,0 +1,4 @@
id,c1,c2,c3
1,c1.v1,c2.v1,c3.v1
2,c1.v2,c2.v2,c3.v2
3,c1.v3,c2.v3,c3.v3
1 id c1 c2 c3
2 1 c1.v1 c2.v1 c3.v1
3 2 c1.v2 c2.v2 c3.v2
4 3 c1.v3 c2.v3 c3.v3

View File

@@ -0,0 +1,126 @@
package csv
import (
"fmt"
"github.com/cortezaproject/corteza/server/pkg/envoy/resource"
)
type (
seqNode []interface{}
mapNode map[string]interface{}
)
// mapTimestamps helper encodes Timestamps into the mapping node
func mapTimestamps(n mapNode, ts *resource.Timestamps) (mapNode, error) {
if ts == nil {
return n, nil
}
return addMap(n,
"createdAt", ts.CreatedAt,
"updatedAt", ts.UpdatedAt,
"deletedAt", ts.DeletedAt,
"archivedAt", ts.ArchivedAt,
"suspendedAt", ts.SuspendedAt,
)
}
// mapUserstamps helper encodes Userstamps into the mapping node
func mapUserstamps(n mapNode, us *resource.Userstamps) (mapNode, error) {
if us == nil {
return n, nil
}
return addMap(n,
"createdBy", us.CreatedBy,
"updatedBy", us.UpdatedBy,
"deletedBy", us.DeletedBy,
"ownedBy", us.OwnedBy,
)
}
// cleanMap helper removes any empty k:v nodes from the mapping node
//
// The value is empty when the tag is !!null OR when the value and the content are empty
func cleanMap(n mapNode) mapNode {
m := make(mapNode)
for k, v := range n {
if k != "" && v != nil {
m[k] = v
}
}
return m
}
// makeMap creates a new mapping node based on the provided k, v items
//
// pp is a set of k, v items; where k's lie at i, and v's lie at i+1.
// non-string values (required by YAML nodes) are processed further.
// eg.: ["k1", "v1", "k2", "v2"]
func makeMap(pp ...interface{}) (mapNode, error) {
return addMap(make(mapNode), pp...)
}
// addMap adds a new item to the provided mapping node
//
// pp is a set of k, v items; where k's lie at i, and v's lie at i+1.
// non-string values (required by YAML nodes) are processed further.
// eg.: ["k1", "v1", "k2", "v2"]
func addMap(n mapNode, pp ...interface{}) (mapNode, error) {
if len(pp) == 0 {
return n, nil
}
if len(pp)%2 == 1 {
return nil, fmt.Errorf("uneven number of elements provided (%d): %v", len(pp), pp)
}
for i := 0; i < len(pp); i += 2 {
kRaw := pp[i]
vRaw := pp[i+1]
k, ok := kRaw.(string)
if !ok {
return nil, fmt.Errorf("keys must be of type string: %v", kRaw)
}
switch v := vRaw.(type) {
case string:
if v == "" {
continue
}
case bool:
if !v {
continue
}
case int, uint,
int32, uint32,
int64, uint64:
if v == 0 {
continue
}
}
n[k] = vRaw
}
return n, nil
}
// makeSeq creates a new sequence node based on the provided items
func makeSeq(vv ...interface{}) (seqNode, error) {
return addSeq(make(seqNode, 0, len(vv)), vv...)
}
// addSeq adds new items to the sequence node
func addSeq(n seqNode, vv ...interface{}) (seqNode, error) {
for _, vRaw := range vv {
if vRaw != "" && vRaw != nil {
n = append(n, vRaw)
}
}
return n, nil
}

View File

@@ -0,0 +1,9 @@
package envoy
type (
// DecoderOpts provides additional context for source decoders
DecoderOpts struct {
Name string
Path string
}
)

View File

@@ -0,0 +1,91 @@
package directory
import (
"context"
"fmt"
"io"
"os"
"path"
"path/filepath"
"github.com/cortezaproject/corteza/server/pkg/envoy"
"github.com/cortezaproject/corteza/server/pkg/envoy/resource"
)
type (
Decoder interface {
CanDecodeExt(string) bool
CanDecodeFile(io.Reader) bool
Decode(context.Context, io.Reader, *envoy.DecoderOpts) ([]resource.Interface, error)
}
)
// DecodeDirectory is a helper to run the decoding process over the entire directory
func Decode(ctx context.Context, p string, decoders ...Decoder) ([]resource.Interface, error) {
var (
f *os.File
// decoded nodes
dnn []resource.Interface
// agregated resources
nn = make([]resource.Interface, 0, 100)
)
if len(decoders) == 0 {
return nil, fmt.Errorf("no decoders provided")
}
return nn, filepath.Walk(p, func(p string, info os.FileInfo, err error) error {
if err != nil {
return err
}
if info.IsDir() {
return nil
}
if f, err = os.Open(p); err != nil {
return err
}
defer f.Close()
for _, d := range decoders {
if !d.CanDecodeFile(f) {
// decoder cannot handle this file
// Make sure to reset it, as the above check consumes the reader
if _, err = f.Seek(0, 0); err != nil {
return err
}
continue
}
if !d.CanDecodeExt(info.Name()) {
// this decoder cannot handle this extension
continue
}
if _, err = f.Seek(0, 0); err != nil {
return err
}
dir, fn := path.Split(p)
do := &envoy.DecoderOpts{
Name: fn,
Path: dir,
}
if dnn, err = d.Decode(ctx, f, do); err != nil {
return fmt.Errorf("failed to decode %s: %w", info.Name(), err)
}
nn = append(nn, dnn...)
// found compatible decoder
return nil
}
return nil
})
}

View File

@@ -0,0 +1,18 @@
package envoy
import (
"context"
)
type (
Provider interface {
NextInverted(ctx context.Context) (*ResourceState, error)
}
)
// @todo errors!
func Encode(ctx context.Context, p Provider, e Encoder) error {
// @todo add support for multiple encoders at the same time.
// The issue occurs with routines and error handling...
return e.Encode(ctx, p)
}

302
server/pkg/envoy/graph.go Normal file
View File

@@ -0,0 +1,302 @@
package envoy
import (
"context"
"fmt"
"github.com/cortezaproject/corteza/server/pkg/envoy/resource"
)
type (
nodeMap map[*node]bool
graph struct {
nn nodeSet
resIndex map[resource.Interface]*node
missingRefs resource.RefSet
// Config flags
inverted bool
processed nodeMap
conflicting nodeMap
}
ResourceState struct {
Res resource.Interface
Conflicting bool
DepResources []resource.Interface
ParentResources []resource.Interface
}
)
func newGraph() *graph {
return &graph{
nn: make(nodeSet, 0, 100),
resIndex: make(map[resource.Interface]*node),
inverted: false,
processed: make(nodeMap),
conflicting: make(nodeMap),
}
}
func (g *graph) MissingDeps() resource.RefSet {
return g.missingRefs
}
func (g *graph) addNode(nn ...*node) {
for _, n := range nn {
g.resIndex[n.res] = n
}
g.nn = g.nn.add(nn...)
}
func (g *graph) removeNode(nn ...*node) {
g.nn = g.nn.remove(nn...)
for _, n := range nn {
g.removeChild(n)
g.removeParent(n)
}
}
func (g *graph) invert() {
g.inverted = !g.inverted
}
func (g *graph) childNodes(n *node) nodeSet {
if g.inverted {
return n.pp
}
return n.cc
}
func (g *graph) parentNodes(n *node) nodeSet {
if g.inverted {
return n.cc
}
return n.pp
}
func (g *graph) nodes() nodeSet {
return g.nn
}
func (g *graph) markProcessed(n *node) {
g.processed = g.processed.add(n)
}
func (g *graph) markConflicting(n *node) {
g.conflicting = g.conflicting.add(n)
}
func (g *graph) NextInverted(ctx context.Context) (s *ResourceState, err error) {
g.inverted = true
defer func() {
g.inverted = false
}()
return g.Next(ctx)
}
func (g *graph) Next(ctx context.Context) (s *ResourceState, err error) {
upNN := g.removeProcessed(g.nodes())
// We are done here
if len(upNN) <= 0 {
return nil, nil
}
var nx *node
for _, upN := range upNN {
// We should not take into account conflicted parent nodes,
// as they already resolved the conflict.
if len(g.removeProcessed(g.removeConflicting(g.parentNodes(upN)))) <= 0 {
nx = upN
break
}
}
if nx != nil {
// Prepare the required context for the processing.
// Perform some basic cleanup.
es := g.prepExecState(nx, false)
g.markProcessed(nx)
return es, nil
}
// There are only conflicting nodes
// Try to get a cycle node to resolve the conflict
nx = g.findCycleNode(g.removeConflicting(upNN))
if nx == nil {
// This is basically impossible, unless I've messed up the algorithm
return nil, fmt.Errorf("could not determine non-conflicting node")
}
// Prepare the required context for the processing.
es := g.prepExecState(nx, true)
g.markConflicting(nx)
return es, nil
}
// findCycleNode returns the first graph node that caused a cycle
//
// General outline:
// - DFS from a start node(s)
// - if a child node is already in path, return that node
// - else return nil and cleanup the path until the first node with
// unprocessed child nodes
//
// @note we could complicate this further by doing cycle enumeration algorithms.
//
// I might do it when no one is watching :)
func (g *graph) findCycleNode(nn nodeSet) *node {
path := make(nodeMap)
processed := make(nodeMap)
for _, n := range nn {
if !processed.has(n) {
m := g.traverse(n, path, processed)
if m != nil {
return m
}
}
}
return nil
}
func (g *graph) traverse(n *node, path, processed nodeMap) *node {
processed.add(n)
// Found ourselves a cycle
if path.has(n) {
return n
}
cnn := g.removeProcessed(g.removeConflicting(g.childNodes(n)))
// Nothing else to look at
if len(cnn) == 0 {
return nil
}
path.add(n)
for _, c := range cnn {
m := g.traverse(c, path, processed)
if m != nil {
return m
}
}
path.remove(n)
return nil
}
// util
func (g *graph) removeProcessed(nn nodeSet) nodeSet {
mm := make(nodeSet, 0, len(nn))
for _, n := range nn {
if !g.processed.has(n) {
mm = mm.add(n)
}
}
return mm
}
func (g *graph) removeConflicting(nn nodeSet) nodeSet {
mm := make(nodeSet, 0, len(nn))
for _, n := range nn {
if !g.conflicting.has(n) {
mm = mm.add(n)
}
}
return mm
}
func (g *graph) addChild(n *node, mm ...*node) {
if g.inverted {
n.pp = n.pp.add(mm...)
} else {
n.cc = n.cc.add(mm...)
}
}
func (g *graph) removeChild(n *node, mm ...*node) {
if len(mm) <= 0 {
if g.inverted {
n.pp = make(nodeSet, 0, 10)
} else {
n.cc = make(nodeSet, 0, 10)
}
} else {
if g.inverted {
n.pp = n.pp.remove(mm...)
} else {
n.cc = n.cc.remove(mm...)
}
}
}
func (g *graph) addParent(n *node, mm ...*node) {
if g.inverted {
n.cc = n.cc.add(mm...)
} else {
n.pp = n.pp.add(mm...)
}
}
func (g *graph) removeParent(n *node, mm ...*node) {
if len(mm) <= 0 {
if g.inverted {
n.cc = make(nodeSet, 0, 10)
} else {
n.pp = make(nodeSet, 0, 10)
}
} else {
if g.inverted {
n.cc = n.cc.remove(mm...)
} else {
n.pp = n.pp.remove(mm...)
}
}
}
func (g *graph) nodeResource(nn ...*node) []resource.Interface {
rr := make([]resource.Interface, 0, len(nn))
for _, n := range nn {
rr = append(rr, n.res)
}
return rr
}
func (g *graph) prepExecState(n *node, conflicting bool) *ResourceState {
return &ResourceState{
Res: n.res,
DepResources: g.nodeResource(g.childNodes(n)...),
ParentResources: g.nodeResource(g.parentNodes(n)...),
Conflicting: conflicting,
}
}
func (nm nodeMap) add(n *node) nodeMap {
nm[n] = true
return nm
}
func (nm nodeMap) has(n *node) bool {
return nm[n]
}
func (nm nodeMap) remove(n *node) nodeMap {
delete(nm, n)
return nm
}

View File

@@ -0,0 +1,429 @@
package envoy
import (
"context"
"testing"
"github.com/cortezaproject/corteza/server/pkg/envoy/resource"
"github.com/stretchr/testify/require"
)
func TestGraph_Walk(t *testing.T) {
req := require.New(t)
ctx := context.Background()
t.Run("simple node link; a -> b", func(t *testing.T) {
bl := NewBuilder()
a := &testResource{
resType: "test:resource:1:",
identifiers: resource.Identifiers{"id1"},
refs: resource.RefSet{&resource.Ref{ResourceType: "test:resource:1:", Identifiers: resource.Identifiers{"id2"}}},
}
b := &testResource{
resType: "test:resource:1:",
identifiers: resource.Identifiers{"id2"},
refs: nil,
}
rr := []resource.Interface{
a,
b,
}
g, err := bl.Build(ctx, rr...)
req.NoError(err)
g.invert()
s, err := g.Next(ctx)
req.Equal(b, s.Res)
req.False(s.Conflicting)
s, err = g.Next(ctx)
req.Equal(a, s.Res)
req.False(s.Conflicting)
})
t.Run("cyclic node link; a -> b -> a", func(t *testing.T) {
bl := NewBuilder()
a := &testResource{
resType: "test:resource:1:",
identifiers: resource.Identifiers{"id1"},
refs: resource.RefSet{&resource.Ref{ResourceType: "test:resource:1:", Identifiers: resource.Identifiers{"id2"}}},
}
b := &testResource{
resType: "test:resource:1:",
identifiers: resource.Identifiers{"id2"},
refs: resource.RefSet{&resource.Ref{ResourceType: "test:resource:1:", Identifiers: resource.Identifiers{"id1"}}},
}
rr := []resource.Interface{
a,
b,
}
g, err := bl.Build(ctx, rr...)
req.NoError(err)
g.invert()
s, err := g.Next(ctx)
req.Equal(a, s.Res)
req.True(s.Conflicting)
s, err = g.Next(ctx)
req.Equal(b, s.Res)
req.False(s.Conflicting)
s, err = g.Next(ctx)
req.Equal(a, s.Res)
req.False(s.Conflicting)
})
t.Run("self-cycle; a -> a", func(t *testing.T) {
bl := NewBuilder()
a := &testResource{
resType: "test:resource:1:",
identifiers: resource.Identifiers{"id1"},
refs: resource.RefSet{&resource.Ref{ResourceType: "test:resource:1:", Identifiers: resource.Identifiers{"id1"}}},
}
rr := []resource.Interface{
a,
}
g, err := bl.Build(ctx, rr...)
req.NoError(err)
g.invert()
s, err := g.Next(ctx)
req.Equal(a, s.Res)
req.True(s.Conflicting)
s, err = g.Next(ctx)
req.Equal(a, s.Res)
req.False(s.Conflicting)
})
}
func TestGraph_WalkFlags(t *testing.T) {
req := require.New(t)
ctx := context.Background()
t.Run("simple node link; a -> b -> c -> c", func(t *testing.T) {
bl := NewBuilder()
a := &testResource{
resType: "test:resource:1:",
identifiers: resource.Identifiers{"id1"},
refs: resource.RefSet{&resource.Ref{ResourceType: "test:resource:1:", Identifiers: resource.Identifiers{"id2"}}},
}
b := &testResource{
resType: "test:resource:1:",
identifiers: resource.Identifiers{"id2"},
refs: resource.RefSet{&resource.Ref{ResourceType: "test:resource:1:", Identifiers: resource.Identifiers{"id3"}}},
}
c := &testResource{
resType: "test:resource:1:",
identifiers: resource.Identifiers{"id3"},
refs: resource.RefSet{&resource.Ref{ResourceType: "test:resource:1:", Identifiers: resource.Identifiers{"id3"}}},
}
rr := []resource.Interface{
a,
b,
c,
}
g, err := bl.Build(ctx, rr...)
req.NoError(err)
s, err := g.NextInverted(ctx)
req.NoError(err)
req.Equal(c, s.Res)
req.True(s.Conflicting)
s, err = g.NextInverted(ctx)
req.NoError(err)
req.Equal(b, s.Res)
req.False(s.Conflicting)
s, err = g.NextInverted(ctx)
req.NoError(err)
req.Equal(a, s.Res)
req.False(s.Conflicting)
s, err = g.NextInverted(ctx)
req.NoError(err)
req.Equal(c, s.Res)
req.False(s.Conflicting)
s, err = g.NextInverted(ctx)
req.NoError(err)
req.Nil(s)
})
t.Run("simple node link; a -> b -> c -> d -> b; a -> b -> e", func(t *testing.T) {
bl := NewBuilder()
a := &testResource{
resType: "test:resource:1:",
identifiers: resource.Identifiers{"id1"},
refs: resource.RefSet{&resource.Ref{ResourceType: "test:resource:1:", Identifiers: resource.Identifiers{"id2"}}},
}
b := &testResource{
resType: "test:resource:1:",
identifiers: resource.Identifiers{"id2"},
refs: resource.RefSet{&resource.Ref{ResourceType: "test:resource:1:", Identifiers: resource.Identifiers{"id3"}}, &resource.Ref{ResourceType: "test:resource:1:", Identifiers: resource.Identifiers{"id5"}}},
}
c := &testResource{
resType: "test:resource:1:",
identifiers: resource.Identifiers{"id3"},
refs: resource.RefSet{&resource.Ref{ResourceType: "test:resource:1:", Identifiers: resource.Identifiers{"id4"}}},
}
d := &testResource{
resType: "test:resource:1:",
identifiers: resource.Identifiers{"id4"},
refs: resource.RefSet{&resource.Ref{ResourceType: "test:resource:1:", Identifiers: resource.Identifiers{"id2"}}},
}
e := &testResource{
resType: "test:resource:1:",
identifiers: resource.Identifiers{"id5"},
refs: nil,
}
rr := []resource.Interface{
a,
b,
c,
d,
e,
}
g, err := bl.Build(ctx, rr...)
req.NoError(err)
s, err := g.NextInverted(ctx)
req.NoError(err)
req.Equal(e, s.Res)
req.False(s.Conflicting)
s, err = g.NextInverted(ctx)
req.NoError(err)
req.Equal(b, s.Res)
req.True(s.Conflicting)
s, err = g.NextInverted(ctx)
req.NoError(err)
req.Equal(a, s.Res)
req.False(s.Conflicting)
s, err = g.NextInverted(ctx)
req.NoError(err)
req.Equal(d, s.Res)
req.False(s.Conflicting)
s, err = g.NextInverted(ctx)
req.NoError(err)
req.Equal(c, s.Res)
req.False(s.Conflicting)
s, err = g.NextInverted(ctx)
req.NoError(err)
req.Equal(b, s.Res)
req.False(s.Conflicting)
s, err = g.NextInverted(ctx)
req.NoError(err)
req.Nil(s)
})
t.Run("simple node link; a -> b -> c -> d -> b; a -> b -> c -> e -> b", func(t *testing.T) {
bl := NewBuilder()
a := &testResource{
resType: "test:resource:1:",
identifiers: resource.Identifiers{"id1"},
refs: resource.RefSet{&resource.Ref{ResourceType: "test:resource:1:", Identifiers: resource.Identifiers{"id2"}}},
}
b := &testResource{
resType: "test:resource:1:",
identifiers: resource.Identifiers{"id2"},
refs: resource.RefSet{&resource.Ref{ResourceType: "test:resource:1:", Identifiers: resource.Identifiers{"id3"}}},
}
c := &testResource{
resType: "test:resource:1:",
identifiers: resource.Identifiers{"id3"},
refs: resource.RefSet{&resource.Ref{ResourceType: "test:resource:1:", Identifiers: resource.Identifiers{"id4"}}, &resource.Ref{ResourceType: "test:resource:1:", Identifiers: resource.Identifiers{"id5"}}},
}
d := &testResource{
resType: "test:resource:1:",
identifiers: resource.Identifiers{"id4"},
refs: resource.RefSet{&resource.Ref{ResourceType: "test:resource:1:", Identifiers: resource.Identifiers{"id2"}}},
}
e := &testResource{
resType: "test:resource:1:",
identifiers: resource.Identifiers{"id5"},
refs: resource.RefSet{&resource.Ref{ResourceType: "test:resource:1:", Identifiers: resource.Identifiers{"id2"}}},
}
rr := []resource.Interface{
a,
b,
c,
d,
e,
}
g, err := bl.Build(ctx, rr...)
req.NoError(err)
s, err := g.NextInverted(ctx)
req.NoError(err)
req.Equal(b, s.Res)
req.True(s.Conflicting)
s, err = g.NextInverted(ctx)
req.NoError(err)
req.Equal(a, s.Res)
req.False(s.Conflicting)
s, err = g.NextInverted(ctx)
req.NoError(err)
req.Equal(d, s.Res)
req.False(s.Conflicting)
s, err = g.NextInverted(ctx)
req.NoError(err)
req.Equal(e, s.Res)
req.False(s.Conflicting)
s, err = g.NextInverted(ctx)
req.NoError(err)
req.Equal(c, s.Res)
req.False(s.Conflicting)
s, err = g.NextInverted(ctx)
req.NoError(err)
req.Equal(b, s.Res)
req.False(s.Conflicting)
s, err = g.NextInverted(ctx)
req.NoError(err)
req.Nil(s)
})
}
func TestGraph_Utilities(t *testing.T) {
req := require.New(t)
t.Run("adding & removing nodes", func(t *testing.T) {
g := newGraph()
n1 := newNode(nil)
n2 := newNode(nil)
// Adding nodes
g.addNode(n1, n2)
req.Len(g.nn, 2)
req.Equal(n1, g.nn[0])
req.Equal(n2, g.nn[1])
// Dependencies
g.addChild(n1, n2)
g.addParent(n2, n1)
req.True(g.childNodes(n1).has(n2))
req.True(g.parentNodes(n2).has(n1))
// Removing nodes
g.removeNode(n1, n2)
req.False(g.childNodes(n1).has(n2))
req.False(g.parentNodes(n2).has(n1))
})
t.Run("inverted graph", func(t *testing.T) {
g := newGraph()
n1 := newNode(nil)
n2 := newNode(nil)
// Adding nodes
g.addNode(n1, n2)
req.Len(g.nn, 2)
req.Equal(n1, g.nn[0])
req.Equal(n2, g.nn[1])
// Dependencies
g.addChild(n1, n2)
g.addParent(n2, n1)
g.invert()
req.True(g.parentNodes(n1).has(n2))
req.True(g.childNodes(n2).has(n1))
})
t.Run("inverted child dependencies", func(t *testing.T) {
g := newGraph()
n1 := newNode(nil)
n2 := newNode(nil)
n3 := newNode(nil)
g.addNode(n1, n2, n3)
g.addChild(n1, n2)
g.invert()
g.addChild(n1, n3)
req.Equal(n2, n1.cc[0])
req.Equal(n3, n1.pp[0])
g.removeChild(n1, n3)
req.Len(n1.pp, 0)
})
t.Run("inverted parent dependencies", func(t *testing.T) {
g := newGraph()
n1 := newNode(nil)
n2 := newNode(nil)
n3 := newNode(nil)
g.addNode(n1, n2, n3)
g.addParent(n1, n2)
g.invert()
g.addParent(n1, n3)
req.Equal(n2, n1.pp[0])
req.Equal(n3, n1.cc[0])
g.removeParent(n1, n3)
req.Len(n1.cc, 0)
})
// This one is just for codecoverage; the logic is nothing
t.Run("remove all child/parent nodes", func(t *testing.T) {
g := newGraph()
n1 := newNode(nil)
n2 := newNode(nil)
g.addNode(n1, n2)
g.addChild(n1, n2)
g.addParent(n1, n2)
g.removeChild(n1)
g.removeParent(n1)
req.Len(n1.cc, 0)
req.Len(n1.pp, 0)
// Inverted graph
g.addChild(n1, n2)
g.addParent(n1, n2)
g.invert()
g.removeChild(n1)
g.removeParent(n1)
req.Len(n1.cc, 0)
req.Len(n1.pp, 0)
})
}

View File

@@ -0,0 +1,135 @@
package json
import (
"context"
"encoding/json"
"fmt"
"io"
"github.com/cortezaproject/corteza/server/compose/types"
"github.com/cortezaproject/corteza/server/pkg/envoy"
"github.com/cortezaproject/corteza/server/pkg/envoy/resource"
)
type (
bulkComposeRecordEncoder struct {
encoderConfig *EncoderConfig
res *resource.ComposeRecord
refNs string
refMod string
relMod *types.Module
}
)
func bulkComposeRecordEncoderFromResource(rec *resource.ComposeRecord, cfg *EncoderConfig) *bulkComposeRecordEncoder {
return &bulkComposeRecordEncoder{
encoderConfig: cfg,
res: rec,
}
}
func (n *bulkComposeRecordEncoder) Prepare(ctx context.Context, state *envoy.ResourceState) (err error) {
_, ok := state.Res.(*resource.ComposeRecord)
if !ok {
return encoderErrInvalidResource(types.RecordResourceType, state.Res.ResourceType())
}
return nil
}
func (n *bulkComposeRecordEncoder) Encode(ctx context.Context, w io.Writer, state *envoy.ResourceState) (err error) {
enc := json.NewEncoder(w)
all := len(n.encoderConfig.Fields) == 0
getMapValue := func(v resource.ComposeRecordRawValue, proc func(k any) *resource.Userstamp) any {
if v.IsMulti {
if proc == nil {
return v.Values
}
out := make([]interface{}, 0, len(v.Values))
for _, vv := range v.Values {
out = append(out, proc(vv))
}
return out
}
if proc == nil {
return v.Value
}
return proc(v.Value)
}
err = n.res.Walker(func(r *resource.ComposeRecordRaw) error {
m, err := makeMap(
"id", r.ID,
)
r.Ts, err = r.Ts.Model(n.encoderConfig.TimeLayout, n.encoderConfig.Timezone)
if err != nil {
return err
}
// Encode system values
sysValues := make(map[string]interface{})
sysValues["createdAt"] = r.Ts.CreatedAt
sysValues["updatedAt"] = r.Ts.UpdatedAt
sysValues["deletedAt"] = r.Ts.DeletedAt
sysValues["createdBy"] = n.res.UserFlakes.GetByStamp(r.Us.CreatedBy)
sysValues["updatedBy"] = n.res.UserFlakes.GetByStamp(r.Us.UpdatedBy)
sysValues["deletedBy"] = n.res.UserFlakes.GetByStamp(r.Us.DeletedBy)
sysValues["ownedBy"] = n.res.UserFlakes.GetByStamp(r.Us.OwnedBy)
for k, v := range sysValues {
// Skip fields we don't want
if !all && !n.encoderConfig.Fields[k] {
continue
}
m, err = addMap(m,
k, v,
)
if err != nil {
return err
}
}
// Encode record values
for k, v := range r.Values {
// Skip fields we don't want
if !all && !n.encoderConfig.Fields[k] {
continue
}
f := n.res.RelMod.Fields.FindByName(k)
if f == nil {
return fmt.Errorf("field %s not found", k)
}
if f.Kind == "User" {
m, err = addMap(m,
k, getMapValue(v, n.res.UserFlakes.GetByKey),
)
} else {
m, err = addMap(m,
k, getMapValue(v, nil),
)
}
if err != nil {
return err
}
}
return enc.Encode(m)
})
if err != nil {
return err
}
return nil
}

View File

@@ -0,0 +1,139 @@
package json
import (
"bytes"
"context"
"encoding/json"
"io"
"strings"
"github.com/cortezaproject/corteza/server/pkg/envoy"
"github.com/cortezaproject/corteza/server/pkg/envoy/resource"
"github.com/gabriel-vasile/mimetype"
)
type (
// wrapper struct for json related methods
decoder struct{}
// json decoder wrapper for additional bits
reader struct {
header []string
count uint64
cache []map[string]string
cacheIndex int
}
)
// Decoder initializes and returns a fresh JSON decoder
//
// We'll only do jsonl for now -- for record importing.
// We'll expand this to work with other resources later on.
func Decoder() *decoder {
return &decoder{}
}
// CanDecodeFile determines if the file can be decoded by this decoder
func (d *decoder) CanDecodeFile(f io.Reader) bool {
var buff bytes.Buffer
tr := io.TeeReader(f, &buff)
m, err := mimetype.DetectReader(tr)
if err != nil {
return false
}
ext := m.Extension()
return d.CanDecodeExt(ext)
}
func (d *decoder) CanDecodeMime(m string) bool {
return m == "application/json" || m == "application/jsonlines"
}
func (d *decoder) CanDecodeExt(ext string) bool {
pt := strings.Split(ext, ".")
ext = strings.TrimSpace(pt[len(pt)-1])
return ext == "jsonl" || ext == "json" || ext == "ndjson"
}
// Decode decodes the given io.Reader into a generic resource dataset
func (d *decoder) Decode(ctx context.Context, r io.Reader, do *envoy.DecoderOpts) ([]resource.Interface, error) {
jr := &reader{
cacheIndex: 0,
cache: make([]map[string]string, 0, 1000),
}
err := jr.prepare(r)
if err != nil {
return nil, err
}
return []resource.Interface{resource.NewResourceDataset(do.Name, jr)}, nil
}
// The prepare step caches all of the rows into a cache array that will be used
// to access already visited json rows.
//
// @todo implement FS cache file for larger files
func (jr *reader) prepare(r io.Reader) (err error) {
jReader := json.NewDecoder(r)
// JSON can omit empty values, so we can't be 100% that all of the headers
// were read in the first go.
// We'll determine all of the headers below where we count the entries
hx := make(map[string]bool)
jr.header = make([]string, 0, 100)
for jReader.More() {
aux := make(map[string]string)
err = jReader.Decode(&aux)
if err == io.EOF {
break
} else if err != nil {
return
}
// Get all the header fields
for h := range aux {
if !hx[h] {
jr.header = append(jr.header, h)
hx[h] = true
}
}
// Entry count
jr.count++
// Cache entry
jr.cache = append(jr.cache, aux)
}
return nil
}
func (jr *reader) Reset() error {
jr.cacheIndex = 0
return nil
}
// Fields returns every available field in this dataset
func (jr *reader) Fields() []string {
return jr.header
}
// Next returns the field: value mapping for the next row
func (jr *reader) Next() (map[string]string, error) {
if jr.cacheIndex >= len(jr.cache) {
return nil, nil
}
mr := jr.cache[jr.cacheIndex]
jr.cacheIndex++
return mr, nil
}
func (cr *reader) Count() uint64 {
return cr.count
}

View File

@@ -0,0 +1,58 @@
package json
import (
"context"
"fmt"
"os"
"testing"
"github.com/cortezaproject/corteza/server/pkg/envoy"
"github.com/cortezaproject/corteza/server/pkg/envoy/resource"
"github.com/stretchr/testify/require"
)
func parseDocument(ctx context.Context, name string) (*resource.ResourceDataset, error) {
path := fmt.Sprintf("testdata/%s.jsonl", name)
f, err := os.Open(path)
if err != nil {
return nil, err
}
defer f.Close()
cd := Decoder()
ii, err := cd.Decode(ctx, f, &envoy.DecoderOpts{
Name: name,
Path: path,
})
if err != nil {
return nil, err
}
rd, _ := ii[0].(*resource.ResourceDataset)
return rd, nil
}
func TestDecoder(t *testing.T) {
ctx := context.Background()
t.Run("doc 1", func(t *testing.T) {
req := require.New(t)
ds, err := parseDocument(ctx, "records_1")
req.NoError(err)
req.NotNil(ds)
req.Equal(resource.MakeIdentifiers("records_1"), ds.Identifiers())
req.Subset([]string{"id", "c1", "c2", "c3"}, ds.P.Fields())
req.Equal(uint64(3), ds.P.Count())
for i := 0; i < 3; i++ {
n, err := ds.P.Next()
req.NoError(err)
req.NotNil(n)
req.Len(n, 4)
}
n, err := ds.P.Next()
req.Nil(n)
req.Nil(err)
})
}

View File

@@ -0,0 +1,163 @@
package json
import (
"bytes"
"context"
"fmt"
"io"
"strings"
"github.com/cortezaproject/corteza/server/pkg/envoy"
"github.com/cortezaproject/corteza/server/pkg/envoy/resource"
)
type (
bulkRecordEncoder struct {
cfg *EncoderConfig
resState map[resource.Interface]*encoderState
}
encoderState struct {
res resourceState
source io.ReadWriter
resourceType string
Scope string
identifier string
}
// EncoderConfig allows us to configure the resource encoding process
EncoderConfig struct {
// Timezone defines what timezone should be used when encoding timestamps
//
// If not defined, UTC is used
Timezone string
// TimeLayout defines how to format the encoded timestamp
//
// If not defined, RFC3339 is used (this one - 2006-01-02T15:04:05Z07:00)
TimeLayout string
// Fields specifies what fields we wish to include in the export
Fields map[string]bool
}
// resourceState holds some intermedia values to help with encoding
resourceState interface {
Prepare(ctx context.Context, state *envoy.ResourceState) (err error)
Encode(ctx context.Context, w io.Writer, state *envoy.ResourceState) (err error)
}
)
var (
ErrUnknownResource = fmt.Errorf("unknown resource")
ErrResourceStateUndefined = fmt.Errorf("undefined resource state")
)
func NewBulkRecordEncoder(cfg *EncoderConfig) envoy.PrepareEncodeStreamer {
if cfg == nil {
cfg = &EncoderConfig{}
}
return &bulkRecordEncoder{
cfg: cfg,
resState: make(map[resource.Interface]*encoderState),
}
}
// Prepare prepares the encoder for the given set of resources
//
// It initializes and prepares the resource state for each provided resource
func (se *bulkRecordEncoder) Prepare(ctx context.Context, ee ...*envoy.ResourceState) (err error) {
f := func(rs resourceState, es *envoy.ResourceState) error {
err = rs.Prepare(ctx, es)
if err != nil {
return err
}
se.resState[es.Res] = &encoderState{
res: rs,
source: &bytes.Buffer{},
resourceType: es.Res.ResourceType(),
identifier: es.Res.Identifiers().First(),
}
return nil
}
for _, e := range ee {
switch res := e.Res.(type) {
// @todo other resources; we'll only do records for now
case *resource.ComposeRecord:
err = f(bulkComposeRecordEncoderFromResource(res, se.cfg), e)
default:
err = ErrUnknownResource
}
if err != nil {
return se.WrapError("prepare", e.Res, err)
}
}
return nil
}
// Encode encodes the resources into a series of jsonl documents
func (se *bulkRecordEncoder) Encode(ctx context.Context, p envoy.Provider) error {
var e *envoy.ResourceState
var err error
// Encode the resources into document structs
for {
e, err = p.NextInverted(ctx)
if err != nil {
return err
}
if e == nil {
break
}
state := se.resState[e.Res]
if state == nil {
err = ErrResourceStateUndefined
} else {
err = state.res.Encode(ctx, state.source, e)
}
if err != nil {
return se.WrapError("encode: build doc", e.Res, err)
}
}
for _, s := range se.resState {
s.res = nil
}
return nil
}
func (se *bulkRecordEncoder) Stream() []*envoy.Stream {
ss := make([]*envoy.Stream, 0, 20)
for _, s := range se.resState {
ss = append(ss, &envoy.Stream{
Resource: s.resourceType,
Identifier: s.identifier,
Source: s.source,
})
}
return ss
}
// WrapError wraps errors related to json encoding
//
// Always wrap your errors.
func (se *bulkRecordEncoder) WrapError(act string, res resource.Interface, err error) error {
rt := strings.Join(strings.Split(strings.TrimSpace(strings.TrimRight(res.ResourceType(), ":")), ":"), " ")
return fmt.Errorf("json encoder %s %s %v: %s", act, rt, res.Identifiers().StringSlice(), err)
}
func encoderErrInvalidResource(exp, got string) error {
return fmt.Errorf("invalid resource type: expecting %s, got %s", exp, got)
}

View File

@@ -0,0 +1,3 @@
{"id": "1", "c1": "c1.v1", "c2": "c2.v1", "c3": "c3.v1"}
{"id": "2", "c1": "c1.v2", "c2": "c2.v2", "c3": "c3.v2"}
{"id": "3", "c1": "c1.v3", "c2": "c2.v3", "c3": "c3.v3"}

View File

@@ -0,0 +1,143 @@
package json
import (
"fmt"
"reflect"
"github.com/cortezaproject/corteza/server/pkg/envoy/resource"
)
type (
seqNode []interface{}
mapNode map[string]interface{}
)
// mapTimestamps helper encodes Timestamps into the mapping node
func mapTimestamps(n mapNode, ts *resource.Timestamps) (mapNode, error) {
if ts == nil {
return n, nil
}
return addMap(n,
"createdAt", ts.CreatedAt,
"updatedAt", ts.UpdatedAt,
"deletedAt", ts.DeletedAt,
"archivedAt", ts.ArchivedAt,
"suspendedAt", ts.SuspendedAt,
)
}
// mapUserstamps helper encodes Userstamps into the mapping node
func mapUserstamps(n mapNode, us *resource.Userstamps) (mapNode, error) {
if us == nil {
return n, nil
}
return addMap(n,
"createdBy", us.CreatedBy,
"updatedBy", us.UpdatedBy,
"deletedBy", us.DeletedBy,
"ownedBy", us.OwnedBy,
"runAs", us.RunAs,
)
}
// cleanMap helper removes any empty k:v nodes from the mapping node
//
// The value is empty when the tag is !!null OR when the value and the content are empty
func cleanMap(n mapNode) mapNode {
m := make(mapNode)
for k, v := range n {
if k != "" && v != nil {
m[k] = v
}
}
return m
}
// makeMap creates a new mapping node based on the provided k, v items
//
// pp is a set of k, v items; where k's lie at i, and v's lie at i+1.
// non-string values (required by YAML nodes) are processed further.
// eg.: ["k1", "v1", "k2", "v2"]
func makeMap(pp ...interface{}) (mapNode, error) {
return addMap(make(mapNode), pp...)
}
func isNil(i interface{}) bool {
if i == nil {
return true
}
switch reflect.TypeOf(i).Kind() {
case reflect.Ptr, reflect.Map, reflect.Array, reflect.Chan, reflect.Slice:
return reflect.ValueOf(i).IsNil()
}
return false
}
// addMap adds a new item to the provided mapping node
//
// pp is a set of k, v items; where k's lie at i, and v's lie at i+1.
// non-string values (required by YAML nodes) are processed further.
// eg.: ["k1", "v1", "k2", "v2"]
func addMap(n mapNode, pp ...interface{}) (mapNode, error) {
if len(pp) == 0 {
return n, nil
}
if len(pp)%2 == 1 {
return nil, fmt.Errorf("uneven number of elements provided (%d): %v", len(pp), pp)
}
for i := 0; i < len(pp); i += 2 {
kRaw := pp[i]
vRaw := pp[i+1]
if isNil(vRaw) {
continue
}
k, ok := kRaw.(string)
if !ok {
return nil, fmt.Errorf("keys must be of type string: %v", kRaw)
}
switch v := vRaw.(type) {
case string:
if v == "" {
continue
}
case bool:
if !v {
continue
}
case int, uint,
int32, uint32,
int64, uint64:
if v == 0 {
continue
}
}
n[k] = vRaw
}
return n, nil
}
// makeSeq creates a new sequence node based on the provided items
func makeSeq(vv ...interface{}) (seqNode, error) {
return addSeq(make(seqNode, 0, len(vv)), vv...)
}
// addSeq adds new items to the sequence node
func addSeq(n seqNode, vv ...interface{}) (seqNode, error) {
for _, vRaw := range vv {
if vRaw != "" && vRaw != nil {
n = append(n, vRaw)
}
}
return n, nil
}

View File

@@ -0,0 +1,99 @@
package envoy
import (
"github.com/cortezaproject/corteza/server/pkg/envoy/resource"
)
// NormalizeResourceTranslations takes the provided resource.ResourceTranslation
// and merges duplicates based on the Priority parameter
func NormalizeResourceTranslations(rr ...resource.Interface) []resource.Interface {
out := make([]resource.Interface, 0, len(rr))
locales := make([]*resource.ResourceTranslation, 0, len(rr))
// - collect all locale resources
for _, r := range rr {
if l, ok := r.(*resource.ResourceTranslation); ok {
locales = append(locales, l)
} else {
out = append(out, r)
}
}
// make an index
var byResource map[string][2]*resource.ResourceTranslation
byResource = make(map[string][2]*resource.ResourceTranslation)
for _, locale := range locales {
pp := byResource[locale.RefResource]
pp[locale.Priority] = locale
byResource[locale.RefResource] = pp
}
// squash index by priority ascending
for _, pp := range byResource {
var aux *resource.ResourceTranslation
seen := make(map[string]bool)
for _, p := range pp {
if p == nil {
continue
}
if aux == nil {
aux = p
} else {
for _, r := range p.Res {
if seen[r.Lang.String()+r.K] {
continue
}
aux.Res = append(aux.Res, r)
}
}
for _, r := range p.Res {
if r.K != "" {
seen[r.Lang.String()+r.K] = true
}
}
}
if aux != nil {
out = append(out, aux)
}
}
return out
}
func appendRefSet(a resource.RefSet, b *resource.Ref) resource.RefSet {
return append(a, b)
}
// FilterRequiredResourceTranslations returns only resource translations relevant for the given resources
func FilterRequiredResourceTranslations(request resource.InterfaceSet, translations []*resource.ResourceTranslation) (out []*resource.ResourceTranslation) {
out = make([]*resource.ResourceTranslation, 0, 100)
// Filter
procResSet(request, func(r resource.Interface) {
localeRes, ok := r.(resource.LocaleInterface)
if !ok {
return
}
_, ref, pp := localeRes.ResourceTranslationParts()
resourceRefSet := appendRefSet(pp, ref)
for _, t := range translations {
translationRefSet := appendRefSet(t.RefPath, t.RefRes)
// Res. tr. use strict equality to determine where it falls into
if !translationRefSet.Equals(resourceRefSet) {
continue
}
out = append(out, t)
}
})
return
}

View File

@@ -0,0 +1,46 @@
package envoy
import (
"fmt"
"reflect"
"github.com/cortezaproject/corteza/server/pkg/envoy/resource"
)
type (
Marshaller interface {
MarshalEnvoy() ([]resource.Interface, error)
}
)
// MarshalMerge takes one or more nodes and Marshals and merges all
func CollectNodes(ii ...interface{}) (nn []resource.Interface, err error) {
for _, i := range ii {
// i == nil is not enough in go
if i == nil || (reflect.ValueOf(i).Kind() == reflect.Ptr && reflect.ValueOf(i).IsNil()) {
continue
}
switch c := i.(type) {
// case NodeSet:
// nn = append(nn, c...)
case resource.Interface:
nn = append(nn, c)
case []resource.Interface:
nn = append(nn, c...)
case Marshaller:
if tmp, err := c.MarshalEnvoy(); err != nil {
return nil, err
} else {
nn = append(nn, tmp...)
}
default:
// @todo resoruceset?
return nil, fmt.Errorf("failed to merge %T; expecting Resource, ResourceSet? or Marshaller interface", i)
}
}
return nn, nil
}

129
server/pkg/envoy/node.go Normal file
View File

@@ -0,0 +1,129 @@
package envoy
import "github.com/cortezaproject/corteza/server/pkg/envoy/resource"
type (
// the node struct is used for nicer graph state management
nodeSet []*node
node struct {
res resource.Interface
missing resource.RefSet
pp nodeSet
cc nodeSet
}
// resource type -> identifier -> []nodes
// There can be multiple resources with same identifier; for example
// two modules under different namespaces.
nodeIndex map[string]map[string]nodeSet
)
func newNode(res resource.Interface) *node {
return &node{
res: res,
cc: make(nodeSet, 0, 10),
pp: make(nodeSet, 0, 10),
}
}
func (nn nodeSet) add(mm ...*node) nodeSet {
return append(nn, mm...)
}
func (nn nodeSet) has(m *node) bool {
for _, n := range nn {
if n == m {
return true
}
}
return false
}
func (nn nodeSet) remove(mm ...*node) nodeSet {
if len(mm) <= 0 {
return nn
}
nClean := make(nodeSet, 0, len(nn))
mmSet := make(nodeSet, 0, len(mm))
mmSet = append(mmSet, mm...)
for _, n := range nn {
if !mmSet.has(n) {
nClean = append(nClean, n)
}
}
return nClean
}
func (ri nodeIndex) Add(nn ...*node) {
for _, n := range nn {
rt := n.res.ResourceType()
if _, has := ri[rt]; !has {
ri[rt] = make(map[string]nodeSet)
}
for _, i := range n.res.Identifiers() {
if ri[rt][i] == nil {
ri[rt][i] = make(nodeSet, 0, 5)
}
ri[rt][i] = append(ri[rt][i], n)
}
}
}
func (ri nodeIndex) GetRef(ref *resource.Ref) *node {
refIi, has := ri[ref.ResourceType]
if !has {
return nil
}
for _, i := range ref.Identifiers {
rr, has := refIi[i]
if !has || len(rr) == 0 {
continue
}
// No constraints? no worries
if ref.Constraints == nil || len(ref.Constraints) == 0 {
return rr[0]
}
// Constraints? check if ok
// If this loop makes you sick, don't worry; this will be at most 3x3x1
for _, r := range rr {
for _, c := range ref.Constraints {
for _, ref := range r.res.Refs() {
if ref.ResourceType == c.ResourceType && ref.Identifiers.HasAny(c.Identifiers) {
return r
}
}
}
}
}
return nil
}
func (ri nodeIndex) GetResourceType(rt string) nodeSet {
ix, has := ri[rt]
if !has {
return nil
}
nm := make(map[*node]bool)
for _, nn := range ix {
for _, n := range nn {
nm[n] = true
}
}
nn := make(nodeSet, 0, len(nm))
for n := range nm {
nn = append(nn, n)
}
return nn
}

41
server/pkg/envoy/rbac.go Normal file
View File

@@ -0,0 +1,41 @@
package envoy
import (
"fmt"
"github.com/cortezaproject/corteza/server/pkg/envoy/resource"
)
// FilterRequestedRBACRules returns only RBAC rules relevant for the given resources
func FilterRequestedRBACRules(request resource.InterfaceSet, rules []*resource.RbacRule) (out []*resource.RbacRule) {
out = make([]*resource.RbacRule, 0, 10)
// Filter
dupRuleIndex := make(map[string]bool)
procResSet(request, func(r resource.Interface) {
rbacRes, ok := r.(resource.RBACInterface)
if !ok {
return
}
_, ref, pp := rbacRes.RBACParts()
resourceRefSet := appendRefSet(pp, ref)
for _, rule := range rules {
k := fmt.Sprintf("%s, %s, %d; %d", rule.Res.Resource, rule.Res.Operation, rule.Res.Access, rule.Res.RoleID)
if dupRuleIndex[k] {
continue
}
dupRuleIndex[k] = true
ruleRefSet := appendRefSet(rule.RefPath, rule.RefRes)
// Checking if rule is <= resource since wildflags can be used
if !ruleRefSet.IsSubset(resourceRefSet) {
continue
}
out = append(out, rule)
}
})
return
}

View File

@@ -0,0 +1,99 @@
package resource
import (
"fmt"
"strconv"
"github.com/cortezaproject/corteza/server/system/types"
)
type (
APIGateway struct {
*base
Res *types.ApigwRoute
Filters []*APIGatewayFilter
}
APIGatewayFilter struct {
*base
Res *types.ApigwFilter
}
)
func NewAPIGateway(res *types.ApigwRoute) *APIGateway {
// @todo we could use method + path combo to uniquely identify them also
r := &APIGateway{
base: &base{},
}
r.SetResourceType(types.ApigwRouteResourceType)
r.Res = res
r.AddIdentifier(identifiers(res.ID)...)
// Initial stamps
r.SetTimestamps(MakeTimestampsCUDA(&res.CreatedAt, res.UpdatedAt, res.DeletedAt, nil))
us := MakeUserstampsCUDO(res.CreatedBy, res.UpdatedBy, res.DeletedBy, 0)
r.SetUserstamps(us)
return r
}
func (r *APIGateway) Resource() interface{} {
return r.Res
}
func (r *APIGateway) RBACParts() (resource string, ref *Ref, path []*Ref) {
ref = r.Ref()
path = nil
resource = fmt.Sprintf(types.ApigwRouteRbacResourceTpl(), types.ApigwRouteResourceType, firstOkString(strconv.FormatUint(r.Res.ID, 10)))
return
}
func (r *APIGateway) AddGatewayFilter(res *types.ApigwFilter) *APIGatewayFilter {
f := &APIGatewayFilter{
base: &base{},
}
f.Res = res
// Initial stamps
f.SetTimestamps(MakeTimestampsCUDA(&res.CreatedAt, res.UpdatedAt, res.DeletedAt, nil))
f.SetUserstamps(MakeUserstampsCUDO(res.CreatedBy, res.UpdatedBy, res.DeletedBy, 0))
r.Filters = append(r.Filters, f)
return f
}
func (r *APIGateway) SysID() uint64 {
return r.Res.ID
}
// FindAPIGateway looks for the ApigwRoute in the resource set
func FindAPIGateway(rr InterfaceSet, ii Identifiers) (ns *types.ApigwRoute) {
var wfRes *APIGateway
rr.Walk(func(r Interface) error {
wr, ok := r.(*APIGateway)
if !ok {
return nil
}
if wr.Identifiers().HasAny(ii) {
wfRes = wr
}
return nil
})
// Found it
if wfRes != nil {
return wfRes.Res
}
return nil
}
func APIGatewayErrUnresolved(ii Identifiers) error {
return fmt.Errorf("automation apu gateway unresolved %v", ii.StringSlice())
}

View File

@@ -0,0 +1,80 @@
package resource
import (
"fmt"
"strconv"
"github.com/cortezaproject/corteza/server/system/types"
)
type (
// Application represents a Application
Application struct {
*base
Res *types.Application
}
)
func NewApplication(res *types.Application) *Application {
r := &Application{base: &base{}}
r.SetResourceType(types.ApplicationResourceType)
r.Res = res
r.AddIdentifier(identifiers(res.Name, res.ID)...)
// Initial timestamps
r.SetTimestamps(MakeTimestampsCUDA(&res.CreatedAt, res.UpdatedAt, res.DeletedAt, nil))
// Initial userstamps
if res.OwnerID > 0 {
r.SetUserstamps(&Userstamps{
OwnedBy: &Userstamp{UserID: res.OwnerID},
})
}
return r
}
func (r *Application) Resource() interface{} {
return r.Res
}
func (r *Application) SysID() uint64 {
return r.Res.ID
}
func (r *Application) RBACParts() (resource string, ref *Ref, path []*Ref) {
ref = r.Ref()
path = nil
resource = fmt.Sprintf(types.ApplicationRbacResourceTpl(), types.ApplicationResourceType, firstOkString(strconv.FormatUint(r.Res.ID, 10)))
return
}
// FindApplication looks for the app in the resource set
func FindApplication(rr InterfaceSet, ii Identifiers) (ap *types.Application) {
var apRes *Application
rr.Walk(func(r Interface) error {
ar, ok := r.(*Application)
if !ok {
return nil
}
if ar.Identifiers().HasAny(ii) {
apRes = ar
}
return nil
})
// Found it
if apRes != nil {
return apRes.Res
}
return nil
}
func ApplicationErrUnresolved(ii Identifiers) error {
return fmt.Errorf("application unresolved %v", ii.StringSlice())
}

View File

@@ -0,0 +1,155 @@
package resource
import (
"fmt"
"strconv"
"github.com/cortezaproject/corteza/server/automation/types"
)
type (
AutomationWorkflow struct {
*base
Res *types.Workflow
Triggers []*AutomationTrigger
Steps []*AutomationWorkflowStep
Paths []*AutomationWorkflowPath
}
AutomationTrigger struct {
*base
Res *types.Trigger
}
AutomationWorkflowStep struct {
*base
Res *types.WorkflowStep
}
AutomationWorkflowPath struct {
*base
Res *types.WorkflowPath
ParentStep Identifiers
ChildStep Identifiers
}
)
func NewAutomationWorkflow(res *types.Workflow) *AutomationWorkflow {
r := &AutomationWorkflow{
base: &base{},
}
r.SetResourceType(types.WorkflowResourceType)
r.Res = res
r.AddIdentifier(identifiers(res.Handle, res.Meta.Name, res.ID)...)
// Initial stamps
r.SetTimestamps(MakeTimestampsCUDA(&res.CreatedAt, res.UpdatedAt, res.DeletedAt, nil))
us := MakeUserstampsCUDO(res.CreatedBy, res.UpdatedBy, res.DeletedBy, res.OwnedBy)
us.RunAs = MakeUserstampFromID(res.RunAs)
r.SetUserstamps(us)
return r
}
func (r *AutomationWorkflow) Resource() interface{} {
return r.Res
}
func (r *AutomationWorkflow) AddAutomationTrigger(res *types.Trigger) *AutomationTrigger {
t := &AutomationTrigger{
base: &base{},
}
t.Res = res
// Initial stamps
t.SetTimestamps(MakeTimestampsCUDA(&res.CreatedAt, res.UpdatedAt, res.DeletedAt, nil))
t.SetUserstamps(MakeUserstampsCUDO(res.CreatedBy, res.UpdatedBy, res.DeletedBy, res.OwnedBy))
if r.Triggers == nil {
r.Triggers = make([]*AutomationTrigger, 0, 2)
}
r.Triggers = append(r.Triggers, t)
return t
}
func (r *AutomationWorkflow) RBACParts() (resource string, ref *Ref, path []*Ref) {
ref = r.Ref()
path = nil
resource = fmt.Sprintf(types.WorkflowRbacResourceTpl(), types.WorkflowResourceType, firstOkString(strconv.FormatUint(r.Res.ID, 10), r.Res.Handle))
return
}
func (r *AutomationWorkflow) ResourceTranslationParts() (resource string, ref *Ref, path []*Ref) {
ref = r.Ref()
path = nil
resource = fmt.Sprintf(types.WorkflowResourceTranslationTpl(), types.WorkflowResourceTranslationType, firstOkString(strconv.FormatUint(r.Res.ID, 10), r.Res.Handle))
return
}
func (r *AutomationWorkflow) AddAutomationWorkflowStep(res *types.WorkflowStep) *AutomationWorkflowStep {
s := &AutomationWorkflowStep{
base: &base{},
}
s.Res = res
if r.Steps == nil {
r.Steps = make([]*AutomationWorkflowStep, 0, 100)
}
r.Steps = append(r.Steps, s)
return s
}
func (r *AutomationWorkflow) AddAutomationWorkflowPath(res *types.WorkflowPath) *AutomationWorkflowPath {
p := &AutomationWorkflowPath{
base: &base{},
}
p.Res = res
if r.Paths == nil {
r.Paths = make([]*AutomationWorkflowPath, 0, 100)
}
r.Paths = append(r.Paths, p)
return p
}
func (r *AutomationWorkflow) SysID() uint64 {
return r.Res.ID
}
// FindAutomationWorkflow looks for the workflow in the resource set
func FindAutomationWorkflow(rr InterfaceSet, ii Identifiers) (ns *types.Workflow) {
var wfRes *AutomationWorkflow
rr.Walk(func(r Interface) error {
wr, ok := r.(*AutomationWorkflow)
if !ok {
return nil
}
if wr.Identifiers().HasAny(ii) {
wfRes = wr
}
return nil
})
// Found it
if wfRes != nil {
return wfRes.Res
}
return nil
}
func AutomationWorkflowErrUnresolved(ii Identifiers) error {
return fmt.Errorf("automation workflow unresolved %v", ii.StringSlice())
}

View File

@@ -0,0 +1,114 @@
package resource
import (
"fmt"
"strconv"
"github.com/cortezaproject/corteza/server/compose/types"
)
type (
// ComposeChart represents a ComposeChart
ComposeChart struct {
*base
Res *types.Chart
// Might keep track of related namespace
RefNs *Ref
RefMods RefSet
}
)
func NewComposeChart(res *types.Chart, nsRef *Ref, mmRef RefSet) *ComposeChart {
r := &ComposeChart{
base: &base{},
RefMods: make(RefSet, len(mmRef)),
}
r.SetResourceType(types.ChartResourceType)
r.Res = res
r.AddIdentifier(identifiers(res.Handle, res.Name, res.ID)...)
r.RefNs = r.addRef(nsRef)
for i, mRef := range mmRef {
r.RefMods[i] = r.addRef(mRef).Constraint(r.RefNs)
}
// Initial timestamps
r.SetTimestamps(MakeTimestampsCUDA(&res.CreatedAt, res.UpdatedAt, res.DeletedAt, nil))
return r
}
func (r *ComposeChart) Resource() interface{} {
return r.Res
}
func (r *ComposeChart) ReRef(old RefSet, new RefSet) {
r.base.ReRef(old, new)
// Additional references
for _, n := range new {
if n.ResourceType == types.NamespaceResourceType {
r.RefNs = MakeRef(types.NamespaceResourceType, n.Identifiers)
}
}
for i, o := range old {
if o.ResourceType == types.ModuleResourceType {
r.RefMods = r.RefMods.replaceRef(o, new[i])
}
}
}
func (r *ComposeChart) SysID() uint64 {
return r.Res.ID
}
func (r *ComposeChart) resourceParts(tpl string) (resource string, ref *Ref, path []*Ref) {
ref = r.Ref().Constraint(r.RefNs)
path = []*Ref{r.RefNs}
resource = fmt.Sprintf(types.ChartRbacResourceTpl(), types.ChartResourceType, r.RefNs.Identifiers.First(), firstOkString(strconv.FormatUint(r.Res.ID, 10), r.Res.Handle))
return
}
func (r *ComposeChart) RBACParts() (resource string, ref *Ref, path []*Ref) {
return r.resourceParts(types.ChartRbacResourceTpl())
}
func (r *ComposeChart) ResourceTranslationParts() (resource string, ref *Ref, path []*Ref) {
return r.resourceParts(types.ChartResourceTranslationTpl())
}
func (r *ComposeChart) encodeTranslations() ([]*ResourceTranslation, error) {
return nil, nil
}
// FindComposeChart looks for the chart in the resources
func FindComposeChart(rr InterfaceSet, ii Identifiers) (ch *types.Chart) {
var chRes *ComposeChart
rr.Walk(func(r Interface) error {
cr, ok := r.(*ComposeChart)
if !ok {
return nil
}
if cr.Identifiers().HasAny(ii) {
chRes = cr
}
return nil
})
// Found it
if chRes != nil {
return chRes.Res
}
return nil
}
func ComposeChartErrUnresolved(ii Identifiers) error {
return fmt.Errorf("compose chart unresolved %v", ii.StringSlice())
}

View File

@@ -0,0 +1,220 @@
package resource
import (
"fmt"
"strconv"
automationTypes "github.com/cortezaproject/corteza/server/automation/types"
"github.com/cortezaproject/corteza/server/compose/types"
)
type (
// ComposePageLayout represents a ComposePageLayout
ComposePageLayout struct {
*base
Res *types.PageLayout
RefNs *Ref
RefMod *Ref
RefParent *Ref
WfRefs RefSet
ModRefs RefSet
RefCharts RefSet
BlockRefs map[int]RefSet
}
)
func NewComposePageLayout(pg *types.PageLayout, nsRef, modRef, parentRef *Ref) *ComposePageLayout {
r := &ComposePageLayout{
base: &base{},
WfRefs: make(RefSet, 0, 10),
ModRefs: make(RefSet, 0, 10),
RefCharts: make(RefSet, 0, 10),
BlockRefs: make(map[int]RefSet),
}
r.SetResourceType(types.PageLayoutResourceType)
r.Res = pg
r.AddIdentifier(identifiers(pg.Handle, pg.Meta.Title, pg.ID)...)
r.RefNs = r.addRef(nsRef)
if modRef != nil {
r.RefMod = r.addRef(modRef).Constraint(r.RefNs)
}
if parentRef != nil {
r.RefParent = r.addRef(parentRef).Constraint(r.RefNs)
}
// Initial timestamps
r.SetTimestamps(MakeTimestampsCUDA(&pg.CreatedAt, pg.UpdatedAt, pg.DeletedAt, nil))
return r
}
func UnpackComposePageLayout(p *types.PageLayout) (*types.PageLayout, *Ref, *Ref) {
var modRef *Ref
var parentRef *Ref
return p, modRef, parentRef
}
func (r *ComposePageLayout) Resource() interface{} {
return r.Res
}
func (r *ComposePageLayout) ReRef(old RefSet, new RefSet) {
r.base.ReRef(old, new)
for _, n := range new {
switch n.ResourceType {
case types.NamespaceResourceType:
r.RefNs = MakeRef(types.NamespaceResourceType, n.Identifiers)
case types.ModuleResourceType:
r.RefMod = MakeRef(types.ModuleResourceType, n.Identifiers)
case types.PageLayoutResourceType:
r.RefParent = MakeRef(types.PageLayoutResourceType, n.Identifiers)
}
}
}
func (r *ComposePageLayout) Prune(ref *Ref) {
}
func (r *ComposePageLayout) SysID() uint64 {
return r.Res.ID
}
func (r *ComposePageLayout) resourceParts(tpl string) (resource string, ref *Ref, path []*Ref) {
ref = r.Ref().Constraint(r.RefNs)
path = []*Ref{r.RefNs}
resource = fmt.Sprintf(types.PageLayoutRbacResourceTpl(), types.PageLayoutResourceType, r.RefNs.Identifiers.First(), firstOkString(strconv.FormatUint(r.Res.ID, 10), r.Res.Handle))
return
}
func (r *ComposePageLayout) RBACParts() (resource string, ref *Ref, path []*Ref) {
return r.resourceParts(types.PageLayoutRbacResourceTpl())
}
func (r *ComposePageLayout) ResourceTranslationParts() (resource string, ref *Ref, path []*Ref) {
return r.resourceParts(types.PageLayoutResourceTranslationTpl())
}
func (r *ComposePageLayout) encodeTranslations() ([]*ResourceTranslation, error) {
return nil, nil
}
// FindComposePageLayout looks for the page in the resources
func FindComposePageLayout(rr InterfaceSet, ii Identifiers) (pg *types.PageLayout) {
var pgRes *ComposePageLayout
rr.Walk(func(r Interface) error {
pr, ok := r.(*ComposePageLayout)
if !ok {
return nil
}
if pr.Identifiers().HasAny(ii) {
pgRes = pr
}
return nil
})
// Found it
if pgRes != nil {
return pgRes.Res
}
return nil
}
func ComposePageLayoutErrUnresolved(ii Identifiers) error {
return fmt.Errorf("compose page unresolved %v", ii.StringSlice())
}
// PageLayout block utilities
func (r *ComposePageLayout) removeBlock(i int) {
// do the swap remove thing
r.Res.Blocks[i] = r.Res.Blocks[len(r.Res.Blocks)-1]
r.Res.Blocks = r.Res.Blocks[:len(r.Res.Blocks)-1]
// correct block refs.
// +1 here because it's already cut
r.BlockRefs[i] = r.BlockRefs[len(r.Res.Blocks)]
delete(r.BlockRefs, len(r.Res.Blocks))
}
func (r *ComposePageLayout) 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 (r *ComposePageLayout) pbRecordList(opt map[string]interface{}) (out *Ref) {
id := r.optString(opt, "module", "moduleID")
if id == "" || id == "0" {
return
}
return MakeRef(types.ModuleResourceType, MakeIdentifiers(id)).Constraint(r.RefNs)
}
func (r *ComposePageLayout) pbComment(opt map[string]interface{}) (out *Ref) {
id := r.optString(opt, "module", "moduleID")
if id == "" || id == "0" {
return
}
return MakeRef(types.ModuleResourceType, MakeIdentifiers(id)).Constraint(r.RefNs)
}
func (r *ComposePageLayout) pbAutomation(opt map[string]interface{}) (out *Ref) {
id := r.optString(opt, "workflow", "workflowID")
if id == "" || id == "0" {
return
}
return MakeRef(automationTypes.WorkflowResourceType, MakeIdentifiers(id))
}
func (r *ComposePageLayout) pbRecordOrganizer(opt map[string]interface{}) (out *Ref) {
id := r.optString(opt, "module", "moduleID")
if id == "" || id == "0" {
return
}
return MakeRef(types.ModuleResourceType, MakeIdentifiers(id)).Constraint(r.RefNs)
}
func (r *ComposePageLayout) pbChart(opt map[string]interface{}) (out *Ref) {
id := r.optString(opt, "chart", "chartID")
if id == "" || id == "0" {
return
}
return MakeRef(types.ChartResourceType, MakeIdentifiers(id)).Constraint(r.RefNs)
}
func (r *ComposePageLayout) pbCalendar(opt map[string]interface{}) (out *Ref) {
id := r.optString(opt, "module", "moduleID")
if id == "" || id == "0" {
return
}
return MakeRef(types.ModuleResourceType, MakeIdentifiers(id)).Constraint(r.RefNs)
}
func (r *ComposePageLayout) pbMetric(opt map[string]interface{}) (out *Ref) {
id := r.optString(opt, "module", "moduleID")
if id == "" || id == "0" {
return
}
return MakeRef(types.ModuleResourceType, MakeIdentifiers(id)).Constraint(r.RefNs)
}

View File

@@ -0,0 +1,308 @@
package resource
import (
"fmt"
"strconv"
"github.com/cortezaproject/corteza/server/compose/types"
systemTypes "github.com/cortezaproject/corteza/server/system/types"
"github.com/modern-go/reflect2"
"github.com/spf13/cast"
)
type (
ComposeModuleField struct {
*base
Res *types.ModuleField
RefNs *Ref
RefMod *Ref
}
ComposeModule struct {
*base
Res *types.Module
ResFields []*ComposeModuleField
// Might keep track of related NS
RefNs *Ref
RefMods RefSet
RefRoles RefSet
}
)
func NewComposeModule(res *types.Module, nsRef *Ref) *ComposeModule {
r := &ComposeModule{
base: &base{},
RefMods: make(RefSet, 0, len(res.Fields)),
}
r.SetResourceType(types.ModuleResourceType)
r.Res = res
r.AddIdentifier(identifiers(res.Handle, res.Name, res.ID)...)
r.RefNs = r.addRef(nsRef)
// Field deps
for _, f := range res.Fields {
switch f.Kind {
case "Record":
refMod := f.Options.String("module")
if refMod == "" {
refMod = f.Options.String("moduleID")
}
if refMod != "" && refMod != "0" {
r.RefMods = append(r.RefMods, r.AddRef(types.ModuleResourceType, refMod).Constraint(r.RefNs))
}
case "User":
refRoles := ComposeModuleFieldExtractUserFieldRoles(f.Options["roles"])
if len(refRoles) == 0 {
refRoles = ComposeModuleFieldExtractUserFieldRoles(f.Options["role"])
}
if len(refRoles) == 0 {
refRoles = ComposeModuleFieldExtractUserFieldRoles(f.Options["roleID"])
}
for _, refRole := range refRoles {
r.RefRoles = append(r.RefRoles, r.AddRef(systemTypes.RoleResourceType, refRole))
}
}
}
// Initial timestamps
r.SetTimestamps(MakeTimestampsCUDA(&res.CreatedAt, res.UpdatedAt, res.DeletedAt, nil))
return r
}
func (r *ComposeModule) Resource() interface{} {
return r.Res
}
func (r *ComposeModule) ReRef(old RefSet, new RefSet) {
r.base.ReRef(old, new)
// Additional references
for _, n := range new {
if n.ResourceType == types.NamespaceResourceType {
r.RefNs = MakeRef(types.NamespaceResourceType, n.Identifiers)
}
}
for i, o := range old {
if o.ResourceType == types.ModuleResourceType {
r.RefMods = r.RefMods.replaceRef(o, new[i])
}
}
}
func (r *ComposeModule) SysID() uint64 {
return r.Res.ID
}
func (r *ComposeModule) resourceParts(tpl string) (resource string, ref *Ref, path []*Ref) {
ref = r.Ref().Constraint(r.RefNs)
path = []*Ref{r.RefNs}
resource = fmt.Sprintf(tpl, types.ModuleResourceType, r.RefNs.Identifiers.First(), firstOkString(strconv.FormatUint(r.Res.ID, 10), r.Res.Handle))
return
}
func (r *ComposeModule) RBACParts() (resource string, ref *Ref, path []*Ref) {
return r.resourceParts(types.ModuleRbacResourceTpl())
}
func (r *ComposeModule) ResourceTranslationParts() (resource string, ref *Ref, path []*Ref) {
return r.resourceParts(types.ModuleResourceTranslationTpl())
}
func (r *ComposeModule) encodeTranslations() ([]*ResourceTranslation, error) {
out := make([]*ResourceTranslation, 0, len(r.ResFields))
for _, f := range r.ResFields {
rr := f.Res.EncodeTranslations()
rr.SetLanguage(defaultLanguage)
res, ref, pp := f.ResourceTranslationParts()
out = append(out, NewResourceTranslation(systemTypes.FromLocale(rr), res, ref, pp...))
}
return out, nil
}
// FindComposeModule looks for the module in the resource set
func FindComposeModule(rr InterfaceSet, ii Identifiers) (ns *types.Module) {
var modRes *ComposeModule
rr.Walk(func(r Interface) error {
mr, ok := r.(*ComposeModule)
if !ok {
return nil
}
if mr.Identifiers().HasAny(ii) {
modRes = mr
}
return nil
})
// Found it
if modRes != nil {
return modRes.Res
}
return nil
}
func (r *ComposeModule) AddField(f *ComposeModuleField) {
r.ResFields = append(r.ResFields, f)
}
// FindComposeModule looks for the module in the resource set
func FindComposeModuleField(rr InterfaceSet, mod, ii Identifiers) (field *types.ModuleField) {
var fieldRes *ComposeModuleField
rr.Walk(func(r Interface) error {
mr, ok := r.(*ComposeModule)
if !ok {
return nil
}
if !mr.Identifiers().HasAny(mod) {
return nil
}
for _, rf := range mr.ResFields {
if rf.Identifiers().HasAny(ii) {
fieldRes = rf
}
}
return nil
})
// Found it
if fieldRes != nil {
return fieldRes.Res
}
return nil
}
func (r *ComposeModuleField) resourceParts(tpl string) (resource string, ref *Ref, path []*Ref) {
ref = r.Ref().Constraint(r.RefNs).Constraint(r.RefMod)
path = []*Ref{r.RefNs, r.RefMod}
resource = fmt.Sprintf(types.ModuleFieldResourceTranslationTpl(), types.ModuleFieldResourceTranslationType, r.RefNs.Identifiers.First(), r.RefMod.Identifiers.First(), firstOkString(strconv.FormatUint(r.Res.ID, 10), r.Res.Name))
return
}
func (r *ComposeModuleField) RBACParts() (resource string, ref *Ref, path []*Ref) {
return r.resourceParts(types.ModuleFieldRbacResourceTpl())
}
func (r *ComposeModuleField) ResourceTranslationParts() (resource string, ref *Ref, path []*Ref) {
return r.resourceParts(types.ModuleFieldResourceTranslationTpl())
}
func (r *ComposeModuleField) ResourceTranslations() ([]*ResourceTranslation, error) {
out := make([]*ResourceTranslation, 0, 10)
rr := r.Res.EncodeTranslations()
rr.SetLanguage(defaultLanguage)
res, ref, pp := r.ResourceTranslationParts()
out = append(out, NewResourceTranslation(systemTypes.FromLocale(rr), res, ref, pp...))
return out, nil
}
func ComposeModuleErrUnresolved(ii Identifiers) error {
return fmt.Errorf("compose module unresolved %v", ii.StringSlice())
}
func ComposeModuleFieldErrUnresolved(ii Identifiers) error {
return fmt.Errorf("compose module field unresolved %v", ii.StringSlice())
}
func NewComposeModuleField(res *types.ModuleField, nsRef, modRef *Ref) *ComposeModuleField {
r := &ComposeModuleField{
base: &base{},
}
r.SetResourceType(types.ModuleFieldResourceType)
r.Res = res
r.AddIdentifier(identifiers(res.Name, res.ID)...)
r.RefNs = r.addRef(nsRef)
r.RefMod = r.addRef(modRef).Constraint(r.RefNs)
// Initial timestamps
r.SetTimestamps(MakeTimestampsCUDA(&res.CreatedAt, res.UpdatedAt, res.DeletedAt, nil))
return r
}
func (r *ComposeModuleField) Resource() interface{} {
return r.Res
}
// ComposeModuleFieldExtractUserFieldRoles is a helper to extract roles
// from the given filer options.
func ComposeModuleFieldExtractUserFieldRoles(i interface{}) []string {
if reflect2.IsNil(i) {
return nil
}
out := make([]string, 0, 1)
isOk := func(v string) bool {
return v != "" && v != "0"
}
switch v := i.(type) {
case uint64:
aux := strconv.FormatUint(v, 10)
if !isOk(aux) {
return nil
}
return []string{aux}
case []uint64:
for _, i := range v {
aux := strconv.FormatUint(i, 10)
if !isOk(aux) {
continue
}
out = append(out, aux)
}
return out
case string:
if !isOk(v) {
return nil
}
return []string{v}
case []string:
for _, aux := range v {
if !isOk(aux) {
continue
}
out = append(out, aux)
}
return out
case []interface{}:
for _, i := range v {
aux := cast.ToString(i)
if !isOk(aux) {
continue
}
out = append(out, aux)
}
return out
case interface{}:
aux := cast.ToString(v)
if !isOk(aux) {
return nil
}
return []string{aux}
}
return nil
}

View File

@@ -0,0 +1,80 @@
package resource
import (
"fmt"
"strconv"
"github.com/cortezaproject/corteza/server/compose/types"
)
type (
ComposeNamespace struct {
*base
Res *types.Namespace
}
)
func NewComposeNamespace(ns *types.Namespace) *ComposeNamespace {
r := &ComposeNamespace{base: &base{}}
r.SetResourceType(types.NamespaceResourceType)
r.Res = ns
r.AddIdentifier(identifiers(ns.Slug, ns.Name, ns.ID)...)
// Initial timestamps
r.SetTimestamps(MakeTimestampsCUDA(&ns.CreatedAt, ns.UpdatedAt, ns.DeletedAt, nil))
return r
}
func (r *ComposeNamespace) Resource() interface{} {
return r.Res
}
func (r *ComposeNamespace) RBACParts() (resource string, ref *Ref, path []*Ref) {
ref = r.Ref()
path = nil
resource = fmt.Sprintf(types.NamespaceRbacResourceTpl(), types.NamespaceResourceType, firstOkString(strconv.FormatUint(r.Res.ID, 10), r.Res.Slug))
return
}
func (r *ComposeNamespace) ResourceTranslationParts() (resource string, ref *Ref, path []*Ref) {
ref = r.Ref()
path = nil
resource = fmt.Sprintf(types.NamespaceResourceTranslationTpl(), types.NamespaceResourceTranslationType, firstOkString(strconv.FormatUint(r.Res.ID, 10), r.Res.Slug))
return
}
func (r *ComposeNamespace) SysID() uint64 {
return r.Res.ID
}
// FindComposeNamespace looks for the namespace in the resource set
func FindComposeNamespace(rr InterfaceSet, ii Identifiers) (ns *types.Namespace) {
var nsRes *ComposeNamespace
rr.Walk(func(r Interface) error {
nr, ok := r.(*ComposeNamespace)
if !ok {
return nil
}
if nr.Identifiers().HasAny(ii) {
nsRes = nr
}
return nil
})
// Found it
if nsRes != nil {
return nsRes.Res
}
return nil
}
func ComposeNamespaceErrUnresolved(ii Identifiers) error {
return fmt.Errorf("compose namespace unresolved %v", ii.StringSlice())
}

View File

@@ -0,0 +1,340 @@
package resource
import (
"fmt"
"strconv"
automationTypes "github.com/cortezaproject/corteza/server/automation/types"
"github.com/cortezaproject/corteza/server/compose/types"
)
type (
// ComposePage represents a ComposePage
ComposePage struct {
*base
Res *types.Page
RefNs *Ref
RefMod *Ref
RefParent *Ref
WfRefs RefSet
ModRefs RefSet
RefCharts RefSet
BlockRefs map[int]RefSet
}
)
func NewComposePage(pg *types.Page, nsRef, modRef, parentRef *Ref) *ComposePage {
r := &ComposePage{
base: &base{},
WfRefs: make(RefSet, 0, 10),
ModRefs: make(RefSet, 0, 10),
RefCharts: make(RefSet, 0, 10),
BlockRefs: make(map[int]RefSet),
}
r.SetResourceType(types.PageResourceType)
r.Res = pg
r.AddIdentifier(identifiers(pg.Handle, pg.Title, pg.ID)...)
r.RefNs = r.addRef(nsRef)
if modRef != nil {
r.RefMod = r.addRef(modRef).Constraint(r.RefNs)
}
if parentRef != nil {
r.RefParent = r.addRef(parentRef).Constraint(r.RefNs)
}
var ref *Ref
for i, b := range pg.Blocks {
switch b.Kind {
case "RecordList":
ref = r.pbRecordList(b.Options)
if ref != nil {
r.addRef(ref)
r.BlockRefs[i] = append(r.BlockRefs[i], ref)
r.ModRefs = append(r.ModRefs, ref)
}
case "Automation":
bb, _ := b.Options["buttons"].([]interface{})
for _, b := range bb {
button, _ := b.(map[string]interface{})
ref = r.pbAutomation(button)
if ref != nil {
r.addRef(ref)
r.BlockRefs[i] = append(r.BlockRefs[i], ref)
r.WfRefs = append(r.WfRefs, ref)
}
}
case "RecordOrganizer":
ref = r.pbRecordList(b.Options)
if ref != nil {
r.addRef(ref)
r.BlockRefs[i] = append(r.BlockRefs[i], ref)
r.ModRefs = append(r.ModRefs, ref)
}
case "Chart":
ref = r.pbChart(b.Options)
if ref != nil {
r.addRef(ref)
r.BlockRefs[i] = append(r.BlockRefs[i], ref)
r.RefCharts = append(r.RefCharts, ref)
}
case "Calendar":
ff, _ := b.Options["feeds"].([]interface{})
for _, f := range ff {
feed, _ := f.(map[string]interface{})
fOpts, _ := (feed["options"]).(map[string]interface{})
ref = r.pbCalendar(fOpts)
if ref != nil {
r.addRef(ref)
r.BlockRefs[i] = append(r.BlockRefs[i], ref)
r.ModRefs = append(r.ModRefs, ref)
}
}
case "Metric":
mm, _ := b.Options["metrics"].([]interface{})
for _, m := range mm {
mops, _ := m.(map[string]interface{})
ref = r.pbMetric(mops)
if ref != nil {
r.addRef(ref)
r.BlockRefs[i] = append(r.BlockRefs[i], ref)
r.ModRefs = append(r.ModRefs, ref)
}
}
case "Comment":
ref = r.pbComment(b.Options)
if ref != nil {
r.addRef(ref.Constraint(r.RefNs))
r.BlockRefs[i] = append(r.BlockRefs[i], ref)
r.ModRefs = append(r.ModRefs, ref)
}
}
}
// Initial timestamps
r.SetTimestamps(MakeTimestampsCUDA(&pg.CreatedAt, pg.UpdatedAt, pg.DeletedAt, nil))
return r
}
func UnpackComposePage(p *types.Page) (*types.Page, *Ref, *Ref) {
var modRef *Ref
var parentRef *Ref
if p.ModuleID != 0 {
modRef = MakeModuleRef(p.ModuleID, "", "")
}
if p.SelfID != 0 {
parentRef = MakePageRef(p.SelfID, "", "")
}
return p, modRef, parentRef
}
func (r *ComposePage) Resource() interface{} {
return r.Res
}
func (r *ComposePage) ReRef(old RefSet, new RefSet) {
r.base.ReRef(old, new)
for _, n := range new {
switch n.ResourceType {
case types.NamespaceResourceType:
r.RefNs = MakeRef(types.NamespaceResourceType, n.Identifiers)
case types.ModuleResourceType:
r.RefMod = MakeRef(types.ModuleResourceType, n.Identifiers)
case types.PageResourceType:
r.RefParent = MakeRef(types.PageResourceType, n.Identifiers)
}
}
}
func (r *ComposePage) Prune(ref *Ref) {
var auxRef *Ref
outer:
for i := len(r.Res.Blocks) - 1; i >= 0; i-- {
b := r.Res.Blocks[i]
switch b.Kind {
// Implement the rest when support is needed
case "Automation":
if b.Options["buttons"] == nil {
// In case the block isn't connected to a workflow (placeholder, script)
if auxRef == nil {
r.removeBlock(i)
continue outer
}
} else {
bb, _ := b.Options["buttons"].([]interface{})
for _, b := range bb {
button, _ := b.(map[string]interface{})
auxRef = r.pbAutomation(button)
// In case the block isn't connected to a workflow (placeholder, script)
if auxRef == nil {
r.removeBlock(i)
continue outer
}
// In case we are removing it
if auxRef.equals(ref) {
r.ReplaceRef(ref, nil)
r.WfRefs = r.WfRefs.replaceRef(ref, nil)
r.removeBlock(i)
continue outer
}
}
}
}
}
}
func (r *ComposePage) SysID() uint64 {
return r.Res.ID
}
func (r *ComposePage) resourceParts(tpl string) (resource string, ref *Ref, path []*Ref) {
ref = r.Ref().Constraint(r.RefNs)
path = []*Ref{r.RefNs}
resource = fmt.Sprintf(types.PageRbacResourceTpl(), types.PageResourceType, r.RefNs.Identifiers.First(), firstOkString(strconv.FormatUint(r.Res.ID, 10), r.Res.Handle))
return
}
func (r *ComposePage) RBACParts() (resource string, ref *Ref, path []*Ref) {
return r.resourceParts(types.PageRbacResourceTpl())
}
func (r *ComposePage) ResourceTranslationParts() (resource string, ref *Ref, path []*Ref) {
return r.resourceParts(types.PageResourceTranslationTpl())
}
func (r *ComposePage) encodeTranslations() ([]*ResourceTranslation, error) {
return nil, nil
}
// FindComposePage looks for the page in the resources
func FindComposePage(rr InterfaceSet, ii Identifiers) (pg *types.Page) {
var pgRes *ComposePage
rr.Walk(func(r Interface) error {
pr, ok := r.(*ComposePage)
if !ok {
return nil
}
if pr.Identifiers().HasAny(ii) {
pgRes = pr
}
return nil
})
// Found it
if pgRes != nil {
return pgRes.Res
}
return nil
}
func ComposePageErrUnresolved(ii Identifiers) error {
return fmt.Errorf("compose page unresolved %v", ii.StringSlice())
}
// Page block utilities
func (r *ComposePage) removeBlock(i int) {
// do the swap remove thing
r.Res.Blocks[i] = r.Res.Blocks[len(r.Res.Blocks)-1]
r.Res.Blocks = r.Res.Blocks[:len(r.Res.Blocks)-1]
// correct block refs.
// +1 here because it's already cut
r.BlockRefs[i] = r.BlockRefs[len(r.Res.Blocks)]
delete(r.BlockRefs, len(r.Res.Blocks))
}
func (r *ComposePage) 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 (r *ComposePage) pbRecordList(opt map[string]interface{}) (out *Ref) {
id := r.optString(opt, "module", "moduleID")
if id == "" || id == "0" {
return
}
return MakeRef(types.ModuleResourceType, MakeIdentifiers(id)).Constraint(r.RefNs)
}
func (r *ComposePage) pbComment(opt map[string]interface{}) (out *Ref) {
id := r.optString(opt, "module", "moduleID")
if id == "" || id == "0" {
return
}
return MakeRef(types.ModuleResourceType, MakeIdentifiers(id)).Constraint(r.RefNs)
}
func (r *ComposePage) pbAutomation(opt map[string]interface{}) (out *Ref) {
id := r.optString(opt, "workflow", "workflowID")
if id == "" || id == "0" {
return
}
return MakeRef(automationTypes.WorkflowResourceType, MakeIdentifiers(id))
}
func (r *ComposePage) pbRecordOrganizer(opt map[string]interface{}) (out *Ref) {
id := r.optString(opt, "module", "moduleID")
if id == "" || id == "0" {
return
}
return MakeRef(types.ModuleResourceType, MakeIdentifiers(id)).Constraint(r.RefNs)
}
func (r *ComposePage) pbChart(opt map[string]interface{}) (out *Ref) {
id := r.optString(opt, "chart", "chartID")
if id == "" || id == "0" {
return
}
return MakeRef(types.ChartResourceType, MakeIdentifiers(id)).Constraint(r.RefNs)
}
func (r *ComposePage) pbCalendar(opt map[string]interface{}) (out *Ref) {
id := r.optString(opt, "module", "moduleID")
if id == "" || id == "0" {
return
}
return MakeRef(types.ModuleResourceType, MakeIdentifiers(id)).Constraint(r.RefNs)
}
func (r *ComposePage) pbMetric(opt map[string]interface{}) (out *Ref) {
id := r.optString(opt, "module", "moduleID")
if id == "" || id == "0" {
return
}
return MakeRef(types.ModuleResourceType, MakeIdentifiers(id)).Constraint(r.RefNs)
}

View File

@@ -0,0 +1,115 @@
package resource
import (
"fmt"
"github.com/cortezaproject/corteza/server/compose/types"
composeTypes "github.com/cortezaproject/corteza/server/compose/types"
systemTypes "github.com/cortezaproject/corteza/server/system/types"
)
type (
rawSysValues struct {
OwnedBy string
CreatedAt string
CreatedBy string
UpdatedAt string
UpdatedBy string
DeletedAt string
DeletedBy string
}
ComposeRecordRaw struct {
ID string
Values map[string]ComposeRecordRawValue
Ts *Timestamps
Us *Userstamps
Config *EnvoyConfig
}
ComposeRecordRawSet []*ComposeRecordRaw
ComposeRecordRawValue struct {
Name string
IsMulti bool
Value string
Values []string
}
CrsWalker func(f func(r *ComposeRecordRaw) error) error
ComposeRecord struct {
*base
Walker CrsWalker
RefNs *Ref
RefMod *Ref
RelMod *composeTypes.Module
IDMap map[string]uint64
// UserFlakes help the system by predefining a set of potential sys user references.
// This should make the operation cheaper for larger datasets.
UserFlakes UserstampIndex
}
)
func NewComposeRecordSet(w CrsWalker, nsRef, modRef string) *ComposeRecord {
r := &ComposeRecord{
base: &base{},
IDMap: make(map[string]uint64),
}
r.SetResourceType(composeTypes.RecordResourceType)
r.Walker = w
r.AddIdentifier(identifiers(modRef)...)
r.RefNs = r.AddRef(composeTypes.NamespaceResourceType, nsRef)
r.RefMod = r.AddRef(composeTypes.ModuleResourceType, modRef).Constraint(r.RefNs)
return r
}
func (r *ComposeRecord) Resource() interface{} {
return nil
}
func (r *ComposeRecord) SetUserFlakes(uu UserstampIndex) {
r.UserFlakes = uu
// Set user refs as wildflag, indicating it refers to any user resource
r.AddRef(systemTypes.UserResourceType, "*")
}
func (r *ComposeRecord) RBACParts() (resource string, ref *Ref, path []*Ref) {
ref = r.Ref()
path = []*Ref{r.RefNs, r.RefMod}
// @todo specific records
resource = fmt.Sprintf(types.RecordRbacResourceTpl(), types.RecordResourceType, r.RefNs.Identifiers.First(), r.RefMod.Identifiers.First(), "*")
return
}
func FindComposeRecordResource(rr InterfaceSet, ii Identifiers) (rec *ComposeRecord) {
rr.Walk(func(r Interface) error {
crr, ok := r.(*ComposeRecord)
if !ok {
return nil
}
if crr.Identifiers().HasAny(ii) {
rec = crr
}
return nil
})
return rec
}
func ComposeRecordErrUnresolved(ii Identifiers) error {
return fmt.Errorf("compose record unresolved %v", ii.StringSlice())
}

View File

@@ -0,0 +1,205 @@
package resource
import (
"strings"
)
type (
// A simple wrapper struct for related resources
composeRecordShaper struct{}
)
// ComposeRecordShaper initializes and returns a new compose record resource shaper
func ComposeRecordShaper() shaper {
return &composeRecordShaper{}
}
// Shape shapes ResourceDatasets based on the related compose template
//
// The first step finds the matching pair;
// The second step creates an actual resource based on the two.
func (crt *composeRecordShaper) Shape(rr []Interface) ([]Interface, error) {
// This will do for most cases
ii := make([]Interface, 0, int(len(rr)/2)+1)
for _, r := range rr {
rt, ok := r.(*ComposeRecordTemplate)
if !ok {
continue
}
rd := findResourceDataset(rr, rt.Identifiers())
if rd == nil {
return nil, genericSourceErrUnresolved(rt.Identifiers())
}
ii = append(ii, crt.toResource(rt, rd))
}
return ii, nil
}
func (r *composeRecordShaper) Resource() interface{} {
return nil
}
func (crt *composeRecordShaper) toResource(def *ComposeRecordTemplate, dt *ResourceDataset) Interface {
w := func(f func(r *ComposeRecordRaw) error) error {
if err := dt.P.Reset(); err != nil {
return err
}
for {
mr, err := dt.P.Next()
if err != nil {
return err
}
if mr == nil {
return nil
}
// Get the bits in order
vv := crt.mapValues(mr, def.FieldMap, def.Defaultable)
auxVals := make(map[string]ComposeRecordRawValue)
for k, v := range vv {
auxVals[k] = ComposeRecordRawValue{
Name: k,
Value: v,
}
}
rRaw := &ComposeRecordRaw{
ID: crt.getKey(mr, def.Key),
Values: auxVals,
}
crt.getTimestamps(rRaw)
crt.getUserstamps(rRaw)
// Process it
err = f(rRaw)
if err != nil {
return err
}
}
}
return NewComposeRecordSet(w, def.NsRef.Identifiers.First(), def.ModRef.Identifiers.First())
}
// mapValues maps original values based on the provided mapping
func (crt *composeRecordShaper) mapValues(ov map[string]string, fm MappingTplSet, defaultable bool) map[string]string {
nv := make(map[string]string)
// Mappings are provided as a slice since we'll want to do some additional conditional mappings.
// We'll make an index for a nicer lookup for now.
mx := make(map[string]*MappingTpl)
for _, m := range fm {
mx[m.Cell] = m
}
if defaultable {
for k, v := range ov {
if m, has := mx[k]; has {
if !m.IsIgnored() {
nv[m.Field] = v
}
} else {
nv[k] = v
}
}
} else {
if len(mx) > 0 {
for k, v := range ov {
if m, has := mx[k]; has && !m.IsIgnored() {
nv[m.Field] = v
}
}
} else {
for k, v := range ov {
nv[k] = v
}
}
}
return nv
}
func (crt *composeRecordShaper) getTimestamps(r *ComposeRecordRaw) {
ts := &Timestamps{}
// Provided values are already mapped
for k, v := range crt.cloneValues(r) {
switch strings.ToLower(k) {
case "createdat",
"created_at":
ts.CreatedAt = MakeTimestamp(v.Value)
delete(r.Values, k)
case "updatedat",
"updated_at":
ts.UpdatedAt = MakeTimestamp(v.Value)
delete(r.Values, k)
case "deletedat",
"deleted_at":
ts.DeletedAt = MakeTimestamp(v.Value)
delete(r.Values, k)
}
}
r.Ts = ts
}
func (crt *composeRecordShaper) getUserstamps(r *ComposeRecordRaw) {
us := &Userstamps{}
// Provided values are already mapped
for k, v := range crt.cloneValues(r) {
switch strings.ToLower(k) {
case "createdby",
"creatorid",
"creator":
us.CreatedBy = MakeUserstampFromRef(v.Value)
delete(r.Values, k)
case "updatedby":
us.UpdatedBy = MakeUserstampFromRef(v.Value)
delete(r.Values, k)
case "deletedby":
us.DeletedBy = MakeUserstampFromRef(v.Value)
delete(r.Values, k)
case "ownedby",
"ownerid",
"owner":
us.OwnedBy = MakeUserstampFromRef(v.Value)
delete(r.Values, k)
}
}
r.Us = us
}
func (crt *composeRecordShaper) cloneValues(r *ComposeRecordRaw) map[string]ComposeRecordRawValue {
rr := make(map[string]ComposeRecordRawValue)
for k, v := range r.Values {
rr[k] = ComposeRecordRawValue{
Name: v.Name,
Value: v.Value,
}
}
return rr
}
func (crt *composeRecordShaper) getKey(vv map[string]string, kk []string) (rtr string) {
if len(kk) <= 0 {
return ""
}
for _, k := range kk {
rtr += k + "."
}
// Remove the trailing dot
return vv[rtr[0:len(rtr)-1]]
}

View File

@@ -0,0 +1,77 @@
package resource
import (
"github.com/cortezaproject/corteza/server/compose/types"
)
type (
MappingTpl struct {
// Index is like Cell, but the index
Index uint
Cell string
Field string
// If specifies when the field should be mapped
// @todo
If string
// Expr specifies how to aditionally manipulate the value
// @todo
Expr string
}
MappingTplSet []*MappingTpl
ComposeRecordTemplate struct {
// We'll do this so it conforms to the resource.Interface
*base
ModRef *Ref
NsRef *Ref
// Name is the source name; topically file name
Name string
// Key determines what defines an identifier
Key []string
FieldMap MappingTplSet
Defaultable bool
}
)
// NewComposeRecordTemplate returns a record template based on the provided parameters.
//
// The template is only valid up until the resource shaping; it is not allowed after the fact.
func NewComposeRecordTemplate(modRef, nsRef, name string, defaultable bool, fieldMap MappingTplSet, key ...string) *ComposeRecordTemplate {
r := &ComposeRecordTemplate{base: &base{}}
r.Name = name
r.Key = key
r.FieldMap = fieldMap
r.Defaultable = defaultable
r.NsRef = r.AddRef(types.NamespaceResourceType, nsRef)
r.ModRef = r.AddRef(types.ModuleResourceType, modRef).Constraint(r.NsRef)
r.SetResourceType(DataSourceResourceType)
r.AddIdentifier(identifiers(name)...)
return r
}
func (r *ComposeRecordTemplate) Resource() interface{} {
return nil
}
// MapToMappingTplSet converts the given string map to a propper MappingTplSet
func MapToMappingTplSet(b map[string]string) MappingTplSet {
mp := make(MappingTplSet, 0, len(b))
for c, f := range b {
mp = append(mp, &MappingTpl{
Cell: c,
Field: f,
})
}
return mp
}
func (t *MappingTpl) IsIgnored() bool {
// @todo expand this?
return t.Field == "/"
}

View File

@@ -0,0 +1,30 @@
package resource
type (
provider interface {
Fields() []string
Count() uint64
Reset() error
Next() (map[string]string, error)
}
ResourceDataset struct {
*base
P provider
}
)
func NewResourceDataset(name string, p provider) *ResourceDataset {
r := &ResourceDataset{base: &base{}}
r.P = p
r.SetResourceType(DataSourceResourceType)
r.AddIdentifier(identifiers(name)...)
return r
}
func (r *ResourceDataset) Resource() interface{} {
return nil
}

View File

@@ -0,0 +1,24 @@
package resource
// This file is auto-generated.
//
// Changes to this file may cause incorrect behavior and will be lost if
// the code is regenerated.
//
import (
"github.com/cortezaproject/corteza/server/automation/types"
)
// AutomationWorkflowRbacReferences generates RBAC references
//
// Resources with "envoy: false" are skipped
//
// This function is auto-generated
func AutomationWorkflowRbacReferences(workflow string) (res *Ref, pp []*Ref, err error) {
if workflow != "*" {
res = &Ref{ResourceType: types.WorkflowResourceType, Identifiers: MakeIdentifiers(workflow)}
}
return
}

View File

@@ -0,0 +1,129 @@
package resource
// This file is auto-generated.
//
// Changes to this file may cause incorrect behavior and will be lost if
// the code is regenerated.
//
import (
"github.com/cortezaproject/corteza/server/compose/types"
)
// ComposeChartRbacReferences generates RBAC references
//
// Resources with "envoy: false" are skipped
//
// This function is auto-generated
func ComposeChartRbacReferences(namespaceID string, chart string) (res *Ref, pp []*Ref, err error) {
if namespaceID != "*" {
pp = append(pp, &Ref{ResourceType: types.NamespaceResourceType, Identifiers: MakeIdentifiers(namespaceID)})
}
if chart != "*" {
res = &Ref{ResourceType: types.ChartResourceType, Identifiers: MakeIdentifiers(chart)}
}
return
}
// ComposeModuleRbacReferences generates RBAC references
//
// Resources with "envoy: false" are skipped
//
// This function is auto-generated
func ComposeModuleRbacReferences(namespaceID string, module string) (res *Ref, pp []*Ref, err error) {
if namespaceID != "*" {
pp = append(pp, &Ref{ResourceType: types.NamespaceResourceType, Identifiers: MakeIdentifiers(namespaceID)})
}
if module != "*" {
res = &Ref{ResourceType: types.ModuleResourceType, Identifiers: MakeIdentifiers(module)}
}
return
}
// ComposeModuleFieldRbacReferences generates RBAC references
//
// Resources with "envoy: false" are skipped
//
// This function is auto-generated
func ComposeModuleFieldRbacReferences(namespaceID string, moduleID string, moduleField string) (res *Ref, pp []*Ref, err error) {
if namespaceID != "*" {
pp = append(pp, &Ref{ResourceType: types.NamespaceResourceType, Identifiers: MakeIdentifiers(namespaceID)})
}
if moduleID != "*" {
pp = append(pp, &Ref{ResourceType: types.ModuleResourceType, Identifiers: MakeIdentifiers(moduleID)})
}
if moduleField != "*" {
res = &Ref{ResourceType: types.ModuleFieldResourceType, Identifiers: MakeIdentifiers(moduleField)}
}
return
}
// ComposeNamespaceRbacReferences generates RBAC references
//
// Resources with "envoy: false" are skipped
//
// This function is auto-generated
func ComposeNamespaceRbacReferences(namespace string) (res *Ref, pp []*Ref, err error) {
if namespace != "*" {
res = &Ref{ResourceType: types.NamespaceResourceType, Identifiers: MakeIdentifiers(namespace)}
}
return
}
// ComposePageRbacReferences generates RBAC references
//
// Resources with "envoy: false" are skipped
//
// This function is auto-generated
func ComposePageRbacReferences(namespaceID string, page string) (res *Ref, pp []*Ref, err error) {
if namespaceID != "*" {
pp = append(pp, &Ref{ResourceType: types.NamespaceResourceType, Identifiers: MakeIdentifiers(namespaceID)})
}
if page != "*" {
res = &Ref{ResourceType: types.PageResourceType, Identifiers: MakeIdentifiers(page)}
}
return
}
// ComposePageLayoutRbacReferences generates RBAC references
//
// Resources with "envoy: false" are skipped
//
// This function is auto-generated
func ComposePageLayoutRbacReferences(namespaceID string, pageID string, pageLayout string) (res *Ref, pp []*Ref, err error) {
if namespaceID != "*" {
pp = append(pp, &Ref{ResourceType: types.NamespaceResourceType, Identifiers: MakeIdentifiers(namespaceID)})
}
if pageID != "*" {
pp = append(pp, &Ref{ResourceType: types.PageResourceType, Identifiers: MakeIdentifiers(pageID)})
}
if pageLayout != "*" {
res = &Ref{ResourceType: types.PageLayoutResourceType, Identifiers: MakeIdentifiers(pageLayout)}
}
return
}
// ComposeRecordRbacReferences generates RBAC references
//
// Resources with "envoy: false" are skipped
//
// This function is auto-generated
func ComposeRecordRbacReferences(namespaceID string, moduleID string, record string) (res *Ref, pp []*Ref, err error) {
if namespaceID != "*" {
pp = append(pp, &Ref{ResourceType: types.NamespaceResourceType, Identifiers: MakeIdentifiers(namespaceID)})
}
if moduleID != "*" {
pp = append(pp, &Ref{ResourceType: types.ModuleResourceType, Identifiers: MakeIdentifiers(moduleID)})
}
if record != "*" {
res = &Ref{ResourceType: types.RecordResourceType, Identifiers: MakeIdentifiers(record)}
}
return
}

View File

@@ -0,0 +1,56 @@
package resource
// This file is auto-generated.
//
// Changes to this file may cause incorrect behavior and will be lost if
// the code is regenerated.
//
import (
"github.com/cortezaproject/corteza/server/federation/types"
)
// FederationNodeRbacReferences generates RBAC references
//
// Resources with "envoy: false" are skipped
//
// This function is auto-generated
func FederationNodeRbacReferences(node string) (res *Ref, pp []*Ref, err error) {
if node != "*" {
res = &Ref{ResourceType: types.NodeResourceType, Identifiers: MakeIdentifiers(node)}
}
return
}
// FederationExposedModuleRbacReferences generates RBAC references
//
// Resources with "envoy: false" are skipped
//
// This function is auto-generated
func FederationExposedModuleRbacReferences(nodeID string, exposedModule string) (res *Ref, pp []*Ref, err error) {
if nodeID != "*" {
pp = append(pp, &Ref{ResourceType: types.NodeResourceType, Identifiers: MakeIdentifiers(nodeID)})
}
if exposedModule != "*" {
res = &Ref{ResourceType: types.ExposedModuleResourceType, Identifiers: MakeIdentifiers(exposedModule)}
}
return
}
// FederationSharedModuleRbacReferences generates RBAC references
//
// Resources with "envoy: false" are skipped
//
// This function is auto-generated
func FederationSharedModuleRbacReferences(nodeID string, sharedModule string) (res *Ref, pp []*Ref, err error) {
if nodeID != "*" {
pp = append(pp, &Ref{ResourceType: types.NodeResourceType, Identifiers: MakeIdentifiers(nodeID)})
}
if sharedModule != "*" {
res = &Ref{ResourceType: types.SharedModuleResourceType, Identifiers: MakeIdentifiers(sharedModule)}
}
return
}

View File

@@ -0,0 +1,141 @@
package resource
// This file is auto-generated.
//
// Changes to this file may cause incorrect behavior and will be lost if
// the code is regenerated.
//
import (
"github.com/cortezaproject/corteza/server/system/types"
)
// SystemApplicationRbacReferences generates RBAC references
//
// Resources with "envoy: false" are skipped
//
// This function is auto-generated
func SystemApplicationRbacReferences(application string) (res *Ref, pp []*Ref, err error) {
if application != "*" {
res = &Ref{ResourceType: types.ApplicationResourceType, Identifiers: MakeIdentifiers(application)}
}
return
}
// SystemApigwRouteRbacReferences generates RBAC references
//
// Resources with "envoy: false" are skipped
//
// This function is auto-generated
func SystemApigwRouteRbacReferences(apigwRoute string) (res *Ref, pp []*Ref, err error) {
if apigwRoute != "*" {
res = &Ref{ResourceType: types.ApigwRouteResourceType, Identifiers: MakeIdentifiers(apigwRoute)}
}
return
}
// SystemAuthClientRbacReferences generates RBAC references
//
// Resources with "envoy: false" are skipped
//
// This function is auto-generated
func SystemAuthClientRbacReferences(authClient string) (res *Ref, pp []*Ref, err error) {
if authClient != "*" {
res = &Ref{ResourceType: types.AuthClientResourceType, Identifiers: MakeIdentifiers(authClient)}
}
return
}
// SystemDataPrivacyRequestRbacReferences generates RBAC references
//
// Resources with "envoy: false" are skipped
//
// This function is auto-generated
func SystemDataPrivacyRequestRbacReferences(dataPrivacyRequest string) (res *Ref, pp []*Ref, err error) {
if dataPrivacyRequest != "*" {
res = &Ref{ResourceType: types.DataPrivacyRequestResourceType, Identifiers: MakeIdentifiers(dataPrivacyRequest)}
}
return
}
// SystemQueueRbacReferences generates RBAC references
//
// Resources with "envoy: false" are skipped
//
// This function is auto-generated
func SystemQueueRbacReferences(queue string) (res *Ref, pp []*Ref, err error) {
if queue != "*" {
res = &Ref{ResourceType: types.QueueResourceType, Identifiers: MakeIdentifiers(queue)}
}
return
}
// SystemReportRbacReferences generates RBAC references
//
// Resources with "envoy: false" are skipped
//
// This function is auto-generated
func SystemReportRbacReferences(report string) (res *Ref, pp []*Ref, err error) {
if report != "*" {
res = &Ref{ResourceType: types.ReportResourceType, Identifiers: MakeIdentifiers(report)}
}
return
}
// SystemRoleRbacReferences generates RBAC references
//
// Resources with "envoy: false" are skipped
//
// This function is auto-generated
func SystemRoleRbacReferences(role string) (res *Ref, pp []*Ref, err error) {
if role != "*" {
res = &Ref{ResourceType: types.RoleResourceType, Identifiers: MakeIdentifiers(role)}
}
return
}
// SystemTemplateRbacReferences generates RBAC references
//
// Resources with "envoy: false" are skipped
//
// This function is auto-generated
func SystemTemplateRbacReferences(template string) (res *Ref, pp []*Ref, err error) {
if template != "*" {
res = &Ref{ResourceType: types.TemplateResourceType, Identifiers: MakeIdentifiers(template)}
}
return
}
// SystemUserRbacReferences generates RBAC references
//
// Resources with "envoy: false" are skipped
//
// This function is auto-generated
func SystemUserRbacReferences(user string) (res *Ref, pp []*Ref, err error) {
if user != "*" {
res = &Ref{ResourceType: types.UserResourceType, Identifiers: MakeIdentifiers(user)}
}
return
}
// SystemDalConnectionRbacReferences generates RBAC references
//
// Resources with "envoy: false" are skipped
//
// This function is auto-generated
func SystemDalConnectionRbacReferences(dalConnection string) (res *Ref, pp []*Ref, err error) {
if dalConnection != "*" {
res = &Ref{ResourceType: types.DalConnectionResourceType, Identifiers: MakeIdentifiers(dalConnection)}
}
return
}

View File

@@ -0,0 +1,122 @@
package resource
import (
"fmt"
composeTypes "github.com/cortezaproject/corteza/server/compose/types"
"github.com/cortezaproject/corteza/server/pkg/rbac"
)
type (
RbacRule struct {
*base
Res *rbac.Rule
RefResource string
RefRes *Ref
RefRole *Ref
RefPath []*Ref
}
)
func NewRbacRule(res *rbac.Rule, refRole, refRes *Ref, refResource string, refPath ...*Ref) *RbacRule {
r := &RbacRule{base: &base{}}
r.SetResourceType(RbacResourceType)
r.Res = res
r.RefRole = r.addRef(refRole)
r.RefResource = refResource
if refRes != nil {
r.RefRes = r.addRef(refRes)
}
// any additional constraints
for i, rp := range refPath {
ref := MakeRef(rp.ResourceType, rp.Identifiers)
if r.RefRes != nil {
r.RefRes.Constraint(ref)
}
if i > 0 {
for j := i - 1; j < i; j++ {
aux := refPath[j]
ref = ref.Constraint(MakeRef(aux.ResourceType, aux.Identifiers))
}
}
r.RefPath = append(r.RefPath, r.addRef(ref))
}
// Handle cases for nested resources
if refRes != nil {
switch refRes.ResourceType {
case composeTypes.RecordResourceType:
r.handleComposeRecord(res, refPath)
case composeTypes.ModuleFieldResourceType:
r.handleComposeModuleField(res, refPath)
}
}
return r
}
func (r *RbacRule) WrapError(act string, err error) error {
return fmt.Errorf("%s %s %v: %s",
act,
r.ResourceType(),
fmt.Sprintf("{ role: %s, resource: %s, operation: %s, access: %s }", r.RefRole.Identifiers.First(), r.RefResource, r.Res.Operation, r.Res.Access),
err,
)
}
func (r *RbacRule) Resource() interface{} {
return r.Res
}
func (r *RbacRule) ReRef(old RefSet, new RefSet) {
r.base.ReRef(old, new)
// Care for wildcards
if r.RefRes != nil {
for i, o := range old {
if o.equals(r.RefRes) {
r.RefRes = new[i]
break
}
}
}
for i, o := range old {
if RefSet(r.RefPath).findRef(o) > -1 {
r.RefPath = RefSet(r.RefPath).replaceRef(o, new[i])
}
}
}
func (r *RbacRule) handleComposeRecord(res *rbac.Rule, refPath []*Ref) {
// records are grouped under module
if len(refPath) < 2 {
return
}
r.AddRef(composeTypes.RecordResourceType, refPath[1].Identifiers.StringSlice()...)
}
func (r *RbacRule) handleComposeModuleField(res *rbac.Rule, refPath []*Ref) {
// module fields are grouped under module
if len(refPath) < 2 {
return
}
r.AddRef(composeTypes.ModuleFieldResourceTranslationType, refPath[1].Identifiers.StringSlice()...)
}
func RbacResourceErrNotFound(ii Identifiers) error {
return fmt.Errorf("rbac resource not found %v", ii.StringSlice())
}

View File

@@ -0,0 +1,282 @@
package resource
// This file is auto-generated.
//
// Changes to this file may cause incorrect behavior and will be lost if
// the code is regenerated.
//
import (
"fmt"
automationTypes "github.com/cortezaproject/corteza/server/automation/types"
composeTypes "github.com/cortezaproject/corteza/server/compose/types"
federationTypes "github.com/cortezaproject/corteza/server/federation/types"
systemTypes "github.com/cortezaproject/corteza/server/system/types"
"strings"
)
// Parse generates resource setting logic for each resource
//
// Resources with "envoy: false" are skipped
//
// This function is auto-generated
func ParseRule(res string) (string, *Ref, []*Ref, error) {
if res == "" {
return "", nil, nil, fmt.Errorf("empty resource")
}
sp := "/"
res = strings.TrimSpace(res)
res = strings.TrimRight(res, sp)
rr := strings.Split(res, sp)
// only service defined (corteza::system, corteza::compose, ...)
if len(rr) == 1 {
return res, nil, nil, nil
}
// full thing
resourceType, path := rr[0], rr[1:]
for p := 1; p < len(path); p++ {
if path[p] != "*" && path[p-1] == "*" {
return "", nil, nil, fmt.Errorf("invalid path wildcard combination for '%s'", res)
}
}
// make the resource provide the slice of parent resources we should nest under
switch resourceType {
case systemTypes.ApplicationResourceType:
if len(path) != 1 {
return "", nil, nil, fmt.Errorf("expecting 1 reference components in path, got %d", len(path))
}
ref, pp, err := SystemApplicationRbacReferences(
path[0],
)
return resourceType, ref, pp, err
case systemTypes.ApigwRouteResourceType:
if len(path) != 1 {
return "", nil, nil, fmt.Errorf("expecting 1 reference components in path, got %d", len(path))
}
ref, pp, err := SystemApigwRouteRbacReferences(
path[0],
)
return resourceType, ref, pp, err
case systemTypes.AuthClientResourceType:
if len(path) != 1 {
return "", nil, nil, fmt.Errorf("expecting 1 reference components in path, got %d", len(path))
}
ref, pp, err := SystemAuthClientRbacReferences(
path[0],
)
return resourceType, ref, pp, err
case systemTypes.DataPrivacyRequestResourceType:
if len(path) != 1 {
return "", nil, nil, fmt.Errorf("expecting 1 reference components in path, got %d", len(path))
}
ref, pp, err := SystemDataPrivacyRequestRbacReferences(
path[0],
)
return resourceType, ref, pp, err
case systemTypes.QueueResourceType:
if len(path) != 1 {
return "", nil, nil, fmt.Errorf("expecting 1 reference components in path, got %d", len(path))
}
ref, pp, err := SystemQueueRbacReferences(
path[0],
)
return resourceType, ref, pp, err
case systemTypes.ReportResourceType:
if len(path) != 1 {
return "", nil, nil, fmt.Errorf("expecting 1 reference components in path, got %d", len(path))
}
ref, pp, err := SystemReportRbacReferences(
path[0],
)
return resourceType, ref, pp, err
case systemTypes.RoleResourceType:
if len(path) != 1 {
return "", nil, nil, fmt.Errorf("expecting 1 reference components in path, got %d", len(path))
}
ref, pp, err := SystemRoleRbacReferences(
path[0],
)
return resourceType, ref, pp, err
case systemTypes.TemplateResourceType:
if len(path) != 1 {
return "", nil, nil, fmt.Errorf("expecting 1 reference components in path, got %d", len(path))
}
ref, pp, err := SystemTemplateRbacReferences(
path[0],
)
return resourceType, ref, pp, err
case systemTypes.UserResourceType:
if len(path) != 1 {
return "", nil, nil, fmt.Errorf("expecting 1 reference components in path, got %d", len(path))
}
ref, pp, err := SystemUserRbacReferences(
path[0],
)
return resourceType, ref, pp, err
case systemTypes.DalConnectionResourceType:
if len(path) != 1 {
return "", nil, nil, fmt.Errorf("expecting 1 reference components in path, got %d", len(path))
}
ref, pp, err := SystemDalConnectionRbacReferences(
path[0],
)
return resourceType, ref, pp, err
case composeTypes.ChartResourceType:
if len(path) != 2 {
return "", nil, nil, fmt.Errorf("expecting 2 reference components in path, got %d", len(path))
}
ref, pp, err := ComposeChartRbacReferences(
path[0],
path[1],
)
return resourceType, ref, pp, err
case composeTypes.ModuleResourceType:
if len(path) != 2 {
return "", nil, nil, fmt.Errorf("expecting 2 reference components in path, got %d", len(path))
}
ref, pp, err := ComposeModuleRbacReferences(
path[0],
path[1],
)
return resourceType, ref, pp, err
case composeTypes.ModuleFieldResourceType:
if len(path) != 3 {
return "", nil, nil, fmt.Errorf("expecting 3 reference components in path, got %d", len(path))
}
ref, pp, err := ComposeModuleFieldRbacReferences(
path[0],
path[1],
path[2],
)
return resourceType, ref, pp, err
case composeTypes.NamespaceResourceType:
if len(path) != 1 {
return "", nil, nil, fmt.Errorf("expecting 1 reference components in path, got %d", len(path))
}
ref, pp, err := ComposeNamespaceRbacReferences(
path[0],
)
return resourceType, ref, pp, err
case composeTypes.PageResourceType:
if len(path) != 2 {
return "", nil, nil, fmt.Errorf("expecting 2 reference components in path, got %d", len(path))
}
ref, pp, err := ComposePageRbacReferences(
path[0],
path[1],
)
return resourceType, ref, pp, err
case composeTypes.PageLayoutResourceType:
if len(path) != 3 {
return "", nil, nil, fmt.Errorf("expecting 3 reference components in path, got %d", len(path))
}
ref, pp, err := ComposePageLayoutRbacReferences(
path[0],
path[1],
path[2],
)
return resourceType, ref, pp, err
case composeTypes.RecordResourceType:
if len(path) != 3 {
return "", nil, nil, fmt.Errorf("expecting 3 reference components in path, got %d", len(path))
}
ref, pp, err := ComposeRecordRbacReferences(
path[0],
path[1],
path[2],
)
return resourceType, ref, pp, err
case automationTypes.WorkflowResourceType:
if len(path) != 1 {
return "", nil, nil, fmt.Errorf("expecting 1 reference components in path, got %d", len(path))
}
ref, pp, err := AutomationWorkflowRbacReferences(
path[0],
)
return resourceType, ref, pp, err
case federationTypes.NodeResourceType:
if len(path) != 1 {
return "", nil, nil, fmt.Errorf("expecting 1 reference components in path, got %d", len(path))
}
ref, pp, err := FederationNodeRbacReferences(
path[0],
)
return resourceType, ref, pp, err
case federationTypes.ExposedModuleResourceType:
if len(path) != 2 {
return "", nil, nil, fmt.Errorf("expecting 2 reference components in path, got %d", len(path))
}
ref, pp, err := FederationExposedModuleRbacReferences(
path[0],
path[1],
)
return resourceType, ref, pp, err
case federationTypes.SharedModuleResourceType:
if len(path) != 2 {
return "", nil, nil, fmt.Errorf("expecting 2 reference components in path, got %d", len(path))
}
ref, pp, err := FederationSharedModuleRbacReferences(
path[0],
path[1],
)
return resourceType, ref, pp, err
case systemTypes.ComponentResourceType:
if len(path) != 0 {
return "", nil, nil, fmt.Errorf("expecting 0 reference components in path, got %d", len(path))
}
// Component resource, no path
return resourceType, nil, nil, nil
case composeTypes.ComponentResourceType:
if len(path) != 0 {
return "", nil, nil, fmt.Errorf("expecting 0 reference components in path, got %d", len(path))
}
// Component resource, no path
return resourceType, nil, nil, nil
case automationTypes.ComponentResourceType:
if len(path) != 0 {
return "", nil, nil, fmt.Errorf("expecting 0 reference components in path, got %d", len(path))
}
// Component resource, no path
return resourceType, nil, nil, nil
case federationTypes.ComponentResourceType:
if len(path) != 0 {
return "", nil, nil, fmt.Errorf("expecting 0 reference components in path, got %d", len(path))
}
// Component resource, no path
return resourceType, nil, nil, nil
}
// return unhandled resource as-is
return resourceType, nil, nil, nil
}

View File

@@ -0,0 +1,121 @@
package resource
import (
"fmt"
"strconv"
"github.com/cortezaproject/corteza/server/system/types"
)
type (
Report struct {
*base
Res *types.Report
Sources []*ReportSource
Blocks []*ReportBlock
}
ReportSource struct {
*base
Res *types.ReportDataSource
}
ReportBlock struct {
*base
Res *types.ReportBlock
}
)
func NewReport(res *types.Report) *Report {
r := &Report{
base: &base{},
}
r.SetResourceType(types.ReportResourceType)
r.Res = res
r.AddIdentifier(identifiers(res.Handle, res.ID)...)
// Initial stamps
r.SetTimestamps(MakeTimestampsCUDA(&res.CreatedAt, res.UpdatedAt, res.DeletedAt, nil))
r.SetUserstamps(MakeUserstampsCUDO(res.CreatedBy, res.UpdatedBy, res.DeletedBy, res.OwnedBy))
return r
}
func (r *Report) Resource() interface{} {
return r.Res
}
func (r *Report) AddReportSource(res *types.ReportDataSource) *ReportSource {
s := &ReportSource{
base: &base{},
}
s.Res = res
r.Sources = append(r.Sources, s)
return s
}
func (r *Report) RBACParts() (resource string, ref *Ref, path []*Ref) {
ref = r.Ref()
path = nil
resource = fmt.Sprintf(types.ReportRbacResourceTpl(), types.ReportResourceType, firstOkString(strconv.FormatUint(r.Res.ID, 10), r.Res.Handle))
return
}
// func (r *Report) ResourceTranslationParts() (resource string, ref *Ref, path []*Ref) {
// ref = r.Ref()
// path = nil
// resource = fmt.Sprintf(types.ReportResourceTranslationTpl(), types.ReportResourceTranslationType, firstOkString(strconv.FormatUint(r.Res.ID, 10), r.Res.Handle))
// return
// }
func (r *Report) AddReportBlock(res *types.ReportBlock) *ReportBlock {
p := &ReportBlock{
base: &base{},
}
p.Res = res
r.Blocks = append(r.Blocks, p)
return p
}
func (r *Report) SysID() uint64 {
return r.Res.ID
}
func (r *Report) encodeTranslations() ([]*ResourceTranslation, error) {
return nil, nil
}
// FindReport looks for the workflow in the resource set
func FindReport(rr InterfaceSet, ii Identifiers) (ns *types.Report) {
var wfRes *Report
rr.Walk(func(r Interface) error {
wr, ok := r.(*Report)
if !ok {
return nil
}
if wr.Identifiers().HasAny(ii) {
wfRes = wr
}
return nil
})
// Found it
if wfRes != nil {
return wfRes.Res
}
return nil
}
func ReportErrUnresolved(ii Identifiers) error {
return fmt.Errorf("report unresolved %v", ii.StringSlice())
}

View File

@@ -0,0 +1,232 @@
package resource
import (
"fmt"
"strconv"
composeTypes "github.com/cortezaproject/corteza/server/compose/types"
"github.com/cortezaproject/corteza/server/system/types"
)
type (
base struct {
rt string
ii Identifiers
rr RefSet
ph bool
ts *Timestamps
us *Userstamps
cfg *EnvoyConfig
urefs RefSet
}
EnvoyConfig struct {
// SkipIf determines when the encoding should be skipped for this resource
SkipIf string
OnExisting MergeAlg
}
MergeAlg int
)
const (
// Default takes the operation defined default
Default MergeAlg = iota
// Skip skips the existing resource
Skip
// Replace replaces the existing resource
Replace
// MergeLeft updates the existing resource, giving priority to the existing data
MergeLeft
// MergeRight updates the existing resource, giving priority to the new data
MergeRight
)
// fn converts identifier values (string, fmt.Stringer, uint64) to string slice
//
// Each value is checked and should not be empty or zero
func identifiers(ii ...interface{}) []string {
ss := make([]string, 0, len(ii))
for _, i := range ii {
switch c := i.(type) {
case uint64:
if c == 0 {
continue
}
ss = append(ss, strconv.FormatUint(c, 10))
case fmt.Stringer:
if c.String() == "" {
continue
}
ss = append(ss, c.String())
case string:
if c == "" {
continue
}
ss = append(ss, c)
}
}
return ss
}
// Check checks if the identifier c is in the set of identifiers ii
func Check(a string, ii ...interface{}) bool {
for _, i := range ii {
switch pi := i.(type) {
case string:
if a == pi {
return true
}
case uint64:
if pi > 0 && strconv.FormatUint(pi, 10) == i {
return true
}
}
}
return false
}
// AddIdentifier adds a set of identifiers to the current resource
func (t *base) AddIdentifier(ss ...string) {
t.ii = t.ii.Add(ss...)
}
// SetIdentifier sets the identifiers to whatever was provided
func (t *base) SetIdentifier(ii Identifiers) {
t.ii = ii
}
// AddRef adds a new reference to the current resource
func (t *base) AddRef(rt string, ii ...string) *Ref {
iiC := make([]string, 0, len(ii))
for _, i := range ii {
if i != "" {
iiC = append(iiC, i)
}
}
return t.addRef(&Ref{ResourceType: rt, Identifiers: Identifiers{}.Add(iiC...)})
}
func (t *base) addRef(r *Ref) *Ref {
t.rr = append(t.rr, r)
return r
}
// ReplaceRef replaces the given ref with the new one.
// The new ref is added regardles if the old one exists or not.
func (t *base) ReplaceRef(old, new *Ref) {
t.rr = t.rr.replaceRef(old, new)
}
// SetResourceType sets the resource type of the current resource struct
func (t *base) SetResourceType(rt string) {
t.rt = rt
}
func (t *base) SetTimestamps(ts *Timestamps) {
t.ts = ts
}
func (t *base) Timestamps() *Timestamps {
return t.ts
}
func (t *base) SetUserstamps(us *Userstamps) {
t.us = us
if us != nil {
uu := []*Userstamp{us.CreatedBy, us.UpdatedBy, us.DeletedBy, us.OwnedBy, us.RunAs}
t.SetUserRefs(uu)
}
}
func (t *base) Userstamps() *Userstamps {
return t.us
}
func (t *base) SetConfig(cfg *EnvoyConfig) {
t.cfg = cfg
}
func (t *base) Config() *EnvoyConfig {
return t.cfg
}
func (t *base) SetUserRefs(uu []*Userstamp) {
if t.urefs == nil {
t.urefs = make(RefSet, 0, 4)
}
for _, u := range uu {
if u == nil {
continue
}
if u.UserID > 0 {
t.urefs = append(t.urefs, t.AddRef(types.UserResourceType, strconv.FormatUint(u.UserID, 10)))
} else if u.Ref != "" {
t.urefs = append(t.urefs, t.AddRef(types.UserResourceType, u.Ref))
}
}
}
func (t *base) UserRefs() RefSet {
return t.urefs
}
func (t *base) Identifiers() Identifiers {
return t.ii
}
func (t *base) ResourceType() string {
return t.rt
}
func (t *base) Refs() RefSet {
return t.rr
}
func (t *base) Ref() *Ref {
return &Ref{ResourceType: t.rt, Identifiers: t.ii}
}
func (t *base) HasRefs() bool {
return t.rr == nil || len(t.rr) == 0
}
// MarkPlaceholder denotes that the given resource should be treated as a placeholder
//
// Placeholder resources should not be encoded but should only provide additional
// context to resources that depend on it
func (t *base) MarkPlaceholder() {
t.ph = true
}
// Placeholder resources should not be encoded but should only provide additional
// context to resources that depend on it
func (t *base) Placeholder() bool {
return t.ph
}
func IgnoreDepResolution(ref *Ref) bool {
return ref.ResourceType == composeTypes.ModuleFieldResourceType
}
func (t *base) ReID(ii Identifiers) {
t.SetIdentifier(ii)
}
func (t *base) ReRef(old RefSet, new RefSet) {
for i, o := range old {
t.ReplaceRef(o, new[i])
}
}

View File

@@ -0,0 +1,80 @@
package resource
// This file is auto-generated.
//
// Changes to this file may cause incorrect behavior and will be lost if
// the code is regenerated.
//
import (
systemTypes "github.com/cortezaproject/corteza/server/system/types"
)
func (r *ComposeChart) EncodeTranslations() ([]*ResourceTranslation, error) {
out := make([]*ResourceTranslation, 0, 10)
rr := r.Res.EncodeTranslations()
rr.SetLanguage(defaultLanguage)
res, ref, pp := r.ResourceTranslationParts()
out = append(out, NewResourceTranslation(systemTypes.FromLocale(rr), res, ref, pp...))
tmp, err := r.encodeTranslations()
return append(out, tmp...), err
}
func (r *ComposeModule) EncodeTranslations() ([]*ResourceTranslation, error) {
out := make([]*ResourceTranslation, 0, 10)
rr := r.Res.EncodeTranslations()
rr.SetLanguage(defaultLanguage)
res, ref, pp := r.ResourceTranslationParts()
out = append(out, NewResourceTranslation(systemTypes.FromLocale(rr), res, ref, pp...))
tmp, err := r.encodeTranslations()
return append(out, tmp...), err
}
func (r *ComposeModuleField) EncodeTranslations() ([]*ResourceTranslation, error) {
out := make([]*ResourceTranslation, 0, 10)
rr := r.Res.EncodeTranslations()
rr.SetLanguage(defaultLanguage)
res, ref, pp := r.ResourceTranslationParts()
out = append(out, NewResourceTranslation(systemTypes.FromLocale(rr), res, ref, pp...))
return out, nil
}
func (r *ComposeNamespace) EncodeTranslations() ([]*ResourceTranslation, error) {
out := make([]*ResourceTranslation, 0, 10)
rr := r.Res.EncodeTranslations()
rr.SetLanguage(defaultLanguage)
res, ref, pp := r.ResourceTranslationParts()
out = append(out, NewResourceTranslation(systemTypes.FromLocale(rr), res, ref, pp...))
return out, nil
}
func (r *ComposePage) EncodeTranslations() ([]*ResourceTranslation, error) {
out := make([]*ResourceTranslation, 0, 10)
rr := r.Res.EncodeTranslations()
rr.SetLanguage(defaultLanguage)
res, ref, pp := r.ResourceTranslationParts()
out = append(out, NewResourceTranslation(systemTypes.FromLocale(rr), res, ref, pp...))
tmp, err := r.encodeTranslations()
return append(out, tmp...), err
}
func (r *ComposePageLayout) EncodeTranslations() ([]*ResourceTranslation, error) {
out := make([]*ResourceTranslation, 0, 10)
rr := r.Res.EncodeTranslations()
rr.SetLanguage(defaultLanguage)
res, ref, pp := r.ResourceTranslationParts()
out = append(out, NewResourceTranslation(systemTypes.FromLocale(rr), res, ref, pp...))
tmp, err := r.encodeTranslations()
return append(out, tmp...), err
}

View File

@@ -0,0 +1,92 @@
package resource
import (
"fmt"
"github.com/cortezaproject/corteza/server/system/types"
"golang.org/x/text/language"
)
type (
ResourceTranslation struct {
*base
Res types.ResourceTranslationSet
RefResource string
RefRes *Ref
RefPath []*Ref
Priority int
}
)
var (
defaultLanguage = language.English
)
func NewResourceTranslation(res types.ResourceTranslationSet, refResource string, refRes *Ref, refPath ...*Ref) *ResourceTranslation {
r := &ResourceTranslation{base: &base{}}
r.SetResourceType(ResourceTranslationType)
r.Res = res
r.RefResource = refResource
r.RefRes = r.addRef(refRes)
// any additional constraints
for i, rp := range refPath {
ref := MakeRef(rp.ResourceType, rp.Identifiers)
r.RefRes.Constraint(ref)
if i > 0 {
for j := i - 1; j < i; j++ {
aux := refPath[j]
ref = ref.Constraint(MakeRef(aux.ResourceType, aux.Identifiers))
}
}
r.RefPath = append(r.RefPath, r.addRef(ref))
}
return r
}
func (r *ResourceTranslation) Resource() interface{} {
return r.Res
}
func (r *ResourceTranslation) ReRef(old RefSet, new RefSet) {
r.base.ReRef(old, new)
for i, o := range old {
if o.equals(r.RefRes) {
r.RefRes = new[i]
break
}
}
for i, o := range old {
if RefSet(r.RefPath).findRef(o) > -1 {
r.RefPath = RefSet(r.RefPath).replaceRef(o, new[i])
}
}
}
func (l *ResourceTranslation) MarkDefault() {
l.Priority = 1
}
func (l *ResourceTranslation) IsDefault() bool {
return l.Priority == 1
}
func (l *ResourceTranslation) MarkGeneric() {
l.Priority = 0
}
func (l *ResourceTranslation) IsGeneric() bool {
return l.Priority == 0
}
func ResourceTranslationErrNotFound(ii Identifiers) error {
return fmt.Errorf("resource translation not found %v", ii.StringSlice())
}

View File

@@ -0,0 +1,115 @@
package resource
// This file is auto-generated.
//
// Changes to this file may cause incorrect behavior and will be lost if
// the code is regenerated.
//
import (
"fmt"
composeTypes "github.com/cortezaproject/corteza/server/compose/types"
"strings"
)
// ParseResourceTranslation generates resource setting logic for each resource
//
// Resources with "envoy: false" are skipped
//
// This function is auto-generated
func ParseResourceTranslation(res string) (string, *Ref, []*Ref, error) {
if res == "" {
return "", nil, nil, fmt.Errorf("empty resource")
}
sp := "/"
if strings.Index(res, "corteza::") == 0 {
res = res[9:]
}
res = strings.TrimSpace(res)
res = strings.TrimRight(res, sp)
rr := strings.Split(res, sp)
// only service defined (corteza::system, corteza::compose, ...)
if len(rr) == 1 {
return "", nil, nil, fmt.Errorf("only service defined: %s", res)
}
// full thing
resourceType, path := rr[0], rr[1:]
for p := 1; p < len(path); p++ {
if path[p] == "*" {
return "", nil, nil, fmt.Errorf("path wildcard not allowed for locale resources: '%s'", res)
}
}
// make the resource provide the slice of parent resources we should nest under
switch resourceType {
case composeTypes.ChartResourceTranslationType:
if len(path) != 2 {
return "", nil, nil, fmt.Errorf("expecting 2 reference components in path, got %d", len(path))
}
ref, pp, err := ComposeChartResourceTranslationReferences(
path[0],
path[1],
)
return composeTypes.ChartResourceTranslationType, ref, pp, err
case composeTypes.ModuleResourceTranslationType:
if len(path) != 2 {
return "", nil, nil, fmt.Errorf("expecting 2 reference components in path, got %d", len(path))
}
ref, pp, err := ComposeModuleResourceTranslationReferences(
path[0],
path[1],
)
return composeTypes.ModuleResourceTranslationType, ref, pp, err
case composeTypes.ModuleFieldResourceTranslationType:
if len(path) != 3 {
return "", nil, nil, fmt.Errorf("expecting 3 reference components in path, got %d", len(path))
}
ref, pp, err := ComposeModuleFieldResourceTranslationReferences(
path[0],
path[1],
path[2],
)
return composeTypes.ModuleFieldResourceTranslationType, ref, pp, err
case composeTypes.NamespaceResourceTranslationType:
if len(path) != 1 {
return "", nil, nil, fmt.Errorf("expecting 1 reference components in path, got %d", len(path))
}
ref, pp, err := ComposeNamespaceResourceTranslationReferences(
path[0],
)
return composeTypes.NamespaceResourceTranslationType, ref, pp, err
case composeTypes.PageResourceTranslationType:
if len(path) != 2 {
return "", nil, nil, fmt.Errorf("expecting 2 reference components in path, got %d", len(path))
}
ref, pp, err := ComposePageResourceTranslationReferences(
path[0],
path[1],
)
return composeTypes.PageResourceTranslationType, ref, pp, err
case composeTypes.PageLayoutResourceTranslationType:
if len(path) != 3 {
return "", nil, nil, fmt.Errorf("expecting 3 reference components in path, got %d", len(path))
}
ref, pp, err := ComposePageLayoutResourceTranslationReferences(
path[0],
path[1],
path[2],
)
return composeTypes.PageLayoutResourceTranslationType, ref, pp, err
}
// return unhandled resource as-is
return resourceType, nil, nil, nil
}

View File

@@ -0,0 +1,25 @@
package resource
// 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:
// - automation.workflow.yaml
import (
"github.com/cortezaproject/corteza/server/automation/types"
)
// AutomationWorkflowResourceTranslationReferences generates Locale references
//
// Resources with "envoy: false" are skipped
//
// This function is auto-generated
func AutomationWorkflowResourceTranslationReferences(workflow string) (res *Ref, pp []*Ref, err error) {
res = &Ref{ResourceType: types.WorkflowResourceType, Identifiers: MakeIdentifiers(workflow)}
return
}

View File

@@ -0,0 +1,72 @@
package resource
// This file is auto-generated.
//
// Changes to this file may cause incorrect behavior and will be lost if
// the code is regenerated.
//
import (
"github.com/cortezaproject/corteza/server/compose/types"
)
// ComposeChartResourceTranslationReferences generates Locale references
//
// This function is auto-generated
func ComposeChartResourceTranslationReferences(namespaceID string, self string) (res *Ref, pp []*Ref, err error) {
res = &Ref{ResourceType: types.ChartResourceType, Identifiers: MakeIdentifiers(self)}
pp = append(pp, &Ref{ResourceType: types.NamespaceResourceType, Identifiers: MakeIdentifiers(namespaceID)})
return
}
// ComposeModuleResourceTranslationReferences generates Locale references
//
// This function is auto-generated
func ComposeModuleResourceTranslationReferences(namespaceID string, self string) (res *Ref, pp []*Ref, err error) {
res = &Ref{ResourceType: types.ModuleResourceType, Identifiers: MakeIdentifiers(self)}
pp = append(pp, &Ref{ResourceType: types.NamespaceResourceType, Identifiers: MakeIdentifiers(namespaceID)})
return
}
// ComposeModuleFieldResourceTranslationReferences generates Locale references
//
// This function is auto-generated
func ComposeModuleFieldResourceTranslationReferences(namespaceID string, moduleID string, self string) (res *Ref, pp []*Ref, err error) {
res = &Ref{ResourceType: types.ModuleFieldResourceType, Identifiers: MakeIdentifiers(self)}
pp = append(pp, &Ref{ResourceType: types.NamespaceResourceType, Identifiers: MakeIdentifiers(namespaceID)})
pp = append(pp, &Ref{ResourceType: types.ModuleResourceType, Identifiers: MakeIdentifiers(moduleID)})
return
}
// ComposeNamespaceResourceTranslationReferences generates Locale references
//
// This function is auto-generated
func ComposeNamespaceResourceTranslationReferences(self string) (res *Ref, pp []*Ref, err error) {
res = &Ref{ResourceType: types.NamespaceResourceType, Identifiers: MakeIdentifiers(self)}
return
}
// ComposePageResourceTranslationReferences generates Locale references
//
// This function is auto-generated
func ComposePageResourceTranslationReferences(namespaceID string, self string) (res *Ref, pp []*Ref, err error) {
res = &Ref{ResourceType: types.PageResourceType, Identifiers: MakeIdentifiers(self)}
pp = append(pp, &Ref{ResourceType: types.NamespaceResourceType, Identifiers: MakeIdentifiers(namespaceID)})
return
}
// ComposePageLayoutResourceTranslationReferences generates Locale references
//
// This function is auto-generated
func ComposePageLayoutResourceTranslationReferences(namespaceID string, pageID string, self string) (res *Ref, pp []*Ref, err error) {
res = &Ref{ResourceType: types.PageLayoutResourceType, Identifiers: MakeIdentifiers(self)}
pp = append(pp, &Ref{ResourceType: types.NamespaceResourceType, Identifiers: MakeIdentifiers(namespaceID)})
pp = append(pp, &Ref{ResourceType: types.PageResourceType, Identifiers: MakeIdentifiers(pageID)})
return
}

View File

@@ -0,0 +1,73 @@
package resource
import (
"fmt"
"strconv"
"github.com/cortezaproject/corteza/server/system/types"
)
type (
// Role represents a Role
Role struct {
*base
Res *types.Role
}
)
func NewRole(rl *types.Role) *Role {
r := &Role{base: &base{}}
r.SetResourceType(types.RoleResourceType)
r.Res = rl
r.AddIdentifier(identifiers(rl.Handle, rl.Name, rl.ID)...)
// Initial timestamps
r.SetTimestamps(MakeTimestampsCUDA(&rl.CreatedAt, rl.UpdatedAt, rl.DeletedAt, rl.ArchivedAt))
return r
}
func (r *Role) Resource() interface{} {
return r.Res
}
func (r *Role) SysID() uint64 {
return r.Res.ID
}
func (r *Role) RBACParts() (resource string, ref *Ref, path []*Ref) {
ref = r.Ref()
path = nil
resource = fmt.Sprintf(types.RoleRbacResourceTpl(), types.RoleResourceType, firstOkString(strconv.FormatUint(r.Res.ID, 10), r.Res.Handle))
return
}
// FindRole looks for the role in the resources
func FindRole(rr InterfaceSet, ii Identifiers) (rl *types.Role) {
var rlRes *Role
rr.Walk(func(r Interface) error {
rr, ok := r.(*Role)
if !ok {
return nil
}
if rr.Identifiers().HasAny(ii) {
rlRes = rr
}
return nil
})
// Found it
if rlRes != nil {
return rlRes.Res
}
return nil
}
func RoleErrUnresolved(ii Identifiers) error {
return fmt.Errorf("role unresolved %v", ii.StringSlice())
}

View File

@@ -0,0 +1,57 @@
package resource
import (
"github.com/cortezaproject/corteza/server/system/types"
)
type (
Settings struct {
*base
Res types.SettingValueSet
}
Setting struct {
*base
Res types.SettingValue
}
)
func NewSettings(vv map[string]interface{}) *Settings {
r := &Settings{base: &base{}}
r.SetResourceType(SettingsResourceType)
r.Res = make(types.SettingValueSet, 0, len(vv))
for k, v := range vv {
sv := &types.SettingValue{
Name: k,
}
sv.SetSetting(v)
r.Res = append(r.Res, sv)
}
return r
}
func NewSetting(s *types.SettingValue) *Setting {
r := &Setting{base: &base{}}
r.SetResourceType(SettingsResourceType)
r.Res = *s
r.AddIdentifier(identifiers(s.Name)...)
// Set initial stamps
r.SetTimestamps(MakeTimestampsCUDA(nil, &s.UpdatedAt, nil, nil))
r.SetUserstamps(&Userstamps{
UpdatedBy: &Userstamp{UserID: s.UpdatedBy},
})
return r
}
func (r *Setting) Resource() interface{} {
return r.Res
}
func (r *Settings) Resource() interface{} {
return r.Res
}

View File

@@ -0,0 +1,64 @@
package resource
import "fmt"
type (
shaper interface {
Shape([]Interface) ([]Interface, error)
}
)
// Shape shapes ResourceDatasets based on their correlated Template
//
// During the shaping step, no data is removed, nor added to the original resource slice.
// The shapers are ran in the same order they were provided.
func Shape(rr []Interface, ss ...shaper) ([]Interface, error) {
ii := make([]Interface, 0, int(len(rr)/2))
// Firstly handle any resource shaping
for _, s := range ss {
sdd, err := s.Shape(rr)
if err != nil {
return nil, err
}
ii = append(ii, sdd...)
}
// Cleanup the final shaped resource slice.
// @todo make this cleaner!!1!'
//
// After this point, all of the data should be ready for further processing.
for _, r := range rr {
if _, ok := r.(*ComposeRecordTemplate); ok {
continue
}
if _, ok := r.(*ResourceDataset); ok {
continue
}
ii = append(ii, r)
}
return ii, nil
}
// findResourceDataset finds and returns the first dataset that matches the
// provided identifiers.
func findResourceDataset(rr []Interface, ii Identifiers) *ResourceDataset {
for _, r := range rr {
genR, ok := r.(*ResourceDataset)
if !ok {
continue
}
if !genR.Identifiers().HasAny(ii) {
continue
}
return genR
}
return nil
}
func genericSourceErrUnresolved(ii Identifiers) error {
return fmt.Errorf("generic source unresolved %v", ii.StringSlice())
}

View File

@@ -0,0 +1,73 @@
package resource
import (
"fmt"
"strconv"
"github.com/cortezaproject/corteza/server/system/types"
)
type (
// Template represents a Template
Template struct {
*base
Res *types.Template
}
)
func NewTemplate(t *types.Template) *Template {
r := &Template{base: &base{}}
r.SetResourceType(types.TemplateResourceType)
r.Res = t
r.AddIdentifier(identifiers(t.Handle, t.Meta.Short, t.ID)...)
// Initial timestamps
r.SetTimestamps(MakeTimestampsCUDAS(&t.CreatedAt, t.UpdatedAt, t.DeletedAt, nil, nil))
return r
}
func (r *Template) Resource() interface{} {
return r.Res
}
func (r *Template) SysID() uint64 {
return r.Res.ID
}
func (r *Template) RBACParts() (resource string, ref *Ref, path []*Ref) {
ref = r.Ref()
path = nil
resource = fmt.Sprintf(types.TemplateRbacResourceTpl(), types.TemplateResourceType, firstOkString(strconv.FormatUint(r.Res.ID, 10), r.Res.Handle))
return
}
// FindTemplate looks for the template in the resources
func FindTemplate(rr InterfaceSet, ii Identifiers) (u *types.Template) {
var tRes *Template
rr.Walk(func(r Interface) error {
tr, ok := r.(*Template)
if !ok {
return nil
}
if tr.Identifiers().HasAny(ii) {
tRes = tr
}
return nil
})
// Found it
if tRes != nil {
return tRes.Res
}
return nil
}
func TemplateErrUnresolved(ii Identifiers) error {
return fmt.Errorf("template unresolved %v", ii.StringSlice())
}

View File

@@ -0,0 +1,119 @@
package resource
import (
"encoding/json"
"time"
)
type (
Timestamp struct {
// S is a stringified representation of the timestamp
S string
// T is a time representation of the timestamp
T *time.Time
// Tz is the timezone the timestamp will be encoded into
Tz string
// Tpl is the template the timestamp will be encoded into
Tpl string
}
Timestamps struct {
CreatedAt *Timestamp
UpdatedAt *Timestamp
DeletedAt *Timestamp
ArchivedAt *Timestamp
SuspendedAt *Timestamp
}
)
func (ts *Timestamp) MarshalYAML() (interface{}, error) {
return ts.S, nil
}
func (ts *Timestamp) MarshalJSON() ([]byte, error) {
return json.Marshal(ts.S)
}
// MakeTimestamp initializes a Timestamp from the passed timestamp
func MakeTimestamp(ts string) *Timestamp {
t := toTime(ts)
if t == nil {
return nil
}
return &Timestamp{
S: ts,
T: t,
}
}
// MakeTimestampsCUDA initializes timestamps for createdAt, updatedAt, deletedAt and archivedAt
func MakeTimestampsCUDA(c, u, d, a *time.Time) *Timestamps {
t := &Timestamps{}
if c != nil && !c.IsZero() {
t.CreatedAt = &Timestamp{T: c}
}
if u != nil && !u.IsZero() {
t.UpdatedAt = &Timestamp{T: u}
}
if d != nil && !d.IsZero() {
t.DeletedAt = &Timestamp{T: d}
}
if a != nil && !a.IsZero() {
t.ArchivedAt = &Timestamp{T: a}
}
return t
}
// MakeTimestampsCUDAS initializes timestamps for createdAt, updatedAt, deletedAt, archivedAt and suspendedAt
func MakeTimestampsCUDAS(c, u, d, a, s *time.Time) *Timestamps {
t := MakeTimestampsCUDA(c, u, d, a)
if s != nil && !s.IsZero() {
t.SuspendedAt = &Timestamp{T: s}
}
return t
}
// Model stringifies the timestamps based on the passed template and timezone
func (tt *Timestamps) Model(tpl string, tz string) (*Timestamps, error) {
var err error
if tt.CreatedAt != nil {
tt.CreatedAt, err = tt.CreatedAt.Model(tpl, tz)
}
if tt.UpdatedAt != nil {
tt.UpdatedAt, err = tt.UpdatedAt.Model(tpl, tz)
}
if tt.DeletedAt != nil {
tt.DeletedAt, err = tt.DeletedAt.Model(tpl, tz)
}
if tt.ArchivedAt != nil {
tt.ArchivedAt, err = tt.ArchivedAt.Model(tpl, tz)
}
if tt.SuspendedAt != nil {
tt.SuspendedAt, err = tt.SuspendedAt.Model(tpl, tz)
}
return tt, err
}
// Model stringifies the timestamp based on the passed template and timezone
func (ts *Timestamp) Model(tpl string, tz string) (*Timestamp, error) {
if tz != "" {
tzL, err := time.LoadLocation(tz)
if err != nil {
return nil, err
}
t := ts.T.In(tzL)
ts.T = &t
}
if tpl == "" {
tpl = time.RFC3339
}
ts.S = ts.T.Format(tpl)
return ts, nil
}

View File

@@ -0,0 +1,343 @@
package resource
import (
"strconv"
)
type (
Interface interface {
Identifiers() Identifiers
ResourceType() string
Resource() interface{}
Refs() RefSet
MarkPlaceholder()
Placeholder() bool
ReID(Identifiers)
ReRef(old RefSet, new RefSet)
}
ErrorWrapper interface {
WrapError(act string, err error) error
}
InterfaceSet []Interface
IdentifiableInterface interface {
Interface
SysID() uint64
}
RefableInterface interface {
Interface
Ref() string
}
RBACInterface interface {
Interface
RBACParts() (string, *Ref, []*Ref)
}
LocaleInterface interface {
Interface
ResourceTranslationParts() (string, *Ref, []*Ref)
EncodeTranslations() ([]*ResourceTranslation, error)
}
PrunableInterface interface {
Interface
Prune(*Ref)
}
RefSet []*Ref
Ref struct {
// @todo check with Denis regarding strings here (the cdocs comment)
// @todo should this become node type instead?
ResourceType string
Identifiers Identifiers
Constraints RefSet
}
Identifiers []string
)
var (
DataSourceResourceType = "data:raw"
SettingsResourceType = "setting"
RbacResourceType = "rbac-rule"
ResourceTranslationType = "resource-translation"
)
func MakeRef(rt string, ii Identifiers) *Ref {
return &Ref{ResourceType: rt, Identifiers: ii}
}
func MakeWildRef(rt string) *Ref {
return &Ref{ResourceType: rt, Identifiers: MakeIdentifiers("*")}
}
func MakeIdentifiers(ss ...string) Identifiers {
ii := make(Identifiers, 0, len(ss))
ii = ii.Add(ss...)
return ii
}
func (ri Identifiers) Add(ii ...string) Identifiers {
for _, i := range ii {
if len(i) > 0 {
ri = append(ri, i)
}
}
return ri
}
func (ri Identifiers) Clone() Identifiers {
out := make(Identifiers, 0, len(ri))
out = append(out, ri...)
return out
}
func (ri Identifiers) HasAny(check Identifiers) bool {
// The size of these will be tiny so no need for hashmaps
for _, i := range ri {
for _, j := range check {
if i == j {
return true
}
}
}
return false
}
func (ri Identifiers) StringSlice() []string {
return ri
}
func (ri Identifiers) First() string {
if len(ri) == 0 {
return ""
}
return ri[0]
}
func (ri Identifiers) FirstID() uint64 {
if len(ri) <= 0 {
return 0
}
for _, s := range ri {
if v, err := strconv.ParseUint(s, 10, 64); err != nil {
continue
} else {
return v
}
}
return 0
}
func (rr InterfaceSet) Walk(f func(r Interface) error) (err error) {
for _, r := range rr {
err = f(r)
if err != nil {
return
}
}
return nil
}
// SearchForIdentifiers returns the resources where the provided identifiers exist
//
// The Resource is matching if at least one identifier matches.
func (rr InterfaceSet) SearchForIdentifiers(resource string, ii Identifiers) (out InterfaceSet) {
out = make(InterfaceSet, 0, len(rr)/2)
for _, r := range rr {
if r.ResourceType() != resource {
continue
}
if r.Identifiers().HasAny(ii) {
out = append(out, r)
}
}
return
}
// SearchForReferences returns the resources where the provided references exist
//
// The Resource is matching if at least one reference matches.
func (rr InterfaceSet) SearchForReferences(ref *Ref) (out InterfaceSet) {
out = make(InterfaceSet, 0, len(rr)/2)
for _, r := range rr {
if r.Refs().HasRef(ref) {
out = append(out, r)
}
}
return
}
// Constraint returns the current reference with added constraint
func (r *Ref) Constraint(c *Ref) *Ref {
if r.Constraints == nil {
r.Constraints = make(RefSet, 0, 1)
}
r.Constraints = append(r.Constraints, &Ref{
ResourceType: c.ResourceType,
Identifiers: MakeIdentifiers(c.Identifiers.StringSlice()...),
})
return r
}
// IsWildcard checks if this Ref points to all resources of a specific resource type
func (r *Ref) IsWildcard() bool {
if len(r.Identifiers) == 0 {
return false
}
for _, i := range r.Identifiers {
if i == "*" {
return true
}
}
return false
}
func (a *Ref) equals(b *Ref) bool {
if a == nil && a == b {
return true
}
if a == nil && a != b {
return false
}
if a.ResourceType != b.ResourceType {
return false
}
if !b.IsWildcard() && !a.Identifiers.HasAny(b.Identifiers) {
return false
}
for _, c := range b.Constraints {
if !a.Constraints.HasRef(c) {
return false
}
}
return true
}
func (rr RefSet) findRef(ref *Ref) int {
for i, r := range rr {
if r.equals(ref) {
return i
}
}
return -1
}
// replaceRef replaces the reference both on the ref level and on the
// constraint level.
func (rr RefSet) replaceRef(old, new *Ref) RefSet {
found := false
for x := len(rr) - 1; x >= 0; x-- {
r := rr[x]
if r.equals(old) {
found = true
if new == nil {
rr[x] = rr[len(rr)-1]
return rr[:len(rr)-1]
} else {
rr[x] = new
}
} else {
if len(r.Constraints) > 0 {
r.Constraints = r.Constraints.replaceRef(old, new)
}
}
}
if !found && new != nil {
return append(rr, new)
}
return rr
}
func (rr RefSet) HasRef(ref *Ref) bool {
return rr.findRef(ref) > -1
}
// IsSbubset checks if aa is subset of bb
//
// Order is important; primary use is RBAC and res. tr. path checking.
func (aa RefSet) IsSubset(bb RefSet) bool {
if len(bb) > len(aa) {
return false
}
return aa[0:len(bb)].Equals(bb)
}
// Equals checks if two RefSets match
//
// They match if everything matches, even constraints (important)
func (aa RefSet) Equals(bb RefSet) bool {
if len(aa) != len(bb) {
return false
}
for i, a := range aa {
if !a.equals(bb[i]) {
return false
}
}
return true
}
// Unique returns only unique references
//
// Uniqueness is defined as "two references may not define
// the same resource type and identifier" combinations.
func (rr RefSet) Unique() RefSet {
out := make(RefSet, 0, len(rr))
seen := make(map[string]Identifiers)
for _, r := range rr {
ii, ok := seen[r.ResourceType]
// type not seen at all, unique
if !ok {
out = append(out, r)
seen[r.ResourceType] = r.Identifiers.Clone()
continue
}
// not yet seen
if !ii.HasAny(r.Identifiers) {
out = append(out, r)
seen[r.ResourceType] = seen[r.ResourceType].Add(r.Identifiers...)
}
}
return out
}

View File

@@ -0,0 +1,96 @@
package resource
import (
"fmt"
"strconv"
"github.com/cortezaproject/corteza/server/system/types"
)
type (
// User represents a User
User struct {
*base
Res *types.User
RoleMembership []Identifiers
RefRoles RefSet
}
)
func NewUser(u *types.User, roles ...string) *User {
r := &User{base: &base{}}
r.SetResourceType(types.UserResourceType)
r.Res = u
r.AddIdentifier(identifiers(u.Handle, u.Email, u.Name, u.ID)...)
// Role membership
for _, role := range roles {
rid := MakeIdentifiers(role)
r.RoleMembership = append(r.RoleMembership, rid)
r.RefRoles = append(r.RefRoles, r.AddRef(types.RoleResourceType, role))
}
// Initial timestamps
r.SetTimestamps(MakeTimestampsCUDAS(&u.CreatedAt, u.UpdatedAt, u.DeletedAt, nil, u.SuspendedAt))
return r
}
func (u *User) AddRoles(roles ...*types.Role) *User {
for _, r := range roles {
idf := firstOkString(r.Handle, strconv.FormatUint(r.ID, 10))
rid := MakeIdentifiers(idf)
u.RoleMembership = append(u.RoleMembership, rid)
u.RefRoles = append(u.RefRoles, u.AddRef(types.RoleResourceType, idf))
}
return u
}
func (r *User) Resource() interface{} {
return r.Res
}
func (r *User) SysID() uint64 {
return r.Res.ID
}
func (r *User) RBACParts() (resource string, ref *Ref, path []*Ref) {
ref = r.Ref()
path = nil
resource = fmt.Sprintf(types.UserRbacResourceTpl(), types.UserResourceType, firstOkString(strconv.FormatUint(r.Res.ID, 10), r.Res.Handle, r.Res.Username))
return
}
// FindUser looks for the user in the resources
func FindUser(rr InterfaceSet, ii Identifiers) (u *types.User) {
var uRes *User
rr.Walk(func(r Interface) error {
ur, ok := r.(*User)
if !ok {
return nil
}
if ur.Identifiers().HasAny(ii) {
uRes = ur
}
return nil
})
// Found it
if uRes != nil {
return uRes.Res
}
return nil
}
func UserErrUnresolved(ii Identifiers) error {
return fmt.Errorf("user unresolved %v", ii.StringSlice())
}

View File

@@ -0,0 +1,242 @@
package resource
import (
"encoding/json"
"fmt"
"strconv"
"github.com/cortezaproject/corteza/server/system/types"
)
type (
Userstamp struct {
UserID uint64
Ref string
U *types.User
// S is the stringified user representation
S string
}
Userstamps struct {
CreatedBy *Userstamp
UpdatedBy *Userstamp
DeletedBy *Userstamp
OwnedBy *Userstamp
RunAs *Userstamp
}
UserstampIndex map[string]*Userstamp
)
func (us *Userstamp) MarshalYAML() (interface{}, error) {
if us == nil {
return nil, nil
}
if us.U != nil {
if us.U.Handle != "" {
return us.U.Handle, nil
}
if us.U.Username != "" {
return us.U.Username, nil
}
if us.U.Email != "" {
return us.U.Email, nil
}
if us.U.Name != "" {
return us.U.Name, nil
}
}
if us.Ref != "" {
return us.Ref, nil
}
if us.UserID > 0 {
return us.UserID, nil
}
return nil, fmt.Errorf("invalid userstamp")
}
func (us *Userstamp) MarshalJSON() ([]byte, error) {
if us == nil {
return nil, nil
}
l := ""
if us.U != nil {
if us.U.Handle != "" {
l = us.U.Handle
}
if us.U.Username != "" {
l = us.U.Username
}
if us.U.Email != "" {
l = us.U.Email
}
if us.U.Name != "" {
l = us.U.Name
}
} else {
if us.Ref != "" {
l = us.Ref
}
if us.UserID > 0 {
l = strconv.FormatUint(us.UserID, 10)
}
}
if l == "" {
return nil, fmt.Errorf("invalid userstamp")
}
return json.Marshal(l)
}
// MakeUserstamp initializes a userstamp from the passed user struct
func MakeUserstamp(u *types.User) *Userstamp {
sID := strconv.FormatUint(u.ID, 10)
return &Userstamp{
UserID: u.ID,
U: u,
Ref: firstOkString(u.Handle, u.Email, u.Username, sID),
}
}
// MakeUserstampFromRef initializes a userstamp from the passed reference
//
// If possible, the function determines the userID
func MakeUserstampFromRef(ref string) *Userstamp {
id, err := strconv.ParseUint(ref, 10, 64)
us := &Userstamp{}
if err == nil && id != 0 {
us.UserID = id
us.U = &types.User{ID: id}
}
us.Ref = ref
return us
}
// MakeUserstampFromRef initializes a userstamp from the passed userID
func MakeUserstampFromID(ID uint64) *Userstamp {
if ID == 0 {
return nil
}
return MakeUserstampFromRef(strconv.FormatUint(ID, 10))
}
// MakeUserstampsCUDO initializes userstamps for createdAt, updatedAt, deletedAt and ownedBy
func MakeUserstampsCUDO(c, u, d, o uint64) *Userstamps {
us := &Userstamps{}
if c > 0 {
us.CreatedBy = &Userstamp{
UserID: c,
Ref: strconv.FormatUint(c, 10),
}
}
if u > 0 {
us.UpdatedBy = &Userstamp{
UserID: u,
Ref: strconv.FormatUint(u, 10),
}
}
if d > 0 {
us.DeletedBy = &Userstamp{
UserID: d,
Ref: strconv.FormatUint(d, 10),
}
}
if o > 0 {
us.OwnedBy = &Userstamp{
UserID: o,
Ref: strconv.FormatUint(o, 10),
}
}
return us
}
// Model stringifies the userstamp
//
// @todo make this configurable
func (us *Userstamp) Model() (string, error) {
if us == nil {
return "", nil
}
if us.U != nil {
if us.U.Handle != "" {
return us.U.Handle, nil
}
if us.U.Username != "" {
return us.U.Username, nil
}
if us.U.Email != "" {
return us.U.Email, nil
}
if us.U.Name != "" {
return us.U.Name, nil
}
}
if us.Ref != "" {
return us.Ref, nil
}
if us.UserID > 0 {
return strconv.FormatUint(us.UserID, 10), nil
}
return "", fmt.Errorf("invalid userstamp")
}
func (ux UserstampIndex) Add(uu ...*types.User) {
for _, u := range uu {
sID := strconv.FormatUint(u.ID, 10)
s := MakeUserstamp(u)
ux[sID] = s
ux[u.Email] = s
if u.Handle != "" {
ux[u.Handle] = s
}
if u.Username != "" {
ux[u.Username] = s
}
if u.Name != "" {
ux[u.Name+" "+u.Email] = s
}
}
}
func (ux UserstampIndex) GetByKey(kr interface{}) *Userstamp {
if k, ok := kr.(string); ok {
return ux[k]
} else if k, ok := kr.(uint64); ok {
return ux[strconv.FormatUint(k, 10)]
}
return nil
}
func (ux UserstampIndex) GetByStamp(s *Userstamp) *Userstamp {
if s == nil {
return nil
}
if s.Ref != "" {
return ux.GetByKey(s.Ref)
}
if s.UserID > 0 {
return ux.GetByKey(s.UserID)
}
if s.U != nil && s.U.ID > 0 {
return ux.GetByKey(s.U.ID)
}
return s
}

View File

@@ -0,0 +1,80 @@
package resource
import (
"time"
"github.com/cortezaproject/corteza/server/compose/types"
systemTypes "github.com/cortezaproject/corteza/server/system/types"
)
func firstOkString(ss ...string) string {
for _, s := range ss {
if s != "" && s != "0" {
return s
}
}
return ""
}
// Taken (and modified) from compose/service/values/sanitizer.go
func toTime(v string) *time.Time {
ff := []string{
time.RFC3339,
time.RFC1123Z,
time.RFC1123,
time.RFC850,
time.RFC822Z,
time.RFC822,
time.RubyDate,
time.UnixDate,
time.ANSIC,
"2006/_1/_2 15:04:05",
"2006/_1/_2 15:04",
}
for _, f := range ff {
parsed, err := time.Parse(f, v)
if err == nil {
return &parsed
}
}
return nil
}
// Ref builders
func MakeNamespaceRef(id uint64, handle, name string) *Ref {
return makeGenericRef(types.NamespaceResourceType, id, handle, name)
}
func MakeModuleRef(id uint64, handle, name string) *Ref {
return makeGenericRef(types.ModuleResourceType, id, handle, name)
}
func MakePageRef(id uint64, handle, title string) *Ref {
return makeGenericRef(types.PageResourceType, id, handle, title)
}
func MakeRoleRef(id uint64, handle, name string) *Ref {
return makeGenericRef(systemTypes.RoleResourceType, id, handle, name)
}
func makeGenericRef(t string, id uint64, ii ...string) *Ref {
args := make([]interface{}, len(ii)+1)
args[0] = id
for i := range ii {
args[i+1] = ii[i]
}
aux := &Ref{
ResourceType: t,
Identifiers: identifiers(args...),
}
if t == "" || len(aux.Identifiers) == 0 {
return nil
}
return aux
}

View File

@@ -0,0 +1,19 @@
package envoy
import "github.com/cortezaproject/corteza/server/pkg/envoy/resource"
// procResSet is a little utility to run some op over given resources
//
// Helps cover special cases such as modules & module fields
func procResSet(resources resource.InterfaceSet, fn func(r resource.Interface)) {
for _, res := range resources {
fn(res)
// Special case for modules since it has
if modR, ok := res.(*resource.ComposeModule); ok {
for _, f := range modR.ResFields {
fn(f)
}
}
}
}

View File

@@ -0,0 +1,113 @@
package store
import (
"context"
"github.com/cortezaproject/corteza/server/pkg/envoy/resource"
"github.com/cortezaproject/corteza/server/store"
"github.com/cortezaproject/corteza/server/system/types"
)
type (
apiGateway struct {
cfg *EncoderConfig
res *resource.APIGateway
gwr *types.ApigwRoute
ff types.ApigwFilterSet
ux *userIndex
}
apiGatewaySet []*apiGateway
apiGatewayFilter struct {
cfg *EncoderConfig
res *resource.APIGatewayFilter
tr *types.ApigwFilter
}
apiGatewayFilterSet []*apiGatewayFilter
)
// mergeAPIGateways merges b into a, prioritising a
func mergeAPIGateways(a, b *types.ApigwRoute) *types.ApigwRoute {
c := a
if c.Endpoint == "" {
c.Endpoint = b.Endpoint
}
if c.Method == "" {
c.Method = b.Method
}
c.Enabled = b.Enabled
if c.Group == 0 {
c.Group = b.Group
}
c.Meta = b.Meta
if c.CreatedBy == 0 {
c.CreatedBy = b.CreatedBy
}
if c.UpdatedBy == 0 {
c.UpdatedBy = b.UpdatedBy
}
if c.DeletedBy == 0 {
c.DeletedBy = b.DeletedBy
}
if c.CreatedAt.IsZero() {
c.CreatedAt = b.CreatedAt
}
if c.UpdatedAt == nil {
c.UpdatedAt = b.UpdatedAt
}
if c.DeletedAt == nil {
c.DeletedAt = b.DeletedAt
}
return c
}
// findAPIGateway looks for the workflow in the resources & the store
//
// Provided resources are prioritized.
func findAPIGateway(ctx context.Context, s store.Storer, rr resource.InterfaceSet, ii resource.Identifiers) (wf *types.ApigwRoute, err error) {
wf = resource.FindAPIGateway(rr, ii)
if wf != nil {
return wf, nil
}
return findAPIGatewayStore(ctx, s, makeGenericFilter(ii))
}
// findAPIGatewayStore looks for the workflow in the store
func findAPIGatewayStore(ctx context.Context, s store.Storer, gf genericFilter) (wf *types.ApigwRoute, err error) {
if gf.id > 0 {
wf, err = store.LookupApigwRouteByID(ctx, s, gf.id)
if err != nil && err != store.ErrNotFound {
return nil, err
}
if wf != nil {
return
}
}
for _, i := range gf.identifiers {
wf, err = store.LookupApigwRouteByEndpoint(ctx, s, i)
if err != nil && err != store.ErrNotFound {
return nil, err
}
if wf != nil {
return
}
}
return nil, nil
}

View File

@@ -0,0 +1,215 @@
package store
import (
"context"
"github.com/cortezaproject/corteza/server/pkg/envoy/resource"
"github.com/cortezaproject/corteza/server/store"
"github.com/cortezaproject/corteza/server/system/types"
)
func newAPIGatewayFromResource(res *resource.APIGateway, cfg *EncoderConfig) resourceState {
return &apiGateway{
cfg: mergeConfig(cfg, res.Config()),
res: res,
}
}
func (n *apiGateway) Prepare(ctx context.Context, pl *payload) (err error) {
err = n.prepareRoute(ctx, pl)
if err != nil {
return err
}
return n.prepareFilters(ctx, pl)
}
func (n *apiGateway) prepareRoute(ctx context.Context, pl *payload) (err error) {
// Reset old identifiers
n.res.Res.ID = 0
// Try to get the original workflow
n.gwr, err = findAPIGatewayStore(ctx, pl.s, makeGenericFilter(n.res.Identifiers()))
if err != nil {
return err
}
if n.gwr != nil {
n.res.Res.ID = n.gwr.ID
}
return nil
}
func (n *apiGateway) prepareFilters(ctx context.Context, pl *payload) (err error) {
if n.gwr == nil || n.gwr.ID == 0 {
return nil
}
// Reset old identifiers
for _, rf := range n.res.Filters {
rf.Res.ID = 0
}
// Try to find any related filters for this route
tt, _, err := store.SearchApigwFilters(ctx, pl.s, types.ApigwFilterFilter{
RouteID: n.gwr.ID,
})
if err != nil {
return err
}
n.ff = tt
return nil
}
func (n *apiGateway) Encode(ctx context.Context, pl *payload) (err error) {
err = n.encodeRoute(ctx, pl)
if err != nil {
return err
}
return n.encodeFilters(ctx, pl)
}
func (n *apiGateway) encodeRoute(ctx context.Context, pl *payload) (err error) {
res := n.res.Res
exists := n.gwr != nil && n.gwr.ID > 0
// Determine the ID
if res.ID <= 0 && exists {
res.ID = n.gwr.ID
}
if res.ID <= 0 {
res.ID = NextID()
}
// Sys users
us, err := resolveUserstamps(ctx, pl.s, pl.state.ParentResources, n.res.Userstamps())
if err != nil {
return err
}
ts := n.res.Timestamps()
if ts != nil {
if ts.CreatedAt != nil {
res.CreatedAt = *ts.CreatedAt.T
} else {
res.CreatedAt = *now()
}
if ts.UpdatedAt != nil {
res.UpdatedAt = ts.UpdatedAt.T
}
if ts.DeletedAt != nil {
res.DeletedAt = ts.DeletedAt.T
}
}
res.CreatedBy = pl.invokerID
if us != nil {
if us.CreatedBy != nil {
res.CreatedBy = us.CreatedBy.UserID
}
if us.UpdatedBy != nil {
res.UpdatedBy = us.UpdatedBy.UserID
}
if us.DeletedBy != nil {
res.DeletedBy = us.DeletedBy.UserID
}
}
// Evaluate the resource skip expression
// @todo expand available parameters; similar implementation to automation/types/record@Dict
if skip, err := basicSkipEval(ctx, n.cfg, !exists); err != nil {
return err
} else if skip {
return nil
}
// Create a fresh workflow
if !exists {
return store.CreateApigwRoute(ctx, pl.s, res)
}
// Update existing workflow
switch n.cfg.OnExisting {
case resource.Skip:
return nil
case resource.MergeLeft:
res = mergeAPIGateways(n.gwr, res)
case resource.MergeRight:
res = mergeAPIGateways(res, n.gwr)
}
err = store.UpdateApigwRoute(ctx, pl.s, res)
if err != nil {
return err
}
n.res.Res = res
return nil
}
func (n *apiGateway) encodeFilters(ctx context.Context, pl *payload) (err error) {
exists := len(n.ff) > 0
ff := make([]*types.ApigwFilter, 0, len(n.res.Filters))
for _, rf := range n.res.Filters {
res := rf.Res
res.Route = n.res.Res.ID
res.ID = NextID()
// Sys users
us, err := resolveUserstamps(ctx, pl.s, pl.state.ParentResources, rf.Userstamps())
if err != nil {
return err
}
ts := rf.Timestamps()
if ts != nil {
if ts.CreatedAt != nil {
res.CreatedAt = *ts.CreatedAt.T
} else {
res.CreatedAt = *now()
}
if ts.UpdatedAt != nil {
res.UpdatedAt = ts.UpdatedAt.T
}
if ts.DeletedAt != nil {
res.DeletedAt = ts.DeletedAt.T
}
}
res.CreatedBy = pl.invokerID
if us != nil {
if us.CreatedBy != nil {
res.CreatedBy = us.CreatedBy.UserID
}
if us.UpdatedBy != nil {
res.UpdatedBy = us.UpdatedBy.UserID
}
if us.DeletedBy != nil {
res.DeletedBy = us.DeletedBy.UserID
}
}
ff = append(ff, res)
}
// Create a fresh workflow
if !exists {
return store.CreateApigwFilter(ctx, pl.s, ff...)
}
// If these filters already exist and we wish to modify them,
// remove the old ones and create new ones
switch n.cfg.OnExisting {
case resource.Skip,
resource.MergeLeft:
return nil
}
err = store.DeleteApigwFilter(ctx, pl.s, n.ff...)
return store.CreateApigwFilter(ctx, pl.s, ff...)
}

View File

@@ -0,0 +1,30 @@
package store
import (
"github.com/cortezaproject/corteza/server/pkg/envoy"
"github.com/cortezaproject/corteza/server/pkg/envoy/resource"
"github.com/cortezaproject/corteza/server/system/types"
)
func newAPIGateway(gwr *types.ApigwRoute, ff types.ApigwFilterSet, ux *userIndex) *apiGateway {
return &apiGateway{
gwr: gwr,
ff: ff,
ux: ux,
}
}
func (awf *apiGateway) MarshalEnvoy() ([]resource.Interface, error) {
rs := resource.NewAPIGateway(awf.gwr)
syncUserStamps(rs.Userstamps(), awf.ux)
for _, f := range awf.ff {
rt := rs.AddGatewayFilter(f)
syncUserStamps(rt.Userstamps(), awf.ux)
}
return envoy.CollectNodes(
rs,
)
}

View File

@@ -0,0 +1,90 @@
package store
import (
"context"
"github.com/cortezaproject/corteza/server/pkg/envoy/resource"
"github.com/cortezaproject/corteza/server/store"
"github.com/cortezaproject/corteza/server/system/types"
)
type (
application struct {
cfg *EncoderConfig
res *resource.Application
app *types.Application
ux *userIndex
}
)
// mergeApplications merges b into a, prioritising a
func mergeApplications(a, b *types.Application) *types.Application {
c := *a
if c.Name == "" {
c.Name = b.Name
}
c.OwnerID = b.OwnerID
if c.CreatedAt.IsZero() {
c.CreatedAt = b.CreatedAt
}
if c.UpdatedAt == nil {
c.UpdatedAt = b.UpdatedAt
}
if c.DeletedAt == nil {
c.DeletedAt = b.DeletedAt
}
// I'll just compare the entire struct for now
if c.Unify == nil || *c.Unify == (types.ApplicationUnify{}) {
c.Unify = b.Unify
}
return &c
}
// findApplication looks for the app in the resources & the store
//
// Provided resources are prioritized.
func findApplication(ctx context.Context, s store.Storer, rr resource.InterfaceSet, ii resource.Identifiers) (ap *types.Application, err error) {
ap = resource.FindApplication(rr, ii)
if ap != nil {
return ap, nil
}
return findApplicationStore(ctx, s, makeGenericFilter(ii))
}
// findApplicationStore looks for the app in the store
func findApplicationStore(ctx context.Context, s store.Storer, gf genericFilter) (ap *types.Application, err error) {
if gf.id > 0 {
ap, err = store.LookupApplicationByID(ctx, s, gf.id)
if err != nil && err != store.ErrNotFound {
return nil, err
}
if ap != nil {
return
}
}
for _, i := range gf.identifiers {
var aa types.ApplicationSet
aa, _, err = store.SearchApplications(ctx, s, types.ApplicationFilter{Name: i})
if err != nil && err != store.ErrNotFound {
return nil, err
}
if len(aa) > 1 {
return nil, resourceErrIdentifierNotUnique(i)
}
if len(aa) == 1 {
ap = aa[0]
return
}
}
return nil, nil
}

View File

@@ -0,0 +1,108 @@
package store
import (
"context"
"github.com/cortezaproject/corteza/server/pkg/envoy/resource"
"github.com/cortezaproject/corteza/server/store"
"github.com/cortezaproject/corteza/server/system/types"
)
func NewApplicationFromResource(res *resource.Application, cfg *EncoderConfig) resourceState {
return &application{
cfg: mergeConfig(cfg, res.Config()),
res: res,
}
}
func (n *application) Prepare(ctx context.Context, pl *payload) (err error) {
// Get the existing app
n.app, err = findApplicationStore(ctx, pl.s, makeGenericFilter(n.res.Identifiers()))
if err != nil {
return err
}
if n.app != nil {
n.res.Res.ID = n.app.ID
}
return nil
}
func (n *application) Encode(ctx context.Context, pl *payload) (err error) {
res := n.res.Res
exists := n.app != nil && n.app.ID > 0
// Determine the ID
if res.ID <= 0 && exists {
res.ID = n.app.ID
}
if res.ID <= 0 {
res.ID = NextID()
}
// Sys users
us, err := resolveUserstamps(ctx, pl.s, pl.state.ParentResources, n.res.Userstamps())
if err != nil {
return err
}
// Unify
if res.Unify == nil {
res.Unify = &types.ApplicationUnify{}
}
ts := n.res.Timestamps()
if ts != nil {
if ts.CreatedAt != nil {
res.CreatedAt = *ts.CreatedAt.T
} else {
res.CreatedAt = *now()
}
if ts.UpdatedAt != nil {
res.UpdatedAt = ts.UpdatedAt.T
}
if ts.DeletedAt != nil {
res.DeletedAt = ts.DeletedAt.T
}
}
if us != nil {
if us.OwnedBy != nil {
res.OwnerID = us.OwnedBy.UserID
}
}
// Evaluate the resource skip expression
// @todo expand available parameters; similar implementation to compose/types/record@Dict
if skip, err := basicSkipEval(ctx, n.cfg, !exists); err != nil {
return err
} else if skip {
return nil
}
// Create fresh application
if !exists {
return store.CreateApplication(ctx, pl.s, res)
}
// Update existing application
switch n.cfg.OnExisting {
case resource.Skip:
return nil
case resource.MergeLeft:
res = mergeApplications(n.app, res)
case resource.MergeRight:
res = mergeApplications(res, n.app)
}
err = store.UpdateApplication(ctx, pl.s, res)
if err != nil {
return err
}
n.res.Res = res
return nil
}

View File

@@ -0,0 +1,54 @@
package store
import (
"testing"
"time"
"github.com/cortezaproject/corteza/server/system/types"
"github.com/stretchr/testify/require"
)
func TestApplication_Merger(t *testing.T) {
req := require.New(t)
ub := &types.ApplicationUnify{
Config: "config",
Icon: "icon",
}
now := time.Time{}
nowP := &time.Time{}
empty := &types.Application{}
full := &types.Application{
Name: "name",
OwnerID: 1,
Enabled: true,
Unify: ub,
CreatedAt: now,
UpdatedAt: nowP,
DeletedAt: nowP,
}
t.Run("merge on empty", func(t *testing.T) {
c := mergeApplications(empty, full)
req.Equal("name", c.Name)
req.Equal(uint64(1), c.OwnerID)
req.False(c.Enabled)
req.Equal(ub, c.Unify)
req.Equal(now, c.CreatedAt)
req.Equal(nowP, c.UpdatedAt)
req.Equal(nowP, c.DeletedAt)
})
t.Run("merge with empty", func(t *testing.T) {
c := mergeApplications(full, empty)
req.Equal("name", c.Name)
req.Equal(uint64(0), c.OwnerID)
req.True(c.Enabled)
req.Equal(ub, c.Unify)
req.Equal(now, c.CreatedAt)
req.Equal(nowP, c.UpdatedAt)
req.Equal(nowP, c.DeletedAt)
})
}

View File

@@ -0,0 +1,23 @@
package store
import (
"github.com/cortezaproject/corteza/server/pkg/envoy"
"github.com/cortezaproject/corteza/server/pkg/envoy/resource"
"github.com/cortezaproject/corteza/server/system/types"
)
func newApplication(app *types.Application, ux *userIndex) *application {
return &application{
app: app,
ux: ux,
}
}
func (app *application) MarshalEnvoy() ([]resource.Interface, error) {
rs := resource.NewApplication(app.app)
syncUserStamps(rs.Userstamps(), app.ux)
return envoy.CollectNodes(
rs,
)
}

View File

@@ -0,0 +1,176 @@
package store
import (
"context"
"strings"
"github.com/cortezaproject/corteza/server/automation/types"
"github.com/cortezaproject/corteza/server/pkg/envoy"
"github.com/cortezaproject/corteza/server/pkg/envoy/resource"
"github.com/cortezaproject/corteza/server/pkg/filter"
"github.com/cortezaproject/corteza/server/pkg/id"
"github.com/cortezaproject/corteza/server/store"
)
type (
automationWorkflowFilter types.WorkflowFilter
automationStore interface {
store.AutomationWorkflows
store.AutomationTriggers
}
automationDecoder struct {
resourceID []uint64
ux *userIndex
}
)
func newAutomationDecoder(ux *userIndex) *automationDecoder {
return &automationDecoder{
resourceID: make([]uint64, 0, 200),
ux: ux,
}
}
func (d *automationDecoder) decodeWorkflows(ctx context.Context, s automationStore, ff []*automationWorkflowFilter) *auxRsp {
mm := make([]envoy.Marshaller, 0, 100)
if ff == nil {
return &auxRsp{
mm: mm,
}
}
var nn types.WorkflowSet
var fn types.WorkflowFilter
var err error
for _, f := range ff {
aux := *f
if aux.Limit == 0 {
aux.Limit = 1000
}
for {
nn, fn, err = s.SearchAutomationWorkflows(ctx, types.WorkflowFilter(aux))
if err != nil {
return &auxRsp{
err: err,
}
}
for _, n := range nn {
// Index users
err = d.ux.add(
ctx,
n.RunAs,
n.OwnedBy,
n.CreatedBy,
n.UpdatedBy,
n.DeletedBy,
)
if err != nil {
return &auxRsp{
err: err,
}
}
tt, _, err := s.SearchAutomationTriggers(ctx, types.TriggerFilter{
WorkflowID: id.Strings(n.ID),
Disabled: filter.StateInclusive,
})
if err != nil {
return &auxRsp{
err: err,
}
}
for _, t := range tt {
// Index users
err = d.ux.add(
ctx,
t.OwnedBy,
t.CreatedBy,
t.UpdatedBy,
t.DeletedBy,
)
}
if err != nil {
return &auxRsp{
err: err,
}
}
mm = append(mm, newAutomationWorkflow(n, tt, d.ux))
d.resourceID = append(d.resourceID, n.ID)
}
if fn.NextPage != nil {
aux.PageCursor = fn.NextPage
} else {
break
}
}
}
return &auxRsp{
mm: mm,
}
}
func (df *DecodeFilter) automationFromResource(rr ...string) *DecodeFilter {
for _, r := range rr {
if !strings.HasPrefix(r, "automation") {
continue
}
id := ""
if strings.Count(r, ":") == 2 && !strings.HasSuffix(r, "*") {
// There is an identifier
aux := strings.Split(r, ":")
id = aux[len(aux)-1]
r = strings.Join(aux[:len(aux)-1], ":")
}
switch strings.ToLower(r) {
case "automation:workflow":
df = df.AutomationWorkflows(&types.WorkflowFilter{
Query: id,
Disabled: filter.StateInclusive,
})
}
}
return df
}
func (df *DecodeFilter) automationFromRef(rr ...*resource.Ref) *DecodeFilter {
for _, r := range rr {
if strings.Index(r.ResourceType, "automation") < 0 {
continue
}
switch r.ResourceType {
case types.WorkflowResourceType:
for _, i := range r.Identifiers.StringSlice() {
df = df.AutomationWorkflows(&types.WorkflowFilter{
Query: i,
Disabled: filter.StateInclusive,
})
}
}
}
return df
}
// AutomationWorkflows adds a new WorkflowFilter
func (df *DecodeFilter) AutomationWorkflows(f *types.WorkflowFilter) *DecodeFilter {
if df.automationWorkflow == nil {
df.automationWorkflow = make([]*automationWorkflowFilter, 0, 1)
}
df.automationWorkflow = append(df.automationWorkflow, (*automationWorkflowFilter)(f))
return df
}

View File

@@ -0,0 +1,138 @@
package store
import (
"context"
"github.com/cortezaproject/corteza/server/automation/types"
"github.com/cortezaproject/corteza/server/pkg/envoy/resource"
"github.com/cortezaproject/corteza/server/pkg/filter"
"github.com/cortezaproject/corteza/server/store"
)
type (
automationWorkflow struct {
cfg *EncoderConfig
res *resource.AutomationWorkflow
wf *types.Workflow
tt types.TriggerSet
ux *userIndex
}
automationWorkflowSet []*automationWorkflow
automationTrigger struct {
cfg *EncoderConfig
res *resource.AutomationTrigger
tr *types.Trigger
}
automationTriggerSet []*automationTrigger
)
// mergeAutomationWorkflows merges b into a, prioritising a
func mergeAutomationWorkflows(a, b *types.Workflow) *types.Workflow {
c := a
if c.Handle == "" {
c.Handle = b.Handle
}
if c.Meta == nil {
c.Meta = b.Meta
}
if c.Scope == nil {
c.Scope = b.Scope
}
if c.Steps == nil {
c.Steps = b.Steps
}
if c.Paths == nil {
c.Paths = b.Paths
}
if c.RunAs == 0 {
c.RunAs = b.RunAs
}
if c.OwnedBy == 0 {
c.OwnedBy = b.OwnedBy
}
if c.CreatedBy == 0 {
c.CreatedBy = b.CreatedBy
}
if c.UpdatedBy == 0 {
c.UpdatedBy = b.UpdatedBy
}
if c.DeletedBy == 0 {
c.DeletedBy = b.DeletedBy
}
if c.CreatedAt.IsZero() {
c.CreatedAt = b.CreatedAt
}
if c.UpdatedAt == nil {
c.UpdatedAt = b.UpdatedAt
}
if c.DeletedAt == nil {
c.DeletedAt = b.DeletedAt
}
return c
}
// findAutomationWorkflow looks for the workflow in the resources & the store
//
// Provided resources are prioritized.
func findAutomationWorkflow(ctx context.Context, s store.Storer, rr resource.InterfaceSet, ii resource.Identifiers) (wf *types.Workflow, err error) {
wf = resource.FindAutomationWorkflow(rr, ii)
if wf != nil {
return wf, nil
}
return findAutomationWorkflowStore(ctx, s, makeGenericFilter(ii))
}
// findAutomationWorkflowStore looks for the workflow in the store
func findAutomationWorkflowStore(ctx context.Context, s store.Storer, gf genericFilter) (wf *types.Workflow, err error) {
if gf.id > 0 {
wf, err = store.LookupAutomationWorkflowByID(ctx, s, gf.id)
if err != nil && err != store.ErrNotFound {
return nil, err
}
if wf != nil {
return
}
}
for _, i := range gf.identifiers {
wf, err = store.LookupAutomationWorkflowByHandle(ctx, s, i)
if err == store.ErrNotFound {
var nn types.WorkflowSet
nn, _, err = store.SearchAutomationWorkflows(ctx, s, types.WorkflowFilter{
Query: i,
Paging: filter.Paging{
Limit: 2,
},
})
if len(nn) > 1 {
return nil, resourceErrIdentifierNotUnique(i)
}
if len(nn) == 1 {
wf = nn[0]
}
}
if err != nil && err != store.ErrNotFound {
return nil, err
}
if wf != nil {
return
}
}
return nil, nil
}

View File

@@ -0,0 +1,239 @@
package store
import (
"context"
"github.com/cortezaproject/corteza/server/automation/types"
"github.com/cortezaproject/corteza/server/pkg/envoy/resource"
"github.com/cortezaproject/corteza/server/pkg/filter"
"github.com/cortezaproject/corteza/server/pkg/id"
"github.com/cortezaproject/corteza/server/store"
)
func newAutomationWorkflowFromResource(res *resource.AutomationWorkflow, cfg *EncoderConfig) resourceState {
return &automationWorkflow{
cfg: mergeConfig(cfg, res.Config()),
res: res,
}
}
func (n *automationWorkflow) Prepare(ctx context.Context, pl *payload) (err error) {
err = n.prepareWorkflows(ctx, pl)
if err != nil {
return err
}
return n.prepareTriggers(ctx, pl)
}
func (n *automationWorkflow) prepareWorkflows(ctx context.Context, pl *payload) (err error) {
// Reset old identifiers
n.res.Res.ID = 0
// Try to get the original workflow
n.wf, err = findAutomationWorkflowStore(ctx, pl.s, makeGenericFilter(n.res.Identifiers()))
if err != nil {
return err
}
if n.wf != nil {
n.res.Res.ID = n.wf.ID
}
return nil
}
func (n *automationWorkflow) prepareTriggers(ctx context.Context, pl *payload) (err error) {
if n.wf == nil || n.wf.ID == 0 {
return nil
}
// Reset old identifiers
for _, t := range n.res.Triggers {
t.Res.ID = 0
t.Res.WorkflowID = 0
}
// Try to find any related triggers for this workflow
tt, _, err := store.SearchAutomationTriggers(ctx, pl.s, types.TriggerFilter{
WorkflowID: id.Strings(n.wf.ID),
Disabled: filter.StateInclusive,
})
if err != nil {
return err
}
n.tt = tt
return nil
}
func (n *automationWorkflow) Encode(ctx context.Context, pl *payload) (err error) {
err = n.encodeWorkflow(ctx, pl)
if err != nil {
return err
}
return n.encodeTriggers(ctx, pl)
}
func (n *automationWorkflow) encodeWorkflow(ctx context.Context, pl *payload) (err error) {
res := n.res.Res
exists := n.wf != nil && n.wf.ID > 0
// Determine the ID
if res.ID <= 0 && exists {
res.ID = n.wf.ID
}
if res.ID <= 0 {
res.ID = NextID()
}
// Sys users
us, err := resolveUserstamps(ctx, pl.s, pl.state.ParentResources, n.res.Userstamps())
if err != nil {
return err
}
ts := n.res.Timestamps()
if ts != nil {
if ts.CreatedAt != nil {
res.CreatedAt = *ts.CreatedAt.T
} else {
res.CreatedAt = *now()
}
if ts.UpdatedAt != nil {
res.UpdatedAt = ts.UpdatedAt.T
}
if ts.DeletedAt != nil {
res.DeletedAt = ts.DeletedAt.T
}
}
res.CreatedBy = pl.invokerID
if us != nil {
if us.OwnedBy != nil {
res.OwnedBy = us.OwnedBy.UserID
}
if us.CreatedBy != nil {
res.CreatedBy = us.CreatedBy.UserID
}
if us.UpdatedBy != nil {
res.UpdatedBy = us.UpdatedBy.UserID
}
if us.DeletedBy != nil {
res.DeletedBy = us.DeletedBy.UserID
}
if us.RunAs != nil {
res.RunAs = us.RunAs.UserID
}
}
res.Steps = make(types.WorkflowStepSet, 0, 100)
for _, sres := range n.res.Steps {
res.Steps = append(res.Steps, sres.Res)
}
res.Paths = make(types.WorkflowPathSet, 0, 100)
for _, pres := range n.res.Paths {
p := pres.Res
res.Paths = append(res.Paths, p)
}
// Evaluate the resource skip expression
// @todo expand available parameters; similar implementation to automation/types/record@Dict
if skip, err := basicSkipEval(ctx, n.cfg, !exists); err != nil {
return err
} else if skip {
return nil
}
// Create a fresh workflow
if !exists {
return store.CreateAutomationWorkflow(ctx, pl.s, res)
}
// Update existing workflow
switch n.cfg.OnExisting {
case resource.Skip:
return nil
case resource.MergeLeft:
res = mergeAutomationWorkflows(n.wf, res)
case resource.MergeRight:
res = mergeAutomationWorkflows(res, n.wf)
}
err = store.UpdateAutomationWorkflow(ctx, pl.s, res)
if err != nil {
return err
}
n.res.Res = res
return nil
}
func (n *automationWorkflow) encodeTriggers(ctx context.Context, pl *payload) (err error) {
exists := len(n.tt) > 0
rr := make([]*types.Trigger, 0, len(n.res.Triggers))
for _, tr := range n.res.Triggers {
res := tr.Res
res.WorkflowID = n.res.Res.ID
res.ID = NextID()
// Sys users
us, err := resolveUserstamps(ctx, pl.s, pl.state.ParentResources, tr.Userstamps())
if err != nil {
return err
}
ts := tr.Timestamps()
if ts != nil {
if ts.CreatedAt != nil {
res.CreatedAt = *ts.CreatedAt.T
} else {
res.CreatedAt = *now()
}
if ts.UpdatedAt != nil {
res.UpdatedAt = ts.UpdatedAt.T
}
if ts.DeletedAt != nil {
res.DeletedAt = ts.DeletedAt.T
}
}
res.CreatedBy = pl.invokerID
if us != nil {
if us.OwnedBy != nil {
res.OwnedBy = us.OwnedBy.UserID
}
if us.CreatedBy != nil {
res.CreatedBy = us.CreatedBy.UserID
}
if us.UpdatedBy != nil {
res.UpdatedBy = us.UpdatedBy.UserID
}
if us.DeletedBy != nil {
res.DeletedBy = us.DeletedBy.UserID
}
}
rr = append(rr, res)
}
// Create a fresh workflow
if !exists {
return store.CreateAutomationTrigger(ctx, pl.s, rr...)
}
// If these triggers already exist and we wish to modify them,
// remove the old ones and create new ones
switch n.cfg.OnExisting {
case resource.Skip,
resource.MergeLeft:
return nil
}
err = store.DeleteAutomationTrigger(ctx, pl.s, n.tt...)
return store.CreateAutomationTrigger(ctx, pl.s, rr...)
}

View File

@@ -0,0 +1,38 @@
package store
import (
"github.com/cortezaproject/corteza/server/automation/types"
"github.com/cortezaproject/corteza/server/pkg/envoy"
"github.com/cortezaproject/corteza/server/pkg/envoy/resource"
)
func newAutomationWorkflow(wf *types.Workflow, tt types.TriggerSet, ux *userIndex) *automationWorkflow {
return &automationWorkflow{
wf: wf,
tt: tt,
ux: ux,
}
}
func (awf *automationWorkflow) MarshalEnvoy() ([]resource.Interface, error) {
rs := resource.NewAutomationWorkflow(awf.wf)
syncUserStamps(rs.Userstamps(), awf.ux)
for _, t := range awf.tt {
rt := rs.AddAutomationTrigger(t)
syncUserStamps(rt.Userstamps(), awf.ux)
}
for _, s := range awf.wf.Steps {
rs.AddAutomationWorkflowStep(s)
}
for _, p := range awf.wf.Paths {
rs.AddAutomationWorkflowPath(p)
}
return envoy.CollectNodes(
rs,
)
}

View File

@@ -0,0 +1,579 @@
package store
import (
"context"
"strconv"
"strings"
"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/envoy"
"github.com/cortezaproject/corteza/server/pkg/envoy/resource"
"github.com/cortezaproject/corteza/server/pkg/filter"
"github.com/cortezaproject/corteza/server/store"
stypes "github.com/cortezaproject/corteza/server/system/types"
)
type (
composeNamespaceFilter types.NamespaceFilter
composeModuleFilter types.ModuleFilter
composeRecordFilter types.RecordFilter
composePageFilter types.PageFilter
composeChartFilter types.ChartFilter
composeStore interface {
store.ComposeAttachments
store.ComposeCharts
store.ComposeModuleFields
store.ComposeModules
store.ComposeNamespaces
store.ComposePages
}
dalService interface {
Create(ctx context.Context, m dal.ModelRef, operations dal.OperationSet, vv ...dal.ValueGetter) error
Search(ctx context.Context, m dal.ModelRef, operations dal.OperationSet, f filter.Filter) (dal.Iterator, error)
Delete(ctx context.Context, m dal.ModelRef, operations dal.OperationSet, pkv ...dal.ValueGetter) (err error)
Update(ctx context.Context, m dal.ModelRef, operations dal.OperationSet, pkv ...dal.ValueGetter) (err error)
ReplaceModel(ctx context.Context, currentAlts []*dal.Alteration, model *dal.Model) (newAlts []*dal.Alteration, err error)
GetConnectionByID(uint64) *dal.ConnectionWrap
}
composeDecoder struct {
resourceID []uint64
namespaceID []uint64
dal dalService
}
)
func newComposeDecoder(dal dalService) *composeDecoder {
return &composeDecoder{
resourceID: make([]uint64, 0, 200),
namespaceID: make([]uint64, 0, 200),
dal: dal,
}
}
func (d *composeDecoder) decodeComposeNamespace(ctx context.Context, s composeStore, ff []*composeNamespaceFilter) *auxRsp {
mm := make([]envoy.Marshaller, 0, 100)
if ff == nil {
return &auxRsp{
mm: mm,
}
}
var nn types.NamespaceSet
var fn types.NamespaceFilter
var err error
for _, f := range ff {
aux := *f
if aux.Limit == 0 {
aux.Limit = 1000
}
for {
nn, fn, err = s.SearchComposeNamespaces(ctx, types.NamespaceFilter(aux))
if err != nil {
return &auxRsp{
err: err,
}
}
for _, n := range nn {
d.namespaceID = append(d.namespaceID, n.ID)
d.resourceID = append(d.resourceID, n.ID)
mm = append(mm, newComposeNamespace(n))
}
if f.NextPage != nil {
aux.PageCursor = fn.NextPage
} else {
break
}
}
}
return &auxRsp{
mm: mm,
}
}
func (d *composeDecoder) decodeComposeModule(ctx context.Context, s composeStore, ff []*composeModuleFilter) *auxRsp {
mm := make([]envoy.Marshaller, 0, 100)
if ff == nil {
return &auxRsp{
mm: mm,
}
}
if len(d.namespaceID) > 0 {
ffNs := make([]*composeModuleFilter, 0, len(ff)+len(d.namespaceID))
for _, nsID := range d.namespaceID {
for _, f := range ff {
fNs := *f
fNs.NamespaceID = nsID
ffNs = append(ffNs, &fNs)
}
}
ff = ffNs
}
var nn types.ModuleSet
var fn types.ModuleFilter
var err error
for _, f := range ff {
aux := *f
if aux.Limit == 0 {
aux.Limit = 1000
}
for {
nn, fn, err = s.SearchComposeModules(ctx, types.ModuleFilter(aux))
if err != nil {
return &auxRsp{
err: err,
}
}
for _, n := range nn {
d.resourceID = append(d.resourceID, n.ID)
n.Fields, _, err = s.SearchComposeModuleFields(ctx, types.ModuleFieldFilter{
ModuleID: []uint64{n.ID},
})
if err != nil {
return &auxRsp{
err: err,
}
}
mm = append(mm, newComposeModule(n))
}
if f.NextPage != nil {
aux.PageCursor = fn.NextPage
} else {
break
}
}
}
return &auxRsp{
mm: mm,
}
}
func (d *composeDecoder) decodeComposeRecord(ctx context.Context, s store.Storer, ff []*composeRecordFilter) *auxRsp {
mm := make([]envoy.Marshaller, 0, 100)
if ff == nil {
return &auxRsp{
mm: mm,
}
}
ac := service.AccessControl(s)
if len(d.namespaceID) > 0 {
ffNs := make([]*composeRecordFilter, 0, len(ff)+len(d.namespaceID))
for _, nsID := range d.namespaceID {
for _, f := range ff {
nn, _, err := s.SearchComposeModules(ctx, types.ModuleFilter{
NamespaceID: nsID,
})
if err != nil {
continue
}
for _, n := range nn {
fNs := *f
fNs.NamespaceID = nsID
fNs.ModuleID = n.ID
ffNs = append(ffNs, &fNs)
}
}
}
ff = ffNs
}
// When decoding large amounts of records (milions) we can probably assume
// that each system user exists somewhere in the record set.
//
// That said, it's probably cheeper to simply say that we probably need all of the
// references to hold.
// If a user is not there in the preparation step, we shouldn't fail straight away.
//
// @todo use some heuristic to determine when to just list all of the users
// and when to preprocess the records to find out what users to get.
relUsers := make(resource.UserstampIndex)
uu, _, err := store.SearchUsers(ctx, s, stypes.UserFilter{
Paging: filter.Paging{
Limit: 0,
},
})
if err != nil {
return &auxRsp{
err: err,
}
}
relUsers.Add(uu...)
mapValues := func(r *types.Record) map[string]resource.ComposeRecordRawValue {
mod := r.GetModule()
out := make(map[string]resource.ComposeRecordRawValue, len(r.Values))
for _, v := range r.Values {
f := mod.Fields.FindByName(v.Name)
if f == nil || !f.Multi {
out[v.Name] = resource.ComposeRecordRawValue{
Name: v.Name,
Value: v.Value,
}
} else {
if _, ok := out[v.Name]; !ok {
out[v.Name] = resource.ComposeRecordRawValue{
Name: v.Name,
IsMulti: true,
}
}
vx := out[v.Name]
vx.Values = append(vx.Values, v.Value)
out[v.Name] = vx
}
}
return out
}
// Prepare a series of resource.ComposeRecord instances; one for each provided filter
for _, f := range ff {
aux := *f
if aux.Limit == 0 {
aux.Limit = 1000
}
mod, err := store.LookupComposeModuleByID(ctx, s, f.ModuleID)
if err != nil {
return &auxRsp{
err: err,
}
}
ff, _, err := store.SearchComposeModuleFields(ctx, s, types.ModuleFieldFilter{ModuleID: []uint64{mod.ID}})
if err != nil {
return &auxRsp{
err: err,
}
}
_ = ff.Walk(func(f *types.ModuleField) error {
f.NamespaceID = mod.NamespaceID
return nil
})
mod.Fields = ff
aux.Check = service.ComposeRecordFilterChecker(ctx, ac, mod)
// Refs
auxRecord := &composeRecordAux{
refMod: strconv.FormatUint(f.ModuleID, 10),
relMod: mod,
refNs: strconv.FormatUint(f.NamespaceID, 10),
relUsers: relUsers,
}
// Walker
auxRecord.walker = func(cb func(r *resource.ComposeRecordRaw) error) error {
var rr types.RecordSet
var fn types.RecordFilter
var err error
for {
rr, fn, err = dalutils.ComposeRecordsList(ctx, d.dal, mod, types.RecordFilter(aux))
if err != nil {
return err
}
service.ComposeRecordFilterAC(ctx, ac, mod, rr...)
for _, n := range rr {
// Create a raw record
r := &resource.ComposeRecordRaw{
ID: strconv.FormatUint(n.ID, 10),
Values: mapValues(n),
Ts: resource.MakeTimestampsCUDA(&n.CreatedAt, n.UpdatedAt, n.DeletedAt, nil),
Us: resource.MakeUserstampsCUDO(n.CreatedBy, n.UpdatedBy, n.DeletedBy, n.OwnedBy),
}
err = cb(r)
if err != nil {
return err
}
}
if fn.NextPage != nil {
aux.PageCursor = fn.NextPage
} else {
break
}
}
return nil
}
mm = append(mm, newComposeRecordFromAux(auxRecord))
}
return &auxRsp{
mm: mm,
}
}
func (d *composeDecoder) decodeComposePage(ctx context.Context, s composeStore, ff []*composePageFilter) *auxRsp {
mm := make([]envoy.Marshaller, 0, 100)
if ff == nil {
return &auxRsp{
mm: mm,
}
}
if len(d.namespaceID) > 0 {
ffNs := make([]*composePageFilter, 0, len(ff)+len(d.namespaceID))
for _, nsID := range d.namespaceID {
for _, f := range ff {
fNs := *f
fNs.NamespaceID = nsID
ffNs = append(ffNs, &fNs)
}
}
ff = ffNs
}
var nn types.PageSet
var fn types.PageFilter
var err error
for _, f := range ff {
aux := *f
if aux.Limit == 0 {
aux.Limit = 1000
}
for {
nn, fn, err = s.SearchComposePages(ctx, types.PageFilter(aux))
if err != nil {
return &auxRsp{
err: err,
}
}
for _, n := range nn {
d.resourceID = append(d.resourceID, n.ID)
mm = append(mm, newComposePage(n))
}
if fn.NextPage != nil {
aux.PageCursor = fn.NextPage
} else {
break
}
}
}
return &auxRsp{
mm: mm,
}
}
func (d *composeDecoder) decodeComposeChart(ctx context.Context, s composeStore, ff []*composeChartFilter) *auxRsp {
mm := make([]envoy.Marshaller, 0, 100)
if ff == nil {
return &auxRsp{
mm: mm,
}
}
if len(d.namespaceID) > 0 {
ffNs := make([]*composeChartFilter, 0, len(ff)+len(d.namespaceID))
for _, nsID := range d.namespaceID {
for _, f := range ff {
fNs := *f
fNs.NamespaceID = nsID
ffNs = append(ffNs, &fNs)
}
}
ff = ffNs
}
var nn types.ChartSet
var fn types.ChartFilter
var err error
for _, f := range ff {
aux := *f
if aux.Limit == 0 {
aux.Limit = 1000
}
for {
nn, fn, err = s.SearchComposeCharts(ctx, types.ChartFilter(aux))
if err != nil {
return &auxRsp{
err: err,
}
}
for _, n := range nn {
d.resourceID = append(d.resourceID, n.ID)
mm = append(mm, newComposeChart(n))
}
if f.Limit > 0 {
break
} else if fn.NextPage != nil {
aux.PageCursor = fn.NextPage
} else {
break
}
}
}
return &auxRsp{
mm: mm,
}
}
func (df *DecodeFilter) composeFromResource(rr ...string) *DecodeFilter {
for _, r := range rr {
if !strings.HasPrefix(r, "compose") {
continue
}
id := ""
if strings.Count(r, ":") == 2 && !strings.HasSuffix(r, "*") {
// There is an identifier
aux := strings.Split(r, ":")
id = aux[len(aux)-1]
r = strings.Join(aux[:len(aux)-1], ":")
}
switch strings.ToLower(r) {
case "compose:namespace":
df = df.ComposeNamespace(&types.NamespaceFilter{
Query: id,
})
case "compose:module":
df = df.ComposeModule(&types.ModuleFilter{
Query: id,
})
case "compose:record":
df = df.ComposeRecord(&types.RecordFilter{
Query: id,
})
case "compose:page":
df = df.ComposePage(&types.PageFilter{
Query: id,
})
case "compose:chart":
df = df.ComposeChart(&types.ChartFilter{
Query: id,
})
}
}
return df
}
func (df *DecodeFilter) composeFromRef(rr ...*resource.Ref) *DecodeFilter {
for _, r := range rr {
if strings.Index(r.ResourceType, "compose") < 0 {
continue
}
switch r.ResourceType {
case types.NamespaceResourceType:
for _, i := range r.Identifiers.StringSlice() {
df = df.ComposeNamespace(&types.NamespaceFilter{
Query: i,
})
}
case types.ModuleResourceType:
for _, i := range r.Identifiers.StringSlice() {
df = df.ComposeModule(&types.ModuleFilter{
Query: i,
})
}
case types.PageResourceType:
for _, i := range r.Identifiers.StringSlice() {
df = df.ComposePage(&types.PageFilter{
Query: i,
})
}
case types.ChartResourceType:
for _, i := range r.Identifiers.StringSlice() {
df = df.ComposeChart(&types.ChartFilter{
Query: i,
})
}
}
}
return df
}
// ComposeNamespace adds a new compose NamespaceFilter
func (df *DecodeFilter) ComposeNamespace(f *types.NamespaceFilter) *DecodeFilter {
if df.composeNamespace == nil {
df.composeNamespace = make([]*composeNamespaceFilter, 0, 1)
}
df.composeNamespace = append(df.composeNamespace, (*composeNamespaceFilter)(f))
return df
}
// ComposeModule adds a new compose ModuleFilter
func (df *DecodeFilter) ComposeModule(f *types.ModuleFilter) *DecodeFilter {
if df.composeModule == nil {
df.composeModule = make([]*composeModuleFilter, 0, 1)
}
df.composeModule = append(df.composeModule, (*composeModuleFilter)(f))
return df
}
// ComposeRecord adds a new compose RecordFilter
func (df *DecodeFilter) ComposeRecord(f *types.RecordFilter) *DecodeFilter {
if df.composeRecord == nil {
df.composeRecord = make([]*composeRecordFilter, 0, 1)
}
df.composeRecord = append(df.composeRecord, (*composeRecordFilter)(f))
return df
}
// ComposePage adds a new compose PageFilter
func (df *DecodeFilter) ComposePage(f *types.PageFilter) *DecodeFilter {
if df.composePage == nil {
df.composePage = make([]*composePageFilter, 0, 1)
}
df.composePage = append(df.composePage, (*composePageFilter)(f))
return df
}
// ComposeChart adds a new compose ChartFilter
func (df *DecodeFilter) ComposeChart(f *types.ChartFilter) *DecodeFilter {
if df.composeChart == nil {
df.composeChart = make([]*composeChartFilter, 0, 1)
}
df.composeChart = append(df.composeChart, (*composeChartFilter)(f))
return df
}

View File

@@ -0,0 +1,118 @@
package store
import (
"context"
"github.com/cortezaproject/corteza/server/compose/types"
"github.com/cortezaproject/corteza/server/pkg/envoy/resource"
"github.com/cortezaproject/corteza/server/pkg/filter"
"github.com/cortezaproject/corteza/server/store"
)
type (
composeChart struct {
cfg *EncoderConfig
res *resource.ComposeChart
chr *types.Chart
relNS *types.Namespace
relMods types.ModuleSet
}
)
// mergeComposeChart merges b into a, prioritising a
func mergeComposeChart(a, b *types.Chart) *types.Chart {
c := *a
if c.Handle == "" {
c.Handle = b.Handle
}
if c.Name == "" {
c.Name = b.Name
}
c.NamespaceID = b.NamespaceID
if c.CreatedAt.IsZero() {
c.CreatedAt = b.CreatedAt
}
if c.UpdatedAt == nil {
c.UpdatedAt = b.UpdatedAt
}
if c.DeletedAt == nil {
c.DeletedAt = b.DeletedAt
}
if len(c.Config.Reports) <= 0 {
c.Config.Reports = b.Config.Reports
}
if c.Config.ColorScheme == "" {
c.Config.ColorScheme = b.Config.ColorScheme
}
return &c
}
// findComposeChart looks for the chart in the resources & the store
//
// Provided resources are prioritized.
func findComposeChart(ctx context.Context, s store.Storer, nsID uint64, rr resource.InterfaceSet, ii resource.Identifiers) (ch *types.Chart, err error) {
ch = resource.FindComposeChart(rr, ii)
if ch != nil {
return ch, nil
}
if nsID <= 0 {
return nil, nil
}
// Go in the store
return findComposeChartStore(ctx, s, nsID, makeGenericFilter(ii))
}
// findComposeChartStore looks for the chart in the store
func findComposeChartStore(ctx context.Context, s store.Storer, nsID uint64, gf genericFilter) (ch *types.Chart, err error) {
if nsID == 0 {
return nil, nil
}
if gf.id > 0 {
ch, err = store.LookupComposeChartByID(ctx, s, gf.id)
if err != nil && err != store.ErrNotFound {
return nil, err
}
if ch != nil {
return
}
}
for _, i := range gf.identifiers {
ch, err = store.LookupComposeChartByNamespaceIDHandle(ctx, s, nsID, i)
if err == store.ErrNotFound {
var cc types.ChartSet
cc, _, err = store.SearchComposeCharts(ctx, s, types.ChartFilter{
NamespaceID: nsID,
Name: i,
Paging: filter.Paging{
Limit: 2,
},
})
if len(cc) > 1 {
return nil, resourceErrIdentifierNotUnique(i)
}
if len(cc) == 1 {
ch = cc[0]
}
}
if err != nil && err != store.ErrNotFound {
return nil, err
}
if ch != nil {
return
}
}
return nil, nil
}

View File

@@ -0,0 +1,140 @@
package store
import (
"context"
"github.com/cortezaproject/corteza/server/compose/types"
"github.com/cortezaproject/corteza/server/pkg/envoy/resource"
"github.com/cortezaproject/corteza/server/store"
)
func newComposeChartFromResource(res *resource.ComposeChart, cfg *EncoderConfig) resourceState {
return &composeChart{
cfg: mergeConfig(cfg, res.Config()),
res: res,
}
}
func (n *composeChart) Prepare(ctx context.Context, pl *payload) (err error) {
// Reset old identifiers
n.res.Res.ID = 0
n.res.Res.NamespaceID = 0
// Get related namespace
n.relNS, err = findComposeNamespace(ctx, pl.s, pl.state.ParentResources, n.res.RefNs.Identifiers)
if err != nil {
return err
}
if n.relNS == nil {
return resource.ComposeNamespaceErrUnresolved(n.res.RefNs.Identifiers)
}
// Get related modules
n.relMods = make(types.ModuleSet, len(n.res.RefMods))
for i, rMod := range n.res.RefMods {
n.relMods[i], err = findComposeModule(ctx, pl.s, n.relNS.ID, pl.state.ParentResources, rMod.Identifiers)
if err != nil {
return err
}
if n.relMods[i] == nil {
return resource.ComposeModuleErrUnresolved(rMod.Identifiers)
}
}
// Try to get the original chart
n.chr, err = findComposeChartStore(ctx, pl.s, n.relNS.ID, makeGenericFilter(n.res.Identifiers()))
if err != nil {
return err
}
if n.chr != nil {
n.res.Res.ID = n.chr.ID
n.res.Res.NamespaceID = n.chr.NamespaceID
}
return nil
}
func (n *composeChart) Encode(ctx context.Context, pl *payload) (err error) {
res := n.res.Res
exists := n.chr != nil && n.chr.ID > 0
// Determine the ID
if res.ID <= 0 && exists {
res.ID = n.chr.ID
}
if res.ID <= 0 {
res.ID = NextID()
}
ts := n.res.Timestamps()
if ts != nil {
if ts.CreatedAt != nil {
res.CreatedAt = *ts.CreatedAt.T
} else {
res.CreatedAt = *now()
}
if ts.UpdatedAt != nil {
res.UpdatedAt = ts.UpdatedAt.T
}
if ts.DeletedAt != nil {
res.DeletedAt = ts.DeletedAt.T
}
}
// Namespace
res.NamespaceID = n.relNS.ID
if res.NamespaceID <= 0 {
ns := resource.FindComposeNamespace(pl.state.ParentResources, n.res.RefNs.Identifiers)
res.NamespaceID = ns.ID
}
if res.NamespaceID <= 0 {
return resource.ComposeNamespaceErrUnresolved(n.res.RefNs.Identifiers)
}
// Report modules
for i, r := range res.Config.Reports {
relMod := n.relMods[i]
if relMod == nil {
relMod = resource.FindComposeModule(pl.state.ParentResources, n.res.RefMods[i].Identifiers)
}
if relMod == nil || relMod.ID <= 0 {
return resource.ComposeModuleErrUnresolved(n.res.RefMods[i].Identifiers)
}
r.ModuleID = relMod.ID
}
// Evaluate the resource skip expression
// @todo expand available parameters; similar implementation to compose/types/record@Dict
if skip, err := basicSkipEval(ctx, n.cfg, !exists); err != nil {
return err
} else if skip {
return nil
}
// Create fresh chart
if !exists {
return store.CreateComposeChart(ctx, pl.s, res)
}
// Update existing chart
switch n.cfg.OnExisting {
case resource.Skip:
return nil
case resource.MergeLeft:
res = mergeComposeChart(n.chr, res)
case resource.MergeRight:
res = mergeComposeChart(res, n.chr)
}
err = store.UpdateComposeChart(ctx, pl.s, res)
if err != nil {
return err
}
n.res.Res = res
return nil
}

View File

@@ -0,0 +1,54 @@
package store
import (
"testing"
"time"
"github.com/cortezaproject/corteza/server/compose/types"
"github.com/stretchr/testify/require"
)
func TestComposeChart_Merger(t *testing.T) {
req := require.New(t)
now := time.Time{}
nowP := &time.Time{}
empty := &types.Chart{}
full := &types.Chart{
Handle: "handle",
Name: "name",
Config: types.ChartConfig{
Reports: []*types.ChartConfigReport{&types.ChartConfigReport{}},
ColorScheme: "colorScheme",
},
NamespaceID: 1,
CreatedAt: now,
UpdatedAt: nowP,
DeletedAt: nowP,
}
t.Run("merge on empty", func(t *testing.T) {
c := mergeComposeChart(empty, full)
req.Equal("name", c.Name)
req.Equal("handle", c.Handle)
req.Len(c.Config.Reports, 1)
req.Equal("colorScheme", c.Config.ColorScheme)
req.Equal(uint64(1), c.NamespaceID)
req.Equal(now, c.CreatedAt)
req.Equal(nowP, c.UpdatedAt)
req.Equal(nowP, c.DeletedAt)
})
t.Run("merge with empty", func(t *testing.T) {
c := mergeComposeChart(full, empty)
req.Equal("name", c.Name)
req.Equal("handle", c.Handle)
req.Len(c.Config.Reports, 1)
req.Equal("colorScheme", c.Config.ColorScheme)
req.Equal(uint64(0), c.NamespaceID)
req.Equal(now, c.CreatedAt)
req.Equal(nowP, c.UpdatedAt)
req.Equal(nowP, c.DeletedAt)
})
}

View File

@@ -0,0 +1,25 @@
package store
import (
"github.com/cortezaproject/corteza/server/compose/types"
"github.com/cortezaproject/corteza/server/pkg/envoy"
"github.com/cortezaproject/corteza/server/pkg/envoy/resource"
)
func newComposeChart(chr *types.Chart) *composeChart {
return &composeChart{
chr: chr,
}
}
func (chr *composeChart) MarshalEnvoy() ([]resource.Interface, error) {
refNs := resource.MakeNamespaceRef(chr.chr.NamespaceID, "", "")
refMods := make(resource.RefSet, 0, 2)
for _, r := range chr.chr.Config.Reports {
refMods = append(refMods, resource.MakeModuleRef(r.ModuleID, "", ""))
}
return envoy.CollectNodes(
resource.NewComposeChart(chr.chr, refNs, refMods),
)
}

View File

@@ -0,0 +1,210 @@
package store
import (
"context"
"fmt"
"github.com/cortezaproject/corteza/server/compose/types"
"github.com/cortezaproject/corteza/server/pkg/envoy/resource"
"github.com/cortezaproject/corteza/server/pkg/filter"
"github.com/cortezaproject/corteza/server/store"
)
type (
composeModule struct {
cfg *EncoderConfig
res *resource.ComposeModule
mod *types.Module
relNS *types.Namespace
recFields map[string]uint64
userFields map[string]uint64
}
)
// mergeComposeModuleFields merges b into a, prioritising a
func mergeComposeModuleFields(a, b types.ModuleFieldSet) types.ModuleFieldSet {
ff := a.Clone()
missing := make(types.ModuleFieldSet, 0, len(b))
for _, fb := range b {
for _, fa := range ff {
if fb.ID != fa.ID && fb.Name != fa.Name {
continue
}
fa.ModuleID = fb.ModuleID
fa.Place = fb.Place
if fa.Kind == "" {
fa.Kind = fb.Kind
}
if fa.Name == "" {
fa.Name = fb.Name
}
if fa.Label == "" {
fa.Label = fb.Label
}
if fa.Options == nil {
fa.Options = fb.Options
}
if fa.DefaultValue == nil {
fa.DefaultValue = fb.DefaultValue
}
if fa.Expressions.ValueExpr == "" {
fa.Expressions.ValueExpr = fb.Expressions.ValueExpr
}
if len(fa.Expressions.Sanitizers) == 0 {
fa.Expressions.Sanitizers = fb.Expressions.Sanitizers
}
if len(fa.Expressions.Validators) == 0 {
fa.Expressions.Validators = fb.Expressions.Validators
}
if len(fa.Expressions.Formatters) == 0 {
fa.Expressions.Formatters = fb.Expressions.Formatters
}
if fa.CreatedAt.IsZero() {
fa.CreatedAt = fb.CreatedAt
}
if fa.UpdatedAt == nil {
fa.UpdatedAt = fb.UpdatedAt
}
if fa.DeletedAt == nil {
fa.DeletedAt = fb.DeletedAt
}
goto out
}
missing = append(missing, fb)
out:
}
ff = append(ff, missing...)
return ff
}
// mergeComposeModule merges b into a, prioritising a
func mergeComposeModule(a, b *types.Module) *types.Module {
c := a.Clone()
if c.Handle == "" {
c.Handle = b.Handle
}
if c.Name == "" {
c.Name = b.Name
}
c.NamespaceID = b.NamespaceID
if c.CreatedAt.IsZero() {
c.CreatedAt = b.CreatedAt
}
if c.UpdatedAt == nil {
c.UpdatedAt = b.UpdatedAt
}
if c.DeletedAt == nil {
c.DeletedAt = b.DeletedAt
}
// I'll just compare the entire struct for now
if c.Meta == nil {
c.Meta = b.Meta
}
return c
}
// findComposeModule looks for the chart in the resources & the store
//
// Provided resources are prioritized.
func findComposeModule(ctx context.Context, s store.Storer, nsID uint64, rr resource.InterfaceSet, ii resource.Identifiers) (mod *types.Module, err error) {
mod = resource.FindComposeModule(rr, ii)
if mod != nil {
return mod, nil
}
if nsID <= 0 {
return nil, nil
}
// Go in the store
return findComposeModuleStore(ctx, s, nsID, makeGenericFilter(ii))
}
// findComposeModuleStore looks for the module in the store
func findComposeModuleStore(ctx context.Context, s store.Storer, nsID uint64, gf genericFilter) (mod *types.Module, err error) {
if nsID == 0 {
return nil, nil
}
if gf.id > 0 {
mod, err = store.LookupComposeModuleByID(ctx, s, gf.id)
if err != nil && err != store.ErrNotFound {
return nil, err
}
if mod != nil {
return
}
}
for _, i := range gf.identifiers {
mod, err = store.LookupComposeModuleByNamespaceIDHandle(ctx, s, nsID, i)
if err == store.ErrNotFound {
var mm types.ModuleSet
mm, _, err = store.SearchComposeModules(ctx, s, types.ModuleFilter{
NamespaceID: nsID,
Name: i,
Paging: filter.Paging{
Limit: 2,
},
})
if len(mm) > 1 {
return nil, resourceErrIdentifierNotUnique(i)
}
if len(mm) == 1 {
mod = mm[0]
}
}
if err != nil && err != store.ErrNotFound {
return nil, err
}
if mod != nil {
return
}
}
return nil, nil
}
// findComposeModuleFieldsStore looks for the module fields in the store
func findComposeModuleFieldsStore(ctx context.Context, s store.Storer, mod *types.Module) (types.ModuleFieldSet, error) {
if mod.ID <= 0 {
return mod.Fields, nil
}
ff, _, err := store.SearchComposeModuleFields(ctx, s, types.ModuleFieldFilter{
ModuleID: []uint64{mod.ID},
})
if err != nil {
return nil, err
}
_ = ff.Walk(func(f *types.ModuleField) error {
f.NamespaceID = mod.NamespaceID
return nil
})
return ff, nil
}
func composeModuleErrUnresolvedRecordField(ii resource.Identifiers) error {
return fmt.Errorf("record module field unresolved %v", ii.StringSlice())
}
func composeModuleErrUnresolvedUserField(ii resource.Identifiers) error {
return fmt.Errorf("user module field unresolved %v", ii.StringSlice())
}

View File

@@ -0,0 +1,356 @@
package store
import (
"context"
"strconv"
"strings"
"github.com/cortezaproject/corteza/server/compose/service"
"github.com/cortezaproject/corteza/server/pkg/dal"
"github.com/cortezaproject/corteza/server/pkg/revisions"
"github.com/cortezaproject/corteza/server/compose/types"
"github.com/cortezaproject/corteza/server/pkg/envoy/resource"
"github.com/cortezaproject/corteza/server/store"
systemTypes "github.com/cortezaproject/corteza/server/system/types"
)
func NewComposeModuleFromResource(res *resource.ComposeModule, cfg *EncoderConfig) resourceState {
return &composeModule{
cfg: mergeConfig(cfg, res.Config()),
res: res,
recFields: make(map[string]uint64),
userFields: make(map[string]uint64),
}
}
func (n *composeModule) Prepare(ctx context.Context, pl *payload) (err error) {
// Reset old identifiers
n.res.Res.ID = 0
n.res.Res.NamespaceID = 0
for _, rf := range n.res.ResFields {
rf.Res.ID = 0
rf.Res.ModuleID = 0
rf.Res.NamespaceID = 0
}
// Get related namespace
n.relNS, err = findComposeNamespace(ctx, pl.s, pl.state.ParentResources, n.res.RefNs.Identifiers)
if err != nil {
return err
}
if n.relNS == nil {
return resource.ComposeNamespaceErrUnresolved(n.res.RefNs.Identifiers)
}
n.res.Res.NamespaceID = n.relNS.ID
// Get related record field modules
for _, refMod := range n.res.RefMods {
var mod *types.Module
if n.relNS.ID > 0 {
mod, err = findComposeModuleStore(ctx, pl.s, n.relNS.ID, makeGenericFilter(refMod.Identifiers))
if err != nil {
return err
}
}
if mod == nil {
mod = resource.FindComposeModule(pl.state.ParentResources, refMod.Identifiers)
}
if mod == nil {
return composeModuleErrUnresolvedRecordField(refMod.Identifiers)
}
for _, i := range refMod.Identifiers {
n.recFields[i] = mod.ID
}
}
// Get related user field roles
for _, refRole := range n.res.RefRoles {
var rl *systemTypes.Role
rl, err = findRoleStore(ctx, pl.s, makeGenericFilter(refRole.Identifiers))
if err != nil {
return err
}
if rl == nil {
rl = resource.FindRole(pl.state.ParentResources, refRole.Identifiers)
}
if rl == nil {
return composeModuleErrUnresolvedUserField(refRole.Identifiers)
}
for _, i := range refRole.Identifiers {
n.userFields[i] = rl.ID
}
}
// Can't do anything else, since the NS doesn't yet exist
if n.relNS.ID <= 0 {
return nil
}
// Try to get the original module
n.mod, err = findComposeModuleStore(ctx, pl.s, n.relNS.ID, makeGenericFilter(n.res.Identifiers()))
if err != nil {
return err
}
// Nothing else to do
if n.mod == nil {
return nil
}
// Get the original module fields
// These are used later for some merging logic
n.mod.Fields, err = findComposeModuleFieldsStore(ctx, pl.s, n.mod)
if err != nil {
return err
}
if n.mod != nil {
n.res.Res.ID = n.mod.ID
n.res.Res.NamespaceID = n.mod.NamespaceID
}
return nil
}
func (n *composeModule) Encode(ctx context.Context, pl *payload) (err error) {
res := n.res.Res
exists := n.mod != nil && n.mod.ID > 0
// Determine the ID
if res.ID <= 0 && exists {
res.ID = n.mod.ID
}
if res.ID <= 0 {
res.ID = NextID()
}
ns := n.relNS
res.NamespaceID = ns.ID
if res.NamespaceID <= 0 {
ns = resource.FindComposeNamespace(pl.state.ParentResources, n.res.RefNs.Identifiers)
res.NamespaceID = ns.ID
}
if res.NamespaceID <= 0 {
return resource.ComposeNamespaceErrUnresolved(n.res.RefNs.Identifiers)
}
if pl.state.Conflicting {
return nil
}
ts := n.res.Timestamps()
if ts != nil {
if ts.CreatedAt != nil {
res.CreatedAt = *ts.CreatedAt.T
} else {
res.CreatedAt = *now()
}
if ts.UpdatedAt != nil {
res.UpdatedAt = ts.UpdatedAt.T
}
if ts.DeletedAt != nil {
res.DeletedAt = ts.DeletedAt.T
}
}
// Fields
var originalFields types.ModuleFieldSet
if n.mod != nil && n.mod.Fields != nil {
originalFields = n.mod.Fields
} else {
originalFields = make(types.ModuleFieldSet, 0)
}
// Get max validatorID for later use
vvID := make([]uint64, len(res.Fields))
for i, f := range res.Fields {
for _, v := range f.Expressions.Validators {
if vvID[i] < v.ValidatorID {
vvID[i] = v.ValidatorID
}
}
}
for i, f := range res.Fields {
of := originalFields.FindByName(f.Name)
if of != nil {
f.ID = of.ID
} else {
f.ID = NextID()
}
f.ModuleID = res.ID
f.Place = i
f.DeletedAt = nil
f.CreatedAt = *now()
// Assure validatorIDs
for j, v := range f.Expressions.Validators {
if v.ValidatorID == 0 {
vvID[i] += 1
v.ValidatorID = vvID[i]
f.Expressions.Validators[j] = v
}
}
if f.Options != nil && f.Kind == "Record" {
refMod := f.Options.String("module")
if refMod == "" {
refMod = f.Options.String("moduleID")
}
modID := n.recFields[refMod]
if modID <= 0 {
ii := resource.MakeIdentifiers(refMod)
mod := resource.FindComposeModule(pl.state.ParentResources, ii)
if mod == nil || mod.ID <= 0 {
return composeModuleErrUnresolvedRecordField(ii)
}
modID = mod.ID
}
f.Options["moduleID"] = strconv.FormatUint(modID, 10)
delete(f.Options, "module")
}
if f.Options != nil && f.Kind == "User" {
roles := resource.ComposeModuleFieldExtractUserFieldRoles(f.Options["roles"])
if len(roles) == 0 {
roles = resource.ComposeModuleFieldExtractUserFieldRoles(f.Options["role"])
}
if len(roles) == 0 {
roles = resource.ComposeModuleFieldExtractUserFieldRoles(f.Options["roleID"])
}
var out []string
for _, r := range roles {
roleID := n.userFields[r]
if roleID <= 0 {
ii := resource.MakeIdentifiers(r)
role := resource.FindRole(pl.state.ParentResources, ii)
if role == nil || role.ID == 0 {
return composeModuleErrUnresolvedUserField(ii)
}
roleID = role.ID
}
out = append(out, strconv.FormatUint(roleID, 10))
}
f.Options["roles"] = out
delete(f.Options, "role")
delete(f.Options, "roleID")
}
}
// Evaluate the resource skip expression
// @todo expand available parameters; similar implementation to compose/types/record@Dict
if skip, err := basicSkipEval(ctx, n.cfg, !exists); err != nil {
return err
} else if skip {
return nil
}
// Create a fresh module
if !exists {
err = store.CreateComposeModule(ctx, pl.s, res)
if err != nil {
return err
}
err = store.CreateComposeModuleField(ctx, pl.s, res.Fields...)
if err != nil {
return err
}
} else {
// Update existing module
switch n.cfg.OnExisting {
case resource.Skip:
return nil
case resource.MergeLeft:
res = mergeComposeModule(n.mod, res)
res.Fields = mergeComposeModuleFields(n.mod.Fields, res.Fields)
case resource.MergeRight:
res = mergeComposeModule(res, n.mod)
res.Fields = mergeComposeModuleFields(res.Fields, n.mod.Fields)
}
err = store.UpdateComposeModule(ctx, pl.s, res)
if err != nil {
return err
}
err = store.DeleteComposeModuleField(ctx, pl.s, n.mod.Fields...)
if err != nil {
return err
}
err = store.CreateComposeModuleField(ctx, pl.s, res.Fields...)
if err != nil {
return err
}
n.res.Res = res
}
var (
model *dal.Model
con = pl.dal.GetConnectionByID(0)
)
// convert module to model and assume compose_record for default ident
if model, err = service.ModuleToModel(ns, res, "compose_record"); err != nil {
return
}
model.ConnectionID = con.ID
// @note code copied from the service/module.go
// Set base constraints
if model.Ident == "compose_record" {
model.Constraints = map[string][]any{
"moduleID": {res.ID},
"namespaceID": {res.NamespaceID},
}
}
// replace all partition replacement pairs
rpl := strings.NewReplacer("{{module}}", res.Handle, "{{namespace}}", ns.Slug)
model.Ident = rpl.Replace(model.Ident)
// @todo validate ident with connection's ident validator
// Create the revisions model if enabled
if res.Config.RecordRevisions.Enabled {
rModel := revisions.Model()
// reuse the connection from the module
rModel.ConnectionID = con.ID
rModel.Resource = model.Resource
rModel.ResourceID = NextID()
if rModel.Ident = res.Config.RecordRevisions.Ident; rModel.Ident == "" {
rModel.Ident = "compose_record_revisions"
}
rModel.Ident = rpl.Replace(rModel.Ident)
// @todo validate ident with connection's ident validator
if _, err = pl.dal.ReplaceModel(ctx, nil, rModel); err != nil {
return
}
}
if _, err = pl.dal.ReplaceModel(ctx, nil, model); err != nil {
return
}
return nil
}

View File

@@ -0,0 +1,111 @@
package store
import (
"testing"
"time"
"github.com/cortezaproject/corteza/server/compose/types"
st "github.com/jmoiron/sqlx/types"
"github.com/stretchr/testify/require"
)
func TestComposeModule_Merger(t *testing.T) {
req := require.New(t)
now := time.Time{}
nowP := &time.Time{}
empty := &types.Module{}
full := &types.Module{
Handle: "handle",
Name: "name",
Meta: st.JSONText{},
NamespaceID: 1,
CreatedAt: now,
UpdatedAt: nowP,
DeletedAt: nowP,
}
t.Run("merge on empty", func(t *testing.T) {
c := mergeComposeModule(empty, full)
req.Equal("name", c.Name)
req.Equal("handle", c.Handle)
req.NotNil(c.Meta)
req.Equal(uint64(1), c.NamespaceID)
req.Equal(now, c.CreatedAt)
req.Equal(nowP, c.UpdatedAt)
req.Equal(nowP, c.DeletedAt)
})
t.Run("merge with empty", func(t *testing.T) {
c := mergeComposeModule(full, empty)
req.Equal("name", c.Name)
req.Equal("handle", c.Handle)
req.NotNil(c.Meta)
req.Equal(uint64(0), c.NamespaceID)
req.Equal(now, c.CreatedAt)
req.Equal(nowP, c.UpdatedAt)
req.Equal(nowP, c.DeletedAt)
})
}
func TestComposeModuleField_Merger(t *testing.T) {
req := require.New(t)
now := time.Time{}
nowP := &time.Time{}
empty := types.ModuleFieldSet{}
full := types.ModuleFieldSet{
&types.ModuleField{
ModuleID: 1,
Place: 2,
Kind: "kind",
Name: "name",
Label: "label",
Options: types.ModuleFieldOptions{},
DefaultValue: types.RecordValueSet{},
Expressions: types.ModuleFieldExpr{},
CreatedAt: now,
UpdatedAt: nowP,
DeletedAt: nowP,
},
}
t.Run("merge on empty", func(t *testing.T) {
cc := mergeComposeModuleFields(empty, full)
req.Len(cc, 1)
c := cc[0]
req.Equal(uint64(1), c.ModuleID)
req.Equal(2, c.Place)
req.Equal("kind", c.Kind)
req.Equal("name", c.Name)
req.Equal("label", c.Label)
req.NotNil(c.Options)
req.NotNil(c.DefaultValue)
req.NotNil(c.Expressions)
req.Equal(now, c.CreatedAt)
req.Equal(nowP, c.UpdatedAt)
req.Equal(nowP, c.DeletedAt)
})
t.Run("merge with empty", func(t *testing.T) {
cc := mergeComposeModuleFields(full, empty)
req.Len(cc, 1)
c := cc[0]
req.Equal(uint64(1), c.ModuleID)
req.Equal(2, c.Place)
req.Equal("kind", c.Kind)
req.Equal("name", c.Name)
req.Equal("label", c.Label)
req.NotNil(c.Options)
req.NotNil(c.DefaultValue)
req.NotNil(c.Expressions)
req.Equal(now, c.CreatedAt)
req.Equal(nowP, c.UpdatedAt)
req.Equal(nowP, c.DeletedAt)
})
}

View File

@@ -0,0 +1,28 @@
package store
import (
"github.com/cortezaproject/corteza/server/compose/types"
"github.com/cortezaproject/corteza/server/pkg/envoy"
"github.com/cortezaproject/corteza/server/pkg/envoy/resource"
)
func newComposeModule(mod *types.Module) *composeModule {
return &composeModule{
mod: mod,
}
}
func (mod *composeModule) MarshalEnvoy() ([]resource.Interface, error) {
refNs := resource.MakeNamespaceRef(mod.mod.NamespaceID, "", "")
refMod := resource.MakeModuleRef(mod.mod.ID, mod.mod.Handle, mod.mod.Name)
rMod := resource.NewComposeModule(mod.mod, refNs)
for _, f := range mod.mod.Fields {
r := resource.NewComposeModuleField(f, refNs, refMod)
rMod.AddField(r)
}
return envoy.CollectNodes(
rMod,
)
}

View File

@@ -0,0 +1,105 @@
package store
import (
"context"
"github.com/cortezaproject/corteza/server/compose/types"
"github.com/cortezaproject/corteza/server/pkg/envoy/resource"
"github.com/cortezaproject/corteza/server/pkg/filter"
"github.com/cortezaproject/corteza/server/store"
)
type (
composeNamespace struct {
cfg *EncoderConfig
res *resource.ComposeNamespace
ns *types.Namespace
}
composeNamespaceSet []*composeNamespace
)
// mergeComposeNamespaces merges b into a, prioritising a
func mergeComposeNamespaces(a, b *types.Namespace) *types.Namespace {
c := a.Clone()
if c.Name == "" {
c.Name = b.Name
}
if c.Slug == "" {
c.Slug = b.Slug
}
if c.CreatedAt.IsZero() {
c.CreatedAt = b.CreatedAt
}
if c.UpdatedAt == nil {
c.UpdatedAt = b.UpdatedAt
}
if c.DeletedAt == nil {
c.DeletedAt = b.DeletedAt
}
// I'll just compare the entire struct for now
if c.Meta == (types.NamespaceMeta{}) {
c.Meta = b.Meta
}
return c
}
// findComposeNamespace looks for the namespace in the resources & the store
//
// Provided resources are prioritized.
func findComposeNamespace(ctx context.Context, s store.Storer, rr resource.InterfaceSet, ii resource.Identifiers) (ns *types.Namespace, err error) {
ns = resource.FindComposeNamespace(rr, ii)
if ns != nil {
return ns, nil
}
return findComposeNamespaceStore(ctx, s, makeGenericFilter(ii))
}
// findComposeNamespaceStore looks for the namespace in the store
func findComposeNamespaceStore(ctx context.Context, s store.Storer, gf genericFilter) (ns *types.Namespace, err error) {
if gf.id > 0 {
ns, err = store.LookupComposeNamespaceByID(ctx, s, gf.id)
if err != nil && err != store.ErrNotFound {
return nil, err
}
if ns != nil {
return
}
}
for _, i := range gf.identifiers {
ns, err = store.LookupComposeNamespaceBySlug(ctx, s, i)
if err == store.ErrNotFound {
var nn types.NamespaceSet
nn, _, err = store.SearchComposeNamespaces(ctx, s, types.NamespaceFilter{
Name: i,
Paging: filter.Paging{
Limit: 2,
},
})
if len(nn) > 1 {
return nil, resourceErrIdentifierNotUnique(i)
}
if len(nn) == 1 {
ns = nn[0]
}
}
if err != nil && err != store.ErrNotFound {
return nil, err
}
if ns != nil {
return
}
}
return nil, nil
}

View File

@@ -0,0 +1,93 @@
package store
import (
"context"
"github.com/cortezaproject/corteza/server/pkg/envoy/resource"
"github.com/cortezaproject/corteza/server/store"
)
func newComposeNamespaceFromResource(res *resource.ComposeNamespace, cfg *EncoderConfig) resourceState {
return &composeNamespace{
cfg: mergeConfig(cfg, res.Config()),
res: res,
}
}
func (n *composeNamespace) Prepare(ctx context.Context, pl *payload) (err error) {
// Reset old identifiers
n.res.Res.ID = 0
// Try to get the original namespace
n.ns, err = findComposeNamespaceStore(ctx, pl.s, makeGenericFilter(n.res.Identifiers()))
if err != nil {
return err
}
if n.ns != nil {
n.res.Res.ID = n.ns.ID
}
return nil
}
func (n *composeNamespace) Encode(ctx context.Context, pl *payload) (err error) {
res := n.res.Res
exists := n.ns != nil && n.ns.ID > 0
// Determine the ID
if res.ID <= 0 && exists {
res.ID = n.ns.ID
}
if res.ID <= 0 {
res.ID = NextID()
}
ts := n.res.Timestamps()
if ts != nil {
if ts.CreatedAt != nil {
res.CreatedAt = *ts.CreatedAt.T
} else {
res.CreatedAt = *now()
}
if ts.UpdatedAt != nil {
res.UpdatedAt = ts.UpdatedAt.T
}
if ts.DeletedAt != nil {
res.DeletedAt = ts.DeletedAt.T
}
}
// Evaluate the resource skip expression
// @todo expand available parameters; similar implementation to compose/types/record@Dict
if skip, err := basicSkipEval(ctx, n.cfg, !exists); err != nil {
return err
} else if skip {
return nil
}
// Create a fresh namespace
if !exists {
return store.CreateComposeNamespace(ctx, pl.s, res)
}
// Update existing namespace
switch n.cfg.OnExisting {
case resource.Skip:
return nil
case resource.MergeLeft:
res = mergeComposeNamespaces(n.ns, res)
case resource.MergeRight:
res = mergeComposeNamespaces(res, n.ns)
}
err = store.UpdateComposeNamespace(ctx, pl.s, res)
if err != nil {
return err
}
n.res.Res = res
return nil
}

View File

@@ -0,0 +1,53 @@
package store
import (
"testing"
"time"
"github.com/cortezaproject/corteza/server/compose/types"
"github.com/stretchr/testify/require"
)
func TestComposeNamespace_Merger(t *testing.T) {
req := require.New(t)
now := time.Time{}
nowP := &time.Time{}
empty := &types.Namespace{}
full := &types.Namespace{
Name: "name",
Slug: "slug",
Enabled: false,
Meta: types.NamespaceMeta{
Description: "dsc",
Subtitle: "sub",
},
CreatedAt: now,
UpdatedAt: nowP,
DeletedAt: nowP,
}
t.Run("merge on empty", func(t *testing.T) {
c := mergeComposeNamespaces(empty, full)
req.Equal("name", c.Name)
req.Equal("slug", c.Slug)
req.Equal("dsc", c.Meta.Description)
req.Equal("sub", c.Meta.Subtitle)
req.Equal(now, c.CreatedAt)
req.Equal(nowP, c.UpdatedAt)
req.Equal(nowP, c.DeletedAt)
})
t.Run("merge with empty", func(t *testing.T) {
c := mergeComposeNamespaces(full, empty)
req.Equal("name", c.Name)
req.Equal("slug", c.Slug)
req.Equal("dsc", c.Meta.Description)
req.Equal("sub", c.Meta.Subtitle)
req.Equal(now, c.CreatedAt)
req.Equal(nowP, c.UpdatedAt)
req.Equal(nowP, c.DeletedAt)
})
}

View File

@@ -0,0 +1,19 @@
package store
import (
"github.com/cortezaproject/corteza/server/compose/types"
"github.com/cortezaproject/corteza/server/pkg/envoy"
"github.com/cortezaproject/corteza/server/pkg/envoy/resource"
)
func newComposeNamespace(ns *types.Namespace) *composeNamespace {
return &composeNamespace{
ns: ns,
}
}
func (ns *composeNamespace) MarshalEnvoy() ([]resource.Interface, error) {
return envoy.CollectNodes(
resource.NewComposeNamespace(ns.ns),
)
}

View File

@@ -0,0 +1,130 @@
package store
import (
"context"
atypes "github.com/cortezaproject/corteza/server/automation/types"
"github.com/cortezaproject/corteza/server/compose/types"
"github.com/cortezaproject/corteza/server/pkg/envoy/resource"
"github.com/cortezaproject/corteza/server/pkg/filter"
"github.com/cortezaproject/corteza/server/store"
)
type (
composePage struct {
cfg *EncoderConfig
res *resource.ComposePage
pg *types.Page
relNS *types.Namespace
relMod *types.Module
relParent *types.Page
relWfs map[string]*atypes.Workflow
relMods map[string]*types.Module
relCharts map[string]*types.Chart
}
)
// mergeComposePage merges b into a, prioritising a
func mergeComposePage(a, b *types.Page) *types.Page {
c := a.Clone()
c.SelfID = b.SelfID
c.NamespaceID = b.NamespaceID
c.ModuleID = b.ModuleID
c.Weight = b.Weight
if c.Handle == "" {
c.Handle = b.Handle
}
if c.Title == "" {
c.Title = b.Title
}
if c.Description == "" {
c.Description = b.Description
}
if len(c.Blocks) <= 0 {
c.Blocks = b.Blocks
}
if len(c.Children) <= 0 {
c.Children = b.Children
}
if c.CreatedAt.IsZero() {
c.CreatedAt = b.CreatedAt
}
if c.UpdatedAt == nil {
c.UpdatedAt = b.UpdatedAt
}
if c.DeletedAt == nil {
c.DeletedAt = b.DeletedAt
}
return c
}
// findComposePage looks for the page in the resources & the store
//
// Provided resources are prioritized.
func findComposePage(ctx context.Context, s store.Storer, nsID uint64, rr resource.InterfaceSet, ii resource.Identifiers) (pg *types.Page, err error) {
pg = resource.FindComposePage(rr, ii)
if pg != nil {
return pg, nil
}
if nsID <= 0 {
return nil, nil
}
// Go in the store
return findComposePageStore(ctx, s, nsID, makeGenericFilter(ii))
}
// findComposePageStore looks for the page in the store
func findComposePageStore(ctx context.Context, s store.Storer, nsID uint64, gf genericFilter) (pg *types.Page, err error) {
if nsID == 0 {
return nil, nil
}
if gf.id > 0 {
pg, err = store.LookupComposePageByID(ctx, s, gf.id)
if err != nil && err != store.ErrNotFound {
return nil, err
}
if pg != nil {
return
}
}
for _, i := range gf.identifiers {
pg, err = store.LookupComposePageByNamespaceIDHandle(ctx, s, nsID, i)
if err == store.ErrNotFound {
var pp types.PageSet
pp, _, err = store.SearchComposePages(ctx, s, types.PageFilter{
NamespaceID: nsID,
Title: i,
Paging: filter.Paging{
Limit: 2,
},
})
if len(pp) > 1 {
return nil, resourceErrIdentifierNotUnique(i)
}
if len(pp) == 1 {
pg = pp[0]
}
}
if err != nil && err != store.ErrNotFound {
return nil, err
}
if pg != nil {
return
}
}
return nil, nil
}

View File

@@ -0,0 +1,369 @@
package store
import (
"context"
"strconv"
atypes "github.com/cortezaproject/corteza/server/automation/types"
"github.com/cortezaproject/corteza/server/compose/types"
"github.com/cortezaproject/corteza/server/pkg/envoy/resource"
"github.com/cortezaproject/corteza/server/store"
)
func newComposePageFromResource(res *resource.ComposePage, cfg *EncoderConfig) resourceState {
return &composePage{
cfg: mergeConfig(cfg, res.Config()),
res: res,
relWfs: make(map[string]*atypes.Workflow),
relMods: make(map[string]*types.Module),
relCharts: make(map[string]*types.Chart),
}
}
func (n *composePage) Prepare(ctx context.Context, pl *payload) (err error) {
// Reset old identifiers
n.res.Res.ID = 0
n.res.Res.SelfID = 0
n.res.Res.NamespaceID = 0
n.res.Res.ModuleID = 0
// Get related namespace
n.relNS, err = findComposeNamespace(ctx, pl.s, pl.state.ParentResources, n.res.RefNs.Identifiers)
if err != nil {
return err
}
if n.relNS == nil {
return resource.ComposeNamespaceErrUnresolved(n.res.RefNs.Identifiers)
}
// Get related module
// If this isn't a record page, there is no related module
if n.res.RefMod != nil {
n.relMod, err = findComposeModule(ctx, pl.s, n.relNS.ID, pl.state.ParentResources, n.res.RefMod.Identifiers)
if err != nil {
return err
}
if n.relMod == nil {
return resource.ComposeModuleErrUnresolved(n.res.RefMod.Identifiers)
}
}
// Get parent page
if n.res.RefParent != nil {
n.relParent, err = findComposePage(ctx, pl.s, n.relNS.ID, pl.state.ParentResources, n.res.RefParent.Identifiers)
if err != nil {
return err
}
if n.relParent == nil {
return resource.ComposePageErrUnresolved(n.res.RefParent.Identifiers)
}
}
// Get related workflows
for _, wfr := range n.res.WfRefs {
wf, err := findAutomationWorkflow(ctx, pl.s, pl.state.ParentResources, wfr.Identifiers)
if err != nil {
return err
}
if wf == nil {
return resource.AutomationWorkflowErrUnresolved(wfr.Identifiers)
}
for _, id := range wfr.Identifiers {
n.relWfs[id] = wf
}
}
// Get other related modules
for _, mr := range n.res.ModRefs {
mod, err := findComposeModule(ctx, pl.s, n.relNS.ID, pl.state.ParentResources, mr.Identifiers)
if err != nil {
return err
}
if mod == nil {
return resource.ComposeModuleErrUnresolved(mr.Identifiers)
}
for _, id := range mr.Identifiers {
n.relMods[id] = mod
}
}
// Get related charts
for _, refChart := range n.res.RefCharts {
chr, err := findComposeChart(ctx, pl.s, n.relNS.ID, pl.state.ParentResources, refChart.Identifiers)
if err != nil {
return err
}
if chr == nil {
return resource.ComposeChartErrUnresolved(refChart.Identifiers)
}
for _, id := range refChart.Identifiers {
n.relCharts[id] = chr
}
}
// Try to get the original page
n.pg, err = findComposePageStore(ctx, pl.s, n.relNS.ID, makeGenericFilter(n.res.Identifiers()))
if err != nil {
return err
}
if n.pg != nil {
n.res.Res.ID = n.pg.ID
n.res.Res.NamespaceID = n.pg.NamespaceID
}
return nil
}
func (n *composePage) Encode(ctx context.Context, pl *payload) (err error) {
res := n.res.Res
exists := n.pg != nil && n.pg.ID > 0
// Determine the ID
if res.ID <= 0 && exists {
res.ID = n.pg.ID
}
if res.ID <= 0 {
res.ID = NextID()
}
ts := n.res.Timestamps()
if ts != nil {
if ts.CreatedAt != nil {
res.CreatedAt = *ts.CreatedAt.T
} else {
res.CreatedAt = *now()
}
if ts.UpdatedAt != nil {
res.UpdatedAt = ts.UpdatedAt.T
}
if ts.DeletedAt != nil {
res.DeletedAt = ts.DeletedAt.T
}
}
// Namespace
res.NamespaceID = n.relNS.ID
if res.NamespaceID <= 0 {
ns := resource.FindComposeNamespace(pl.state.ParentResources, n.res.RefNs.Identifiers)
res.NamespaceID = ns.ID
}
if res.NamespaceID <= 0 {
return resource.ComposeNamespaceErrUnresolved(n.res.RefNs.Identifiers)
}
// Module?
if n.res.RefMod != nil {
res.ModuleID = n.relMod.ID
if res.ModuleID <= 0 {
mod := resource.FindComposeModule(pl.state.ParentResources, n.res.RefMod.Identifiers)
res.ModuleID = mod.ID
}
}
// Parent?
if n.res.RefParent != nil {
res.SelfID = n.relParent.ID
if res.SelfID <= 0 {
mod := resource.FindComposePage(pl.state.ParentResources, n.res.RefParent.Identifiers)
res.SelfID = mod.ID
}
}
// Blocks
getModID := func(id string) uint64 {
mod := n.relMods[id]
if mod == nil || mod.ID <= 0 {
mod = resource.FindComposeModule(pl.state.ParentResources, resource.MakeIdentifiers(id))
if mod == nil || mod.ID <= 0 {
return 0
}
}
return mod.ID
}
getWfID := func(id string) uint64 {
wf := n.relWfs[id]
if wf == nil || wf.ID <= 0 {
wf = resource.FindAutomationWorkflow(pl.state.ParentResources, resource.MakeIdentifiers(id))
if wf == nil || wf.ID <= 0 {
return 0
}
}
return wf.ID
}
getChartID := func(id string) uint64 {
chr := n.relCharts[id]
if chr == nil || chr.ID <= 0 {
chr = resource.FindComposeChart(pl.state.ParentResources, resource.MakeIdentifiers(id))
if chr == nil || chr.ID <= 0 {
return 0
}
}
return chr.ID
}
// Quick utility to extract references from options
ss := func(m map[string]interface{}, kk ...string) string {
for _, k := range kk {
if vr, has := m[k]; has {
v, _ := vr.(string)
return v
}
}
return ""
}
// Get max blockID for later use
blockID := uint64(0)
for _, b := range res.Blocks {
if b.BlockID > blockID {
blockID = b.BlockID
}
}
for i, b := range res.Blocks {
// Assure blockIDs
if b.BlockID == 0 {
blockID++
b.BlockID = blockID
res.Blocks[i] = b
}
switch b.Kind {
case "RecordList":
id := ss(b.Options, "module", "moduleID")
if id == "" {
continue
}
mID := getModID(id)
if mID <= 0 {
return resource.ComposeModuleErrUnresolved(resource.MakeIdentifiers(id))
}
b.Options["moduleID"] = strconv.FormatUint(mID, 10)
delete(b.Options, "module")
case "Automation":
bb, _ := b.Options["buttons"].([]interface{})
for _, b := range bb {
button, _ := b.(map[string]interface{})
id := ss(button, "workflow", "workflowID")
if id == "" {
continue
}
wfID := getWfID(id)
if wfID <= 0 {
return resource.AutomationWorkflowErrUnresolved(resource.MakeIdentifiers(id))
}
button["workflowID"] = strconv.FormatUint(wfID, 10)
delete(button, "workflow")
}
case "RecordOrganizer":
id := ss(b.Options, "module", "moduleID")
if id == "" {
continue
}
mID := getModID(id)
if mID <= 0 {
return resource.ComposeModuleErrUnresolved(resource.MakeIdentifiers(id))
}
b.Options["moduleID"] = strconv.FormatUint(mID, 10)
delete(b.Options, "module")
case "Calendar":
ff, _ := b.Options["feeds"].([]interface{})
for _, f := range ff {
feed, _ := f.(map[string]interface{})
fOpts, _ := (feed["options"]).(map[string]interface{})
id := ss(fOpts, "module", "moduleID")
if id == "" {
continue
}
mID := getModID(id)
if mID <= 0 {
return resource.ComposeModuleErrUnresolved(resource.MakeIdentifiers(id))
}
fOpts["moduleID"] = strconv.FormatUint(mID, 10)
delete(fOpts, "module")
}
case "Chart":
id := ss(b.Options, "chart", "chartID")
if id == "" {
continue
}
chrID := getChartID(id)
if chrID == 0 {
return resource.ComposeChartErrUnresolved(resource.MakeIdentifiers(id))
}
b.Options["chartID"] = strconv.FormatUint(chrID, 10)
delete(b.Options, "chart")
case "Metric":
mm, _ := b.Options["metrics"].([]interface{})
for _, m := range mm {
mops, _ := m.(map[string]interface{})
id := ss(mops, "module", "moduleID")
if id == "" {
continue
}
mID := getModID(id)
if mID <= 0 {
return resource.ComposeModuleErrUnresolved(resource.MakeIdentifiers(id))
}
mops["moduleID"] = strconv.FormatUint(mID, 10)
delete(mops, "module")
}
case "Comment":
id := ss(b.Options, "module", "moduleID")
if id == "" {
continue
}
mID := getModID(id)
if mID <= 0 {
return resource.ComposeModuleErrUnresolved(resource.MakeIdentifiers(id))
}
b.Options["moduleID"] = strconv.FormatUint(mID, 10)
delete(b.Options, "module")
}
}
// Evaluate the resource skip expression
// @todo expand available parameters; similar implementation to compose/types/record@Dict
if skip, err := basicSkipEval(ctx, n.cfg, !exists); err != nil {
return err
} else if skip {
return nil
}
// Create a fresh page
if !exists {
return store.CreateComposePage(ctx, pl.s, res)
}
// Update existing page
switch n.cfg.OnExisting {
case resource.Skip:
return nil
case resource.MergeLeft:
res = mergeComposePage(n.pg, res)
case resource.MergeRight:
res = mergeComposePage(res, n.pg)
}
err = store.UpdateComposePage(ctx, pl.s, res)
if err != nil {
return err
}
n.res.Res = res
return nil
}

View File

@@ -0,0 +1,69 @@
package store
import (
"testing"
"time"
"github.com/cortezaproject/corteza/server/compose/types"
"github.com/stretchr/testify/require"
)
func TestComposePage_Merger(t *testing.T) {
req := require.New(t)
now := time.Time{}
nowP := &time.Time{}
empty := &types.Page{}
full := &types.Page{
SelfID: 1,
NamespaceID: 2,
ModuleID: 3,
Handle: "handle",
Title: "title",
Description: "description",
Blocks: types.PageBlocks{types.PageBlock{}},
Children: types.PageSet{&types.Page{}},
Weight: 4,
CreatedAt: now,
UpdatedAt: nowP,
DeletedAt: nowP,
}
t.Run("merge on empty", func(t *testing.T) {
c := mergeComposePage(empty, full)
req.Equal(uint64(1), c.SelfID)
req.Equal(uint64(2), c.NamespaceID)
req.Equal(uint64(3), c.ModuleID)
req.Equal("handle", c.Handle)
req.Equal("title", c.Title)
req.Equal("description", c.Description)
req.Len(c.Blocks, 1)
req.Len(c.Children, 1)
req.Equal(4, c.Weight)
req.Equal(now, c.CreatedAt)
req.Equal(nowP, c.UpdatedAt)
req.Equal(nowP, c.DeletedAt)
})
t.Run("merge with empty", func(t *testing.T) {
c := mergeComposePage(full, empty)
req.Equal(uint64(0), c.SelfID)
req.Equal(uint64(0), c.NamespaceID)
req.Equal(uint64(0), c.ModuleID)
req.Equal("handle", c.Handle)
req.Equal("title", c.Title)
req.Equal("description", c.Description)
req.Len(c.Blocks, 1)
req.Len(c.Children, 1)
req.Equal(0, c.Weight)
req.Equal(now, c.CreatedAt)
req.Equal(nowP, c.UpdatedAt)
req.Equal(nowP, c.DeletedAt)
})
}

View File

@@ -0,0 +1,31 @@
package store
import (
"github.com/cortezaproject/corteza/server/compose/types"
"github.com/cortezaproject/corteza/server/pkg/envoy"
"github.com/cortezaproject/corteza/server/pkg/envoy/resource"
)
func newComposePage(pg *types.Page) *composePage {
return &composePage{
pg: pg,
}
}
func (pg *composePage) MarshalEnvoy() ([]resource.Interface, error) {
var (
refNs = resource.MakeNamespaceRef(pg.pg.NamespaceID, "", "")
refMod *resource.Ref
refParent *resource.Ref
)
if pg.pg.ModuleID > 0 {
refMod = resource.MakeModuleRef(pg.pg.ModuleID, "", "")
}
if pg.pg.SelfID > 0 {
refParent = resource.MakePageRef(pg.pg.SelfID, "", "")
}
return envoy.CollectNodes(
resource.NewComposePage(pg.pg, refNs, refMod, refParent),
)
}

View File

@@ -0,0 +1,35 @@
package store
import (
"github.com/cortezaproject/corteza/server/compose/types"
"github.com/cortezaproject/corteza/server/pkg/envoy/resource"
)
type (
composeRecordAux struct {
refMod string
relMod *types.Module
refNs string
relUsers resource.UserstampIndex
walker resource.CrsWalker
}
composeRecord struct {
cfg *EncoderConfig
res *resource.ComposeRecord
rec *composeRecordAux
relNS *types.Namespace
relMod *types.Module
fieldModRef map[string]resource.Identifiers
// module identifier -> record identifier -> recordID
externalRef map[string]map[string]uint64
recMap map[string]*types.Record
// Little helper flag for conditional encoding
missing bool
}
)

View File

@@ -0,0 +1,471 @@
package store
import (
"context"
"fmt"
"strconv"
"time"
"github.com/cortezaproject/corteza/server/compose/dalutils"
"github.com/cortezaproject/corteza/server/compose/service"
"github.com/cortezaproject/corteza/server/compose/service/values"
composeTypes "github.com/cortezaproject/corteza/server/compose/types"
"github.com/cortezaproject/corteza/server/pkg/envoy/resource"
"github.com/cortezaproject/corteza/server/pkg/filter"
"github.com/cortezaproject/corteza/server/store"
systemTypes "github.com/cortezaproject/corteza/server/system/types"
)
var (
rvSanitizer = values.Sanitizer()
rvValidator = values.Validator()
rvFormatter = values.Formatter()
)
func NewComposeRecordFromResource(res *resource.ComposeRecord, cfg *EncoderConfig) resourceState {
return &composeRecord{
cfg: cfg,
fieldModRef: make(map[string]resource.Identifiers),
externalRef: make(map[string]map[string]uint64),
recMap: make(map[string]*composeTypes.Record),
res: res,
}
}
func (n *composeRecord) Prepare(ctx context.Context, pl *payload) (err error) {
// Get related namespace
n.relNS, err = findComposeNamespace(ctx, pl.s, pl.state.ParentResources, n.res.RefNs.Identifiers)
if err != nil {
return err
}
if n.relNS == nil {
return resource.ComposeNamespaceErrUnresolved(n.res.RefNs.Identifiers)
}
n.missing = true
n.relMod = resource.FindComposeModule(pl.state.ParentResources, n.res.RefMod.Identifiers)
if n.relMod == nil && n.relNS.ID > 0 {
n.relMod, err = findComposeModuleStore(ctx, pl.s, n.relNS.ID, makeGenericFilter(n.res.RefMod.Identifiers))
if err != nil {
return err
}
if n.relMod != nil {
// Preload existing fields
n.relMod.Fields, err = findComposeModuleFieldsStore(ctx, pl.s, n.relMod)
if err != nil {
return err
}
}
}
if n.relMod == nil {
return resource.ComposeModuleErrUnresolved(n.res.RefMod.Identifiers)
}
// Preload sys users
if n.res.UserFlakes == nil {
n.res.UserFlakes = make(resource.UserstampIndex)
}
if len(n.res.UserFlakes) == 0 {
// No users provided, let's try to fetch them
uu, _, err := store.SearchUsers(ctx, pl.s, systemTypes.UserFilter{
Paging: filter.Paging{
Limit: 0,
},
})
if err != nil {
return err
}
n.res.UserFlakes.Add(uu...)
}
// Add missing refs
preloadRefs := make(resource.RefSet, 0, int(len(n.relMod.Fields)/2)+1)
for _, f := range n.relMod.Fields {
switch f.Kind {
case "Record":
refM := f.Options.String("module")
if refM == "" {
refM = f.Options.String("moduleID")
}
if refM != "" && refM != "0" {
// Make a reference with that module's records
ref := n.res.AddRef(composeTypes.RecordResourceType, refM).Constraint(n.res.RefNs)
n.fieldModRef[f.Name] = ref.Identifiers
preloadRefs = append(preloadRefs, ref)
}
}
}
// Can't do anything else, since the NS doesn't yet exist
if n.relNS.ID == 0 {
return nil
}
// Preload potential references
//
// This is a fairly primitive approach, try to think of something a bit nicer
for _, ref := range preloadRefs {
mod, err := findComposeModuleStore(ctx, pl.s, n.relNS.ID, makeGenericFilter(ref.Identifiers))
if err != nil && err != store.ErrNotFound {
return err
}
auxMap := make(map[string]uint64)
for _, i := range ref.Identifiers {
n.externalRef[i] = auxMap
}
// Preload all records
rr, _, err := dalutils.ComposeRecordsList(ctx, pl.dal, mod, composeTypes.RecordFilter{
ModuleID: mod.ID,
NamespaceID: mod.NamespaceID,
Paging: filter.Paging{
Limit: 0,
},
})
if err != nil {
return err
}
for _, r := range rr {
auxMap[strconv.FormatUint(r.ID, 10)] = r.ID
}
}
// Can't work with own record because the module doesn't yet exist
if n.relMod.ID == 0 {
return nil
}
// Preload own records
rr, _, err := dalutils.ComposeRecordsList(ctx, pl.dal, n.relMod, composeTypes.RecordFilter{
ModuleID: n.relMod.ID,
NamespaceID: n.relNS.ID,
Paging: filter.Paging{
Limit: 0,
},
})
if err != nil && err != store.ErrNotFound {
return err
}
n.missing = len(rr) == 0
// Map existing records so we can perform updates
// Map to xref map for easier use later
auxMap := make(map[string]uint64)
for _, i := range n.res.RefMod.Identifiers {
n.externalRef[i] = auxMap
}
for _, r := range rr {
key := strconv.FormatUint(r.ID, 10)
n.recMap[key] = r
// Map IDs to xref map
auxMap[key] = r.ID
}
return nil
}
// @note composeRecord.Encode can raise an error in case of unresolved user dependencies.
func (n *composeRecord) Encode(ctx context.Context, pl *payload) (err error) {
// Namespace
nsID := n.relNS.ID
if nsID <= 0 {
ns := resource.FindComposeNamespace(pl.state.ParentResources, n.res.RefNs.Identifiers)
nsID = ns.ID
}
// Module
mod := n.relMod
if mod.ID <= 0 {
m := resource.FindComposeModule(pl.state.ParentResources, n.res.RefMod.Identifiers)
mod.ID = m.ID
}
// Aggregate all of the available users
// ux will map { identifier: userID }
ux := make(map[string]uint64)
// Firstly the users provided by the record pl.state itself
for _, ur := range n.res.UserFlakes {
u := ur.U
ux[strconv.FormatUint(u.ID, 10)] = u.ID
if u.Handle != "" {
ux[u.Handle] = u.ID
}
if u.Name != "" {
ux[u.Name] = u.ID
}
if u.Email != "" {
ux[u.Email] = u.ID
}
}
// Next all of the encoded users.
// If identifiers overwrite eachother, that's fine.
for _, ref := range pl.state.ParentResources {
if ref.ResourceType() == systemTypes.UserResourceType {
refUsr := ref.(*resource.User)
for _, i := range refUsr.Identifiers() {
ux[i] = refUsr.SysID()
}
}
}
// Some pointing
rm := n.recMap
im := n.res.IDMap
getKey := func(i int, k string) string {
if k == "" {
return strconv.FormatInt(int64(i), 10)
}
return k
}
checkXRef := func(ii resource.Identifiers, ref string) (uint64, error) {
var auxMap map[string]uint64
for _, ri := range ii {
if mp, ok := n.externalRef[ri]; ok {
auxMap = mp
break
}
}
if auxMap == nil || len(auxMap) == 0 {
return 0, fmt.Errorf("referenced record not resolved: %s", resource.ComposeRecordErrUnresolved(resource.MakeIdentifiers(ref)))
}
return auxMap[ref], nil
}
i := -1
return n.res.Walker(func(r *resource.ComposeRecordRaw) error {
i++
// So we don't have to worry about nil
cfg := r.Config
if cfg == nil {
cfg = &resource.EnvoyConfig{}
}
if cfg.SkipIf != "" {
evl, err := exprP.NewEvaluable(cfg.SkipIf)
if err != nil {
return err
}
// @todo expand this
skip, err := evl.EvalBool(ctx, map[string]interface{}{
"missing": n.missing,
})
if err != nil {
return err
}
if skip {
return nil
}
}
rec := &composeTypes.Record{
ID: im[getKey(i, r.ID)],
NamespaceID: nsID,
ModuleID: mod.ID,
CreatedAt: time.Now(),
}
exists := false
var old *composeTypes.Record
if r.ID != "" {
old = rm[r.ID]
exists = old != nil
}
if rec.ID == 0 && exists {
rec.ID = rm[r.ID].ID
}
if rec.ID == 0 {
rec.ID = NextID()
}
im[getKey(i, r.ID)] = rec.ID
if pl.state.Conflicting {
return nil
}
err := func() error {
// Timestamps
if r.Ts != nil {
if r.Ts.CreatedAt != nil {
rec.CreatedAt = *r.Ts.CreatedAt.T
} else {
rec.CreatedAt = *now()
}
if r.Ts.UpdatedAt != nil {
rec.UpdatedAt = r.Ts.UpdatedAt.T
}
if r.Ts.DeletedAt != nil {
rec.DeletedAt = r.Ts.DeletedAt.T
}
}
// Userstamps
rec.CreatedBy = pl.invokerID
if r.Us != nil {
if r.Us.CreatedBy != nil {
rec.CreatedBy = ux[r.Us.CreatedBy.Ref]
}
if r.Us.UpdatedBy != nil {
rec.UpdatedBy = ux[r.Us.UpdatedBy.Ref]
}
if r.Us.DeletedBy != nil {
rec.DeletedBy = ux[r.Us.DeletedBy.Ref]
}
if r.Us.OwnedBy != nil {
rec.OwnedBy = ux[r.Us.OwnedBy.Ref]
}
}
rec.OwnedBy = service.CalcRecordOwner(0, rec.OwnedBy, pl.invokerID)
rvs := make(composeTypes.RecordValueSet, 0, len(r.Values))
for k, rr := range r.Values {
v := rr.Value
rv := &composeTypes.RecordValue{
RecordID: rec.ID,
Name: k,
Value: v,
Updated: true,
}
f := mod.Fields.FindByName(k)
if f != nil && v != "" {
switch f.Kind {
case "User":
uID := ux[v]
if uID == 0 {
return resource.UserErrUnresolved(resource.MakeIdentifiers(v))
}
rv.Value = strconv.FormatUint(uID, 10)
rv.Ref = uID
case "Record":
refIdentifiers, ok := n.fieldModRef[f.Name]
if !ok {
return fmt.Errorf("module field record reference not resoled: %s", f.Name)
}
// if self...
if n.res.RefMod.Identifiers.HasAny(refIdentifiers) {
rID := im[v]
// Check if its in the store
if rID == 0 {
// Check if we have an xref
rID, err = checkXRef(refIdentifiers, v)
if err != nil {
return err
}
}
if rID == 0 {
return resource.ComposeRecordErrUnresolved(resource.MakeIdentifiers(v))
}
rv.Value = strconv.FormatUint(rID, 10)
rv.Ref = rID
} else {
// not self...
rID := uint64(0)
refRes := resource.FindComposeRecordResource(pl.state.ParentResources, refIdentifiers)
if refRes != nil {
// check if parent has it
rID = refRes.IDMap[v]
}
if rID == 0 {
// Check if we have an xref
rID, err = checkXRef(refIdentifiers, v)
if err != nil {
return err
}
}
if rID == 0 {
return fmt.Errorf("referenced record not resolved: %s", resource.ComposeRecordErrUnresolved(resource.MakeIdentifiers(v)))
}
rv.Value = strconv.FormatUint(rID, 10)
rv.Ref = rID
}
}
}
rvs = append(rvs, rv)
}
if err = service.RecordValueSanitization(mod, rvs); err != nil {
return err
}
rec.Values = rvs
rec.Values.SetUpdatedFlag(true)
// @todo owner?
var (
rve *composeTypes.RecordValueErrorSet
canAccessField = func(f *composeTypes.ModuleField) bool {
return true
}
)
if old != nil {
rec.Values = old.Values.Merge(mod.Fields, rec.Values, canAccessField)
}
rec.Values, _ = rec.Values.Filter(func(v *composeTypes.RecordValue) (bool, error) {
return mod.Fields.HasName(v.Name), nil
})
rve = service.RecordValueUpdateOpCheck(ctx, nil, mod, rec.Values)
if !rve.IsValid() {
return rve
}
rve = service.RecordPreparer(ctx, pl.s, rvSanitizer, rvValidator, rvFormatter, mod, rec)
if !rve.IsValid() {
return rve
}
// Create a new record
if !exists {
err = dalutils.ComposeRecordCreate(ctx, pl.dal, mod, rec)
return err
}
// Update existing
err = dalutils.ComposeRecordUpdate(ctx, pl.dal, mod, rec)
return err
}()
if n.cfg.Defer != nil {
n.cfg.Defer()
}
if err != nil {
if n.cfg.DeferNok != nil {
return n.cfg.DeferNok(err)
}
return err
} else if n.cfg.DeferOk != nil {
n.cfg.DeferOk()
}
return nil
})
}

View File

@@ -0,0 +1,23 @@
package store
import (
"github.com/cortezaproject/corteza/server/pkg/envoy"
"github.com/cortezaproject/corteza/server/pkg/envoy/resource"
)
func newComposeRecordFromAux(rec *composeRecordAux) *composeRecord {
return &composeRecord{
rec: rec,
relMod: rec.relMod,
}
}
func (rec *composeRecord) MarshalEnvoy() ([]resource.Interface, error) {
rr := resource.NewComposeRecordSet(rec.rec.walker, rec.rec.refNs, rec.rec.refMod)
rr.SetUserFlakes(rec.rec.relUsers)
rr.RelMod = rec.relMod
return envoy.CollectNodes(
rr,
)
}

View File

@@ -0,0 +1,211 @@
package store
import (
"context"
"github.com/cortezaproject/corteza/server/pkg/envoy"
"github.com/cortezaproject/corteza/server/pkg/envoy/resource"
"github.com/cortezaproject/corteza/server/pkg/id"
"github.com/cortezaproject/corteza/server/store"
"github.com/cortezaproject/corteza/server/system/types"
)
type (
decoder struct {
ux *userIndex
}
DecodeFilter struct {
// Compose stuff
composeNamespace []*composeNamespaceFilter
composeModule []*composeModuleFilter
composeRecord []*composeRecordFilter
composePage []*composePageFilter
composeChart []*composeChartFilter
// System stuff
roles []*roleFilter
users []*userFilter
templates []*templateFilter
applications []*applicationFilter
apiGwRoutes []*apiGwRouteFilter
reports []*reportFilter
settings []*settingFilter
rbac []*rbacFilter
resourceTranslations []*resourceTranslationFilter
// Automation stuff
automationWorkflow []*automationWorkflowFilter
}
// These two aux things let us simplify the code a little bit
auxMarshaller []envoy.Marshaller
auxRsp struct {
mm []envoy.Marshaller
err error
}
// We'll use the userIndex to hold required users
userIndex struct {
users map[uint64]*types.User
s store.Users
}
)
func Decoder() *decoder {
return &decoder{}
}
func NewDecodeFilter() *DecodeFilter {
return &DecodeFilter{}
}
func (df *DecodeFilter) FromResource(rr ...string) *DecodeFilter {
df = df.systemFromResource(rr...)
df = df.automationFromResource(rr...)
df = df.composeFromResource(rr...)
return df
}
func (df *DecodeFilter) FromRef(rr ...*resource.Ref) *DecodeFilter {
df = df.systemFromRef(rr...)
df = df.automationFromRef(rr...)
df = df.composeFromRef(rr...)
return df
}
func (aum auxMarshaller) MarshalEnvoy() ([]resource.Interface, error) {
ii := make([]resource.Interface, 0, len(aum))
for _, m := range aum {
tmp, err := m.MarshalEnvoy()
if err != nil {
return nil, err
}
// Default locales for applicable resources
for _, t := range tmp {
if li, ok := t.(resource.LocaleInterface); ok {
ll, err := li.EncodeTranslations()
if err != nil {
return nil, err
}
for _, l := range ll {
l.MarkDefault()
ii = append(ii, l)
}
}
}
ii = append(ii, tmp...)
}
return ii, nil
}
// Decode decodes all of the things in the provided store
func (d *decoder) Decode(ctx context.Context, s store.Storer, dal dalService, f *DecodeFilter) ([]resource.Interface, error) {
mm := make(auxMarshaller, 0, 100)
if d.ux == nil {
d.ux = &userIndex{
users: make(map[uint64]*types.User),
}
}
d.ux.s = s
pof := func(rr ...*auxRsp) (auxMarshaller, error) {
mm := make(auxMarshaller, 0, 200)
for _, r := range rr {
if r == nil {
continue
}
if r.err != nil {
return nil, r.err
}
mm = append(mm, r.mm...)
}
return mm, nil
}
compose := newComposeDecoder(dal)
system := newSystemDecoder(d.ux)
automation := newAutomationDecoder(d.ux)
mm, err := pof(
compose.decodeComposeNamespace(ctx, s, f.composeNamespace),
compose.decodeComposeModule(ctx, s, f.composeModule),
compose.decodeComposeRecord(ctx, s, f.composeRecord),
compose.decodeComposePage(ctx, s, f.composePage),
compose.decodeComposeChart(ctx, s, f.composeChart),
system.decodeRoles(ctx, s, f.roles),
system.decodeUsers(ctx, s, f.users),
system.decodeTemplates(ctx, s, f.templates),
system.decodeApplications(ctx, s, f.applications),
system.decodeAPIGWRoutes(ctx, s, f.apiGwRoutes),
system.decodeReports(ctx, s, f.reports),
system.decodeSettings(ctx, s, f.settings),
system.decodeResourceTranslation(ctx, s, f.resourceTranslations),
automation.decodeWorkflows(ctx, s, f.automationWorkflow),
)
if err != nil {
return nil, err
}
f.allowRbacResource(compose.resourceID...)
f.allowRbacResource(system.resourceID...)
f.allowRbacResource(automation.resourceID...)
rr, err := pof(
system.decodeRbac(ctx, s, f.rbac),
)
if err != nil {
return nil, err
}
return append(rr, mm...).MarshalEnvoy()
}
func (ux *userIndex) add(ctx context.Context, uu ...uint64) error {
// List of filtered users
filtered := make([]uint64, 0, len(uu))
for _, u := range uu {
// not defined, we don't need
if u == 0 {
continue
}
// we have, no need
if ux.users[u] != nil {
continue
}
filtered = append(filtered, u)
}
if len(filtered) == 0 {
return nil
}
users, _, err := store.SearchUsers(ctx, ux.s, types.UserFilter{
UserID: id.Strings(filtered...),
})
if err != nil {
return err
}
for _, u := range users {
ux.users[u.ID] = u
}
return nil
}

View File

@@ -0,0 +1,229 @@
package store
import (
"context"
"fmt"
"github.com/cortezaproject/corteza/server/compose/types"
"github.com/cortezaproject/corteza/server/pkg/auth"
"github.com/cortezaproject/corteza/server/pkg/envoy"
"github.com/cortezaproject/corteza/server/pkg/envoy/resource"
"github.com/cortezaproject/corteza/server/pkg/rbac"
"github.com/cortezaproject/corteza/server/store"
)
type (
storeEncoder struct {
s store.Storer
dal dalService
cfg *EncoderConfig
// Each resource should define its own state that is used when encoding the resource.
// Such approach removes the need for a janky generic global state.
// This also simplifies any slight deviations between resource complexities.
state map[resource.Interface]resourceState
}
// EncoderConfig allows us to configure the resource encoding process
EncoderConfig struct {
// OnExisting defines what to do if the resource exists
OnExisting resource.MergeAlg
// Skip if defines a pkg/expr expression when to skip the resource
SkipIf string
// Defer is called after the resource is encoded, regardles of the result
Defer func()
// DeferOk is called after the resource is encoded, only when successful
DeferOk func()
// DeferNok is called after the resource is encoded, only when failed
// If you return an error, the encoding will terminate.
// If you return nil (ignore the error), the encoding will continue.
DeferNok func(error) error
}
accessControlRBACServicer interface {
Can([]uint64, string, rbac.Resource) bool
}
composeAccessController interface {
composeRecordValueAccessController
composeRecordAccessController
}
composeRecordValueAccessController interface {
CanReadRecordValueOnModuleField(context.Context, *types.ModuleField) bool
CanUpdateRecordValueOnModuleField(context.Context, *types.ModuleField) bool
}
composeRecordAccessController interface {
CanCreateRecordOnModule(context.Context, *types.Module) bool
CanUpdateRecord(context.Context, *types.Record) bool
CanDeleteRecord(context.Context, *types.Record) bool
}
payload struct {
s store.Storer
dal dalService
state *envoy.ResourceState
invokerID uint64
}
// resourceState allows each conforming struct to be initialized and encoded
// by the store encoder
resourceState interface {
Prepare(context.Context, *payload) (err error)
Encode(context.Context, *payload) (err error)
}
)
var (
ErrUnknownResource = fmt.Errorf("unknown resource")
ErrResourceStateUndefined = fmt.Errorf("undefined resource state")
)
// NewStoreEncoder initializes a fresh store encoder
//
// If no config is provided, it uses Skip as the default merge alg.
func NewStoreEncoder(s store.Storer, dal dalService, cfg *EncoderConfig) envoy.PrepareEncoder {
if cfg == nil {
cfg = &EncoderConfig{
OnExisting: resource.Skip,
}
}
return &storeEncoder{
s: s,
dal: dal,
cfg: cfg,
state: make(map[resource.Interface]resourceState),
}
}
// Prepare prepares the encoder for the given set of resources
//
// It initializes and prepares the resource state for each provided resource
func (se *storeEncoder) Prepare(ctx context.Context, ee ...*envoy.ResourceState) (err error) {
f := func(rs resourceState, ers *envoy.ResourceState) error {
if rs == nil {
return nil
}
err = rs.Prepare(ctx, se.makePayload(ctx, se.s, se.dal, ers))
if err != nil {
return err
}
se.state[ers.Res] = rs
return nil
}
for _, ers := range ee {
// Skip placeholders
if ers.Res.Placeholder() {
continue
}
switch res := ers.Res.(type) {
// Compose resources
case *resource.ComposeNamespace:
err = f(newComposeNamespaceFromResource(res, se.cfg), ers)
case *resource.ComposeModule:
err = f(NewComposeModuleFromResource(res, se.cfg), ers)
case *resource.ComposeRecord:
err = f(NewComposeRecordFromResource(res, se.cfg), ers)
case *resource.ComposeChart:
err = f(newComposeChartFromResource(res, se.cfg), ers)
case *resource.ComposePage:
err = f(newComposePageFromResource(res, se.cfg), ers)
// System resources
case *resource.User:
err = f(NewUserFromResource(res, se.cfg), ers)
case *resource.Template:
err = f(NewTemplateFromResource(res, se.cfg), ers)
case *resource.Role:
err = f(NewRoleFromResource(res, se.cfg), ers)
case *resource.Application:
err = f(NewApplicationFromResource(res, se.cfg), ers)
case *resource.APIGateway:
err = f(newAPIGatewayFromResource(res, se.cfg), ers)
case *resource.Report:
err = f(newReportFromResource(res, se.cfg), ers)
case *resource.Setting:
err = f(NewSettingFromResource(res, se.cfg), ers)
case *resource.RbacRule:
err = f(newRbacRuleFromResource(res, se.cfg), ers)
case *resource.ResourceTranslation:
err = f(newResourceTranslationFromResource(res, se.cfg), ers)
// Automation resources
case *resource.AutomationWorkflow:
err = f(newAutomationWorkflowFromResource(res, se.cfg), ers)
default:
err = ErrUnknownResource
}
if err != nil {
return se.WrapError("prepare", ers.Res, err)
}
}
return nil
}
// Encode encodes available resource states using the given store encoder
func (se *storeEncoder) Encode(ctx context.Context, p envoy.Provider) error {
var ers *envoy.ResourceState
return store.Tx(ctx, se.s, func(ctx context.Context, s store.Storer) (err error) {
for {
ers, err = p.NextInverted(ctx)
if err != nil {
return err
}
if ers == nil {
return nil
}
// Skip placeholders
if ers.Res.Placeholder() {
continue
}
state := se.state[ers.Res]
if state == nil {
err = ErrResourceStateUndefined
} else {
if state != nil {
err = state.Encode(ctx, se.makePayload(ctx, s, se.dal, ers))
}
}
if err != nil {
return se.WrapError("encode", ers.Res, err)
}
}
})
}
func (se *storeEncoder) makePayload(ctx context.Context, s store.Storer, dal dalService, ers *envoy.ResourceState) *payload {
return &payload{
s: s,
dal: dal,
state: ers,
invokerID: auth.GetIdentityFromContext(ctx).Identity(),
}
}
func (se *storeEncoder) WrapError(act string, res resource.Interface, err error) error {
if ww, ok := res.(resource.ErrorWrapper); ok {
err = ww.WrapError(act, err)
} else {
err = fmt.Errorf("%s %s %v: %s", act, res.ResourceType(), res.Identifiers().StringSlice(), err)
}
return fmt.Errorf("store encoder %s", err)
}
func resourceErrIdentifierNotUnique(i string) error {
return fmt.Errorf("bad resource identifier %v: not unique", i)
}

View File

@@ -0,0 +1,30 @@
package store
import (
"fmt"
"github.com/cortezaproject/corteza/server/pkg/envoy/resource"
"github.com/cortezaproject/corteza/server/pkg/rbac"
"github.com/cortezaproject/corteza/server/system/types"
)
type (
rbacRule struct {
cfg *EncoderConfig
rule *rbac.Rule
// point to the rbac rule
refRbacResource string
refRbacRes *resource.Ref
refPathRes []*resource.Ref
refRole *resource.Ref
role *types.Role
}
)
func rbacResourceErrUnidentifiable(ii resource.Identifiers) error {
return fmt.Errorf("rbac resource unidentifiable %v", ii.StringSlice())
}

View File

@@ -0,0 +1,397 @@
package store
import (
"context"
"fmt"
"strings"
automationTypes "github.com/cortezaproject/corteza/server/automation/types"
composeTypes "github.com/cortezaproject/corteza/server/compose/types"
federationTypes "github.com/cortezaproject/corteza/server/federation/types"
"github.com/cortezaproject/corteza/server/pkg/envoy/resource"
"github.com/cortezaproject/corteza/server/pkg/rbac"
"github.com/cortezaproject/corteza/server/store"
systemTypes "github.com/cortezaproject/corteza/server/system/types"
)
var (
gRbacRules map[string]bool
)
func newRbacRuleFromResource(res *resource.RbacRule, cfg *EncoderConfig) resourceState {
return &rbacRule{
cfg: mergeConfig(cfg, res.Config()),
rule: res.Res,
refRbacResource: res.RefResource,
refRbacRes: res.RefRes,
refPathRes: res.RefPath,
refRole: res.RefRole,
}
}
func (n *rbacRule) Prepare(ctx context.Context, pl *payload) (err error) {
// Init global state
if gRbacRules == nil {
err = n.initGlobalIndex(ctx, pl.s)
if err != nil {
return
}
}
// Related role
n.role, err = findRole(ctx, pl.s, pl.state.ParentResources, n.refRole.Identifiers)
if err != nil {
return err
}
if n.role == nil {
return resource.RoleErrUnresolved(n.refRole.Identifiers)
}
refRes := n.refRbacRes
if refRes == nil {
return
}
// For now we will only allow resource specific RBAC rules if that resource is
// also present.
// Here we check if we can find it in case we're handling a resource specific rule.
found := false
switch refRes.ResourceType {
case composeTypes.RecordResourceType:
if found = n.handleComposeRecord(pl.state.ParentResources); !found {
return resource.RbacResourceErrNotFound(refRes.Identifiers)
}
case composeTypes.ModuleFieldResourceType:
if found = n.handleComposeModuleField(pl.state.ParentResources); !found {
return resource.RbacResourceErrNotFound(refRes.Identifiers)
}
default:
for _, r := range pl.state.ParentResources {
if refRes.ResourceType == r.ResourceType() && r.Identifiers().HasAny(refRes.Identifiers) {
return
}
}
// We couldn't find it...
return resource.RbacResourceErrNotFound(refRes.Identifiers)
}
return
}
func (n *rbacRule) Encode(ctx context.Context, pl *payload) (err error) {
// Assure correct resource
n.rule.Resource, err = n.makeRBACResource(pl)
if err != nil {
return err
}
// Assure correct role
n.rule.RoleID = n.role.ID
if n.rule.RoleID == 0 {
rl := resource.FindRole(pl.state.ParentResources, n.refRole.Identifiers)
n.rule.RoleID = rl.ID
}
if n.rule.RoleID == 0 {
return resource.RoleErrUnresolved(n.refRole.Identifiers)
}
// Upsert
if _, exists := gRbacRules[n.rbacRuleIndex((n.rule))]; !exists {
return store.CreateRbacRule(ctx, pl.s, n.rule)
}
// On existing rbac rule, replace/merge right basically overwrites the existing rule;
// otherwise, the new rule is ignored.
switch n.cfg.OnExisting {
case resource.Replace,
resource.MergeRight:
return store.UpdateRbacRule(ctx, pl.s, n.rule)
}
return nil
}
func (n *rbacRule) rbacRuleIndex(r *rbac.Rule) string {
return fmt.Sprintf("%d_%s_%s", r.RoleID, r.Resource, r.Operation)
}
func (n *rbacRule) initGlobalIndex(ctx context.Context, s store.Storer) error {
gRbacRules = make(map[string]bool)
rr, _, err := store.SearchRbacRules(ctx, s, rbac.RuleFilter{})
if err != store.ErrNotFound && err != nil {
return err
}
for _, r := range rr {
gRbacRules[n.rbacRuleIndex(r)] = true
}
return nil
}
func (n *rbacRule) makeRBACResource(pl *payload) (string, error) {
p0ID := uint64(0)
p1ID := uint64(0)
p2ID := uint64(0)
rt := strings.Split(n.refRbacResource, "/")[0]
// Component level stuff
switch rt {
case composeTypes.ComponentResourceType:
return composeTypes.ComponentRbacResource(), nil
case systemTypes.ComponentResourceType:
return systemTypes.ComponentRbacResource(), nil
case automationTypes.ComponentResourceType:
return automationTypes.ComponentRbacResource(), nil
case federationTypes.ComponentResourceType:
return federationTypes.ComponentRbacResource(), nil
}
// Component resource stuff
switch rt {
case automationTypes.WorkflowResourceType:
if n.refRbacRes != nil {
p1 := resource.FindAutomationWorkflow(pl.state.ParentResources, n.refRbacRes.Identifiers)
if p1 == nil {
return "", resource.AutomationWorkflowErrUnresolved(n.refRbacRes.Identifiers)
}
p1ID = p1.ID
}
return automationTypes.WorkflowRbacResource(p1ID), nil
case composeTypes.NamespaceResourceType:
if n.refRbacRes != nil {
p1 := resource.FindComposeNamespace(pl.state.ParentResources, n.refRbacRes.Identifiers)
if p1 == nil {
return "", resource.ComposeNamespaceErrUnresolved(n.refRbacRes.Identifiers)
}
p1ID = p1.ID
}
return composeTypes.NamespaceRbacResource(p1ID), nil
case composeTypes.ModuleResourceType:
if len(n.refPathRes) > 0 {
p0 := resource.FindComposeNamespace(pl.state.ParentResources, n.refPathRes[0].Identifiers)
if p0 == nil {
return "", resource.ComposeNamespaceErrUnresolved(n.refPathRes[0].Identifiers)
}
p0ID = p0.ID
}
if n.refRbacRes != nil {
p1 := resource.FindComposeModule(pl.state.ParentResources, n.refRbacRes.Identifiers)
if p1 == nil {
return "", resource.ComposeModuleErrUnresolved(n.refRbacRes.Identifiers)
}
p1ID = p1.ID
}
return composeTypes.ModuleRbacResource(p0ID, p1ID), nil
case composeTypes.ChartResourceType:
if len(n.refPathRes) > 0 {
p0 := resource.FindComposeNamespace(pl.state.ParentResources, n.refPathRes[0].Identifiers)
if p0 == nil {
return "", resource.ComposeNamespaceErrUnresolved(n.refPathRes[0].Identifiers)
}
p0ID = p0.ID
}
if n.refRbacRes != nil {
p1 := resource.FindComposeChart(pl.state.ParentResources, n.refRbacRes.Identifiers)
if p1 == nil {
return "", resource.ComposeChartErrUnresolved(n.refRbacRes.Identifiers)
}
p1ID = p1.ID
}
return composeTypes.ChartRbacResource(p0ID, p1ID), nil
case composeTypes.PageResourceType:
if len(n.refPathRes) > 0 {
p0 := resource.FindComposeNamespace(pl.state.ParentResources, n.refPathRes[0].Identifiers)
if p0 == nil {
return "", resource.ComposeNamespaceErrUnresolved(n.refPathRes[0].Identifiers)
}
p0ID = p0.ID
}
if n.refRbacRes != nil {
p1 := resource.FindComposePage(pl.state.ParentResources, n.refRbacRes.Identifiers)
if p1 == nil {
return "", resource.ComposePageErrUnresolved(n.refRbacRes.Identifiers)
}
p1ID = p1.ID
}
return composeTypes.PageRbacResource(p0ID, p1ID), nil
case composeTypes.RecordResourceType:
if len(n.refPathRes) > 0 {
p0 := resource.FindComposeNamespace(pl.state.ParentResources, n.refPathRes[0].Identifiers)
if p0 == nil {
return "", resource.ComposeNamespaceErrUnresolved(n.refPathRes[0].Identifiers)
}
p0ID = p0.ID
}
if len(n.refPathRes) > 1 {
p1 := resource.FindComposeModule(pl.state.ParentResources, n.refPathRes[1].Identifiers)
if p1 == nil {
return "", resource.ComposeModuleErrUnresolved(n.refPathRes[1].Identifiers)
}
p1ID = p1.ID
}
if n.refRbacRes != nil {
ref := n.refPathRes[1]
p2 := resource.FindComposeRecordResource(pl.state.ParentResources, ref.Identifiers)
if p2 == nil {
return "", resource.ComposeRecordErrUnresolved(n.refRbacRes.Identifiers)
}
for _, i := range n.refRbacRes.Identifiers {
if p2ID = p2.IDMap[i]; p2ID > 0 {
break
}
}
if p2ID == 0 {
return "", resource.ComposeRecordErrUnresolved(n.refRbacRes.Identifiers)
}
}
return composeTypes.RecordRbacResource(p0ID, p1ID, p2ID), nil
case composeTypes.ModuleFieldResourceType:
if len(n.refPathRes) > 0 {
p0 := resource.FindComposeNamespace(pl.state.ParentResources, n.refPathRes[0].Identifiers)
if p0 == nil {
return "", resource.ComposeNamespaceErrUnresolved(n.refPathRes[0].Identifiers)
}
p0ID = p0.ID
}
if len(n.refPathRes) > 1 {
p1 := resource.FindComposeModule(pl.state.ParentResources, n.refPathRes[1].Identifiers)
if p1 == nil {
return "", resource.ComposeModuleErrUnresolved(n.refPathRes[1].Identifiers)
}
p1ID = p1.ID
}
if n.refRbacRes != nil {
modRef := n.refPathRes[1]
p2 := resource.FindComposeModuleField(pl.state.ParentResources, modRef.Identifiers, n.refRbacRes.Identifiers)
if p2 == nil {
return "", resource.ComposeModuleFieldErrUnresolved(n.refRbacRes.Identifiers)
}
p2ID = p2.ID
}
return composeTypes.ModuleFieldRbacResource(p0ID, p1ID, p2ID), nil
case systemTypes.UserResourceType:
if n.refRbacRes != nil {
p1 := resource.FindUser(pl.state.ParentResources, n.refRbacRes.Identifiers)
if p1 == nil {
return "", resource.UserErrUnresolved(n.refRbacRes.Identifiers)
}
p1ID = p1.ID
}
return systemTypes.UserRbacResource(p1ID), nil
case systemTypes.RoleResourceType:
if n.refRbacRes != nil {
p1 := resource.FindRole(pl.state.ParentResources, n.refRbacRes.Identifiers)
if p1 == nil {
return "", resource.RoleErrUnresolved(n.refRbacRes.Identifiers)
}
p1ID = p1.ID
}
return systemTypes.RoleRbacResource(p1ID), nil
case systemTypes.ApplicationResourceType:
if n.refRbacRes != nil {
p1 := resource.FindApplication(pl.state.ParentResources, n.refRbacRes.Identifiers)
if p1 == nil {
return "", resource.ApplicationErrUnresolved(n.refRbacRes.Identifiers)
}
p1ID = p1.ID
}
return systemTypes.ApplicationRbacResource(p1ID), nil
case systemTypes.ApigwRouteResourceType:
if n.refRbacRes != nil {
p1 := resource.FindAPIGateway(pl.state.ParentResources, n.refRbacRes.Identifiers)
if p1 == nil {
return "", resource.APIGatewayErrUnresolved(n.refRbacRes.Identifiers)
}
p1ID = p1.ID
}
return systemTypes.ApigwRouteRbacResource(p1ID), nil
case systemTypes.AuthClientResourceType:
// @todo add support for importing rbac rules for specific client
return systemTypes.AuthClientRbacResource(p1ID), nil
case systemTypes.TemplateResourceType:
if n.refRbacRes != nil {
p1 := resource.FindTemplate(pl.state.ParentResources, n.refRbacRes.Identifiers)
if p1 == nil {
return "", resource.TemplateErrUnresolved(n.refRbacRes.Identifiers)
}
p1ID = p1.ID
}
return systemTypes.TemplateRbacResource(p1ID), nil
case systemTypes.ReportResourceType:
if n.refRbacRes != nil {
p1 := resource.FindReport(pl.state.ParentResources, n.refRbacRes.Identifiers)
if p1 == nil {
return "", resource.ReportErrUnresolved(n.refRbacRes.Identifiers)
}
p1ID = p1.ID
}
return systemTypes.ReportRbacResource(p1ID), nil
case systemTypes.QueueResourceType:
// @todo add support for importing rbac rules for specific queue
return systemTypes.QueueRbacResource(p1ID), nil
case systemTypes.DataPrivacyRequestResourceType:
return systemTypes.DataPrivacyRequestRbacResource(p1ID), nil
case federationTypes.NodeResourceType:
return federationTypes.NodeRbacResource(p1ID), nil
case federationTypes.SharedModuleResourceType:
return federationTypes.SharedModuleRbacResource(p0ID, p1ID), nil
case federationTypes.ExposedModuleResourceType:
return federationTypes.ExposedModuleRbacResource(p0ID, p1ID), nil
}
// @todo if we wish to support rbac for external stuff, this needs to pass through.
// this also requires some tweaks in the path ID thing.
return "", fmt.Errorf("unsupported resource type '%s' for RBAC store encode", rt)
}
func (n *rbacRule) handleComposeRecord(pp []resource.Interface) bool {
for _, p := range pp {
if p.ResourceType() == composeTypes.RecordResourceType && p.Identifiers().HasAny(n.refPathRes[1].Identifiers) {
return true
}
}
return false
}
func (n *rbacRule) handleComposeModuleField(pp []resource.Interface) bool {
for _, p := range pp {
if p.ResourceType() == composeTypes.ModuleResourceType && p.Identifiers().HasAny(n.refPathRes[1].Identifiers) {
return true
}
}
return false
}

View File

@@ -0,0 +1,32 @@
package store
import (
"strconv"
"github.com/cortezaproject/corteza/server/pkg/envoy"
"github.com/cortezaproject/corteza/server/pkg/envoy/resource"
"github.com/cortezaproject/corteza/server/pkg/rbac"
"github.com/cortezaproject/corteza/server/system/types"
)
func newRbacRule(rl *rbac.Rule) (*rbacRule, error) {
res := rl.Resource
_, ref, pp, err := resource.ParseRule(res)
return &rbacRule{
rule: rl,
refRbacResource: res,
refRbacRes: ref,
refPathRes: pp,
refRole: resource.MakeRef(types.RoleResourceType, resource.MakeIdentifiers(strconv.FormatUint(rl.RoleID, 10))),
}, err
}
func (rl *rbacRule) MarshalEnvoy() ([]resource.Interface, error) {
return envoy.CollectNodes(
resource.NewRbacRule(rl.rule, rl.refRole, rl.refRbacRes, rl.refRbacResource, rl.refPathRes...),
)
}

View File

@@ -0,0 +1,115 @@
package store
import (
"context"
"github.com/cortezaproject/corteza/server/pkg/envoy/resource"
"github.com/cortezaproject/corteza/server/store"
"github.com/cortezaproject/corteza/server/system/types"
)
type (
report struct {
cfg *EncoderConfig
res *resource.Report
rp *types.Report
ss types.ReportDataSourceSet
pp types.ReportBlockSet
ux *userIndex
}
reportSet []*report
reportSource struct {
cfg *EncoderConfig
res *resource.ReportSource
tr *types.ReportDataSource
}
reportSourceSet []*reportSource
)
// mergeReports merges b into a, prioritising a
func mergeReports(a, b *types.Report) *types.Report {
c := a
if c.Handle == "" {
c.Handle = b.Handle
}
if c.Meta == nil {
c.Meta = b.Meta
}
if c.Sources == nil {
c.Sources = b.Sources
}
if c.Blocks == nil {
c.Blocks = b.Blocks
}
if c.OwnedBy == 0 {
c.OwnedBy = b.OwnedBy
}
if c.CreatedBy == 0 {
c.CreatedBy = b.CreatedBy
}
if c.UpdatedBy == 0 {
c.UpdatedBy = b.UpdatedBy
}
if c.DeletedBy == 0 {
c.DeletedBy = b.DeletedBy
}
if c.CreatedAt.IsZero() {
c.CreatedAt = b.CreatedAt
}
if c.UpdatedAt == nil {
c.UpdatedAt = b.UpdatedAt
}
if c.DeletedAt == nil {
c.DeletedAt = b.DeletedAt
}
return c
}
// findReport looks for the report in the resources & the store
//
// Provided resources are prioritized.
func findReport(ctx context.Context, s store.Storer, rr resource.InterfaceSet, ii resource.Identifiers) (wf *types.Report, err error) {
wf = resource.FindReport(rr, ii)
if wf != nil {
return wf, nil
}
return findReportStore(ctx, s, makeGenericFilter(ii))
}
// findReportStore looks for the report in the store
func findReportStore(ctx context.Context, s store.Storer, gf genericFilter) (wf *types.Report, err error) {
if gf.id > 0 {
wf, err = store.LookupReportByID(ctx, s, gf.id)
if err != nil && err != store.ErrNotFound {
return nil, err
}
if wf != nil {
return
}
}
for _, i := range gf.identifiers {
wf, err = store.LookupReportByHandle(ctx, s, i)
if err != nil && err != store.ErrNotFound {
return nil, err
}
if wf != nil {
return
}
}
return nil, nil
}

View File

@@ -0,0 +1,126 @@
package store
import (
"context"
"github.com/cortezaproject/corteza/server/pkg/envoy/resource"
"github.com/cortezaproject/corteza/server/store"
"github.com/cortezaproject/corteza/server/system/types"
)
func newReportFromResource(res *resource.Report, cfg *EncoderConfig) resourceState {
return &report{
cfg: mergeConfig(cfg, res.Config()),
res: res,
}
}
func (n *report) Prepare(ctx context.Context, pl *payload) (err error) {
// Reset old identifiers
n.res.Res.ID = 0
// Try to get the original report
n.rp, err = findReportStore(ctx, pl.s, makeGenericFilter(n.res.Identifiers()))
if err != nil {
return err
}
if n.rp != nil {
n.res.Res.ID = n.rp.ID
}
return nil
}
func (n *report) Encode(ctx context.Context, pl *payload) (err error) {
res := n.res.Res
exists := n.rp != nil && n.rp.ID > 0
// Determine the ID
if res.ID <= 0 && exists {
res.ID = n.rp.ID
}
if res.ID <= 0 {
res.ID = NextID()
}
// Sys users
us, err := resolveUserstamps(ctx, pl.s, pl.state.ParentResources, n.res.Userstamps())
if err != nil {
return err
}
ts := n.res.Timestamps()
if ts != nil {
if ts.CreatedAt != nil {
res.CreatedAt = *ts.CreatedAt.T
} else {
res.CreatedAt = *now()
}
if ts.UpdatedAt != nil {
res.UpdatedAt = ts.UpdatedAt.T
}
if ts.DeletedAt != nil {
res.DeletedAt = ts.DeletedAt.T
}
}
res.CreatedBy = pl.invokerID
if us != nil {
if us.OwnedBy != nil {
res.OwnedBy = us.OwnedBy.UserID
}
if us.CreatedBy != nil {
res.CreatedBy = us.CreatedBy.UserID
}
if us.UpdatedBy != nil {
res.UpdatedBy = us.UpdatedBy.UserID
}
if us.DeletedBy != nil {
res.DeletedBy = us.DeletedBy.UserID
}
}
res.Sources = make(types.ReportDataSourceSet, 0, 10)
for _, rp := range n.res.Sources {
res.Sources = append(res.Sources, rp.Res)
}
res.Blocks = make(types.ReportBlockSet, 0, 10)
for _, rp := range n.res.Blocks {
res.Blocks = append(res.Blocks, rp.Res)
}
// Evaluate the resource skip expression
// @todo expand available parameters; similar implementation to automation/types/record@Dict
if skip, err := basicSkipEval(ctx, n.cfg, !exists); err != nil {
return err
} else if skip {
return nil
}
// Create a fresh report
if !exists {
return store.CreateReport(ctx, pl.s, res)
}
// Update existing report
switch n.cfg.OnExisting {
case resource.Skip:
return nil
case resource.MergeLeft:
res = mergeReports(n.rp, res)
case resource.MergeRight:
res = mergeReports(res, n.rp)
}
err = store.UpdateReport(ctx, pl.s, res)
if err != nil {
return err
}
n.res.Res = res
return nil
}

View File

@@ -0,0 +1,34 @@
package store
import (
"github.com/cortezaproject/corteza/server/pkg/envoy"
"github.com/cortezaproject/corteza/server/pkg/envoy/resource"
"github.com/cortezaproject/corteza/server/system/types"
)
func newReport(wf *types.Report, ux *userIndex) *report {
return &report{
rp: wf,
ss: wf.Sources,
pp: wf.Blocks,
ux: ux,
}
}
func (awf *report) MarshalEnvoy() ([]resource.Interface, error) {
rs := resource.NewReport(awf.rp)
syncUserStamps(rs.Userstamps(), awf.ux)
for _, s := range awf.ss {
rs.AddReportSource(s)
}
for _, p := range awf.pp {
rs.AddReportBlock(p)
}
return envoy.CollectNodes(
rs,
)
}

View File

@@ -0,0 +1,26 @@
package store
import (
"fmt"
"github.com/cortezaproject/corteza/server/pkg/envoy/resource"
"github.com/cortezaproject/corteza/server/system/types"
)
type (
resourceTranslation struct {
cfg *EncoderConfig
locales types.ResourceTranslationSet
// point to the resource translation
refResourceTranslation string
refLocaleRes *resource.Ref
refPathRes []*resource.Ref
}
)
func resourceTranslationErrUnidentifiable(ii resource.Identifiers) error {
return fmt.Errorf("resource translation unidentifiable %v", ii.StringSlice())
}

View File

@@ -0,0 +1,250 @@
package store
import (
"context"
"fmt"
composeTypes "github.com/cortezaproject/corteza/server/compose/types"
"github.com/cortezaproject/corteza/server/pkg/envoy/resource"
"github.com/cortezaproject/corteza/server/store"
"github.com/cortezaproject/corteza/server/system/types"
"golang.org/x/text/language"
)
var (
gResourceTranslations map[language.Tag]map[string]*types.ResourceTranslation
resourceTranslationIndex = func(l *types.ResourceTranslation) string {
return fmt.Sprintf("%s/%s", l.Resource, l.K)
}
)
func newResourceTranslationFromResource(res *resource.ResourceTranslation, cfg *EncoderConfig) resourceState {
return &resourceTranslation{
cfg: mergeConfig(cfg, res.Config()),
locales: res.Res,
refResourceTranslation: res.Res[0].Resource,
refLocaleRes: res.RefRes,
refPathRes: res.RefPath,
}
}
func (n *resourceTranslation) Prepare(ctx context.Context, pl *payload) (err error) {
// Init global state
if gResourceTranslations == nil {
err = n.initGlobalIndex(ctx, pl.s)
if err != nil {
return err
}
}
// For now we will only allow resource specific locale resources if that resource is
// also present.
// Here we check if we can find it in case we're handling a resource specific rule.
refRes := n.refLocaleRes
if refRes.ResourceType == composeTypes.ModuleFieldResourceType {
for _, r := range pl.state.ParentResources {
if r.ResourceType() == composeTypes.ModuleResourceType && r.Identifiers().HasAny(n.refPathRes[1].Identifiers) {
return
}
}
} else {
for _, r := range pl.state.ParentResources {
if refRes.ResourceType == r.ResourceType() && r.Identifiers().HasAny(refRes.Identifiers) {
return
}
}
// We couldn't find it...
return resource.ResourceTranslationErrNotFound(refRes.Identifiers)
}
return
}
func (n *resourceTranslation) Encode(ctx context.Context, pl *payload) (err error) {
// @todo move out of the encoding logic
if n == nil {
return
}
localeRes, err := n.makeResourceTranslation(pl)
if err != nil {
return err
}
// Assure correct resources
for _, l := range n.locales {
l.Resource = localeRes
}
ll := make(types.ResourceTranslationSet, 0, len(n.locales))
for _, l := range n.locales {
if l.Message == "" {
continue
}
old, ok := gResourceTranslations[l.Lang.Tag][resourceTranslationIndex(l)]
// not exist
if !ok {
l.ID = NextID()
l.CreatedAt = *now()
l.CreatedBy = pl.invokerID
ll = append(ll, l)
continue
}
// exists
switch n.cfg.OnExisting {
case resource.Skip,
resource.MergeLeft:
continue
case resource.MergeRight,
resource.Default:
old.Message = l.Message
ll = append(ll, old)
}
}
// Update global index for following runs
n.updateGlobalIndex(ll)
return store.UpsertResourceTranslation(ctx, pl.s, ll...)
}
func (n *resourceTranslation) makeResourceTranslation(pl *payload) (string, error) {
p0ID := uint64(0)
p1ID := uint64(0)
p2ID := uint64(0)
localeRes := ""
_ = localeRes
switch n.refLocaleRes.ResourceType {
// case automationTypes.WorkflowResourceType:
// p1 := resource.FindAutomationWorkflow(pl.state.ParentResources, n.refLocaleRes.Identifiers)
// if p1 == nil {
// return "", resource.AutomationWorkflowErrUnresolved(n.refLocaleRes.Identifiers)
// }
// p1ID = p1.ID
// return automationTypes.WorkflowResourceTranslation(p1ID), nil
case composeTypes.NamespaceResourceType:
p1 := resource.FindComposeNamespace(pl.state.ParentResources, n.refLocaleRes.Identifiers)
if p1 == nil {
return "", resource.ComposeNamespaceErrUnresolved(n.refLocaleRes.Identifiers)
}
p1ID = p1.ID
return composeTypes.NamespaceResourceTranslation(p1ID), nil
case composeTypes.ModuleResourceType:
p0 := resource.FindComposeNamespace(pl.state.ParentResources, n.refPathRes[0].Identifiers)
if p0 == nil {
return "", resource.ComposeNamespaceErrUnresolved(n.refPathRes[0].Identifiers)
}
p0ID = p0.ID
p1 := resource.FindComposeModule(pl.state.ParentResources, n.refLocaleRes.Identifiers)
if p1 == nil {
return "", resource.ComposeModuleErrUnresolved(n.refLocaleRes.Identifiers)
}
p1ID = p1.ID
return composeTypes.ModuleResourceTranslation(p0ID, p1ID), nil
case composeTypes.PageResourceType:
p0 := resource.FindComposeNamespace(pl.state.ParentResources, n.refPathRes[0].Identifiers)
if p0 == nil {
return "", resource.ComposeNamespaceErrUnresolved(n.refPathRes[0].Identifiers)
}
p0ID = p0.ID
p1 := resource.FindComposePage(pl.state.ParentResources, n.refLocaleRes.Identifiers)
if p1 == nil {
return "", resource.ComposePageErrUnresolved(n.refLocaleRes.Identifiers)
}
p1ID = p1.ID
return composeTypes.PageResourceTranslation(p0ID, p1ID), nil
case composeTypes.ChartResourceType:
p0 := resource.FindComposeNamespace(pl.state.ParentResources, n.refPathRes[0].Identifiers)
if p0 == nil {
return "", resource.ComposeNamespaceErrUnresolved(n.refPathRes[0].Identifiers)
}
p0ID = p0.ID
p1 := resource.FindComposeChart(pl.state.ParentResources, n.refLocaleRes.Identifiers)
if p1 == nil {
return "", resource.ComposeChartErrUnresolved(n.refLocaleRes.Identifiers)
}
p1ID = p1.ID
return composeTypes.ChartResourceTranslation(p0ID, p1ID), nil
case composeTypes.ModuleFieldResourceType:
p0 := resource.FindComposeNamespace(pl.state.ParentResources, n.refPathRes[0].Identifiers)
if p0 == nil {
return "", resource.ComposeNamespaceErrUnresolved(n.refPathRes[0].Identifiers)
}
p0ID = p0.ID
p1 := resource.FindComposeModule(pl.state.ParentResources, n.refPathRes[1].Identifiers)
if p1 == nil {
return "", resource.ComposeModuleErrUnresolved(n.refPathRes[1].Identifiers)
}
p1ID = p1.ID
// field
f := resource.FindComposeModuleField(pl.state.ParentResources, n.refPathRes[1].Identifiers, n.refLocaleRes.Identifiers)
if f == nil {
return "", resource.ComposeModuleFieldErrUnresolved(n.refLocaleRes.Identifiers)
}
p2ID = f.ID
return composeTypes.ModuleFieldResourceTranslation(p0ID, p1ID, p2ID), nil
// case types.ReportResourceType:
// p1 := resource.FindReport(pl.state.ParentResources, n.refLocaleRes.Identifiers)
// if p1 == nil {
// return "", resource.ReportErrUnresolved(n.refLocaleRes.Identifiers)
// }
// p1ID = p1.ID
// return types.ReportResourceTranslation(p1ID), nil
default:
// @todo if we wish to support res. trans. for external stuff, this needs to pass through.
// this also requires some tweaks in the path ID thing.
return "", fmt.Errorf("unsupported resource type '%s' for resource translation store encode", n.refLocaleRes.ResourceType)
}
}
func (n *resourceTranslation) initGlobalIndex(ctx context.Context, s store.Storer) (err error) {
gResourceTranslations = make(map[language.Tag]map[string]*types.ResourceTranslation)
ll, _, err := store.SearchResourceTranslations(ctx, s, types.ResourceTranslationFilter{})
if err != store.ErrNotFound && err != nil {
return err
}
n.updateGlobalIndex(ll)
return nil
}
func (n *resourceTranslation) updateGlobalIndex(ll types.ResourceTranslationSet) {
for _, l := range ll {
if _, ok := gResourceTranslations[l.Lang.Tag]; !ok {
gResourceTranslations[l.Lang.Tag] = make(map[string]*types.ResourceTranslation)
}
gResourceTranslations[l.Lang.Tag][resourceTranslationIndex(l)] = l
}
}

View File

@@ -0,0 +1,27 @@
package store
import (
"github.com/cortezaproject/corteza/server/pkg/envoy"
"github.com/cortezaproject/corteza/server/pkg/envoy/resource"
"github.com/cortezaproject/corteza/server/system/types"
)
func newResourceTranslation(l types.ResourceTranslationSet) (*resourceTranslation, error) {
res := l[0].Resource
_, ref, pp, err := resource.ParseResourceTranslation(res)
return &resourceTranslation{
locales: l,
refResourceTranslation: res,
refLocaleRes: ref,
refPathRes: pp,
}, err
}
func (lr *resourceTranslation) MarshalEnvoy() ([]resource.Interface, error) {
return envoy.CollectNodes(
resource.NewResourceTranslation(lr.locales, lr.refResourceTranslation, lr.refLocaleRes, lr.refPathRes...),
)
}

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