diff --git a/public/vue/dndex-ui b/public/vue/dndex-ui index e2afbac..c98901f 160000 --- a/public/vue/dndex-ui +++ b/public/vue/dndex-ui @@ -1 +1 @@ -Subproject commit e2afbacc8ed75cddb91cdccdedf77752107cb9ba +Subproject commit c98901f929a0c46f6bbf72d8e251c6ad485d81b5 diff --git a/storage/graph.go b/storage/graph.go index a4eebfd..e93be2b 100644 --- a/storage/graph.go +++ b/storage/graph.go @@ -2,6 +2,7 @@ package storage import ( "context" + "errors" "fmt" "local/dndex/config" "local/dndex/storage/driver" @@ -34,6 +35,16 @@ func (g Graph) Search(ctx context.Context, namespace string, nameContains string return g.find(ctx, namespace, filter) } +func (g Graph) ListCaseInsensitive(ctx context.Context, namespace string, from ...string) ([]entity.One, error) { + if len(from) == 0 { + return g.List(ctx, namespace) + } + pattern := strings.Join(from, "|") + pattern = "^(?i)(" + pattern + ")" + filter := operator.Regex{Key: entity.Name, Value: pattern} + return g.find(ctx, namespace, filter) +} + func (g Graph) List(ctx context.Context, namespace string, from ...string) ([]entity.One, error) { filter := operator.NewFilterIn(entity.Name, from) return g.find(ctx, namespace, filter) @@ -60,6 +71,11 @@ func (g Graph) gatherOnes(ctx context.Context, ch <-chan bson.Raw) ([]entity.One } func (g Graph) Insert(ctx context.Context, namespace string, one entity.One) error { + if ones, err := g.ListCaseInsensitive(ctx, namespace, one.Name); err != nil { + return err + } else if len(ones) > 0 { + return errors.New("collision on primary key when case insensitive") + } return g.driver.Insert(ctx, namespace, one) } diff --git a/storage/graph_test.go b/storage/graph_test.go index 6a0b3e2..d749d22 100644 --- a/storage/graph_test.go +++ b/storage/graph_test.go @@ -7,6 +7,7 @@ import ( "local/dndex/storage/entity" "local/dndex/storage/operator" "os" + "strings" "testing" "time" @@ -37,8 +38,17 @@ func TestIntegration(t *testing.T) { randomOne(), } ones[0].Connections = map[string]entity.One{ones[2].Name: entity.One{Name: ones[2].Name, Relationship: ":("}} + cleanFill := func() { + clean() + for i := range ones { + if err := graph.driver.Insert(context.TODO(), "col", ones[i]); err != nil { + t.Fatal(err) + } + } + } t.Run("graph.Insert(...)", func(t *testing.T) { + clean() for _, one := range ones { err := graph.Insert(ctx, "col", one) if err != nil { @@ -48,6 +58,7 @@ func TestIntegration(t *testing.T) { }) t.Run("graph.List", func(t *testing.T) { + cleanFill() all, err := graph.List(ctx, "col") if err != nil { t.Fatal(err) @@ -58,7 +69,20 @@ func TestIntegration(t *testing.T) { } }) + t.Run("graph.ListCaseInsensitive", func(t *testing.T) { + cleanFill() + all, err := graph.ListCaseInsensitive(ctx, "col") + if err != nil { + t.Fatal(err) + } + t.Logf("\nall = %+v", all) + if len(all) != 3 { + t.Fatalf("%v: %+v", len(all), all) + } + }) + t.Run("graph.List(foo => *)", func(t *testing.T) { + cleanFill() some, err := graph.List(ctx, "col", ones[0].Peers()...) if err != nil { t.Fatal(err) @@ -69,7 +93,40 @@ func TestIntegration(t *testing.T) { } }) + t.Run("graph.List(FOO => *)", func(t *testing.T) { + cleanFill() + peers := ones[0].Peers() + for i := range peers { + peers[i] = strings.ToUpper(peers[i]) + } + some, err := graph.List(ctx, "col", peers...) + if err != nil { + t.Fatal(err) + } + t.Logf("\nsom = %+v", some) + if len(some) != 0 { + t.Fatalf("%+v: %+v", len(some), some) + } + }) + + t.Run("graph.ListCaseInsensitive(FOO => *)", func(t *testing.T) { + cleanFill() + peers := ones[0].Peers() + for i := range peers { + peers[i] = strings.ToUpper(peers[i]) + } + some, err := graph.ListCaseInsensitive(ctx, "col", peers...) + if err != nil { + t.Fatal(err) + } + t.Logf("\nsom = %+v", some) + if len(some) != 1 { + t.Fatalf("%+v: %+v", len(some), some) + } + }) + t.Run("graph.Search(foo => *)", func(t *testing.T) { + cleanFill() some, err := graph.Search(ctx, "col", ones[0].Name[:3]) if err != nil { t.Fatal(err) @@ -81,6 +138,7 @@ func TestIntegration(t *testing.T) { }) t.Run("graph.Update(foo, --bar)", func(t *testing.T) { + cleanFill() err := graph.Update(ctx, "col", ones[0].Query(), operator.Set{entity.Connections, map[string]interface{}{}}) if err != nil { t.Fatal(err) @@ -104,6 +162,7 @@ func TestIntegration(t *testing.T) { }) t.Run("graph.Update(foo, +=2); graph.Update(foo, -=1)", func(t *testing.T) { + cleanFill() err := graph.Update(ctx, "col", ones[0].Query(), operator.Set{entity.Connections, map[string]entity.One{ "hello": entity.One{Name: "hello", Relationship: ":("}, "world": entity.One{Name: "world", Relationship: ":("}, @@ -142,6 +201,7 @@ func TestIntegration(t *testing.T) { }) t.Run("graph.Update(new attachment), Update(--new attachment)", func(t *testing.T) { + cleanFill() err := graph.Update(ctx, "col", ones[0].Query(), operator.Set{Key: fmt.Sprintf("%s.new attachment", entity.Attachments), Value: "my new attachment"}) if err != nil { t.Fatal(err) @@ -180,11 +240,67 @@ func TestIntegration(t *testing.T) { t.Fatal(len(some2[0].Attachments), some2[0].Attachments) } }) + + t.Run("graph.Insert Collision(...)", func(t *testing.T) { + cleanFill() + one := randomOne() + err := graph.Insert(ctx, "col", one) + if err != nil { + t.Fatal(err) + } + + one.Name = strings.ToUpper(one.Name) + err = graph.Insert(ctx, "col", one) + if err == nil { + t.Fatal(err) + } + + err = graph.Delete(ctx, "col", operator.Regex{Key: entity.Name, Value: "^(?i)" + one.Name}) + if err != nil { + t.Fatal(err) + } + + ones, err = graph.ListCaseInsensitive(ctx, "col", one.Name) + if err != nil { + t.Fatal(err) + } + if len(ones) > 0 { + t.Fatal(err) + } + }) + + t.Run("graph.Insert Collision(...)", func(t *testing.T) { + cleanFill() + one := randomOne() + err := graph.Insert(ctx, "col", one) + if err != nil { + t.Fatal(err) + } + + err = graph.Insert(ctx, "col", one) + if err == nil { + t.Fatal(err) + } + + err = graph.Delete(ctx, "col", operator.Regex{Key: entity.Name, Value: "^(?i)" + one.Name}) + if err != nil { + t.Fatal(err) + } + + ones, err = graph.ListCaseInsensitive(ctx, "col", one.Name) + if err != nil { + t.Fatal(err) + } + if len(ones) > 0 { + t.Fatal(err) + } + }) + } func randomOne() entity.One { return entity.One{ - Name: uuid.New().String()[:5], + Name: "name-" + uuid.New().String()[:5], Type: "Humman", Title: "Biggus", Text: "tee hee xd", diff --git a/view/who.go b/view/who.go index e7067d4..9b6184d 100644 --- a/view/who.go +++ b/view/who.go @@ -49,7 +49,7 @@ func whoGet(namespace string, g storage.Graph, w http.ResponseWriter, r *http.Re } _, light := r.URL.Query()["light"] - ones, err := g.List(r.Context(), namespace, id) + ones, err := g.ListCaseInsensitive(r.Context(), namespace, id) if err != nil { return err } @@ -63,7 +63,7 @@ func whoGet(namespace string, g storage.Graph, w http.ResponseWriter, r *http.Re one := ones[0] if !light && len(one.Connections) > 0 { - ones, err := g.List(r.Context(), namespace, one.Peers()...) + ones, err := g.ListCaseInsensitive(r.Context(), namespace, one.Peers()...) if err != nil { return err } @@ -187,7 +187,7 @@ func whoPatch(namespace string, g storage.Graph, w http.ResponseWriter, r *http. } func whoTrace(namespace string, g storage.Graph, w http.ResponseWriter, r *http.Request) error { - ones, err := g.List(r.Context(), namespace) + ones, err := g.ListCaseInsensitive(r.Context(), namespace) if err != nil { return err } diff --git a/view/who_test.go b/view/who_test.go index 5236ecb..e108beb 100644 --- a/view/who_test.go +++ b/view/who_test.go @@ -90,6 +90,68 @@ func TestWho(t *testing.T) { t.Logf("POST GET:\n%s", b) }) + t.Run("get real light", func(t *testing.T) { + iwant := want + r := httptest.NewRequest(http.MethodGet, "/who?namespace=col&light&id="+iwant.Name, nil) + w := httptest.NewRecorder() + handler.ServeHTTP(w, r) + if w.Code != http.StatusOK { + t.Fatalf("%d: %s", w.Code, w.Body.Bytes()) + } + o := entity.One{} + if err := json.Unmarshal(w.Body.Bytes(), &o); err != nil { + t.Fatal(err) + } + if fmt.Sprint(o) == fmt.Sprint(iwant) { + t.Fatal(o, iwant) + } + if len(o.Connections) != len(iwant.Connections) { + t.Fatal(len(o.Connections), len(iwant.Connections)) + } + iwant.Connections = o.Connections + iwant.Modified = 0 + o.Modified = 0 + if fmt.Sprint(o) != fmt.Sprint(iwant) { + t.Fatalf("after resolving connections and modified, iwant and got differ: \nwant %+v\n got %+v", iwant, o) + } + for _, connection := range iwant.Connections { + if len(connection.Connections) != 0 { + t.Fatal(connection) + } + } + b, _ := json.MarshalIndent(o, "", " ") + t.Logf("POST GET:\n%s", b) + }) + + t.Run("get real but case is wrong", func(t *testing.T) { + iwant := want + iwantName := strings.ToUpper(iwant.Name) + r := httptest.NewRequest(http.MethodGet, "/who?namespace=col&id="+iwantName, nil) + w := httptest.NewRecorder() + handler.ServeHTTP(w, r) + if w.Code != http.StatusOK { + t.Fatalf("%d: %s", w.Code, w.Body.Bytes()) + } + o := entity.One{} + if err := json.Unmarshal(w.Body.Bytes(), &o); err != nil { + t.Fatal(err) + } + if fmt.Sprint(o) == fmt.Sprint(iwant) { + t.Fatal(o, iwant) + } + if len(o.Connections) != len(iwant.Connections) { + t.Fatal(len(o.Connections), len(iwant.Connections)) + } + iwant.Connections = o.Connections + iwant.Modified = 0 + o.Modified = 0 + if fmt.Sprint(o) != fmt.Sprint(iwant) { + t.Fatalf("after resolving connections and modified, iwant and got differ: \nwant %+v\n got %+v", iwant, o) + } + b, _ := json.MarshalIndent(o, "", " ") + t.Logf("POST GET:\n%s", b) + }) + t.Run("put fake", func(t *testing.T) { iwant := want r := httptest.NewRequest(http.MethodPut, "/who?namespace=col&id=FAKER"+iwant.Name, strings.NewReader(`{"title":"this should fail to find someone"}`))