From 2cbc5605bfccab7835dffac740d77a98478544d2 Mon Sep 17 00:00:00 2001 From: Peter Grlica Date: Thu, 17 Sep 2020 15:26:45 +0200 Subject: [PATCH] Added exposed module service, added details and delete endpoints, removed module service --- federation/rest.yaml | 33 +- federation/rest/handlers/module.go | 2 +- federation/rest/handlers/syncStructure.go | 86 ++ federation/rest/module.go | 8 +- federation/rest/request/syncStructure.go | 169 ++++ federation/rest/router.go | 8 +- federation/rest/sync_structure.go | 24 + .../service/exposed_module_actions.gen.go | 847 ++++++++++++++++++ .../service/exposed_module_actions.yaml | 82 ++ federation/service/module.go | 37 - federation/service/sync/structure.go | 41 - federation/service/sync_structure.go | 143 +++ federation/types/exposed_module.go | 30 + federation/types/module_field_mapping.go | 36 + federation/types/type_set.gen.go | 61 ++ federation/types/type_set.gen_test.go | 90 ++ federation/types/types.yaml | 1 + store/federation_exposed_modules.gen.go | 77 ++ store/federation_exposed_modules.yaml | 24 + store/interfaces.gen.go | 2 + store/rdbms/federation_exposed_modules.gen.go | 509 +++++++++++ store/rdbms/federation_exposed_modules.go | 26 + store/tests/gen_test.go | 6 + 23 files changed, 2251 insertions(+), 91 deletions(-) create mode 100644 federation/rest/handlers/syncStructure.go create mode 100644 federation/rest/request/syncStructure.go create mode 100644 federation/rest/sync_structure.go create mode 100644 federation/service/exposed_module_actions.gen.go create mode 100644 federation/service/exposed_module_actions.yaml delete mode 100644 federation/service/module.go delete mode 100644 federation/service/sync/structure.go create mode 100644 federation/service/sync_structure.go create mode 100644 federation/types/exposed_module.go create mode 100644 federation/types/module_field_mapping.go create mode 100644 store/federation_exposed_modules.gen.go create mode 100644 store/federation_exposed_modules.yaml create mode 100644 store/rdbms/federation_exposed_modules.gen.go create mode 100644 store/rdbms/federation_exposed_modules.go diff --git a/federation/rest.yaml b/federation/rest.yaml index 16c1a30e3..b784efa81 100644 --- a/federation/rest.yaml +++ b/federation/rest.yaml @@ -78,18 +78,37 @@ endpoints: type: string required: true title: Auth token of the origin node - - title: Modules - description: Federation module definitions - entrypoint: module - path: "/structure/module" + + - title: Federation sync structure + description: Federation structure sync + entrypoint: syncStructure + path: "/nodes/{nodeID}/modules/{moduleID}" authentication: [] apis: - - name: read + - name: readExposed method: GET - title: Read federated module - path: "/{moduleID}" + title: Exposed settings for module + path: "/exposed" parameters: path: + - type: uint64 + name: nodeID + required: true + title: Node ID + - type: uint64 + name: moduleID + required: true + title: Module ID + - name: remove + method: DELETE + title: Remove from federation + path: "/exposed" + parameters: + path: + - type: uint64 + name: nodeID + required: true + title: Node ID - type: uint64 name: moduleID required: true diff --git a/federation/rest/handlers/module.go b/federation/rest/handlers/module.go index 4fcae427e..d942ef37d 100644 --- a/federation/rest/handlers/module.go +++ b/federation/rest/handlers/module.go @@ -58,6 +58,6 @@ func NewModule(h ModuleAPI) *Module { func (h Module) MountRoutes(r chi.Router, middlewares ...func(http.Handler) http.Handler) { r.Group(func(r chi.Router) { r.Use(middlewares...) - r.Get("/structure/module/{moduleID}", h.Read) + r.Get("/exposed/modules/{moduleID}", h.Read) }) } diff --git a/federation/rest/handlers/syncStructure.go b/federation/rest/handlers/syncStructure.go new file mode 100644 index 000000000..722c1a45e --- /dev/null +++ b/federation/rest/handlers/syncStructure.go @@ -0,0 +1,86 @@ +package handlers + +// This file is auto-generated. +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +// Definitions file that controls how this file is generated: +// + +import ( + "context" + "github.com/go-chi/chi" + "github.com/titpetric/factory/resputil" + "net/http" + + "github.com/cortezaproject/corteza-server/federation/rest/request" + "github.com/cortezaproject/corteza-server/pkg/logger" +) + +type ( + // Internal API interface + SyncStructureAPI interface { + ReadExposed(context.Context, *request.SyncStructureReadExposed) (interface{}, error) + Remove(context.Context, *request.SyncStructureRemove) (interface{}, error) + } + + // HTTP API interface + SyncStructure struct { + ReadExposed func(http.ResponseWriter, *http.Request) + Remove func(http.ResponseWriter, *http.Request) + } +) + +func NewSyncStructure(h SyncStructureAPI) *SyncStructure { + return &SyncStructure{ + ReadExposed: func(w http.ResponseWriter, r *http.Request) { + defer r.Body.Close() + params := request.NewSyncStructureReadExposed() + if err := params.Fill(r); err != nil { + logger.LogParamError("SyncStructure.ReadExposed", r, err) + resputil.JSON(w, err) + return + } + + value, err := h.ReadExposed(r.Context(), params) + if err != nil { + logger.LogControllerError("SyncStructure.ReadExposed", r, err, params.Auditable()) + resputil.JSON(w, err) + return + } + logger.LogControllerCall("SyncStructure.ReadExposed", r, params.Auditable()) + if !serveHTTP(value, w, r) { + resputil.JSON(w, value) + } + }, + Remove: func(w http.ResponseWriter, r *http.Request) { + defer r.Body.Close() + params := request.NewSyncStructureRemove() + if err := params.Fill(r); err != nil { + logger.LogParamError("SyncStructure.Remove", r, err) + resputil.JSON(w, err) + return + } + + value, err := h.Remove(r.Context(), params) + if err != nil { + logger.LogControllerError("SyncStructure.Remove", r, err, params.Auditable()) + resputil.JSON(w, err) + return + } + logger.LogControllerCall("SyncStructure.Remove", r, params.Auditable()) + if !serveHTTP(value, w, r) { + resputil.JSON(w, value) + } + }, + } +} + +func (h SyncStructure) MountRoutes(r chi.Router, middlewares ...func(http.Handler) http.Handler) { + r.Group(func(r chi.Router) { + r.Use(middlewares...) + r.Get("/nodes/{nodeID}/modules/{moduleID}/exposed", h.ReadExposed) + r.Delete("/nodes/{nodeID}/modules/{moduleID}/exposed", h.Remove) + }) +} diff --git a/federation/rest/module.go b/federation/rest/module.go index 05ebf06f8..f59ecd571 100644 --- a/federation/rest/module.go +++ b/federation/rest/module.go @@ -4,6 +4,7 @@ import ( "context" "github.com/cortezaproject/corteza-server/federation/rest/request" + "github.com/cortezaproject/corteza-server/federation/service" ) type ( @@ -17,5 +18,10 @@ func (Module) New() *Module { func (ctrl Module) Read(ctx context.Context, r *request.ModuleRead) (interface{}, error) { // use filtering and call structure sync service - return &struct{}{}, nil + s := service.ExposedModule() + + // find the correct node (from request) and use it here + mod, err := s.FindByID(context.Background(), 0, r.GetModuleID()) + + return mod, err } diff --git a/federation/rest/request/syncStructure.go b/federation/rest/request/syncStructure.go new file mode 100644 index 000000000..82b84803e --- /dev/null +++ b/federation/rest/request/syncStructure.go @@ -0,0 +1,169 @@ +package request + +// This file is auto-generated. +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +// Definitions file that controls how this file is generated: +// + +import ( + "encoding/json" + "fmt" + "github.com/cortezaproject/corteza-server/pkg/payload" + "github.com/go-chi/chi" + "io" + "mime/multipart" + "net/http" + "strings" +) + +// dummy vars to prevent +// unused imports complain +var ( + _ = chi.URLParam + _ = multipart.ErrMessageTooLarge + _ = payload.ParseUint64s +) + +type ( + // Internal API interface + SyncStructureReadExposed struct { + // NodeID PATH parameter + // + // Node ID + NodeID uint64 `json:",string"` + + // ModuleID PATH parameter + // + // Module ID + ModuleID uint64 `json:",string"` + } + + SyncStructureRemove struct { + // NodeID PATH parameter + // + // Node ID + NodeID uint64 `json:",string"` + + // ModuleID PATH parameter + // + // Module ID + ModuleID uint64 `json:",string"` + } +) + +// NewSyncStructureReadExposed request +func NewSyncStructureReadExposed() *SyncStructureReadExposed { + return &SyncStructureReadExposed{} +} + +// Auditable returns all auditable/loggable parameters +func (r SyncStructureReadExposed) Auditable() map[string]interface{} { + return map[string]interface{}{ + "nodeID": r.NodeID, + "moduleID": r.ModuleID, + } +} + +// Auditable returns all auditable/loggable parameters +func (r SyncStructureReadExposed) GetNodeID() uint64 { + return r.NodeID +} + +// Auditable returns all auditable/loggable parameters +func (r SyncStructureReadExposed) GetModuleID() uint64 { + return r.ModuleID +} + +// Fill processes request and fills internal variables +func (r *SyncStructureReadExposed) Fill(req *http.Request) (err error) { + if strings.ToLower(req.Header.Get("content-type")) == "application/json" { + err = json.NewDecoder(req.Body).Decode(r) + + switch { + case err == io.EOF: + err = nil + case err != nil: + return fmt.Errorf("error parsing http request body: %w", err) + } + } + + { + var val string + // path params + + val = chi.URLParam(req, "nodeID") + r.NodeID, err = payload.ParseUint64(val), nil + if err != nil { + return err + } + + val = chi.URLParam(req, "moduleID") + r.ModuleID, err = payload.ParseUint64(val), nil + if err != nil { + return err + } + + } + + return err +} + +// NewSyncStructureRemove request +func NewSyncStructureRemove() *SyncStructureRemove { + return &SyncStructureRemove{} +} + +// Auditable returns all auditable/loggable parameters +func (r SyncStructureRemove) Auditable() map[string]interface{} { + return map[string]interface{}{ + "nodeID": r.NodeID, + "moduleID": r.ModuleID, + } +} + +// Auditable returns all auditable/loggable parameters +func (r SyncStructureRemove) GetNodeID() uint64 { + return r.NodeID +} + +// Auditable returns all auditable/loggable parameters +func (r SyncStructureRemove) GetModuleID() uint64 { + return r.ModuleID +} + +// Fill processes request and fills internal variables +func (r *SyncStructureRemove) Fill(req *http.Request) (err error) { + if strings.ToLower(req.Header.Get("content-type")) == "application/json" { + err = json.NewDecoder(req.Body).Decode(r) + + switch { + case err == io.EOF: + err = nil + case err != nil: + return fmt.Errorf("error parsing http request body: %w", err) + } + } + + { + var val string + // path params + + val = chi.URLParam(req, "nodeID") + r.NodeID, err = payload.ParseUint64(val), nil + if err != nil { + return err + } + + val = chi.URLParam(req, "moduleID") + r.ModuleID, err = payload.ParseUint64(val), nil + if err != nil { + return err + } + + } + + return err +} diff --git a/federation/rest/router.go b/federation/rest/router.go index 7bae14ec6..0df61eae0 100644 --- a/federation/rest/router.go +++ b/federation/rest/router.go @@ -10,10 +10,11 @@ import ( func MountRoutes(r chi.Router) { r.Group(func(r chi.Router) { handlers.NewPairRequest(NodePairRequest{}.New()).MountRoutes(r) + + // temporary because of acl + handlers.NewModule((Module{}.New())).MountRoutes(r) + handlers.NewSyncStructure((SyncStructure{}.New())).MountRoutes(r) }) - var ( - module = Module{}.New() - ) // Protect all _private_ routes r.Group(func(r chi.Router) { @@ -22,6 +23,5 @@ func MountRoutes(r chi.Router) { handlers.NewIdentity(NodeIdentity{}.New()).MountRoutes(r) handlers.NewPair(NodePair{}.New()).MountRoutes(r) - handlers.NewModule(module).MountRoutes(r) }) } diff --git a/federation/rest/sync_structure.go b/federation/rest/sync_structure.go new file mode 100644 index 000000000..3972f8556 --- /dev/null +++ b/federation/rest/sync_structure.go @@ -0,0 +1,24 @@ +package rest + +import ( + "context" + + "github.com/cortezaproject/corteza-server/federation/rest/request" + "github.com/cortezaproject/corteza-server/federation/service" +) + +type ( + SyncStructure struct{} +) + +func (SyncStructure) New() *SyncStructure { + return &SyncStructure{} +} + +func (ctrl SyncStructure) Remove(ctx context.Context, r *request.SyncStructureRemove) (interface{}, error) { + return nil, (service.ExposedModule()).DeleteByID(ctx, r.NodeID, r.ModuleID) +} + +func (ctrl SyncStructure) ReadExposed(ctx context.Context, r *request.SyncStructureReadExposed) (interface{}, error) { + return (service.ExposedModule()).FindByID(context.Background(), r.GetNodeID(), r.GetModuleID()) +} diff --git a/federation/service/exposed_module_actions.gen.go b/federation/service/exposed_module_actions.gen.go new file mode 100644 index 000000000..d5cfdbbf5 --- /dev/null +++ b/federation/service/exposed_module_actions.gen.go @@ -0,0 +1,847 @@ +package service + +// This file is auto-generated. +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +// Definitions file that controls how this file is generated: +// federation/service/exposed_module_actions.yaml + +import ( + "context" + "errors" + "fmt" + "strings" + "time" + + "github.com/cortezaproject/corteza-server/federation/types" + "github.com/cortezaproject/corteza-server/pkg/actionlog" +) + +type ( + moduleActionProps struct { + module *types.ExposedModule + changed *types.ExposedModule + filter *types.ExposedModuleFilter + } + + moduleAction struct { + timestamp time.Time + resource string + action string + log string + severity actionlog.Severity + + // prefix for error when action fails + errorMessage string + + props *moduleActionProps + } + + moduleError struct { + timestamp time.Time + error string + resource string + action string + message string + log string + severity actionlog.Severity + + wrap error + + props *moduleActionProps + } +) + +var ( + // just a placeholder to cover template cases w/o fmt package use + _ = fmt.Println +) + +// ********************************************************************************************************************* +// ********************************************************************************************************************* +// Props methods +// setModule updates moduleActionProps's module +// +// Allows method chaining +// +// This function is auto-generated. +// +func (p *moduleActionProps) setModule(module *types.ExposedModule) *moduleActionProps { + p.module = module + return p +} + +// setChanged updates moduleActionProps's changed +// +// Allows method chaining +// +// This function is auto-generated. +// +func (p *moduleActionProps) setChanged(changed *types.ExposedModule) *moduleActionProps { + p.changed = changed + return p +} + +// setFilter updates moduleActionProps's filter +// +// Allows method chaining +// +// This function is auto-generated. +// +func (p *moduleActionProps) setFilter(filter *types.ExposedModuleFilter) *moduleActionProps { + p.filter = filter + return p +} + +// serialize converts moduleActionProps to actionlog.Meta +// +// This function is auto-generated. +// +func (p moduleActionProps) serialize() actionlog.Meta { + var ( + m = make(actionlog.Meta) + ) + + if p.module != nil { + m.Set("module.ID", p.module.ID, true) + m.Set("module.ComposeModuleID", p.module.ComposeModuleID, true) + } + if p.changed != nil { + m.Set("changed.ID", p.changed.ID, true) + m.Set("changed.ComposeModuleID", p.changed.ComposeModuleID, true) + } + if p.filter != nil { + m.Set("filter.query", p.filter.Query, true) + m.Set("filter.sort", p.filter.Sort, true) + m.Set("filter.limit", p.filter.Limit, true) + } + + return m +} + +// tr translates string and replaces meta value placeholder with values +// +// This function is auto-generated. +// +func (p moduleActionProps) tr(in string, err error) string { + var ( + pairs = []string{"{err}"} + // first non-empty string + fns = func(ii ...interface{}) string { + for _, i := range ii { + if s := fmt.Sprintf("%v", i); len(s) > 0 { + return s + } + } + + return "" + } + ) + + if err != nil { + for { + // Unwrap errors + ue := errors.Unwrap(err) + if ue == nil { + break + } + + err = ue + } + + pairs = append(pairs, err.Error()) + } else { + pairs = append(pairs, "nil") + } + + if p.module != nil { + // replacement for "{module}" (in order how fields are defined) + pairs = append( + pairs, + "{module}", + fns( + p.module.ID, + p.module.ComposeModuleID, + ), + ) + pairs = append(pairs, "{module.ID}", fns(p.module.ID)) + pairs = append(pairs, "{module.ComposeModuleID}", fns(p.module.ComposeModuleID)) + } + + if p.changed != nil { + // replacement for "{changed}" (in order how fields are defined) + pairs = append( + pairs, + "{changed}", + fns( + p.changed.ID, + p.changed.ComposeModuleID, + ), + ) + pairs = append(pairs, "{changed.ID}", fns(p.changed.ID)) + pairs = append(pairs, "{changed.ComposeModuleID}", fns(p.changed.ComposeModuleID)) + } + + if p.filter != nil { + // replacement for "{filter}" (in order how fields are defined) + pairs = append( + pairs, + "{filter}", + fns( + p.filter.Query, + p.filter.Sort, + p.filter.Limit, + ), + ) + pairs = append(pairs, "{filter.query}", fns(p.filter.Query)) + pairs = append(pairs, "{filter.sort}", fns(p.filter.Sort)) + pairs = append(pairs, "{filter.limit}", fns(p.filter.Limit)) + } + return strings.NewReplacer(pairs...).Replace(in) +} + +// ********************************************************************************************************************* +// ********************************************************************************************************************* +// Action methods + +// String returns loggable description as string +// +// This function is auto-generated. +// +func (a *moduleAction) String() string { + var props = &moduleActionProps{} + + if a.props != nil { + props = a.props + } + + return props.tr(a.log, nil) +} + +func (e *moduleAction) LoggableAction() *actionlog.Action { + return &actionlog.Action{ + Timestamp: e.timestamp, + Resource: e.resource, + Action: e.action, + Severity: e.severity, + Description: e.String(), + Meta: e.props.serialize(), + } +} + +// ********************************************************************************************************************* +// ********************************************************************************************************************* +// Error methods + +// String returns loggable description as string +// +// It falls back to message if log is not set +// +// This function is auto-generated. +// +func (e *moduleError) String() string { + var props = &moduleActionProps{} + + if e.props != nil { + props = e.props + } + + if e.wrap != nil && !strings.Contains(e.log, "{err}") { + // Suffix error log with {err} to ensure + // we log the cause for this error + e.log += ": {err}" + } + + return props.tr(e.log, e.wrap) +} + +// Error satisfies +// +// This function is auto-generated. +// +func (e *moduleError) Error() string { + var props = &moduleActionProps{} + + if e.props != nil { + props = e.props + } + + return props.tr(e.message, e.wrap) +} + +// Is fn for error equality check +// +// This function is auto-generated. +// +func (e *moduleError) Is(err error) bool { + t, ok := err.(*moduleError) + if !ok { + return false + } + + return t.resource == e.resource && t.error == e.error +} + +// Is fn for error equality check +// +// This function is auto-generated. +// +func (e *moduleError) IsGeneric() bool { + return e.error == "generic" +} + +// Wrap wraps moduleError around another error +// +// This function is auto-generated. +// +func (e *moduleError) Wrap(err error) *moduleError { + e.wrap = err + return e +} + +// Unwrap returns wrapped error +// +// This function is auto-generated. +// +func (e *moduleError) Unwrap() error { + return e.wrap +} + +func (e *moduleError) LoggableAction() *actionlog.Action { + return &actionlog.Action{ + Timestamp: e.timestamp, + Resource: e.resource, + Action: e.action, + Severity: e.severity, + Description: e.String(), + Error: e.Error(), + Meta: e.props.serialize(), + } +} + +// ********************************************************************************************************************* +// ********************************************************************************************************************* +// Action constructors + +// ModuleActionSearch returns "federation:exposed_module.search" error +// +// This function is auto-generated. +// +func ModuleActionSearch(props ...*moduleActionProps) *moduleAction { + a := &moduleAction{ + timestamp: time.Now(), + resource: "federation:exposed_module", + action: "search", + log: "searched for modules", + severity: actionlog.Info, + } + + if len(props) > 0 { + a.props = props[0] + } + + return a +} + +// ModuleActionLookup returns "federation:exposed_module.lookup" error +// +// This function is auto-generated. +// +func ModuleActionLookup(props ...*moduleActionProps) *moduleAction { + a := &moduleAction{ + timestamp: time.Now(), + resource: "federation:exposed_module", + action: "lookup", + log: "looked-up for a {module}", + severity: actionlog.Info, + } + + if len(props) > 0 { + a.props = props[0] + } + + return a +} + +// ModuleActionCreate returns "federation:exposed_module.create" error +// +// This function is auto-generated. +// +func ModuleActionCreate(props ...*moduleActionProps) *moduleAction { + a := &moduleAction{ + timestamp: time.Now(), + resource: "federation:exposed_module", + action: "create", + log: "created {module}", + severity: actionlog.Notice, + } + + if len(props) > 0 { + a.props = props[0] + } + + return a +} + +// ModuleActionUpdate returns "federation:exposed_module.update" error +// +// This function is auto-generated. +// +func ModuleActionUpdate(props ...*moduleActionProps) *moduleAction { + a := &moduleAction{ + timestamp: time.Now(), + resource: "federation:exposed_module", + action: "update", + log: "updated {module}", + severity: actionlog.Notice, + } + + if len(props) > 0 { + a.props = props[0] + } + + return a +} + +// ModuleActionDelete returns "federation:exposed_module.delete" error +// +// This function is auto-generated. +// +func ModuleActionDelete(props ...*moduleActionProps) *moduleAction { + a := &moduleAction{ + timestamp: time.Now(), + resource: "federation:exposed_module", + action: "delete", + log: "deleted {module}", + severity: actionlog.Notice, + } + + if len(props) > 0 { + a.props = props[0] + } + + return a +} + +// ModuleActionUndelete returns "federation:exposed_module.undelete" error +// +// This function is auto-generated. +// +func ModuleActionUndelete(props ...*moduleActionProps) *moduleAction { + a := &moduleAction{ + timestamp: time.Now(), + resource: "federation:exposed_module", + action: "undelete", + log: "undeleted {module}", + severity: actionlog.Notice, + } + + if len(props) > 0 { + a.props = props[0] + } + + return a +} + +// ********************************************************************************************************************* +// ********************************************************************************************************************* +// Error constructors + +// ModuleErrGeneric returns "federation:exposed_module.generic" audit event as actionlog.Error +// +// +// This function is auto-generated. +// +func ModuleErrGeneric(props ...*moduleActionProps) *moduleError { + var e = &moduleError{ + timestamp: time.Now(), + resource: "federation:exposed_module", + error: "generic", + action: "error", + message: "failed to complete request due to internal error", + log: "{err}", + severity: actionlog.Error, + props: func() *moduleActionProps { + if len(props) > 0 { + return props[0] + } + return nil + }(), + } + + if len(props) > 0 { + e.props = props[0] + } + + return e + +} + +// ModuleErrNotFound returns "federation:exposed_module.notFound" audit event as actionlog.Warning +// +// +// This function is auto-generated. +// +func ModuleErrNotFound(props ...*moduleActionProps) *moduleError { + var e = &moduleError{ + timestamp: time.Now(), + resource: "federation:exposed_module", + error: "notFound", + action: "error", + message: "module does not exist", + log: "module does not exist", + severity: actionlog.Warning, + props: func() *moduleActionProps { + if len(props) > 0 { + return props[0] + } + return nil + }(), + } + + if len(props) > 0 { + e.props = props[0] + } + + return e + +} + +// ModuleErrInvalidID returns "federation:exposed_module.invalidID" audit event as actionlog.Warning +// +// +// This function is auto-generated. +// +func ModuleErrInvalidID(props ...*moduleActionProps) *moduleError { + var e = &moduleError{ + timestamp: time.Now(), + resource: "federation:exposed_module", + error: "invalidID", + action: "error", + message: "invalid ID", + log: "invalid ID", + severity: actionlog.Warning, + props: func() *moduleActionProps { + if len(props) > 0 { + return props[0] + } + return nil + }(), + } + + if len(props) > 0 { + e.props = props[0] + } + + return e + +} + +// ModuleErrStaleData returns "federation:exposed_module.staleData" audit event as actionlog.Warning +// +// +// This function is auto-generated. +// +func ModuleErrStaleData(props ...*moduleActionProps) *moduleError { + var e = &moduleError{ + timestamp: time.Now(), + resource: "federation:exposed_module", + error: "staleData", + action: "error", + message: "stale data", + log: "stale data", + severity: actionlog.Warning, + props: func() *moduleActionProps { + if len(props) > 0 { + return props[0] + } + return nil + }(), + } + + if len(props) > 0 { + e.props = props[0] + } + + return e + +} + +// ModuleErrNotAllowedToRead returns "federation:exposed_module.notAllowedToRead" audit event as actionlog.Error +// +// +// This function is auto-generated. +// +func ModuleErrNotAllowedToRead(props ...*moduleActionProps) *moduleError { + var e = &moduleError{ + timestamp: time.Now(), + resource: "federation:exposed_module", + error: "notAllowedToRead", + action: "error", + message: "not allowed to read this module", + log: "could not read {module}; insufficient permissions", + severity: actionlog.Error, + props: func() *moduleActionProps { + if len(props) > 0 { + return props[0] + } + return nil + }(), + } + + if len(props) > 0 { + e.props = props[0] + } + + return e + +} + +// ModuleErrNotAllowedToListModules returns "federation:exposed_module.notAllowedToListModules" audit event as actionlog.Error +// +// +// This function is auto-generated. +// +func ModuleErrNotAllowedToListModules(props ...*moduleActionProps) *moduleError { + var e = &moduleError{ + timestamp: time.Now(), + resource: "federation:exposed_module", + error: "notAllowedToListModules", + action: "error", + message: "not allowed to list modules", + log: "could not list modules; insufficient permissions", + severity: actionlog.Error, + props: func() *moduleActionProps { + if len(props) > 0 { + return props[0] + } + return nil + }(), + } + + if len(props) > 0 { + e.props = props[0] + } + + return e + +} + +// ModuleErrNotAllowedToCreate returns "federation:exposed_module.notAllowedToCreate" audit event as actionlog.Error +// +// +// This function is auto-generated. +// +func ModuleErrNotAllowedToCreate(props ...*moduleActionProps) *moduleError { + var e = &moduleError{ + timestamp: time.Now(), + resource: "federation:exposed_module", + error: "notAllowedToCreate", + action: "error", + message: "not allowed to create modules", + log: "could not create modules; insufficient permissions", + severity: actionlog.Error, + props: func() *moduleActionProps { + if len(props) > 0 { + return props[0] + } + return nil + }(), + } + + if len(props) > 0 { + e.props = props[0] + } + + return e + +} + +// ModuleErrNotAllowedToUpdate returns "federation:exposed_module.notAllowedToUpdate" audit event as actionlog.Error +// +// +// This function is auto-generated. +// +func ModuleErrNotAllowedToUpdate(props ...*moduleActionProps) *moduleError { + var e = &moduleError{ + timestamp: time.Now(), + resource: "federation:exposed_module", + error: "notAllowedToUpdate", + action: "error", + message: "not allowed to update this module", + log: "could not update {module}; insufficient permissions", + severity: actionlog.Error, + props: func() *moduleActionProps { + if len(props) > 0 { + return props[0] + } + return nil + }(), + } + + if len(props) > 0 { + e.props = props[0] + } + + return e + +} + +// ModuleErrNotAllowedToDelete returns "federation:exposed_module.notAllowedToDelete" audit event as actionlog.Error +// +// +// This function is auto-generated. +// +func ModuleErrNotAllowedToDelete(props ...*moduleActionProps) *moduleError { + var e = &moduleError{ + timestamp: time.Now(), + resource: "federation:exposed_module", + error: "notAllowedToDelete", + action: "error", + message: "not allowed to delete this module", + log: "could not delete {module}; insufficient permissions", + severity: actionlog.Error, + props: func() *moduleActionProps { + if len(props) > 0 { + return props[0] + } + return nil + }(), + } + + if len(props) > 0 { + e.props = props[0] + } + + return e + +} + +// ModuleErrNotAllowedToUndelete returns "federation:exposed_module.notAllowedToUndelete" audit event as actionlog.Error +// +// +// This function is auto-generated. +// +func ModuleErrNotAllowedToUndelete(props ...*moduleActionProps) *moduleError { + var e = &moduleError{ + timestamp: time.Now(), + resource: "federation:exposed_module", + error: "notAllowedToUndelete", + action: "error", + message: "not allowed to undelete this module", + log: "could not undelete {module}; insufficient permissions", + severity: actionlog.Error, + props: func() *moduleActionProps { + if len(props) > 0 { + return props[0] + } + return nil + }(), + } + + if len(props) > 0 { + e.props = props[0] + } + + return e + +} + +// ********************************************************************************************************************* +// ********************************************************************************************************************* + +// recordAction is a service helper function wraps function that can return error +// +// context is used to enrich audit log entry with current user info, request ID, IP address... +// props are collected action/error properties +// action (optional) fn will be used to construct moduleAction struct from given props (and error) +// err is any error that occurred while action was happening +// +// Action has success and fail (error) state: +// - when recorded without an error (4th param), action is recorded as successful. +// - when an additional error is given (4th param), action is used to wrap +// the additional error +// +// This function is auto-generated. +// +func (svc module) recordAction(ctx context.Context, props *moduleActionProps, action func(...*moduleActionProps) *moduleAction, err error) error { + var ( + ok bool + + // Return error + retError *moduleError + + // Recorder error + recError *moduleError + ) + + if err != nil { + if retError, ok = err.(*moduleError); !ok { + // got non-module error, wrap it with ModuleErrGeneric + retError = ModuleErrGeneric(props).Wrap(err) + + if action != nil { + // copy action to returning and recording error + retError.action = action().action + } + + // we'll use ModuleErrGeneric for recording too + // because it can hold more info + recError = retError + } else if retError != nil { + if action != nil { + // copy action to returning and recording error + retError.action = action().action + } + // start with copy of return error for recording + // this will be updated with tha root cause as we try and + // unwrap the error + recError = retError + + // find the original recError for this error + // for the purpose of logging + var unwrappedError error = retError + for { + if unwrappedError = errors.Unwrap(unwrappedError); unwrappedError == nil { + // nothing wrapped + break + } + + // update recError ONLY of wrapped error is of type moduleError + if unwrappedSinkError, ok := unwrappedError.(*moduleError); ok { + recError = unwrappedSinkError + } + } + + if retError.props == nil { + // set props on returning error if empty + retError.props = props + } + + if recError.props == nil { + // set props on recording error if empty + recError.props = props + } + } + } + + if svc.actionlog != nil { + if retError != nil { + // failed action, log error + svc.actionlog.Record(ctx, recError) + } else if action != nil { + // successful + svc.actionlog.Record(ctx, action(props)) + } + } + + if err == nil { + // retError not an interface and that WILL (!!) cause issues + // with nil check (== nil) when it is not explicitly returned + return nil + } + + return retError +} diff --git a/federation/service/exposed_module_actions.yaml b/federation/service/exposed_module_actions.yaml new file mode 100644 index 000000000..e37163f6d --- /dev/null +++ b/federation/service/exposed_module_actions.yaml @@ -0,0 +1,82 @@ +# List of loggable service actions + +resource: federation:exposed_module +service: module + +# Default sensitivity for actions +defaultActionSeverity: notice + +# default severity for errors +defaultErrorSeverity: error + +import: + - github.com/cortezaproject/corteza-server/federation/types + +props: + - name: module + type: "*types.ExposedModule" + fields: [ ID, ComposeModuleID ] + - name: changed + type: "*types.ExposedModule" + fields: [ ID, ComposeModuleID ] + - name: filter + type: "*types.ExposedModuleFilter" + fields: [ query, sort, limit ] + +actions: + - action: search + log: "searched for modules" + severity: info + + - action: lookup + log: "looked-up for a {module}" + severity: info + + - action: create + log: "created {module}" + + - action: update + log: "updated {module}" + + - action: delete + log: "deleted {module}" + + - action: undelete + log: "undeleted {module}" + +errors: + - error: notFound + message: "module does not exist" + severity: warning + + - error: invalidID + message: "invalid ID" + severity: warning + + - error: staleData + message: "stale data" + severity: warning + + - error: notAllowedToRead + message: "not allowed to read this module" + log: "could not read {module}; insufficient permissions" + + - error: notAllowedToListModules + message: "not allowed to list modules" + log: "could not list modules; insufficient permissions" + + - error: notAllowedToCreate + message: "not allowed to create modules" + log: "could not create modules; insufficient permissions" + + - error: notAllowedToUpdate + message: "not allowed to update this module" + log: "could not update {module}; insufficient permissions" + + - error: notAllowedToDelete + message: "not allowed to delete this module" + log: "could not delete {module}; insufficient permissions" + + - error: notAllowedToUndelete + message: "not allowed to undelete this module" + log: "could not undelete {module}; insufficient permissions" diff --git a/federation/service/module.go b/federation/service/module.go deleted file mode 100644 index 7e9ab4806..000000000 --- a/federation/service/module.go +++ /dev/null @@ -1,37 +0,0 @@ -package service - -import ( - "context" - - composeService "github.com/cortezaproject/corteza-server/compose/service" - composeTypes "github.com/cortezaproject/corteza-server/compose/types" - "github.com/cortezaproject/corteza-server/federation/types" - "github.com/cortezaproject/corteza-server/store" -) - -type ( - module struct { - ctx context.Context - fetcher composeService.ModuleService - // actionlog actionlog.Recorder - // ac moduleAccessController - store store.Storer - } - - ModuleService interface { - Find(ctx context.Context, filter types.ModuleFilter) (composeTypes.ModuleSet, error) - } -) - -func Module() ModuleService { - return &module{ - ctx: context.Background(), - // ac: DefaultAccessControl, - // eventbus: eventbus.Service(), - store: composeService.DefaultStore, - } -} - -func (svc module) Find(ctx context.Context, filter types.ModuleFilter) (set composeTypes.ModuleSet, err error) { - return composeTypes.ModuleSet{}, nil -} diff --git a/federation/service/sync/structure.go b/federation/service/sync/structure.go deleted file mode 100644 index edc4a48d6..000000000 --- a/federation/service/sync/structure.go +++ /dev/null @@ -1,41 +0,0 @@ -package sync - -import ( - "context" - - composeService "github.com/cortezaproject/corteza-server/compose/service" - composeTypes "github.com/cortezaproject/corteza-server/compose/types" - "github.com/cortezaproject/corteza-server/federation/service" - "github.com/cortezaproject/corteza-server/federation/types" - "github.com/cortezaproject/corteza-server/store" -) - -type ( - module struct { - ctx context.Context - compose composeService.ModuleService - federation service.ModuleService - store store.Storable - } - - ModuleService interface { - FindForNode(ctx context.Context, filter types.ModuleFilter) (composeTypes.ModuleSet, error) - } -) - -func Module() ModuleService { - return &module{ - ctx: context.Background(), - store: composeService.DefaultNgStore, - compose: composeService.Module(), - federation: service.Module(), - } -} - -func (svc module) FindForNode(ctx context.Context, filter types.ModuleFilter) (set composeTypes.ModuleSet, err error) { - // get all modules per-node - // feed the id's into the compose moduleservice - // get the data - // transform (but not here) - return composeTypes.ModuleSet{}, nil -} diff --git a/federation/service/sync_structure.go b/federation/service/sync_structure.go new file mode 100644 index 000000000..5e68c1ac2 --- /dev/null +++ b/federation/service/sync_structure.go @@ -0,0 +1,143 @@ +package service + +import ( + "context" + "strconv" + + composeService "github.com/cortezaproject/corteza-server/compose/service" + "github.com/cortezaproject/corteza-server/federation/types" + "github.com/cortezaproject/corteza-server/pkg/actionlog" + "github.com/cortezaproject/corteza-server/store" + "github.com/davecgh/go-spew/spew" +) + +type ( + module struct { + ctx context.Context + compose composeService.ModuleService + store store.Storer + actionlog actionlog.Recorder + } + + ExposedModuleService interface { + Find(ctx context.Context, filter types.ExposedModuleFilter) (types.ExposedModuleSet, error) + FindByID(ctx context.Context, nodeID uint64, moduleID uint64) (*types.ExposedModule, error) + FindByAny(ctx context.Context, nodeID uint64, identifier interface{}) (*types.ExposedModule, error) + DeleteByID(ctx context.Context, nodeID, moduleID uint64) error + // Remove(ctx context.Context, filter types.ExposedModuleFilter) (err error) + } + + moduleUpdateHandler func(ctx context.Context, ns *types.Node, c *types.ExposedModule) (bool, bool, error) +) + +func ExposedModule() ExposedModuleService { + return &module{ + ctx: context.Background(), + compose: composeService.Module(), + store: DefaultStore, + actionlog: DefaultActionlog, + } +} + +// FindByAny tries to find module in a particular namespace by id, handle or name +func (svc module) FindByAny(ctx context.Context, nodeID uint64, identifier interface{}) (m *types.ExposedModule, err error) { + if ID, ok := identifier.(uint64); ok { + m, err = svc.FindByID(ctx, nodeID, ID) + } else if strIdentifier, ok := identifier.(string); ok { + if ID, _ := strconv.ParseUint(strIdentifier, 10, 64); ID > 0 { + m, err = svc.FindByID(ctx, nodeID, ID) + } + } else { + // force invalid ID error + // we do that to wrap error with lookup action context + _, err = svc.FindByID(ctx, nodeID, 0) + } + + if err != nil { + return nil, err + } + + return m, nil +} + +func (svc module) FindByID(ctx context.Context, nodeID uint64, moduleID uint64) (module *types.ExposedModule, err error) { + err = func() error { + if module, err = store.LookupFederationExposedModuleByID(svc.ctx, svc.store, moduleID); err != nil { + return err + } + + return nil + }() + + return module, err +} + +func (svc module) DeleteByID(ctx context.Context, nodeID, moduleID uint64) error { + return trim1st(svc.updater(ctx, nodeID, moduleID, ModuleActionDelete, svc.handleDelete)) +} + +func (svc module) updater(ctx context.Context, nodeID, moduleID uint64, action func(...*moduleActionProps) *moduleAction, fn moduleUpdateHandler) (*types.ExposedModule, error) { + var ( + moduleChanged, fieldsChanged bool + + n *types.Node + m *types.ExposedModule + // m, old *types.ExposedModule + aProps = &moduleActionProps{module: &types.ExposedModule{ID: moduleID, NodeID: nodeID}} + err error + ) + + spew.Dump("before handle delete", fn, n, m) + + err = store.Tx(ctx, svc.store, func(ctx context.Context, s store.Storer) (err error) { + if m, err = svc.store.LookupFederationExposedModuleByID(ctx, moduleID); err != nil { + return err + } + + // TODO - handle node id also + if moduleChanged, fieldsChanged, err = fn(ctx, n, m); err != nil { + return err + } + + return err + }) + + return m, svc.recordAction(ctx, aProps, action, err) +} + +func (svc module) handleDelete(ctx context.Context, n *types.Node, m *types.ExposedModule) (bool, bool, error) { + if err := store.DeleteFederationExposedModuleByID(ctx, svc.store, m.ID); err != nil { + return false, false, err + } + + return false, false, nil +} + +func (svc module) Find(ctx context.Context, filter types.ExposedModuleFilter) (set types.ExposedModuleSet, err error) { + // get all modules per-node + // feed the id's into the compose moduleservice + // get the data + // transform (but not here) + + // go to store and fetch the id's, first as module id in filter + // then without it + + err = func() error { + if set, _, err = store.SearchFederationExposedModules(svc.ctx, svc.store, filter); err != nil { + return err + } + + return nil + + // return loadModuleFields(svc.ctx, svc.store, set...) + }() + + spew.Dump("ERR", err) + + return set, err +} + +// trim1st removes 1st param and returns only error +func trim1st(_ interface{}, err error) error { + return err +} diff --git a/federation/types/exposed_module.go b/federation/types/exposed_module.go new file mode 100644 index 000000000..637ccfd04 --- /dev/null +++ b/federation/types/exposed_module.go @@ -0,0 +1,30 @@ +package types + +import ( + "time" + + "github.com/cortezaproject/corteza-server/pkg/filter" +) + +type ( + ExposedModule struct { + ID uint64 `json:"moduleID,string"` + NodeID uint64 `json:"nodeID,string"` + ComposeModuleID uint64 `json:"ComposeModuleID,string"` + Fields ModuleFieldMappingList `json:"fields"` + + CreatedAt time.Time `json:"createdAt,omitempty"` + UpdatedAt *time.Time `json:"updatedAt,omitempty"` + DeletedAt *time.Time `json:"deletedAt,omitempty"` + } + + ExposedModuleFilter struct { + NodeID uint64 `json:"node"` + Query string `json:"query"` + + Check func(*ExposedModule) (bool, error) `json:"-"` + + filter.Sorting + filter.Paging + } +) diff --git a/federation/types/module_field_mapping.go b/federation/types/module_field_mapping.go new file mode 100644 index 000000000..b76ea9b37 --- /dev/null +++ b/federation/types/module_field_mapping.go @@ -0,0 +1,36 @@ +package types + +import ( + "database/sql/driver" + "encoding/json" + "errors" + "fmt" +) + +type ( + ModuleFieldMappingList []*ModuleFieldMapping + + ModuleFieldMapping struct { + Kind string `json:"kind"` + Name string `json:"name"` + Label string `json:"label"` + IsMulti bool `json:"isMulti"` + } +) + +func (list ModuleFieldMappingList) Value() (driver.Value, error) { + return json.Marshal(list) +} + +func (list *ModuleFieldMappingList) Scan(value interface{}) error { + switch value.(type) { + case nil: + *list = ModuleFieldMappingList{} + case []uint8: + if err := json.Unmarshal(value.([]byte), list); err != nil { + return errors.New(fmt.Sprintf("Can not scan '%v' into RecordValueSet", value)) + } + } + + return nil +} diff --git a/federation/types/type_set.gen.go b/federation/types/type_set.gen.go index 596601a24..304a38b26 100644 --- a/federation/types/type_set.gen.go +++ b/federation/types/type_set.gen.go @@ -10,12 +10,73 @@ package types type ( + // ExposedModuleSet slice of ExposedModule + // + // This type is auto-generated. + ExposedModuleSet []*ExposedModule + // NodeSet slice of Node // // This type is auto-generated. NodeSet []*Node ) +// Walk iterates through every slice item and calls w(ExposedModule) err +// +// This function is auto-generated. +func (set ExposedModuleSet) Walk(w func(*ExposedModule) error) (err error) { + for i := range set { + if err = w(set[i]); err != nil { + return + } + } + + return +} + +// Filter iterates through every slice item, calls f(ExposedModule) (bool, err) and return filtered slice +// +// This function is auto-generated. +func (set ExposedModuleSet) Filter(f func(*ExposedModule) (bool, error)) (out ExposedModuleSet, err error) { + var ok bool + out = ExposedModuleSet{} + for i := range set { + if ok, err = f(set[i]); err != nil { + return + } else if ok { + out = append(out, set[i]) + } + } + + return +} + +// FindByID finds items from slice by its ID property +// +// This function is auto-generated. +func (set ExposedModuleSet) FindByID(ID uint64) *ExposedModule { + for i := range set { + if set[i].ID == ID { + return set[i] + } + } + + return nil +} + +// IDs returns a slice of uint64s from all items in the set +// +// This function is auto-generated. +func (set ExposedModuleSet) IDs() (IDs []uint64) { + IDs = make([]uint64, len(set)) + + for i := range set { + IDs[i] = set[i].ID + } + + return +} + // Walk iterates through every slice item and calls w(Node) err // // This function is auto-generated. diff --git a/federation/types/type_set.gen_test.go b/federation/types/type_set.gen_test.go index f6d0ea32e..ec58c65af 100644 --- a/federation/types/type_set.gen_test.go +++ b/federation/types/type_set.gen_test.go @@ -14,6 +14,96 @@ import ( "testing" ) +func TestExposedModuleSetWalk(t *testing.T) { + var ( + value = make(ExposedModuleSet, 3) + req = require.New(t) + ) + + // check walk with no errors + { + err := value.Walk(func(*ExposedModule) error { + return nil + }) + req.NoError(err) + } + + // check walk with error + req.Error(value.Walk(func(*ExposedModule) error { return fmt.Errorf("walk error") })) +} + +func TestExposedModuleSetFilter(t *testing.T) { + var ( + value = make(ExposedModuleSet, 3) + req = require.New(t) + ) + + // filter nothing + { + set, err := value.Filter(func(*ExposedModule) (bool, error) { + return true, nil + }) + req.NoError(err) + req.Equal(len(set), len(value)) + } + + // filter one item + { + found := false + set, err := value.Filter(func(*ExposedModule) (bool, error) { + if !found { + found = true + return found, nil + } + return false, nil + }) + req.NoError(err) + req.Len(set, 1) + } + + // filter error + { + _, err := value.Filter(func(*ExposedModule) (bool, error) { + return false, fmt.Errorf("filter error") + }) + req.Error(err) + } +} + +func TestExposedModuleSetIDs(t *testing.T) { + var ( + value = make(ExposedModuleSet, 3) + req = require.New(t) + ) + + // construct objects + value[0] = new(ExposedModule) + value[1] = new(ExposedModule) + value[2] = new(ExposedModule) + // set ids + value[0].ID = 1 + value[1].ID = 2 + value[2].ID = 3 + + // Find existing + { + val := value.FindByID(2) + req.Equal(uint64(2), val.ID) + } + + // Find non-existing + { + val := value.FindByID(4) + req.Nil(val) + } + + // List IDs from set + { + val := value.IDs() + req.Equal(len(val), len(value)) + } +} + func TestNodeSetWalk(t *testing.T) { var ( value = make(NodeSet, 3) diff --git a/federation/types/types.yaml b/federation/types/types.yaml index dfda807d0..e7d58ed86 100644 --- a/federation/types/types.yaml +++ b/federation/types/types.yaml @@ -1,2 +1,3 @@ types: Node: + ExposedModule: diff --git a/store/federation_exposed_modules.gen.go b/store/federation_exposed_modules.gen.go new file mode 100644 index 000000000..cc403ae2e --- /dev/null +++ b/store/federation_exposed_modules.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_exposed_modules.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 ( + FederationExposedModules interface { + SearchFederationExposedModules(ctx context.Context, f types.ExposedModuleFilter) (types.ExposedModuleSet, types.ExposedModuleFilter, error) + LookupFederationExposedModuleByID(ctx context.Context, id uint64) (*types.ExposedModule, error) + + CreateFederationExposedModule(ctx context.Context, rr ...*types.ExposedModule) error + + UpdateFederationExposedModule(ctx context.Context, rr ...*types.ExposedModule) error + + UpsertFederationExposedModule(ctx context.Context, rr ...*types.ExposedModule) error + + DeleteFederationExposedModule(ctx context.Context, rr ...*types.ExposedModule) error + DeleteFederationExposedModuleByID(ctx context.Context, ID uint64) error + + TruncateFederationExposedModules(ctx context.Context) error + } +) + +var _ *types.ExposedModule +var _ context.Context + +// SearchFederationExposedModules returns all matching FederationExposedModules from store +func SearchFederationExposedModules(ctx context.Context, s FederationExposedModules, f types.ExposedModuleFilter) (types.ExposedModuleSet, types.ExposedModuleFilter, error) { + return s.SearchFederationExposedModules(ctx, f) +} + +// LookupFederationExposedModuleByID searches for federation module by ID +// +// It returns federation module +func LookupFederationExposedModuleByID(ctx context.Context, s FederationExposedModules, id uint64) (*types.ExposedModule, error) { + return s.LookupFederationExposedModuleByID(ctx, id) +} + +// CreateFederationExposedModule creates one or more FederationExposedModules in store +func CreateFederationExposedModule(ctx context.Context, s FederationExposedModules, rr ...*types.ExposedModule) error { + return s.CreateFederationExposedModule(ctx, rr...) +} + +// UpdateFederationExposedModule updates one or more (existing) FederationExposedModules in store +func UpdateFederationExposedModule(ctx context.Context, s FederationExposedModules, rr ...*types.ExposedModule) error { + return s.UpdateFederationExposedModule(ctx, rr...) +} + +// UpsertFederationExposedModule creates new or updates existing one or more FederationExposedModules in store +func UpsertFederationExposedModule(ctx context.Context, s FederationExposedModules, rr ...*types.ExposedModule) error { + return s.UpsertFederationExposedModule(ctx, rr...) +} + +// DeleteFederationExposedModule Deletes one or more FederationExposedModules from store +func DeleteFederationExposedModule(ctx context.Context, s FederationExposedModules, rr ...*types.ExposedModule) error { + return s.DeleteFederationExposedModule(ctx, rr...) +} + +// DeleteFederationExposedModuleByID Deletes FederationExposedModule from store +func DeleteFederationExposedModuleByID(ctx context.Context, s FederationExposedModules, ID uint64) error { + return s.DeleteFederationExposedModuleByID(ctx, ID) +} + +// TruncateFederationExposedModules Deletes all FederationExposedModules from store +func TruncateFederationExposedModules(ctx context.Context, s FederationExposedModules) error { + return s.TruncateFederationExposedModules(ctx) +} diff --git a/store/federation_exposed_modules.yaml b/store/federation_exposed_modules.yaml new file mode 100644 index 000000000..da9568d57 --- /dev/null +++ b/store/federation_exposed_modules.yaml @@ -0,0 +1,24 @@ +import: + - github.com/cortezaproject/corteza-server/federation/types + +types: + type: types.ExposedModule + +fields: + - { field: ID } + - { field: NodeID } + - { field: ComposeModuleID } + - { field: Fields, type: "json.Text" } + + +lookups: + - fields: [ID] + description: |- + searches for federation module by ID + + It returns federation module + +rdbms: + alias: cmd + table: federation_module_exposed + customFilterConverter: true diff --git a/store/interfaces.gen.go b/store/interfaces.gen.go index 2180977bf..a2cbe928a 100644 --- a/store/interfaces.gen.go +++ b/store/interfaces.gen.go @@ -16,6 +16,7 @@ package store // - store/compose_record_values.yaml // - store/compose_records.yaml // - store/credentials.yaml +// - store/federation_exposed_modules.yaml // - store/labels.yaml // - store/messaging_attachments.yaml // - store/messaging_channel_members.yaml @@ -51,6 +52,7 @@ type ( ComposeRecordValues ComposeRecords Credentials + FederationExposedModules Labels MessagingAttachments MessagingChannelMembers diff --git a/store/rdbms/federation_exposed_modules.gen.go b/store/rdbms/federation_exposed_modules.gen.go new file mode 100644 index 000000000..08eae4616 --- /dev/null +++ b/store/rdbms/federation_exposed_modules.gen.go @@ -0,0 +1,509 @@ +package rdbms + +// This file is an auto-generated file +// +// Template: pkg/codegen/assets/store_rdbms.gen.go.tpl +// Definitions: store/federation_exposed_modules.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/pkg/filter" + "github.com/cortezaproject/corteza-server/store" +) + +var _ = errors.Is + +// SearchFederationExposedModules returns all matching rows +// +// This function calls convertFederationExposedModuleFilter with the given +// types.ExposedModuleFilter and expects to receive a working squirrel.SelectBuilder +func (s Store) SearchFederationExposedModules(ctx context.Context, f types.ExposedModuleFilter) (types.ExposedModuleSet, types.ExposedModuleFilter, error) { + var ( + err error + set []*types.ExposedModule + q squirrel.SelectBuilder + ) + q, err = s.convertFederationExposedModuleFilter(f) + if err != nil { + return nil, f, err + } + + // Cleanup anything we've accidentally received... + f.PrevPage, f.NextPage = nil, nil + + // When cursor for a previous page is used it's marked as reversed + // This tells us to flip the descending flag on all used sort keys + reversedCursor := f.PageCursor != nil && f.PageCursor.Reverse + + // If paging with reverse cursor, change the sorting + // direction for all columns we're sorting by + curSort := f.Sort.Clone() + if reversedCursor { + curSort.Reverse() + } + + return set, f, s.config.ErrorHandler(func() error { + set, err = s.fetchFullPageOfFederationExposedModules(ctx, q, curSort, f.PageCursor, f.Limit, f.Check) + + if err != nil { + return err + } + + if f.Limit > 0 && len(set) > 0 { + if f.PageCursor != nil && (!f.PageCursor.Reverse || uint(len(set)) == f.Limit) { + f.PrevPage = s.collectFederationExposedModuleCursorValues(set[0], curSort.Columns()...) + f.PrevPage.Reverse = true + } + + // Less items fetched then requested by page-limit + // not very likely there's another page + f.NextPage = s.collectFederationExposedModuleCursorValues(set[len(set)-1], curSort.Columns()...) + } + + f.PageCursor = nil + return nil + }()) +} + +// fetchFullPageOfFederationExposedModules collects all requested results. +// +// Function applies: +// - cursor conditions (where ...) +// - sorting rules (order by ...) +// - limit +// +// Main responsibility of this function is to perform additional sequential queries in case when not enough results +// are collected due to failed check on a specific row (by check fn). Function then moves cursor to the last item fetched +func (s Store) fetchFullPageOfFederationExposedModules( + ctx context.Context, + q squirrel.SelectBuilder, + sort filter.SortExprSet, + cursor *filter.PagingCursor, + limit uint, + check func(*types.ExposedModule) (bool, error), +) ([]*types.ExposedModule, error) { + var ( + set = make([]*types.ExposedModule, 0, DefaultSliceCapacity) + aux []*types.ExposedModule + last *types.ExposedModule + + // When cursor for a previous page is used it's marked as reversed + // This tells us to flip the descending flag on all used sort keys + reversedCursor = cursor != nil && cursor.Reverse + + // copy of the select builder + tryQuery squirrel.SelectBuilder + + fetched uint + err error + ) + + // Make sure we always end our sort by primary keys + if sort.Get("id") == nil { + sort = append(sort, &filter.SortExpr{Column: "id"}) + } + + // Apply sorting expr from filter to query + if q, err = setOrderBy(q, sort, s.sortableFederationExposedModuleColumns()...); err != nil { + return nil, err + } + + for try := 0; try < MaxRefetches; try++ { + tryQuery = setCursorCond(q, cursor) + if limit > 0 { + tryQuery = tryQuery.Limit(uint64(limit)) + } + + if aux, fetched, last, err = s.QueryFederationExposedModules(ctx, tryQuery, check); err != nil { + return nil, err + } + + if limit > 0 && uint(len(aux)) >= limit { + // we should use only as much as requested + set = append(set, aux[0:limit]...) + break + } else { + set = append(set, aux...) + } + + // if limit is not set or we've already collected enough items + // we can break the loop right away + if limit == 0 || fetched == 0 || fetched < limit { + break + } + + // In case limit is set very low and we've missed records in the first fetch, + // make sure next fetch limit is a bit higher + if limit < MinEnsureFetchLimit { + limit = MinEnsureFetchLimit + } + + // @todo improve strategy for collecting next page with lower limit + + // Point cursor to the last fetched element + if cursor = s.collectFederationExposedModuleCursorValues(last, sort.Columns()...); cursor == nil { + break + } + } + + if reversedCursor { + // Cursor for previous page was used + // Fetched set needs to be reverseCursor because we've forced a descending order to + // get the previous page + for i, j := 0, len(set)-1; i < j; i, j = i+1, j-1 { + set[i], set[j] = set[j], set[i] + } + } + + return set, nil +} + +// QueryFederationExposedModules 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) QueryFederationExposedModules( + ctx context.Context, + q squirrel.Sqlizer, + check func(*types.ExposedModule) (bool, error), +) ([]*types.ExposedModule, uint, *types.ExposedModule, error) { + var ( + set = make([]*types.ExposedModule, 0, DefaultSliceCapacity) + res *types.ExposedModule + + // 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.internalFederationExposedModuleRowScanner(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() +} + +// LookupFederationExposedModuleByID searches for federation module by ID +// +// It returns federation module +func (s Store) LookupFederationExposedModuleByID(ctx context.Context, id uint64) (*types.ExposedModule, error) { + return s.execLookupFederationExposedModule(ctx, squirrel.Eq{ + s.preprocessColumn("cmd.id", ""): s.preprocessValue(id, ""), + }) +} + +// CreateFederationExposedModule creates one or more rows in federation_module_exposed table +func (s Store) CreateFederationExposedModule(ctx context.Context, rr ...*types.ExposedModule) (err error) { + for _, res := range rr { + err = s.checkFederationExposedModuleConstraints(ctx, res) + if err != nil { + return err + } + + err = s.execCreateFederationExposedModules(ctx, s.internalFederationExposedModuleEncoder(res)) + if err != nil { + return err + } + } + + return +} + +// UpdateFederationExposedModule updates one or more existing rows in federation_module_exposed +func (s Store) UpdateFederationExposedModule(ctx context.Context, rr ...*types.ExposedModule) error { + return s.config.ErrorHandler(s.partialFederationExposedModuleUpdate(ctx, nil, rr...)) +} + +// partialFederationExposedModuleUpdate updates one or more existing rows in federation_module_exposed +func (s Store) partialFederationExposedModuleUpdate(ctx context.Context, onlyColumns []string, rr ...*types.ExposedModule) (err error) { + for _, res := range rr { + err = s.checkFederationExposedModuleConstraints(ctx, res) + if err != nil { + return err + } + + err = s.execUpdateFederationExposedModules( + ctx, + squirrel.Eq{ + s.preprocessColumn("cmd.id", ""): s.preprocessValue(res.ID, ""), + }, + s.internalFederationExposedModuleEncoder(res).Skip("id").Only(onlyColumns...)) + if err != nil { + return s.config.ErrorHandler(err) + } + } + + return +} + +// UpsertFederationExposedModule updates one or more existing rows in federation_module_exposed +func (s Store) UpsertFederationExposedModule(ctx context.Context, rr ...*types.ExposedModule) (err error) { + for _, res := range rr { + err = s.checkFederationExposedModuleConstraints(ctx, res) + if err != nil { + return err + } + + err = s.config.ErrorHandler(s.execUpsertFederationExposedModules(ctx, s.internalFederationExposedModuleEncoder(res))) + if err != nil { + return err + } + } + + return nil +} + +// DeleteFederationExposedModule Deletes one or more rows from federation_module_exposed table +func (s Store) DeleteFederationExposedModule(ctx context.Context, rr ...*types.ExposedModule) (err error) { + for _, res := range rr { + + err = s.execDeleteFederationExposedModules(ctx, squirrel.Eq{ + s.preprocessColumn("cmd.id", ""): s.preprocessValue(res.ID, ""), + }) + if err != nil { + return s.config.ErrorHandler(err) + } + } + + return nil +} + +// DeleteFederationExposedModuleByID Deletes row from the federation_module_exposed table +func (s Store) DeleteFederationExposedModuleByID(ctx context.Context, ID uint64) error { + return s.execDeleteFederationExposedModules(ctx, squirrel.Eq{ + s.preprocessColumn("cmd.id", ""): s.preprocessValue(ID, ""), + }) +} + +// TruncateFederationExposedModules Deletes all rows from the federation_module_exposed table +func (s Store) TruncateFederationExposedModules(ctx context.Context) error { + return s.config.ErrorHandler(s.Truncate(ctx, s.federationExposedModuleTable())) +} + +// execLookupFederationExposedModule prepares FederationExposedModule query and executes it, +// returning types.ExposedModule (or error) +func (s Store) execLookupFederationExposedModule(ctx context.Context, cnd squirrel.Sqlizer) (res *types.ExposedModule, err error) { + var ( + row rowScanner + ) + + row, err = s.QueryRow(ctx, s.federationExposedModulesSelectBuilder().Where(cnd)) + if err != nil { + return + } + + res, err = s.internalFederationExposedModuleRowScanner(row) + if err != nil { + return + } + + return res, nil +} + +// execCreateFederationExposedModules updates all matched (by cnd) rows in federation_module_exposed with given data +func (s Store) execCreateFederationExposedModules(ctx context.Context, payload store.Payload) error { + return s.config.ErrorHandler(s.Exec(ctx, s.InsertBuilder(s.federationExposedModuleTable()).SetMap(payload))) +} + +// execUpdateFederationExposedModules updates all matched (by cnd) rows in federation_module_exposed with given data +func (s Store) execUpdateFederationExposedModules(ctx context.Context, cnd squirrel.Sqlizer, set store.Payload) error { + return s.config.ErrorHandler(s.Exec(ctx, s.UpdateBuilder(s.federationExposedModuleTable("cmd")).Where(cnd).SetMap(set))) +} + +// execUpsertFederationExposedModules inserts new or updates matching (by-primary-key) rows in federation_module_exposed with given data +func (s Store) execUpsertFederationExposedModules(ctx context.Context, set store.Payload) error { + upsert, err := s.config.UpsertBuilder( + s.config, + s.federationExposedModuleTable(), + set, + "id", + ) + + if err != nil { + return err + } + + return s.config.ErrorHandler(s.Exec(ctx, upsert)) +} + +// execDeleteFederationExposedModules Deletes all matched (by cnd) rows in federation_module_exposed with given data +func (s Store) execDeleteFederationExposedModules(ctx context.Context, cnd squirrel.Sqlizer) error { + return s.config.ErrorHandler(s.Exec(ctx, s.DeleteBuilder(s.federationExposedModuleTable("cmd")).Where(cnd))) +} + +func (s Store) internalFederationExposedModuleRowScanner(row rowScanner) (res *types.ExposedModule, err error) { + res = &types.ExposedModule{} + + if _, has := s.config.RowScanners["federationExposedModule"]; has { + scanner := s.config.RowScanners["federationExposedModule"].(func(_ rowScanner, _ *types.ExposedModule) error) + err = scanner(row, res) + } else { + err = row.Scan( + &res.ID, + &res.NodeID, + &res.ComposeModuleID, + &res.Fields, + ) + } + + if err == sql.ErrNoRows { + return nil, store.ErrNotFound + } + + if err != nil { + return nil, fmt.Errorf("could not scan db row for FederationExposedModule: %w", err) + } else { + return res, nil + } +} + +// QueryFederationExposedModules returns squirrel.SelectBuilder with set table and all columns +func (s Store) federationExposedModulesSelectBuilder() squirrel.SelectBuilder { + return s.SelectBuilder(s.federationExposedModuleTable("cmd"), s.federationExposedModuleColumns("cmd")...) +} + +// federationExposedModuleTable name of the db table +func (Store) federationExposedModuleTable(aa ...string) string { + var alias string + if len(aa) > 0 { + alias = " AS " + aa[0] + } + + return "federation_module_exposed" + alias +} + +// FederationExposedModuleColumns returns all defined table columns +// +// With optional string arg, all columns are returned aliased +func (Store) federationExposedModuleColumns(aa ...string) []string { + var alias string + if len(aa) > 0 { + alias = aa[0] + "." + } + + return []string{ + alias + "id", + alias + "rel_node", + alias + "rel_compose_module", + alias + "fields", + } +} + +// {true true true true true} + +// sortableFederationExposedModuleColumns returns all FederationExposedModule columns flagged as sortable +// +// With optional string arg, all columns are returned aliased +func (Store) sortableFederationExposedModuleColumns() []string { + return []string{ + "id", + } +} + +// internalFederationExposedModuleEncoder encodes fields from types.ExposedModule to store.Payload (map) +// +// Encoding is done by using generic approach or by calling encodeFederationExposedModule +// func when rdbms.customEncoder=true +func (s Store) internalFederationExposedModuleEncoder(res *types.ExposedModule) store.Payload { + return store.Payload{ + "id": res.ID, + "rel_node": res.NodeID, + "rel_compose_module": res.ComposeModuleID, + "fields": res.Fields, + } +} + +// collectFederationExposedModuleCursorValues collects values from the given resource that and sets them to the cursor +// to be used for pagination +// +// Values that are collected must come from sortable, unique or primary columns/fields +// At least one of the collected columns must be flagged as unique, otherwise fn appends primary keys at the end +// +// Known issue: +// when collecting cursor values for query that sorts by unique column with partial index (ie: unique handle on +// undeleted items) +func (s Store) collectFederationExposedModuleCursorValues(res *types.ExposedModule, cc ...string) *filter.PagingCursor { + var ( + cursor = &filter.PagingCursor{} + + hasUnique bool + + // All known primary key columns + + pkId bool + + collect = func(cc ...string) { + for _, c := range cc { + switch c { + case "id": + cursor.Set(c, res.ID, false) + + pkId = true + + } + } + } + ) + + collect(cc...) + if !hasUnique || !(pkId && true) { + collect("id") + } + + return cursor +} + +// checkFederationExposedModuleConstraints 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) checkFederationExposedModuleConstraints(ctx context.Context, res *types.ExposedModule) 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_exposed_modules.go b/store/rdbms/federation_exposed_modules.go new file mode 100644 index 000000000..ddb9a2610 --- /dev/null +++ b/store/rdbms/federation_exposed_modules.go @@ -0,0 +1,26 @@ +package rdbms + +import ( + "github.com/Masterminds/squirrel" + "github.com/cortezaproject/corteza-server/federation/types" +) + +func (s Store) convertFederationExposedModuleFilter(f types.ExposedModuleFilter) (query squirrel.SelectBuilder, err error) { + query = s.federationExposedModulesSelectBuilder() + + // query = filter.StateCondition(query, "cmd.deleted_at", f.Deleted) + + if f.NodeID > 0 { + query = query.Where("cmd.rel_node = ?", f.NodeID) + } + + // if f.Query != "" { + // q := "%" + strings.ToLower(f.Query) + "%" + // query = query.Where(squirrel.Or{ + // squirrel.Like{"LOWER(cmd.name)": q}, + // squirrel.Like{"LOWER(cmd.handle)": q}, + // }) + // } + + return +} diff --git a/store/tests/gen_test.go b/store/tests/gen_test.go index 5c353654e..439ec03e0 100644 --- a/store/tests/gen_test.go +++ b/store/tests/gen_test.go @@ -14,6 +14,7 @@ package tests // - store/compose_namespaces.yaml // - store/compose_pages.yaml // - store/credentials.yaml +// - store/federation_exposed_modules.yaml // - store/labels.yaml // - store/messaging_attachments.yaml // - store/messaging_channel_members.yaml @@ -101,6 +102,11 @@ func testAllGenerated(t *testing.T, s store.Storer) { testCredentials(t, s) }) + // Run generated tests for FederationExposedModules + t.Run("FederationExposedModules", func(t *testing.T) { + testFederationExposedModules(t, s) + }) + // Run generated tests for Labels t.Run("Labels", func(t *testing.T) { testLabels(t, s)