Skip to content

Commit

Permalink
Add saved searches APIs.
Browse files Browse the repository at this point in the history
This implements the list, get, post, and delete routes. It omits the put
route for now, since we might not actually need it yet, and it will
probably require some significant refactoring.
  • Loading branch information
richardxia committed Jun 26, 2024
1 parent 71b7c25 commit 3ad5979
Show file tree
Hide file tree
Showing 8 changed files with 593 additions and 4 deletions.
8 changes: 8 additions & 0 deletions cmd/sheltertech-go/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import (
"github.com/sheltertechsf/sheltertech-go/internal/db"
"github.com/sheltertechsf/sheltertech-go/internal/folders"
"github.com/sheltertechsf/sheltertech-go/internal/resources"
"github.com/sheltertechsf/sheltertech-go/internal/savedsearches"
"github.com/sheltertechsf/sheltertech-go/internal/services"
"github.com/sheltertechsf/sheltertech-go/internal/users"

Expand Down Expand Up @@ -75,6 +76,7 @@ func main() {
resourcesManager := resources.New(dbManager)
usersManager := users.New(dbManager, jwtKeyfunc)
bookmarksManager := bookmarks.New(dbManager)
savedSearchesManager := savedsearches.New(dbManager)

if err := sentry.Init(sentry.ClientOptions{
Dsn: "https://[email protected]/4505843152846848",
Expand Down Expand Up @@ -120,6 +122,12 @@ func main() {
r.Put("/api/bookmarks/{id}", bookmarksManager.Update)
r.Delete("/api/bookmarks/{id}", bookmarksManager.DeleteByID)

r.Get("/api/saved_searches", savedSearchesManager.Get)
r.Post("/api/saved_searches", savedSearchesManager.Post)
r.Get("/api/saved_searches/{id}", savedSearchesManager.GetByID)
// r.Put("/api/saved_searches/{id}", savedSearchesManager.Put)
r.Delete("/api/saved_searches/{id}", savedSearchesManager.Delete)

docs.SwaggerInfo.Title = "Swagger Example API"
docs.SwaggerInfo.Description = "This is a sample server Petstore server."
docs.SwaggerInfo.Version = "1.0"
Expand Down
144 changes: 142 additions & 2 deletions internal/db/manager.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import (
"fmt"
"log"

_ "github.com/lib/pq"
"github.com/lib/pq"
)

type Manager struct {
Expand Down Expand Up @@ -98,6 +98,36 @@ func (m *Manager) GetCategoryByID(categoryId int) *Category {
return scanCategory(row)
}

func (m *Manager) GetCategoriesByIDs(ids []int) []*Category {
var rows *sql.Rows
var err error
stmt, err := m.DB.Prepare(categoriesByIDsSql)
if err != nil {
log.Printf("Prepare failed: %v\n", err)
return nil
}
rows, err = stmt.Query(pq.Array(ids))
if err != nil {
log.Printf("%v\n", err)
}
return scanCategories(rows)
}

func (m *Manager) GetCategoriesByNames(names []string) []*Category {
var rows *sql.Rows
var err error
stmt, err := m.DB.Prepare(categoriesByNamesSql)
if err != nil {
log.Printf("Prepare failed: %v\n", err)
return nil
}
rows, err = stmt.Query(pq.Array(names))
if err != nil {
log.Printf("%v\n", err)
}
return scanCategories(rows)
}

func (m *Manager) GetCategoriesByServiceID(serviceId int) []*Category {
var rows *sql.Rows
var err error
Expand Down Expand Up @@ -268,7 +298,37 @@ func scanPhones(rows *sql.Rows) []*Phone {
return phones
}

func (m *Manager) GetEligibitiesByServiceID(serviceId int) []*Eligibility {
func (m *Manager) GetEligibilitiesByIDs(ids []int) []*Eligibility {
var rows *sql.Rows
var err error
stmt, err := m.DB.Prepare(eligibilitiesByIDsSql)
if err != nil {
log.Printf("Prepare failed: %v\n", err)
return nil
}
rows, err = stmt.Query(pq.Array(ids))
if err != nil {
log.Printf("%v\n", err)
}
return scanEligibilities(rows)
}

func (m *Manager) GetEligibilitiesByNames(names []string) []*Eligibility {
var rows *sql.Rows
var err error
stmt, err := m.DB.Prepare(eligibilitiesByNamesSql)
if err != nil {
log.Printf("Prepare failed: %v\n", err)
return nil
}
rows, err = stmt.Query(pq.Array(names))
if err != nil {
log.Printf("%v\n", err)
}
return scanEligibilities(rows)
}

func (m *Manager) GetEligibilitiesByServiceID(serviceId int) []*Eligibility {
var rows *sql.Rows
var err error
rows, err = m.DB.Query(eligibilitiesByServiceIDSql, serviceId)
Expand Down Expand Up @@ -724,6 +784,86 @@ func (m *Manager) DeleteFolderById(folderId int) error {
return tx.Commit()
}

func scanSavedSearch(row *sql.Row) *SavedSearch {
var savedSearch SavedSearch
err := row.Scan(&savedSearch.Id, &savedSearch.UserId, &savedSearch.Name, &savedSearch.Search)
if err != nil {
switch err {
case sql.ErrNoRows:
fmt.Println("No rows were returned!")
return nil
default:
panic(err)
}
}
return &savedSearch
}

func (m *Manager) GetSavedSearchById(savedSearchId int) *SavedSearch {
row := m.DB.QueryRow(savedSearchByIDSql, savedSearchId)
return scanSavedSearch(row)
}

func scanSavedSearches(rows *sql.Rows) []*SavedSearch {
var savedSearches []*SavedSearch
for rows.Next() {
var savedSearch SavedSearch
err := rows.Scan(&savedSearch.Id, &savedSearch.UserId, &savedSearch.Name, &savedSearch.Search)
switch err {
case sql.ErrNoRows:
fmt.Println("No rows were returned!")
return nil
}
savedSearches = append(savedSearches, &savedSearch)
}
return savedSearches
}

func (m *Manager) GetSavedSearches(userId int) []*SavedSearch {
var rows *sql.Rows
var err error
rows, err = m.DB.Query(savedSearchesByUserIDSql, userId)
if err != nil {
log.Printf("%v\n", err)
}
return scanSavedSearches(rows)
}

func (m *Manager) CreateSavedSearch(savedSearch *SavedSearch) (int, error) {
tx, err := m.DB.Begin()
if err != nil {
return -1, err
}
row := tx.QueryRow(createSavedSearchSql, savedSearch.UserId, savedSearch.Name, savedSearch.Search)
var id int
err = row.Scan(&id)
if err != nil {
return -1, err
}
return id, tx.Commit()
}

func (m *Manager) DeleteSavedSearchById(id int) error {
tx, err := m.DB.Begin()
if err != nil {
return err
}

res, err := tx.Exec(deleteSavedSearchSql, id)
if err != nil {
return err
}
rowCount, err := res.RowsAffected()
if err != nil {
return err
}
if rowCount != 1 {
defer tx.Rollback()
return errors.New(fmt.Sprintf("unexpected rows modified, expected one, saw %v", rowCount))
}
return tx.Commit()
}

func (m *Manager) GetUserByUserExternalID(userExternalId string) *User {
row := m.DB.QueryRow(userByUserExternalIDSql, userExternalId)
return scanUser(row)
Expand Down
47 changes: 47 additions & 0 deletions internal/db/sql.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,18 @@ FROM public.categories
WHERE id = $1
`

const categoriesByIDsSql = `
SELECT c.id, c.name, c.top_level, c.featured
FROM public.categories c
WHERE c.id = ANY ($1)
`

const categoriesByNamesSql = `
SELECT c.id, c.name, c.top_level, c.featured
FROM public.categories c
WHERE c.name = ANY ($1)
`

const categoriesByServiceIDSql = `
SELECT c.id, c.name, c.top_level, c.featured
FROM public.categories c
Expand Down Expand Up @@ -97,12 +109,47 @@ WHERE f.id = $1
// WHERE b.folder_id = $1
// `

const savedSearchByIDSql = `
SELECT id, user_id, name, search
FROM public.saved_searches
WHERE id = $1
`

const savedSearchesByUserIDSql = `
SELECT ss.id, ss.user_id, ss.name, ss.search
FROM public.saved_searches ss
WHERE ss.user_id = $1
`

const createSavedSearchSql = `
INSERT INTO public.saved_searches (user_id, name, search, created_at, updated_at)
VALUES ($1, $2, $3, now(), now())
RETURNING id
`

const deleteSavedSearchSql = `
DELETE FROM public.saved_searches ss
WHERE ss.id = $1
`

const phonesByResourceIDSql = `
SELECT p.id, p.number, p.service_type
FROM public.phones p
WHERE p.resource_id = $1
`

const eligibilitiesByIDsSql = `
SELECT e.id, e.name, e.feature_rank
FROM public.eligibilities e
WHERE e.id = ANY ($1)
`

const eligibilitiesByNamesSql = `
SELECT e.id, e.name, e.feature_rank
FROM public.eligibilities e
WHERE e.name = ANY ($1)
`

const eligibilitiesByServiceIDSql = `
SELECT e.id, e.name, e.feature_rank
FROM public.eligibilities e
Expand Down
33 changes: 33 additions & 0 deletions internal/db/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@ package db

import (
"database/sql"
"database/sql/driver"
"encoding/json"
"errors"
"time"
)

Expand Down Expand Up @@ -180,6 +183,36 @@ type Folder struct {
UserId int
}

type SavedSearchQuery struct {
Eligibilities []int `json:"eligibilities"`
Categories []int `json:"categories"`
Lat *float64 `json:"lat"`
Lng *float64 `json:"lng"`
Query string `json:"query"`
}

// Methods needed for automatic serialization/deserialization to JSONB column.
// https://www.alexedwards.net/blog/using-postgresql-jsonb

func (s SavedSearchQuery) Value() (driver.Value, error) {
return json.Marshal(s)
}

func (s *SavedSearchQuery) Scan(value interface{}) error {
b, ok := value.([]byte)
if !ok {
return errors.New("type assertion to []byte failed")
}
return json.Unmarshal(b, &s)
}

type SavedSearch struct {
Id int
UserId int
Name string
Search SavedSearchQuery
}

type User struct {
Id int
Name string
Expand Down
2 changes: 1 addition & 1 deletion internal/resources/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -164,7 +164,7 @@ func ConvertServicesToResourceServices(dbServices []*db.Service, dbClient *db.Ma
resourceService.Categories = categories.FromDBTypeArray(dbClient.GetCategoriesByServiceID(dbService.Id))
resourceService.Notes = notes.FromNoteDBTypeArray(dbClient.GetNotesByServiceID(dbService.Id))
resourceService.Addresses = addresses.FromAddressesDBTypeArray(dbClient.GetAddressesByServiceID(dbService.Id))
resourceService.Eligibilities = eligibilities.FromEligibilitiesDBTypeArray(dbClient.GetEligibitiesByServiceID(dbService.Id))
resourceService.Eligibilities = eligibilities.FromEligibilitiesDBTypeArray(dbClient.GetEligibilitiesByServiceID(dbService.Id))
resourceService.Instructions = instructions.FromInstructionDBTypeArray(dbClient.GetInstructionsByServiceID(dbService.Id))
resourceService.Documents = documents.FromDocumentDBTypeArray(dbClient.GetDocumentsByServiceID(dbService.Id))
resourceService.Schedule = schedules.FromDBType(dbClient.GetScheduleByServiceId(dbService.Id))
Expand Down
Loading

0 comments on commit 3ad5979

Please sign in to comment.