Wrote simple event to subgraph function.
Updated related code to align.
This commit is contained in:
61
event.go
Normal file
61
event.go
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
package heartwood
|
||||||
|
|
||||||
|
import (
|
||||||
|
roots "git.wisehodl.dev/jay/go-roots/events"
|
||||||
|
)
|
||||||
|
|
||||||
|
func EventToSubgraph(e roots.Event) *Subgraph {
|
||||||
|
subgraph := NewSubgraph()
|
||||||
|
|
||||||
|
// Create Event node
|
||||||
|
eventNode := NewEventNode(e.ID)
|
||||||
|
eventNode.Props["created_at"] = e.CreatedAt
|
||||||
|
eventNode.Props["kind"] = e.Kind
|
||||||
|
eventNode.Props["content"] = e.Content
|
||||||
|
|
||||||
|
// Create User node
|
||||||
|
userNode := NewUserNode(e.PubKey)
|
||||||
|
|
||||||
|
// Create SIGNED rel
|
||||||
|
signedRel := NewSignedRel(userNode, eventNode, nil)
|
||||||
|
|
||||||
|
// Create Tag nodes
|
||||||
|
tagNodes := []*Node{}
|
||||||
|
for _, tag := range e.Tags {
|
||||||
|
if !isValidTag(tag) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
tagNodes = append(tagNodes, NewTagNode(tag[0], tag[1]))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create Tag rels
|
||||||
|
tagRels := []*Relationship{}
|
||||||
|
for _, tagNode := range tagNodes {
|
||||||
|
tagRels = append(tagRels, NewTaggedRel(eventNode, tagNode, nil))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Populate subgraph
|
||||||
|
subgraph.AddNode(eventNode)
|
||||||
|
subgraph.AddNode(userNode)
|
||||||
|
subgraph.AddRel(signedRel)
|
||||||
|
for _, node := range tagNodes {
|
||||||
|
subgraph.AddNode(node)
|
||||||
|
}
|
||||||
|
for _, rel := range tagRels {
|
||||||
|
subgraph.AddRel(rel)
|
||||||
|
}
|
||||||
|
|
||||||
|
return subgraph
|
||||||
|
}
|
||||||
|
|
||||||
|
func isValidTag(t roots.Tag) bool {
|
||||||
|
if len(t) < 2 {
|
||||||
|
// Skip tags that do not have name and value fields
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if len(t[0])+len(t[1]) > 8192 {
|
||||||
|
// Skip tags that are too large for the neo4j indexer
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
204
event_test.go
Normal file
204
event_test.go
Normal file
@@ -0,0 +1,204 @@
|
|||||||
|
package heartwood
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
roots "git.wisehodl.dev/jay/go-roots/events"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"reflect"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
var ids = map[string]string{
|
||||||
|
"a": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
|
||||||
|
"b": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
|
||||||
|
"c": "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc",
|
||||||
|
"d": "dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd",
|
||||||
|
}
|
||||||
|
|
||||||
|
var static = roots.Event{
|
||||||
|
CreatedAt: 1000,
|
||||||
|
Kind: 1,
|
||||||
|
Content: "hello",
|
||||||
|
}
|
||||||
|
|
||||||
|
func newFullEventNode(id string, createdAt, kind int, content string) *Node {
|
||||||
|
n := NewEventNode(id)
|
||||||
|
n.Props["created_at"] = createdAt
|
||||||
|
n.Props["kind"] = kind
|
||||||
|
n.Props["content"] = content
|
||||||
|
return n
|
||||||
|
}
|
||||||
|
|
||||||
|
func baseSubgraph(eventID, pubkey string) (*Subgraph, *Node, *Node) {
|
||||||
|
s := NewSubgraph()
|
||||||
|
eventNode := newFullEventNode(eventID, static.CreatedAt, static.Kind, static.Content)
|
||||||
|
userNode := NewUserNode(pubkey)
|
||||||
|
s.AddNode(eventNode)
|
||||||
|
s.AddNode(userNode)
|
||||||
|
s.AddRel(NewSignedRel(userNode, eventNode, nil))
|
||||||
|
return s, eventNode, userNode
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEventToSubgraph(t *testing.T) {
|
||||||
|
cases := []struct {
|
||||||
|
name string
|
||||||
|
event roots.Event
|
||||||
|
expected *Subgraph
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "bare event",
|
||||||
|
event: roots.Event{
|
||||||
|
ID: ids["a"], PubKey: ids["b"],
|
||||||
|
CreatedAt: static.CreatedAt, Kind: static.Kind, Content: static.Content,
|
||||||
|
},
|
||||||
|
expected: func() *Subgraph {
|
||||||
|
s, _, _ := baseSubgraph(ids["a"], ids["b"])
|
||||||
|
return s
|
||||||
|
}(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "single generic tag",
|
||||||
|
event: roots.Event{
|
||||||
|
ID: ids["a"], PubKey: ids["b"],
|
||||||
|
CreatedAt: static.CreatedAt, Kind: static.Kind, Content: static.Content,
|
||||||
|
Tags: []roots.Tag{{"t", "bitcoin"}},
|
||||||
|
},
|
||||||
|
expected: func() *Subgraph {
|
||||||
|
s, eventNode, _ := baseSubgraph(ids["a"], ids["b"])
|
||||||
|
tagNode := NewTagNode("t", "bitcoin")
|
||||||
|
s.AddNode(tagNode)
|
||||||
|
s.AddRel(NewTaggedRel(eventNode, tagNode, nil))
|
||||||
|
return s
|
||||||
|
}(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "tag with fewer than 2 elements",
|
||||||
|
event: roots.Event{
|
||||||
|
ID: ids["a"], PubKey: ids["b"],
|
||||||
|
CreatedAt: static.CreatedAt, Kind: static.Kind, Content: static.Content,
|
||||||
|
Tags: []roots.Tag{{"t"}},
|
||||||
|
},
|
||||||
|
expected: func() *Subgraph {
|
||||||
|
s, _, _ := baseSubgraph(ids["a"], ids["b"])
|
||||||
|
return s
|
||||||
|
}(),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range cases {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
got := EventToSubgraph(tc.event)
|
||||||
|
assertSubgraphsEqual(t, tc.expected, got)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// helpers
|
||||||
|
|
||||||
|
func nodesEqual(expected, got *Node) error {
|
||||||
|
// Compare label counts
|
||||||
|
if expected.Labels.Length() != got.Labels.Length() {
|
||||||
|
return fmt.Errorf(
|
||||||
|
"number of labels does not match. expected %d, got %d",
|
||||||
|
expected.Labels.Length(), got.Labels.Length())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compare label values
|
||||||
|
for _, label := range expected.Labels.ToArray() {
|
||||||
|
if !got.Labels.Contains(label) {
|
||||||
|
return fmt.Errorf("missing label %q", label)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compare property values
|
||||||
|
if err := propsEqual(expected.Props, got.Props); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func relsEqual(expected, got *Relationship) error {
|
||||||
|
// Compare type
|
||||||
|
if expected.Type != got.Type {
|
||||||
|
return fmt.Errorf("type: expected %q, got %q", expected.Type, got.Type)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compare property values
|
||||||
|
if err := propsEqual(expected.Props, got.Props); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compare endpoints
|
||||||
|
if err := nodesEqual(expected.Start, got.Start); err != nil {
|
||||||
|
return fmt.Errorf("start node: %w", err)
|
||||||
|
}
|
||||||
|
if err := nodesEqual(expected.End, got.End); err != nil {
|
||||||
|
return fmt.Errorf("end node: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func propsEqual(expected, got Properties) error {
|
||||||
|
if len(expected) != len(got) {
|
||||||
|
return fmt.Errorf(
|
||||||
|
"number of props does not match. expected %d, got %d",
|
||||||
|
len(expected), len(got))
|
||||||
|
}
|
||||||
|
|
||||||
|
for key, expectedVal := range expected {
|
||||||
|
gotVal, exists := got[key]
|
||||||
|
if !exists {
|
||||||
|
return fmt.Errorf("missing prop %q", key)
|
||||||
|
}
|
||||||
|
if !reflect.DeepEqual(expectedVal, gotVal) {
|
||||||
|
return fmt.Errorf("prop %q: expected %v, got %v", key, expectedVal, gotVal)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func assertSubgraphsEqual(t *testing.T, expected, got *Subgraph) {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
gotNodes := make([]*Node, len(got.Nodes()))
|
||||||
|
copy(gotNodes, got.Nodes())
|
||||||
|
|
||||||
|
gotRels := make([]*Relationship, len(got.Rels()))
|
||||||
|
copy(gotRels, got.Rels())
|
||||||
|
|
||||||
|
for _, expectedNode := range expected.Nodes() {
|
||||||
|
index := findInList(expectedNode, gotNodes, nodesEqual)
|
||||||
|
if index == -1 {
|
||||||
|
assert.Fail(t, fmt.Sprintf("missing expected node: %+v", expectedNode))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
gotNodes = removeFromList(index, gotNodes)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, expectedRel := range expected.Rels() {
|
||||||
|
index := findInList(expectedRel, gotRels, relsEqual)
|
||||||
|
if index == -1 {
|
||||||
|
assert.Fail(t, fmt.Sprintf("missing expected rel: %+v", expectedRel))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
gotRels = removeFromList(index, gotRels)
|
||||||
|
}
|
||||||
|
|
||||||
|
assert.Empty(t, gotNodes, "unexpected nodes in subgraph")
|
||||||
|
assert.Empty(t, gotRels, "unexpected rels in subgraph")
|
||||||
|
}
|
||||||
|
|
||||||
|
func findInList[T any](item *T, list []*T, equal func(*T, *T) error) int {
|
||||||
|
for i, candidate := range list {
|
||||||
|
if equal(item, candidate) == nil {
|
||||||
|
return i
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
|
||||||
|
func removeFromList[T any](i int, list []*T) []*T {
|
||||||
|
return append(list[:i], list[i+1:]...)
|
||||||
|
}
|
||||||
8
graph.go
8
graph.go
@@ -191,6 +191,14 @@ func (s *Subgraph) AddRel(rel *Relationship) {
|
|||||||
s.rels = append(s.rels, rel)
|
s.rels = append(s.rels, rel)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *Subgraph) Nodes() []*Node {
|
||||||
|
return s.nodes
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Subgraph) Rels() []*Relationship {
|
||||||
|
return s.rels
|
||||||
|
}
|
||||||
|
|
||||||
// ========================================
|
// ========================================
|
||||||
// Structured Subgraph
|
// Structured Subgraph
|
||||||
// ========================================
|
// ========================================
|
||||||
|
|||||||
@@ -161,7 +161,7 @@ func TestStructuredSubgraphAddRel(t *testing.T) {
|
|||||||
|
|
||||||
userNode := NewUserNode("pubkey1")
|
userNode := NewUserNode("pubkey1")
|
||||||
eventNode := NewEventNode("abc123")
|
eventNode := NewEventNode("abc123")
|
||||||
rel, _ := NewSignedRel(userNode, eventNode, nil)
|
rel := NewSignedRel(userNode, eventNode, nil)
|
||||||
|
|
||||||
err := subgraph.AddRel(rel)
|
err := subgraph.AddRel(rel)
|
||||||
|
|
||||||
|
|||||||
39
schema.go
39
schema.go
@@ -40,11 +40,10 @@ func NewEventNode(id string) *Node {
|
|||||||
return NewNode("Event", Properties{"id": id})
|
return NewNode("Event", Properties{"id": id})
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewTagNode(name string, value string, rest []string) *Node {
|
func NewTagNode(name string, value string) *Node {
|
||||||
return NewNode("Tag", Properties{
|
return NewNode("Tag", Properties{
|
||||||
"name": name,
|
"name": name,
|
||||||
"value": value,
|
"value": value})
|
||||||
"rest": rest})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ========================================
|
// ========================================
|
||||||
@@ -52,26 +51,26 @@ func NewTagNode(name string, value string, rest []string) *Node {
|
|||||||
// ========================================
|
// ========================================
|
||||||
|
|
||||||
func NewSignedRel(
|
func NewSignedRel(
|
||||||
start *Node, end *Node, props Properties) (*Relationship, error) {
|
start *Node, end *Node, props Properties) *Relationship {
|
||||||
return NewRelationshipWithValidation(
|
return NewRelationshipWithValidation(
|
||||||
"SIGNED", "User", "Event", start, end, props)
|
"SIGNED", "User", "Event", start, end, props)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewTaggedRel(
|
func NewTaggedRel(
|
||||||
start *Node, end *Node, props Properties) (*Relationship, error) {
|
start *Node, end *Node, props Properties) *Relationship {
|
||||||
return NewRelationshipWithValidation(
|
return NewRelationshipWithValidation(
|
||||||
"TAGGED", "Event", "Tag", start, end, props)
|
"TAGGED", "Event", "Tag", start, end, props)
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewReferencesEventRel(
|
func NewReferencesEventRel(
|
||||||
start *Node, end *Node, props Properties) (*Relationship, error) {
|
start *Node, end *Node, props Properties) *Relationship {
|
||||||
return NewRelationshipWithValidation(
|
return NewRelationshipWithValidation(
|
||||||
"REFERENCES", "Tag", "Event", start, end, props)
|
"REFERENCES", "Tag", "Event", start, end, props)
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewReferencesUserRel(
|
func NewReferencesUserRel(
|
||||||
start *Node, end *Node, props Properties) (*Relationship, error) {
|
start *Node, end *Node, props Properties) *Relationship {
|
||||||
return NewRelationshipWithValidation(
|
return NewRelationshipWithValidation(
|
||||||
"REFERENCES", "Tag", "User", start, end, props)
|
"REFERENCES", "Tag", "User", start, end, props)
|
||||||
}
|
}
|
||||||
@@ -80,15 +79,13 @@ func NewReferencesUserRel(
|
|||||||
// Relationship Constructor Helpers
|
// Relationship Constructor Helpers
|
||||||
// ========================================
|
// ========================================
|
||||||
|
|
||||||
func validateNodeLabel(node *Node, role string, expectedLabel string) error {
|
func validateNodeLabel(node *Node, role string, expectedLabel string) {
|
||||||
if !node.Labels.Contains(expectedLabel) {
|
if !node.Labels.Contains(expectedLabel) {
|
||||||
return fmt.Errorf(
|
panic(fmt.Errorf(
|
||||||
"expected %s node to have label '%s'. got %v",
|
"expected %s node to have label %q. got %v",
|
||||||
role, expectedLabel, node.Labels.ToArray(),
|
role, expectedLabel, node.Labels.ToArray(),
|
||||||
)
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewRelationshipWithValidation(
|
func NewRelationshipWithValidation(
|
||||||
@@ -97,20 +94,12 @@ func NewRelationshipWithValidation(
|
|||||||
endLabel string,
|
endLabel string,
|
||||||
start *Node,
|
start *Node,
|
||||||
end *Node,
|
end *Node,
|
||||||
props Properties) (*Relationship, error) {
|
props Properties) *Relationship {
|
||||||
var err error
|
|
||||||
|
|
||||||
err = validateNodeLabel(start, "start", startLabel)
|
validateNodeLabel(start, "start", startLabel)
|
||||||
if err != nil {
|
validateNodeLabel(end, "end", endLabel)
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
err = validateNodeLabel(end, "end", endLabel)
|
return NewRelationship(rtype, start, end, props)
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return NewRelationship(rtype, start, end, props), nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ========================================
|
// ========================================
|
||||||
|
|||||||
@@ -7,11 +7,11 @@ import (
|
|||||||
|
|
||||||
func TestNewRelationshipWithValidation(t *testing.T) {
|
func TestNewRelationshipWithValidation(t *testing.T) {
|
||||||
cases := []struct {
|
cases := []struct {
|
||||||
name string
|
name string
|
||||||
start *Node
|
start *Node
|
||||||
end *Node
|
end *Node
|
||||||
wantErr bool
|
wantPanic bool
|
||||||
wantErrText string
|
wantPanicText string
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
name: "valid start and end nodes",
|
name: "valid start and end nodes",
|
||||||
@@ -19,24 +19,23 @@ func TestNewRelationshipWithValidation(t *testing.T) {
|
|||||||
end: NewEventNode("abc123"),
|
end: NewEventNode("abc123"),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "mismatched start node label",
|
name: "mismatched start node label",
|
||||||
start: NewEventNode("abc123"),
|
start: NewEventNode("abc123"),
|
||||||
end: NewEventNode("abc123"),
|
end: NewEventNode("abc123"),
|
||||||
wantErr: true,
|
wantPanic: true,
|
||||||
wantErrText: "expected start node to have label 'User'",
|
wantPanicText: "expected start node to have label \"User\". got [Event]",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, tc := range cases {
|
for _, tc := range cases {
|
||||||
t.Run(tc.name, func(t *testing.T) {
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
rel, err := NewSignedRel(tc.start, tc.end, nil)
|
if tc.wantPanic {
|
||||||
if tc.wantErr {
|
assert.PanicsWithError(t, tc.wantPanicText, func() {
|
||||||
assert.Error(t, err)
|
NewSignedRel(tc.start, tc.end, nil)
|
||||||
assert.ErrorContains(t, err, tc.wantErrText)
|
})
|
||||||
assert.Nil(t, rel)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
assert.NoError(t, err)
|
rel := NewSignedRel(tc.start, tc.end, nil)
|
||||||
assert.Equal(t, "SIGNED", rel.Type)
|
assert.Equal(t, "SIGNED", rel.Type)
|
||||||
assert.Contains(t, rel.Start.Labels.ToArray(), "User")
|
assert.Contains(t, rel.Start.Labels.ToArray(), "User")
|
||||||
assert.Contains(t, rel.End.Labels.ToArray(), "Event")
|
assert.Contains(t, rel.End.Labels.ToArray(), "Event")
|
||||||
|
|||||||
16
util.go
16
util.go
@@ -29,6 +29,22 @@ func (s Set[T]) Contains(item T) bool {
|
|||||||
return exists
|
return exists
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s Set[T]) Equal(other Set[T]) bool {
|
||||||
|
if len(s.inner) != len(other.inner) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
for item := range s.inner {
|
||||||
|
if !other.Contains(item) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s Set[T]) Length() int {
|
||||||
|
return len(s.inner)
|
||||||
|
}
|
||||||
|
|
||||||
func (s Set[T]) ToArray() []T {
|
func (s Set[T]) ToArray() []T {
|
||||||
array := []T{}
|
array := []T{}
|
||||||
for i := range s.inner {
|
for i := range s.inner {
|
||||||
|
|||||||
Reference in New Issue
Block a user