restructure repo

This commit is contained in:
2025-03-17 21:48:21 +01:00
parent 833c396b9b
commit bbf66ddc74
160 changed files with 49 additions and 1163 deletions

View File

@@ -0,0 +1,11 @@
package pkg
import (
"os"
)
// Checks if file on path exists or not
func FileExists(path string) bool {
_, err := os.Stat(path)
return !os.IsNotExist(err)
}

View File

@@ -0,0 +1,27 @@
package pkg
import (
"os"
"testing"
)
func TestFileExists(t *testing.T) {
// Create a temporary file for testing
file, err := os.CreateTemp("", "testfile")
if err != nil {
t.Fatalf("Failed to create temporary file: %v", err)
}
defer os.Remove(file.Name())
// Test with an existing file
exists := FileExists(file.Name())
if !exists {
t.Errorf("FileExists(%q) = false, want true", file.Name())
}
// Test with a non-existing file
exists = FileExists("non_existing_file.txt")
if exists {
t.Errorf("FileExists(%q) = true, want false", "non_existing_file.txt")
}
}

View File

@@ -0,0 +1,54 @@
package healthcheck
import (
"net/http"
"sync"
"github.com/gin-gonic/gin"
)
type HealthCheck interface {
SetStatus(status string)
GetStatus() string
HealthCheckHandler() gin.HandlerFunc
}
type healthCheck struct {
status string
sync sync.Mutex
}
func New() HealthCheck {
return &healthCheck{
status: "ok",
}
}
func (hc *healthCheck) SetStatus(status string) {
hc.sync.Lock()
defer hc.sync.Unlock()
hc.status = status
}
func (hc *healthCheck) GetStatus() string {
hc.sync.Lock()
defer hc.sync.Unlock()
return hc.status
}
func (hc *healthCheck) HealthCheckHandler() gin.HandlerFunc {
return func(c *gin.Context) {
switch hc.GetStatus() {
case "nok":
c.JSON(http.StatusInternalServerError, gin.H{
"status": hc.GetStatus(),
})
default:
c.JSON(http.StatusOK, gin.H{
"status": hc.GetStatus(),
})
}
}
}

View File

@@ -0,0 +1,131 @@
package healthcheck
import (
"net/http"
"net/http/httptest"
"testing"
"github.com/gin-gonic/gin"
)
func TestHealthCheck_GetStatus(t *testing.T) {
var hc HealthCheck
type fields struct {
status string
}
tests := []struct {
name string
fields fields
want string
}{
{
name: "Test GetStatus with status ok",
fields: fields{
status: "ok",
},
want: "ok",
},
{
name: "Test GetStatus with status nok",
fields: fields{
status: "nok",
},
want: "nok",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
hc = &healthCheck{
status: tt.fields.status,
}
if got := hc.GetStatus(); got != tt.want {
t.Errorf("HealthCheck.GetStatus() = %v, want %v", got, tt.want)
}
})
}
}
func TestHealthCheck_SetStatus(t *testing.T) {
hc := New()
type args struct {
status string
}
tests := []struct {
name string
args args
want string
}{
{
name: "Test SetStatus with status ok",
args: args{
status: "ok",
},
want: "ok",
},
{
name: "Test SetStatus with status nok",
args: args{
status: "nok",
},
want: "nok",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
hc.SetStatus(tt.args.status)
if got := hc.GetStatus(); got != tt.want {
t.Errorf("HealthCheck.GetStatus() = %v, want %v", got, tt.want)
}
})
}
}
func TestHealthCheck_HealthCheckHandler(t *testing.T) {
r := gin.Default()
hc := New()
r.GET("/health", hc.HealthCheckHandler())
type args struct {
status string
}
tests := []struct {
name string
args args
want string
statusCode int
}{
{
name: "Test respond with status ok",
args: args{
status: "ok",
},
want: `{"status":"ok"}`,
statusCode: http.StatusOK,
},
{
name: "Test respond with status nok",
args: args{
status: "nok",
},
want: `{"status":"nok"}`,
statusCode: http.StatusInternalServerError,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
hc.SetStatus(tt.args.status)
w := httptest.NewRecorder()
req, _ := http.NewRequest("GET", "/health", nil)
r.ServeHTTP(w, req)
if got := w.Code; got != tt.statusCode {
t.Errorf("HealthCheck response status code = %v, want %v", got, tt.statusCode)
}
if got := w.Body.String(); got != tt.want {
t.Errorf("HealthCheck response = %v, want %v", got, tt.want)
}
})
}
}

View File

@@ -0,0 +1,30 @@
package memgraph
import (
"fmt"
"time"
"github.com/neo4j/neo4j-go-driver/v5/neo4j"
"golang.org/x/net/context"
)
func (p *Person) CreatePerson(driver neo4j.DriverWithContext) (*neo4j.Record, error) {
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
session := driver.NewSession(ctx, neo4j.SessionConfig{AccessMode: neo4j.AccessModeWrite})
defer session.Close(ctx)
if err := p.Verify(); err != nil {
return nil, err
}
query := fmt.Sprintf("CREATE (n:Person {%s}) RETURN n;", p.ToString())
result, err := session.Run(ctx, query, nil)
if err != nil {
return nil, err
}
return result.Single(ctx)
}

View File

@@ -0,0 +1,39 @@
package memgraph
import (
"fmt"
"time"
"github.com/neo4j/neo4j-go-driver/v5/neo4j"
"golang.org/x/net/context"
)
func (r *Relationship) CreateRelationship(driver neo4j.DriverWithContext) (*neo4j.Record, error) {
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
session := driver.NewSession(ctx, neo4j.SessionConfig{AccessMode: neo4j.AccessModeWrite})
defer session.Close(ctx)
if err := r.Verify(); err != nil {
return nil, err
}
query := fmt.Sprintf(`MATCH (a:Person), (b:Person) WHERE a.id = '%s' AND b.id = '%s'`, r.FirstPersonID, r.SecondPersonID)
if r.Direction == "->" {
query = fmt.Sprintf(`%s CREATE (a)-[r:%s {verified: false}]->(b) RETURN r;`, query, r.Relationship)
} else if r.Direction == "<-" {
query = fmt.Sprintf(`%s CREATE (a)<-[r:%s {verified: false}]-(b) RETURN r;`, query, r.Relationship)
} else {
query = fmt.Sprintf(`%s CREATE (a)<-[r1:%s {verified: True}]-(b) CREATE (a)-[r2:%s {verified: True}]->(b) RETURN r1, r2;`,
query, r.Relationship, r.Relationship)
}
result, err := session.Run(ctx, query, nil)
if err != nil {
return nil, err
}
return result.Single(ctx)
}

View File

@@ -0,0 +1,45 @@
package memgraph
import (
"fmt"
"strings"
"time"
"github.com/google/uuid"
"github.com/neo4j/neo4j-go-driver/v5/neo4j"
"golang.org/x/net/context"
)
func (rp *RelationshipAndPerson) CreateRelationshipAndPerson(driver neo4j.DriverWithContext) (*neo4j.Record, error) {
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
session := driver.NewSession(ctx, neo4j.SessionConfig{AccessMode: neo4j.AccessModeWrite})
defer session.Close(ctx)
if err := rp.Verify(); err != nil {
return nil, err
}
rp.Person.ID = strings.ReplaceAll(uuid.New().String(), "-", "")
query := fmt.Sprintf(`MATCH (a:Person) WHERE a.id = '%s'`, rp.Relationship.FirstPersonID)
query = fmt.Sprintf("%s CREATE (b:Person {%s})", query, rp.Person.ToString())
if rp.Relationship.Direction == "->" {
query = fmt.Sprintf(`%s CREATE (a)-[r:%s {verified: True}]->(b) RETURN r;`, query, rp.Relationship.Relationship)
} else if rp.Relationship.Direction == "<-" {
query = fmt.Sprintf(`%s CREATE (a)<-[r:%s {verified: True}]-(b) RETURN r;`, query, rp.Relationship.Relationship)
} else {
query = fmt.Sprintf(`%s CREATE (a)<-[r1:%s {verified: True}]-(b) CREATE (a)-[r2:%s {verified: True}]->(b) RETURN r1, r2, b;`,
query, rp.Relationship.Relationship, rp.Relationship.Relationship)
}
result, err := session.Run(ctx, query, nil)
if err != nil {
return nil, err
}
return result.Single(ctx)
}

View File

@@ -0,0 +1,66 @@
package memgraph
import (
"time"
"github.com/neo4j/neo4j-go-driver/v5/neo4j"
"golang.org/x/net/context"
)
const dbCreateSchemaTimeout = 10 * time.Second
func createIndexes(driver neo4j.DriverWithContext) error {
ctx, cancel := context.WithTimeout(context.Background(), dbCreateSchemaTimeout)
defer cancel()
session := driver.NewSession(ctx, neo4j.SessionConfig{AccessMode: neo4j.AccessModeWrite})
defer session.Close(ctx)
indexes := []string{
`CREATE INDEX ON :Person(id);`,
`CREATE INDEX ON :Person(last_name);`,
`CREATE INDEX ON :Person(first_name);`,
`CREATE INDEX ON :Person(born);`,
`CREATE INDEX ON :Person(mothers_first_name);`,
`CREATE INDEX ON :Person(mothers_last_name);`,
}
// Run index queries via implicit auto-commit transaction
for _, index := range indexes {
_, err := session.Run(ctx, index, nil)
if err != nil {
return err
}
}
return nil
}
func createConstraints(driver neo4j.DriverWithContext) error {
ctx, cancel := context.WithTimeout(context.Background(), dbCreateSchemaTimeout)
defer cancel()
session := driver.NewSession(ctx, neo4j.SessionConfig{AccessMode: neo4j.AccessModeWrite})
defer session.Close(ctx)
constraints := []string{
`CREATE CONSTRAINT ON (n:Person) ASSERT EXISTS (n.id);`,
`CREATE CONSTRAINT ON (n:Person) ASSERT EXISTS (n.last_name);`,
`CREATE CONSTRAINT ON (n:Person) ASSERT EXISTS (n.first_name);`,
`CREATE CONSTRAINT ON (n:Person) ASSERT EXISTS (n.born);`,
`CREATE CONSTRAINT ON (n:Person) ASSERT EXISTS (n.mothers_first_name);`,
`CREATE CONSTRAINT ON (n:Person) ASSERT EXISTS (n.mothers_last_name);`,
`CREATE CONSTRAINT ON (n:Person) ASSERT n.id IS UNIQUE;`,
`CREATE CONSTRAINT ON (n:Person) ASSERT n.last_name, n.first_name, n.born, n.mothers_first_name, n.mothers_last_name IS UNIQUE;`,
}
// Run index queries via implicit auto-commit transaction
for _, constraint := range constraints {
_, err := session.Run(ctx, constraint, nil)
if err != nil {
return err
}
}
return nil
}

View File

@@ -0,0 +1,124 @@
package memgraph
import (
"fmt"
"strings"
)
var cypherKeywords = []string{
"CREATE",
"DELETE",
"DETACH",
"DETACH DELETE",
"FOREACH",
"LOAD CSV",
"MERGE",
"MATCH",
"ON",
"OPTIONAL MATCH",
"REMOVE",
"SET",
"START",
"UNION",
"UNWIND",
"WITH",
"RETURN",
"ORDER BY",
"SKIP",
"LIMIT",
"ASC",
"DESC",
"EXISTS",
"CALL",
"USING",
"CONSTRAINT",
"DROP",
"INDEX",
"WHERE",
}
var cypherOperators = []string{
"+",
"-",
"*",
"/",
"%",
"^",
"=",
"<",
">",
"<=",
">=",
"<>",
"AND",
"OR",
"XOR",
"NOT",
"IN",
"STARTS WITH",
"ENDS WITH",
"CONTAINS",
"IS NULL",
"IS NOT NULL",
"IS UNIQUE",
"IS NODE",
"IS RELATIONSHIP",
"IS PROPERTY KEY",
"IS MAP",
"IS LIST",
"IS BOOLEAN",
"IS STRING",
"IS NUMBER",
"IS INTEGER",
"IS FLOAT",
"IS NODE",
"IS RELATIONSHIP",
"IS PATH",
"IS POINT",
"IS DATE",
"IS DURATION",
}
// cypherDelimiters contains the delimiters that need to be escaped in a string to prevent cypher injection keys are the delimiters that need to be escaped and values are the escaped delimiters
var cypherDelimiters = map[string]string{
"'": `\'`,
`"`: `\"`,
`\u0027`: `\\u0027`,
`\u0022`: "\\\\u0022",
"`": "``",
"\\u0060": "\\u0060\\u0060",
}
// VerifyString verifies if a string is valid and does not contain cypher injection
func VerifyString(s string) error {
s = strings.ToUpper(s)
for _, keyword := range cypherKeywords {
if strings.Contains(s, keyword) {
return fmt.Errorf("invalid string: %s contains cypher keyword: %s", s, keyword)
}
}
for _, operator := range cypherOperators {
if strings.Contains(s, operator) {
return fmt.Errorf("invalid string: %s contains cypher operator: %s", s, operator)
}
}
for key := range cypherDelimiters {
if strings.Contains(s, key) {
return fmt.Errorf("invalid string: %s contains cypher delimiter: %s", s, key)
}
}
return nil
}
// EscapeString escapes delimiters in a string to prevent cypher injection
func EscapeString(s string) string {
result := s
for k, v := range cypherDelimiters {
result = strings.ReplaceAll(result, k, v)
}
return result
}

View File

@@ -0,0 +1,27 @@
package memgraph
import (
"fmt"
"time"
"github.com/neo4j/neo4j-go-driver/v5/neo4j"
"golang.org/x/net/context"
)
func (p *Person) DeletePerson(driver neo4j.DriverWithContext) error {
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
session := driver.NewSession(ctx, neo4j.SessionConfig{AccessMode: neo4j.AccessModeWrite})
defer session.Close(ctx)
if err := p.Verify(); err != nil {
return err
}
query := fmt.Sprintf("MATCH (n:Person {id: '%s'}) DELETE n;", p.ID)
_, err := session.Run(ctx, query, nil)
return err
}

View File

@@ -0,0 +1,36 @@
package memgraph
import (
"fmt"
"time"
"github.com/neo4j/neo4j-go-driver/v5/neo4j"
"golang.org/x/net/context"
)
func (r *Relationship) DeleteRelationship(driver neo4j.DriverWithContext) error {
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
session := driver.NewSession(ctx, neo4j.SessionConfig{AccessMode: neo4j.AccessModeWrite})
defer session.Close(ctx)
if err := r.Verify(); err != nil {
return err
}
query := ""
if r.Direction == "->" {
query = fmt.Sprintf(`MATCH (a)-[r:%s]->(b)`, r.Relationship)
} else if r.Direction == "<-" {
query = fmt.Sprintf(`MATCH (a)<-[r:%s]-(b)`, r.Relationship)
} else {
query = fmt.Sprintf(`MATCH (a)-[r:%s]-(b)`, r.Relationship)
}
query = fmt.Sprintf(`%s WHERE a.id = '%s' AND b.id = '%s' DELETE r;`, query, r.FirstPersonID, r.SecondPersonID)
_, err := session.Run(ctx, query, nil)
return err
}

View File

@@ -0,0 +1,32 @@
package memgraph
import (
"context"
"log"
"github.com/neo4j/neo4j-go-driver/v5/neo4j"
)
func InitDatabase(dbURI, dbUser, dbPassword string) neo4j.DriverWithContext {
driver, err := neo4j.NewDriverWithContext(dbURI, neo4j.BasicAuth(dbUser, dbPassword, ""))
if err != nil {
log.Panicln(err)
}
ctx := context.Background()
err = driver.VerifyConnectivity(ctx)
if err != nil {
log.Panicln(err)
}
if err := createIndexes(driver); err != nil {
log.Panicln(err)
}
if err := createConstraints(driver); err != nil {
log.Panicln(err)
}
return driver
}

View File

@@ -0,0 +1,272 @@
package memgraph
import (
"fmt"
"time"
)
var RelationshipTypes = []string{
"Parent",
"Child",
"Spouse",
"Sibling",
}
type Person struct {
ID string `json:"id"`
Firstname string `json:"first_name"`
Middlename string `json:"middle_name"`
Lastname string `json:"last_name"`
Titles []string `json:"titles"` // e.g. Jr., Sr., III
Suffixes []string `json:"suffixes"` // e.g. Ph.D., M.D.
ExtraNames []string `json:"extra_names"`
Aliases []string `json:"aliases"`
MothersFirstname string `json:"mothers_first_name"`
MothersLastname string `json:"mothers_last_name"`
Born time.Time `json:"born"`
Birthplace string `json:"birthplace"`
Residence string `json:"residence"`
Death time.Time `json:"death"`
Deathplace string `json:"deathplace"`
LifeEvents []map[string]string `json:"life_events"`
Occupations []string `json:"occupation"`
OccupationToDisplay string `json:"occupation_to_display"`
OthersSaid map[string]string `json:"others_said"`
Photos map[string]string `json:"photos"`
ProfilePicture string `json:"profile_picture"`
verified bool
}
func (p *Person) ToString() string {
result := fmt.Sprintf("id: '%s'", p.ID)
if p.Firstname != "" {
result = fmt.Sprintf("%s, first_name: '%s'", result, p.Firstname)
}
if p.Lastname != "" {
result = fmt.Sprintf("%s, last_name: '%s'", result, p.Lastname)
}
if p.Middlename != "" {
result = fmt.Sprintf("%s, middle_name: '%s'", result, p.Middlename)
}
if p.MothersFirstname != "" {
result = fmt.Sprintf("%s, mothers_first_name: '%s'", result, p.MothersFirstname)
}
if p.MothersLastname != "" {
result = fmt.Sprintf("%s, mothers_last_name: '%s'", result, p.MothersLastname)
}
if !p.Born.IsZero() {
result = fmt.Sprintf("%s, born: date({year:%d, month:%d, day:%d})", result, p.Born.Year(), p.Born.Month(), p.Born.Day())
}
if !p.Death.IsZero() {
result = fmt.Sprintf("%s, death: date({year:%d, month:%d, day:%d})", result, p.Death.Year(), p.Death.Month(), p.Death.Day())
}
if p.Birthplace != "" {
result = fmt.Sprintf("%s, birthplace: '%s'", result, p.Birthplace)
}
if p.Residence != "" {
result = fmt.Sprintf("%s, residence: '%s'", result, p.Residence)
}
if p.Deathplace != "" {
result = fmt.Sprintf("%s, deathplace: '%s'", result, p.Deathplace)
}
if p.OccupationToDisplay != "" {
result = fmt.Sprintf("%s, occupation_to_display: '%s'", result, p.OccupationToDisplay)
}
if p.ProfilePicture != "" {
result = fmt.Sprintf("%s, profile_picture: '%s'", result, p.ProfilePicture)
}
if p.Titles != nil && len(p.Titles) > 0 {
result = fmt.Sprintf("%s, titles: [", result)
for _, title := range p.Titles {
result = fmt.Sprintf("%s'%s', ", result, title)
}
result = fmt.Sprintf("%s]", result[:len(result)-2])
}
if p.Suffixes != nil && len(p.Suffixes) > 0 {
result = fmt.Sprintf("%s, suffixes: [", result)
for _, suffix := range p.Suffixes {
result = fmt.Sprintf("%s'%s', ", result, suffix)
}
result = fmt.Sprintf("%s]", result[:len(result)-2])
}
if p.ExtraNames != nil && len(p.ExtraNames) > 0 {
result = fmt.Sprintf("%s, extra_names: [", result)
for _, extraName := range p.ExtraNames {
result = fmt.Sprintf("%s'%s', ", result, extraName)
}
result = fmt.Sprintf("%s]", result[:len(result)-2])
}
if p.Aliases != nil && len(p.Aliases) > 0 {
result = fmt.Sprintf("%s, aliases: [", result)
for _, alias := range p.Aliases {
result = fmt.Sprintf("%s'%s', ", result, alias)
}
result = fmt.Sprintf("%s]", result[:len(result)-2])
}
if p.LifeEvents != nil && len(p.LifeEvents) > 0 {
result = fmt.Sprintf("%s, life_events: [", result)
for i := 0; i < len(p.LifeEvents); i++ {
date, dok := p.LifeEvents[i]["date"]
event, eok := p.LifeEvents[i]["event"]
if dok && eok {
result = fmt.Sprintf("%s{date: '%s', event: '%s'}, ", result, date, event)
}
}
result = fmt.Sprintf("%s]", result[:len(result)-2])
}
if p.Occupations != nil && len(p.Occupations) > 0 {
result = fmt.Sprintf("%s, occupations: [", result)
for _, occupation := range p.Occupations {
result = fmt.Sprintf("%s'%s', ", result, occupation)
}
result = fmt.Sprintf("%s]", result[:len(result)-2])
}
if p.OthersSaid != nil {
result = fmt.Sprintf("%s, others_said: {", result)
for key, value := range p.OthersSaid {
result = fmt.Sprintf("%s%s: '%s', ", result, key, value)
}
result = fmt.Sprintf("%s}", result[:len(result)-2])
}
if p.Photos != nil && len(p.Photos) > 0 {
result = fmt.Sprintf("%s, photos: {", result)
for key, value := range p.Photos {
result = fmt.Sprintf("%s%s: '%s', ", result, key, value)
}
result = fmt.Sprintf("%s}", result[:len(result)-2])
}
return result
}
// Verify checks if the person is valid and does not contain cypher injection it also escapes the delimiters contained in any of the strings
func (p *Person) Verify() error {
if p.verified {
return nil
}
if err := VerifyString(p.ID); err != nil {
return fmt.Errorf("invalid ID type %s", err)
}
p.Firstname = EscapeString(p.Firstname)
p.Middlename = EscapeString(p.Middlename)
p.Lastname = EscapeString(p.Lastname)
p.MothersFirstname = EscapeString(p.MothersFirstname)
p.MothersLastname = EscapeString(p.MothersLastname)
p.Birthplace = EscapeString(p.Birthplace)
p.Residence = EscapeString(p.Residence)
p.Deathplace = EscapeString(p.Deathplace)
p.OccupationToDisplay = EscapeString(p.OccupationToDisplay)
p.ProfilePicture = EscapeString(p.ProfilePicture)
for i, title := range p.Titles {
p.Titles[i] = EscapeString(title)
}
for i, suffix := range p.Suffixes {
p.Suffixes[i] = EscapeString(suffix)
}
for i, extraName := range p.ExtraNames {
p.ExtraNames[i] = EscapeString(extraName)
}
for i, alias := range p.Aliases {
p.Aliases[i] = EscapeString(alias)
}
for i, lifeEvent := range p.LifeEvents {
for key, value := range lifeEvent {
if key != "date" && key != "event" {
return fmt.Errorf("invalid key in life event")
}
p.LifeEvents[i][key] = EscapeString(value)
}
}
for i, occupation := range p.Occupations {
p.Occupations[i] = EscapeString(occupation)
}
for key, value := range p.OthersSaid {
if err := VerifyString(key); err != nil {
return fmt.Errorf("invalid key in others said %s", err)
}
p.OthersSaid[key] = EscapeString(value)
}
for key, value := range p.Photos {
if err := VerifyString(key); err != nil {
return fmt.Errorf("invalid key in photos %s", err)
}
p.Photos[key] = EscapeString(value)
}
p.verified = true
return nil
}
type Relationship struct {
FirstPersonID string `json:"first_person_id"`
SecondPersonID string `json:"second_person_id"`
Relationship string `json:"relationship"`
Direction string `json:"direction"`
}
// Verify checks if the relationship is valid and does not contain cypher injection
func (r *Relationship) Verify() error {
if r.Direction != "->" && r.Direction != "<-" && r.Direction != "-" {
return fmt.Errorf("invalid direction for relationship")
}
// Check if the relationship is in the list of valid relationships
found := false
for _, relationship := range RelationshipTypes {
if r.Relationship == relationship {
found = true
break
}
}
if !found {
return fmt.Errorf("invalid relationship type")
}
if err := VerifyString(r.FirstPersonID); err != nil {
return fmt.Errorf("invalid FirstPersonID type %s", err)
}
if err := VerifyString(r.SecondPersonID); err != nil {
return fmt.Errorf("invalid SecondPersonID type %s", err)
}
return nil
}
type RelationshipAndPerson struct {
Relationship Relationship `json:"relationship"`
Person Person `json:"person"`
}
func (r *RelationshipAndPerson) Verify() error {
if err := r.Relationship.Verify(); err != nil {
return err
}
if err := r.Person.Verify(); err != nil {
return err
}
return nil
}

View File

@@ -0,0 +1,77 @@
package memgraph
import (
"testing"
"time"
)
func TestPerson_ToString(t *testing.T) {
tests := []struct {
name string
p *Person
want string
}{
{
name: "Test with nil values",
p: &Person{
ID: "1",
Firstname: "John",
Lastname: "Doe",
MothersFirstname: "Jane",
MothersLastname: "Doe",
Born: time.Date(2021, 1, 1, 0, 0, 0, 0, time.UTC),
Birthplace: "New York",
Residence: "New York",
Death: time.Date(2021, 1, 1, 0, 0, 0, 0, time.UTC),
Deathplace: "New York",
},
want: "ID: '1', Firstname: 'John', Lastname: 'Doe', MothersFirstname: 'Jane', MothersLastname: 'Doe', Born: date({year:2021, month:1, day:1}), Death: date({year:2021, month:1, day:1}), Birthplace: 'New York', Residence: 'New York', Deathplace: 'New York', OccupationToDisplay: '', ProfilePicture: ''",
}, {
name: "Test with All values",
p: &Person{
ID: "1",
Firstname: "John",
Lastname: "Doe",
MothersFirstname: "Jane",
MothersLastname: "Doe",
Born: time.Date(2021, 1, 1, 0, 0, 0, 0, time.UTC),
Birthplace: "New York",
Residence: "New York",
Death: time.Date(2021, 1, 1, 0, 0, 0, 0, time.UTC),
Deathplace: "New York",
LifeEvents: []map[string]string{
{
"date": "2021-01-01",
"event": "Event 1",
},
{
"date": "2021-01-02",
"event": "Event 2",
},
},
Occupations: []string{
"Welder",
"Plumber",
},
OccupationToDisplay: "Welder",
OthersSaid: map[string]string{
"Beni": "He is a good person",
"Jani": "He is a bad person",
},
Photos: map[string]string{
"Profile": "profile.jpg",
"Family": "family.jpg",
},
ProfilePicture: "profile.jpg",
},
want: "ID: '1', Firstname: 'John', Lastname: 'Doe', MothersFirstname: 'Jane', MothersLastname: 'Doe', Born: date({year:2021, month:1, day:1}), Death: date({year:2021, month:1, day:1}), Birthplace: 'New York', Residence: 'New York', Deathplace: 'New York', OccupationToDisplay: 'Welder', ProfilePicture: 'profile.jpg', LifeEvents: [{date: '2021-01-01', event: 'Event 1'}, {date: '2021-01-02', event: 'Event 2'}], Occupations: ['Welder', 'Plumber'], OthersSaid: {Beni: 'He is a good person', Jani: 'He is a bad person'}, Photos: {Profile: 'profile.jpg', Family: 'family.jpg'}",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := tt.p.ToString(); got != tt.want {
t.Errorf("Person.ToString() = %v, want %v", got, tt.want)
}
})
}
}

View File

@@ -0,0 +1,30 @@
package memgraph
import (
"fmt"
"time"
"github.com/neo4j/neo4j-go-driver/v5/neo4j"
"golang.org/x/net/context"
)
func (p *Person) UpdatePerson(driver neo4j.DriverWithContext) (*neo4j.Record, error) {
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
session := driver.NewSession(ctx, neo4j.SessionConfig{AccessMode: neo4j.AccessModeWrite})
defer session.Close(ctx)
if err := p.Verify(); err != nil {
return nil, err
}
query := fmt.Sprintf("MATCH (n:Person {id: '%s'}) SET n += {%s} RETURN n;", p.ID, p.ToString())
result, err := session.Run(ctx, query, nil)
if err != nil {
return nil, err
}
return result.Single(ctx)
}

View File

@@ -0,0 +1,39 @@
package memgraph
import (
"fmt"
"time"
"github.com/neo4j/neo4j-go-driver/v5/neo4j"
"golang.org/x/net/context"
)
func (r *Relationship) VerifyRelationship(driver neo4j.DriverWithContext) (*neo4j.Record, error) {
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
session := driver.NewSession(ctx, neo4j.SessionConfig{AccessMode: neo4j.AccessModeWrite})
defer session.Close(ctx)
if err := r.Verify(); err != nil {
return nil, err
}
query := ""
if r.Direction == "->" {
query = fmt.Sprintf(`MATCH (a)-[r:%s]->(b)`, r.Relationship)
} else if r.Direction == "<-" {
query = fmt.Sprintf(`MATCH (a)<-[r:%s]-(b)`, r.Relationship)
} else {
query = fmt.Sprintf(`MATCH (a)-[r:%s]-(b)`, r.Relationship)
}
query = fmt.Sprintf(`%s WHERE a.id = %s AND b.id = %s set r.verified = true return r;`, query, r.FirstPersonID, r.SecondPersonID)
result, err := session.Run(ctx, query, nil)
if err != nil {
return nil, err
}
return result.Single(ctx)
}

View File

@@ -0,0 +1,40 @@
package pkg
import (
"errors"
"log"
"net/http"
"time"
)
func SetupHttpsServer(router http.Handler, cert, key, httpsPort, httpPort string, requestTimeout time.Duration) (server *http.Server) {
if FileExists(cert) && FileExists(key) {
server = &http.Server{
Addr: httpsPort,
Handler: router,
ReadTimeout: 5 * time.Second,
WriteTimeout: 10 * time.Second,
}
go func() {
log.Println("Server starts at port ", httpsPort)
if err := server.ListenAndServeTLS(cert, key); err != nil && errors.Is(err, http.ErrServerClosed) {
log.Fatal(err)
}
}()
} else {
server = &http.Server{
Addr: httpPort,
Handler: router,
ReadTimeout: requestTimeout * time.Second,
WriteTimeout: requestTimeout * time.Second,
}
go func() {
log.Println("Server starts at port ", httpPort)
if err := server.ListenAndServe(); err != nil && errors.Is(err, http.ErrServerClosed) {
log.Fatal(err)
}
}()
}
return server
}

View File

@@ -0,0 +1,29 @@
package pkg
import (
"fmt"
"io"
"log"
"os"
"time"
"github.com/gin-gonic/gin"
)
func SetupLogger(logToFileAndStd bool, logToFile bool) {
if logToFileAndStd || logToFile {
gin.DisableConsoleColor() // Disable Console Color, you don't need console color when writing the logs to file.
path := fmt.Sprintf("private/logs/%02dy_%02dm_%02dd_%02dh_%02dm_%02ds.log", time.Now().Year(), time.Now().Month(), time.Now().Day(), time.Now().Hour(), time.Now().Minute(), time.Now().Second())
logerror1 := os.MkdirAll("private/logs/", 0755)
f, logerror2 := os.Create(path)
if logerror1 != nil || logerror2 != nil {
log.Println("Cant log to file")
} else if logToFileAndStd {
gin.DefaultWriter = io.MultiWriter(f, os.Stdout)
} else {
gin.DefaultWriter = io.MultiWriter(f)
}
}
log.SetFlags(log.Ldate | log.Ltime | log.Lshortfile)
log.SetOutput(gin.DefaultErrorWriter)
}