From 3d2b758d1bb2e3999b5102cf0ca88c555184396d Mon Sep 17 00:00:00 2001 From: Gabriel Aboy Date: Mon, 16 Sep 2024 14:00:31 -0600 Subject: [PATCH 1/5] refactor models and api to features listings and contacts --- cli/server/server.go | 11 +- .../services/api/features/features_service.go | 54 ++++++++++ .../services/api/features/test_features.go | 0 .../services/api/frontend/frontend_service.go | 61 +++++++++++ .../services/api/frontend/test_frontend.go | 0 internal/store/database/divisions.go | 99 ----------------- internal/store/database/feature_store.go | 101 ++++++++++++++++++ internal/store/database/listing_store.go | 40 +++++++ pkg/types/consts.go | 43 ++++++++ pkg/types/types.go | 45 ++++---- 10 files changed, 334 insertions(+), 120 deletions(-) create mode 100644 internal/services/api/features/features_service.go create mode 100644 internal/services/api/features/test_features.go create mode 100644 internal/services/api/frontend/frontend_service.go create mode 100644 internal/services/api/frontend/test_frontend.go delete mode 100644 internal/store/database/divisions.go create mode 100644 internal/store/database/feature_store.go create mode 100644 internal/store/database/listing_store.go diff --git a/cli/server/server.go b/cli/server/server.go index 70c52e3..ba98c5c 100644 --- a/cli/server/server.go +++ b/cli/server/server.go @@ -53,10 +53,15 @@ func initialize(cfg *config.Config) (s *server.Server, err error) { db, err := db.Connect(cfg) var services []services.Service + // Stores + featureStore := database.NewFeatureStore(db) + listingStore := database.NewListingStore(db) - divisionStore := database.NewDivisionStore(db) - divisionService := api.NewDivisionService(*divisionStore) - services = append(services, divisionService) + // Services + featuresService := api.NewFeaturesService(*featureStore) + frontendService := api.NewFrontendService(*featureStore, *listingStore) + + services = append(services, featuresService, frontendService) var middlewares chi.Middlewares middlewares = append(middlewares, middleware.Logger) diff --git a/internal/services/api/features/features_service.go b/internal/services/api/features/features_service.go new file mode 100644 index 0000000..df60a6a --- /dev/null +++ b/internal/services/api/features/features_service.go @@ -0,0 +1,54 @@ +package api + +import ( + "encoding/json" + "net/http" + "strconv" + + "github.com/go-chi/chi/v5" + + "directory/internal/store/database" +) + +type FeatureService struct { + featureStore database.FeatureStore +} + +func NewFeatureService(featuresStore database.FeatureStore) *FeatureService { + return &FeatureService{ + featureStore: featuresStore, + } +} + +func (s *FeatureService) RegisterRoutes(mux *chi.Mux) { + mux.Get("/features/{id}", s.FindByID) +} + +// TODO add query param for find 1 or recursive +func (s *FeatureService) FindByID(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + + id, err := strconv.Atoi(chi.URLParam(r, "id")) + if err != nil { + http.Error(w, "feature id is invalid", http.StatusBadRequest) + return + } + + features, err := s.featureStore.FindRelationsByID(ctx, id) + if err != nil { + http.Error(w, "feature id was not found", http.StatusNotFound) + } + + response, err := json.Marshal(features) + if err != nil { + http.Error(w, "Error marshalling JSON", http.StatusInternalServerError) + return + } + + w.WriteHeader(http.StatusOK) + _, err = w.Write(response) + if err != nil { + http.Error(w, "Error writing response", http.StatusInternalServerError) + return + } +} diff --git a/internal/services/api/features/test_features.go b/internal/services/api/features/test_features.go new file mode 100644 index 0000000..e69de29 diff --git a/internal/services/api/frontend/frontend_service.go b/internal/services/api/frontend/frontend_service.go new file mode 100644 index 0000000..8636c0b --- /dev/null +++ b/internal/services/api/frontend/frontend_service.go @@ -0,0 +1,61 @@ +package api + +import ( + "encoding/json" + "net/http" + "strconv" + + "github.com/go-chi/chi/v5" + + "directory/internal/store/database" +) + +type FrontendService struct { + featureStore database.FeatureStore + listingStore database.ListingStore +} + +func NewFrontendService(featureStore database.FeatureStore, listingStore database.ListingStore) *FrontendService { + return &FrontendService{ + featureStore: featureStore, + listingStore: listingStore, + } +} + +func (s *FrontendService) RegisterRoutes(mux *chi.Mux) { + mux.Get("/frontend/feature/{featureId}/listing", s.FindListingsByFeatureId) +} + +// TODO add query param for find 1 or recursive +func (s *FrontendService) FindListingsByFeatureId(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + + featureId, err := strconv.Atoi(chi.URLParam(r, "featureId")) + if err != nil { + http.Error(w, "feature id is invalid", http.StatusBadRequest) + return + } + + featureTree, featureIds, err := s.featureStore.FindRelationsByID(ctx, featureId) + if err != nil { + http.Error(w, "feature id was not found", http.StatusNotFound) + } + + featureListings, err := s.listingStore.FindByListingIDs(ctx, featureIds) + if err != nil { + http.Error(w, "feature id was not found", http.StatusNotFound) + } + + response, err := json.Marshal(featureTree) + if err != nil { + http.Error(w, "Error marshalling JSON", http.StatusInternalServerError) + return + } + + w.WriteHeader(http.StatusOK) + _, err = w.Write(response) + if err != nil { + http.Error(w, "Error writing response", http.StatusInternalServerError) + return + } +} diff --git a/internal/services/api/frontend/test_frontend.go b/internal/services/api/frontend/test_frontend.go new file mode 100644 index 0000000..e69de29 diff --git a/internal/store/database/divisions.go b/internal/store/database/divisions.go deleted file mode 100644 index fe0c44b..0000000 --- a/internal/store/database/divisions.go +++ /dev/null @@ -1,99 +0,0 @@ -package database - -import ( - "context" - "log" - - db "directory/pkg/database" - "directory/pkg/types" -) - -type DivisionStore struct { - store db.Pool -} - -func NewDivisionStore(s db.Pool) *DivisionStore { - return &DivisionStore{store: s} -} - -func (s *DivisionStore) Create(ctx context.Context, division types.Division) (createdId int, err error) { - err = s.store.QueryRow(ctx, createQuery, division.Name, division.Type, division.ParentId).Scan(&createdId) - return -} - -func (s *DivisionStore) FindByID(ctx context.Context, id int) (division *types.Division, err error) { - division = &types.Division{} - err = s.store.QueryRow(ctx, findByIDQuery, id).Scan(&division.Id, &division.Name, &division.Type, &division.ParentId) - return -} - -func (s *DivisionStore) Update(ctx context.Context, division types.Division) (updatedId int, err error) { - err = s.store.QueryRow(ctx, updateQuery, division.Id, division.Name, division.Type, division.ParentId).Scan(&updatedId) - return -} - -func (s *DivisionStore) Delete(ctx context.Context, id int) (deletedId int, err error) { - err = s.store.QueryRow(ctx, deleteQuery, id).Scan(&deletedId) - return -} - -func (s *DivisionStore) FindRelationsByID(ctx context.Context, id int) (divisions []*types.Division, err error) { - rows, err := s.store.Query(ctx, recursiveFindByIDQuery, id) - if err != nil { - log.Fatalf("failed to query rows: %v", err) - } - - for rows.Next() { - var d types.Division - err := rows.Scan(&d.Id, &d.Name, &d.Type, &d.ParentId) - if err != nil { - log.Fatalf("failed to scan row: %v", err) - } - divisions = append(divisions, &d) - } - - return -} - -const createQuery = "INSERT INTO directory.divisions (name, type, parent_id) VALUES ($1, $2, $3) RETURNING id" - -const findByIDQuery = "SELECT d.id, d.name, d.type, d.parent_id FROM directory.divisions d WHERE id = $1" - -const updateQuery = "UPDATE directory.divisions SET name = $2, type = $3, parent_id = $4 WHERE id = $1 RETURNING id" - -const deleteQuery = "DELETE FROM directory.divisions WHERE id = $1 RETURNING id" - -// WARNING Can this ever be circular? -const recursiveFindByIDQuery = ` - WITH RECURSIVE ParentCTE AS ( - -- Start with the given record and find its children - SELECT id, name, type, parent_id - FROM directory.divisions - WHERE id = $1 - - UNION ALL - - -- Find all parents of the current record - SELECT loc.id, loc.name, loc.type, loc.parent_id - FROM directory.divisions loc - JOIN ParentCTE p ON loc.id = p.parent_id - ), - ChildCTE AS ( - -- Start with the given record and find its children - SELECT id, name, type, parent_id - FROM directory.divisions - WHERE id = $1 - - UNION ALL - - -- Find all children of the current record - SELECT loc.id, loc.name, loc.type, loc.parent_id - FROM directory.divisions loc - JOIN ChildCTE c ON loc.parent_id = c.id - ) - -- Combine results from both ParentCTE and ChildCTE - SELECT * FROM ParentCTE - UNION - SELECT * FROM ChildCTE - ORDER BY id; -` diff --git a/internal/store/database/feature_store.go b/internal/store/database/feature_store.go new file mode 100644 index 0000000..05041a9 --- /dev/null +++ b/internal/store/database/feature_store.go @@ -0,0 +1,101 @@ +package database + +import ( + "context" + "log" + + db "directory/pkg/database" + "directory/pkg/types" +) + +// TODO Rename to features +type FeatureStore struct { + store db.Pool +} + +func NewFeatureStore(s db.Pool) *FeatureStore { + return &FeatureStore{store: s} +} + +func (s *FeatureStore) Create(ctx context.Context, feature types.Feature) (createdId int, err error) { + err = s.store.QueryRow(ctx, createQuery, feature.Name, feature.Type, feature.ParentId).Scan(&createdId) + return +} + +func (s *FeatureStore) FindByID(ctx context.Context, id int) (feature *types.Feature, err error) { + feature = &types.Feature{} + err = s.store.QueryRow(ctx, findByIDQuery, id).Scan(&feature.Id, &feature.Name, &feature.Type, &feature.ParentId) + return +} + +func (s *FeatureStore) Update(ctx context.Context, feature types.Feature) (updatedId int, err error) { + err = s.store.QueryRow(ctx, updateQuery, feature.Id, feature.Name, feature.Type, feature.ParentId).Scan(&updatedId) + return +} + +func (s *FeatureStore) Delete(ctx context.Context, id int) (deletedId int, err error) { + err = s.store.QueryRow(ctx, deleteQuery, id).Scan(&deletedId) + return +} + +func (s *FeatureStore) FindRelationsByID(ctx context.Context, id int) (features []*types.Feature, featureIds []*int, err error) { + rows, err := s.store.Query(ctx, recursiveFindByIDQuery, id) + if err != nil { + log.Fatalf("failed to query rows: %v", err) + } + for rows.Next() { + var feature types.Feature + err := rows.Scan(&feature.Id, &feature.Name, &feature.Type, &feature.ParentId) + if err != nil { + log.Fatalf("failed to scan row: %v", err) + } + features = append(features, &feature) + featureIds = append(featureIds, &feature.Id) + } + + return +} + +const createQuery = "INSERT INTO directory.features (name, type, parent_id) VALUES ($1, $2, $3) RETURNING id" + +const findByIDQuery = "SELECT d.id, d.name, d.type, d.parent_id FROM directory.features d WHERE id = $1" + +const updateQuery = "UPDATE directory.features SET name = $2, type = $3, parent_id = $4 WHERE id = $1 RETURNING id" + +const deleteQuery = "DELETE FROM directory.features WHERE id = $1 RETURNING id" + +// WARNING Can this ever be circular? +// If a parent references a child incorrectly -> child references parent correctly +const recursiveFindByIDQuery = ` +WITH RECURSIVE ParentCTE AS ( + -- Start with the given record and find its children + SELECT internal_id, id, name, type, parent_id + FROM directory.features + WHERE internal_id = 1 + + UNION ALL + + -- Find all parents of the current record + SELECT loc.internal_id, loc.id, loc.name, loc.type, loc.parent_id + FROM directory.features loc + JOIN ParentCTE p ON loc.internal_id = p.parent_id + ), + ChildCTE AS ( + -- Start with the given record and find its children + SELECT internal_id, id, name, type, parent_id + FROM directory.features + WHERE internal_id = 1 + + UNION ALL + + -- Find all children of the current record + SELECT loc.internal_id, loc.id, loc.name, loc.type, loc.parent_id + FROM directory.features loc + JOIN ChildCTE c ON loc.parent_id = c.internal_id + ) + -- Combine results from both ParentCTE and ChildCTE + SELECT * FROM ParentCTE + UNION + SELECT * FROM ChildCTE + ORDER BY id; +` diff --git a/internal/store/database/listing_store.go b/internal/store/database/listing_store.go new file mode 100644 index 0000000..1d3746e --- /dev/null +++ b/internal/store/database/listing_store.go @@ -0,0 +1,40 @@ +package database + +import ( + "context" + + db "directory/pkg/database" + "directory/pkg/types" +) + +type ListingStore struct { + store db.Pool +} + +func NewListingStore(s db.Pool) *ListingStore { + return &ListingStore{store: s} +} + +func (s *ListingStore) FindByListingIDs(ctx context.Context, featureIds []int) (listings *[]types.Listing, err error) { + err = s.store.QueryRow(ctx, findContactsByListingIDs, featureIds).Scan(&listings.Id, &listings.Name, &listings.Type, &listings.ParentId) + return +} + +const findContactsByListingIDs = ` + SELECT + l.id AS listing_id, + l.name AS listing_name, + l.details AS listing_details, + l.contact_ids, -- Array of contact_ids + c.id AS contact_id, + c.name AS contact_name, + c.type AS contact_type, + c.details AS contact_details + FROM + listings l + LEFT JOIN LATERAL + unnest(l.contact_ids) AS contact_id ON true + LEFT JOIN + contacts c ON c.internal_id = contact_id; + WHERE l.internal_id in ($1) +` diff --git a/pkg/types/consts.go b/pkg/types/consts.go index ab1254f..fe3a1e0 100644 --- a/pkg/types/consts.go +++ b/pkg/types/consts.go @@ -1 +1,44 @@ package types + +type ListingType string + +const ( + POLICE ListingType = "Police" + HOSPITAL ListingType = "Hospital" + FIRE_DEPARTMENT ListingType = "Fire Department" + AMBULANCE ListingType = "Ambulance" + POISON_CONTROL ListingType = "Poison Control" + COAST_GUARD ListingType = "Coast Guard" + ELECTRICITY_EMERGENCY ListingType = "Electricity Emergency" + GAS_LEAK_EMERGENCY ListingType = "Gas Leak Emergency" + ROAD_ASSISTANCE ListingType = "Road Assistance" + MENTAL_HEALTH ListingType = "Mental Health" + DOMESTIC_VIOLENCE ListingType = "Domestic Violence" + MISCELLANEOUS ListingType = "Miscellaneous" +) + +type FeatureType string + +const ( + COUNTRY FeatureType = "country" + REGION FeatureType = "region" + POSTCODE FeatureType = "postcode" + DISTRICT FeatureType = "district" + PLACE FeatureType = "place" + LOCALITY FeatureType = "locality" + NEIGHBORHOOD FeatureType = "neighborhood" +) + +type ContactType string + +const ( + EMAIL ContactType = "Email" + PHONE ContactType = "Phone" +) + +type AdType string + +const ( + LAWYER AdType = "Lawyer" + DOCTOR AdType = "Doctor" +) diff --git a/pkg/types/types.go b/pkg/types/types.go index 377193b..b8e5399 100644 --- a/pkg/types/types.go +++ b/pkg/types/types.go @@ -1,5 +1,6 @@ package types +<<<<<<< Updated upstream type Division struct { Id int `json:"id"` Name string `json:"name"` @@ -36,31 +37,39 @@ type Directory struct { Listings []*Listing `json:"listings"` Ads []*Ad `json:"ads"` +======= +type Feature struct { + Id int `json:"id"` + Name string `json:"name"` + Type FeatureType `json:"type"` + ParentId *int `json:"parent_id"` +>>>>>>> Stashed changes } type Listing struct { - Type ListingType `json:"type"` - Name string `json:"name"` - Phone string `json:"phone"` + Id int `json:"id"` + Name string `json:"name"` + Type ListingType `json:"type"` + // TODO rename to feature_internal_id FeatureInternalId + // TODO should be hidden from response + FeatureId int `json:"feature_id"` + Address string `json:"address"` + ContactIds []int `json:"contact_ids"` + Details *string `json:"details"` + Contacts []Contact `json:"contacts"` + // last_modified } +type Contact struct { + Id int `json:"id"` + Name string `json:"name"` + Type FeatureType `json:"type"` + ParentId *int `json:"parent_id"` +} + +// TODO Implement these structs type Ad struct { Type AdType `json:"type"` Name string `json:"name"` Phone string `json:"phone"` } - -type ListingType string - -const ( - POLICE ListingType = "Police" - FIRE ListingType = "Fire" - EMS ListingType = "EMS" -) - -type AdType string - -const ( - LAWYER AdType = "Lawyer" - DOCTOR AdType = "Doctor" -) From 5b0852ecb6e2488a2421a3e2967bc4c0db9ec8bc Mon Sep 17 00:00:00 2001 From: Gabriel Aboy Date: Sat, 18 Jan 2025 00:12:45 -0700 Subject: [PATCH 2/5] Cleanup & review comments --- cli/server/server.go | 9 +- internal/services/api/divisions.go | 180 ------------------ .../{features_service.go => features.go} | 2 +- .../services/api/features/test_features.go | 0 .../services/api/frontend/test_frontend.go | 0 .../listings.go} | 12 +- .../database/{feature_store.go => feature.go} | 0 .../database/{listing_store.go => listing.go} | 18 +- pkg/types/types.go | 28 +-- 9 files changed, 31 insertions(+), 218 deletions(-) delete mode 100644 internal/services/api/divisions.go rename internal/services/api/features/{features_service.go => features.go} (94%) delete mode 100644 internal/services/api/features/test_features.go delete mode 100644 internal/services/api/frontend/test_frontend.go rename internal/services/api/{frontend/frontend_service.go => listings/listings.go} (74%) rename internal/store/database/{feature_store.go => feature.go} (100%) rename internal/store/database/{listing_store.go => listing.go} (56%) diff --git a/cli/server/server.go b/cli/server/server.go index ba98c5c..50e73ef 100644 --- a/cli/server/server.go +++ b/cli/server/server.go @@ -2,6 +2,8 @@ package server import ( "context" + features "directory/internal/services/api/features" + listings "directory/internal/services/api/listings" "fmt" "os" "os/signal" @@ -12,7 +14,6 @@ import ( "directory/internal/router" "directory/internal/services" - "directory/internal/services/api" "directory/internal/store/database" "directory/pkg/config" db "directory/pkg/database" @@ -58,10 +59,10 @@ func initialize(cfg *config.Config) (s *server.Server, err error) { listingStore := database.NewListingStore(db) // Services - featuresService := api.NewFeaturesService(*featureStore) - frontendService := api.NewFrontendService(*featureStore, *listingStore) + featuresService := features.NewFeatureService(*featureStore) + listingService := listings.NewListingService(*featureStore, *listingStore) - services = append(services, featuresService, frontendService) + services = append(services, featuresService, listingService) var middlewares chi.Middlewares middlewares = append(middlewares, middleware.Logger) diff --git a/internal/services/api/divisions.go b/internal/services/api/divisions.go deleted file mode 100644 index 0323738..0000000 --- a/internal/services/api/divisions.go +++ /dev/null @@ -1,180 +0,0 @@ -package api - -import ( - "encoding/json" - "io" - "net/http" - "strconv" - - "github.com/go-chi/chi/v5" - - "directory/internal/store/database" - "directory/pkg/types" -) - -type DivisionService struct { - divisionStore database.DivisionStore -} - -func NewDivisionService(divisionsStore database.DivisionStore) *DivisionService { - return &DivisionService{ - divisionStore: divisionsStore, - } -} - -func (s *DivisionService) RegisterRoutes(mux *chi.Mux) { - mux.Post("/divisions", s.Create) - mux.Get("/divisions/{id}", s.FindByID) - mux.Put("/divisions/{id}", s.Update) - mux.Delete("/divisions/{id}", s.Delete) -} - -func (s *DivisionService) Create(w http.ResponseWriter, r *http.Request) { - ctx := r.Context() - - body, err := io.ReadAll(r.Body) - if err != nil { - http.Error(w, "Failed to read request body", http.StatusInternalServerError) - return - } - - var data map[string]interface{} - if err := json.Unmarshal(body, &data); err != nil { - http.Error(w, "Failed to unmarshal body as JSON", http.StatusBadRequest) - return - } - - name := data["name"].(string) - divisionType := data["type"].(string) - parentId := int(data["parent_id"].(float64)) - - division := types.Division{ - Name: name, - Type: divisionType, - ParentId: &parentId, - } - - id, err := s.divisionStore.Create(ctx, division) - if err != nil { - http.Error(w, "Failed to create division", http.StatusInternalServerError) - return - } - division.Id = id - - response, err := json.Marshal(division) - if err != nil { - http.Error(w, "Error marshalling JSON", http.StatusInternalServerError) - return - } - w.WriteHeader(http.StatusOK) - _, err = w.Write(response) - if err != nil { - http.Error(w, "Error writing response", http.StatusInternalServerError) - return - } -} - -// TODO add query param for find 1 or recursive -func (s *DivisionService) FindByID(w http.ResponseWriter, r *http.Request) { - ctx := r.Context() - - id, err := strconv.Atoi(chi.URLParam(r, "id")) - if err != nil { - http.Error(w, "division id is invalid", http.StatusBadRequest) - return - } - - divisions, err := s.divisionStore.FindRelationsByID(ctx, id) - if err != nil { - http.Error(w, "division id was not found", http.StatusNotFound) - } - - response, err := json.Marshal(divisions) - if err != nil { - http.Error(w, "Error marshalling JSON", http.StatusInternalServerError) - return - } - - w.WriteHeader(http.StatusOK) - _, err = w.Write(response) - if err != nil { - http.Error(w, "Error writing response", http.StatusInternalServerError) - return - } -} - -func (s *DivisionService) Update(w http.ResponseWriter, r *http.Request) { - ctx := r.Context() - id, err := strconv.Atoi(chi.URLParam(r, "id")) - if err != nil { - http.Error(w, "division id is invalid", http.StatusBadRequest) - return - } - - body, err := io.ReadAll(r.Body) - if err != nil { - http.Error(w, "Failed to update request body", http.StatusInternalServerError) - return - } - - var data map[string]interface{} - - if err := json.Unmarshal(body, &data); err != nil { - http.Error(w, "Failed to unmarshal body as JSON", http.StatusBadRequest) - return - } - - division, err := s.divisionStore.FindByID(ctx, id) - if err != nil { - http.Error(w, "Failed to find division", http.StatusNotFound) - return - } - - if data["name"] != nil { - division.Name = data["name"].(string) - } - if data["type"] != nil { - division.Type = data["type"].(string) - } - if data["parent_id"] != nil { - parentId := int(data["parent_id"].(float64)) - division.ParentId = &parentId - } - - _, err = s.divisionStore.Update(ctx, *division) - if err != nil { - http.Error(w, "Failed to update division", http.StatusInternalServerError) - return - } - - response, err := json.Marshal(division) - if err != nil { - http.Error(w, "Error marshalling JSON", http.StatusInternalServerError) - return - } - w.WriteHeader(http.StatusOK) - _, err = w.Write(response) - - if err != nil { - http.Error(w, "Error updating division", http.StatusInternalServerError) - return - } -} - -func (s *DivisionService) Delete(w http.ResponseWriter, r *http.Request) { - ctx := r.Context() - - id, err := strconv.Atoi(chi.URLParam(r, "id")) - if err != nil { - http.Error(w, "Invalid Division ID", http.StatusBadRequest) - return - } - - _, err = s.divisionStore.Delete(ctx, id) - if err != nil { - http.Error(w, "Cannot delete division", http.StatusConflict) - return - } - - w.WriteHeader(http.StatusNoContent) -} diff --git a/internal/services/api/features/features_service.go b/internal/services/api/features/features.go similarity index 94% rename from internal/services/api/features/features_service.go rename to internal/services/api/features/features.go index df60a6a..7323cfd 100644 --- a/internal/services/api/features/features_service.go +++ b/internal/services/api/features/features.go @@ -34,7 +34,7 @@ func (s *FeatureService) FindByID(w http.ResponseWriter, r *http.Request) { return } - features, err := s.featureStore.FindRelationsByID(ctx, id) + features, _, err := s.featureStore.FindRelationsByID(ctx, id) if err != nil { http.Error(w, "feature id was not found", http.StatusNotFound) } diff --git a/internal/services/api/features/test_features.go b/internal/services/api/features/test_features.go deleted file mode 100644 index e69de29..0000000 diff --git a/internal/services/api/frontend/test_frontend.go b/internal/services/api/frontend/test_frontend.go deleted file mode 100644 index e69de29..0000000 diff --git a/internal/services/api/frontend/frontend_service.go b/internal/services/api/listings/listings.go similarity index 74% rename from internal/services/api/frontend/frontend_service.go rename to internal/services/api/listings/listings.go index 8636c0b..36bbb28 100644 --- a/internal/services/api/frontend/frontend_service.go +++ b/internal/services/api/listings/listings.go @@ -10,24 +10,24 @@ import ( "directory/internal/store/database" ) -type FrontendService struct { +type ListingService struct { featureStore database.FeatureStore listingStore database.ListingStore } -func NewFrontendService(featureStore database.FeatureStore, listingStore database.ListingStore) *FrontendService { - return &FrontendService{ +func NewListingService(featureStore database.FeatureStore, listingStore database.ListingStore) *ListingService { + return &ListingService{ featureStore: featureStore, listingStore: listingStore, } } -func (s *FrontendService) RegisterRoutes(mux *chi.Mux) { +func (s *ListingService) RegisterRoutes(mux *chi.Mux) { mux.Get("/frontend/feature/{featureId}/listing", s.FindListingsByFeatureId) } // TODO add query param for find 1 or recursive -func (s *FrontendService) FindListingsByFeatureId(w http.ResponseWriter, r *http.Request) { +func (s *ListingService) FindListingsByFeatureId(w http.ResponseWriter, r *http.Request) { ctx := r.Context() featureId, err := strconv.Atoi(chi.URLParam(r, "featureId")) @@ -41,7 +41,7 @@ func (s *FrontendService) FindListingsByFeatureId(w http.ResponseWriter, r *http http.Error(w, "feature id was not found", http.StatusNotFound) } - featureListings, err := s.listingStore.FindByListingIDs(ctx, featureIds) + _, err = s.listingStore.FindByListingIDs(ctx, featureIds) if err != nil { http.Error(w, "feature id was not found", http.StatusNotFound) } diff --git a/internal/store/database/feature_store.go b/internal/store/database/feature.go similarity index 100% rename from internal/store/database/feature_store.go rename to internal/store/database/feature.go diff --git a/internal/store/database/listing_store.go b/internal/store/database/listing.go similarity index 56% rename from internal/store/database/listing_store.go rename to internal/store/database/listing.go index 1d3746e..4fba0d5 100644 --- a/internal/store/database/listing_store.go +++ b/internal/store/database/listing.go @@ -2,6 +2,7 @@ package database import ( "context" + "fmt" db "directory/pkg/database" "directory/pkg/types" @@ -15,8 +16,19 @@ func NewListingStore(s db.Pool) *ListingStore { return &ListingStore{store: s} } -func (s *ListingStore) FindByListingIDs(ctx context.Context, featureIds []int) (listings *[]types.Listing, err error) { - err = s.store.QueryRow(ctx, findContactsByListingIDs, featureIds).Scan(&listings.Id, &listings.Name, &listings.Type, &listings.ParentId) +func (s *ListingStore) FindByListingIDs(ctx context.Context, featureIds []*int) (listings []*types.Listing, err error) { + rows, err := s.store.Query(ctx, findContactsByListingIDs, featureIds) + if err != nil { + return nil, fmt.Errorf("query failed: %w", err) + } + // Iterate over the rows and map to struct + for rows.Next() { + var listing types.Listing + if err := rows.Scan(&listing.Id, &listing.Name, &listing.Type, &listing.ParentId); err != nil { + return nil, fmt.Errorf("row scan failed: %w", err) + } + listings = append(listings, &listing) + } return } @@ -35,6 +47,6 @@ const findContactsByListingIDs = ` LEFT JOIN LATERAL unnest(l.contact_ids) AS contact_id ON true LEFT JOIN - contacts c ON c.internal_id = contact_id; + contacts c ON c.internal_id = contact_id WHERE l.internal_id in ($1) ` diff --git a/pkg/types/types.go b/pkg/types/types.go index b8e5399..a638a62 100644 --- a/pkg/types/types.go +++ b/pkg/types/types.go @@ -1,35 +1,14 @@ package types -<<<<<<< Updated upstream type Division struct { Id int `json:"id"` Name string `json:"name"` - Type string `json:"type"` + Type string `json:"type"` ParentId *int `json:"parent_id"` } type Type string -const ( - COUNTRY Type = "country" - STATE Type = "state" - PROVINCE Type = "province" - OBLAST Type = "oblast" - LAND Type = "land" - REGION Type = "region" - COMARCA Type = "comarca" - RAION Type = "raion" - DISTRICT Type = "district" - MUNICIPALITY Type = "municipality" - COMMUNE Type = "commune" - COMMUNITY Type = "community" - DEPARTMENT Type = "department" - CANTON Type = "canton" - PREFECTURE Type = "prefecture" - COUNTY Type = "county" - GOVERNORATE Type = "governorate" -) - type Directory struct { Country string `json:"country"` State string `json:"state"` @@ -37,13 +16,13 @@ type Directory struct { Listings []*Listing `json:"listings"` Ads []*Ad `json:"ads"` -======= +} + type Feature struct { Id int `json:"id"` Name string `json:"name"` Type FeatureType `json:"type"` ParentId *int `json:"parent_id"` ->>>>>>> Stashed changes } type Listing struct { @@ -53,6 +32,7 @@ type Listing struct { // TODO rename to feature_internal_id FeatureInternalId // TODO should be hidden from response FeatureId int `json:"feature_id"` + ParentId int `json:"parent_id"` Address string `json:"address"` ContactIds []int `json:"contact_ids"` Details *string `json:"details"` From 6f70b96c5f8ad18d46931ba703de1c371189bc3a Mon Sep 17 00:00:00 2001 From: Gabriel Aboy Date: Sat, 18 Jan 2025 01:41:08 -0700 Subject: [PATCH 3/5] working featureTree with listings --- cli/server/server.go | 2 +- internal/services/api/features/features.go | 56 +++++++++++++++++++++- internal/services/api/listings/listings.go | 42 +--------------- internal/store/database/feature.go | 16 +++---- internal/store/database/listing.go | 34 ++++++------- pkg/types/types.go | 29 ++++++----- 6 files changed, 97 insertions(+), 82 deletions(-) diff --git a/cli/server/server.go b/cli/server/server.go index 50e73ef..2ddf023 100644 --- a/cli/server/server.go +++ b/cli/server/server.go @@ -59,7 +59,7 @@ func initialize(cfg *config.Config) (s *server.Server, err error) { listingStore := database.NewListingStore(db) // Services - featuresService := features.NewFeatureService(*featureStore) + featuresService := features.NewFeatureService(*featureStore, *listingStore) listingService := listings.NewListingService(*featureStore, *listingStore) services = append(services, featuresService, listingService) diff --git a/internal/services/api/features/features.go b/internal/services/api/features/features.go index 7323cfd..b978e11 100644 --- a/internal/services/api/features/features.go +++ b/internal/services/api/features/features.go @@ -1,7 +1,9 @@ package api import ( + "directory/pkg/types" "encoding/json" + "fmt" "net/http" "strconv" @@ -12,16 +14,20 @@ import ( type FeatureService struct { featureStore database.FeatureStore + listingStore database.ListingStore } -func NewFeatureService(featuresStore database.FeatureStore) *FeatureService { +func NewFeatureService(featuresStore database.FeatureStore, listingStore database.ListingStore) *FeatureService { return &FeatureService{ featureStore: featuresStore, + listingStore: listingStore, } } func (s *FeatureService) RegisterRoutes(mux *chi.Mux) { mux.Get("/features/{id}", s.FindByID) + mux.Get("/features/{featureId}/listings", s.FindListingsByFeatureId) + } // TODO add query param for find 1 or recursive @@ -52,3 +58,51 @@ func (s *FeatureService) FindByID(w http.ResponseWriter, r *http.Request) { return } } + +// TODO add query param for find 1 or recursive +func (s *FeatureService) FindListingsByFeatureId(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + + featureId, err := strconv.Atoi(chi.URLParam(r, "featureId")) + if err != nil { + http.Error(w, "feature id is invalid", http.StatusBadRequest) + return + } + + featureTree, featureInternalIds, err := s.featureStore.FindRelationsByID(ctx, featureId) + if err != nil { + http.Error(w, "feature id was not found", http.StatusNotFound) + } + listings, err := s.listingStore.FindByListingFeatureInternalIDs(ctx, featureInternalIds) + listingMap := listingsToMap(listings) + if err != nil { + http.Error(w, "feature id was not found", http.StatusNotFound) + } + fmt.Println(listingMap) + for _, feature := range featureTree { + listing, match := listingMap[feature.InternalId] + if match { + feature.Listings = append(feature.Listings, listing) + } + } + response, err := json.Marshal(featureTree) + if err != nil { + http.Error(w, "Error marshalling JSON", http.StatusInternalServerError) + return + } + + w.WriteHeader(http.StatusOK) + _, err = w.Write(response) + if err != nil { + http.Error(w, "Error writing response", http.StatusInternalServerError) + return + } +} + +func listingsToMap(listings []*types.Listing) map[int]*types.Listing { + i := map[int]*types.Listing{} + for _, listing := range listings { + i[listing.FeatureId] = listing + } + return i +} diff --git a/internal/services/api/listings/listings.go b/internal/services/api/listings/listings.go index 36bbb28..7513136 100644 --- a/internal/services/api/listings/listings.go +++ b/internal/services/api/listings/listings.go @@ -1,10 +1,6 @@ package api import ( - "encoding/json" - "net/http" - "strconv" - "github.com/go-chi/chi/v5" "directory/internal/store/database" @@ -22,40 +18,4 @@ func NewListingService(featureStore database.FeatureStore, listingStore database } } -func (s *ListingService) RegisterRoutes(mux *chi.Mux) { - mux.Get("/frontend/feature/{featureId}/listing", s.FindListingsByFeatureId) -} - -// TODO add query param for find 1 or recursive -func (s *ListingService) FindListingsByFeatureId(w http.ResponseWriter, r *http.Request) { - ctx := r.Context() - - featureId, err := strconv.Atoi(chi.URLParam(r, "featureId")) - if err != nil { - http.Error(w, "feature id is invalid", http.StatusBadRequest) - return - } - - featureTree, featureIds, err := s.featureStore.FindRelationsByID(ctx, featureId) - if err != nil { - http.Error(w, "feature id was not found", http.StatusNotFound) - } - - _, err = s.listingStore.FindByListingIDs(ctx, featureIds) - if err != nil { - http.Error(w, "feature id was not found", http.StatusNotFound) - } - - response, err := json.Marshal(featureTree) - if err != nil { - http.Error(w, "Error marshalling JSON", http.StatusInternalServerError) - return - } - - w.WriteHeader(http.StatusOK) - _, err = w.Write(response) - if err != nil { - http.Error(w, "Error writing response", http.StatusInternalServerError) - return - } -} +func (s *ListingService) RegisterRoutes(mux *chi.Mux) {} diff --git a/internal/store/database/feature.go b/internal/store/database/feature.go index 05041a9..10cf0ca 100644 --- a/internal/store/database/feature.go +++ b/internal/store/database/feature.go @@ -38,19 +38,19 @@ func (s *FeatureStore) Delete(ctx context.Context, id int) (deletedId int, err e return } -func (s *FeatureStore) FindRelationsByID(ctx context.Context, id int) (features []*types.Feature, featureIds []*int, err error) { +func (s *FeatureStore) FindRelationsByID(ctx context.Context, id int) (features []*types.Feature, featureIds []int, err error) { rows, err := s.store.Query(ctx, recursiveFindByIDQuery, id) if err != nil { log.Fatalf("failed to query rows: %v", err) } for rows.Next() { var feature types.Feature - err := rows.Scan(&feature.Id, &feature.Name, &feature.Type, &feature.ParentId) + err := rows.Scan(&feature.InternalId, &feature.Name, &feature.Type, &feature.ParentId) if err != nil { log.Fatalf("failed to scan row: %v", err) } features = append(features, &feature) - featureIds = append(featureIds, &feature.Id) + featureIds = append(featureIds, feature.InternalId) } return @@ -71,7 +71,7 @@ WITH RECURSIVE ParentCTE AS ( -- Start with the given record and find its children SELECT internal_id, id, name, type, parent_id FROM directory.features - WHERE internal_id = 1 + WHERE internal_id = $1 UNION ALL @@ -84,7 +84,7 @@ WITH RECURSIVE ParentCTE AS ( -- Start with the given record and find its children SELECT internal_id, id, name, type, parent_id FROM directory.features - WHERE internal_id = 1 + WHERE internal_id = $1 UNION ALL @@ -94,8 +94,8 @@ WITH RECURSIVE ParentCTE AS ( JOIN ChildCTE c ON loc.parent_id = c.internal_id ) -- Combine results from both ParentCTE and ChildCTE - SELECT * FROM ParentCTE + SELECT internal_id, name, type, parent_id FROM ParentCTE UNION - SELECT * FROM ChildCTE - ORDER BY id; + SELECT internal_id, name, type, parent_id FROM ChildCTE + ORDER BY internal_id; ` diff --git a/internal/store/database/listing.go b/internal/store/database/listing.go index 4fba0d5..ea184b4 100644 --- a/internal/store/database/listing.go +++ b/internal/store/database/listing.go @@ -16,7 +16,7 @@ func NewListingStore(s db.Pool) *ListingStore { return &ListingStore{store: s} } -func (s *ListingStore) FindByListingIDs(ctx context.Context, featureIds []*int) (listings []*types.Listing, err error) { +func (s *ListingStore) FindByListingFeatureInternalIDs(ctx context.Context, featureIds []int) (listings []*types.Listing, err error) { rows, err := s.store.Query(ctx, findContactsByListingIDs, featureIds) if err != nil { return nil, fmt.Errorf("query failed: %w", err) @@ -24,29 +24,31 @@ func (s *ListingStore) FindByListingIDs(ctx context.Context, featureIds []*int) // Iterate over the rows and map to struct for rows.Next() { var listing types.Listing - if err := rows.Scan(&listing.Id, &listing.Name, &listing.Type, &listing.ParentId); err != nil { + if err := rows.Scan(&listing.Id, &listing.Name, &listing.Type, &listing.FeatureId, &listing.Address, &listing.Details, &listing.ContactIds); err != nil { return nil, fmt.Errorf("row scan failed: %w", err) } listings = append(listings, &listing) } + fmt.Print(listings) return } const findContactsByListingIDs = ` SELECT - l.id AS listing_id, - l.name AS listing_name, - l.details AS listing_details, - l.contact_ids, -- Array of contact_ids - c.id AS contact_id, - c.name AS contact_name, - c.type AS contact_type, - c.details AS contact_details + l.id, + l.name, + l.type, + l.feature_id, + l.address, + l.details, + l.contact_ids FROM - listings l - LEFT JOIN LATERAL - unnest(l.contact_ids) AS contact_id ON true - LEFT JOIN - contacts c ON c.internal_id = contact_id - WHERE l.internal_id in ($1) + directory.listings l + + WHERE l.feature_id = ANY($1) ` + +//LEFT JOIN LATERAL +//unnest(l.contact_ids) AS contact_id ON true +//LEFT JOIN +//directory.contacts c ON c.internal_id = contact_id diff --git a/pkg/types/types.go b/pkg/types/types.go index a638a62..c0e5bef 100644 --- a/pkg/types/types.go +++ b/pkg/types/types.go @@ -19,24 +19,23 @@ type Directory struct { } type Feature struct { - Id int `json:"id"` - Name string `json:"name"` - Type FeatureType `json:"type"` - ParentId *int `json:"parent_id"` + Id int `json:"id"` + InternalId int `json:"internal_id"` + Name string `json:"name"` + Type FeatureType `json:"type"` + ParentId *int `json:"parent_id"` + Listings []*Listing `json:"listings"` } type Listing struct { - Id int `json:"id"` - Name string `json:"name"` - Type ListingType `json:"type"` - // TODO rename to feature_internal_id FeatureInternalId - // TODO should be hidden from response - FeatureId int `json:"feature_id"` - ParentId int `json:"parent_id"` - Address string `json:"address"` - ContactIds []int `json:"contact_ids"` - Details *string `json:"details"` - Contacts []Contact `json:"contacts"` + Id string `json:"id"` + Name string `json:"name"` + Type ListingType `json:"type"` + FeatureId int `json:"feature_id"` + Address string `json:"address"` + ContactIds []int `json:"contact_ids"` + Details *string `json:"details"` + Contacts []Contact `json:"contacts"` // last_modified } From 19932b057ca0ecebe74c2584e3da60d819e91e42 Mon Sep 17 00:00:00 2001 From: Gabriel Aboy Date: Sat, 18 Jan 2025 14:10:34 -0700 Subject: [PATCH 4/5] update to not expose internal_id and contacts listing query --- cli/server/server.go | 4 +- internal/services/api/features/features.go | 28 +++-------- internal/services/api/listings/listings.go | 39 ++++++++++++--- internal/store/database/contacts.go | 49 +++++++++++++++++++ .../database/{feature.go => features.go} | 29 ++++++----- .../database/{listing.go => listings.go} | 9 +--- pkg/types/types.go | 39 ++++++--------- 7 files changed, 124 insertions(+), 73 deletions(-) create mode 100644 internal/store/database/contacts.go rename internal/store/database/{feature.go => features.go} (78%) rename internal/store/database/{listing.go => listings.go} (83%) diff --git a/cli/server/server.go b/cli/server/server.go index 2ddf023..3bd788c 100644 --- a/cli/server/server.go +++ b/cli/server/server.go @@ -4,6 +4,7 @@ import ( "context" features "directory/internal/services/api/features" listings "directory/internal/services/api/listings" + "fmt" "os" "os/signal" @@ -57,10 +58,11 @@ func initialize(cfg *config.Config) (s *server.Server, err error) { // Stores featureStore := database.NewFeatureStore(db) listingStore := database.NewListingStore(db) + contactStore := database.NewContactsStore(db) // Services featuresService := features.NewFeatureService(*featureStore, *listingStore) - listingService := listings.NewListingService(*featureStore, *listingStore) + listingService := listings.NewListingService(*listingStore, *contactStore) services = append(services, featuresService, listingService) diff --git a/internal/services/api/features/features.go b/internal/services/api/features/features.go index b978e11..c650ad6 100644 --- a/internal/services/api/features/features.go +++ b/internal/services/api/features/features.go @@ -3,11 +3,8 @@ package api import ( "directory/pkg/types" "encoding/json" - "fmt" - "net/http" - "strconv" - "github.com/go-chi/chi/v5" + "net/http" "directory/internal/store/database" ) @@ -30,17 +27,12 @@ func (s *FeatureService) RegisterRoutes(mux *chi.Mux) { } -// TODO add query param for find 1 or recursive func (s *FeatureService) FindByID(w http.ResponseWriter, r *http.Request) { ctx := r.Context() - id, err := strconv.Atoi(chi.URLParam(r, "id")) - if err != nil { - http.Error(w, "feature id is invalid", http.StatusBadRequest) - return - } + id := chi.URLParam(r, "id") - features, _, err := s.featureStore.FindRelationsByID(ctx, id) + features, err := s.featureStore.FindByID(ctx, id) if err != nil { http.Error(w, "feature id was not found", http.StatusNotFound) } @@ -59,32 +51,28 @@ func (s *FeatureService) FindByID(w http.ResponseWriter, r *http.Request) { } } -// TODO add query param for find 1 or recursive func (s *FeatureService) FindListingsByFeatureId(w http.ResponseWriter, r *http.Request) { ctx := r.Context() - featureId, err := strconv.Atoi(chi.URLParam(r, "featureId")) - if err != nil { - http.Error(w, "feature id is invalid", http.StatusBadRequest) - return - } + featureId := chi.URLParam(r, "featureId") featureTree, featureInternalIds, err := s.featureStore.FindRelationsByID(ctx, featureId) if err != nil { http.Error(w, "feature id was not found", http.StatusNotFound) } listings, err := s.listingStore.FindByListingFeatureInternalIDs(ctx, featureInternalIds) - listingMap := listingsToMap(listings) if err != nil { - http.Error(w, "feature id was not found", http.StatusNotFound) + http.Error(w, "listings for feature ids were not found", http.StatusNotFound) } - fmt.Println(listingMap) + + listingMap := listingsToMap(listings) for _, feature := range featureTree { listing, match := listingMap[feature.InternalId] if match { feature.Listings = append(feature.Listings, listing) } } + response, err := json.Marshal(featureTree) if err != nil { http.Error(w, "Error marshalling JSON", http.StatusInternalServerError) diff --git a/internal/services/api/listings/listings.go b/internal/services/api/listings/listings.go index 7513136..fc60d86 100644 --- a/internal/services/api/listings/listings.go +++ b/internal/services/api/listings/listings.go @@ -1,21 +1,48 @@ package api import ( - "github.com/go-chi/chi/v5" - "directory/internal/store/database" + "encoding/json" + "github.com/go-chi/chi/v5" + "net/http" ) type ListingService struct { - featureStore database.FeatureStore listingStore database.ListingStore + contactStore database.ContactsStore } -func NewListingService(featureStore database.FeatureStore, listingStore database.ListingStore) *ListingService { +func NewListingService(listingStore database.ListingStore, contactStore database.ContactsStore) *ListingService { return &ListingService{ - featureStore: featureStore, + contactStore: contactStore, listingStore: listingStore, } } -func (s *ListingService) RegisterRoutes(mux *chi.Mux) {} +func (s *ListingService) RegisterRoutes(mux *chi.Mux) { + mux.Get("/listing/{id}/contacts", s.FindContactByListingID) +} + +func (s *ListingService) FindContactByListingID(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + + id := chi.URLParam(r, "id") + + contacts, err := s.contactStore.FindContactsByListingIDs(ctx, id) + if err != nil { + http.Error(w, "contacts were not found for the requested listing", http.StatusNotFound) + } + + response, err := json.Marshal(contacts) + if err != nil { + http.Error(w, "Error marshalling JSON", http.StatusInternalServerError) + return + } + + w.WriteHeader(http.StatusOK) + _, err = w.Write(response) + if err != nil { + http.Error(w, "Error writing response", http.StatusInternalServerError) + return + } +} diff --git a/internal/store/database/contacts.go b/internal/store/database/contacts.go new file mode 100644 index 0000000..f6ff278 --- /dev/null +++ b/internal/store/database/contacts.go @@ -0,0 +1,49 @@ +package database + +import ( + "context" + "fmt" + + db "directory/pkg/database" + "directory/pkg/types" +) + +type ContactsStore struct { + store db.Pool +} + +func NewContactsStore(s db.Pool) *ContactsStore { + return &ContactsStore{store: s} +} + +func (s *ContactsStore) FindContactsByListingIDs(ctx context.Context, listingId string) (contacts []*types.Contact, err error) { + rows, err := s.store.Query(ctx, findContactsByIDs, listingId) + if err != nil { + return nil, fmt.Errorf("query failed: %w", err) + } + for rows.Next() { + var contact types.Contact + if err := rows.Scan(&contact.Id, &contact.Name, &contact.Type, &contact.Details); err != nil { + return nil, fmt.Errorf("row scan failed: %w", err) + } + contacts = append(contacts, &contact) + } + return +} + +const findContactsByIDs = ` + SELECT + id, + name, + type, + details + FROM directory.contacts + WHERE internal_id + = ANY(SELECT unnest(contact_ids) FROM directory.listings WHERE id = $1) +` + +// Notes +//LEFT JOIN LATERAL +//unnest(l.contact_ids) AS contact_id ON true +//LEFT JOIN +//directory.contacts c ON c.internal_id = contact_id diff --git a/internal/store/database/feature.go b/internal/store/database/features.go similarity index 78% rename from internal/store/database/feature.go rename to internal/store/database/features.go index 10cf0ca..c298685 100644 --- a/internal/store/database/feature.go +++ b/internal/store/database/features.go @@ -8,7 +8,6 @@ import ( "directory/pkg/types" ) -// TODO Rename to features type FeatureStore struct { store db.Pool } @@ -22,30 +21,30 @@ func (s *FeatureStore) Create(ctx context.Context, feature types.Feature) (creat return } -func (s *FeatureStore) FindByID(ctx context.Context, id int) (feature *types.Feature, err error) { - feature = &types.Feature{} - err = s.store.QueryRow(ctx, findByIDQuery, id).Scan(&feature.Id, &feature.Name, &feature.Type, &feature.ParentId) +func (s *FeatureStore) Update(ctx context.Context, feature types.Feature) (updatedId string, err error) { + err = s.store.QueryRow(ctx, updateQuery, feature.Id, feature.Name, feature.Type, feature.ParentId).Scan(&updatedId) return } -func (s *FeatureStore) Update(ctx context.Context, feature types.Feature) (updatedId int, err error) { - err = s.store.QueryRow(ctx, updateQuery, feature.Id, feature.Name, feature.Type, feature.ParentId).Scan(&updatedId) +func (s *FeatureStore) FindByID(ctx context.Context, id string) (feature *types.Feature, err error) { + feature = &types.Feature{} + err = s.store.QueryRow(ctx, findByIDQuery, id).Scan(&feature.Id, &feature.Name, &feature.Type, &feature.ParentId) return } -func (s *FeatureStore) Delete(ctx context.Context, id int) (deletedId int, err error) { +func (s *FeatureStore) Delete(ctx context.Context, id string) (deletedId string, err error) { err = s.store.QueryRow(ctx, deleteQuery, id).Scan(&deletedId) return } -func (s *FeatureStore) FindRelationsByID(ctx context.Context, id int) (features []*types.Feature, featureIds []int, err error) { +func (s *FeatureStore) FindRelationsByID(ctx context.Context, id string) (features []*types.Feature, featureIds []int, err error) { rows, err := s.store.Query(ctx, recursiveFindByIDQuery, id) if err != nil { log.Fatalf("failed to query rows: %v", err) } for rows.Next() { var feature types.Feature - err := rows.Scan(&feature.InternalId, &feature.Name, &feature.Type, &feature.ParentId) + err := rows.Scan(&feature.Id, &feature.InternalId, &feature.Name, &feature.Type, &feature.ParentId) if err != nil { log.Fatalf("failed to scan row: %v", err) } @@ -58,7 +57,7 @@ func (s *FeatureStore) FindRelationsByID(ctx context.Context, id int) (features const createQuery = "INSERT INTO directory.features (name, type, parent_id) VALUES ($1, $2, $3) RETURNING id" -const findByIDQuery = "SELECT d.id, d.name, d.type, d.parent_id FROM directory.features d WHERE id = $1" +const findByIDQuery = "SELECT id, name, type, parent_id FROM directory.features WHERE id = $1" const updateQuery = "UPDATE directory.features SET name = $2, type = $3, parent_id = $4 WHERE id = $1 RETURNING id" @@ -71,7 +70,7 @@ WITH RECURSIVE ParentCTE AS ( -- Start with the given record and find its children SELECT internal_id, id, name, type, parent_id FROM directory.features - WHERE internal_id = $1 + WHERE id = $1 UNION ALL @@ -84,7 +83,7 @@ WITH RECURSIVE ParentCTE AS ( -- Start with the given record and find its children SELECT internal_id, id, name, type, parent_id FROM directory.features - WHERE internal_id = $1 + WHERE id = $1 UNION ALL @@ -94,8 +93,8 @@ WITH RECURSIVE ParentCTE AS ( JOIN ChildCTE c ON loc.parent_id = c.internal_id ) -- Combine results from both ParentCTE and ChildCTE - SELECT internal_id, name, type, parent_id FROM ParentCTE + SELECT id, internal_id, name, type, parent_id FROM ParentCTE UNION - SELECT internal_id, name, type, parent_id FROM ChildCTE - ORDER BY internal_id; + SELECT id, internal_id, name, type, parent_id FROM ChildCTE + ORDER BY id; ` diff --git a/internal/store/database/listing.go b/internal/store/database/listings.go similarity index 83% rename from internal/store/database/listing.go rename to internal/store/database/listings.go index ea184b4..1500c6e 100644 --- a/internal/store/database/listing.go +++ b/internal/store/database/listings.go @@ -21,7 +21,7 @@ func (s *ListingStore) FindByListingFeatureInternalIDs(ctx context.Context, feat if err != nil { return nil, fmt.Errorf("query failed: %w", err) } - // Iterate over the rows and map to struct + for rows.Next() { var listing types.Listing if err := rows.Scan(&listing.Id, &listing.Name, &listing.Type, &listing.FeatureId, &listing.Address, &listing.Details, &listing.ContactIds); err != nil { @@ -29,7 +29,6 @@ func (s *ListingStore) FindByListingFeatureInternalIDs(ctx context.Context, feat } listings = append(listings, &listing) } - fmt.Print(listings) return } @@ -44,11 +43,5 @@ const findContactsByListingIDs = ` l.contact_ids FROM directory.listings l - WHERE l.feature_id = ANY($1) ` - -//LEFT JOIN LATERAL -//unnest(l.contact_ids) AS contact_id ON true -//LEFT JOIN -//directory.contacts c ON c.internal_id = contact_id diff --git a/pkg/types/types.go b/pkg/types/types.go index c0e5bef..2745511 100644 --- a/pkg/types/types.go +++ b/pkg/types/types.go @@ -1,26 +1,10 @@ package types -type Division struct { - Id int `json:"id"` - Name string `json:"name"` - Type string `json:"type"` - ParentId *int `json:"parent_id"` -} - type Type string -type Directory struct { - Country string `json:"country"` - State string `json:"state"` - City string `json:"city"` - - Listings []*Listing `json:"listings"` - Ads []*Ad `json:"ads"` -} - type Feature struct { - Id int `json:"id"` - InternalId int `json:"internal_id"` + Id string `json:"id"` + InternalId int `json:",omitempty" db:"internal_id"` Name string `json:"name"` Type FeatureType `json:"type"` ParentId *int `json:"parent_id"` @@ -29,6 +13,7 @@ type Feature struct { type Listing struct { Id string `json:"id"` + InternalId int `json:",omitempty" db:"internal_id"` Name string `json:"name"` Type ListingType `json:"type"` FeatureId int `json:"feature_id"` @@ -36,14 +21,14 @@ type Listing struct { ContactIds []int `json:"contact_ids"` Details *string `json:"details"` Contacts []Contact `json:"contacts"` - // last_modified } type Contact struct { - Id int `json:"id"` - Name string `json:"name"` - Type FeatureType `json:"type"` - ParentId *int `json:"parent_id"` + Id string `json:"id"` + InternalId int `json:",omitempty" db:"internal_id"` + Name string `json:"name"` + Type ContactType `json:"type"` + Details *string `json:"details"` } // TODO Implement these structs @@ -52,3 +37,11 @@ type Ad struct { Name string `json:"name"` Phone string `json:"phone"` } + +type Directory struct { + Country string `json:"country"` + State string `json:"state"` + City string `json:"city"` + Listings []*Listing `json:"listings"` + Ads []*Ad `json:"ads"` +} From 68e6777d31973bb2e0295fdb67859400657315a6 Mon Sep 17 00:00:00 2001 From: Gabriel Aboy Date: Sat, 18 Jan 2025 14:19:23 -0700 Subject: [PATCH 5/5] add search_path to connection string --- example.env | 2 +- internal/store/database/contacts.go | 6 +++--- internal/store/database/features.go | 16 ++++++++-------- internal/store/database/listings.go | 2 +- 4 files changed, 13 insertions(+), 13 deletions(-) diff --git a/example.env b/example.env index 3f903d9..18bef2d 100644 --- a/example.env +++ b/example.env @@ -1,7 +1,7 @@ SERVER_ADDRESS=:6000 DATABASE_DRIVER=postgres -DATABASE_SOURCE= +DATABASE_SOURCE=postgres://:@:5432/directory?search_path=directory DATABASE_MAX_CONN_LIFETIME=1h DATABASE_MAX_CONNECTIONS=20 DATABASE_CONNECT_TIMEOUT=5s diff --git a/internal/store/database/contacts.go b/internal/store/database/contacts.go index f6ff278..ae97f1e 100644 --- a/internal/store/database/contacts.go +++ b/internal/store/database/contacts.go @@ -37,13 +37,13 @@ const findContactsByIDs = ` name, type, details - FROM directory.contacts + FROM contacts WHERE internal_id - = ANY(SELECT unnest(contact_ids) FROM directory.listings WHERE id = $1) + = ANY(SELECT unnest(contact_ids) FROM listings WHERE id = $1) ` // Notes //LEFT JOIN LATERAL //unnest(l.contact_ids) AS contact_id ON true //LEFT JOIN -//directory.contacts c ON c.internal_id = contact_id +//contacts c ON c.internal_id = contact_id diff --git a/internal/store/database/features.go b/internal/store/database/features.go index c298685..461121e 100644 --- a/internal/store/database/features.go +++ b/internal/store/database/features.go @@ -55,13 +55,13 @@ func (s *FeatureStore) FindRelationsByID(ctx context.Context, id string) (featur return } -const createQuery = "INSERT INTO directory.features (name, type, parent_id) VALUES ($1, $2, $3) RETURNING id" +const createQuery = "INSERT INTO features (name, type, parent_id) VALUES ($1, $2, $3) RETURNING id" -const findByIDQuery = "SELECT id, name, type, parent_id FROM directory.features WHERE id = $1" +const findByIDQuery = "SELECT id, name, type, parent_id FROM features WHERE id = $1" -const updateQuery = "UPDATE directory.features SET name = $2, type = $3, parent_id = $4 WHERE id = $1 RETURNING id" +const updateQuery = "UPDATE features SET name = $2, type = $3, parent_id = $4 WHERE id = $1 RETURNING id" -const deleteQuery = "DELETE FROM directory.features WHERE id = $1 RETURNING id" +const deleteQuery = "DELETE FROM features WHERE id = $1 RETURNING id" // WARNING Can this ever be circular? // If a parent references a child incorrectly -> child references parent correctly @@ -69,27 +69,27 @@ const recursiveFindByIDQuery = ` WITH RECURSIVE ParentCTE AS ( -- Start with the given record and find its children SELECT internal_id, id, name, type, parent_id - FROM directory.features + FROM features WHERE id = $1 UNION ALL -- Find all parents of the current record SELECT loc.internal_id, loc.id, loc.name, loc.type, loc.parent_id - FROM directory.features loc + FROM features loc JOIN ParentCTE p ON loc.internal_id = p.parent_id ), ChildCTE AS ( -- Start with the given record and find its children SELECT internal_id, id, name, type, parent_id - FROM directory.features + FROM features WHERE id = $1 UNION ALL -- Find all children of the current record SELECT loc.internal_id, loc.id, loc.name, loc.type, loc.parent_id - FROM directory.features loc + FROM features loc JOIN ChildCTE c ON loc.parent_id = c.internal_id ) -- Combine results from both ParentCTE and ChildCTE diff --git a/internal/store/database/listings.go b/internal/store/database/listings.go index 1500c6e..1b60272 100644 --- a/internal/store/database/listings.go +++ b/internal/store/database/listings.go @@ -42,6 +42,6 @@ const findContactsByListingIDs = ` l.details, l.contact_ids FROM - directory.listings l + listings l WHERE l.feature_id = ANY($1) `