diff --git a/store/federation_nodes.gen.go b/store/federation_nodes.gen.go new file mode 100644 index 000000000..79afb2f94 --- /dev/null +++ b/store/federation_nodes.gen.go @@ -0,0 +1,77 @@ +package store + +// This file is auto-generated. +// +// Template: pkg/codegen/assets/store_base.gen.go.tpl +// Definitions: store/federation_nodes.yaml +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. + +import ( + "context" + "github.com/cortezaproject/corteza-server/federation/types" +) + +type ( + FederationNodes interface { + SearchFederationNodes(ctx context.Context, f types.NodeFilter) (types.NodeSet, types.NodeFilter, error) + LookupFederationNodeByID(ctx context.Context, id uint64) (*types.Node, error) + + CreateFederationNode(ctx context.Context, rr ...*types.Node) error + + UpdateFederationNode(ctx context.Context, rr ...*types.Node) error + + UpsertFederationNode(ctx context.Context, rr ...*types.Node) error + + DeleteFederationNode(ctx context.Context, rr ...*types.Node) error + DeleteFederationNodeByID(ctx context.Context, ID uint64) error + + TruncateFederationNodes(ctx context.Context) error + } +) + +var _ *types.Node +var _ context.Context + +// SearchFederationNodes returns all matching FederationNodes from store +func SearchFederationNodes(ctx context.Context, s FederationNodes, f types.NodeFilter) (types.NodeSet, types.NodeFilter, error) { + return s.SearchFederationNodes(ctx, f) +} + +// LookupFederationNodeByID searches for shared federation node by ID +// +// It returns shared federation node +func LookupFederationNodeByID(ctx context.Context, s FederationNodes, id uint64) (*types.Node, error) { + return s.LookupFederationNodeByID(ctx, id) +} + +// CreateFederationNode creates one or more FederationNodes in store +func CreateFederationNode(ctx context.Context, s FederationNodes, rr ...*types.Node) error { + return s.CreateFederationNode(ctx, rr...) +} + +// UpdateFederationNode updates one or more (existing) FederationNodes in store +func UpdateFederationNode(ctx context.Context, s FederationNodes, rr ...*types.Node) error { + return s.UpdateFederationNode(ctx, rr...) +} + +// UpsertFederationNode creates new or updates existing one or more FederationNodes in store +func UpsertFederationNode(ctx context.Context, s FederationNodes, rr ...*types.Node) error { + return s.UpsertFederationNode(ctx, rr...) +} + +// DeleteFederationNode Deletes one or more FederationNodes from store +func DeleteFederationNode(ctx context.Context, s FederationNodes, rr ...*types.Node) error { + return s.DeleteFederationNode(ctx, rr...) +} + +// DeleteFederationNodeByID Deletes FederationNode from store +func DeleteFederationNodeByID(ctx context.Context, s FederationNodes, ID uint64) error { + return s.DeleteFederationNodeByID(ctx, ID) +} + +// TruncateFederationNodes Deletes all FederationNodes from store +func TruncateFederationNodes(ctx context.Context, s FederationNodes) error { + return s.TruncateFederationNodes(ctx) +} diff --git a/store/federation_nodes.yaml b/store/federation_nodes.yaml new file mode 100644 index 000000000..13ce0f305 --- /dev/null +++ b/store/federation_nodes.yaml @@ -0,0 +1,40 @@ +import: + - github.com/cortezaproject/corteza-server/federation/types + +types: + type: types.Node + +fields: + - { field: ID } + - { field: Name } + - { field: SharedNodeID } + - { field: BaseURL } + - { field: Status } + - { field: PairToken } + - { field: AuthToken } + - { field: CreatedBy } + - { field: UpdatedBy } + - { field: DeletedBy } + - { field: CreatedAt } + - { field: UpdatedAt } + - { field: DeletedAt } + + +lookups: + - fields: [ID] + description: |- + searches for shared federation node by ID + + It returns shared federation node + +search: + enablePaging: false + enableSorting: false + +rdbms: + alias: fdn + table: federation_nodes + customFilterConverter: true + mapFields: + BaseURL: { column: base_url } + SharedNodeID: { column: shared_node_id } diff --git a/store/interfaces.gen.go b/store/interfaces.gen.go index 4e537972e..517c3b26d 100644 --- a/store/interfaces.gen.go +++ b/store/interfaces.gen.go @@ -18,6 +18,7 @@ package store // - store/credentials.yaml // - store/federation_exposed_modules.yaml // - store/federation_module_mappings.yaml +// - store/federation_nodes.yaml // - store/federation_shared_modules.yaml // - store/labels.yaml // - store/messaging_attachments.yaml @@ -56,6 +57,7 @@ type ( Credentials FederationExposedModules FederationModuleMappings + FederationNodes FederationSharedModules Labels MessagingAttachments diff --git a/store/rdbms/federation_nodes.gen.go b/store/rdbms/federation_nodes.gen.go new file mode 100644 index 000000000..28d6f2402 --- /dev/null +++ b/store/rdbms/federation_nodes.gen.go @@ -0,0 +1,363 @@ +package rdbms + +// This file is an auto-generated file +// +// Template: pkg/codegen/assets/store_rdbms.gen.go.tpl +// Definitions: store/federation_nodes.yaml +// +// Changes to this file may cause incorrect behavior +// and will be lost if the code is regenerated. + +import ( + "context" + "database/sql" + "errors" + "fmt" + "github.com/Masterminds/squirrel" + "github.com/cortezaproject/corteza-server/federation/types" + "github.com/cortezaproject/corteza-server/store" +) + +var _ = errors.Is + +// SearchFederationNodes returns all matching rows +// +// This function calls convertFederationNodeFilter with the given +// types.NodeFilter and expects to receive a working squirrel.SelectBuilder +func (s Store) SearchFederationNodes(ctx context.Context, f types.NodeFilter) (types.NodeSet, types.NodeFilter, error) { + var ( + err error + set []*types.Node + q squirrel.SelectBuilder + ) + q, err = s.convertFederationNodeFilter(f) + if err != nil { + return nil, f, err + } + + return set, f, s.config.ErrorHandler(func() error { + set, _, _, err = s.QueryFederationNodes(ctx, q, f.Check) + return err + + }()) +} + +// QueryFederationNodes queries the database, converts and checks each row and +// returns collected set +// +// Fn also returns total number of fetched items and last fetched item so that the caller can construct cursor +// for next page of results +func (s Store) QueryFederationNodes( + ctx context.Context, + q squirrel.Sqlizer, + check func(*types.Node) (bool, error), +) ([]*types.Node, uint, *types.Node, error) { + var ( + set = make([]*types.Node, 0, DefaultSliceCapacity) + res *types.Node + + // Query rows with + rows, err = s.Query(ctx, q) + + fetched uint + ) + + if err != nil { + return nil, 0, nil, err + } + + defer rows.Close() + for rows.Next() { + fetched++ + if err = rows.Err(); err == nil { + res, err = s.internalFederationNodeRowScanner(rows) + } + + if err != nil { + return nil, 0, nil, err + } + + // If check function is set, call it and act accordingly + if check != nil { + if chk, err := check(res); err != nil { + return nil, 0, nil, err + } else if !chk { + // did not pass the check + // go with the next row + continue + } + } + + set = append(set, res) + } + + return set, fetched, res, rows.Err() +} + +// LookupFederationNodeByID searches for shared federation node by ID +// +// It returns shared federation node +func (s Store) LookupFederationNodeByID(ctx context.Context, id uint64) (*types.Node, error) { + return s.execLookupFederationNode(ctx, squirrel.Eq{ + s.preprocessColumn("fdn.id", ""): s.preprocessValue(id, ""), + }) +} + +// CreateFederationNode creates one or more rows in federation_nodes table +func (s Store) CreateFederationNode(ctx context.Context, rr ...*types.Node) (err error) { + for _, res := range rr { + err = s.checkFederationNodeConstraints(ctx, res) + if err != nil { + return err + } + + err = s.execCreateFederationNodes(ctx, s.internalFederationNodeEncoder(res)) + if err != nil { + return err + } + } + + return +} + +// UpdateFederationNode updates one or more existing rows in federation_nodes +func (s Store) UpdateFederationNode(ctx context.Context, rr ...*types.Node) error { + return s.config.ErrorHandler(s.partialFederationNodeUpdate(ctx, nil, rr...)) +} + +// partialFederationNodeUpdate updates one or more existing rows in federation_nodes +func (s Store) partialFederationNodeUpdate(ctx context.Context, onlyColumns []string, rr ...*types.Node) (err error) { + for _, res := range rr { + err = s.checkFederationNodeConstraints(ctx, res) + if err != nil { + return err + } + + err = s.execUpdateFederationNodes( + ctx, + squirrel.Eq{ + s.preprocessColumn("fdn.id", ""): s.preprocessValue(res.ID, ""), + }, + s.internalFederationNodeEncoder(res).Skip("id").Only(onlyColumns...)) + if err != nil { + return s.config.ErrorHandler(err) + } + } + + return +} + +// UpsertFederationNode updates one or more existing rows in federation_nodes +func (s Store) UpsertFederationNode(ctx context.Context, rr ...*types.Node) (err error) { + for _, res := range rr { + err = s.checkFederationNodeConstraints(ctx, res) + if err != nil { + return err + } + + err = s.config.ErrorHandler(s.execUpsertFederationNodes(ctx, s.internalFederationNodeEncoder(res))) + if err != nil { + return err + } + } + + return nil +} + +// DeleteFederationNode Deletes one or more rows from federation_nodes table +func (s Store) DeleteFederationNode(ctx context.Context, rr ...*types.Node) (err error) { + for _, res := range rr { + + err = s.execDeleteFederationNodes(ctx, squirrel.Eq{ + s.preprocessColumn("fdn.id", ""): s.preprocessValue(res.ID, ""), + }) + if err != nil { + return s.config.ErrorHandler(err) + } + } + + return nil +} + +// DeleteFederationNodeByID Deletes row from the federation_nodes table +func (s Store) DeleteFederationNodeByID(ctx context.Context, ID uint64) error { + return s.execDeleteFederationNodes(ctx, squirrel.Eq{ + s.preprocessColumn("fdn.id", ""): s.preprocessValue(ID, ""), + }) +} + +// TruncateFederationNodes Deletes all rows from the federation_nodes table +func (s Store) TruncateFederationNodes(ctx context.Context) error { + return s.config.ErrorHandler(s.Truncate(ctx, s.federationNodeTable())) +} + +// execLookupFederationNode prepares FederationNode query and executes it, +// returning types.Node (or error) +func (s Store) execLookupFederationNode(ctx context.Context, cnd squirrel.Sqlizer) (res *types.Node, err error) { + var ( + row rowScanner + ) + + row, err = s.QueryRow(ctx, s.federationNodesSelectBuilder().Where(cnd)) + if err != nil { + return + } + + res, err = s.internalFederationNodeRowScanner(row) + if err != nil { + return + } + + return res, nil +} + +// execCreateFederationNodes updates all matched (by cnd) rows in federation_nodes with given data +func (s Store) execCreateFederationNodes(ctx context.Context, payload store.Payload) error { + return s.config.ErrorHandler(s.Exec(ctx, s.InsertBuilder(s.federationNodeTable()).SetMap(payload))) +} + +// execUpdateFederationNodes updates all matched (by cnd) rows in federation_nodes with given data +func (s Store) execUpdateFederationNodes(ctx context.Context, cnd squirrel.Sqlizer, set store.Payload) error { + return s.config.ErrorHandler(s.Exec(ctx, s.UpdateBuilder(s.federationNodeTable("fdn")).Where(cnd).SetMap(set))) +} + +// execUpsertFederationNodes inserts new or updates matching (by-primary-key) rows in federation_nodes with given data +func (s Store) execUpsertFederationNodes(ctx context.Context, set store.Payload) error { + upsert, err := s.config.UpsertBuilder( + s.config, + s.federationNodeTable(), + set, + "id", + ) + + if err != nil { + return err + } + + return s.config.ErrorHandler(s.Exec(ctx, upsert)) +} + +// execDeleteFederationNodes Deletes all matched (by cnd) rows in federation_nodes with given data +func (s Store) execDeleteFederationNodes(ctx context.Context, cnd squirrel.Sqlizer) error { + return s.config.ErrorHandler(s.Exec(ctx, s.DeleteBuilder(s.federationNodeTable("fdn")).Where(cnd))) +} + +func (s Store) internalFederationNodeRowScanner(row rowScanner) (res *types.Node, err error) { + res = &types.Node{} + + if _, has := s.config.RowScanners["federationNode"]; has { + scanner := s.config.RowScanners["federationNode"].(func(_ rowScanner, _ *types.Node) error) + err = scanner(row, res) + } else { + err = row.Scan( + &res.ID, + &res.Name, + &res.SharedNodeID, + &res.BaseURL, + &res.Status, + &res.PairToken, + &res.AuthToken, + &res.CreatedBy, + &res.UpdatedBy, + &res.DeletedBy, + &res.CreatedAt, + &res.UpdatedAt, + &res.DeletedAt, + ) + } + + if err == sql.ErrNoRows { + return nil, store.ErrNotFound + } + + if err != nil { + return nil, fmt.Errorf("could not scan db row for FederationNode: %w", err) + } else { + return res, nil + } +} + +// QueryFederationNodes returns squirrel.SelectBuilder with set table and all columns +func (s Store) federationNodesSelectBuilder() squirrel.SelectBuilder { + return s.SelectBuilder(s.federationNodeTable("fdn"), s.federationNodeColumns("fdn")...) +} + +// federationNodeTable name of the db table +func (Store) federationNodeTable(aa ...string) string { + var alias string + if len(aa) > 0 { + alias = " AS " + aa[0] + } + + return "federation_nodes" + alias +} + +// FederationNodeColumns returns all defined table columns +// +// With optional string arg, all columns are returned aliased +func (Store) federationNodeColumns(aa ...string) []string { + var alias string + if len(aa) > 0 { + alias = aa[0] + "." + } + + return []string{ + alias + "id", + alias + "name", + alias + "shared_node_id", + alias + "base_url", + alias + "status", + alias + "pair_token", + alias + "auth_token", + alias + "created_by", + alias + "updated_by", + alias + "deleted_by", + alias + "created_at", + alias + "updated_at", + alias + "deleted_at", + } +} + +// {true true false false true} + +// internalFederationNodeEncoder encodes fields from types.Node to store.Payload (map) +// +// Encoding is done by using generic approach or by calling encodeFederationNode +// func when rdbms.customEncoder=true +func (s Store) internalFederationNodeEncoder(res *types.Node) store.Payload { + return store.Payload{ + "id": res.ID, + "name": res.Name, + "shared_node_id": res.SharedNodeID, + "base_url": res.BaseURL, + "status": res.Status, + "pair_token": res.PairToken, + "auth_token": res.AuthToken, + "created_by": res.CreatedBy, + "updated_by": res.UpdatedBy, + "deleted_by": res.DeletedBy, + "created_at": res.CreatedAt, + "updated_at": res.UpdatedAt, + "deleted_at": res.DeletedAt, + } +} + +// checkFederationNodeConstraints performs lookups (on valid) resource to check if any of the values on unique fields +// already exists in the store +// +// Using built-in constraint checking would be more performant but unfortunately we can not rely +// on the full support (MySQL does not support conditional indexes) +func (s *Store) checkFederationNodeConstraints(ctx context.Context, res *types.Node) error { + // Consider resource valid when all fields in unique constraint check lookups + // have valid (non-empty) value + // + // Only string and uint64 are supported for now + // feel free to add additional types if needed + var valid = true + + if !valid { + return nil + } + + return nil +} diff --git a/store/rdbms/federation_nodes.go b/store/rdbms/federation_nodes.go new file mode 100644 index 000000000..abbb257e9 --- /dev/null +++ b/store/rdbms/federation_nodes.go @@ -0,0 +1,28 @@ +package rdbms + +import ( + "github.com/Masterminds/squirrel" + "github.com/cortezaproject/corteza-server/federation/types" + "github.com/cortezaproject/corteza-server/pkg/filter" + "strings" +) + +func (s Store) convertFederationNodeFilter(f types.NodeFilter) (query squirrel.SelectBuilder, err error) { + query = s.federationNodesSelectBuilder() + + query = filter.StateCondition(query, "fdn.deleted_at", f.Deleted) + + if f.Query != "" { + q := "%" + strings.ToLower(f.Query) + "%" + query = query.Where(squirrel.Or{ + squirrel.Like{"LOWER(fdn.name)": q}, + squirrel.Like{"LOWER(fdn.base_url)": q}, + }) + } + + if f.Status != "" { + query = query.Where(squirrel.Eq{"fdn.status": strings.ToLower(f.Status)}) + } + + return +} diff --git a/store/tests/federation_exposed_modules_test.go b/store/tests/federation_exposed_modules_test.go new file mode 100644 index 000000000..9e1401a38 --- /dev/null +++ b/store/tests/federation_exposed_modules_test.go @@ -0,0 +1,10 @@ +package tests + +import ( + "github.com/cortezaproject/corteza-server/store" + "testing" +) + +func testFederationExposedModules(t *testing.T, s store.FederationExposedModules) { + // @todo +} diff --git a/store/tests/federation_module_mappings_test.go b/store/tests/federation_module_mappings_test.go new file mode 100644 index 000000000..bd3548571 --- /dev/null +++ b/store/tests/federation_module_mappings_test.go @@ -0,0 +1,10 @@ +package tests + +import ( + "github.com/cortezaproject/corteza-server/store" + "testing" +) + +func testFederationModuleMappings(t *testing.T, s store.FederationModuleMappings) { + // @todo +} diff --git a/store/tests/federation_nodes_test.go b/store/tests/federation_nodes_test.go new file mode 100644 index 000000000..772d7c286 --- /dev/null +++ b/store/tests/federation_nodes_test.go @@ -0,0 +1,144 @@ +package tests + +import ( + "context" + "github.com/cortezaproject/corteza-server/federation/types" + "github.com/cortezaproject/corteza-server/pkg/filter" + "github.com/cortezaproject/corteza-server/pkg/id" + "github.com/cortezaproject/corteza-server/pkg/rand" + "github.com/cortezaproject/corteza-server/store" + "github.com/stretchr/testify/require" + "testing" + "time" +) + +func testFederationNodes(t *testing.T, s store.FederationNodes) { + var ( + ctx = context.Background() + req = require.New(t) + + makeNew = func(name string) *types.Node { + // minimum data set for new FederationNode + return &types.Node{ + ID: id.Next(), + CreatedAt: time.Now(), + Name: name, + } + } + + truncAndCreate = func(t *testing.T) (*require.Assertions, *types.Node) { + req := require.New(t) + req.NoError(s.TruncateFederationNodes(ctx)) + res := makeNew(string(rand.Bytes(10))) + req.NoError(s.CreateFederationNode(ctx, res)) + return req, res + } + ) + + t.Run("create", func(t *testing.T) { + FederationNode := makeNew("FederationNodeCRUD") + req.NoError(s.CreateFederationNode(ctx, FederationNode)) + }) + + t.Run("lookup by ID", func(t *testing.T) { + req, FederationNode := truncAndCreate(t) + fetched, err := s.LookupFederationNodeByID(ctx, FederationNode.ID) + req.NoError(err) + req.Equal(FederationNode.Name, fetched.Name) + req.Equal(FederationNode.ID, fetched.ID) + req.NotNil(fetched.CreatedAt) + req.Nil(fetched.UpdatedAt) + req.Nil(fetched.DeletedAt) + }) + + t.Run("update", func(t *testing.T) { + req, FederationNode := truncAndCreate(t) + FederationNode.Name = "FederationNodeCRUD+2" + + req.NoError(s.UpdateFederationNode(ctx, FederationNode)) + + updated, err := s.LookupFederationNodeByID(ctx, FederationNode.ID) + req.NoError(err) + req.Equal(FederationNode.Name, updated.Name) + }) + + t.Run("upsert", func(t *testing.T) { + t.Run("existing", func(t *testing.T) { + req, FederationNode := truncAndCreate(t) + FederationNode.Name = "FederationNodeCRUD+2" + + req.NoError(s.UpsertFederationNode(ctx, FederationNode)) + + updated, err := s.LookupFederationNodeByID(ctx, FederationNode.ID) + req.NoError(err) + req.Equal(FederationNode.Name, updated.Name) + }) + + t.Run("new", func(t *testing.T) { + FederationNode := makeNew("upsert me") + FederationNode.Name = "ComposeChartCRUD+2" + + req.NoError(s.UpsertFederationNode(ctx, FederationNode)) + + upserted, err := s.LookupFederationNodeByID(ctx, FederationNode.ID) + req.NoError(err) + req.Equal(FederationNode.Name, upserted.Name) + }) + }) + + t.Run("delete", func(t *testing.T) { + t.Run("by FederationNode", func(t *testing.T) { + req, FederationNode := truncAndCreate(t) + req.NoError(s.DeleteFederationNode(ctx, FederationNode)) + _, err := s.LookupFederationNodeByID(ctx, FederationNode.ID) + req.EqualError(err, store.ErrNotFound.Error()) + }) + + t.Run("by ID", func(t *testing.T) { + req, FederationNode := truncAndCreate(t) + req.NoError(s.DeleteFederationNodeByID(ctx, FederationNode.ID)) + _, err := s.LookupFederationNodeByID(ctx, FederationNode.ID) + req.EqualError(err, store.ErrNotFound.Error()) + }) + }) + + t.Run("search", func(t *testing.T) { + prefill := []*types.Node{ + makeNew("/one-one"), + makeNew("/one-two"), + makeNew("/two-one"), + makeNew("/two-two"), + makeNew("/two-deleted"), + } + + count := len(prefill) + + prefill[4].DeletedAt = &prefill[4].CreatedAt + valid := count - 1 + + req.NoError(s.TruncateFederationNodes(ctx)) + req.NoError(s.CreateFederationNode(ctx, prefill...)) + + // search for all valid + set, f, err := s.SearchFederationNodes(ctx, types.NodeFilter{}) + req.NoError(err) + req.Len(set, valid) // we've deleted one + + // search for ALL + set, f, err = s.SearchFederationNodes(ctx, types.NodeFilter{Deleted: filter.StateInclusive}) + req.NoError(err) + req.Len(set, count) // we've deleted one + + // search for deleted only + set, f, err = s.SearchFederationNodes(ctx, types.NodeFilter{Deleted: filter.StateExclusive}) + req.NoError(err) + req.Len(set, 1) // we've deleted one + + // find all prefixed + set, f, err = s.SearchFederationNodes(ctx, types.NodeFilter{Query: "/two"}) + req.NoError(err) + req.Len(set, 2) + + _ = f // dummy + }) +} diff --git a/store/tests/federation_shared_modules_test.go b/store/tests/federation_shared_modules_test.go new file mode 100644 index 000000000..13f0891c3 --- /dev/null +++ b/store/tests/federation_shared_modules_test.go @@ -0,0 +1,10 @@ +package tests + +import ( + "github.com/cortezaproject/corteza-server/store" + "testing" +) + +func testFederationSharedModules(t *testing.T, s store.FederationSharedModules) { + // @todo +} diff --git a/store/tests/gen_test.go b/store/tests/gen_test.go index f79a5e183..ebc323d00 100644 --- a/store/tests/gen_test.go +++ b/store/tests/gen_test.go @@ -16,6 +16,7 @@ package tests // - store/credentials.yaml // - store/federation_exposed_modules.yaml // - store/federation_module_mappings.yaml +// - store/federation_nodes.yaml // - store/federation_shared_modules.yaml // - store/labels.yaml // - store/messaging_attachments.yaml @@ -114,6 +115,11 @@ func testAllGenerated(t *testing.T, s store.Storer) { testFederationModuleMappings(t, s) }) + // Run generated tests for FederationNodes + t.Run("FederationNodes", func(t *testing.T) { + testFederationNodes(t, s) + }) + // Run generated tests for FederationSharedModules t.Run("FederationSharedModules", func(t *testing.T) { testFederationSharedModules(t, s)