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
552 lines
14 KiB
Go
552 lines
14 KiB
Go
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
|
|
}
|