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:
472
server/compose/service/values/validator.go
Normal file
472
server/compose/service/values/validator.go
Normal 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
|
||||
}
|
||||
Reference in New Issue
Block a user