snapshot of https://github.com/cortezaproject/corteza.git (2024.9.x, 2026-09-18; blobs > 131072B omitted)
Some checks failed
SonarQube / SonarQube (push) Failing after 5m28s
E2e test on main branch push / server and client test (map[name:admin port:8080]) (push) Failing after 3m33s
Server and lib tests / Library test (js) (push) Failing after 1m36s
Server and lib tests / Library test (vue) (push) Successful in 2m24s
Server and lib tests / Web client test (admin) (push) Successful in 2m52s
Server and lib tests / Web client test (compose) (push) Successful in 3m23s
Server and lib tests / Web client test (discovery) (push) Successful in 2m49s
Server and lib tests / Web client test (one) (push) Successful in 2m52s
Server and lib tests / Web client test (privacy) (push) Successful in 2m44s
Server and lib tests / Web client test (reporter) (push) Successful in 2m56s
Server and lib tests / Web client test (workflow) (push) Successful in 3m0s
Server and lib tests / Server tests (push) Failing after 4m55s
E2e test on main branch push / notify-failure (push) Failing after 9s
Server and lib tests / Send matrix testing notification (push) Successful in 2s
Mark stale issues and pull requests / stale (push) Successful in 40s
Some checks failed
SonarQube / SonarQube (push) Failing after 5m28s
E2e test on main branch push / server and client test (map[name:admin port:8080]) (push) Failing after 3m33s
Server and lib tests / Library test (js) (push) Failing after 1m36s
Server and lib tests / Library test (vue) (push) Successful in 2m24s
Server and lib tests / Web client test (admin) (push) Successful in 2m52s
Server and lib tests / Web client test (compose) (push) Successful in 3m23s
Server and lib tests / Web client test (discovery) (push) Successful in 2m49s
Server and lib tests / Web client test (one) (push) Successful in 2m52s
Server and lib tests / Web client test (privacy) (push) Successful in 2m44s
Server and lib tests / Web client test (reporter) (push) Successful in 2m56s
Server and lib tests / Web client test (workflow) (push) Successful in 3m0s
Server and lib tests / Server tests (push) Failing after 4m55s
E2e test on main branch push / notify-failure (push) Failing after 9s
Server and lib tests / Send matrix testing notification (push) Successful in 2s
Mark stale issues and pull requests / stale (push) Successful in 40s
This commit is contained in:
137
server/discovery/README.adoc
Normal file
137
server/discovery/README.adoc
Normal file
@@ -0,0 +1,137 @@
|
||||
= Corteza Discovery
|
||||
|
||||
This package provides API endpoints and business logic to serve indexable resources.
|
||||
|
||||
== Access control
|
||||
|
||||
When Discovery capabilities are installed to Corteza server three combinations of clients, roles and users are provisioned:
|
||||
|
||||
- one auth client, role and user for *public* indexing
|
||||
- one auth client, role and user for *protected* indexing
|
||||
- one auth client, role and user for *private* indexing
|
||||
|
||||
This setup enables discovery indexing service to connect to the API with restricted privileges and have access only to explicitly allowed resources.
|
||||
|
||||
=== Additional limitations and protection of auth client
|
||||
|
||||
Each role is configured on each client as forced role and each client as impersonated user.
|
||||
Auth clients are configured with `client_credentials` grant.
|
||||
|
||||
Additionally, auth clients only have `discovery` scope enabled.
|
||||
|
||||
|
||||
=== Use of auth clients on indexer
|
||||
|
||||
Indexer supports configuration for multiple indexes with different access credentials.
|
||||
This allows it to set up different indexing schemas and control how what data is available in an index.
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
== Index field mapping
|
||||
|
||||
=== Static mapping
|
||||
|
||||
API only serves dynamic index mappings for different types of records (per module).
|
||||
Static mappings for resources like users, applications, modules, namespaces are not served from Corteza server API.
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
== Terminology
|
||||
Index access restrictions or `iar`::
|
||||
Can be one of public, private or protected.
|
||||
Some resource-types can not be accessed publicly.
|
||||
|
||||
Index name::
|
||||
Logicaly structured name of the resource index:
|
||||
|
||||
<prefix-for-distinction>-<iar>-<resource-like-suffix>
|
||||
|
||||
corteza-private-system-users
|
||||
corteza-private-compose-records-1-2 (namespaceID: 1, moduleID: 2)
|
||||
|
||||
|
||||
|
||||
|
||||
== REST API Endpoints
|
||||
|
||||
/api/discovery/resources/<iar>::
|
||||
Returns resource types that can be indexable
|
||||
Used when indexer need to crawl over all resources.
|
||||
|
||||
/api/discovery/resources/<iar>/types/<resource-type>::
|
||||
IDs of indexable resources that can be indexed for a certain access restrictions.
|
||||
Used when indexer need to crawl over all resources.
|
||||
|
||||
/api/discovery/resources/<iar>/<resource-type>/<id>::
|
||||
Indexable resource with all details that can be indexed for a certain access restrictions.
|
||||
|
||||
/api/discovery/feed/<iar>?limit=<limit:uint>&cursor=<cursor:uint64>::
|
||||
Feed of indexable resources that were changed from the cursor on.
|
||||
Contains resource type, id and index ID that can be used for cursor
|
||||
|
||||
|
||||
== Store structure
|
||||
|
||||
.Resource updates (`discovery_update_feed`)
|
||||
[cols="1,1,2"]
|
||||
|===
|
||||
| Field | Type |
|
||||
|
||||
| `id`
|
||||
| Uint64
|
||||
| PK, pagination cursor
|
||||
|
||||
| `iar`
|
||||
| Int (bitmask)
|
||||
| Index access restriction: is public, protected and/or private
|
||||
|
||||
| `index_suffix`
|
||||
| String
|
||||
| Resource type
|
||||
|
||||
| `resource_id`
|
||||
| Uint64
|
||||
| Resource identifier
|
||||
|
||||
| `removed`
|
||||
| Boolean
|
||||
| Was resource removed?
|
||||
|
||||
| `created_at`
|
||||
| Timestamp
|
||||
| Data retention control
|
||||
|===
|
||||
|
||||
== Data flow
|
||||
|
||||
We utilize EventBus to handle updates to indexable resources table when resources are created (`afterCreate`), or updated (`afterUpdate`, `afterDelete`).
|
||||
|
||||
|
||||
[NOTE]
|
||||
====
|
||||
Corteza resources that support soft-delete are only removed from public and protected indexes.
|
||||
Private index keeps deleted resources to allow searching through them.
|
||||
====
|
||||
|
||||
|
||||
== Lifecycle
|
||||
|
||||
=== Server start
|
||||
. Register garbage collector that removes
|
||||
. Register garbage collection
|
||||
|
||||
=== Server running
|
||||
|
||||
|
||||
=== Server stop
|
||||
|
||||
81
server/discovery/rest.yaml
Normal file
81
server/discovery/rest.yaml
Normal file
@@ -0,0 +1,81 @@
|
||||
---
|
||||
|
||||
endpoints:
|
||||
- path: "/mappings"
|
||||
entrypoint: mappings
|
||||
title: Index mappings
|
||||
apis:
|
||||
- name: list
|
||||
method: GET
|
||||
title: List of all
|
||||
path: "/"
|
||||
|
||||
|
||||
- path: "/resources"
|
||||
entrypoint: resources
|
||||
title: Resources
|
||||
parameters:
|
||||
get:
|
||||
- { name: limit, type: "uint", title: "Limit" }
|
||||
- { name: pageCursor, type: "string", title: "Page cursor" }
|
||||
apis:
|
||||
- name: systemUsers
|
||||
method: GET
|
||||
title: Resource
|
||||
path: "/system/users"
|
||||
parameters:
|
||||
get:
|
||||
- { name: userID, type: uint64, title: User ID }
|
||||
- { name: deleted, type: uint, title: Exclude (0, default), include (1) or return only (2) deleted users }
|
||||
|
||||
- name: composeNamespaces
|
||||
method: GET
|
||||
title: Resource
|
||||
path: "/compose/namespaces"
|
||||
parameters:
|
||||
get:
|
||||
- { name: namespaceID, type: uint64, title: Namespace ID }
|
||||
- { name: deleted, type: uint, title: Exclude (0, default), include (1) or return only (2) deleted namespaces }
|
||||
|
||||
- name: composeModules
|
||||
method: GET
|
||||
title: Resource
|
||||
path: "/compose/namespaces/{namespaceID}/modules"
|
||||
parameters:
|
||||
path:
|
||||
- { name: namespaceID, type: uint64, title: Namespace ID }
|
||||
get:
|
||||
- { name: moduleID, type: uint64, title: Module ID }
|
||||
- { name: deleted, type: uint, title: Exclude (0, default), include (1) or return only (2) deleted modules }
|
||||
|
||||
- name: composeRecords
|
||||
method: GET
|
||||
title: Resource
|
||||
path: "/compose/namespaces/{namespaceID}/modules/{moduleID}/records"
|
||||
parameters:
|
||||
path:
|
||||
- { name: namespaceID, type: uint64, title: Namespace ID }
|
||||
- { name: moduleID, type: uint64, title: Module ID }
|
||||
get:
|
||||
- { name: recordID, type: uint64, title: Record ID }
|
||||
- { name: deleted, type: uint, title: Exclude (0, default), include (1) or return only (2) deleted records }
|
||||
|
||||
- path: "/feed"
|
||||
entrypoint: feed
|
||||
title: Change feed
|
||||
imports:
|
||||
- time
|
||||
parameters:
|
||||
get:
|
||||
- { name: limit, type: "uint", title: "Limit" }
|
||||
- { name: pageCursor, type: "string", title: "Page cursor" }
|
||||
|
||||
apis:
|
||||
- name: changes
|
||||
method: GET
|
||||
title: List of resource changes that should be indexed
|
||||
path: "/"
|
||||
parameters:
|
||||
get:
|
||||
- { name: from, type: "*time.Time", title: "From timestamp" }
|
||||
- { name: to, type: "*time.Time", title: "To timestamp" }
|
||||
26
server/discovery/rest/feed.go
Normal file
26
server/discovery/rest/feed.go
Normal file
@@ -0,0 +1,26 @@
|
||||
package rest
|
||||
|
||||
import (
|
||||
"context"
|
||||
"github.com/cortezaproject/corteza/server/discovery/rest/internal/feed"
|
||||
"github.com/cortezaproject/corteza/server/discovery/rest/request"
|
||||
"time"
|
||||
)
|
||||
|
||||
type (
|
||||
feeder struct {
|
||||
resActivityLog interface {
|
||||
ResourceActivities(ctx context.Context, limit uint, cur string, from *time.Time, to *time.Time) (rsp *feed.Response, err error)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
func Feed() *feeder {
|
||||
return &feeder{
|
||||
resActivityLog: feed.ResourceActivity(),
|
||||
}
|
||||
}
|
||||
|
||||
func (ctrl feeder) Changes(ctx context.Context, r *request.FeedChanges) (interface{}, error) {
|
||||
return ctrl.resActivityLog.ResourceActivities(ctx, r.Limit, r.PageCursor, r.From, r.To)
|
||||
}
|
||||
57
server/discovery/rest/handlers/feed.go
Normal file
57
server/discovery/rest/handlers/feed.go
Normal file
@@ -0,0 +1,57 @@
|
||||
package handlers
|
||||
|
||||
// This file is auto-generated.
|
||||
//
|
||||
// Changes to this file may cause incorrect behavior and will be lost if
|
||||
// the code is regenerated.
|
||||
//
|
||||
// Definitions file that controls how this file is generated:
|
||||
//
|
||||
|
||||
import (
|
||||
"context"
|
||||
"github.com/cortezaproject/corteza/server/discovery/rest/request"
|
||||
"github.com/cortezaproject/corteza/server/pkg/api"
|
||||
"github.com/go-chi/chi/v5"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
type (
|
||||
// Internal API interface
|
||||
FeedAPI interface {
|
||||
Changes(context.Context, *request.FeedChanges) (interface{}, error)
|
||||
}
|
||||
|
||||
// HTTP API interface
|
||||
Feed struct {
|
||||
Changes func(http.ResponseWriter, *http.Request)
|
||||
}
|
||||
)
|
||||
|
||||
func NewFeed(h FeedAPI) *Feed {
|
||||
return &Feed{
|
||||
Changes: func(w http.ResponseWriter, r *http.Request) {
|
||||
defer r.Body.Close()
|
||||
params := request.NewFeedChanges()
|
||||
if err := params.Fill(r); err != nil {
|
||||
api.Send(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
value, err := h.Changes(r.Context(), params)
|
||||
if err != nil {
|
||||
api.Send(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
api.Send(w, r, value)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (h Feed) MountRoutes(r chi.Router, middlewares ...func(http.Handler) http.Handler) {
|
||||
r.Group(func(r chi.Router) {
|
||||
r.Use(middlewares...)
|
||||
r.Get("/feed/", h.Changes)
|
||||
})
|
||||
}
|
||||
57
server/discovery/rest/handlers/mappings.go
Normal file
57
server/discovery/rest/handlers/mappings.go
Normal file
@@ -0,0 +1,57 @@
|
||||
package handlers
|
||||
|
||||
// This file is auto-generated.
|
||||
//
|
||||
// Changes to this file may cause incorrect behavior and will be lost if
|
||||
// the code is regenerated.
|
||||
//
|
||||
// Definitions file that controls how this file is generated:
|
||||
//
|
||||
|
||||
import (
|
||||
"context"
|
||||
"github.com/cortezaproject/corteza/server/discovery/rest/request"
|
||||
"github.com/cortezaproject/corteza/server/pkg/api"
|
||||
"github.com/go-chi/chi/v5"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
type (
|
||||
// Internal API interface
|
||||
MappingsAPI interface {
|
||||
List(context.Context, *request.MappingsList) (interface{}, error)
|
||||
}
|
||||
|
||||
// HTTP API interface
|
||||
Mappings struct {
|
||||
List func(http.ResponseWriter, *http.Request)
|
||||
}
|
||||
)
|
||||
|
||||
func NewMappings(h MappingsAPI) *Mappings {
|
||||
return &Mappings{
|
||||
List: func(w http.ResponseWriter, r *http.Request) {
|
||||
defer r.Body.Close()
|
||||
params := request.NewMappingsList()
|
||||
if err := params.Fill(r); err != nil {
|
||||
api.Send(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
value, err := h.List(r.Context(), params)
|
||||
if err != nil {
|
||||
api.Send(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
api.Send(w, r, value)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (h Mappings) MountRoutes(r chi.Router, middlewares ...func(http.Handler) http.Handler) {
|
||||
r.Group(func(r chi.Router) {
|
||||
r.Use(middlewares...)
|
||||
r.Get("/mappings/", h.List)
|
||||
})
|
||||
}
|
||||
114
server/discovery/rest/handlers/resources.go
Normal file
114
server/discovery/rest/handlers/resources.go
Normal file
@@ -0,0 +1,114 @@
|
||||
package handlers
|
||||
|
||||
// This file is auto-generated.
|
||||
//
|
||||
// Changes to this file may cause incorrect behavior and will be lost if
|
||||
// the code is regenerated.
|
||||
//
|
||||
// Definitions file that controls how this file is generated:
|
||||
//
|
||||
|
||||
import (
|
||||
"context"
|
||||
"github.com/cortezaproject/corteza/server/discovery/rest/request"
|
||||
"github.com/cortezaproject/corteza/server/pkg/api"
|
||||
"github.com/go-chi/chi/v5"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
type (
|
||||
// Internal API interface
|
||||
ResourcesAPI interface {
|
||||
SystemUsers(context.Context, *request.ResourcesSystemUsers) (interface{}, error)
|
||||
ComposeNamespaces(context.Context, *request.ResourcesComposeNamespaces) (interface{}, error)
|
||||
ComposeModules(context.Context, *request.ResourcesComposeModules) (interface{}, error)
|
||||
ComposeRecords(context.Context, *request.ResourcesComposeRecords) (interface{}, error)
|
||||
}
|
||||
|
||||
// HTTP API interface
|
||||
Resources struct {
|
||||
SystemUsers func(http.ResponseWriter, *http.Request)
|
||||
ComposeNamespaces func(http.ResponseWriter, *http.Request)
|
||||
ComposeModules func(http.ResponseWriter, *http.Request)
|
||||
ComposeRecords func(http.ResponseWriter, *http.Request)
|
||||
}
|
||||
)
|
||||
|
||||
func NewResources(h ResourcesAPI) *Resources {
|
||||
return &Resources{
|
||||
SystemUsers: func(w http.ResponseWriter, r *http.Request) {
|
||||
defer r.Body.Close()
|
||||
params := request.NewResourcesSystemUsers()
|
||||
if err := params.Fill(r); err != nil {
|
||||
api.Send(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
value, err := h.SystemUsers(r.Context(), params)
|
||||
if err != nil {
|
||||
api.Send(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
api.Send(w, r, value)
|
||||
},
|
||||
ComposeNamespaces: func(w http.ResponseWriter, r *http.Request) {
|
||||
defer r.Body.Close()
|
||||
params := request.NewResourcesComposeNamespaces()
|
||||
if err := params.Fill(r); err != nil {
|
||||
api.Send(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
value, err := h.ComposeNamespaces(r.Context(), params)
|
||||
if err != nil {
|
||||
api.Send(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
api.Send(w, r, value)
|
||||
},
|
||||
ComposeModules: func(w http.ResponseWriter, r *http.Request) {
|
||||
defer r.Body.Close()
|
||||
params := request.NewResourcesComposeModules()
|
||||
if err := params.Fill(r); err != nil {
|
||||
api.Send(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
value, err := h.ComposeModules(r.Context(), params)
|
||||
if err != nil {
|
||||
api.Send(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
api.Send(w, r, value)
|
||||
},
|
||||
ComposeRecords: func(w http.ResponseWriter, r *http.Request) {
|
||||
defer r.Body.Close()
|
||||
params := request.NewResourcesComposeRecords()
|
||||
if err := params.Fill(r); err != nil {
|
||||
api.Send(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
value, err := h.ComposeRecords(r.Context(), params)
|
||||
if err != nil {
|
||||
api.Send(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
api.Send(w, r, value)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (h Resources) MountRoutes(r chi.Router, middlewares ...func(http.Handler) http.Handler) {
|
||||
r.Group(func(r chi.Router) {
|
||||
r.Use(middlewares...)
|
||||
r.Get("/resources/system/users", h.SystemUsers)
|
||||
r.Get("/resources/compose/namespaces", h.ComposeNamespaces)
|
||||
r.Get("/resources/compose/namespaces/{namespaceID}/modules", h.ComposeModules)
|
||||
r.Get("/resources/compose/namespaces/{namespaceID}/modules/{moduleID}/records", h.ComposeRecords)
|
||||
})
|
||||
}
|
||||
551
server/discovery/rest/internal/documents/compose.go
Normal file
551
server/discovery/rest/internal/documents/compose.go
Normal file
@@ -0,0 +1,551 @@
|
||||
package documents
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
cmpService "github.com/cortezaproject/corteza/server/compose/service"
|
||||
cmpTypes "github.com/cortezaproject/corteza/server/compose/types"
|
||||
"github.com/cortezaproject/corteza/server/discovery/service"
|
||||
"github.com/cortezaproject/corteza/server/pkg/errors"
|
||||
"github.com/cortezaproject/corteza/server/pkg/filter"
|
||||
"github.com/cortezaproject/corteza/server/pkg/id"
|
||||
"github.com/cortezaproject/corteza/server/pkg/options"
|
||||
"github.com/cortezaproject/corteza/server/pkg/rbac"
|
||||
sysService "github.com/cortezaproject/corteza/server/system/service"
|
||||
sysTypes "github.com/cortezaproject/corteza/server/system/types"
|
||||
)
|
||||
|
||||
type (
|
||||
composeResources struct {
|
||||
opt options.DiscoveryOpt
|
||||
settings *sysTypes.AppSettings
|
||||
|
||||
rbac interface {
|
||||
SignificantRoles(res rbac.Resource, op string) (aRR, dRR []uint64)
|
||||
}
|
||||
|
||||
ac interface {
|
||||
CanReadModule(ctx context.Context, r *cmpTypes.Module) bool
|
||||
CanReadNamespace(ctx context.Context, r *cmpTypes.Namespace) bool
|
||||
CanReadRecord(ctx context.Context, r *cmpTypes.Record) bool
|
||||
CanReadRecordValueOnModuleField(ctx context.Context, r *cmpTypes.ModuleField) bool
|
||||
CanReadChart(ctx context.Context, r *cmpTypes.Chart) bool
|
||||
CanReadPage(ctx context.Context, r *cmpTypes.Page) bool
|
||||
}
|
||||
|
||||
ns interface {
|
||||
FindByID(context.Context, uint64) (*cmpTypes.Namespace, error)
|
||||
Find(context.Context, cmpTypes.NamespaceFilter) (cmpTypes.NamespaceSet, cmpTypes.NamespaceFilter, error)
|
||||
}
|
||||
|
||||
mod interface {
|
||||
FindByID(context.Context, uint64, uint64) (*cmpTypes.Module, error)
|
||||
Find(ctx context.Context, filter cmpTypes.ModuleFilter) (set cmpTypes.ModuleSet, f cmpTypes.ModuleFilter, err error)
|
||||
}
|
||||
|
||||
rec interface {
|
||||
Find(ctx context.Context, filter cmpTypes.RecordFilter) (set cmpTypes.RecordSet, f cmpTypes.RecordFilter, err error)
|
||||
}
|
||||
|
||||
page interface {
|
||||
Find(ctx context.Context, filter cmpTypes.PageFilter) (set cmpTypes.PageSet, f cmpTypes.PageFilter, err error)
|
||||
}
|
||||
|
||||
usr interface {
|
||||
FindByID(ctx context.Context, id uint64) (*sysTypes.User, error)
|
||||
}
|
||||
}
|
||||
|
||||
pageDetail struct {
|
||||
namespaceSlug string
|
||||
moduleID uint64
|
||||
pageID uint64
|
||||
recordID uint64
|
||||
}
|
||||
)
|
||||
|
||||
var (
|
||||
modulePages map[uint64]pageDetail
|
||||
)
|
||||
|
||||
const (
|
||||
maxValueLength = 512
|
||||
)
|
||||
|
||||
func ComposeResources() *composeResources {
|
||||
return &composeResources{
|
||||
opt: service.DefaultOption,
|
||||
settings: sysService.CurrentSettings,
|
||||
rbac: rbac.Global(),
|
||||
ac: cmpService.DefaultAccessControl,
|
||||
ns: cmpService.DefaultNamespace,
|
||||
mod: cmpService.DefaultModule,
|
||||
rec: cmpService.DefaultRecord,
|
||||
page: cmpService.DefaultPage,
|
||||
usr: sysService.DefaultUser,
|
||||
}
|
||||
}
|
||||
|
||||
func (d composeResources) Namespaces(ctx context.Context, limit uint, cur string, namespaceID uint64, deleted uint) (rsp *Response, err error) {
|
||||
return rsp, func() (err error) {
|
||||
if !d.settings.Discovery.ComposeNamespaces.Enabled {
|
||||
return errors.Internal("compose namespace indexing disabled")
|
||||
}
|
||||
|
||||
var (
|
||||
resourceType = "compose:namespace" // @todo use RBAC resourceType
|
||||
ps cmpTypes.PageSet
|
||||
pf cmpTypes.PageFilter
|
||||
nss cmpTypes.NamespaceSet
|
||||
f = cmpTypes.NamespaceFilter{
|
||||
Deleted: filter.State(deleted),
|
||||
}
|
||||
)
|
||||
|
||||
if namespaceID > 0 {
|
||||
f.NamespaceID = append(f.NamespaceID, id.String(namespaceID))
|
||||
}
|
||||
|
||||
if f.Paging, err = filter.NewPaging(limit, cur); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if nss, f, err = d.ns.Find(ctx, f); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
rsp = &Response{
|
||||
Documents: make([]Document, len(nss)),
|
||||
Filter: Filter{
|
||||
Limit: limit,
|
||||
NextPage: f.NextPage,
|
||||
},
|
||||
}
|
||||
|
||||
for i, ns := range nss {
|
||||
nsID := ns.ID
|
||||
nsSlug := ns.Slug
|
||||
rsp.Documents[i].ID = nsID
|
||||
pf.NamespaceID = nsID
|
||||
|
||||
// namespace pages
|
||||
if ps, pf, err = d.page.Find(ctx, pf); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if modulePages == nil {
|
||||
modulePages = make(map[uint64]pageDetail)
|
||||
}
|
||||
for _, p := range ps {
|
||||
moduleID := p.ModuleID
|
||||
if len(nsSlug) > 0 && moduleID > 0 {
|
||||
modulePages[moduleID] = pageDetail{
|
||||
namespaceSlug: nsSlug,
|
||||
moduleID: moduleID,
|
||||
pageID: p.ID,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// where should this link to?
|
||||
// namespace root page on the compose?
|
||||
// rsp.Documents[i].URL = "" // added to source
|
||||
doc := &docComposeNamespace{
|
||||
ResourceType: resourceType,
|
||||
NamespaceID: nsID,
|
||||
Name: ns.Name,
|
||||
Handle: nsSlug,
|
||||
Url: d.getUrlToResource(pageDetail{namespaceSlug: nsSlug}),
|
||||
Meta: docPartialComposeNamespaceMeta{
|
||||
Subtitle: ns.Meta.Subtitle,
|
||||
Description: ns.Meta.Description,
|
||||
},
|
||||
Created: makePartialChange(&ns.CreatedAt),
|
||||
Updated: makePartialChange(ns.UpdatedAt),
|
||||
Deleted: makePartialChange(ns.DeletedAt),
|
||||
|
||||
Namespace: docPartialComposeNamespace{
|
||||
NamespaceID: nsID,
|
||||
Name: ns.Name,
|
||||
Handle: nsSlug,
|
||||
},
|
||||
|
||||
CatchAll: []any{nsID, ns.Name, nsSlug},
|
||||
}
|
||||
|
||||
allowedRoles, deniedRoles := d.rbac.SignificantRoles(ns, "read")
|
||||
|
||||
doc.Security = append(doc.Security, docSecurity{
|
||||
AllowedRoles: stringifyUints64(allowedRoles),
|
||||
DeniedRoles: stringifyUints64(deniedRoles),
|
||||
})
|
||||
|
||||
rsp.Documents[i].Source = doc
|
||||
}
|
||||
|
||||
return nil
|
||||
}()
|
||||
}
|
||||
|
||||
func (d composeResources) Modules(ctx context.Context, namespaceID uint64, limit uint, cur string, moduleID uint64, deleted uint) (rsp *Response, err error) {
|
||||
return rsp, func() (err error) {
|
||||
if !d.settings.Discovery.ComposeModules.Enabled {
|
||||
return errors.Internal("compose module indexing disabled")
|
||||
}
|
||||
|
||||
var (
|
||||
ns *cmpTypes.Namespace
|
||||
mm cmpTypes.ModuleSet
|
||||
f = cmpTypes.ModuleFilter{
|
||||
NamespaceID: namespaceID,
|
||||
Deleted: filter.State(deleted),
|
||||
}
|
||||
)
|
||||
|
||||
if moduleID > 0 {
|
||||
f.ModuleID = append(f.ModuleID, id.String(moduleID))
|
||||
}
|
||||
|
||||
if f.Paging, err = filter.NewPaging(limit, cur); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if mm, f, err = d.mod.Find(ctx, f); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
rsp = &Response{
|
||||
Documents: make([]Document, len(mm)),
|
||||
Filter: Filter{
|
||||
Limit: limit,
|
||||
NextPage: f.NextPage,
|
||||
},
|
||||
}
|
||||
|
||||
if ns, err = d.ns.FindByID(ctx, namespaceID); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
nsPartial := docPartialComposeNamespace{
|
||||
NamespaceID: namespaceID,
|
||||
Name: ns.Name,
|
||||
Handle: ns.Slug,
|
||||
}
|
||||
|
||||
for i, mod := range mm {
|
||||
rsp.Documents[i].ID = mod.ID
|
||||
// Where should this link to?
|
||||
// module edit screen in the administration? does this make sense?
|
||||
//rsp.Documents[i].URL = "@todo"
|
||||
doc := &docComposeModule{
|
||||
ResourceType: "compose:module", // @todo use RBAC resourceType
|
||||
ModuleID: mod.ID,
|
||||
Name: mod.Name,
|
||||
Handle: mod.Handle,
|
||||
Fields: func() []*docPartialComposeModuleField {
|
||||
out := make([]*docPartialComposeModuleField, len(mod.Fields))
|
||||
for i, f := range mod.Fields {
|
||||
out[i] = &docPartialComposeModuleField{
|
||||
Name: f.Name,
|
||||
Label: f.Label,
|
||||
}
|
||||
}
|
||||
return out
|
||||
}(),
|
||||
Labels: func() map[string]string {
|
||||
labels := make(map[string]string, len(mod.Labels))
|
||||
for k, v := range mod.Labels {
|
||||
if v.Val != "" {
|
||||
labels[k] = v.Val
|
||||
} else if len(v.Values) > 0 {
|
||||
labels[k] = strings.Join(v.Values, ",")
|
||||
}
|
||||
}
|
||||
return labels
|
||||
}(),
|
||||
|
||||
Created: makePartialChange(&mod.CreatedAt),
|
||||
Updated: makePartialChange(mod.UpdatedAt),
|
||||
Deleted: makePartialChange(mod.DeletedAt),
|
||||
|
||||
CatchAll: []any{mod.ID, mod.Name, mod.Handle},
|
||||
|
||||
Namespace: nsPartial,
|
||||
Module: docPartialComposeModule{
|
||||
ModuleID: mod.ID,
|
||||
Name: mod.Name,
|
||||
Handle: mod.Handle,
|
||||
},
|
||||
}
|
||||
|
||||
for _, f := range mod.Fields {
|
||||
if f.Name != "" {
|
||||
doc.CatchAll = append(doc.CatchAll, f.Name)
|
||||
}
|
||||
|
||||
if f.Label != "" {
|
||||
doc.CatchAll = append(doc.CatchAll, f.Label)
|
||||
}
|
||||
}
|
||||
|
||||
allowedRoles, deniedRoles := d.rbac.SignificantRoles(mod, "read")
|
||||
|
||||
doc.Security = append(doc.Security, docSecurity{
|
||||
AllowedRoles: stringifyUints64(allowedRoles),
|
||||
DeniedRoles: stringifyUints64(deniedRoles),
|
||||
})
|
||||
|
||||
rsp.Documents[i].Source = doc
|
||||
}
|
||||
|
||||
return nil
|
||||
}()
|
||||
}
|
||||
|
||||
func (d composeResources) Records(ctx context.Context, namespaceID, moduleID uint64, limit uint, cur string, recordID uint64, deleted uint) (rsp *Response, err error) {
|
||||
return rsp, func() (err error) {
|
||||
if !d.settings.Discovery.ComposeRecords.Enabled {
|
||||
return errors.Internal("compose record indexing disabled")
|
||||
}
|
||||
|
||||
var (
|
||||
ns *cmpTypes.Namespace
|
||||
mod *cmpTypes.Module
|
||||
rr cmpTypes.RecordSet
|
||||
f = cmpTypes.RecordFilter{
|
||||
NamespaceID: namespaceID,
|
||||
ModuleID: moduleID,
|
||||
Deleted: filter.State(deleted),
|
||||
}
|
||||
)
|
||||
|
||||
if recordID > 0 {
|
||||
f.Query = fmt.Sprintf("recordID=%d", recordID)
|
||||
}
|
||||
|
||||
if f.Paging, err = filter.NewPaging(limit, cur); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if rr, f, err = d.rec.Find(ctx, f); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
rsp = &Response{
|
||||
Documents: make([]Document, len(rr)),
|
||||
Filter: Filter{
|
||||
Limit: limit,
|
||||
NextPage: f.NextPage,
|
||||
},
|
||||
}
|
||||
|
||||
// @todo handle unreadable (access-control) namespaces
|
||||
if ns, err = d.ns.FindByID(ctx, namespaceID); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
nsPartial := docPartialComposeNamespace{
|
||||
NamespaceID: namespaceID,
|
||||
Name: ns.Name,
|
||||
Handle: ns.Slug,
|
||||
}
|
||||
|
||||
// @todo handle unreadable (access-control) modules
|
||||
if mod, err = d.mod.FindByID(ctx, namespaceID, moduleID); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
modPartial := docPartialComposeModule{
|
||||
ModuleID: f.ModuleID,
|
||||
Name: mod.Name,
|
||||
Handle: mod.Handle,
|
||||
}
|
||||
|
||||
users := make(map[uint64]sysTypes.User)
|
||||
for i, rec := range rr {
|
||||
recordID := rec.ID
|
||||
rsp.Documents[i].ID = recordID
|
||||
// where should this link to? record page in the compose?
|
||||
// rsp.Documents[i].URL = "" // added to source
|
||||
|
||||
// fixme refactor me
|
||||
var (
|
||||
createdBy *sysTypes.User
|
||||
updatedBy *sysTypes.User
|
||||
deletedBy *sysTypes.User
|
||||
)
|
||||
if rec.CreatedBy > 0 {
|
||||
createdBy, users, err = d.getUserInfo(ctx, rec.CreatedBy, users)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if rec.UpdatedBy > 0 {
|
||||
updatedBy, users, err = d.getUserInfo(ctx, rec.UpdatedBy, users)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if rec.DeletedBy > 0 {
|
||||
deletedBy, users, err = d.getUserInfo(ctx, rec.DeletedBy, users)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
// fixme refactor me END
|
||||
|
||||
doc := &docComposeRecord{
|
||||
ResourceType: "compose:record", // @todo use RBAC resourceType
|
||||
RecordID: recordID,
|
||||
Namespace: nsPartial,
|
||||
Module: modPartial,
|
||||
Url: d.getUrlToResource(pageDetail{moduleID: moduleID, recordID: recordID}),
|
||||
Meta: rec.Meta,
|
||||
//Values: d.recordValues(ctx, rec, nil),
|
||||
//Created: makePartialChange(&rec.CreatedAt),
|
||||
//Updated: makePartialChange(rec.UpdatedAt),
|
||||
//Deleted: makePartialChange(rec.DeletedAt),
|
||||
|
||||
Created: makeChange(&rec.CreatedAt, createdBy),
|
||||
Updated: makeChange(rec.UpdatedAt, updatedBy),
|
||||
Deleted: makeChange(rec.DeletedAt, deletedBy),
|
||||
}
|
||||
|
||||
// Values and value labels
|
||||
doc.Values, doc.ValueLabels, doc.CatchAll = d.recordValues(ctx, rec, mod.Fields)
|
||||
|
||||
allowedRoles, deniedRoles := d.rbac.SignificantRoles(rec, "read")
|
||||
|
||||
doc.Security = append(doc.Security, docSecurity{
|
||||
AllowedRoles: stringifyUints64(allowedRoles),
|
||||
DeniedRoles: stringifyUints64(deniedRoles),
|
||||
})
|
||||
|
||||
rsp.Documents[i].Source = doc
|
||||
}
|
||||
|
||||
return nil
|
||||
}()
|
||||
}
|
||||
|
||||
func (d composeResources) recordValues(ctx context.Context, rec *cmpTypes.Record, ff cmpTypes.ModuleFieldSet) (map[string][]interface{}, map[string]string, []any) {
|
||||
var (
|
||||
rval = make(map[string][]interface{})
|
||||
fields = make(map[string]string)
|
||||
catchAll = make([]any, 0, len(rec.Values))
|
||||
)
|
||||
|
||||
if rec.GetModule() == nil {
|
||||
return nil, nil, nil
|
||||
}
|
||||
|
||||
_ = rec.GetModule().Fields.Walk(func(f *cmpTypes.ModuleField) error {
|
||||
if !d.ac.CanReadRecordValueOnModuleField(ctx, f) {
|
||||
return nil
|
||||
}
|
||||
|
||||
var (
|
||||
rv = rec.Values.FilterByName(f.Name)
|
||||
vv = make([]interface{}, 0, len(rv))
|
||||
field = ff.FindByName(f.Name)
|
||||
)
|
||||
|
||||
if len(rv) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
for _, val := range rv {
|
||||
// refs needs to be casted to string (json & unsigned 64-bit integers)!
|
||||
if f.IsRef() {
|
||||
aux := fmt.Sprintf("%d", val.Ref)
|
||||
vv = append(vv, aux)
|
||||
catchAll = append(catchAll, aux)
|
||||
continue
|
||||
}
|
||||
|
||||
if tmp, err := val.Cast(f); err == nil {
|
||||
vv = append(vv, tmp)
|
||||
}
|
||||
|
||||
catchAll = append(catchAll, clampString(val.Value, maxValueLength))
|
||||
}
|
||||
|
||||
if len(vv) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
rval[f.Name] = vv
|
||||
if field != nil {
|
||||
fields[f.Name] = field.Label
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
return rval, fields, catchAll
|
||||
}
|
||||
|
||||
// getUrlToResource construct page url for compose resources
|
||||
func (d composeResources) getUrlToResource(page pageDetail) (url string) {
|
||||
var (
|
||||
host = d.opt.CortezaDomain
|
||||
validNsSlung = len(page.namespaceSlug) > 0
|
||||
validModuleID = page.moduleID > 0
|
||||
validRecord = page.recordID > 0
|
||||
validPageID = page.pageID > 0
|
||||
)
|
||||
|
||||
if len(host) == 0 {
|
||||
return
|
||||
}
|
||||
if validModuleID && validRecord {
|
||||
// For record, take moduleID & recordID from params(page) then construct url from modulePages map
|
||||
if p, is := modulePages[page.moduleID]; is {
|
||||
url = fmt.Sprintf("%s/compose/ns/%s/pages/%d/record/%d", host, p.namespaceSlug, p.pageID, page.recordID)
|
||||
}
|
||||
} else if validNsSlung && validPageID {
|
||||
// for module? @todo, we need moduleID ref for module home page
|
||||
// we can check page.self_id is 0 that is modules home pages, but ref_module is also 0
|
||||
url = fmt.Sprintf("%s/compose/ns/%s/pages/%d", host, page.namespaceSlug, page.pageID)
|
||||
} else if validNsSlung {
|
||||
// For namespace, just send to /pages FE handles fallback to home(default) page
|
||||
url = fmt.Sprintf("%s/compose/ns/%s/pages", host, page.namespaceSlug)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (d composeResources) getUserInfo(ctx context.Context, ID uint64, users map[uint64]sysTypes.User) (*sysTypes.User, map[uint64]sysTypes.User, error) {
|
||||
if users == nil {
|
||||
users = make(map[uint64]sysTypes.User)
|
||||
}
|
||||
user, ok := users[ID]
|
||||
if ok {
|
||||
return &user, users, nil
|
||||
} else {
|
||||
u, err := d.usr.FindByID(ctx, ID)
|
||||
if err != nil && u != nil {
|
||||
users[ID] = *u
|
||||
}
|
||||
return u, users, err
|
||||
}
|
||||
}
|
||||
|
||||
func stringifyUints64(uints []uint64) []string {
|
||||
strings := make([]string, len(uints))
|
||||
for i, num := range uints {
|
||||
strings[i] = strconv.FormatUint(num, 10)
|
||||
}
|
||||
return strings
|
||||
}
|
||||
|
||||
func clampString(input string, maxLength int) string {
|
||||
if len(input) > maxLength {
|
||||
return input[:maxLength]
|
||||
}
|
||||
return input
|
||||
}
|
||||
172
server/discovery/rest/internal/documents/documents.go
Normal file
172
server/discovery/rest/internal/documents/documents.go
Normal file
@@ -0,0 +1,172 @@
|
||||
package documents
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/cortezaproject/corteza/server/pkg/filter"
|
||||
sysTypes "github.com/cortezaproject/corteza/server/system/types"
|
||||
)
|
||||
|
||||
type (
|
||||
Response struct {
|
||||
Filter Filter `json:"filter"`
|
||||
Documents []Document `json:"documents"`
|
||||
}
|
||||
|
||||
Document struct {
|
||||
ID uint64 `json:"id,string"`
|
||||
URL string `json:"url,omitempty"`
|
||||
Source interface{} `json:"source"`
|
||||
}
|
||||
|
||||
Filter struct {
|
||||
Limit uint `json:"limit"`
|
||||
NextPage *filter.PagingCursor `json:"nextPage"`
|
||||
}
|
||||
|
||||
docPartialUser struct {
|
||||
UserID uint64 `json:"userID,string"`
|
||||
Email string `json:"email,omitempty"`
|
||||
Name string `json:"name,omitempty"`
|
||||
Username string `json:"username,omitempty"`
|
||||
Handle string `json:"handle,omitempty"`
|
||||
}
|
||||
|
||||
docPartialChange struct {
|
||||
At *time.Time `json:"at,omitempty"`
|
||||
By *docPartialUser `json:"by,omitempty"`
|
||||
}
|
||||
|
||||
docUser struct {
|
||||
ResourceType string `json:"resourceType"`
|
||||
UserID uint64 `json:"userID,string"`
|
||||
Email string `json:"email"`
|
||||
Name string `json:"name,omitempty"`
|
||||
Handle string `json:"handle,omitempty"`
|
||||
Suspended *time.Time `json:"suspendedAt,omitempty"`
|
||||
Url string `json:"url,omitempty"`
|
||||
CatchAll []any `json:"catch_all"`
|
||||
Updated *docPartialChange `json:"updated,omitempty"`
|
||||
Created *docPartialChange `json:"created,omitempty"`
|
||||
Deleted *docPartialChange `json:"deleted,omitempty"`
|
||||
Security []docSecurity `json:"security"`
|
||||
}
|
||||
|
||||
docComposeNamespace struct {
|
||||
ResourceType string `json:"resourceType"`
|
||||
NamespaceID uint64 `json:"namespaceID,string"`
|
||||
Name string `json:"name,omitempty"`
|
||||
Handle string `json:"handle,omitempty"`
|
||||
Url string `json:"url,omitempty"`
|
||||
Enabled bool `json:"enabled"`
|
||||
Meta docPartialComposeNamespaceMeta `json:"meta"`
|
||||
CatchAll []interface{} `json:"catch_all"`
|
||||
|
||||
Updated *docPartialChange `json:"updated,omitempty"`
|
||||
Created *docPartialChange `json:"created,omitempty"`
|
||||
Deleted *docPartialChange `json:"deleted,omitempty"`
|
||||
Security []docSecurity `json:"security"`
|
||||
|
||||
// Aggregation update
|
||||
Namespace docPartialComposeNamespace `json:"namespace"`
|
||||
}
|
||||
|
||||
docPartialComposeNamespaceMeta struct {
|
||||
Subtitle string `json:"subtitle,omitempty"`
|
||||
Description string `json:"description,omitempty"`
|
||||
}
|
||||
|
||||
docPartialComposeNamespace struct {
|
||||
NamespaceID uint64 `json:"namespaceID,string"`
|
||||
Name string `json:"name,omitempty"`
|
||||
Handle string `json:"handle,omitempty"`
|
||||
}
|
||||
|
||||
docComposeModule struct {
|
||||
ResourceType string `json:"resourceType"`
|
||||
ModuleID uint64 `json:"moduleID,string"`
|
||||
Name string `json:"name,omitempty"`
|
||||
Handle string `json:"handle,omitempty"`
|
||||
Url string `json:"url,omitempty"`
|
||||
Labels map[string]string `json:"labels,omitempty"`
|
||||
Fields []*docPartialComposeModuleField `json:"fields"`
|
||||
Updated *docPartialChange `json:"updated,omitempty"`
|
||||
Created *docPartialChange `json:"created,omitempty"`
|
||||
Deleted *docPartialChange `json:"deleted,omitempty"`
|
||||
Security []docSecurity `json:"security"`
|
||||
|
||||
// Aggregation update
|
||||
Namespace docPartialComposeNamespace `json:"namespace"`
|
||||
Module docPartialComposeModule `json:"module"`
|
||||
|
||||
CatchAll []any `json:"catch_all"`
|
||||
}
|
||||
|
||||
docPartialComposeModuleField struct {
|
||||
Name string `json:"name,omitempty"`
|
||||
Label string `json:"label,omitempty"`
|
||||
}
|
||||
|
||||
docPartialComposeModule struct {
|
||||
ModuleID uint64 `json:"moduleID,string"`
|
||||
Name string `json:"name,omitempty"`
|
||||
Handle string `json:"handle,omitempty"`
|
||||
}
|
||||
|
||||
docComposeRecord struct {
|
||||
ResourceType string `json:"resourceType"`
|
||||
RecordID uint64 `json:"recordID,string"`
|
||||
Url string `json:"url,omitempty"`
|
||||
Meta map[string]any `json:"labels,omitempty"`
|
||||
ValueLabels map[string]string `json:"valueLabels,omitempty"`
|
||||
Values map[string][]interface{} `json:"values"`
|
||||
CatchAll []any `json:"catch_all"`
|
||||
Updated *docPartialChange `json:"updated,omitempty"`
|
||||
Created *docPartialChange `json:"created,omitempty"`
|
||||
Deleted *docPartialChange `json:"deleted,omitempty"`
|
||||
Security []docSecurity `json:"security"`
|
||||
|
||||
// Aggregation update
|
||||
Namespace docPartialComposeNamespace `json:"namespace"`
|
||||
Module docPartialComposeModule `json:"module"`
|
||||
|
||||
// Vectors content field
|
||||
VectorsValue []float32 `json:"vectorsValue,omitempty"`
|
||||
}
|
||||
|
||||
docSecurity struct {
|
||||
// list of roles that are allowed to read the resource
|
||||
AllowedRoles []string `json:"allowedRoles"`
|
||||
|
||||
// list of roles that are disallowed to read the resource
|
||||
DeniedRoles []string `json:"deniedRoles"`
|
||||
}
|
||||
)
|
||||
|
||||
func makePartialChange(at *time.Time) *docPartialChange {
|
||||
if at == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
// @todo handle unreadable (access-control) modules
|
||||
// @todo attach security info (allow/deny roles)
|
||||
return &docPartialChange{At: at}
|
||||
}
|
||||
|
||||
func makeChange(at *time.Time, user *sysTypes.User) (out *docPartialChange) {
|
||||
out = &docPartialChange{}
|
||||
if at != nil {
|
||||
out.At = at
|
||||
}
|
||||
if user != nil {
|
||||
out.By = &docPartialUser{
|
||||
UserID: user.ID,
|
||||
Email: user.Email,
|
||||
Name: user.Name,
|
||||
Username: user.Username,
|
||||
Handle: user.Handle,
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
111
server/discovery/rest/internal/documents/system.go
Normal file
111
server/discovery/rest/internal/documents/system.go
Normal file
@@ -0,0 +1,111 @@
|
||||
package documents
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/cortezaproject/corteza/server/discovery/service"
|
||||
"github.com/cortezaproject/corteza/server/pkg/errors"
|
||||
"github.com/cortezaproject/corteza/server/pkg/id"
|
||||
"github.com/cortezaproject/corteza/server/pkg/options"
|
||||
|
||||
"github.com/cortezaproject/corteza/server/pkg/filter"
|
||||
"github.com/cortezaproject/corteza/server/pkg/rbac"
|
||||
sysService "github.com/cortezaproject/corteza/server/system/service"
|
||||
"github.com/cortezaproject/corteza/server/system/types"
|
||||
)
|
||||
|
||||
type (
|
||||
systemResources struct {
|
||||
opt options.DiscoveryOpt
|
||||
settings *types.AppSettings
|
||||
|
||||
rbac interface {
|
||||
SignificantRoles(res rbac.Resource, op string) (aRR, dRR []uint64)
|
||||
}
|
||||
|
||||
ac interface {
|
||||
CanReadUser(ctx context.Context, r *types.User) bool
|
||||
}
|
||||
usr interface {
|
||||
Find(context.Context, types.UserFilter) (types.UserSet, types.UserFilter, error)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
func SystemResources() *systemResources {
|
||||
return &systemResources{
|
||||
opt: service.DefaultOption,
|
||||
settings: sysService.CurrentSettings,
|
||||
rbac: rbac.Global(),
|
||||
ac: sysService.DefaultAccessControl,
|
||||
usr: sysService.DefaultUser,
|
||||
}
|
||||
}
|
||||
|
||||
func (d systemResources) Users(ctx context.Context, limit uint, cur string, userID uint64, deleted uint) (rsp *Response, err error) {
|
||||
return rsp, func() (err error) {
|
||||
if !d.settings.Discovery.SystemUsers.Enabled {
|
||||
return errors.Internal("system user indexing disabled")
|
||||
}
|
||||
|
||||
var (
|
||||
uu types.UserSet
|
||||
f = types.UserFilter{
|
||||
Deleted: filter.State(deleted),
|
||||
}
|
||||
)
|
||||
|
||||
if userID > 0 {
|
||||
f.UserID = append(f.UserID, id.String(userID))
|
||||
}
|
||||
|
||||
if f.Paging, err = filter.NewPaging(limit, cur); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if uu, f, err = d.usr.Find(ctx, f); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
rsp = &Response{
|
||||
Documents: make([]Document, len(uu)),
|
||||
Filter: Filter{
|
||||
Limit: limit,
|
||||
NextPage: f.NextPage,
|
||||
},
|
||||
}
|
||||
|
||||
for i, u := range uu {
|
||||
doc := &docUser{
|
||||
ResourceType: "system:user",
|
||||
UserID: u.ID,
|
||||
Email: u.Email,
|
||||
Name: u.Name,
|
||||
Handle: u.Handle,
|
||||
Suspended: u.SuspendedAt,
|
||||
|
||||
CatchAll: []any{u.ID, u.Email, u.Name, u.Handle},
|
||||
|
||||
Created: makePartialChange(&u.CreatedAt),
|
||||
Updated: makePartialChange(u.UpdatedAt),
|
||||
Deleted: makePartialChange(u.DeletedAt),
|
||||
}
|
||||
if len(d.opt.CortezaDomain) > 0 && u.ID > 0 {
|
||||
doc.Url = fmt.Sprintf("%s/admin/system/user/edit/%d", d.opt.CortezaDomain, u.ID)
|
||||
}
|
||||
|
||||
allowedRoles, deniedRoles := d.rbac.SignificantRoles(u, "read")
|
||||
|
||||
doc.Security = append(doc.Security, docSecurity{
|
||||
AllowedRoles: stringifyUints64(allowedRoles),
|
||||
DeniedRoles: stringifyUints64(deniedRoles),
|
||||
})
|
||||
|
||||
rsp.Documents[i].ID = u.ID
|
||||
rsp.Documents[i].Source = doc
|
||||
}
|
||||
|
||||
return nil
|
||||
}()
|
||||
}
|
||||
28
server/discovery/rest/internal/feed/feed.go
Normal file
28
server/discovery/rest/internal/feed/feed.go
Normal file
@@ -0,0 +1,28 @@
|
||||
package feed
|
||||
|
||||
import (
|
||||
"github.com/cortezaproject/corteza/server/pkg/filter"
|
||||
"github.com/jmoiron/sqlx/types"
|
||||
"time"
|
||||
)
|
||||
|
||||
type (
|
||||
Response struct {
|
||||
Filter Filter `json:"filter"`
|
||||
ActivityLogs []ActivityLog `json:"activityLogs"`
|
||||
}
|
||||
|
||||
ActivityLog struct {
|
||||
ID uint64 `json:"activityID,string"`
|
||||
ResourceID uint64 `json:"resourceID,string"`
|
||||
ResourceType string `json:"resourceType"`
|
||||
ResourceAction string `json:"resourceAction"`
|
||||
Timestamp time.Time `json:"timestamp"`
|
||||
Meta types.JSONText `json:"meta"`
|
||||
}
|
||||
|
||||
Filter struct {
|
||||
Limit uint `json:"limit"`
|
||||
NextPage *filter.PagingCursor `json:"nextPage"`
|
||||
}
|
||||
)
|
||||
105
server/discovery/rest/internal/feed/resource.go
Normal file
105
server/discovery/rest/internal/feed/resource.go
Normal file
@@ -0,0 +1,105 @@
|
||||
package feed
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/cortezaproject/corteza/server/discovery/service"
|
||||
"github.com/cortezaproject/corteza/server/discovery/types"
|
||||
"github.com/cortezaproject/corteza/server/pkg/errors"
|
||||
"github.com/cortezaproject/corteza/server/pkg/filter"
|
||||
"github.com/cortezaproject/corteza/server/pkg/options"
|
||||
|
||||
"github.com/cortezaproject/corteza/server/pkg/rbac"
|
||||
)
|
||||
|
||||
type (
|
||||
resourceActivity struct {
|
||||
opt options.DiscoveryOpt
|
||||
|
||||
rbac interface {
|
||||
SignificantRoles(res rbac.Resource, op string) (aRR, dRR []uint64)
|
||||
}
|
||||
|
||||
ac interface {
|
||||
}
|
||||
|
||||
resActivity service.ResourceActivityService
|
||||
}
|
||||
)
|
||||
|
||||
func ResourceActivity() *resourceActivity {
|
||||
return &resourceActivity{
|
||||
opt: service.DefaultOption,
|
||||
rbac: rbac.Global(),
|
||||
resActivity: service.DefaultResourceActivity,
|
||||
}
|
||||
}
|
||||
|
||||
func (a resourceActivity) ResourceActivities(ctx context.Context, limit uint, cur string, from *time.Time, to *time.Time) (rsp *Response, err error) {
|
||||
return rsp, func() (err error) {
|
||||
var (
|
||||
aa types.ResourceActivitySet
|
||||
|
||||
f = types.ResourceActivityFilter{}
|
||||
)
|
||||
|
||||
if from == nil || from.IsZero() {
|
||||
return errors.Internal("invalid or missing from timestamp")
|
||||
}
|
||||
|
||||
if to == nil || to.IsZero() {
|
||||
now := time.Now()
|
||||
to = &now
|
||||
}
|
||||
|
||||
if from.After(*to) {
|
||||
return errors.Internal("invalid from timestamp, it must be before to timestamp")
|
||||
}
|
||||
|
||||
// Ensure both 'from' and 'to' are in UTC
|
||||
utcFrom := from.UTC()
|
||||
utcTo := to.UTC()
|
||||
|
||||
from = &utcFrom
|
||||
to = &utcTo
|
||||
|
||||
f.FromTimestamp = from
|
||||
f.ToTimestamp = to
|
||||
|
||||
if f.Paging, err = filter.NewPaging(limit, cur); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if aa, f, err = a.resActivity.Find(ctx, f); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
rsp = &Response{
|
||||
ActivityLogs: make([]ActivityLog, 0),
|
||||
Filter: Filter{
|
||||
Limit: limit,
|
||||
NextPage: f.NextPage,
|
||||
},
|
||||
}
|
||||
|
||||
err = aa.Walk(func(a *types.ResourceActivity) error {
|
||||
rsp.ActivityLogs = append(rsp.ActivityLogs, ActivityLog{
|
||||
ID: a.ID,
|
||||
ResourceID: a.ResourceID,
|
||||
ResourceType: a.ResourceType,
|
||||
ResourceAction: a.ResourceAction,
|
||||
Timestamp: a.Timestamp,
|
||||
Meta: a.Meta,
|
||||
})
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}()
|
||||
}
|
||||
324
server/discovery/rest/internal/mapping/compose.go
Normal file
324
server/discovery/rest/internal/mapping/compose.go
Normal file
@@ -0,0 +1,324 @@
|
||||
package mapping
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/cortezaproject/corteza/server/compose/service"
|
||||
"github.com/cortezaproject/corteza/server/compose/types"
|
||||
"github.com/cortezaproject/corteza/server/pkg/options"
|
||||
)
|
||||
|
||||
type (
|
||||
namespaceSearcher interface {
|
||||
Find(ctx context.Context, filter types.NamespaceFilter) (types.NamespaceSet, types.NamespaceFilter, error)
|
||||
}
|
||||
|
||||
moduleSearcher interface {
|
||||
Find(ctx context.Context, filter types.ModuleFilter) (types.ModuleSet, types.ModuleFilter, error)
|
||||
}
|
||||
|
||||
composeAccessControl interface {
|
||||
CanReadRecordValueOnModuleField(ctx context.Context, r *types.ModuleField) bool
|
||||
CanSearchRecordsOnModule(ctx context.Context, r *types.Module) bool
|
||||
}
|
||||
|
||||
composeMapping struct {
|
||||
ac composeAccessControl
|
||||
ns namespaceSearcher
|
||||
mod moduleSearcher
|
||||
opts options.DiscoveryOpt
|
||||
}
|
||||
)
|
||||
|
||||
func ComposeMapping(discoveryOpts options.DiscoveryOpt) *composeMapping {
|
||||
return &composeMapping{
|
||||
ac: service.DefaultAccessControl,
|
||||
ns: service.DefaultNamespace,
|
||||
mod: service.DefaultModule,
|
||||
opts: discoveryOpts,
|
||||
}
|
||||
}
|
||||
|
||||
func (m composeMapping) Namespaces(ctx context.Context) ([]*Mapping, error) {
|
||||
return []*Mapping{&Mapping{
|
||||
Index: "compose-namespaces",
|
||||
Mapping: map[string]*property{
|
||||
"resourceType": {Type: "keyword"},
|
||||
|
||||
"namespaceID": {Type: "long"},
|
||||
|
||||
"name": {Type: "keyword", Boost: 2},
|
||||
"handle": {Type: "keyword", Boost: 2},
|
||||
"labels": {Type: "keyword"},
|
||||
"enabled": {Type: "boolean"},
|
||||
"url": {Type: "text"},
|
||||
|
||||
"meta": {Type: "object", Properties: map[string]*property{
|
||||
"subtitle": {Type: "keyword"},
|
||||
"description": {Type: "text"},
|
||||
}},
|
||||
|
||||
"created": change(),
|
||||
"updated": change(),
|
||||
"deleted": change(),
|
||||
|
||||
"security": security(),
|
||||
|
||||
"namespace": {
|
||||
Type: "nested",
|
||||
Properties: map[string]*property{
|
||||
"namespaceID": {Type: "long"},
|
||||
"name": {Type: "text"},
|
||||
"handle": {Type: "keyword"},
|
||||
},
|
||||
},
|
||||
|
||||
"catch_all": {
|
||||
Type: "text",
|
||||
Analyzer: "standard",
|
||||
},
|
||||
},
|
||||
}}, nil
|
||||
}
|
||||
|
||||
func (m composeMapping) Modules(ctx context.Context) ([]*Mapping, error) {
|
||||
return []*Mapping{&Mapping{
|
||||
Index: fmt.Sprintf("compose-modules"),
|
||||
Mapping: map[string]*property{
|
||||
"resourceType": {Type: "keyword"},
|
||||
|
||||
"moduleID": {Type: "long"},
|
||||
"created": change(),
|
||||
"updated": change(),
|
||||
"deleted": change(),
|
||||
|
||||
"security": security(),
|
||||
|
||||
"name": {Type: "text", Boost: 2},
|
||||
"handle": {Type: "keyword", Boost: 2},
|
||||
"url": {Type: "text"},
|
||||
"labels": {
|
||||
Type: "object",
|
||||
},
|
||||
"fields": {
|
||||
Type: "object",
|
||||
Properties: map[string]*property{
|
||||
"name": {Type: "keyword"},
|
||||
"label": {Type: "text"},
|
||||
},
|
||||
},
|
||||
|
||||
"namespace": {
|
||||
Type: "nested",
|
||||
Properties: map[string]*property{
|
||||
"namespaceID": {Type: "long"},
|
||||
"name": {Type: "text"},
|
||||
"handle": {Type: "keyword"},
|
||||
},
|
||||
},
|
||||
"module": {
|
||||
Type: "nested",
|
||||
Properties: map[string]*property{
|
||||
"moduleID": {Type: "long"},
|
||||
"name": {Type: "text"},
|
||||
"handle": {Type: "keyword"},
|
||||
},
|
||||
},
|
||||
|
||||
"catch_all": {
|
||||
Type: "text",
|
||||
Analyzer: "standard",
|
||||
},
|
||||
},
|
||||
}}, nil
|
||||
}
|
||||
|
||||
func (m composeMapping) Records(ctx context.Context) ([]*Mapping, error) {
|
||||
var (
|
||||
nn, _, err = m.ns.Find(ctx, types.NamespaceFilter{})
|
||||
out = make([]*Mapping, 0, len(nn)*50)
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return out, nn.Walk(func(ns *types.Namespace) (err error) {
|
||||
// @todo do we index records in this namespace?
|
||||
var (
|
||||
mm types.ModuleSet
|
||||
)
|
||||
|
||||
mm, _, err = m.mod.Find(ctx, types.ModuleFilter{NamespaceID: ns.ID})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// collect record mappings for all modules
|
||||
return mm.Walk(func(mod *types.Module) error {
|
||||
// @todo do we index records of module?
|
||||
|
||||
if m := m.records(ctx, mod, mm); m != nil {
|
||||
out = append(out, m)
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
// Module mapping is always returned, even if it contains no properties
|
||||
//
|
||||
// If there are no properties indexer can assume
|
||||
func (m composeMapping) records(ctx context.Context, mod *types.Module, mm types.ModuleSet) (mapping *Mapping) {
|
||||
// @todo check if module can be Mappinged
|
||||
|
||||
mapping = &Mapping{
|
||||
Index: fmt.Sprintf("compose-records-%d-%d", mod.NamespaceID, mod.ID),
|
||||
Mapping: make(map[string]*property),
|
||||
}
|
||||
|
||||
props := m.moduleFieldsMappingProperties(ctx, mod, mm)
|
||||
if len(props) == 0 {
|
||||
// no mapping
|
||||
return
|
||||
}
|
||||
mapping.Mapping["resourceType"] = &property{Type: "keyword"}
|
||||
|
||||
mapping.Mapping["recordID"] = &property{Type: "long"}
|
||||
mapping.Mapping["url"] = &property{Type: "text"}
|
||||
mapping.Mapping["created"] = change()
|
||||
mapping.Mapping["updated"] = change()
|
||||
mapping.Mapping["deleted"] = change()
|
||||
mapping.Mapping["security"] = security()
|
||||
|
||||
mapping.Mapping["meta"] = &property{
|
||||
Type: "object",
|
||||
}
|
||||
mapping.Mapping["values"] = &property{
|
||||
Type: "nested",
|
||||
Properties: props,
|
||||
}
|
||||
|
||||
mapping.Mapping["catch_all"] = &property{
|
||||
Type: "text",
|
||||
Analyzer: "standard",
|
||||
}
|
||||
|
||||
// mapping for record is there, add fields for namespace & module
|
||||
mapping.Mapping["module"] = &property{
|
||||
Type: "nested",
|
||||
Properties: map[string]*property{
|
||||
"moduleID": {Type: "long"},
|
||||
"name": {Type: "text"},
|
||||
"handle": {Type: "keyword"},
|
||||
},
|
||||
}
|
||||
mapping.Mapping["namespace"] = &property{
|
||||
Type: "nested",
|
||||
Properties: map[string]*property{
|
||||
"namespaceID": {Type: "long"},
|
||||
"name": {Type: "text"},
|
||||
"handle": {Type: "keyword"},
|
||||
},
|
||||
}
|
||||
|
||||
if m.opts.EmbeddingsEnabled {
|
||||
mapping.Mapping["vectorsValue"] = vector(m.opts)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (m composeMapping) moduleFieldsMappingProperties(ctx context.Context, mod *types.Module, mm types.ModuleSet) map[string]*property {
|
||||
mf := make(map[string]*property)
|
||||
|
||||
for _, f := range mod.Fields {
|
||||
for k, m := range m.moduleFieldMappingProperties(ctx, f, mm) {
|
||||
mf[k] = m
|
||||
}
|
||||
}
|
||||
|
||||
return mf
|
||||
}
|
||||
|
||||
func (m composeMapping) moduleFieldMappingProperties(ctx context.Context, f *types.ModuleField, mm types.ModuleSet) map[string]*property {
|
||||
if !m.ac.CanReadRecordValueOnModuleField(ctx, f) {
|
||||
return nil
|
||||
}
|
||||
|
||||
var (
|
||||
p = &property{Type: "text"}
|
||||
name = f.Name
|
||||
)
|
||||
// @todo get indexing props from f.Options!
|
||||
|
||||
if p.Type == "" {
|
||||
// Type not explicitly set, get it by converting
|
||||
switch strings.ToLower(f.Kind) {
|
||||
case "bool":
|
||||
p.Type = "boolean"
|
||||
|
||||
case "datetime":
|
||||
p.Type = "date"
|
||||
// https://www.elastic.co/guide/en/elasticsearch/reference/current/date.html
|
||||
// @todo format param!
|
||||
|
||||
case "email":
|
||||
p.Type = "keyword"
|
||||
|
||||
case "file":
|
||||
// @todo ref
|
||||
|
||||
case "number":
|
||||
// @todo what number type to use?
|
||||
return nil
|
||||
|
||||
case "record":
|
||||
//iopt := f.Options.Index()
|
||||
|
||||
// @todo make sure we do not fall into recursive loop
|
||||
|
||||
// if ifc.parents[]
|
||||
|
||||
// @todo if single-value then object
|
||||
// https://www.elastic.co/guide/en/elasticsearch/reference/current/object.html
|
||||
// @todo if multi-value then nested
|
||||
// https://www.elastic.co/guide/en/elasticsearch/reference/current/nested.html
|
||||
|
||||
//var deepDive = 1
|
||||
//
|
||||
//if deepDive == 0 {
|
||||
// // no
|
||||
// return nil
|
||||
//}
|
||||
//
|
||||
//m := mm.FindByID(f.Options.Uint64("moduleID"))
|
||||
//if m == nil {
|
||||
// return nil
|
||||
//}
|
||||
//
|
||||
//if opt.nestingLevel < 2 {
|
||||
// return append(composeModuleFieldsMappingProperties(m, mm, Context{prefix: f.Name, nestingLevel: opt.nestingLevel + 1}), m)
|
||||
//}
|
||||
return nil
|
||||
|
||||
case "select":
|
||||
p.Type = "keyword"
|
||||
|
||||
case "string":
|
||||
p.Type = "text"
|
||||
|
||||
case "url":
|
||||
p.Type = "keyword"
|
||||
|
||||
case "user":
|
||||
// @todo ref
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
return map[string]*property{name: p}
|
||||
}
|
||||
70
server/discovery/rest/internal/mapping/mapping.go
Normal file
70
server/discovery/rest/internal/mapping/mapping.go
Normal file
@@ -0,0 +1,70 @@
|
||||
package mapping
|
||||
|
||||
import "github.com/cortezaproject/corteza/server/pkg/options"
|
||||
|
||||
type (
|
||||
Mapping struct {
|
||||
Index string `json:"index"`
|
||||
Mapping map[string]*property `json:"mapping"`
|
||||
}
|
||||
|
||||
property struct {
|
||||
// https://www.elastic.co/guide/en/elasticsearch/reference/current/mapping-types.html
|
||||
Type string `json:"type,omitempty"`
|
||||
|
||||
// Boost factor
|
||||
// https://www.elastic.co/guide/en/elasticsearch/reference/current/mapping-boost.html
|
||||
Boost float32 `json:"boost,omitempty"`
|
||||
|
||||
Analyzer string `json:"analyzer,omitempty"`
|
||||
|
||||
Properties map[string]*property `json:"properties,omitempty"`
|
||||
|
||||
// for vector indexing
|
||||
Dimension int `json:"dimension,omitempty"`
|
||||
Method map[string]any `json:"method,omitempty"`
|
||||
}
|
||||
|
||||
Context struct {
|
||||
AccessRestriction string
|
||||
}
|
||||
)
|
||||
|
||||
func change() *property {
|
||||
return &property{
|
||||
Type: "nested",
|
||||
Properties: map[string]*property{
|
||||
"at": {Type: "date"},
|
||||
"by": {Type: "nested", Properties: map[string]*property{
|
||||
"id": {Type: "long"},
|
||||
"email": {Type: "keyword"},
|
||||
"name": {Type: "keyword"},
|
||||
"handle": {Type: "keyword"},
|
||||
}},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func security() *property {
|
||||
return &property{
|
||||
Properties: map[string]*property{
|
||||
"allowedRoles": {Type: "long"},
|
||||
"deniedRoles": {Type: "long"},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func vector(opts options.DiscoveryOpt) *property {
|
||||
return &property{
|
||||
Type: "knn_vector",
|
||||
Dimension: opts.EmbeddingsDimension,
|
||||
Method: map[string]any{
|
||||
"name": "hnsw",
|
||||
"engine": "lucene",
|
||||
"parameters": map[string]any{
|
||||
"ef_construction": opts.HnswEfConstruction,
|
||||
"m": opts.HnswM,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
54
server/discovery/rest/internal/mapping/system.go
Normal file
54
server/discovery/rest/internal/mapping/system.go
Normal file
@@ -0,0 +1,54 @@
|
||||
package mapping
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/cortezaproject/corteza/server/system/service"
|
||||
)
|
||||
|
||||
type (
|
||||
systemAccessControl interface {
|
||||
CanSearchUsers(ctx context.Context) bool
|
||||
CanSearchRoles(ctx context.Context) bool
|
||||
CanSearchApplications(ctx context.Context) bool
|
||||
CanSearchAuthClients(ctx context.Context) bool
|
||||
}
|
||||
|
||||
systemMapping struct {
|
||||
ac systemAccessControl
|
||||
}
|
||||
)
|
||||
|
||||
func SystemMapping() *systemMapping {
|
||||
return &systemMapping{
|
||||
ac: service.DefaultAccessControl,
|
||||
}
|
||||
}
|
||||
|
||||
func (svc systemMapping) Users(_ context.Context) ([]*Mapping, error) {
|
||||
return []*Mapping{{
|
||||
Index: "system-users",
|
||||
Mapping: map[string]*property{
|
||||
"resourceType": {Type: "keyword"},
|
||||
|
||||
"userID": {Type: "long"},
|
||||
|
||||
"email": {Type: "keyword", Boost: 2},
|
||||
"name": {Type: "keyword", Boost: 10},
|
||||
"handle": {Type: "keyword", Boost: 2},
|
||||
|
||||
"created": change(),
|
||||
"updated": change(),
|
||||
"deleted": change(),
|
||||
|
||||
"suspendedAt": {Type: "date"},
|
||||
|
||||
"security": security(),
|
||||
|
||||
"catch_all": {
|
||||
Type: "text",
|
||||
Analyzer: "standard",
|
||||
},
|
||||
},
|
||||
}}, nil
|
||||
}
|
||||
75
server/discovery/rest/mappings.go
Normal file
75
server/discovery/rest/mappings.go
Normal file
@@ -0,0 +1,75 @@
|
||||
package rest
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/cortezaproject/corteza/server/discovery/rest/internal/mapping"
|
||||
"github.com/cortezaproject/corteza/server/discovery/rest/request"
|
||||
"github.com/cortezaproject/corteza/server/pkg/options"
|
||||
"github.com/cortezaproject/corteza/server/system/service"
|
||||
"github.com/cortezaproject/corteza/server/system/types"
|
||||
)
|
||||
|
||||
type (
|
||||
mappings struct {
|
||||
settings *types.AppSettings
|
||||
|
||||
sys interface {
|
||||
Users(context.Context) ([]*mapping.Mapping, error)
|
||||
}
|
||||
|
||||
cmp interface {
|
||||
Namespaces(context.Context) ([]*mapping.Mapping, error)
|
||||
Modules(context.Context) ([]*mapping.Mapping, error)
|
||||
Records(context.Context) ([]*mapping.Mapping, error)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
func Mappings(discoveryOpts options.DiscoveryOpt) *mappings {
|
||||
return &mappings{
|
||||
settings: service.CurrentSettings,
|
||||
sys: mapping.SystemMapping(),
|
||||
cmp: mapping.ComposeMapping(discoveryOpts),
|
||||
}
|
||||
}
|
||||
|
||||
func (ctrl mappings) List(ctx context.Context, r *request.MappingsList) (interface{}, error) {
|
||||
var (
|
||||
out = make([]*mapping.Mapping, 0, 100)
|
||||
// Collection of all mapping functions we support
|
||||
//
|
||||
// Each function is responsible to
|
||||
mappingFn = make([]func(ctx context.Context) ([]*mapping.Mapping, error), 4)
|
||||
)
|
||||
|
||||
if ctrl.settings.Discovery.SystemUsers.Enabled {
|
||||
mappingFn = append(mappingFn, ctrl.sys.Users)
|
||||
}
|
||||
if ctrl.settings.Discovery.ComposeNamespaces.Enabled {
|
||||
mappingFn = append(mappingFn, ctrl.cmp.Namespaces)
|
||||
}
|
||||
if ctrl.settings.Discovery.ComposeModules.Enabled {
|
||||
mappingFn = append(mappingFn, ctrl.cmp.Modules)
|
||||
}
|
||||
if ctrl.settings.Discovery.ComposeRecords.Enabled {
|
||||
mappingFn = append(mappingFn, ctrl.cmp.Records)
|
||||
}
|
||||
|
||||
return out, func() error {
|
||||
for _, fn := range mappingFn {
|
||||
if fn == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
mm, err := fn(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
out = append(out, mm...)
|
||||
}
|
||||
|
||||
return nil
|
||||
}()
|
||||
}
|
||||
129
server/discovery/rest/request/feed.go
Normal file
129
server/discovery/rest/request/feed.go
Normal file
@@ -0,0 +1,129 @@
|
||||
package request
|
||||
|
||||
// This file is auto-generated.
|
||||
//
|
||||
// Changes to this file may cause incorrect behavior and will be lost if
|
||||
// the code is regenerated.
|
||||
//
|
||||
// Definitions file that controls how this file is generated:
|
||||
//
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/cortezaproject/corteza/server/pkg/payload"
|
||||
"github.com/go-chi/chi/v5"
|
||||
"io"
|
||||
"mime/multipart"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// dummy vars to prevent
|
||||
// unused imports complain
|
||||
var (
|
||||
_ = chi.URLParam
|
||||
_ = multipart.ErrMessageTooLarge
|
||||
_ = payload.ParseUint64s
|
||||
_ = strings.ToLower
|
||||
_ = io.EOF
|
||||
_ = fmt.Errorf
|
||||
_ = json.NewEncoder
|
||||
)
|
||||
|
||||
type (
|
||||
// Internal API interface
|
||||
FeedChanges struct {
|
||||
// Limit GET parameter
|
||||
//
|
||||
// Limit
|
||||
Limit uint
|
||||
|
||||
// PageCursor GET parameter
|
||||
//
|
||||
// Page cursor
|
||||
PageCursor string
|
||||
|
||||
// From GET parameter
|
||||
//
|
||||
// From timestamp
|
||||
From *time.Time
|
||||
|
||||
// To GET parameter
|
||||
//
|
||||
// To timestamp
|
||||
To *time.Time
|
||||
}
|
||||
)
|
||||
|
||||
// NewFeedChanges request
|
||||
func NewFeedChanges() *FeedChanges {
|
||||
return &FeedChanges{}
|
||||
}
|
||||
|
||||
// Auditable returns all auditable/loggable parameters
|
||||
func (r FeedChanges) Auditable() map[string]interface{} {
|
||||
return map[string]interface{}{
|
||||
"limit": r.Limit,
|
||||
"pageCursor": r.PageCursor,
|
||||
"from": r.From,
|
||||
"to": r.To,
|
||||
}
|
||||
}
|
||||
|
||||
// Auditable returns all auditable/loggable parameters
|
||||
func (r FeedChanges) GetLimit() uint {
|
||||
return r.Limit
|
||||
}
|
||||
|
||||
// Auditable returns all auditable/loggable parameters
|
||||
func (r FeedChanges) GetPageCursor() string {
|
||||
return r.PageCursor
|
||||
}
|
||||
|
||||
// Auditable returns all auditable/loggable parameters
|
||||
func (r FeedChanges) GetFrom() *time.Time {
|
||||
return r.From
|
||||
}
|
||||
|
||||
// Auditable returns all auditable/loggable parameters
|
||||
func (r FeedChanges) GetTo() *time.Time {
|
||||
return r.To
|
||||
}
|
||||
|
||||
// Fill processes request and fills internal variables
|
||||
func (r *FeedChanges) Fill(req *http.Request) (err error) {
|
||||
|
||||
{
|
||||
// GET params
|
||||
tmp := req.URL.Query()
|
||||
|
||||
if val, ok := tmp["limit"]; ok && len(val) > 0 {
|
||||
r.Limit, err = payload.ParseUint(val[0]), nil
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if val, ok := tmp["pageCursor"]; ok && len(val) > 0 {
|
||||
r.PageCursor, err = val[0], nil
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if val, ok := tmp["from"]; ok && len(val) > 0 {
|
||||
r.From, err = payload.ParseISODatePtrWithErr(val[0])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if val, ok := tmp["to"]; ok && len(val) > 0 {
|
||||
r.To, err = payload.ParseISODatePtrWithErr(val[0])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
54
server/discovery/rest/request/mappings.go
Normal file
54
server/discovery/rest/request/mappings.go
Normal file
@@ -0,0 +1,54 @@
|
||||
package request
|
||||
|
||||
// This file is auto-generated.
|
||||
//
|
||||
// Changes to this file may cause incorrect behavior and will be lost if
|
||||
// the code is regenerated.
|
||||
//
|
||||
// Definitions file that controls how this file is generated:
|
||||
//
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/cortezaproject/corteza/server/pkg/payload"
|
||||
"github.com/go-chi/chi/v5"
|
||||
"io"
|
||||
"mime/multipart"
|
||||
"net/http"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// dummy vars to prevent
|
||||
// unused imports complain
|
||||
var (
|
||||
_ = chi.URLParam
|
||||
_ = multipart.ErrMessageTooLarge
|
||||
_ = payload.ParseUint64s
|
||||
_ = strings.ToLower
|
||||
_ = io.EOF
|
||||
_ = fmt.Errorf
|
||||
_ = json.NewEncoder
|
||||
)
|
||||
|
||||
type (
|
||||
// Internal API interface
|
||||
MappingsList struct {
|
||||
}
|
||||
)
|
||||
|
||||
// NewMappingsList request
|
||||
func NewMappingsList() *MappingsList {
|
||||
return &MappingsList{}
|
||||
}
|
||||
|
||||
// Auditable returns all auditable/loggable parameters
|
||||
func (r MappingsList) Auditable() map[string]interface{} {
|
||||
return map[string]interface{}{}
|
||||
}
|
||||
|
||||
// Fill processes request and fills internal variables
|
||||
func (r *MappingsList) Fill(req *http.Request) (err error) {
|
||||
|
||||
return err
|
||||
}
|
||||
470
server/discovery/rest/request/resources.go
Normal file
470
server/discovery/rest/request/resources.go
Normal file
@@ -0,0 +1,470 @@
|
||||
package request
|
||||
|
||||
// This file is auto-generated.
|
||||
//
|
||||
// Changes to this file may cause incorrect behavior and will be lost if
|
||||
// the code is regenerated.
|
||||
//
|
||||
// Definitions file that controls how this file is generated:
|
||||
//
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/cortezaproject/corteza/server/pkg/payload"
|
||||
"github.com/go-chi/chi/v5"
|
||||
"io"
|
||||
"mime/multipart"
|
||||
"net/http"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// dummy vars to prevent
|
||||
// unused imports complain
|
||||
var (
|
||||
_ = chi.URLParam
|
||||
_ = multipart.ErrMessageTooLarge
|
||||
_ = payload.ParseUint64s
|
||||
_ = strings.ToLower
|
||||
_ = io.EOF
|
||||
_ = fmt.Errorf
|
||||
_ = json.NewEncoder
|
||||
)
|
||||
|
||||
type (
|
||||
// Internal API interface
|
||||
ResourcesSystemUsers struct {
|
||||
// Limit GET parameter
|
||||
//
|
||||
// Limit
|
||||
Limit uint
|
||||
|
||||
// PageCursor GET parameter
|
||||
//
|
||||
// Page cursor
|
||||
PageCursor string
|
||||
|
||||
// UserID GET parameter
|
||||
//
|
||||
// User ID
|
||||
UserID uint64 `json:",string"`
|
||||
|
||||
// Deleted GET parameter
|
||||
//
|
||||
// Exclude (0
|
||||
Deleted uint
|
||||
}
|
||||
|
||||
ResourcesComposeNamespaces struct {
|
||||
// Limit GET parameter
|
||||
//
|
||||
// Limit
|
||||
Limit uint
|
||||
|
||||
// PageCursor GET parameter
|
||||
//
|
||||
// Page cursor
|
||||
PageCursor string
|
||||
|
||||
// NamespaceID GET parameter
|
||||
//
|
||||
// Namespace ID
|
||||
NamespaceID uint64 `json:",string"`
|
||||
|
||||
// Deleted GET parameter
|
||||
//
|
||||
// Exclude (0
|
||||
Deleted uint
|
||||
}
|
||||
|
||||
ResourcesComposeModules struct {
|
||||
// NamespaceID PATH parameter
|
||||
//
|
||||
// Namespace ID
|
||||
NamespaceID uint64 `json:",string"`
|
||||
|
||||
// Limit GET parameter
|
||||
//
|
||||
// Limit
|
||||
Limit uint
|
||||
|
||||
// PageCursor GET parameter
|
||||
//
|
||||
// Page cursor
|
||||
PageCursor string
|
||||
|
||||
// ModuleID GET parameter
|
||||
//
|
||||
// Module ID
|
||||
ModuleID uint64 `json:",string"`
|
||||
|
||||
// Deleted GET parameter
|
||||
//
|
||||
// Exclude (0
|
||||
Deleted uint
|
||||
}
|
||||
|
||||
ResourcesComposeRecords struct {
|
||||
// NamespaceID PATH parameter
|
||||
//
|
||||
// Namespace ID
|
||||
NamespaceID uint64 `json:",string"`
|
||||
|
||||
// ModuleID PATH parameter
|
||||
//
|
||||
// Module ID
|
||||
ModuleID uint64 `json:",string"`
|
||||
|
||||
// Limit GET parameter
|
||||
//
|
||||
// Limit
|
||||
Limit uint
|
||||
|
||||
// PageCursor GET parameter
|
||||
//
|
||||
// Page cursor
|
||||
PageCursor string
|
||||
|
||||
// RecordID GET parameter
|
||||
//
|
||||
// Record ID
|
||||
RecordID uint64 `json:",string"`
|
||||
|
||||
// Deleted GET parameter
|
||||
//
|
||||
// Exclude (0
|
||||
Deleted uint
|
||||
}
|
||||
)
|
||||
|
||||
// NewResourcesSystemUsers request
|
||||
func NewResourcesSystemUsers() *ResourcesSystemUsers {
|
||||
return &ResourcesSystemUsers{}
|
||||
}
|
||||
|
||||
// Auditable returns all auditable/loggable parameters
|
||||
func (r ResourcesSystemUsers) Auditable() map[string]interface{} {
|
||||
return map[string]interface{}{
|
||||
"limit": r.Limit,
|
||||
"pageCursor": r.PageCursor,
|
||||
"userID": r.UserID,
|
||||
"deleted": r.Deleted,
|
||||
}
|
||||
}
|
||||
|
||||
// Auditable returns all auditable/loggable parameters
|
||||
func (r ResourcesSystemUsers) GetLimit() uint {
|
||||
return r.Limit
|
||||
}
|
||||
|
||||
// Auditable returns all auditable/loggable parameters
|
||||
func (r ResourcesSystemUsers) GetPageCursor() string {
|
||||
return r.PageCursor
|
||||
}
|
||||
|
||||
// Auditable returns all auditable/loggable parameters
|
||||
func (r ResourcesSystemUsers) GetUserID() uint64 {
|
||||
return r.UserID
|
||||
}
|
||||
|
||||
// Auditable returns all auditable/loggable parameters
|
||||
func (r ResourcesSystemUsers) GetDeleted() uint {
|
||||
return r.Deleted
|
||||
}
|
||||
|
||||
// Fill processes request and fills internal variables
|
||||
func (r *ResourcesSystemUsers) Fill(req *http.Request) (err error) {
|
||||
|
||||
{
|
||||
// GET params
|
||||
tmp := req.URL.Query()
|
||||
|
||||
if val, ok := tmp["limit"]; ok && len(val) > 0 {
|
||||
r.Limit, err = payload.ParseUint(val[0]), nil
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if val, ok := tmp["pageCursor"]; ok && len(val) > 0 {
|
||||
r.PageCursor, err = val[0], nil
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if val, ok := tmp["userID"]; ok && len(val) > 0 {
|
||||
r.UserID, err = payload.ParseUint64(val[0]), nil
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if val, ok := tmp["deleted"]; ok && len(val) > 0 {
|
||||
r.Deleted, err = payload.ParseUint(val[0]), nil
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
// NewResourcesComposeNamespaces request
|
||||
func NewResourcesComposeNamespaces() *ResourcesComposeNamespaces {
|
||||
return &ResourcesComposeNamespaces{}
|
||||
}
|
||||
|
||||
// Auditable returns all auditable/loggable parameters
|
||||
func (r ResourcesComposeNamespaces) Auditable() map[string]interface{} {
|
||||
return map[string]interface{}{
|
||||
"limit": r.Limit,
|
||||
"pageCursor": r.PageCursor,
|
||||
"namespaceID": r.NamespaceID,
|
||||
"deleted": r.Deleted,
|
||||
}
|
||||
}
|
||||
|
||||
// Auditable returns all auditable/loggable parameters
|
||||
func (r ResourcesComposeNamespaces) GetLimit() uint {
|
||||
return r.Limit
|
||||
}
|
||||
|
||||
// Auditable returns all auditable/loggable parameters
|
||||
func (r ResourcesComposeNamespaces) GetPageCursor() string {
|
||||
return r.PageCursor
|
||||
}
|
||||
|
||||
// Auditable returns all auditable/loggable parameters
|
||||
func (r ResourcesComposeNamespaces) GetNamespaceID() uint64 {
|
||||
return r.NamespaceID
|
||||
}
|
||||
|
||||
// Auditable returns all auditable/loggable parameters
|
||||
func (r ResourcesComposeNamespaces) GetDeleted() uint {
|
||||
return r.Deleted
|
||||
}
|
||||
|
||||
// Fill processes request and fills internal variables
|
||||
func (r *ResourcesComposeNamespaces) Fill(req *http.Request) (err error) {
|
||||
|
||||
{
|
||||
// GET params
|
||||
tmp := req.URL.Query()
|
||||
|
||||
if val, ok := tmp["limit"]; ok && len(val) > 0 {
|
||||
r.Limit, err = payload.ParseUint(val[0]), nil
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if val, ok := tmp["pageCursor"]; ok && len(val) > 0 {
|
||||
r.PageCursor, err = val[0], nil
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if val, ok := tmp["namespaceID"]; ok && len(val) > 0 {
|
||||
r.NamespaceID, err = payload.ParseUint64(val[0]), nil
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if val, ok := tmp["deleted"]; ok && len(val) > 0 {
|
||||
r.Deleted, err = payload.ParseUint(val[0]), nil
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
// NewResourcesComposeModules request
|
||||
func NewResourcesComposeModules() *ResourcesComposeModules {
|
||||
return &ResourcesComposeModules{}
|
||||
}
|
||||
|
||||
// Auditable returns all auditable/loggable parameters
|
||||
func (r ResourcesComposeModules) Auditable() map[string]interface{} {
|
||||
return map[string]interface{}{
|
||||
"namespaceID": r.NamespaceID,
|
||||
"limit": r.Limit,
|
||||
"pageCursor": r.PageCursor,
|
||||
"moduleID": r.ModuleID,
|
||||
"deleted": r.Deleted,
|
||||
}
|
||||
}
|
||||
|
||||
// Auditable returns all auditable/loggable parameters
|
||||
func (r ResourcesComposeModules) GetNamespaceID() uint64 {
|
||||
return r.NamespaceID
|
||||
}
|
||||
|
||||
// Auditable returns all auditable/loggable parameters
|
||||
func (r ResourcesComposeModules) GetLimit() uint {
|
||||
return r.Limit
|
||||
}
|
||||
|
||||
// Auditable returns all auditable/loggable parameters
|
||||
func (r ResourcesComposeModules) GetPageCursor() string {
|
||||
return r.PageCursor
|
||||
}
|
||||
|
||||
// Auditable returns all auditable/loggable parameters
|
||||
func (r ResourcesComposeModules) GetModuleID() uint64 {
|
||||
return r.ModuleID
|
||||
}
|
||||
|
||||
// Auditable returns all auditable/loggable parameters
|
||||
func (r ResourcesComposeModules) GetDeleted() uint {
|
||||
return r.Deleted
|
||||
}
|
||||
|
||||
// Fill processes request and fills internal variables
|
||||
func (r *ResourcesComposeModules) Fill(req *http.Request) (err error) {
|
||||
|
||||
{
|
||||
// GET params
|
||||
tmp := req.URL.Query()
|
||||
|
||||
if val, ok := tmp["limit"]; ok && len(val) > 0 {
|
||||
r.Limit, err = payload.ParseUint(val[0]), nil
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if val, ok := tmp["pageCursor"]; ok && len(val) > 0 {
|
||||
r.PageCursor, err = val[0], nil
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if val, ok := tmp["moduleID"]; ok && len(val) > 0 {
|
||||
r.ModuleID, err = payload.ParseUint64(val[0]), nil
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if val, ok := tmp["deleted"]; ok && len(val) > 0 {
|
||||
r.Deleted, err = payload.ParseUint(val[0]), nil
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
var val string
|
||||
// path params
|
||||
|
||||
val = chi.URLParam(req, "namespaceID")
|
||||
r.NamespaceID, err = payload.ParseUint64(val), nil
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
// NewResourcesComposeRecords request
|
||||
func NewResourcesComposeRecords() *ResourcesComposeRecords {
|
||||
return &ResourcesComposeRecords{}
|
||||
}
|
||||
|
||||
// Auditable returns all auditable/loggable parameters
|
||||
func (r ResourcesComposeRecords) Auditable() map[string]interface{} {
|
||||
return map[string]interface{}{
|
||||
"namespaceID": r.NamespaceID,
|
||||
"moduleID": r.ModuleID,
|
||||
"limit": r.Limit,
|
||||
"pageCursor": r.PageCursor,
|
||||
"recordID": r.RecordID,
|
||||
"deleted": r.Deleted,
|
||||
}
|
||||
}
|
||||
|
||||
// Auditable returns all auditable/loggable parameters
|
||||
func (r ResourcesComposeRecords) GetNamespaceID() uint64 {
|
||||
return r.NamespaceID
|
||||
}
|
||||
|
||||
// Auditable returns all auditable/loggable parameters
|
||||
func (r ResourcesComposeRecords) GetModuleID() uint64 {
|
||||
return r.ModuleID
|
||||
}
|
||||
|
||||
// Auditable returns all auditable/loggable parameters
|
||||
func (r ResourcesComposeRecords) GetLimit() uint {
|
||||
return r.Limit
|
||||
}
|
||||
|
||||
// Auditable returns all auditable/loggable parameters
|
||||
func (r ResourcesComposeRecords) GetPageCursor() string {
|
||||
return r.PageCursor
|
||||
}
|
||||
|
||||
// Auditable returns all auditable/loggable parameters
|
||||
func (r ResourcesComposeRecords) GetRecordID() uint64 {
|
||||
return r.RecordID
|
||||
}
|
||||
|
||||
// Auditable returns all auditable/loggable parameters
|
||||
func (r ResourcesComposeRecords) GetDeleted() uint {
|
||||
return r.Deleted
|
||||
}
|
||||
|
||||
// Fill processes request and fills internal variables
|
||||
func (r *ResourcesComposeRecords) Fill(req *http.Request) (err error) {
|
||||
|
||||
{
|
||||
// GET params
|
||||
tmp := req.URL.Query()
|
||||
|
||||
if val, ok := tmp["limit"]; ok && len(val) > 0 {
|
||||
r.Limit, err = payload.ParseUint(val[0]), nil
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if val, ok := tmp["pageCursor"]; ok && len(val) > 0 {
|
||||
r.PageCursor, err = val[0], nil
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if val, ok := tmp["recordID"]; ok && len(val) > 0 {
|
||||
r.RecordID, err = payload.ParseUint64(val[0]), nil
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if val, ok := tmp["deleted"]; ok && len(val) > 0 {
|
||||
r.Deleted, err = payload.ParseUint(val[0]), nil
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
var val string
|
||||
// path params
|
||||
|
||||
val = chi.URLParam(req, "namespaceID")
|
||||
r.NamespaceID, err = payload.ParseUint64(val), nil
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
val = chi.URLParam(req, "moduleID")
|
||||
r.ModuleID, err = payload.ParseUint64(val), nil
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
44
server/discovery/rest/resources.go
Normal file
44
server/discovery/rest/resources.go
Normal file
@@ -0,0 +1,44 @@
|
||||
package rest
|
||||
|
||||
import (
|
||||
"context"
|
||||
"github.com/cortezaproject/corteza/server/discovery/rest/internal/documents"
|
||||
"github.com/cortezaproject/corteza/server/discovery/rest/request"
|
||||
)
|
||||
|
||||
type (
|
||||
resources struct {
|
||||
sys interface {
|
||||
Users(ctx context.Context, limit uint, cur string, userID uint64, deleted uint) (*documents.Response, error)
|
||||
}
|
||||
|
||||
cmp interface {
|
||||
Namespaces(ctx context.Context, limit uint, cur string, namespaceID uint64, deleted uint) (*documents.Response, error)
|
||||
Modules(ctx context.Context, namespaceID uint64, limit uint, cur string, moduleID uint64, deleted uint) (*documents.Response, error)
|
||||
Records(ctx context.Context, namespaceID, moduleID uint64, limit uint, cur string, recordID uint64, deleted uint) (*documents.Response, error)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
func Resources() *resources {
|
||||
return &resources{
|
||||
sys: documents.SystemResources(),
|
||||
cmp: documents.ComposeResources(),
|
||||
}
|
||||
}
|
||||
|
||||
func (ctrl resources) SystemUsers(ctx context.Context, r *request.ResourcesSystemUsers) (interface{}, error) {
|
||||
return ctrl.sys.Users(ctx, r.Limit, r.PageCursor, r.UserID, r.Deleted)
|
||||
}
|
||||
|
||||
func (ctrl resources) ComposeNamespaces(ctx context.Context, r *request.ResourcesComposeNamespaces) (interface{}, error) {
|
||||
return ctrl.cmp.Namespaces(ctx, r.Limit, r.PageCursor, r.NamespaceID, r.Deleted)
|
||||
}
|
||||
|
||||
func (ctrl resources) ComposeModules(ctx context.Context, r *request.ResourcesComposeModules) (interface{}, error) {
|
||||
return ctrl.cmp.Modules(ctx, r.NamespaceID, r.Limit, r.PageCursor, r.ModuleID, r.Deleted)
|
||||
}
|
||||
|
||||
func (ctrl resources) ComposeRecords(ctx context.Context, r *request.ResourcesComposeRecords) (interface{}, error) {
|
||||
return ctrl.cmp.Records(ctx, r.NamespaceID, r.ModuleID, r.Limit, r.PageCursor, r.RecordID, r.Deleted)
|
||||
}
|
||||
21
server/discovery/rest/router.go
Normal file
21
server/discovery/rest/router.go
Normal file
@@ -0,0 +1,21 @@
|
||||
package rest
|
||||
|
||||
import (
|
||||
"github.com/go-chi/chi/v5"
|
||||
|
||||
"github.com/cortezaproject/corteza/server/discovery/rest/handlers"
|
||||
"github.com/cortezaproject/corteza/server/pkg/auth"
|
||||
"github.com/cortezaproject/corteza/server/pkg/options"
|
||||
)
|
||||
|
||||
func MountRoutes(discoveryOpts options.DiscoveryOpt) func(r chi.Router) {
|
||||
return func(r chi.Router) {
|
||||
r.Group(func(r chi.Router) {
|
||||
r.Use(auth.HttpTokenValidator("discovery"))
|
||||
|
||||
handlers.NewResources(Resources()).MountRoutes(r)
|
||||
handlers.NewFeed(Feed()).MountRoutes(r)
|
||||
handlers.NewMappings(Mappings(discoveryOpts)).MountRoutes(r)
|
||||
})
|
||||
}
|
||||
}
|
||||
157
server/discovery/service/resource_activity_log.go
Normal file
157
server/discovery/service/resource_activity_log.go
Normal file
@@ -0,0 +1,157 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"github.com/cortezaproject/corteza/server/pkg/eventbus"
|
||||
"github.com/cortezaproject/corteza/server/pkg/id"
|
||||
"github.com/cortezaproject/corteza/server/pkg/options"
|
||||
"go.uber.org/zap"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/cortezaproject/corteza/server/discovery/types"
|
||||
"github.com/cortezaproject/corteza/server/store"
|
||||
)
|
||||
|
||||
type (
|
||||
resourceActivity struct {
|
||||
// log for repository errors
|
||||
log *zap.Logger
|
||||
opt options.DiscoveryOpt
|
||||
mux sync.RWMutex
|
||||
|
||||
// where the activity log records are kept
|
||||
// store resourceActivityLogStore
|
||||
store store.Storer
|
||||
|
||||
eventbus eventbusRegistry
|
||||
decoder types.ResDecoder
|
||||
}
|
||||
|
||||
eventbusRegistry interface {
|
||||
Register(eventbus.HandlerFn, ...eventbus.HandlerRegOp) uintptr
|
||||
}
|
||||
|
||||
ResourceActivityService interface {
|
||||
Find(context.Context, types.ResourceActivityFilter) (types.ResourceActivitySet, types.ResourceActivityFilter, error)
|
||||
}
|
||||
)
|
||||
|
||||
func ResourceActivity(log *zap.Logger, opt options.DiscoveryOpt, s store.Storer, eb eventbusRegistry) *resourceActivity {
|
||||
return &resourceActivity{
|
||||
log: log.Named("resource-activity"),
|
||||
opt: opt,
|
||||
store: s,
|
||||
eventbus: eb,
|
||||
}
|
||||
}
|
||||
|
||||
func (svc *resourceActivity) logger(a *types.ResourceActivity) {
|
||||
zlf := []zap.Field{
|
||||
zap.Uint8("recordID", uint8(a.ResourceID)),
|
||||
zap.String("ResourceType", a.ResourceType),
|
||||
zap.String("ResourceAction", a.ResourceAction),
|
||||
zap.Time("timestamp", a.Timestamp),
|
||||
}
|
||||
|
||||
svc.log.
|
||||
With(zlf...).
|
||||
// Skipping 3 callers (the most common stack)
|
||||
// discovery.service.log()
|
||||
// discovery.service.Record()
|
||||
// (generated service function)
|
||||
//
|
||||
// One exception, access control, that calls Record fn directly,
|
||||
// without going through generated discovery helpers
|
||||
WithOptions(zap.AddCallerSkip(3)).
|
||||
// This is debug logger, and we log all recordings as debug
|
||||
Debug(fmt.Sprintf("%s of %s", a.ResourceAction, a.ResourceType))
|
||||
}
|
||||
|
||||
func (svc *resourceActivity) InitResourceActivityLog(ctx context.Context, resourceType []string) (err error) {
|
||||
eventType := []string{
|
||||
string(types.AfterCreate),
|
||||
string(types.AfterUpdate),
|
||||
string(types.AfterDelete),
|
||||
}
|
||||
|
||||
svc.mux.RLock()
|
||||
defer svc.mux.RUnlock()
|
||||
|
||||
svc.eventbus.Register(
|
||||
func(_ context.Context, ev eventbus.Event) error {
|
||||
var a *types.ResourceActivity
|
||||
dec, is := ev.(types.ResDecoder)
|
||||
if is {
|
||||
svc.log.Debug("resource changed",
|
||||
zap.String("eventType", ev.EventType()),
|
||||
zap.String("resourceType", ev.ResourceType()),
|
||||
)
|
||||
|
||||
a, err = types.CastToResourceActivity(dec)
|
||||
if err != nil {
|
||||
svc.log.With(zap.Error(err)).Error("could not cast event to activity")
|
||||
return err
|
||||
}
|
||||
|
||||
err = svc.store.CreateResourceActivity(ctx, a)
|
||||
if err != nil {
|
||||
svc.log.With(zap.Error(err)).Error("could not record activity event")
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
eventbus.For(resourceType...),
|
||||
eventbus.On(eventType...),
|
||||
)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (svc *resourceActivity) Find(ctx context.Context, filter types.ResourceActivityFilter) (aa types.ResourceActivitySet, f types.ResourceActivityFilter, err error) {
|
||||
err = func() error {
|
||||
aa, f, err = store.SearchResourceActivitys(ctx, svc.store, filter)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}()
|
||||
|
||||
return aa, f, err
|
||||
}
|
||||
|
||||
func (svc *resourceActivity) Record(ctx context.Context, a *types.ResourceActivity) {
|
||||
if a == nil {
|
||||
// nothing to record
|
||||
return
|
||||
}
|
||||
|
||||
a = enrich(ctx, a)
|
||||
a.ID = id.Next()
|
||||
|
||||
svc.logger(a)
|
||||
|
||||
// We want to prevent any abrupt cancellation
|
||||
// (e.g. canceled request) that would cause
|
||||
// discovery to fail...
|
||||
ctx = context.Background()
|
||||
|
||||
if err := svc.store.CreateResourceActivity(ctx, a); err != nil {
|
||||
svc.log.With(zap.Error(err)).Error("could not record activity event")
|
||||
}
|
||||
}
|
||||
|
||||
// enrich activity with additional info (timestamp, ...)
|
||||
func enrich(_ context.Context, a *types.ResourceActivity) *types.ResourceActivity {
|
||||
if a.Timestamp.IsZero() {
|
||||
a.Timestamp = time.Now()
|
||||
}
|
||||
|
||||
return a
|
||||
}
|
||||
33
server/discovery/service/service.go
Normal file
33
server/discovery/service/service.go
Normal file
@@ -0,0 +1,33 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"github.com/cortezaproject/corteza/server/pkg/eventbus"
|
||||
"github.com/cortezaproject/corteza/server/pkg/options"
|
||||
"github.com/cortezaproject/corteza/server/store"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
var (
|
||||
DefaultOption options.DiscoveryOpt
|
||||
|
||||
// DefaultStore is an interface to storage backend(s)
|
||||
// ng (next-gen) is a temporary prefix
|
||||
// so that we can differentiate between it and the file-only store
|
||||
DefaultStore store.Storer
|
||||
|
||||
DefaultResourceActivity *resourceActivity
|
||||
)
|
||||
|
||||
// Initialize discovery service
|
||||
func Initialize(_ context.Context, log *zap.Logger, opt options.DiscoveryOpt, s store.Storer) (err error) {
|
||||
DefaultOption = opt
|
||||
|
||||
// we're doing conversion to avoid having
|
||||
// store interface exposed or generated inside app package
|
||||
DefaultStore = s
|
||||
|
||||
DefaultResourceActivity = ResourceActivity(log, opt, s, eventbus.Service())
|
||||
|
||||
return
|
||||
}
|
||||
158
server/discovery/types/access_restrictions.go
Normal file
158
server/discovery/types/access_restrictions.go
Normal file
@@ -0,0 +1,158 @@
|
||||
package types
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"regexp"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type (
|
||||
Access int
|
||||
|
||||
accessJsonObjAux struct {
|
||||
Public bool `json:"public"`
|
||||
Private bool `json:"private"`
|
||||
Protected bool `json:"protected"`
|
||||
}
|
||||
|
||||
accessStrSliceAux []string
|
||||
)
|
||||
|
||||
const (
|
||||
Mask Access = 0b111
|
||||
|
||||
Private Access = 0b001
|
||||
Protected Access = 0b010
|
||||
Public Access = 0b100
|
||||
|
||||
PrivateLabel = "private"
|
||||
ProtectedLabel = "protected"
|
||||
PublicLabel = "public"
|
||||
)
|
||||
|
||||
var (
|
||||
accessStrDelimiter = regexp.MustCompile(`\W+`)
|
||||
|
||||
z = int(Protected)
|
||||
)
|
||||
|
||||
func toAccess(str string) (a Access) {
|
||||
for _, str = range accessStrDelimiter.Split(strings.ToLower(str), -1) {
|
||||
switch str {
|
||||
case PrivateLabel:
|
||||
a |= Private
|
||||
case ProtectedLabel:
|
||||
a |= Protected
|
||||
case PublicLabel:
|
||||
a |= Public
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (a Access) Check(str string) bool {
|
||||
return a.Is(toAccess(str))
|
||||
}
|
||||
|
||||
func (a Access) MCheck(str string, mask Access) bool {
|
||||
return a.Is(mask & toAccess(str))
|
||||
}
|
||||
|
||||
func (a Access) Is(i Access) bool { return a&i != 0 }
|
||||
func (a Access) IsPrivate() bool { return a.Is(Private) }
|
||||
func (a Access) IsProtected() bool { return a.Is(Protected) }
|
||||
func (a Access) IsPublic() bool { return a.Is(Public) }
|
||||
|
||||
func (a Access) String() string {
|
||||
var s = ""
|
||||
if a&Public == Public {
|
||||
s += PrivateLabel
|
||||
}
|
||||
if a&Protected == Protected {
|
||||
if len(s) > 0 {
|
||||
s += ","
|
||||
}
|
||||
|
||||
s += PrivateLabel
|
||||
}
|
||||
|
||||
if a&Private == Private {
|
||||
if len(s) > 0 {
|
||||
s += ","
|
||||
}
|
||||
|
||||
s += PrivateLabel
|
||||
}
|
||||
|
||||
return s
|
||||
}
|
||||
|
||||
func (a *Access) UnmarshalJSON(data []byte) (err error) {
|
||||
if len(data) == 0 {
|
||||
// empty input, nothing to parse
|
||||
return nil
|
||||
}
|
||||
|
||||
if data[0] == '"' {
|
||||
*a = toAccess(string(data))
|
||||
return
|
||||
}
|
||||
|
||||
if data[0] == '{' {
|
||||
aux := &accessJsonObjAux{}
|
||||
if err = json.Unmarshal(data, aux); err == nil {
|
||||
*a = aux.toAccess()
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if data[0] == '[' {
|
||||
aux := &accessStrSliceAux{}
|
||||
if err = json.Unmarshal(data, aux); err == nil {
|
||||
*a = aux.toAccess()
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
var aux int
|
||||
if err = json.Unmarshal(data, &aux); err == nil {
|
||||
*a = Access(aux)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (a Access) MarshalJSON() ([]byte, error) {
|
||||
return json.Marshal(accessJsonObjAux{
|
||||
Public: a&Public != 0,
|
||||
Private: a&Private != 0,
|
||||
Protected: a&Protected != 0,
|
||||
})
|
||||
}
|
||||
|
||||
func (aux accessJsonObjAux) toAccess() (a Access) {
|
||||
if aux.Private {
|
||||
a |= Private
|
||||
}
|
||||
|
||||
if aux.Protected {
|
||||
a |= Protected
|
||||
}
|
||||
|
||||
if aux.Public {
|
||||
a |= Public
|
||||
}
|
||||
|
||||
return a
|
||||
}
|
||||
|
||||
func (aux accessStrSliceAux) toAccess() (a Access) {
|
||||
for _, s := range aux {
|
||||
a |= toAccess(s)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
77
server/discovery/types/access_restrictions_test.go
Normal file
77
server/discovery/types/access_restrictions_test.go
Normal file
@@ -0,0 +1,77 @@
|
||||
package types
|
||||
|
||||
import (
|
||||
"github.com/stretchr/testify/require"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func Test_toAccess(t *testing.T) {
|
||||
var (
|
||||
req = require.New(t)
|
||||
)
|
||||
|
||||
req.Equal(Private, toAccess("private"))
|
||||
req.Equal(Public, toAccess("public"))
|
||||
req.Equal(Protected, toAccess("protected"))
|
||||
req.Equal(Private|Protected, toAccess("protected|private"))
|
||||
req.Equal(Access(0), toAccess(""))
|
||||
}
|
||||
|
||||
func TestAccess_Is(t *testing.T) {
|
||||
var (
|
||||
req = require.New(t)
|
||||
)
|
||||
|
||||
req.True(Private.IsPrivate())
|
||||
req.True(Private.Is(Private))
|
||||
req.True(Protected.IsProtected())
|
||||
req.True(Protected.Is(Protected))
|
||||
req.True(Public.IsPublic())
|
||||
req.True(Public.Is(Public))
|
||||
req.True((Public | Protected).Is(Public))
|
||||
req.True((Public | Protected).Is(Protected))
|
||||
req.False((Public | Protected).Is(Private))
|
||||
}
|
||||
|
||||
func TestAccess_UnmarshalJSON(t *testing.T) {
|
||||
var (
|
||||
cc = []struct {
|
||||
Data string
|
||||
WantErr bool
|
||||
Access Access
|
||||
}{
|
||||
{Data: ``, Access: Access(0)},
|
||||
{Data: `{}`, Access: Access(0)},
|
||||
{Data: `{"private":true}`, Access: Private},
|
||||
{Data: `{"private":true,"public":true}`, Access: Private | Public},
|
||||
{Data: `true`, WantErr: true},
|
||||
{Data: `1`, Access: Private},
|
||||
{Data: `3`, Access: Private | Protected},
|
||||
{Data: `"private"`, Access: Private},
|
||||
{Data: `"private|public"`, Access: Private | Public},
|
||||
{Data: `"private,public"`, Access: Private | Public},
|
||||
}
|
||||
)
|
||||
|
||||
for _, c := range cc {
|
||||
t.Run(c.Data, func(t *testing.T) {
|
||||
var (
|
||||
req = require.New(t)
|
||||
a Access
|
||||
)
|
||||
|
||||
err := a.UnmarshalJSON([]byte(c.Data))
|
||||
if c.WantErr {
|
||||
req.Error(err)
|
||||
} else {
|
||||
req.NoError(err)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
req.Equal(a, c.Access)
|
||||
})
|
||||
}
|
||||
}
|
||||
58
server/discovery/types/resource_meta.go
Normal file
58
server/discovery/types/resource_meta.go
Normal file
@@ -0,0 +1,58 @@
|
||||
package types
|
||||
|
||||
type (
|
||||
// ResponseMeta has all access restriction response
|
||||
ResponseMeta struct {
|
||||
Private ResourceMeta `json:"private,omitempty"`
|
||||
Public ResourceMeta `json:"public,omitempty"`
|
||||
Protected ResourceMeta `json:"protected,omitempty"`
|
||||
}
|
||||
|
||||
// ResourceMeta have all resource related meta
|
||||
// for what to display in what order for each resource and its fields
|
||||
ResourceMeta struct {
|
||||
NamespaceMeta []NameMeta `json:"namespace_meta,omitempty"`
|
||||
ModuleMeta []NameMeta `json:"module_meta,omitempty"`
|
||||
RecordMeta []NameMeta `json:"record_meta,omitempty"`
|
||||
}
|
||||
|
||||
// NameMeta is single row of discovery response fields with its weight
|
||||
NameMeta struct {
|
||||
Name string `json:"name"`
|
||||
Title string `json:"title"`
|
||||
Weight int `json:"weight"`
|
||||
}
|
||||
|
||||
/*
|
||||
"discovery": {
|
||||
"public": {
|
||||
"result": [
|
||||
{
|
||||
lang: "", // en, fr
|
||||
fields: [] // “names”, ..
|
||||
}
|
||||
]
|
||||
} // struct
|
||||
},
|
||||
"private": {
|
||||
"response": []
|
||||
}
|
||||
}
|
||||
*/
|
||||
ModuleMeta struct {
|
||||
Public AccessJson `json:"public"`
|
||||
Private AccessJson `json:"private"`
|
||||
Protected AccessJson `json:"protected"`
|
||||
}
|
||||
|
||||
AccessJson struct {
|
||||
Result []Result `json:"result"`
|
||||
}
|
||||
|
||||
Result struct {
|
||||
Lang string `json:"lang"`
|
||||
Fields []string `json:"fields"`
|
||||
|
||||
// @todo? TBD? excludeModuleFields, includeModuleFields <- if passed filter module field accordingly.
|
||||
}
|
||||
)
|
||||
73
server/discovery/types/type_set.gen.go
generated
Normal file
73
server/discovery/types/type_set.gen.go
generated
Normal file
@@ -0,0 +1,73 @@
|
||||
package types
|
||||
|
||||
// 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:
|
||||
// discovery/types/types.yaml
|
||||
|
||||
type (
|
||||
|
||||
// ResourceActivitySet slice of ResourceActivity
|
||||
//
|
||||
// This type is auto-generated.
|
||||
ResourceActivitySet []*ResourceActivity
|
||||
)
|
||||
|
||||
// Walk iterates through every slice item and calls w(ResourceActivity) err
|
||||
//
|
||||
// This function is auto-generated.
|
||||
func (set ResourceActivitySet) Walk(w func(*ResourceActivity) error) (err error) {
|
||||
for i := range set {
|
||||
if err = w(set[i]); err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// Filter iterates through every slice item, calls f(ResourceActivity) (bool, err) and return filtered slice
|
||||
//
|
||||
// This function is auto-generated.
|
||||
func (set ResourceActivitySet) Filter(f func(*ResourceActivity) (bool, error)) (out ResourceActivitySet, err error) {
|
||||
var ok bool
|
||||
out = ResourceActivitySet{}
|
||||
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 ResourceActivitySet) FindByID(ID uint64) *ResourceActivity {
|
||||
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 ResourceActivitySet) IDs() (IDs []uint64) {
|
||||
IDs = make([]uint64, len(set))
|
||||
|
||||
for i := range set {
|
||||
IDs[i] = set[i].ID
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
105
server/discovery/types/type_set.gen_test.go
generated
Normal file
105
server/discovery/types/type_set.gen_test.go
generated
Normal file
@@ -0,0 +1,105 @@
|
||||
package types
|
||||
|
||||
// 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:
|
||||
// discovery/types/types.yaml
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/stretchr/testify/require"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestResourceActivitySetWalk(t *testing.T) {
|
||||
var (
|
||||
value = make(ResourceActivitySet, 3)
|
||||
req = require.New(t)
|
||||
)
|
||||
|
||||
// check walk with no errors
|
||||
{
|
||||
err := value.Walk(func(*ResourceActivity) error {
|
||||
return nil
|
||||
})
|
||||
req.NoError(err)
|
||||
}
|
||||
|
||||
// check walk with error
|
||||
req.Error(value.Walk(func(*ResourceActivity) error { return fmt.Errorf("walk error") }))
|
||||
}
|
||||
|
||||
func TestResourceActivitySetFilter(t *testing.T) {
|
||||
var (
|
||||
value = make(ResourceActivitySet, 3)
|
||||
req = require.New(t)
|
||||
)
|
||||
|
||||
// filter nothing
|
||||
{
|
||||
set, err := value.Filter(func(*ResourceActivity) (bool, error) {
|
||||
return true, nil
|
||||
})
|
||||
req.NoError(err)
|
||||
req.Equal(len(set), len(value))
|
||||
}
|
||||
|
||||
// filter one item
|
||||
{
|
||||
found := false
|
||||
set, err := value.Filter(func(*ResourceActivity) (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(*ResourceActivity) (bool, error) {
|
||||
return false, fmt.Errorf("filter error")
|
||||
})
|
||||
req.Error(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestResourceActivitySetIDs(t *testing.T) {
|
||||
var (
|
||||
value = make(ResourceActivitySet, 3)
|
||||
req = require.New(t)
|
||||
)
|
||||
|
||||
// construct objects
|
||||
value[0] = new(ResourceActivity)
|
||||
value[1] = new(ResourceActivity)
|
||||
value[2] = new(ResourceActivity)
|
||||
// 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))
|
||||
}
|
||||
}
|
||||
192
server/discovery/types/types.go
Normal file
192
server/discovery/types/types.go
Normal file
@@ -0,0 +1,192 @@
|
||||
package types
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
composeTypes "github.com/cortezaproject/corteza/server/compose/types"
|
||||
"github.com/cortezaproject/corteza/server/pkg/filter"
|
||||
"github.com/cortezaproject/corteza/server/pkg/id"
|
||||
systemTypes "github.com/cortezaproject/corteza/server/system/types"
|
||||
"github.com/jmoiron/sqlx/types"
|
||||
)
|
||||
|
||||
type (
|
||||
// ResourceAction determinants resource action event
|
||||
ResourceAction string
|
||||
|
||||
// ResourceActivity Standardized data structure for resource activity
|
||||
ResourceActivity struct {
|
||||
ID uint64 `json:"activityID,string"`
|
||||
|
||||
// ResourceID of the corteza resource
|
||||
ResourceID uint64 `json:"resourceID,string"`
|
||||
|
||||
// ResourceType
|
||||
ResourceType string `json:"resourceType"`
|
||||
|
||||
// ResourceAction Type of action
|
||||
ResourceAction string `json:"resourceAction"`
|
||||
|
||||
// Timestamp of the raised event
|
||||
Timestamp time.Time `json:"timestamp"`
|
||||
|
||||
// Meta of the related resources
|
||||
Meta types.JSONText `json:"meta"`
|
||||
}
|
||||
|
||||
ResourceActivityFilter struct {
|
||||
FromTimestamp *time.Time `json:"from"`
|
||||
ToTimestamp *time.Time `json:"to"`
|
||||
|
||||
// Check fn is called by store backend for each resource found function can
|
||||
// modify the activity and return false if store should not return it
|
||||
//
|
||||
// Store then loads additional resources to satisfy the paging parameters
|
||||
Check func(*ResourceActivity) (bool, error) `json:"-"`
|
||||
|
||||
// Standard helpers for paging and sorting
|
||||
filter.Sorting
|
||||
filter.Paging
|
||||
}
|
||||
|
||||
ResourceActivityMeta struct {
|
||||
NamespaceID uint64 `json:"namespaceID,string"`
|
||||
ModuleID uint64 `json:"moduleID,string"`
|
||||
}
|
||||
|
||||
ResDecoder interface {
|
||||
EventType() string
|
||||
ResourceType() string
|
||||
}
|
||||
userDecoder interface {
|
||||
ResDecoder
|
||||
User() *systemTypes.User
|
||||
OldUser() *systemTypes.User
|
||||
}
|
||||
nsDecoder interface {
|
||||
ResDecoder
|
||||
Namespace() *composeTypes.Namespace
|
||||
OldNamespace() *composeTypes.Namespace
|
||||
}
|
||||
mDecoder interface {
|
||||
ResDecoder
|
||||
Module() *composeTypes.Module
|
||||
OldModule() *composeTypes.Module
|
||||
}
|
||||
recDecoder interface {
|
||||
ResDecoder
|
||||
Record() *composeTypes.Record
|
||||
OldRecord() *composeTypes.Record
|
||||
}
|
||||
)
|
||||
|
||||
const (
|
||||
AfterCreate ResourceAction = "afterCreate"
|
||||
AfterUpdate ResourceAction = "afterUpdate"
|
||||
AfterDelete ResourceAction = "afterDelete"
|
||||
|
||||
ResourceActivityResourceType = "corteza::generic:resource-activity"
|
||||
)
|
||||
|
||||
func (s ResourceAction) String() string {
|
||||
switch s {
|
||||
case AfterCreate:
|
||||
return "create"
|
||||
case AfterUpdate:
|
||||
return "update"
|
||||
case AfterDelete:
|
||||
return "delete"
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
func CastToResourceActivity(dec ResDecoder) (a *ResourceActivity, err error) {
|
||||
a = &ResourceActivity{
|
||||
ID: id.Next(),
|
||||
Timestamp: time.Now(),
|
||||
ResourceType: dec.ResourceType(),
|
||||
ResourceAction: ResourceAction(dec.EventType()).String(),
|
||||
}
|
||||
|
||||
setResourceID := func(ID uint64) {
|
||||
a.ResourceID = ID
|
||||
}
|
||||
setMeta := func(nsID, mID uint64) error {
|
||||
var meta ResourceActivityMeta
|
||||
if nsID > 0 {
|
||||
meta.NamespaceID = nsID
|
||||
}
|
||||
if mID > 0 {
|
||||
meta.ModuleID = mID
|
||||
}
|
||||
|
||||
a.Meta, err = json.Marshal(meta)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
switch a.ResourceType {
|
||||
case "system:user": // @todo system/service/service.go#134
|
||||
if v, ok := dec.(userDecoder); ok {
|
||||
user := v.User()
|
||||
// fallback to OldUser for afterDelete event
|
||||
if user == nil {
|
||||
user = v.OldUser()
|
||||
}
|
||||
if user != nil {
|
||||
setResourceID(user.ID)
|
||||
}
|
||||
}
|
||||
case (composeTypes.Namespace{}).LabelResourceKind():
|
||||
if v, ok := dec.(nsDecoder); ok {
|
||||
ns := v.Namespace()
|
||||
// fallback to OldNamespace for afterDelete event
|
||||
if ns == nil {
|
||||
ns = v.OldNamespace()
|
||||
}
|
||||
if ns != nil {
|
||||
setResourceID(ns.ID)
|
||||
}
|
||||
}
|
||||
case (composeTypes.Module{}).LabelResourceKind():
|
||||
if v, ok := dec.(mDecoder); ok {
|
||||
mod := v.Module()
|
||||
// fallback to OldModule for afterDelete event
|
||||
if mod == nil {
|
||||
mod = v.OldModule()
|
||||
}
|
||||
if mod != nil {
|
||||
setResourceID(mod.ID)
|
||||
err = setMeta(mod.NamespaceID, 0)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
case "compose:record":
|
||||
if v, ok := dec.(recDecoder); ok {
|
||||
rec := v.Record()
|
||||
// fallback to OldRecord for afterDelete event
|
||||
if rec == nil {
|
||||
rec = v.OldRecord()
|
||||
}
|
||||
if rec != nil {
|
||||
setResourceID(rec.ID)
|
||||
err = setMeta(rec.NamespaceID, rec.ModuleID)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
default:
|
||||
return a, fmt.Errorf("unsupported resource type")
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
2
server/discovery/types/types.yaml
Normal file
2
server/discovery/types/types.yaml
Normal file
@@ -0,0 +1,2 @@
|
||||
types:
|
||||
ResourceActivity: {}
|
||||
Reference in New Issue
Block a user