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

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

View File

@@ -0,0 +1,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...),
)
}

View File

@@ -0,0 +1,101 @@
package store
import (
"context"
"github.com/cortezaproject/corteza/server/pkg/envoy/resource"
"github.com/cortezaproject/corteza/server/pkg/filter"
"github.com/cortezaproject/corteza/server/store"
"github.com/cortezaproject/corteza/server/system/types"
)
type (
role struct {
cfg *EncoderConfig
res *resource.Role
rl *types.Role
}
)
// mergeRoles merges b into a, prioritising a
func mergeRoles(a, b *types.Role) *types.Role {
c := *a
if c.Name == "" {
c.Name = b.Name
}
if c.Handle == "" {
c.Handle = b.Handle
}
if c.CreatedAt.IsZero() {
c.CreatedAt = b.CreatedAt
}
if c.UpdatedAt == nil {
c.UpdatedAt = b.UpdatedAt
}
if c.ArchivedAt == nil {
c.ArchivedAt = b.ArchivedAt
}
if c.DeletedAt == nil {
c.DeletedAt = b.DeletedAt
}
return &c
}
// findRole looks for the role in the resources & the store
//
// Provided resources are prioritized.
func findRole(ctx context.Context, s store.Storer, rr resource.InterfaceSet, ii resource.Identifiers) (rl *types.Role, err error) {
rl = resource.FindRole(rr, ii)
if rl != nil {
return rl, nil
}
return findRoleStore(ctx, s, makeGenericFilter(ii))
}
// findRoleStore looks for the role in the store
func findRoleStore(ctx context.Context, s store.Storer, gf genericFilter) (rl *types.Role, err error) {
if gf.id > 0 {
rl, err = store.LookupRoleByID(ctx, s, gf.id)
if err != nil && err != store.ErrNotFound {
return nil, err
}
if rl != nil {
return
}
}
for _, i := range gf.identifiers {
rl, err = store.LookupRoleByHandle(ctx, s, i)
if err == store.ErrNotFound {
var rr types.RoleSet
rr, _, err = store.SearchRoles(ctx, s, types.RoleFilter{
Name: i,
Paging: filter.Paging{
Limit: 2,
},
})
if len(rr) > 1 {
return nil, resourceErrIdentifierNotUnique(i)
}
if len(rr) == 1 {
rl = rr[0]
}
}
if err != nil && err != store.ErrNotFound {
return nil, err
}
if rl != nil {
return
}
}
return nil, nil
}

View File

@@ -0,0 +1,95 @@
package store
import (
"context"
"github.com/cortezaproject/corteza/server/pkg/envoy/resource"
"github.com/cortezaproject/corteza/server/store"
)
func NewRoleFromResource(res *resource.Role, cfg *EncoderConfig) resourceState {
return &role{
cfg: mergeConfig(cfg, res.Config()),
res: res,
}
}
func (n *role) Prepare(ctx context.Context, pl *payload) (err error) {
// Reset old identifiers
n.res.Res.ID = 0
n.rl, err = findRoleStore(ctx, pl.s, makeGenericFilter(n.res.Identifiers()))
if err != nil {
return err
}
if n.rl != nil {
n.res.Res.ID = n.rl.ID
}
return nil
}
func (n *role) Encode(ctx context.Context, pl *payload) (err error) {
res := n.res.Res
exists := n.rl != nil && n.rl.ID > 0
// Determine the ID
if res.ID <= 0 && exists {
res.ID = n.rl.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.ArchivedAt != nil {
res.ArchivedAt = ts.ArchivedAt.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 role
if !exists {
return store.CreateRole(ctx, pl.s, res)
}
// Update existing roles
switch n.cfg.OnExisting {
case resource.Skip:
return nil
case resource.MergeLeft:
res = mergeRoles(n.rl, res)
case resource.MergeRight:
res = mergeRoles(res, n.rl)
}
err = store.UpdateRole(ctx, pl.s, res)
if err != nil {
return err
}
n.res.Res = res
return nil
}

View File

@@ -0,0 +1,46 @@
package store
import (
"testing"
"time"
"github.com/cortezaproject/corteza/server/system/types"
"github.com/stretchr/testify/require"
)
func TestRole_Merger(t *testing.T) {
req := require.New(t)
now := time.Time{}
nowP := &time.Time{}
empty := &types.Role{}
full := &types.Role{
Name: "name",
Handle: "handle",
CreatedAt: now,
UpdatedAt: nowP,
ArchivedAt: nowP,
DeletedAt: nowP,
}
t.Run("merge on empty", func(t *testing.T) {
c := mergeRoles(empty, full)
req.Equal("name", c.Name)
req.Equal("handle", c.Handle)
req.Equal(now, c.CreatedAt)
req.Equal(nowP, c.UpdatedAt)
req.Equal(nowP, c.ArchivedAt)
req.Equal(nowP, c.DeletedAt)
})
t.Run("merge with empty", func(t *testing.T) {
c := mergeRoles(full, empty)
req.Equal("name", c.Name)
req.Equal("handle", c.Handle)
req.Equal(now, c.CreatedAt)
req.Equal(nowP, c.UpdatedAt)
req.Equal(nowP, c.ArchivedAt)
req.Equal(nowP, c.DeletedAt)
})
}

View File

@@ -0,0 +1,19 @@
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 newRole(rl *types.Role) *role {
return &role{
rl: rl,
}
}
func (rl *role) MarshalEnvoy() ([]resource.Interface, error) {
return envoy.CollectNodes(
resource.NewRole(rl.rl),
)
}

View File

@@ -0,0 +1,17 @@
package store
import (
"github.com/cortezaproject/corteza/server/pkg/envoy/resource"
"github.com/cortezaproject/corteza/server/system/types"
)
type (
setting struct {
cfg *EncoderConfig
res *resource.Setting
st *types.SettingValue
ux *userIndex
}
)

View File

@@ -0,0 +1,81 @@
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"
)
var (
gSettings map[string]bool
)
func NewSettingFromResource(res *resource.Setting, cfg *EncoderConfig) resourceState {
return &setting{
cfg: mergeConfig(cfg, res.Config()),
res: res,
st: &res.Res,
}
}
func (n *setting) Prepare(ctx context.Context, pl *payload) (err error) {
// Init global state
if gSettings == nil {
gSettings = make(map[string]bool)
ss, _, err := store.SearchSettingValues(ctx, pl.s, types.SettingsFilter{})
if err != store.ErrNotFound && err != nil {
return err
}
for _, s := range ss {
gSettings[s.Name] = true
}
}
return nil
}
func (n *setting) Encode(ctx context.Context, pl *payload) (err error) {
res := n.st
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.UpdatedAt != nil {
res.UpdatedAt = *ts.UpdatedAt.T
} else {
res.UpdatedAt = *now()
}
} else {
res.UpdatedAt = *now()
}
if us != nil {
if us.OwnedBy != nil {
res.OwnedBy = us.OwnedBy.UserID
}
if us.UpdatedBy != nil {
res.UpdatedBy = us.UpdatedBy.UserID
}
}
if _, exists := gSettings[res.Name]; !exists {
return store.CreateSettingValue(ctx, pl.s, res)
}
// On existing setting, replace/merge right basically overwrites the existing value;
// otherwise, the new value is ignored.
switch n.cfg.OnExisting {
case resource.Replace,
resource.MergeRight:
return store.UpdateSettingValue(ctx, pl.s, res)
}
return nil
}

View File

@@ -0,0 +1,24 @@
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 newSetting(res *types.SettingValue, ux *userIndex) *setting {
return &setting{
st: res,
ux: ux,
}
}
func (st *setting) MarshalEnvoy() ([]resource.Interface, error) {
rs := resource.NewSetting(st.st)
syncUserStamps(rs.Userstamps(), st.ux)
return envoy.CollectNodes(
rs,
)
}

View File

@@ -0,0 +1,764 @@
package store
import (
"context"
"strings"
composeTypes "github.com/cortezaproject/corteza/server/compose/types"
"github.com/cortezaproject/corteza/server/pkg/envoy"
"github.com/cortezaproject/corteza/server/pkg/envoy/resource"
pkgid "github.com/cortezaproject/corteza/server/pkg/id"
"github.com/cortezaproject/corteza/server/pkg/rbac"
"github.com/cortezaproject/corteza/server/store"
"github.com/cortezaproject/corteza/server/system/types"
"github.com/spf13/cast"
)
type (
roleFilter types.RoleFilter
userFilter types.UserFilter
templateFilter types.TemplateFilter
applicationFilter types.ApplicationFilter
apiGwRouteFilter types.ApigwRouteFilter
reportFilter types.ReportFilter
settingFilter types.SettingsFilter
rbacFilter struct {
rbac.RuleFilter
// This will help us determine what rules for what resources we are able to export
resourceID map[uint64]bool
strict bool
}
resourceTranslationFilter types.ResourceTranslationFilter
systemDecoder struct {
resourceID []uint64
ux *userIndex
}
)
func newSystemDecoder(ux *userIndex) *systemDecoder {
return &systemDecoder{
resourceID: make([]uint64, 0, 200),
ux: ux,
}
}
func (d *systemDecoder) decodeRoles(ctx context.Context, s store.Storer, ff []*roleFilter) *auxRsp {
mm := make([]envoy.Marshaller, 0, 100)
if ff == nil {
return &auxRsp{
mm: mm,
}
}
var nn types.RoleSet
var fn types.RoleFilter
var err error
for _, f := range ff {
aux := *f
if aux.Limit == 0 {
aux.Limit = 1000
}
for {
nn, fn, err = s.SearchRoles(ctx, types.RoleFilter(aux))
if err != nil {
return &auxRsp{
err: err,
}
}
for _, n := range nn {
mm = append(mm, newRole(n))
d.resourceID = append(d.resourceID, n.ID)
}
if fn.NextPage != nil {
aux.PageCursor = fn.NextPage
} else {
break
}
}
}
return &auxRsp{
mm: mm,
}
}
func (d *systemDecoder) decodeUsers(ctx context.Context, s store.Storer, ff []*userFilter) *auxRsp {
mm := make([]envoy.Marshaller, 0, 100)
if ff == nil {
return &auxRsp{
mm: mm,
}
}
var nn types.UserSet
var fn types.UserFilter
var err error
for _, f := range ff {
aux := *f
if aux.Limit == 0 {
aux.Limit = 1000
}
for {
nn, fn, err = s.SearchUsers(ctx, types.UserFilter(aux))
if err != nil {
return &auxRsp{
err: err,
}
}
for _, n := range nn {
mm = append(mm, newUser(n))
d.resourceID = append(d.resourceID, n.ID)
}
if fn.NextPage != nil {
aux.PageCursor = fn.NextPage
} else {
break
}
}
}
return &auxRsp{
mm: mm,
}
}
func (d *systemDecoder) decodeTemplates(ctx context.Context, s store.Storer, ff []*templateFilter) *auxRsp {
mm := make([]envoy.Marshaller, 0, 100)
if ff == nil {
return &auxRsp{
mm: mm,
}
}
var nn types.TemplateSet
var fn types.TemplateFilter
var err error
for _, f := range ff {
aux := *f
if aux.Limit == 0 {
aux.Limit = 1000
}
for {
nn, fn, err = s.SearchTemplates(ctx, types.TemplateFilter(aux))
if err != nil {
return &auxRsp{
err: err,
}
}
for _, n := range nn {
mm = append(mm, newTemplate(n))
d.resourceID = append(d.resourceID, n.ID)
}
if fn.NextPage != nil {
aux.PageCursor = fn.NextPage
} else {
break
}
}
}
return &auxRsp{
mm: mm,
}
}
func (d *systemDecoder) decodeAPIGWRoutes(ctx context.Context, s store.Storer, ff []*apiGwRouteFilter) *auxRsp {
mm := make([]envoy.Marshaller, 0, 100)
if ff == nil {
return &auxRsp{
mm: mm,
}
}
var nn types.ApigwRouteSet
var fn types.ApigwRouteFilter
var err error
for _, f := range ff {
aux := *f
if aux.Limit == 0 {
aux.Limit = 1000
}
for {
nn, fn, err = s.SearchApigwRoutes(ctx, types.ApigwRouteFilter(aux))
if err != nil {
return &auxRsp{
err: err,
}
}
// filters
for _, n := range nn {
gwf, _, err := s.SearchApigwFilters(ctx, types.ApigwFilterFilter{RouteID: n.ID})
if err != nil {
return &auxRsp{
err: err,
}
}
mm = append(mm, newAPIGateway(n, gwf, d.ux))
d.resourceID = append(d.resourceID, n.ID)
}
if fn.NextPage != nil {
aux.PageCursor = fn.NextPage
} else {
break
}
}
}
return &auxRsp{
mm: mm,
}
}
func (d *systemDecoder) decodeReports(ctx context.Context, s store.Storer, ff []*reportFilter) *auxRsp {
mm := make([]envoy.Marshaller, 0, 100)
if ff == nil {
return &auxRsp{
mm: mm,
}
}
var nn types.ReportSet
var fn types.ReportFilter
var err error
for _, f := range ff {
aux := *f
if aux.Limit == 0 {
aux.Limit = 1000
}
for {
nn, fn, err = s.SearchReports(ctx, types.ReportFilter(aux))
if err != nil {
return &auxRsp{
err: err,
}
}
for _, n := range nn {
mm = append(mm, newReport(n, d.ux))
d.resourceID = append(d.resourceID, n.ID)
}
if fn.NextPage != nil {
aux.PageCursor = fn.NextPage
} else {
break
}
}
}
return &auxRsp{
mm: mm,
}
}
func (d *systemDecoder) decodeApplications(ctx context.Context, s store.Storer, ff []*applicationFilter) *auxRsp {
mm := make([]envoy.Marshaller, 0, 100)
if ff == nil {
return &auxRsp{
mm: mm,
}
}
var nn types.ApplicationSet
var fn types.ApplicationFilter
var err error
for _, f := range ff {
aux := *f
if aux.Limit == 0 {
aux.Limit = 1000
}
for {
nn, fn, err = s.SearchApplications(ctx, types.ApplicationFilter(aux))
if err != nil {
return &auxRsp{
err: err,
}
}
for _, n := range nn {
// Index users
err = d.ux.add(
ctx,
n.OwnerID,
)
mm = append(mm, newApplication(n, d.ux))
d.resourceID = append(d.resourceID, n.ID)
}
if fn.NextPage != nil {
aux.PageCursor = fn.NextPage
} else {
break
}
}
}
return &auxRsp{
mm: mm,
}
}
func (d *systemDecoder) decodeSettings(ctx context.Context, s store.Storer, ff []*settingFilter) *auxRsp {
mm := make([]envoy.Marshaller, 0, 100)
if ff == nil {
return &auxRsp{
mm: mm,
}
}
var nn types.SettingValueSet
var err error
for _, f := range ff {
aux := *f
for {
nn, _, err = store.SearchSettingValues(ctx, s, types.SettingsFilter(aux))
if err != nil {
return &auxRsp{
err: err,
}
}
for _, n := range nn {
// Index users
err = d.ux.add(
ctx,
n.OwnedBy,
n.UpdatedBy,
)
mm = append(mm, newSetting(n, d.ux))
}
// mm = append(mm, NewSettings(nn))
break
}
}
return &auxRsp{
mm: mm,
}
}
func (d *systemDecoder) decodeRbac(ctx context.Context, s store.Storer, ff []*rbacFilter) *auxRsp {
mm := make([]envoy.Marshaller, 0, 100)
if ff == nil {
return &auxRsp{
mm: mm,
}
}
var nn rbac.RuleSet
var err error
c := func(r *resource.Ref, f *rbacFilter, path ...*resource.Ref) (bool, error) {
if r == nil {
return true, nil
}
// the first identifier is the most specific .. the ID
id, err := cast.ToUint64E(r.Identifiers.First())
if err != nil {
return false, err
}
if r.ResourceType == composeTypes.ModuleFieldResourceType {
id, err = cast.ToUint64E(path[1].Identifiers.First())
if err != nil {
return false, err
}
}
return f.resourceID[id], nil
}
for _, f := range ff {
aux := *f
for {
nn, _, err = s.SearchRbacRules(ctx, rbac.RuleFilter(aux.RuleFilter))
if err != nil {
return &auxRsp{
err: err,
}
}
for _, n := range nn {
r, err := newRbacRule(n)
rt := strings.Split(r.refRbacResource, "/")[0]
if err != nil {
return &auxRsp{
err: err,
}
}
// somesort of a generic rule; no specifc resource
// @todo check for pp inclusion!!
if r.refRbacRes == nil && len(r.refPathRes) == 0 {
mm = append(mm, r)
} else {
// Check the resource ref and the path refs for validity.
// ComposeRecords are fetched in chunks so this check is not valid here.
if rt != composeTypes.RecordResourceType {
if ok, err := c(r.refRbacRes, f, r.refPathRes...); err != nil {
return &auxRsp{
err: err,
}
} else if !ok {
continue
}
}
for _, p := range r.refPathRes {
if ok, err := c(p, f); err != nil {
return &auxRsp{
err: err,
}
} else if !ok {
continue
}
}
mm = append(mm, r)
}
}
break
}
}
return &auxRsp{
mm: mm,
}
}
func (d *systemDecoder) decodeResourceTranslation(ctx context.Context, s store.Storer, ff []*resourceTranslationFilter) *auxRsp {
mm := make([]envoy.Marshaller, 0, 100)
if ff == nil {
return &auxRsp{
mm: mm,
}
}
var (
resMap = make(map[string]*resourceTranslation)
nn types.ResourceTranslationSet
fn types.ResourceTranslationFilter
err error
)
for _, f := range ff {
aux := *f
if aux.Limit == 0 {
aux.Limit = 1000
}
for {
nn, fn, err = s.SearchResourceTranslations(ctx, types.ResourceTranslationFilter(aux))
if err != nil {
return &auxRsp{
err: err,
}
}
for _, n := range nn {
if _, ok := resMap[n.Resource]; !ok {
resMap[n.Resource], err = newResourceTranslation(types.ResourceTranslationSet{n})
if err != nil {
return &auxRsp{
err: err,
}
}
// parse the resource wo we can define relations/check if we can unmarshal it
_, ref, pp, err := resource.ParseResourceTranslation(n.Resource)
resMap[n.Resource].refLocaleRes = ref
resMap[n.Resource].refPathRes = pp
if err != nil {
return &auxRsp{
err: err,
}
}
continue
}
resMap[n.Resource].locales = append(resMap[n.Resource].locales, n)
}
if fn.NextPage != nil {
aux.PageCursor = fn.NextPage
} else {
break
}
break
}
}
for _, lr := range resMap {
mm = append(mm, lr)
}
return &auxRsp{
mm: mm,
}
}
func (df *DecodeFilter) systemFromResource(rr ...string) *DecodeFilter {
for _, r := range rr {
if !strings.HasPrefix(r, "system") {
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 "system:role":
df = df.Roles(&types.RoleFilter{
Query: id,
})
case "system:user":
df = df.Users(&types.UserFilter{
Query: id,
AllKinds: true,
})
case "system:template":
df = df.Templates(&types.TemplateFilter{
Handle: id,
})
templateID, err := cast.ToUint64E(id)
if err == nil && templateID > 0 {
df = df.Templates(&types.TemplateFilter{
TemplateID: pkgid.Strings(templateID),
})
}
case "system:apigw-route":
df = df.APIGWRoutes(&types.ApigwRouteFilter{
Route: id,
})
case "system:report":
df = df.Reports(&types.ReportFilter{
Handle: id,
})
reportID, err := cast.ToUint64E(id)
if err == nil && reportID > 0 {
df = df.Reports(&types.ReportFilter{
ReportID: pkgid.Strings(reportID),
})
}
case "system:application":
df = df.Applications(&types.ApplicationFilter{
Query: id,
})
case "system:setting":
df = df.Settings(&types.SettingsFilter{})
case "system:rbac":
df = df.Rbac(&rbac.RuleFilter{})
case "system:resource-translation":
df = df.Rbac(&rbac.RuleFilter{})
}
}
return df
}
func (df *DecodeFilter) systemFromRef(rr ...*resource.Ref) *DecodeFilter {
for _, r := range rr {
if strings.Index(r.ResourceType, "system") < 0 {
continue
}
switch r.ResourceType {
case types.RoleResourceType:
for _, i := range r.Identifiers.StringSlice() {
df = df.Roles(&types.RoleFilter{
Query: i,
})
}
case types.UserResourceType:
for _, i := range r.Identifiers.StringSlice() {
df = df.Users(&types.UserFilter{
Query: i,
AllKinds: true,
})
}
case types.TemplateResourceType:
for _, i := range r.Identifiers.StringSlice() {
df = df.Templates(&types.TemplateFilter{
Handle: i,
})
templateID, err := cast.ToUint64E(i)
if err == nil && templateID > 0 {
df = df.Templates(&types.TemplateFilter{
TemplateID: pkgid.Strings(templateID),
})
}
}
case types.ApplicationResourceType:
for _, i := range r.Identifiers.StringSlice() {
df = df.Applications(&types.ApplicationFilter{
Query: i,
})
}
}
}
return df
}
// Roles adds a new RoleFilter
func (df *DecodeFilter) Roles(f *types.RoleFilter) *DecodeFilter {
if df.roles == nil {
df.roles = make([]*roleFilter, 0, 1)
}
df.roles = append(df.roles, (*roleFilter)(f))
return df
}
// Users adds a new UserFilter
func (df *DecodeFilter) Users(f *types.UserFilter) *DecodeFilter {
if df.users == nil {
df.users = make([]*userFilter, 0, 1)
}
df.users = append(df.users, (*userFilter)(f))
return df
}
// Templates adds a new TemplateFilter
func (df *DecodeFilter) Templates(f *types.TemplateFilter) *DecodeFilter {
if df.templates == nil {
df.templates = make([]*templateFilter, 0, 1)
}
df.templates = append(df.templates, (*templateFilter)(f))
return df
}
func (df *DecodeFilter) APIGWRoutes(f *types.ApigwRouteFilter) *DecodeFilter {
if df.apiGwRoutes == nil {
df.apiGwRoutes = make([]*apiGwRouteFilter, 0, 1)
}
df.apiGwRoutes = append(df.apiGwRoutes, (*apiGwRouteFilter)(f))
return df
}
func (df *DecodeFilter) Reports(f *types.ReportFilter) *DecodeFilter {
if df.reports == nil {
df.reports = make([]*reportFilter, 0, 1)
}
df.reports = append(df.reports, (*reportFilter)(f))
return df
}
// Applications adds a new ApplicationFilter
func (df *DecodeFilter) Applications(f *types.ApplicationFilter) *DecodeFilter {
if df.applications == nil {
df.applications = make([]*applicationFilter, 0, 1)
}
df.applications = append(df.applications, (*applicationFilter)(f))
return df
}
// Settings adds a new SettingsFilter
func (df *DecodeFilter) Settings(f *types.SettingsFilter) *DecodeFilter {
if df.settings == nil {
df.settings = make([]*settingFilter, 0, 1)
}
df.settings = append(df.settings, (*settingFilter)(f))
return df
}
// Rbac adds a new RuleFilter
func (df *DecodeFilter) Rbac(f *rbac.RuleFilter) *DecodeFilter {
if df.rbac == nil {
df.rbac = make([]*rbacFilter, 0, 1)
} else {
// There can only be a single rbac filter
// since it makes no sense to have multiple of
return df
}
df.rbac = append(df.rbac, &rbacFilter{RuleFilter: *f})
return df
}
// ResourceTranslation adds a new ResourceTranslationFilter
func (df *DecodeFilter) ResourceTranslation(f *types.ResourceTranslationFilter) *DecodeFilter {
if df.resourceTranslations == nil {
df.resourceTranslations = make([]*resourceTranslationFilter, 0, 1)
} else {
// There can only be a single resourceTranslations filter
// since it makes no sense to have multiple of
return df
}
df.resourceTranslations = append(df.resourceTranslations, (*resourceTranslationFilter)(f))
return df
}
func (df *DecodeFilter) RbacStrict(f *rbac.RuleFilter) *DecodeFilter {
if df.rbac == nil {
df.rbac = make([]*rbacFilter, 0, 1)
} else {
// There can only be a single rbac filter
// since it makes no sense to have multiple of
return df
}
df.rbac = append(df.rbac, &rbacFilter{RuleFilter: *f})
return df
}
// allowRbacResource adds a new resource identifier to supported resource rules
func (df *DecodeFilter) allowRbacResource(id ...uint64) {
if df.rbac == nil || len(df.rbac) == 0 {
return
}
rf := df.rbac[0]
if rf.resourceID == nil {
rf.resourceID = make(map[uint64]bool)
}
for _, i := range id {
rf.resourceID[i] = true
}
}

View File

@@ -0,0 +1,98 @@
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 (
template struct {
cfg *EncoderConfig
res *resource.Template
t *types.Template
}
)
// mergeTemplates merges b into a, prioritising a
func mergeTemplates(a, b *types.Template) *types.Template {
c := *a
if c.Handle == "" {
c.Handle = b.Handle
}
if c.Language == "" {
c.Language = b.Language
}
if c.Type == "" {
c.Type = b.Type
}
if c.Meta.Short == "" {
c.Meta.Short = b.Meta.Short
}
if c.Meta.Description == "" {
c.Meta.Description = b.Meta.Description
}
if c.Template == "" {
c.Template = b.Template
}
if c.OwnerID == 0 {
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
}
return &c
}
// findTemplate looks for the template in the resources & the store
//
// Provided resources are prioritized.
func findTemplate(ctx context.Context, s store.Storer, rr resource.InterfaceSet, ii resource.Identifiers) (u *types.Template, err error) {
u = resource.FindTemplate(rr, ii)
if u != nil {
return u, nil
}
return findTemplateStore(ctx, s, makeGenericFilter(ii))
}
// findTemplateStore looks for the template in the store
func findTemplateStore(ctx context.Context, s store.Storer, gf genericFilter) (t *types.Template, err error) {
if gf.id > 0 {
t, err = store.LookupTemplateByID(ctx, s, gf.id)
if err != nil && err != store.ErrNotFound {
return nil, err
}
if t != nil {
return
}
}
for _, i := range gf.identifiers {
// Handle
t, err = store.LookupTemplateByHandle(ctx, s, i)
if err != nil && err != store.ErrNotFound {
return nil, err
}
if t != nil {
return
}
}
return nil, nil
}

View File

@@ -0,0 +1,104 @@
package store
import (
"context"
"github.com/cortezaproject/corteza/server/pkg/envoy/resource"
"github.com/cortezaproject/corteza/server/store"
)
func NewTemplateFromResource(res *resource.Template, cfg *EncoderConfig) resourceState {
return &template{
cfg: mergeConfig(cfg, res.Config()),
res: res,
}
}
func (n *template) Prepare(ctx context.Context, pl *payload) (err error) {
// Reset old identifiers
n.res.Res.ID = 0
// Try to get the original template
n.t, err = findTemplateStore(ctx, pl.s, makeGenericFilter(n.res.Identifiers()))
if err != nil {
return err
}
if n.t != nil {
n.res.Res.ID = n.t.ID
}
return nil
}
func (n *template) Encode(ctx context.Context, pl *payload) (err error) {
res := n.res.Res
exists := n.t != nil && n.t.ID > 0
// Determine the ID
if res.ID <= 0 && exists {
res.ID = n.t.ID
}
if res.ID <= 0 {
res.ID = NextID()
}
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
}
}
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 a fresh template
if !exists {
return store.CreateTemplate(ctx, pl.s, res)
}
// Update existing template
switch n.cfg.OnExisting {
case resource.Skip:
return nil
case resource.MergeLeft:
res = mergeTemplates(n.t, res)
case resource.MergeRight:
res = mergeTemplates(res, n.t)
}
err = store.UpdateTemplate(ctx, pl.s, res)
if err != nil {
return err
}
n.res.Res = res
return nil
}

View File

@@ -0,0 +1,19 @@
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 newTemplate(t *types.Template) *template {
return &template{
t: t,
}
}
func (u *template) MarshalEnvoy() ([]resource.Interface, error) {
return envoy.CollectNodes(
resource.NewTemplate(u.t),
)
}

View File

@@ -0,0 +1,111 @@
package store
import (
"context"
"net/mail"
"github.com/cortezaproject/corteza/server/pkg/envoy/resource"
"github.com/cortezaproject/corteza/server/store"
"github.com/cortezaproject/corteza/server/system/types"
)
type (
user struct {
cfg *EncoderConfig
res *resource.User
u *types.User
}
)
// mergeUsers merges b into a, prioritising a
func mergeUsers(a, b *types.User) *types.User {
c := *a
if c.Username == "" {
c.Username = b.Username
}
if c.Email == "" {
c.Email = b.Email
}
if c.Name == "" {
c.Name = b.Name
}
if c.Handle == "" {
c.Handle = b.Handle
}
if c.Kind == "" {
c.Kind = b.Kind
}
if c.Meta == nil {
c.Meta = b.Meta
}
if c.CreatedAt.IsZero() {
c.CreatedAt = b.CreatedAt
}
if c.UpdatedAt == nil {
c.UpdatedAt = b.UpdatedAt
}
if c.SuspendedAt == nil {
c.SuspendedAt = b.SuspendedAt
}
if c.DeletedAt == nil {
c.DeletedAt = b.DeletedAt
}
return &c
}
// findUser looks for the user in the resources & the store
//
// Provided resources are prioritized.
func findUser(ctx context.Context, s store.Storer, rr resource.InterfaceSet, ii resource.Identifiers) (u *types.User, err error) {
u = resource.FindUser(rr, ii)
if u != nil {
return u, nil
}
return findUserStore(ctx, s, makeGenericFilter(ii))
}
// findUserStore looks for the user in the store
func findUserStore(ctx context.Context, s store.Storer, gf genericFilter) (u *types.User, err error) {
if gf.id > 0 {
u, err = store.LookupUserByID(ctx, s, gf.id)
if err != nil && err != store.ErrNotFound {
return nil, err
}
if u != nil {
return
}
}
for _, i := range gf.identifiers {
// email
if _, err = mail.ParseAddress(i); err == nil {
u, err = store.LookupUserByEmail(ctx, s, i)
if err == store.ErrNotFound {
return nil, nil
} else if err != nil {
return nil, err
}
}
// Handle & username
u, err = store.LookupUserByHandle(ctx, s, i)
if err == store.ErrNotFound {
u, err = store.LookupUserByUsername(ctx, s, i)
}
if err != nil && err != store.ErrNotFound {
return nil, err
}
if u != nil {
return
}
}
return nil, nil
}

View File

@@ -0,0 +1,137 @@
package store
import (
"context"
"fmt"
"github.com/cortezaproject/corteza/server/pkg/envoy/resource"
"github.com/cortezaproject/corteza/server/store"
"github.com/cortezaproject/corteza/server/system/types"
)
func NewUserFromResource(res *resource.User, cfg *EncoderConfig) resourceState {
return &user{
cfg: mergeConfig(cfg, res.Config()),
res: res,
}
}
func (n *user) Prepare(ctx context.Context, pl *payload) (err error) {
// Reset old identifiers
n.res.Res.ID = 0
// Try to get the original user
n.u, err = findUserStore(ctx, pl.s, makeGenericFilter(n.res.Identifiers()))
if err != nil {
return err
}
if n.u != nil {
n.res.Res.ID = n.u.ID
}
return nil
}
func (n *user) Encode(ctx context.Context, pl *payload) (err error) {
res := n.res.Res
exists := n.u != nil && n.u.ID > 0
// Determine the ID
if res.ID <= 0 && exists {
res.ID = n.u.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.SuspendedAt != nil {
res.SuspendedAt = ts.SuspendedAt.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 user
if !exists {
if err = store.CreateUser(ctx, pl.s, res); err != nil {
return
}
return n.membership(ctx, pl.s, res, pl.state.ParentResources, exists)
}
// Update existing user
switch n.cfg.OnExisting {
case resource.Skip:
return nil
case resource.MergeLeft:
res = mergeUsers(n.u, res)
case resource.MergeRight:
res = mergeUsers(res, n.u)
}
if err = store.UpdateUser(ctx, pl.s, res); err != nil {
return err
}
n.res.Res = res
return n.membership(ctx, pl.s, res, pl.state.ParentResources, exists)
}
func (n *user) membership(ctx context.Context, s store.Storer, res *types.User, pp resource.InterfaceSet, exists bool) (err error) {
// find all roles
roles := make([]uint64, 0, 10)
var r *types.Role
for _, m := range n.res.RoleMembership {
r, err = findRole(ctx, s, pp, m)
if err != nil {
return
}
roles = append(roles, r.ID)
}
// update
// @todo some smarter diff calculations; should be fine for now but could be improved.
var mm types.RoleMemberSet
if exists {
mm, _, err = s.SearchRoleMembers(ctx, types.RoleMemberFilter{Resource: fmt.Sprintf("corteza::system:user/%d", n.u.ID)})
if err != nil {
return
}
}
for _, m := range mm {
if err = store.DeleteRoleMember(ctx, s, m); err != nil {
return
}
}
for _, r := range roles {
if err = store.CreateRoleMember(ctx, s, &types.RoleMember{Resource: fmt.Sprintf("corteza::system:user/%d", res.ID), RoleID: r}); err != nil {
return
}
}
return
}

View File

@@ -0,0 +1,58 @@
package store
import (
"testing"
"time"
"github.com/cortezaproject/corteza/server/system/types"
"github.com/stretchr/testify/require"
)
func TestUser_Merger(t *testing.T) {
req := require.New(t)
now := time.Time{}
nowP := &time.Time{}
empty := &types.User{}
full := &types.User{
Username: "username",
Email: "email",
Name: "name",
Handle: "handle",
Kind: types.SystemUser,
Meta: &types.UserMeta{},
CreatedAt: now,
UpdatedAt: nowP,
SuspendedAt: nowP,
DeletedAt: nowP,
}
t.Run("merge on empty", func(t *testing.T) {
c := mergeUsers(empty, full)
req.Equal("username", c.Username)
req.Equal("email", c.Email)
req.Equal("name", c.Name)
req.Equal("handle", c.Handle)
req.Equal(types.SystemUser, c.Kind)
req.Equal(&types.UserMeta{}, c.Meta)
req.Equal(now, c.CreatedAt)
req.Equal(nowP, c.UpdatedAt)
req.Equal(nowP, c.SuspendedAt)
req.Equal(nowP, c.DeletedAt)
})
t.Run("merge with empty", func(t *testing.T) {
c := mergeUsers(full, empty)
req.Equal("username", c.Username)
req.Equal("email", c.Email)
req.Equal("name", c.Name)
req.Equal("handle", c.Handle)
req.Equal(types.SystemUser, c.Kind)
req.Equal(&types.UserMeta{}, c.Meta)
req.Equal(now, c.CreatedAt)
req.Equal(nowP, c.UpdatedAt)
req.Equal(nowP, c.SuspendedAt)
req.Equal(nowP, c.DeletedAt)
})
}

View File

@@ -0,0 +1,19 @@
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 newUser(u *types.User) *user {
return &user{
u: u,
}
}
func (u *user) MarshalEnvoy() ([]resource.Interface, error) {
return envoy.CollectNodes(
resource.NewUser(u.u),
)
}

View File

@@ -0,0 +1,184 @@
package store
import (
"context"
"regexp"
"strconv"
"time"
"github.com/cortezaproject/corteza/server/pkg/envoy/resource"
"github.com/cortezaproject/corteza/server/pkg/expr"
"github.com/cortezaproject/corteza/server/pkg/id"
"github.com/cortezaproject/corteza/server/store"
)
type (
// genericFilter defines resource identifiers in a generic manner
genericFilter struct {
id uint64
identifiers []string
}
)
var (
// simple uint check.
// we'll use the pkg/handle to check for handles.
refy = regexp.MustCompile(`^[1-9](\d*)$`)
// wrapper around NextID that will aid service testing
NextID = func() uint64 {
return id.Next()
}
// wrapper around time.Now() that will aid testing
now = func() *time.Time {
c := time.Now().Round(time.Second)
return &c
}
// Global expression parser
exprP = expr.Parser()
)
func makeGenericFilter(ii resource.Identifiers) (f genericFilter) {
for _, i := range ii {
if i == "" {
continue
}
if refy.MatchString(i) && f.id <= 0 {
id, err := strconv.ParseUint(i, 10, 64)
if err != nil {
continue
}
f.id = id
} else {
f.identifiers = append(f.identifiers, i)
}
}
return f
}
// resolveUserstamps tries to resolve any missing userstamp using the store
//
// @todo rework based on the bellow userIndex
func resolveUserstamps(ctx context.Context, s store.Storer, rr []resource.Interface, us *resource.Userstamps) (*resource.Userstamps, error) {
if us == nil {
return nil, nil
}
fetch := func(us *resource.Userstamp) (*resource.Userstamp, error) {
if us == nil || (us.UserID == 0 && us.Ref == "") {
return nil, nil
}
// This one can be considered as valid
if us.Ref != "" && us.UserID > 0 && us.U != nil {
return us, nil
}
ii := resource.MakeIdentifiers()
if us.UserID > 0 {
ii = ii.Add(strconv.FormatUint(us.UserID, 10))
}
if us.Ref != "" {
ii = ii.Add(us.Ref)
}
u, err := findUser(ctx, s, rr, ii)
if err != nil {
return nil, err
}
if u == nil {
return nil, resource.UserErrUnresolved(ii)
}
return resource.MakeUserstamp(u), nil
}
var err error
us.CreatedBy, err = fetch(us.CreatedBy)
us.UpdatedBy, err = fetch(us.UpdatedBy)
us.DeletedBy, err = fetch(us.DeletedBy)
us.OwnedBy, err = fetch(us.OwnedBy)
us.RunAs, err = fetch(us.RunAs)
if err != nil {
return nil, err
}
return us, nil
}
// syncUserStamps tries to resolve any missing userstamp using the userIndex
func syncUserStamps(us *resource.Userstamps, ux *userIndex) {
if us.CreatedBy != nil && us.CreatedBy.UserID > 0 {
us.CreatedBy.U = ux.users[us.CreatedBy.UserID]
}
if us.UpdatedBy != nil && us.UpdatedBy.UserID > 0 {
us.UpdatedBy.U = ux.users[us.UpdatedBy.UserID]
}
if us.DeletedBy != nil && us.DeletedBy.UserID > 0 {
us.DeletedBy.U = ux.users[us.DeletedBy.UserID]
}
if us.OwnedBy != nil && us.OwnedBy.UserID > 0 {
us.OwnedBy.U = ux.users[us.OwnedBy.UserID]
}
if us.RunAs != nil && us.RunAs.UserID > 0 {
us.RunAs.U = ux.users[us.RunAs.UserID]
}
}
func mergeConfig(ec *EncoderConfig, rs *resource.EnvoyConfig) *EncoderConfig {
// Nothing we can do
if rs == nil {
return ec
}
// Take resource config as base
rr := &EncoderConfig{
OnExisting: rs.OnExisting,
SkipIf: rs.SkipIf,
}
// Default to store config
rr.Defer = ec.Defer
rr.DeferNok = ec.DeferNok
rr.DeferOk = ec.DeferOk
if rr.OnExisting == resource.Default {
rr.OnExisting = ec.OnExisting
}
if rr.SkipIf == "" {
rr.SkipIf = ec.SkipIf
}
return rr
}
func basicSkipEval(ctx context.Context, cfg *EncoderConfig, missing bool) (bool, error) {
if cfg == nil {
cfg = &EncoderConfig{}
}
if cfg.SkipIf != "" {
evl, err := exprP.NewEvaluable(cfg.SkipIf)
if err != nil {
return false, err
}
// @todo expand this
skip, err := evl.EvalBool(ctx, map[string]interface{}{
"missing": missing,
})
if err != nil {
return false, err
}
return skip, nil
}
// Don't skip by default
return false, nil
}