diff --git a/apps/db-adapter/example-payloads/create_person.bru b/apps/db-adapter/example-payloads/create_person.bru new file mode 100644 index 0000000..22b8676 --- /dev/null +++ b/apps/db-adapter/example-payloads/create_person.bru @@ -0,0 +1,28 @@ +meta { + name: create_person + type: http + seq: 5 +} + +post { + url: http://localhost:8080/person + body: json + auth: inherit +} + +headers { + X-User-Id: 1 + X-User-Name: Alice Wonderland +} + +body:json { + { + "first_name": "Jhon", + "last_name": "Doe", + "born": "1985-07-01", + "limit": 1000, + "mothers_first_name": "Mary", + "mothers_last_name": "Smith", + "email": "jd@example.com" + } +} diff --git a/apps/db-adapter/example-payloads/generate_invite_code.bru b/apps/db-adapter/example-payloads/generate_invite_code.bru new file mode 100644 index 0000000..2acdb3d --- /dev/null +++ b/apps/db-adapter/example-payloads/generate_invite_code.bru @@ -0,0 +1,19 @@ +meta { + name: generate_invite_code + type: http + seq: 8 +} + +patch { + url: http://localhost:8080/person/{{id}} + body: none + auth: inherit +} + +headers { + X-User-Id: 2 +} + +vars:pre-request { + id: 10 +} diff --git a/apps/db-adapter/example-payloads/get_person_by_google_id.bru b/apps/db-adapter/example-payloads/get_person_by_google_id.bru index 320a262..61cee17 100644 --- a/apps/db-adapter/example-payloads/get_person_by_google_id.bru +++ b/apps/db-adapter/example-payloads/get_person_by_google_id.bru @@ -9,3 +9,7 @@ get { body: none auth: inherit } + +vars:pre-request { + google_id: alice123 +} diff --git a/apps/db-adapter/example-payloads/hard_delete_person.bru b/apps/db-adapter/example-payloads/hard_delete_person.bru new file mode 100644 index 0000000..88d3606 --- /dev/null +++ b/apps/db-adapter/example-payloads/hard_delete_person.bru @@ -0,0 +1,19 @@ +meta { + name: hard_delete_person + type: http + seq: 9 +} + +delete { + url: http://localhost:8080/person/{{id}} + body: none + auth: inherit +} + +headers { + X-User-Id: 2 +} + +vars:pre-request { + id: 10 +} diff --git a/apps/db-adapter/example-payloads/soft_delete_person.bru b/apps/db-adapter/example-payloads/soft_delete_person.bru new file mode 100644 index 0000000..6fa9d17 --- /dev/null +++ b/apps/db-adapter/example-payloads/soft_delete_person.bru @@ -0,0 +1,11 @@ +meta { + name: soft_delete_person + type: http + seq: 6 +} + +delete { + url: http://localhost:8080/person/{{id}} + body: none + auth: inherit +} diff --git a/apps/db-adapter/example-payloads/update_person.bru b/apps/db-adapter/example-payloads/update_person.bru new file mode 100644 index 0000000..151d650 --- /dev/null +++ b/apps/db-adapter/example-payloads/update_person.bru @@ -0,0 +1,172 @@ +meta { + name: update_person + type: http + seq: 8 +} + +patch { + url: http://localhost:8080/person/{{id}} + body: json + auth: inherit +} + +headers { + X-User-Id: 2 +} + +body:json { + { + "invite_code": "ABCD1234", + "first_name": "John", + "middle_name": "Fitzgerald", + "last_name": "Doe", + "titles": [ + "Dr.", + "Prof." + ], + "suffixes": [ + "Jr." + ], + "extra_names": [ + "Johnny", + "J.D." + ], + "aliases": [ + "The Professor" + ], + "mothers_first_name": "Jane", + "mothers_last_name": "Smith", + "born": "1980-05-15", + "place_of_birth": "New York, USA", + "died": null, + "place_of_death": null, + "life_events": [ + { + "from": "2000-01-01", + "to": "2004-12-31", + "description": "Studied at Harvard University" + }, + { + "from": "2005-01-01", + "to": "2015-01-01", + "description": "Worked at NASA" + } + ], + "occupations": [ + "Engineer", + "Professor" + ], + "occupation_to_display": "Professor", + "others_said": [ + { + "id": 1, + "name": "Alice Johnson", + "relationship": "Colleague", + "description": "John was always a dedicated professional.", + "url": "https://example.com/testimonial" + } + ], + "limit": 10, + "photos": [ + { + "url": "https://example.com/photo1.jpg", + "description": "Graduation day", + "date": "2004-06-15", + "name": "Harvard Graduation" + } + ], + "videos": [ + { + "url": "https://example.com/video1.mp4", + "description": "Interview about Mars mission", + "date": "2012-09-10", + "name": "NASA Interview" + } + ], + "audios": [ + { + "url": "https://example.com/audio1.mp3", + "description": "Podcast guest appearance", + "date": "2020-11-20", + "name": "Science Today Podcast" + } + ], + "profile_picture": "https://example.com/profile.jpg", + "verified": true, + "email": "john.doe@example.com", + "phone": "+1-555-123-4567", + "residence": { + "city": "San Francisco", + "country": "USA", + "zip_code": "94103", + "address_line_1": "123 Main St", + "address_line_2": "Apt 4B" + }, + "religion": "Agnostic", + "baptized": null, + "ideologies": [ + "Environmentalism", + "Humanism" + ], + "blood_type": "O+", + "allergies": [ + "Peanuts", + "Pollen" + ], + "medications": [ + { + "name": "Ibuprofen", + "description": "Pain relief", + "components": "Ibuprofen 400mg", + "dosage": "Once a day", + "from": "2024-01-01", + "to": null + } + ], + "medical_conditions": [ + { + "condition": "Hypertension", + "diagnosed_on": "2023-03-01" + } + ], + "height": 180.5, + "weight": 75.0, + "hair_colour": "Brown", + "skin_colour": "Light", + "eye_colour": "Green", + "sports": [ + "Tennis", + "Cycling" + ], + "hobbies": [ + "Painting", + "Chess" + ], + "interests": [ + "Astronomy", + "AI Research" + ], + "languages": [ + { + "language": "English", + "level": "Native" + }, + { + "language": "French", + "level": "Intermediate" + } + ], + "notes": [ + { + "date": "2023-12-01", + "title": "Family Tree Interview", + "note": "Discussed with John's aunt about his early years.", + "url": "https://example.com/note1" + } + ] + } +} + +vars:pre-request { + id: 10 +} diff --git a/apps/db-adapter/integration-tests/payloads/create_other_person.json b/apps/db-adapter/integration-tests/payloads/create_other_person.json new file mode 100644 index 0000000..23f1750 --- /dev/null +++ b/apps/db-adapter/integration-tests/payloads/create_other_person.json @@ -0,0 +1,9 @@ +{ + "first_name": "Jhon", + "last_name": "Doe", + "born": "1985-07-01", + "limit": 1000, + "mothers_first_name": "Mary", + "mothers_last_name": "Smith", + "email": "jd@example.com" +} \ No newline at end of file diff --git a/apps/db-adapter/integration-tests/payloads/generate_invite_code.json b/apps/db-adapter/integration-tests/payloads/generate_invite_code.json new file mode 100644 index 0000000..0c6fb2c --- /dev/null +++ b/apps/db-adapter/integration-tests/payloads/generate_invite_code.json @@ -0,0 +1,3 @@ +{ + "invite_code": "test-invite-code" +} \ No newline at end of file diff --git a/apps/db-adapter/integration-tests/payloads/update_person.json b/apps/db-adapter/integration-tests/payloads/update_person.json new file mode 100644 index 0000000..890ccf1 --- /dev/null +++ b/apps/db-adapter/integration-tests/payloads/update_person.json @@ -0,0 +1,150 @@ +{ + "invite_code": "ABCD1234", + "first_name": "John", + "middle_name": "Fitzgerald", + "last_name": "Doe", + "titles": [ + "Dr.", + "Prof." + ], + "suffixes": [ + "Jr." + ], + "extra_names": [ + "Johnny", + "J.D." + ], + "aliases": [ + "The Professor" + ], + "mothers_first_name": "Jane", + "mothers_last_name": "Smith", + "born": "1980-05-15", + "place_of_birth": "New York, USA", + "died": null, + "place_of_death": null, + "life_events": [ + { + "from": "2000-01-01", + "to": "2004-12-31", + "description": "Studied at Harvard University" + }, + { + "from": "2005-01-01", + "to": "2015-01-01", + "description": "Worked at NASA" + } + ], + "occupations": [ + "Engineer", + "Professor" + ], + "occupation_to_display": "Professor", + "others_said": [ + { + "id": 1, + "name": "Alice Johnson", + "relationship": "Colleague", + "description": "John was always a dedicated professional.", + "url": "https://example.com/testimonial" + } + ], + "limit": 10, + "photos": [ + { + "url": "https://example.com/photo1.jpg", + "description": "Graduation day", + "date": "2004-06-15", + "name": "Harvard Graduation" + } + ], + "videos": [ + { + "url": "https://example.com/video1.mp4", + "description": "Interview about Mars mission", + "date": "2012-09-10", + "name": "NASA Interview" + } + ], + "audios": [ + { + "url": "https://example.com/audio1.mp3", + "description": "Podcast guest appearance", + "date": "2020-11-20", + "name": "Science Today Podcast" + } + ], + "profile_picture": "https://example.com/profile.jpg", + "verified": true, + "email": "john.doe@example.com", + "phone": "+1-555-123-4567", + "residence": { + "city": "San Francisco", + "country": "USA", + "zip_code": "94103", + "address_line_1": "123 Main St", + "address_line_2": "Apt 4B" + }, + "religion": "Agnostic", + "baptized": null, + "ideologies": [ + "Environmentalism", + "Humanism" + ], + "blood_type": "O+", + "allergies": [ + "Peanuts", + "Pollen" + ], + "medications": [ + { + "name": "Ibuprofen", + "description": "Pain relief", + "components": "Ibuprofen 400mg", + "dosage": "Once a day", + "from": "2024-01-01", + "to": null + } + ], + "medical_conditions": [ + { + "condition": "Hypertension", + "diagnosed_on": "2023-03-01" + } + ], + "height": 180.5, + "weight": 75.0, + "hair_colour": "Brown", + "skin_colour": "Light", + "eye_colour": "Green", + "sports": [ + "Tennis", + "Cycling" + ], + "hobbies": [ + "Painting", + "Chess" + ], + "interests": [ + "Astronomy", + "AI Research" + ], + "languages": [ + { + "language": "English", + "level": "Native" + }, + { + "language": "French", + "level": "Intermediate" + } + ], + "notes": [ + { + "date": "2023-12-01", + "title": "Family Tree Interview", + "note": "Discussed with John's aunt about his early years.", + "url": "https://example.com/note1" + } + ] +} \ No newline at end of file diff --git a/apps/db-adapter/integration-tests/person.go b/apps/db-adapter/integration-tests/person.go index aec988a..43011a7 100644 --- a/apps/db-adapter/integration-tests/person.go +++ b/apps/db-adapter/integration-tests/person.go @@ -1,16 +1,187 @@ package integration_tests -func CreatePerson() { +import ( + "bytes" + _ "embed" + "encoding/json" + "net/http" + "testing" + + "github.com/stretchr/testify/require" + "github.com/vcscsvcscs/GenerationsHeritage/apps/db-adapter/pkg/api" +) + +//go:embed payloads\create_other_person.json +var create_other_person []byte + +func CreatePersonTest(dbAdapterUri string, client *http.Client) func(t *testing.T) { + return func(t *testing.T) { + url := dbAdapterUri + "/person" + + req, err := http.NewRequestWithContext(t.Context(), http.MethodPost, url, bytes.NewBuffer(create_other_person)) + require.NoError(t, err) + req.Header.Set("Content-Type", "application/json") + req.Header.Set("X-User-ID", "0") + req.Header.Set("X-User-Name", "application/json") + + // Send the request + resp, err := client.Do(req) + require.NoError(t, err) + defer resp.Body.Close() + + var responseBody map[string]any + err = json.NewDecoder(resp.Body).Decode(&responseBody) + require.NoError(t, err) + + // Validate the response + require.Equal(t, http.StatusOK, resp.StatusCode) + + _, ok := responseBody["Id"] + require.True(t, ok) + + require.Equal(t, "Jhon", responseBody["Props"].(map[string]any)["first_name"]) + require.Equal(t, "Doe", responseBody["Props"].(map[string]any)["last_name"]) + } } -func GetPersonById() { +func GetPersonById(dbAdapterUri string, client *http.Client) func(t *testing.T) { + return func(t *testing.T) { + url := dbAdapterUri + "/person/0" + + req, err := http.NewRequestWithContext(t.Context(), http.MethodGet, url, http.NoBody) + require.NoError(t, err) + req.Header.Set("Content-Type", "application/json") + req.Header.Set("X-User-ID", "0") + + // Send the request + resp, err := client.Do(req) + require.NoError(t, err) + defer resp.Body.Close() + + var responseBody map[string]any + err = json.NewDecoder(resp.Body).Decode(&responseBody) + require.NoError(t, err) + + // Validate the response + require.Equal(t, http.StatusOK, resp.StatusCode) + + _, ok := responseBody["Id"] + require.True(t, ok) + + require.Equal(t, "Alice", responseBody["Props"].(map[string]any)["first_name"]) + require.Equal(t, "Wonderland", responseBody["Props"].(map[string]any)["last_name"]) + } } -func SoftDeletePerson() { +func SoftDeletePersonTest(dbAdapterUri string, client *http.Client) func(t *testing.T) { //nolint:dupl,lll // This just does not worth abstracting anymore + return func(t *testing.T) { + url := dbAdapterUri + "/person/0" + + req, err := http.NewRequestWithContext(t.Context(), http.MethodDelete, url, http.NoBody) + require.NoError(t, err) + req.Header.Set("Content-Type", "application/json") + req.Header.Set("X-User-ID", "0") + + // Send the request + resp, err := client.Do(req) + require.NoError(t, err) + defer resp.Body.Close() + + var responseBody map[string]any + err = json.NewDecoder(resp.Body).Decode(&responseBody) + require.NoError(t, err) + + // Validate the response + require.Equal(t, http.StatusOK, resp.StatusCode) + + require.Equal(t, "Person soft deleted", responseBody["description"]) + } } -func UpdatePerson() { +//go:embed payloads\update_person.json +var update_person []byte + +func UpdatePersonTest(dbAdapterUri string, client *http.Client) func(t *testing.T) { + return func(t *testing.T) { + url := dbAdapterUri + "/person/1" + + req, err := http.NewRequestWithContext(t.Context(), http.MethodPatch, url, bytes.NewBuffer(update_person)) + require.NoError(t, err) + req.Header.Set("Content-Type", "application/json") + req.Header.Set("X-User-ID", "1") + + // Send the request + resp, err := client.Do(req) + require.NoError(t, err) + defer resp.Body.Close() + + var responseBody map[string]any + err = json.NewDecoder(resp.Body).Decode(&responseBody) + require.NoError(t, err) + + // Validate the response + require.Equal(t, http.StatusOK, resp.StatusCode) + + _, ok := responseBody["Id"] + require.True(t, ok) + + require.Equal(t, "John", responseBody["Props"].(map[string]any)["first_name"]) + require.Equal(t, "Doe", responseBody["Props"].(map[string]any)["last_name"]) + require.Equal(t, "ABCD1234", responseBody["Props"].(map[string]any)["invite_code"]) + } } -func HardDeletePerson() { +//go:embed payloads\generate_invite_code.json +var generate_invite_code []byte + +func UpdatePersonWithInviteCodeTest(dbAdapterUri string, client *http.Client) func(t *testing.T) { + return func(t *testing.T) { + url := dbAdapterUri + "/person/1" + + req, err := http.NewRequestWithContext(t.Context(), http.MethodPatch, url, bytes.NewBuffer(generate_invite_code)) + require.NoError(t, err) + req.Header.Set("Content-Type", "application/json") + req.Header.Set("X-User-ID", "0") + + // Send the request + resp, err := client.Do(req) + require.NoError(t, err) + defer resp.Body.Close() + + var responseBody api.Person + err = json.NewDecoder(resp.Body).Decode(&responseBody) + require.NoError(t, err) + + // Validate the response + require.Equal(t, http.StatusOK, resp.StatusCode) + + require.NotNil(t, responseBody.Id) + + require.Equal(t, "test-invite-code", *responseBody.Props.InviteCode) + } +} + +func HardDeletePersonTest(dbAdapterUri string, client *http.Client) func(t *testing.T) { //nolint:dupl,lll // This just does not worth abstracting anymore + return func(t *testing.T) { + url := dbAdapterUri + "/person/0/hard-delete" + + req, err := http.NewRequestWithContext(t.Context(), http.MethodDelete, url, http.NoBody) + require.NoError(t, err) + req.Header.Set("Content-Type", "application/json") + req.Header.Set("X-User-ID", "0") + + // Send the request + resp, err := client.Do(req) + require.NoError(t, err) + defer resp.Body.Close() + + var responseBody map[string]any + err = json.NewDecoder(resp.Body).Decode(&responseBody) + require.NoError(t, err) + + // Validate the response + require.Equal(t, http.StatusOK, resp.StatusCode) + + require.Equal(t, "Person hard deleted", responseBody["description"]) + } } diff --git a/apps/db-adapter/internal/api/person.go b/apps/db-adapter/internal/api/person.go index 442087c..f54b1d0 100644 --- a/apps/db-adapter/internal/api/person.go +++ b/apps/db-adapter/internal/api/person.go @@ -7,6 +7,7 @@ import ( "github.com/gin-gonic/gin" "github.com/neo4j/neo4j-go-driver/v5/neo4j" + "github.com/neo4j/neo4j-go-driver/v5/neo4j/dbtype" "github.com/vcscsvcscs/GenerationsHeritage/apps/db-adapter/internal/api/auth" "github.com/vcscsvcscs/GenerationsHeritage/apps/db-adapter/internal/memgraph" "github.com/vcscsvcscs/GenerationsHeritage/apps/db-adapter/pkg/api" @@ -38,8 +39,9 @@ func (srv *server) CreatePerson(c *gin.Context, params api.CreatePersonParams) { qctx, qCancel := context.WithTimeout(c.Request.Context(), srv.dbOpTimeout) defer qCancel() + convertedPerson := memgraph.StructToMap(person) res, err := trs.Run(qctx, memgraph.CreatePersonCypherQuery, map[string]any{ - "Person": *person, + "Person": convertedPerson, }) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"msg": err.Error()}) @@ -54,17 +56,19 @@ func (srv *server) CreatePerson(c *gin.Context, params api.CreatePersonParams) { return } - personId, ok := singleRes.Get("id") + createdPerson, ok := singleRes.Get("person") if !ok { - c.JSON(http.StatusInternalServerError, gin.H{"msg": "Person ID not found in response"}) + c.JSON(http.StatusInternalServerError, gin.H{"msg": "Person not found in db response"}) return } + personId := createdPerson.(dbtype.Node).Id //nolint:staticcheck // this is a difference in neo4j and memgraph + actx, acancel := context.WithTimeout(c.Request.Context(), srv.dbOpTimeout) defer acancel() _, aErr := trs.Run(actx, memgraph.CreateAdminRelationshipCypherQuery, map[string]any{ - "id2": personId.(int), + "id2": personId, "id1": params.XUserID, }) if aErr != nil { @@ -80,10 +84,10 @@ func (srv *server) CreatePerson(c *gin.Context, params api.CreatePersonParams) { return } - c.JSON(http.StatusOK, singleRes.AsMap()) + c.JSON(http.StatusOK, createdPerson) } -func (srv *server) GetPersonById(c *gin.Context, id int, params api.GetPersonByIdParams) { //nolint:dupl // not worth abstracting more +func (srv *server) GetPersonById(c *gin.Context, id int, params api.GetPersonByIdParams) { session := srv.createSessionWithTimeout(c.Request.Context()) defer closeSession(c.Request.Context(), srv.logger, session, srv.dbOpTimeout) @@ -107,7 +111,7 @@ func (srv *server) GetPersonById(c *gin.Context, id int, params api.GetPersonByI c.JSON(http.StatusOK, res) } -func (srv *server) SoftDeletePerson(c *gin.Context, id int, params api.SoftDeletePersonParams) { +func (srv *server) SoftDeletePerson(c *gin.Context, id int, params api.SoftDeletePersonParams) { //nolint:dupl,lll // This just does not worth abstracting anymore session := srv.createSessionWithTimeout(c.Request.Context()) defer closeSession(c.Request.Context(), srv.logger, session, srv.dbOpTimeout) @@ -128,7 +132,7 @@ func (srv *server) SoftDeletePerson(c *gin.Context, id int, params api.SoftDelet return } - c.JSON(http.StatusOK, map[string]string{"description": "Person soft deleted"}) + c.JSON(http.StatusOK, gin.H{"description": "Person soft deleted"}) } func (srv *server) UpdatePerson(c *gin.Context, id int, params api.UpdatePersonParams) { @@ -176,12 +180,14 @@ func (srv *server) HardDeletePerson(c *gin.Context, id int, params api.HardDelet qctx, qCancel := context.WithTimeout(c.Request.Context(), srv.dbOpTimeout) defer qCancel() - res, err := session.ExecuteWrite(qctx, memgraph.HardDeletePerson(qctx, id)) + _, err := session.ExecuteWrite(qctx, memgraph.HardDeletePerson(qctx, id)) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"msg": err.Error()}) return } - c.JSON(http.StatusOK, res) + c.JSON(http.StatusOK, gin.H{ + "description": "Person hard deleted", + }) } diff --git a/apps/db-adapter/internal/api/person_and_relationship.go b/apps/db-adapter/internal/api/person_and_relationship.go index 5c925c5..793accf 100644 --- a/apps/db-adapter/internal/api/person_and_relationship.go +++ b/apps/db-adapter/internal/api/person_and_relationship.go @@ -6,6 +6,7 @@ import ( "github.com/gin-gonic/gin" "github.com/neo4j/neo4j-go-driver/v5/neo4j" + "github.com/neo4j/neo4j-go-driver/v5/neo4j/dbtype" "github.com/vcscsvcscs/GenerationsHeritage/apps/db-adapter/internal/memgraph" "github.com/vcscsvcscs/GenerationsHeritage/apps/db-adapter/pkg/api" "go.uber.org/zap" @@ -36,8 +37,9 @@ func (srv *server) CreatePersonAndRelationship(c *gin.Context, id int, params ap qctx, qCancel := context.WithTimeout(context.Background(), srv.dbOpTimeout) defer qCancel() + convertedPerson := memgraph.StructToMap(requestBody.Person) res, err := trs.Run(qctx, memgraph.CreatePersonCypherQuery, map[string]any{ - "Person": requestBody.Person, + "Person": convertedPerson, }) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"msg": err.Error()}) @@ -51,17 +53,19 @@ func (srv *server) CreatePersonAndRelationship(c *gin.Context, id int, params ap return } - personID, ok := singleRes.Get("id") + createdPerson, ok := singleRes.Get("person") if !ok { c.JSON(http.StatusInternalServerError, gin.H{"msg": "Person ID not found in response"}) return } + personID := createdPerson.(dbtype.Node).Id //nolint:staticcheck // this is a difference in neo4j and memgraph + actx, acancel := context.WithTimeout(c.Request.Context(), srv.dbOpTimeout) defer acancel() _, aErr := trs.Run(actx, memgraph.CreateAdminRelationshipCypherQuery, map[string]any{ - "id2": personID.(int), + "id2": personID, "id1": params.XUserID, }) if aErr != nil { @@ -73,36 +77,38 @@ func (srv *server) CreatePersonAndRelationship(c *gin.Context, id int, params ap relCtx, relCCancel := context.WithTimeout(c.Request.Context(), srv.dbOpTimeout) defer relCCancel() + convertedRelationship := memgraph.StructToMap(requestBody.Relationship) + var relationShipResultRaw any var relationshipError error switch *requestBody.Type { case api.CreatePersonAndRelationshipJSONBodyTypeChild: relationShipResultRaw, relationshipError = trs.Run(relCtx, memgraph.CreateChildParentRelationshipCypherQuery, map[string]any{ - "childId": personID.(int), + "childId": personID, "parentId": id, - "childRelationship": requestBody.Relationship, - "parentRelationship": requestBody.Relationship, + "childRelationship": convertedRelationship, + "parentRelationship": convertedRelationship, }) case api.CreatePersonAndRelationshipJSONBodyTypeParent: relationShipResultRaw, relationshipError = trs.Run(relCtx, memgraph.CreateChildParentRelationshipCypherQuery, map[string]any{ "childId": id, - "parentId": personID.(int), - "childRelationship": requestBody.Relationship, - "parentRelationship": requestBody.Relationship, + "parentId": personID, + "childRelationship": convertedRelationship, + "parentRelationship": convertedRelationship, }) case api.CreatePersonAndRelationshipJSONBodyTypeSibling: relationShipResultRaw, relationshipError = trs.Run(relCtx, memgraph.CreateSiblingRelationshipCypherQuery, map[string]any{ "id1": id, - "id2": personID.(int), - "Relationship1": requestBody.Relationship, - "Relationship2": requestBody.Relationship, + "id2": personID, + "Relationship1": convertedRelationship, + "Relationship2": convertedRelationship, }) case api.CreatePersonAndRelationshipJSONBodyTypeSpouse: relationShipResultRaw, relationshipError = trs.Run(relCtx, memgraph.CreateSpouseRelationshipCypherQuery, map[string]any{ - "id1": personID.(int), + "id1": personID, "id2": id, - "Relationship1": requestBody.Relationship, - "Relationship2": requestBody.Relationship, + "Relationship1": convertedRelationship, + "Relationship2": convertedRelationship, }) default: c.JSON(http.StatusBadRequest, gin.H{"msg": "invalid relationship type"}) diff --git a/apps/db-adapter/internal/memgraph/person.go b/apps/db-adapter/internal/memgraph/person.go index 4002612..a8ce183 100644 --- a/apps/db-adapter/internal/memgraph/person.go +++ b/apps/db-adapter/internal/memgraph/person.go @@ -41,7 +41,12 @@ func GetPersonById(ctx context.Context, id int) neo4j.ManagedTransactionWork { return nil, err } - return record.AsMap(), nil + person, ok := record.Get("person") + if !ok { + return nil, fmt.Errorf("person not found") + } + + return person, nil } } @@ -61,7 +66,12 @@ func UpdatePerson(ctx context.Context, id int, person *api.PersonProperties) neo return nil, err } - return record.AsMap(), nil + person, ok := record.Get("person") + if !ok { + return nil, fmt.Errorf("person not found") + } + + return person, nil } } diff --git a/apps/db-adapter/internal/memgraph/queries/soft_delete_person_by_id.cypher b/apps/db-adapter/internal/memgraph/queries/soft_delete_person_by_id.cypher index 446c59c..36944a5 100644 --- a/apps/db-adapter/internal/memgraph/queries/soft_delete_person_by_id.cypher +++ b/apps/db-adapter/internal/memgraph/queries/soft_delete_person_by_id.cypher @@ -1,4 +1,5 @@ -MATCH (n:Person {id: $id}) +MATCH (n:Person) +WHERE id(n)=$id SET n:DeletedPerson REMOVE n:Person RETURN labels(n) AS labels, n AS person \ No newline at end of file diff --git a/apps/db-adapter/internal/memgraph/struct_to_map.go b/apps/db-adapter/internal/memgraph/struct_to_map.go index 57cc48e..11d1d8f 100644 --- a/apps/db-adapter/internal/memgraph/struct_to_map.go +++ b/apps/db-adapter/internal/memgraph/struct_to_map.go @@ -71,6 +71,8 @@ func StructToMap(input any) map[string]any { switch val.Kind() { case reflect.Struct: result[jsonKey] = StructToMap(val.Interface()) + case reflect.Slice, reflect.Array: + result[jsonKey] = processSlice(val) default: result[jsonKey] = val.Interface() } @@ -104,3 +106,19 @@ func isPreservedType(v any) bool { return false } } + +func processSlice(val reflect.Value) []any { + slice := make([]any, val.Len()) + for i := range val.Len() { + item := val.Index(i).Interface() + if isPreservedType(item) { + slice[i] = item + } else if reflect.ValueOf(item).Kind() == reflect.Struct { + slice[i] = StructToMap(item) + } else { + slice[i] = item + } + } + + return slice +} diff --git a/apps/db-adapter/main_test.go b/apps/db-adapter/main_test.go index a4f8aef..aee4d5d 100644 --- a/apps/db-adapter/main_test.go +++ b/apps/db-adapter/main_test.go @@ -1,6 +1,7 @@ package main import ( + "net/http" "testing" "github.com/stretchr/testify/require" @@ -11,6 +12,8 @@ import ( ) func TestIntegration(t *testing.T) { + t.Parallel() + net, err := network.New(t.Context()) if err != nil { t.Logf("failed to create network: %s", err) @@ -97,5 +100,18 @@ func TestIntegration(t *testing.T) { require.NoError(t, err) dbAdapterURI := "http://" + dbAdapterHost + ":" + dbAdapterPort.Port() - t.Run("TestCreatePersonByGoogleIdAndGetById", integration_tests.TestPersonGoogle(dbAdapterURI)) + IntegrationTestFlow(dbAdapterURI)(t) +} + +func IntegrationTestFlow(dbAdapterURI string) func(t *testing.T) { + return func(t *testing.T) { + client := &http.Client{} + t.Run("CreatePersonByGoogleIdAndGetById", integration_tests.TestPersonGoogle(dbAdapterURI)) + t.Run("CreatePerson", integration_tests.CreatePersonTest(dbAdapterURI, client)) + t.Run("UpdatePerson", integration_tests.UpdatePersonTest(dbAdapterURI, client)) + t.Run("AddInviteCodeToPerson", integration_tests.UpdatePersonWithInviteCodeTest(dbAdapterURI, client)) + t.Run("GetPersonById", integration_tests.GetPersonById(dbAdapterURI, client)) + t.Run("SoftDeletePerson", integration_tests.SoftDeletePersonTest(dbAdapterURI, client)) + t.Run("HardDeletePerson", integration_tests.HardDeletePersonTest(dbAdapterURI, client)) + } }