Fix deduplication of unique values in module fields
This commit is contained in:
committed by
Jože Fortun
parent
08fd291cf6
commit
32d09e5afc
@@ -52,19 +52,20 @@
|
||||
</b-input-group>
|
||||
</multi>
|
||||
|
||||
<b-input-group
|
||||
v-else
|
||||
:prepend="field.options.prefix"
|
||||
:append="field.options.suffix"
|
||||
>
|
||||
<b-form-input
|
||||
v-model="value"
|
||||
autocomplete="off"
|
||||
type="number"
|
||||
number
|
||||
/>
|
||||
</b-input-group>
|
||||
<errors :errors="errors" />
|
||||
<template v-else>
|
||||
<b-input-group
|
||||
:prepend="field.options.prefix"
|
||||
:append="field.options.suffix"
|
||||
>
|
||||
<b-form-input
|
||||
v-model="value"
|
||||
autocomplete="off"
|
||||
type="number"
|
||||
number
|
||||
/>
|
||||
</b-input-group>
|
||||
<errors :errors="errors" />
|
||||
</template>
|
||||
</b-form-group>
|
||||
</template>
|
||||
<script>
|
||||
|
||||
@@ -39,6 +39,8 @@
|
||||
:options="selectOptions"
|
||||
stacked
|
||||
/>
|
||||
|
||||
<errors :errors="errors" />
|
||||
</template>
|
||||
|
||||
<multi
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
<span
|
||||
:class="{ 'text-primary': error.kind.includes('warning') }"
|
||||
>
|
||||
{{ $t(error.message, { value: error.meta.value }) }}
|
||||
{{ error.message }}
|
||||
</span>
|
||||
</b-form-invalid-feedback>
|
||||
</div>
|
||||
|
||||
@@ -50,19 +50,21 @@ interface Options {
|
||||
positionField?: string;
|
||||
refField?: string; // When adding a new record, prefill refField value with parent record ID
|
||||
editFields?: unknown[];
|
||||
linkToParent: boolean; // Legacy
|
||||
|
||||
|
||||
// When adding a new record, link it to parent when available
|
||||
linkToParent: boolean;
|
||||
|
||||
// Should records be opened in a new tab
|
||||
// legacy field that has been removed but we keep it for backwards compatibility
|
||||
openInNewTab: boolean;
|
||||
|
||||
|
||||
// Are table rows selectable
|
||||
selectable: boolean;
|
||||
selectMode: 'multi' | 'single' | 'range';
|
||||
|
||||
|
||||
// Ordered list of buttons to display in the block
|
||||
selectionButtons: Array<Button>;
|
||||
|
||||
|
||||
bulkRecordEditEnabled: boolean;
|
||||
inlineRecordEditEnabled: boolean;
|
||||
filterPresets: FilterPreset[];
|
||||
|
||||
@@ -2,5 +2,4 @@ errors:
|
||||
empty: This field is required
|
||||
invalidValue: Invalid field value
|
||||
invalidRef: Invalid field reference
|
||||
duplicateValueInSet: This value already exists in list
|
||||
duplicateValue: The value "{{value}}" already exists in another record
|
||||
duplicateValueInSet: This value already exists in list
|
||||
@@ -359,7 +359,7 @@ func (set RecordBulkSet) ToBulkOperations(dftModule uint64, dftNamespace uint64)
|
||||
}
|
||||
|
||||
// GetValuesByName filters values for records by names
|
||||
func (set RecordSet) GetValuesByName(names ...string) (out RecordValueSet) {
|
||||
func (set RecordSet) GetValuesByName(names ...string) (out map[uint64]RecordValueSet) {
|
||||
nameMap := make(map[string]bool)
|
||||
for _, n := range names {
|
||||
if len(n) > 0 {
|
||||
@@ -367,11 +367,13 @@ func (set RecordSet) GetValuesByName(names ...string) (out RecordValueSet) {
|
||||
}
|
||||
}
|
||||
|
||||
out = make(map[uint64]RecordValueSet)
|
||||
err := set.Walk(func(rec *Record) error {
|
||||
_ = rec.Values.Walk(func(val *RecordValue) error {
|
||||
if val != nil && nameMap[val.Name] {
|
||||
val.RecordID = rec.ID
|
||||
out = append(out, val)
|
||||
|
||||
out[val.RecordID] = append(out[val.RecordID], val)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
@@ -1,265 +1,337 @@
|
||||
package types
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/cortezaproject/corteza/server/pkg/locale"
|
||||
"github.com/cortezaproject/corteza/server/pkg/str"
|
||||
"github.com/spf13/cast"
|
||||
"github.com/cortezaproject/corteza/server/pkg/locale"
|
||||
"github.com/cortezaproject/corteza/server/pkg/str"
|
||||
"github.com/spf13/cast"
|
||||
)
|
||||
|
||||
type (
|
||||
deDup struct {
|
||||
ls localeService
|
||||
}
|
||||
deDup struct {
|
||||
ls localeService
|
||||
}
|
||||
|
||||
localeService interface {
|
||||
T(ctx context.Context, ns, key string, rr ...string) string
|
||||
}
|
||||
localeService interface {
|
||||
T(ctx context.Context, ns, key string, rr ...string) string
|
||||
}
|
||||
|
||||
DeDupRule struct {
|
||||
Name DeDupRuleName `json:"name"`
|
||||
Strict bool `json:"strict"`
|
||||
ErrorMessage string `json:"errorMessage"`
|
||||
ConstraintSet DeDupRuleConstraintSet `json:"constraints"`
|
||||
}
|
||||
DeDupRule struct {
|
||||
Name DeDupRuleName `json:"name"`
|
||||
Strict bool `json:"strict"`
|
||||
ErrorMessage string `json:"errorMessage"`
|
||||
ConstraintSet DeDupRuleConstraintSet `json:"constraints"`
|
||||
}
|
||||
|
||||
DeDupRuleConstraint struct {
|
||||
Attribute string `json:"attribute"`
|
||||
Modifier DeDupValueModifier `json:"modifier"`
|
||||
MultiValue DeDupMultiValueConstraint `json:"multiValue"`
|
||||
}
|
||||
DeDupRuleConstraint struct {
|
||||
Attribute string `json:"attribute"`
|
||||
Modifier DeDupValueModifier `json:"modifier"`
|
||||
MultiValue DeDupMultiValueConstraint `json:"multiValue"`
|
||||
}
|
||||
|
||||
DeDupRuleConstraintSet []*DeDupRuleConstraint
|
||||
DeDupRuleConstraintSet []*DeDupRuleConstraint
|
||||
|
||||
// DeDupRuleName represent the identifier for duplicate detection rule
|
||||
DeDupRuleName string
|
||||
// DeDupRuleName represent the identifier for duplicate detection rule
|
||||
DeDupRuleName string
|
||||
|
||||
// DeDupValueModifier represent the algorithm used to check value string
|
||||
DeDupValueModifier string
|
||||
// DeDupValueModifier represent the algorithm used to check value string
|
||||
DeDupValueModifier string
|
||||
|
||||
// DeDupMultiValueConstraint for matching multi values accordingly
|
||||
DeDupMultiValueConstraint string
|
||||
// DeDupMultiValueConstraint for matching multi values accordingly
|
||||
DeDupMultiValueConstraint string
|
||||
|
||||
// DeDupIssueKind based on strict mode rule or duplication config
|
||||
DeDupIssueKind string
|
||||
// DeDupIssueKind based on strict mode rule or duplication config
|
||||
DeDupIssueKind string
|
||||
)
|
||||
|
||||
const (
|
||||
ignoreCase DeDupValueModifier = "ignore-case"
|
||||
caseSensitive DeDupValueModifier = "case-sensitive"
|
||||
fuzzyMatch DeDupValueModifier = "fuzzy-match"
|
||||
soundsLike DeDupValueModifier = "sounds-like"
|
||||
ignoreCase DeDupValueModifier = "ignore-case"
|
||||
caseSensitive DeDupValueModifier = "case-sensitive"
|
||||
fuzzyMatch DeDupValueModifier = "fuzzy-match"
|
||||
soundsLike DeDupValueModifier = "sounds-like"
|
||||
|
||||
oneOf DeDupMultiValueConstraint = "one-of"
|
||||
equal DeDupMultiValueConstraint = "equal"
|
||||
oneOf DeDupMultiValueConstraint = "one-of"
|
||||
equal DeDupMultiValueConstraint = "equal"
|
||||
|
||||
deDupWarning DeDupIssueKind = "duplication_warning"
|
||||
deDupError DeDupIssueKind = "duplication_error"
|
||||
deDupWarning DeDupIssueKind = "duplication_warning"
|
||||
deDupError DeDupIssueKind = "duplication_error"
|
||||
)
|
||||
|
||||
func DeDup() *deDup {
|
||||
return &deDup{
|
||||
ls: locale.Global(),
|
||||
}
|
||||
return &deDup{
|
||||
ls: locale.Global(),
|
||||
}
|
||||
}
|
||||
|
||||
func (d deDup) CheckDuplication(ctx context.Context, rules DeDupRuleSet, rec Record, rr RecordSet) (out *RecordValueErrorSet, err error) {
|
||||
out = &RecordValueErrorSet{}
|
||||
err = rules.Walk(func(rule *DeDupRule) error {
|
||||
if rule.HasAttributes() {
|
||||
values := rr.GetValuesByName(distinct(rule.Attributes())...)
|
||||
out = &RecordValueErrorSet{}
|
||||
err = rules.Walk(func(rule *DeDupRule) error {
|
||||
if rule.HasAttributes() {
|
||||
values := rr.GetValuesByName(distinct(rule.Attributes())...)
|
||||
|
||||
set := rule.validateValue(ctx, d.ls, rec, values)
|
||||
for valID, value := range values {
|
||||
if valID == rec.ID {
|
||||
continue
|
||||
}
|
||||
|
||||
if !set.IsValid() {
|
||||
out.Push(set.Set...)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
set := rule.validateValue(ctx, d.ls, rec, value)
|
||||
|
||||
if out.IsValid() {
|
||||
out = nil
|
||||
}
|
||||
return
|
||||
if !set.IsValid() {
|
||||
out.Push(set.Set...)
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if out.IsValid() {
|
||||
out = nil
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (rule DeDupIssueKind) String() string {
|
||||
return string(rule)
|
||||
return string(rule)
|
||||
}
|
||||
|
||||
func (rule DeDupRule) HasAttributes() bool {
|
||||
return len(rule.ConstraintSet) > 0 && len(rule.Attributes()) > 0
|
||||
return len(rule.ConstraintSet) > 0 && len(rule.Attributes()) > 0
|
||||
}
|
||||
|
||||
func (rule DeDupRule) Attributes() (out []string) {
|
||||
for _, c := range rule.ConstraintSet {
|
||||
out = append(out, c.Attribute)
|
||||
}
|
||||
return
|
||||
for _, c := range rule.ConstraintSet {
|
||||
out = append(out, c.Attribute)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (rule DeDupRule) IsStrict() bool {
|
||||
return rule.Strict
|
||||
return rule.Strict
|
||||
}
|
||||
|
||||
func (rule DeDupRule) IssueKind() string {
|
||||
out := deDupWarning
|
||||
if rule.Strict {
|
||||
out = deDupError
|
||||
}
|
||||
out := deDupWarning
|
||||
if rule.Strict {
|
||||
out = deDupError
|
||||
}
|
||||
|
||||
return out.String()
|
||||
return out.String()
|
||||
}
|
||||
|
||||
func (rule DeDupRule) IssueMessage() (out string) {
|
||||
return "record-field.errors.duplicateValue"
|
||||
func (rule DeDupRule) IssueMessage(value string) (out string) {
|
||||
return fmt.Sprintf("The value %s already exists in another record", value)
|
||||
}
|
||||
|
||||
func (rule DeDupRule) IssueMultivalueMessage(values []string) (out string) {
|
||||
return fmt.Sprintf("The values [%s] already exist in another record", strings.Join(values, ", "))
|
||||
}
|
||||
|
||||
func (rule DeDupRule) String() string {
|
||||
return fmt.Sprintf("%s duplicate detection on `%s` field", rule.Name, strings.Join(rule.Attributes(), ", "))
|
||||
return fmt.Sprintf("%s duplicate detection on `%s` field", rule.Name, strings.Join(rule.Attributes(), ", "))
|
||||
}
|
||||
|
||||
// validateValue will check duplicate detection based on rules name
|
||||
func (rule DeDupRule) validateValue(ctx context.Context, ls localeService, rec Record, vv RecordValueSet) (out *RecordValueErrorSet) {
|
||||
return rule.checkCaseSensitiveDuplication(ctx, ls, rec, vv)
|
||||
return rule.checkDuplication(ctx, ls, rec, vv)
|
||||
}
|
||||
|
||||
func (rule DeDupRule) checkCaseSensitiveDuplication(ctx context.Context, ls localeService, rec Record, vv RecordValueSet) (out *RecordValueErrorSet) {
|
||||
var (
|
||||
recVal = rec.Values
|
||||
)
|
||||
func (rule DeDupRule) checkDuplication(ctx context.Context, ls localeService, rec Record, vv RecordValueSet) (out *RecordValueErrorSet) {
|
||||
var (
|
||||
recVal = rec.Values
|
||||
)
|
||||
|
||||
for _, c := range rule.ConstraintSet {
|
||||
rvv := recVal.FilterByName(c.Attribute)
|
||||
if rvv.Len() == 0 {
|
||||
continue
|
||||
}
|
||||
for _, c := range rule.ConstraintSet {
|
||||
rvv := recVal.FilterByName(c.Attribute)
|
||||
if rvv.Len() == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
var (
|
||||
valErr = &RecordValueErrorSet{}
|
||||
)
|
||||
var (
|
||||
valErr = &RecordValueErrorSet{}
|
||||
)
|
||||
|
||||
_ = vv.Walk(func(v *RecordValue) error {
|
||||
if v.RecordID != rec.ID {
|
||||
_ = rvv.Walk(func(rv *RecordValue) error {
|
||||
if len(rv.Value) > 0 && matchValue(c.Modifier, rv.Value, v.Value) {
|
||||
valErr.Push(RecordValueError{
|
||||
Kind: rule.IssueKind(),
|
||||
Message: ls.T(ctx, "compose", rule.IssueMessage()),
|
||||
Meta: map[string]interface{}{
|
||||
"field": v.Name,
|
||||
"value": v.Value,
|
||||
"dupValueField": rv.Name,
|
||||
"recordID": cast.ToString(v.RecordID),
|
||||
"rule": rule.String(),
|
||||
},
|
||||
})
|
||||
}
|
||||
return nil
|
||||
})
|
||||
existingVv := vv.FilterByName(c.Attribute)
|
||||
moduleField := rec.module.Fields.FindByName(c.Attribute)
|
||||
|
||||
// 1. multiValue is empty, then all value needs to be a match then return error/warning
|
||||
// 2. multiValue is oneOf, then one or more value needs to be a match then return error/warning
|
||||
// 3. multiValue is equal, then all value needs to be a match then return error/warning
|
||||
if (!valErr.IsValid() && (!c.HasMultiValue() || c.IsAllEqual()) && valErr.Len() == rvv.Len()) || (c.IsOneOf() && valErr.Len() > 0) {
|
||||
if out == nil {
|
||||
out = &RecordValueErrorSet{}
|
||||
}
|
||||
out.Push(valErr.Set...)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
if moduleField.Multi && c.IsAllEqual() {
|
||||
return rule.multiValueAllEqual(ctx, ls, c, rvv, existingVv)
|
||||
}
|
||||
|
||||
return
|
||||
_ = vv.Walk(func(v *RecordValue) error {
|
||||
if v.RecordID != rec.ID {
|
||||
_ = rvv.Walk(func(rv *RecordValue) error {
|
||||
if len(rv.Value) > 0 && matchValue(c.Modifier, rv.Value, v.Value) {
|
||||
valErr.Push(RecordValueError{
|
||||
Kind: rule.IssueKind(),
|
||||
Message: ls.T(ctx, "compose", rule.IssueMessage(v.Value)),
|
||||
Meta: map[string]interface{}{
|
||||
"field": v.Name,
|
||||
"value": v.Value,
|
||||
"dupValueField": rv.Name,
|
||||
"recordID": cast.ToString(v.RecordID),
|
||||
"rule": rule.String(),
|
||||
},
|
||||
})
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
// 1. multiValue is empty, then all value needs to be a match then return error/warning
|
||||
// 2. multiValue is oneOf, then one or more value needs to be a match then return error/warning
|
||||
// 3. multiValue is equal, then all value needs to be a match then return error/warning
|
||||
if (!valErr.IsValid() && (!c.HasMultiValue() || c.IsAllEqual()) && valErr.Len() == rvv.Len()) || (c.IsOneOf() && valErr.Len() > 0) {
|
||||
if out == nil {
|
||||
out = &RecordValueErrorSet{}
|
||||
}
|
||||
out.Push(valErr.Set...)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (rule DeDupRule) multiValueAllEqual(ctx context.Context, ls localeService, c *DeDupRuleConstraint, rvv RecordValueSet, existingVv RecordValueSet) (out *RecordValueErrorSet) {
|
||||
var (
|
||||
valErr = &RecordValueErrorSet{}
|
||||
)
|
||||
|
||||
if rvv.Len() == existingVv.Len() {
|
||||
rvvmap := make(map[string]int)
|
||||
existingVvmap := make(map[string]int)
|
||||
|
||||
dupValues := recordValueFrequencyMap(rvvmap, c.Modifier, rvv)
|
||||
_ = recordValueFrequencyMap(existingVvmap, c.Modifier, existingVv)
|
||||
|
||||
if matchRecordValueFrequencyMap(rvvmap, existingVvmap) {
|
||||
valErr.Push(RecordValueError{
|
||||
Kind: rule.IssueKind(),
|
||||
Message: ls.T(ctx, "compose", rule.IssueMultivalueMessage(dupValues)),
|
||||
Meta: map[string]interface{}{
|
||||
"field": c.Attribute,
|
||||
"dupValueField": c.Attribute,
|
||||
"rule": rule.String(),
|
||||
},
|
||||
})
|
||||
|
||||
return valErr
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (dr DeDupRuleSet) Validate() (err error) {
|
||||
return dr.Walk(func(rule *DeDupRule) (err error) {
|
||||
if !rule.HasAttributes() {
|
||||
err = fmt.Errorf("deduplication not valid (no constraints)")
|
||||
return
|
||||
}
|
||||
return dr.Walk(func(rule *DeDupRule) (err error) {
|
||||
if !rule.HasAttributes() {
|
||||
err = fmt.Errorf("deduplication not valid (no constraints)")
|
||||
return
|
||||
}
|
||||
|
||||
for _, a := range rule.Attributes() {
|
||||
if a == "" {
|
||||
err = fmt.Errorf("deduplication not valid (invalid field)")
|
||||
return
|
||||
}
|
||||
}
|
||||
for _, a := range rule.Attributes() {
|
||||
if a == "" {
|
||||
err = fmt.Errorf("deduplication not valid (invalid field)")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
})
|
||||
return
|
||||
})
|
||||
}
|
||||
|
||||
func (c DeDupRuleConstraint) HasMultiValue() bool {
|
||||
switch c.MultiValue {
|
||||
case oneOf, equal:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
switch c.MultiValue {
|
||||
case oneOf, equal:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func (c DeDupRuleConstraint) IsAllEqual() bool {
|
||||
return c.MultiValue == equal
|
||||
return c.MultiValue == equal
|
||||
}
|
||||
|
||||
func (c DeDupRuleConstraint) IsOneOf() bool {
|
||||
return c.MultiValue == oneOf
|
||||
return c.MultiValue == oneOf
|
||||
}
|
||||
|
||||
func (v *RecordValueErrorSet) SetMetaID(id uint64) {
|
||||
if v.IsValid() {
|
||||
return
|
||||
}
|
||||
if v.IsValid() {
|
||||
return
|
||||
}
|
||||
|
||||
for _, val := range v.Set {
|
||||
if val.Meta != nil {
|
||||
if _, ok := val.Meta["id"]; !ok {
|
||||
val.Meta["id"] = cast.ToString(id)
|
||||
}
|
||||
}
|
||||
}
|
||||
for _, val := range v.Set {
|
||||
if val.Meta != nil {
|
||||
if _, ok := val.Meta["id"]; !ok {
|
||||
val.Meta["id"] = cast.ToString(id)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (v *RecordValueErrorSet) HasStrictErrors() bool {
|
||||
return v.HasKind(deDupError.String())
|
||||
return v.HasKind(deDupError.String())
|
||||
}
|
||||
|
||||
// distinct only list the different (distinct) values
|
||||
func distinct(input []string) (out []string) {
|
||||
keys := make(map[string]bool)
|
||||
for _, val := range input {
|
||||
if _, ok := keys[val]; !ok {
|
||||
keys[val] = true
|
||||
out = append(out, val)
|
||||
}
|
||||
}
|
||||
return
|
||||
keys := make(map[string]bool)
|
||||
for _, val := range input {
|
||||
if _, ok := keys[val]; !ok {
|
||||
keys[val] = true
|
||||
out = append(out, val)
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// matchValue will check if the input matches with target string as per the modifier
|
||||
func matchValue(modifier DeDupValueModifier, input string, target string) bool {
|
||||
switch modifier {
|
||||
case ignoreCase:
|
||||
return str.Match(input, target, str.CaseInSensitiveMatch)
|
||||
case caseSensitive:
|
||||
return str.Match(input, target, str.CaseSensitiveMatch)
|
||||
case fuzzyMatch:
|
||||
return str.Match(input, target, str.LevenshteinDistance)
|
||||
case soundsLike:
|
||||
return str.Match(input, target, str.Soundex)
|
||||
default:
|
||||
// ignoreCase as default, if not specified
|
||||
return str.Match(input, target, str.CaseInSensitiveMatch)
|
||||
}
|
||||
switch modifier {
|
||||
case ignoreCase:
|
||||
return str.Match(input, target, str.CaseInSensitiveMatch)
|
||||
case caseSensitive:
|
||||
return str.Match(input, target, str.CaseSensitiveMatch)
|
||||
case fuzzyMatch:
|
||||
return str.Match(input, target, str.LevenshteinDistance)
|
||||
case soundsLike:
|
||||
return str.Match(input, target, str.Soundex)
|
||||
default:
|
||||
// ignoreCase as default, if not specified
|
||||
return str.Match(input, target, str.CaseInSensitiveMatch)
|
||||
}
|
||||
}
|
||||
|
||||
func recordValueFrequencyMap(rvFreqMap map[string]int, c DeDupValueModifier, vv RecordValueSet) (values []string) {
|
||||
for _, v := range vv {
|
||||
values = append(values, v.Value)
|
||||
|
||||
if c == ignoreCase {
|
||||
v.Value = strings.ToLower(v.Value)
|
||||
}
|
||||
|
||||
rvFreqMap[v.Value]++
|
||||
}
|
||||
|
||||
return values
|
||||
}
|
||||
|
||||
func matchRecordValueFrequencyMap(a, b map[string]int) (ok bool) {
|
||||
for k, keyCount := range a {
|
||||
_, ok = b[k]
|
||||
if !ok || keyCount != b[k] {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -38,6 +38,16 @@ func TestDeDupRule_checkCaseSensitiveDuplication(t *testing.T) {
|
||||
rule: rule1,
|
||||
rec: Record{
|
||||
ID: 1,
|
||||
module: &Module{
|
||||
ID: 1,
|
||||
Fields: ModuleFieldSet{
|
||||
&ModuleField{
|
||||
Name: "name",
|
||||
Kind: "String",
|
||||
Multi: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
Values: RecordValueSet{
|
||||
&RecordValue{
|
||||
RecordID: 1,
|
||||
@@ -57,7 +67,7 @@ func TestDeDupRule_checkCaseSensitiveDuplication(t *testing.T) {
|
||||
Set: []RecordValueError{
|
||||
{
|
||||
Kind: deDupError.String(),
|
||||
Message: rule1.IssueMessage(),
|
||||
Message: rule1.IssueMessage("test"),
|
||||
Meta: map[string]interface{}{
|
||||
"field": "name",
|
||||
"value": "test",
|
||||
@@ -74,8 +84,294 @@ func TestDeDupRule_checkCaseSensitiveDuplication(t *testing.T) {
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
gotOut := tt.rule.checkCaseSensitiveDuplication(ctx, ls, tt.rec, tt.vv)
|
||||
req.Equal(tt.wantOut, gotOut, "checkCaseSensitiveDuplication() = %v, want %v", gotOut, tt.wantOut)
|
||||
gotOut := tt.rule.checkDuplication(ctx, ls, tt.rec, tt.vv)
|
||||
req.Equal(tt.wantOut, gotOut, "checkDuplication() = %v, want %v", gotOut, tt.wantOut)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestDedupRule_checkMultiValueEqualDuplication(t *testing.T) {
|
||||
var (
|
||||
req = require.New(t)
|
||||
ctx = context.Background()
|
||||
ls = locale.Global()
|
||||
|
||||
rule1 = DeDupRule{
|
||||
Name: "",
|
||||
Strict: true,
|
||||
ConstraintSet: []*DeDupRuleConstraint{
|
||||
{
|
||||
Attribute: "name",
|
||||
Modifier: caseSensitive,
|
||||
MultiValue: equal,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
rule2 = DeDupRule{
|
||||
Name: "ignore case rule",
|
||||
Strict: true,
|
||||
ConstraintSet: []*DeDupRuleConstraint{
|
||||
{
|
||||
Attribute: "name",
|
||||
Modifier: ignoreCase,
|
||||
MultiValue: equal,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
numberRule = DeDupRule{
|
||||
Name: "number rule",
|
||||
Strict: true,
|
||||
ConstraintSet: []*DeDupRuleConstraint{
|
||||
{
|
||||
Attribute: "count",
|
||||
Modifier: ignoreCase,
|
||||
MultiValue: equal,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
locationRule = DeDupRule{
|
||||
Name: "location rule",
|
||||
Strict: true,
|
||||
ConstraintSet: []*DeDupRuleConstraint{
|
||||
{
|
||||
Attribute: "location",
|
||||
Modifier: ignoreCase,
|
||||
MultiValue: equal,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
tests = []struct {
|
||||
name string
|
||||
rule DeDupRule
|
||||
rec Record
|
||||
vv RecordValueSet
|
||||
wantOut *RecordValueErrorSet
|
||||
}{
|
||||
{
|
||||
name: "no duplication",
|
||||
rule: rule1,
|
||||
rec: Record{
|
||||
ID: 1,
|
||||
module: &Module{
|
||||
ID: 1,
|
||||
Fields: ModuleFieldSet{
|
||||
&ModuleField{
|
||||
Name: "name",
|
||||
Kind: "String",
|
||||
Multi: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
Values: RecordValueSet{
|
||||
&RecordValue{
|
||||
RecordID: 1,
|
||||
Name: "name",
|
||||
Value: "test",
|
||||
},
|
||||
&RecordValue{
|
||||
RecordID: 1,
|
||||
Name: "name",
|
||||
Value: "test test",
|
||||
},
|
||||
},
|
||||
},
|
||||
vv: RecordValueSet{
|
||||
&RecordValue{
|
||||
RecordID: 0,
|
||||
Name: "name",
|
||||
Value: "test",
|
||||
},
|
||||
&RecordValue{
|
||||
RecordID: 0,
|
||||
Name: "name",
|
||||
Value: "test test",
|
||||
},
|
||||
},
|
||||
wantOut: &RecordValueErrorSet{
|
||||
Set: []RecordValueError{
|
||||
{
|
||||
Kind: deDupError.String(),
|
||||
Message: rule1.IssueMultivalueMessage([]string{"test", "test test"}),
|
||||
Meta: map[string]interface{}{
|
||||
"field": "name",
|
||||
"dupValueField": "name",
|
||||
"rule": rule1.String(),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "no duplication",
|
||||
rule: rule2,
|
||||
rec: Record{
|
||||
ID: 1,
|
||||
module: &Module{
|
||||
ID: 1,
|
||||
Fields: ModuleFieldSet{
|
||||
&ModuleField{
|
||||
Name: "name",
|
||||
Kind: "String",
|
||||
Multi: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
Values: RecordValueSet{
|
||||
&RecordValue{
|
||||
RecordID: 1,
|
||||
Name: "name",
|
||||
Value: "test",
|
||||
},
|
||||
&RecordValue{
|
||||
RecordID: 1,
|
||||
Name: "name",
|
||||
Value: "test tEst",
|
||||
},
|
||||
},
|
||||
},
|
||||
vv: RecordValueSet{
|
||||
&RecordValue{
|
||||
RecordID: 0,
|
||||
Name: "name",
|
||||
Value: "test",
|
||||
},
|
||||
&RecordValue{
|
||||
RecordID: 0,
|
||||
Name: "name",
|
||||
Value: "Test Test",
|
||||
},
|
||||
},
|
||||
wantOut: &RecordValueErrorSet{
|
||||
Set: []RecordValueError{
|
||||
{
|
||||
Kind: deDupError.String(),
|
||||
Message: rule2.IssueMultivalueMessage([]string{"test", "test tEst"}),
|
||||
Meta: map[string]interface{}{
|
||||
"field": "name",
|
||||
"dupValueField": "name",
|
||||
"rule": rule2.String(),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "no duplication",
|
||||
rule: numberRule,
|
||||
rec: Record{
|
||||
ID: 1,
|
||||
module: &Module{
|
||||
ID: 1,
|
||||
Fields: ModuleFieldSet{
|
||||
&ModuleField{
|
||||
Name: "count",
|
||||
Multi: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
Values: RecordValueSet{
|
||||
&RecordValue{
|
||||
RecordID: 1,
|
||||
Name: "count",
|
||||
Value: "234",
|
||||
},
|
||||
&RecordValue{
|
||||
RecordID: 1,
|
||||
Name: "count",
|
||||
Value: "897",
|
||||
},
|
||||
},
|
||||
},
|
||||
vv: RecordValueSet{
|
||||
&RecordValue{
|
||||
RecordID: 0,
|
||||
Name: "count",
|
||||
Value: "897",
|
||||
},
|
||||
&RecordValue{
|
||||
RecordID: 0,
|
||||
Name: "count",
|
||||
Value: "234",
|
||||
},
|
||||
},
|
||||
wantOut: &RecordValueErrorSet{
|
||||
Set: []RecordValueError{
|
||||
{
|
||||
Kind: deDupError.String(),
|
||||
Message: numberRule.IssueMultivalueMessage([]string{"234", "897"}),
|
||||
Meta: map[string]interface{}{
|
||||
"field": "count",
|
||||
"dupValueField": "count",
|
||||
"rule": numberRule.String(),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "no duplication",
|
||||
rule: locationRule,
|
||||
rec: Record{
|
||||
ID: 1,
|
||||
module: &Module{
|
||||
ID: 1,
|
||||
Fields: ModuleFieldSet{
|
||||
&ModuleField{
|
||||
Name: "location",
|
||||
Multi: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
Values: RecordValueSet{
|
||||
&RecordValue{
|
||||
RecordID: 1,
|
||||
Name: "location",
|
||||
Value: "{\"coordinates\":[-6.7833479,20.3768206]}",
|
||||
},
|
||||
&RecordValue{
|
||||
RecordID: 1,
|
||||
Name: "location",
|
||||
Value: "{\"coordinates\":[0.7833479,10.3768206]}",
|
||||
},
|
||||
},
|
||||
},
|
||||
vv: RecordValueSet{
|
||||
&RecordValue{
|
||||
RecordID: 0,
|
||||
Name: "location",
|
||||
Value: "{\"coordinates\":[0.7833479,10.3768206]}",
|
||||
},
|
||||
&RecordValue{
|
||||
RecordID: 0,
|
||||
Name: "location",
|
||||
Value: "{\"coordinates\":[-6.7833479,20.3768206]}",
|
||||
},
|
||||
},
|
||||
wantOut: &RecordValueErrorSet{
|
||||
Set: []RecordValueError{
|
||||
{
|
||||
Kind: deDupError.String(),
|
||||
Message: locationRule.IssueMultivalueMessage([]string{"{\"coordinates\":[-6.7833479,20.3768206]}", "{\"coordinates\":[0.7833479,10.3768206]}"}),
|
||||
Meta: map[string]interface{}{
|
||||
"field": "location",
|
||||
"dupValueField": "location",
|
||||
"rule": locationRule.String(),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
gotOut := tt.rule.checkDuplication(ctx, ls, tt.rec, tt.vv)
|
||||
req.Equal(tt.wantOut, gotOut, "checkDuplication() = %v, want %v", gotOut, tt.wantOut)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user