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

This commit is contained in:
upstream-mirror
2026-09-18 01:22:23 +05:00
commit 7e385fc126
8391 changed files with 1766236 additions and 0 deletions

View File

@@ -0,0 +1,472 @@
package values
import (
"context"
"fmt"
"math/big"
"net/mail"
"net/url"
"strconv"
"strings"
"time"
"github.com/cortezaproject/corteza/server/pkg/locale"
"github.com/cortezaproject/corteza/server/compose/types"
"github.com/cortezaproject/corteza/server/pkg/expr"
"github.com/cortezaproject/corteza/server/pkg/slice"
"github.com/cortezaproject/corteza/server/store"
)
// Validator package provides tooling to validate
// record and it's values against field constraints
//
// Structures and basic logic is similar to what we offer on the frontend
// (see corteza-js/validator package) but with less features as there
// is no need for such level of interaction and dynamic we require on the frontend
type (
UniqueChecker func(context.Context, store.Storer, *types.RecordValue, *types.ModuleField, *types.Module) (uint64, error)
ReferenceChecker func(context.Context, store.Storer, *types.RecordValue, *types.ModuleField, *types.Module) (bool, error)
localeService interface {
T(ctx context.Context, ns, key string, rr ...string) string
TResource(ctx context.Context, ns, key string, rr ...string) string
}
validator struct {
uniqueCheckerFn UniqueChecker
recordRefCheckerFn ReferenceChecker
userRefCheckerFn ReferenceChecker
fileRefCheckerFn ReferenceChecker
localeSvc localeService
now func() time.Time
}
)
func makeInternalErr(field *types.ModuleField, err error) types.RecordValueError {
return types.RecordValueError{
Kind: "internal",
Message: err.Error(),
Meta: map[string]interface{}{"field": field.Name},
}
}
func makeEmptyErr(ctx context.Context, field *types.ModuleField, ls localeService) types.RecordValueError {
return types.RecordValueError{
Kind: "empty",
Meta: map[string]interface{}{"field": field.Name},
Message: ls.T(ctx, "compose", "record-field.errors.empty"),
}
}
func makeInvalidValueErr(ctx context.Context, field *types.ModuleField, value string, ls localeService) types.RecordValueError {
return types.RecordValueError{
Kind: "invalidValue",
Meta: map[string]interface{}{"field": field.Name, "value": value},
Message: ls.T(ctx, "compose", "record-field.errors.invalidValue"),
}
}
func makeInvalidRefErr(ctx context.Context, field *types.ModuleField, ref uint64, ls localeService) types.RecordValueError {
return types.RecordValueError{
Kind: "invalidRef",
Meta: map[string]interface{}{"field": field.Name, "ref": ref},
Message: ls.T(ctx, "compose", "record-field.errors.invalidRef"),
}
}
func makeDuplicateValueInSetErr(ctx context.Context, field *types.ModuleField, value string, ls localeService) types.RecordValueError {
return types.RecordValueError{
Kind: "duplicateValueInSet",
Meta: map[string]interface{}{"field": field.Name, "value": value},
Message: ls.T(ctx, "compose", "record-field.errors.duplicateValueInSet"),
}
}
func makeDuplicateValueErr(ctx context.Context, field *types.ModuleField, recordID uint64, ls localeService) types.RecordValueError {
return types.RecordValueError{
Kind: "duplicateValue",
Meta: map[string]interface{}{"field": field.Name, "recordID": recordID},
Message: ls.T(ctx, "compose", "record-field.errors.duplicateValue"),
}
}
// Simple wrapper for easier error returning from validation functions
func e2s(ee ...types.RecordValueError) []types.RecordValueError {
return ee
}
func Validator() *validator {
return &validator{
now: func() time.Time { return time.Now() },
localeSvc: locale.Global(),
}
}
func (vldtr *validator) UniqueChecker(fn UniqueChecker) {
vldtr.uniqueCheckerFn = fn
}
func (vldtr *validator) RecordRefChecker(fn ReferenceChecker) {
vldtr.recordRefCheckerFn = fn
}
func (vldtr *validator) UserRefChecker(fn ReferenceChecker) {
vldtr.userRefCheckerFn = fn
}
func (vldtr *validator) FileRefChecker(fn ReferenceChecker) {
vldtr.fileRefCheckerFn = fn
}
// Run validates record and it's values against module & module fields options
//
// Validation is done in phases for optimal resource usage:
// - check if required values are present
// - check for unique-multi-value in multi value fields
// - field-kind specific validation on all values
// - unique check on all values
func (vldtr validator) Run(ctx context.Context, s store.Storer, m *types.Module, r *types.Record) (out *types.RecordValueErrorSet) {
var (
f *types.ModuleField
valParser = expr.Parser()
valDict = r.Values.Dict(m.Fields)
)
out = &types.RecordValueErrorSet{}
fields:
for _, f := range m.Fields {
vv := r.Values.FilterByName(f.Name)
if f.Required {
if len(vv) == 0 {
out.Push(makeEmptyErr(ctx, f, vldtr.localeSvc))
continue fields
}
for _, v := range vv {
if len(v.Value) == 0 || (f.IsRef() && v.Ref == 0) {
out.Push(makeEmptyErr(ctx, f, vldtr.localeSvc))
continue fields
}
}
}
if f.Multi && f.Options.IsUniqueMultiValue() {
flipped := make(map[string]bool)
for _, v := range vv {
if flipped[v.Value] {
out.Push(makeDuplicateValueInSetErr(ctx, f, v.Value, vldtr.localeSvc))
continue fields
}
flipped[v.Value] = true
}
}
}
for _, v := range r.Values {
if !v.IsUpdated() || v.IsDeleted() {
// We'll validate only updated (and non-deleted) values
continue
}
if f = m.Fields.FindByName(v.Name); f == nil {
continue
}
if f.Expressions.ValueExpr != "" {
// do not do any validation if field has value expression!
continue
}
if !(f.Expressions.DisableDefaultValidators && len(f.Expressions.Validators) > 0) {
if v.Value == "" {
// Nothing to do with empty value
return nil
}
// Per field type validators
switch strings.ToLower(f.Kind) {
case "bool":
out.Push(vldtr.vBool(ctx, v, f, r, m)...)
case "datetime":
out.Push(vldtr.vDatetime(ctx, v, f, r, m)...)
case "email":
out.Push(vldtr.vEmail(ctx, v, f, r, m)...)
case "file":
out.Push(vldtr.vFile(ctx, s, v, f, r, m)...)
case "number":
out.Push(vldtr.vNumber(ctx, v, f, r, m)...)
case "record":
out.Push(vldtr.vRecord(ctx, s, v, f, r, m)...)
case "select":
out.Push(vldtr.vSelect(ctx, v, f, r, m)...)
//case "string":
// out.Push(vldtr.vString(v, f, r, m)...)
case "url":
out.Push(vldtr.vUrl(ctx, v, f, r, m)...)
case "user":
out.Push(vldtr.vUser(ctx, s, v, f, r, m)...)
}
}
if len(f.Expressions.Validators) > 0 {
for i, cv := range f.Expressions.Validators {
eval, err := valParser.NewEvaluable(cv.Test)
if err != nil {
out.Push(makeInternalErr(f, err))
break
}
invalid, err := eval.EvalBool(ctx, map[string]interface{}{
"value": v.Value,
"oldValue": v.OldValue,
"values": valDict,
})
if err != nil {
out.Push(makeInternalErr(f, err))
break
}
if invalid {
message := cv.Error
// In case tests mit this for easier testing; falling back to whatever
// the field validator has stored.
if vldtr.localeSvc != nil {
validatorID := locale.ContentID(cv.ValidatorID, i)
rpl := strings.NewReplacer(
"{{validatorID}}", strconv.FormatUint(validatorID, 10),
)
k := rpl.Replace(types.LocaleKeyModuleFieldExpressionValidatorValidatorIDError.Path)
message = vldtr.localeSvc.TResource(ctx, f.ResourceTranslation(), k)
}
out.Push(types.RecordValueError{
Kind: "error",
Message: message,
Meta: map[string]interface{}{"field": f.Name}},
)
// break at first failed test
break
}
}
}
}
// This is the most resource-heavy operation
// we'll do in at the end
for _, v := range r.Values {
if f = m.Fields.FindByName(v.Name); f == nil {
continue
}
if !f.Options.IsUnique() {
// Only interested in unique fields
continue
}
if vldtr.uniqueCheckerFn == nil {
return nil
}
duplicateRecordID, err := vldtr.uniqueCheckerFn(ctx, s, v, f, m)
if err != nil {
out.Push(makeInternalErr(f, err))
} else if duplicateRecordID > 0 && duplicateRecordID != r.ID {
out.Push(makeDuplicateValueErr(ctx, f, duplicateRecordID, vldtr.localeSvc))
}
}
if out.IsValid() {
return nil
}
return out
}
func (vldtr validator) vBool(ctx context.Context, v *types.RecordValue, f *types.ModuleField, r *types.Record, m *types.Module) []types.RecordValueError {
if v.Value == "" {
return nil
}
if v.Value != strBoolTrue && v.Value != strBoolFalse && v.Value != strBoolFalseAlt {
return e2s(makeInvalidValueErr(ctx, f, v.Value, vldtr.localeSvc))
}
return nil
}
func (vldtr validator) vDatetime(ctx context.Context, v *types.RecordValue, f *types.ModuleField, r *types.Record, m *types.Module) []types.RecordValueError {
var (
inputFormat string
t time.Time
err error
// We'll validate against this value
refTime = vldtr.now()
)
if f.Options.Bool(fieldOpt_Datetime_onlyDate) {
inputFormat = datetimeInternalFormatDate
// Round down ref time to midnight
refTime = time.Date(refTime.Year(), refTime.Month(), refTime.Day(), 0, 0, 0, 0, refTime.Location())
} else if f.Options.Bool(fieldOpt_Datetime_onlyTime) {
inputFormat = datetimeIntenralFormatTime
// Round down ref time to day one
refTime = time.Date(0, 1, 1, refTime.Hour(), refTime.Minute(), refTime.Second(), refTime.Nanosecond(), refTime.Location())
} else {
inputFormat = datetimeInternalFormatFull
}
t, err = time.Parse(inputFormat, v.Value)
if err != nil {
return e2s(makeInvalidValueErr(ctx, f, v.Value, vldtr.localeSvc))
}
if f.Options.Bool(fieldOpt_Datetime_onlyFutureValues) {
if !t.After(refTime) {
return e2s(makeInvalidValueErr(ctx, f, v.Value, vldtr.localeSvc))
}
} else if f.Options.Bool(fieldOpt_Datetime_onlyPastValues) {
if !t.Before(refTime) {
return e2s(makeInvalidValueErr(ctx, f, v.Value, vldtr.localeSvc))
}
}
// @todo check past/future
return nil
}
func (vldtr validator) vEmail(ctx context.Context, v *types.RecordValue, f *types.ModuleField, r *types.Record, m *types.Module) []types.RecordValueError {
if _, err := mail.ParseAddress(v.Value); err != nil {
return e2s(makeInvalidValueErr(ctx, f, v.Value, vldtr.localeSvc))
}
return nil
}
func (vldtr validator) vFile(ctx context.Context, s store.Storer, v *types.RecordValue, f *types.ModuleField, r *types.Record, m *types.Module) []types.RecordValueError {
if vldtr.fileRefCheckerFn == nil {
return nil
}
if ok, err := vldtr.fileRefCheckerFn(ctx, s, v, f, m); err != nil {
return e2s(makeInternalErr(f, err))
} else if !ok {
return e2s(makeInvalidRefErr(ctx, f, v.Ref, vldtr.localeSvc))
}
return nil
}
func (vldtr validator) vNumber(ctx context.Context, v *types.RecordValue, f *types.ModuleField, r *types.Record, m *types.Module) []types.RecordValueError {
// @todo !! This is temporary; precision and scale require a rework
if _, _, err := big.ParseFloat(v.Value, 0, maxPrecision, big.ToNearestEven); err != nil {
return e2s(makeInvalidValueErr(ctx, f, v.Value, vldtr.localeSvc))
}
return nil
}
func (vldtr validator) vRecord(ctx context.Context, s store.Storer, v *types.RecordValue, f *types.ModuleField, r *types.Record, m *types.Module) []types.RecordValueError {
if vldtr.recordRefCheckerFn == nil {
return nil
}
if ok, err := vldtr.recordRefCheckerFn(ctx, s, v, f, m); err != nil {
return e2s(makeInternalErr(f, err))
} else if !ok {
return e2s(makeInvalidRefErr(ctx, f, v.Ref, vldtr.localeSvc))
}
return nil
}
func (vldtr validator) vSelect(ctx context.Context, v *types.RecordValue, f *types.ModuleField, r *types.Record, m *types.Module) []types.RecordValueError {
var (
sbm = make(map[string]bool)
options, has = f.Options["options"]
)
if !has {
return nil
}
switch oo := options.(type) {
case []string:
sbm = slice.ToStringBoolMap(oo)
case []interface{}:
for _, o := range oo {
switch c := o.(type) {
case string:
sbm[c] = true
case map[string]string:
if value, has := c["value"]; has {
sbm[value] = true
}
case map[string]interface{}:
if value, has := c["value"]; has {
if value, ok := value.(string); ok {
sbm[value] = true
}
}
case types.ModuleFieldOptions:
if value, has := c["value"]; has {
if value, ok := value.(string); ok {
sbm[value] = true
}
}
}
}
}
if len(sbm) == 0 {
return e2s(makeInternalErr(f, fmt.Errorf("invalid select options definition")))
}
if !sbm[v.Value] {
return e2s(makeInvalidValueErr(ctx, f, v.Value, vldtr.localeSvc))
}
return nil
}
//func (vldtr validator) vString(v *types.RecordValue, f *types.ModuleField, r *types.Record, m *types.Module) []types.RecordValueError {
// return nil
//}
func (vldtr validator) vUrl(ctx context.Context, v *types.RecordValue, f *types.ModuleField, r *types.Record, m *types.Module) []types.RecordValueError {
if p, err := url.Parse(v.Value); err != nil {
return e2s(makeInvalidValueErr(ctx, f, v.Value, vldtr.localeSvc))
} else if p.Scheme == "" || p.Host == "" {
return e2s(makeInvalidValueErr(ctx, f, v.Value, vldtr.localeSvc))
} else if f.Options.Bool(fieldOpt_Url_onlySecure) && p.Scheme != "https" {
return e2s(makeInvalidValueErr(ctx, f, v.Value, vldtr.localeSvc))
}
return nil
}
func (vldtr validator) vUser(ctx context.Context, s store.Storer, v *types.RecordValue, f *types.ModuleField, r *types.Record, m *types.Module) []types.RecordValueError {
if vldtr.userRefCheckerFn == nil {
return nil
}
if ok, err := vldtr.userRefCheckerFn(ctx, s, v, f, m); err != nil {
return e2s(makeInternalErr(f, err))
} else if !ok {
return e2s(makeInvalidRefErr(ctx, f, v.Ref, vldtr.localeSvc))
}
return nil
}