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:
653
server/compose/envoy/record_datasource.go
Normal file
653
server/compose/envoy/record_datasource.go
Normal file
@@ -0,0 +1,653 @@
|
||||
package envoy
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/cortezaproject/corteza/server/compose/dalutils"
|
||||
"github.com/cortezaproject/corteza/server/compose/types"
|
||||
"github.com/cortezaproject/corteza/server/pkg/dal"
|
||||
"github.com/cortezaproject/corteza/server/pkg/envoyx"
|
||||
"github.com/cortezaproject/corteza/server/pkg/envoyx/datasource"
|
||||
"github.com/cortezaproject/corteza/server/pkg/errors"
|
||||
"github.com/cortezaproject/corteza/server/pkg/filter"
|
||||
"github.com/cortezaproject/corteza/server/pkg/logger"
|
||||
"github.com/cortezaproject/corteza/server/store"
|
||||
systemTypes "github.com/cortezaproject/corteza/server/system/types"
|
||||
"github.com/modern-go/reflect2"
|
||||
"github.com/spf13/cast"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
type (
|
||||
// RecordDatasource provides a mechanism for you to access large
|
||||
// record datasets optimally
|
||||
RecordDatasource struct {
|
||||
Mapping envoyx.DatasourceMapping
|
||||
Provider envoyx.Provider
|
||||
|
||||
multivalues map[string]bool
|
||||
datetimeFields map[string]bool
|
||||
|
||||
CheckExisting func(ctx context.Context, ref ...[]string) ([]uint64, error)
|
||||
|
||||
currentIndex int
|
||||
|
||||
// Index to map from ref to ID
|
||||
// @todo we might need to flush these to the disc in case a huge dataset is passed in
|
||||
refToID map[string]uint64
|
||||
// @todo might be worth putting both into one map; not sure how much space we'd save up
|
||||
existingIDs map[uint64]bool
|
||||
}
|
||||
|
||||
// iteratorProvider is a wrapper around the dal.Iterator to conform to the
|
||||
// envoy.Provider interface
|
||||
iteratorProvider struct {
|
||||
iter dal.Iterator
|
||||
|
||||
// Items related to ref resolution
|
||||
// @todo can be removed when reworked
|
||||
resolveRefs bool
|
||||
includeRefID bool
|
||||
relMods map[string]refModWrap
|
||||
dal dal.FullService
|
||||
|
||||
// Access control for field-level permissions
|
||||
ac recordValueAccessController
|
||||
mod *types.Module
|
||||
readableFields map[string]bool
|
||||
|
||||
rows []datasource.RawRecord
|
||||
buffIndex int
|
||||
done bool
|
||||
|
||||
// User ref resolution
|
||||
store store.Storer
|
||||
userFields map[string]bool
|
||||
}
|
||||
|
||||
refModWrap struct {
|
||||
modLvl1 *types.Module
|
||||
labelLvl1 string
|
||||
|
||||
modLvl2 *types.Module
|
||||
labelLvl2 string
|
||||
}
|
||||
|
||||
// recordValueAccessController interface for checking field-level read permissions
|
||||
recordValueAccessController interface {
|
||||
CanReadRecordValueOnModuleField(context.Context, *types.ModuleField) bool
|
||||
}
|
||||
)
|
||||
|
||||
const (
|
||||
bufferPullChunkSize = int(100)
|
||||
)
|
||||
|
||||
func mkIteratorProvider(ctx context.Context, ac recordValueAccessController, s store.Storer, dl dal.FullService, iter dal.Iterator, mod *types.Module, resolveRefs bool, includeRefID bool) (out *iteratorProvider, err error) {
|
||||
out = &iteratorProvider{
|
||||
iter: iter,
|
||||
dal: dl,
|
||||
resolveRefs: resolveRefs,
|
||||
includeRefID: includeRefID,
|
||||
|
||||
ac: ac,
|
||||
mod: mod,
|
||||
}
|
||||
|
||||
// Pre-compute readable fields based on access control permissions
|
||||
// This avoids checking permissions on every record iteration
|
||||
if !reflect2.IsNil(ac) {
|
||||
out.readableFields = make(map[string]bool)
|
||||
for _, f := range mod.Fields {
|
||||
// Ensure NamespaceID is set on the field (it may not be loaded from store)
|
||||
// RBAC checks require the full resource path including namespace
|
||||
if f.NamespaceID == 0 {
|
||||
f.NamespaceID = mod.NamespaceID
|
||||
}
|
||||
out.readableFields[f.Name] = ac.CanReadRecordValueOnModuleField(ctx, f)
|
||||
}
|
||||
}
|
||||
|
||||
refMods := make(map[string]refModWrap)
|
||||
userFields := make(map[string]bool)
|
||||
|
||||
for _, name := range []string{"createdBy", "updatedBy", "ownedBy", "deletedBy"} {
|
||||
userFields[name] = true
|
||||
}
|
||||
|
||||
for _, f := range mod.Fields {
|
||||
switch f.Kind {
|
||||
case "Record":
|
||||
refMods[f.Name], err = mkRecordRefWrap(ctx, s, f)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
case "User":
|
||||
userFields[f.Name] = true
|
||||
}
|
||||
}
|
||||
|
||||
out.relMods = refMods
|
||||
out.store = s
|
||||
out.userFields = userFields
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// ResolvedFields returns field names that will have a companion "X ID" column when includeRefID is enabled.
|
||||
func (ip *iteratorProvider) ResolvedFields() []string {
|
||||
if !ip.resolveRefs || !ip.includeRefID {
|
||||
return nil
|
||||
}
|
||||
|
||||
out := make([]string, 0, len(ip.userFields)+len(ip.relMods))
|
||||
for f := range ip.userFields {
|
||||
out = append(out, f)
|
||||
}
|
||||
|
||||
for f := range ip.relMods {
|
||||
out = append(out, f)
|
||||
}
|
||||
|
||||
return out
|
||||
}
|
||||
|
||||
// ResolvedFields returns field names that will have a companion "X value" column.
|
||||
func (rd *RecordDatasource) ResolvedFields() []string {
|
||||
if ip, ok := rd.Provider.(*iteratorProvider); ok {
|
||||
return ip.ResolvedFields()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (rd *RecordDatasource) SetProvider(s envoyx.Provider) bool {
|
||||
if rd.Mapping.SourceIdent != s.Ident() {
|
||||
return false
|
||||
}
|
||||
|
||||
rd.Provider = s
|
||||
return true
|
||||
}
|
||||
|
||||
func (rd *RecordDatasource) Next(ctx context.Context, out datasource.RawRecord) (ident []string, more bool, err error) {
|
||||
rowCache := make(datasource.RawRecord)
|
||||
|
||||
more, err = rd.Provider.Next(ctx, rowCache)
|
||||
if err != nil || !more {
|
||||
return
|
||||
}
|
||||
|
||||
rd.applyMapping(rowCache, out)
|
||||
|
||||
if len(rd.Mapping.KeyField) == 0 {
|
||||
ident = append(ident, strconv.FormatInt(int64(rd.currentIndex), 10))
|
||||
} else {
|
||||
for _, k := range rd.Mapping.KeyField {
|
||||
ident = append(ident, strings.Join(rowCache[k].Values, ","))
|
||||
}
|
||||
}
|
||||
|
||||
rd.currentIndex++
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (rd *iteratorProvider) SetConfigs(map[string]any) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (rd *RecordDatasource) Reset(ctx context.Context) (err error) {
|
||||
rd.currentIndex = 0
|
||||
return rd.Provider.Reset(ctx)
|
||||
}
|
||||
|
||||
func (rd *RecordDatasource) applyMapping(in, out datasource.RawRecord) {
|
||||
if len(rd.Mapping.Mapping.Map) == 0 {
|
||||
if !rd.Mapping.Defaultable {
|
||||
return
|
||||
}
|
||||
|
||||
for k, v := range in {
|
||||
out[k] = v
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if rd.Mapping.Defaultable {
|
||||
rd.applyMappingWithDefaults(in, out)
|
||||
} else {
|
||||
rd.applyMappingWoDefaults(in, out)
|
||||
}
|
||||
}
|
||||
|
||||
func (rd *RecordDatasource) applyMappingWithDefaults(in, out datasource.RawRecord) {
|
||||
maps := make(map[string]envoyx.MapEntry)
|
||||
for k, v := range rd.Mapping.Mapping.Map {
|
||||
maps[k] = v
|
||||
}
|
||||
|
||||
for k, v := range in {
|
||||
if m, ok := maps[k]; ok {
|
||||
if m.Skip {
|
||||
continue
|
||||
}
|
||||
out[m.Field] = v
|
||||
} else {
|
||||
out[k] = v
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (rd *RecordDatasource) applyMappingWoDefaults(in, out datasource.RawRecord) {
|
||||
for _, m := range rd.Mapping.Mapping.Map {
|
||||
if m.Skip {
|
||||
continue
|
||||
}
|
||||
|
||||
out[m.Field] = in[m.Column]
|
||||
}
|
||||
}
|
||||
|
||||
func (rd *RecordDatasource) ResolveRef(ref ...any) (out uint64, err error) {
|
||||
idents, err := cast.ToStringSliceE(ref)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
for i, ident := range idents {
|
||||
idents[i] = strings.Replace(ident, "-", "_", -1)
|
||||
}
|
||||
|
||||
out = rd.refToID[strings.Join(idents, "-")]
|
||||
return
|
||||
}
|
||||
|
||||
func (rd *RecordDatasource) ResolveRefS(ref ...string) (out uint64, err error) {
|
||||
aux := make([]any, len(ref))
|
||||
for i, r := range ref {
|
||||
aux[i] = r
|
||||
}
|
||||
|
||||
return rd.ResolveRef(aux...)
|
||||
}
|
||||
|
||||
// @todo this should be replaced by some smarter structure
|
||||
func (rd *RecordDatasource) AddRef(id uint64, idents ...string) {
|
||||
for i, ident := range idents {
|
||||
idents[i] = strings.Replace(ident, "-", "_", -1)
|
||||
}
|
||||
|
||||
rd.refToID[strings.Join(idents, "-")] = id
|
||||
}
|
||||
|
||||
func (ip *iteratorProvider) Next(ctx context.Context, out datasource.RawRecord) (more bool, err error) {
|
||||
if ip.resolveRefs {
|
||||
return ip.nextResolved(ctx, out)
|
||||
}
|
||||
|
||||
return ip.next(ctx, out)
|
||||
}
|
||||
|
||||
func (ip *iteratorProvider) next(ctx context.Context, out datasource.RawRecord) (more bool, err error) {
|
||||
rowCache := make(datasource.RawRecord)
|
||||
|
||||
if !ip.iter.Next(ctx) {
|
||||
return false, ip.iter.Err()
|
||||
}
|
||||
|
||||
err = ip.iter.Scan(rowCache)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Apply field-level access control filtering
|
||||
ip.filterUnreadableFields(rowCache)
|
||||
|
||||
for k, v := range rowCache {
|
||||
out[k] = v
|
||||
}
|
||||
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func (ip *iteratorProvider) nextResolved(ctx context.Context, out datasource.RawRecord) (more bool, err error) {
|
||||
if ip.done && ip.buffIndex >= len(ip.rows) {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
if ip.buffIndex == len(ip.rows) {
|
||||
|
||||
// pull chunk
|
||||
ip.rows = make([]datasource.RawRecord, 0)
|
||||
ip.buffIndex = 0
|
||||
|
||||
for i := 0; i < bufferPullChunkSize; i++ {
|
||||
rowCache := make(datasource.RawRecord)
|
||||
if !ip.iter.Next(ctx) {
|
||||
ip.done = true
|
||||
|
||||
err := ip.iter.Err()
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
break
|
||||
}
|
||||
|
||||
err = ip.iter.Scan(rowCache)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
ip.rows = append(ip.rows, rowCache)
|
||||
}
|
||||
|
||||
if len(ip.rows) == 0 {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// resolve stuff
|
||||
err = ip.resolveReferences(ctx, ip.dal)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
err = ip.resolveUsers(ctx, ip.store)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
rowCache := ip.rows[ip.buffIndex]
|
||||
ip.buffIndex++
|
||||
|
||||
// Apply field-level access control filtering
|
||||
ip.filterUnreadableFields(rowCache)
|
||||
|
||||
for k, v := range rowCache {
|
||||
out[k] = v
|
||||
}
|
||||
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func (ip *iteratorProvider) resolveReferences(ctx context.Context, ds dal.FullService) (err error) {
|
||||
// @todo I'll need to chunk these up for to reduce DB query count...
|
||||
|
||||
for i, cacheRecord := range ip.rows {
|
||||
|
||||
for refField, refWrap := range ip.relMods {
|
||||
value := cacheRecord[refField]
|
||||
if len(value.Values) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
aux := []string{}
|
||||
for _, v := range value.Values {
|
||||
aux = append(aux, fmt.Sprintf("recordID=%s", v))
|
||||
}
|
||||
|
||||
var relRecords types.RecordSet
|
||||
|
||||
resLab := refWrap.labelLvl1
|
||||
qq := fmt.Sprintf("(%s)", strings.Join(aux, " OR "))
|
||||
relRecords, _, err = dalutils.ComposeRecordsList(ctx, ds, refWrap.modLvl1, types.RecordFilter{
|
||||
Query: qq,
|
||||
Paging: filter.Paging{
|
||||
Limit: uint(len(aux)),
|
||||
},
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// lvl 2 nesting; current max lvl
|
||||
if refWrap.modLvl2 != nil {
|
||||
resLab = refWrap.labelLvl2
|
||||
|
||||
// Iterate related records and collect lvl 2 identifiers
|
||||
aux := []string{}
|
||||
for _, rec := range relRecords {
|
||||
for _, v := range rec.Values.FilterByName(refWrap.labelLvl1) {
|
||||
aux = append(aux, fmt.Sprintf("recordID=%s", v.Value))
|
||||
}
|
||||
}
|
||||
|
||||
qq := fmt.Sprintf("(%s)", strings.Join(aux, " OR "))
|
||||
relRecords, _, err = dalutils.ComposeRecordsList(ctx, ds, refWrap.modLvl2, types.RecordFilter{
|
||||
Query: qq,
|
||||
Paging: filter.Paging{
|
||||
Limit: uint(len(aux)),
|
||||
},
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Build a map from recordID to resolved label for ordered assignment
|
||||
labelByID := make(map[string]string, len(relRecords))
|
||||
for _, rec := range relRecords {
|
||||
if rec.Values == nil {
|
||||
continue
|
||||
}
|
||||
v := rec.Values.Get(resLab, 0)
|
||||
if v == nil {
|
||||
continue
|
||||
}
|
||||
labelByID[strconv.FormatUint(rec.ID, 10)] = v.Value
|
||||
}
|
||||
|
||||
for j, origID := range value.Values {
|
||||
label, ok := labelByID[origID]
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
if ip.includeRefID {
|
||||
cacheRecord.SetValue(fmt.Sprintf("%s ID", refField), uint(j), origID)
|
||||
}
|
||||
cacheRecord.SetValue(refField, uint(j), label)
|
||||
}
|
||||
|
||||
ip.rows[i] = cacheRecord
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// @todo consider omitting these from the interface since they're not always needed
|
||||
func (ip *iteratorProvider) Reset(ctx context.Context) (err error) {
|
||||
return
|
||||
}
|
||||
|
||||
// @todo consider omitting these from the interface since they're not always needed
|
||||
func (ip *iteratorProvider) Ident() (out string) {
|
||||
return
|
||||
}
|
||||
|
||||
// @todo consider omitting these from the interface since they're not always needed
|
||||
func (ip *iteratorProvider) SetIdent(string) {
|
||||
}
|
||||
|
||||
func mkRecordRefWrap(ctx context.Context, s store.Storer, f *types.ModuleField) (wrap refModWrap, err error) {
|
||||
relModID := f.Options.UInt64("moduleID")
|
||||
if relModID == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
var relMod *types.Module
|
||||
relMod, err = s.LookupComposeModuleByID(ctx, relModID)
|
||||
if err != nil {
|
||||
// Referenced module may have been deleted or made inaccessible since the
|
||||
// field was configured. Skip ref resolution for this field instead of
|
||||
// failing the entire export — the column is still emitted, just unresolved.
|
||||
if errors.IsNotFound(err) {
|
||||
logger.Default().Warn(
|
||||
"skipping record ref resolution: referenced module not found",
|
||||
zap.String("field", f.Name),
|
||||
zap.Uint64("moduleID", relModID),
|
||||
)
|
||||
err = nil
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
relMod.Fields, _, err = s.SearchComposeModuleFields(ctx, types.ModuleFieldFilter{
|
||||
ModuleID: []uint64{relMod.ID},
|
||||
})
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
wrap = refModWrap{
|
||||
modLvl1: relMod,
|
||||
labelLvl1: f.Options.String("labelField"),
|
||||
}
|
||||
|
||||
if f.Options.String("recordLabelField") != "" {
|
||||
// Two-level label resolution: the labelField on the related module must
|
||||
// itself be a Record field pointing to another module. If that chain is
|
||||
// broken (labelField renamed/removed, target module missing), skip the
|
||||
// nested resolution instead of aborting the export.
|
||||
labelFieldName := f.Options.String("labelField")
|
||||
nestedRef := wrap.modLvl1.Fields.FindByName(labelFieldName)
|
||||
if nestedRef == nil {
|
||||
logger.Default().Warn(
|
||||
"skipping nested record ref resolution: labelField not found on related module",
|
||||
zap.String("field", f.Name),
|
||||
zap.String("labelField", labelFieldName),
|
||||
zap.Uint64("relatedModuleID", relMod.ID),
|
||||
)
|
||||
return
|
||||
}
|
||||
nestedModID := nestedRef.Options.UInt64("moduleID")
|
||||
if nestedModID == 0 {
|
||||
logger.Default().Warn(
|
||||
"skipping nested record ref resolution: labelField has no moduleID option",
|
||||
zap.String("field", f.Name),
|
||||
zap.String("labelField", labelFieldName),
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
wrap.modLvl2, err = s.LookupComposeModuleByID(ctx, nestedModID)
|
||||
if err != nil {
|
||||
if errors.IsNotFound(err) {
|
||||
logger.Default().Warn(
|
||||
"skipping nested record ref resolution: nested module not found",
|
||||
zap.String("field", f.Name),
|
||||
zap.Uint64("nestedModuleID", nestedModID),
|
||||
)
|
||||
err = nil
|
||||
}
|
||||
return
|
||||
}
|
||||
wrap.labelLvl2 = f.Options.String("recordLabelField")
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (ip *iteratorProvider) resolveUsers(ctx context.Context, s store.Storer) (err error) {
|
||||
if len(ip.userFields) == 0 || s == nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Collect unique user IDs across the current chunk
|
||||
seen := make(map[string]bool)
|
||||
for _, row := range ip.rows {
|
||||
for fieldName := range ip.userFields {
|
||||
for _, val := range row[fieldName].Values {
|
||||
if val != "" && val != "0" {
|
||||
seen[val] = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if len(seen) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
ids := make([]string, 0, len(seen))
|
||||
for id := range seen {
|
||||
ids = append(ids, id)
|
||||
}
|
||||
|
||||
uu, _, err := store.SearchUsers(ctx, s, systemTypes.UserFilter{
|
||||
UserID: ids,
|
||||
Paging: filter.Paging{Limit: 0},
|
||||
Deleted: filter.StateInclusive,
|
||||
Suspended: filter.StateInclusive,
|
||||
})
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
labels := make(map[string]string, len(uu))
|
||||
for _, u := range uu {
|
||||
labels[strconv.FormatUint(u.ID, 10)] = userLabel(u)
|
||||
}
|
||||
|
||||
for i, row := range ip.rows {
|
||||
for fieldName := range ip.userFields {
|
||||
v := row[fieldName]
|
||||
if len(v.Values) == 0 {
|
||||
continue
|
||||
}
|
||||
for j, val := range v.Values {
|
||||
if val == "0" || val == "" {
|
||||
row.SetValue(fieldName, uint(j), "")
|
||||
continue
|
||||
}
|
||||
if label, ok := labels[val]; ok {
|
||||
if ip.includeRefID {
|
||||
row.SetValue(fmt.Sprintf("%s ID", fieldName), uint(j), val)
|
||||
}
|
||||
row.SetValue(fieldName, uint(j), label)
|
||||
}
|
||||
}
|
||||
}
|
||||
ip.rows[i] = row
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func userLabel(u *systemTypes.User) string {
|
||||
if u.Name != "" {
|
||||
return u.Name
|
||||
}
|
||||
if u.Email != "" {
|
||||
return u.Email
|
||||
}
|
||||
if u.Handle != "" {
|
||||
return u.Handle
|
||||
}
|
||||
return strconv.FormatUint(u.ID, 10)
|
||||
}
|
||||
|
||||
// filterUnreadableFields removes values the given user does not have access to
|
||||
func (ip *iteratorProvider) filterUnreadableFields(rowCache datasource.RawRecord) {
|
||||
if reflect2.IsNil(ip.ac) {
|
||||
return
|
||||
}
|
||||
|
||||
for fieldName := range rowCache {
|
||||
// Skip system fields (ID, createdAt, etc.) - these are always readable
|
||||
// System fields are lowercase and don't have associated module field definitions
|
||||
if _, hasField := ip.readableFields[fieldName]; !hasField {
|
||||
// Field not in module definition, likely a system field - keep it
|
||||
continue
|
||||
}
|
||||
|
||||
// Check if user can read this field
|
||||
if !ip.readableFields[fieldName] {
|
||||
delete(rowCache, fieldName)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user