diff --git a/cmd/sheltertech-go/main.go b/cmd/sheltertech-go/main.go index 19315d8f..e3087a94 100644 --- a/cmd/sheltertech-go/main.go +++ b/cmd/sheltertech-go/main.go @@ -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" @@ -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://33395501c62bebff33ef58295a800bb3@o191099.ingest.sentry.io/4505843152846848", @@ -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" diff --git a/internal/db/manager.go b/internal/db/manager.go index 6f4845df..78f97677 100644 --- a/internal/db/manager.go +++ b/internal/db/manager.go @@ -6,7 +6,7 @@ import ( "fmt" "log" - _ "github.com/lib/pq" + "github.com/lib/pq" ) type Manager struct { @@ -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 @@ -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) @@ -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) diff --git a/internal/db/sql.go b/internal/db/sql.go index 6ce8a5cb..4ed6cd25 100644 --- a/internal/db/sql.go +++ b/internal/db/sql.go @@ -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 @@ -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 diff --git a/internal/db/types.go b/internal/db/types.go index b6cb7a0c..75419f28 100644 --- a/internal/db/types.go +++ b/internal/db/types.go @@ -2,6 +2,9 @@ package db import ( "database/sql" + "database/sql/driver" + "encoding/json" + "errors" "time" ) @@ -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 diff --git a/internal/resources/types.go b/internal/resources/types.go index cd39f93b..2635c0c2 100644 --- a/internal/resources/types.go +++ b/internal/resources/types.go @@ -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)) diff --git a/internal/savedsearches/manager.go b/internal/savedsearches/manager.go new file mode 100644 index 00000000..5f5c7091 --- /dev/null +++ b/internal/savedsearches/manager.go @@ -0,0 +1,295 @@ +package savedsearches + +import ( + "encoding/json" + "fmt" + "strings" + + "github.com/go-chi/chi/v5" + "io/ioutil" + "log" + "net/http" + "strconv" + + "github.com/sheltertechsf/sheltertech-go/internal/db" +) + +type Manager struct { + DbClient *db.Manager +} + +func New(dbManager *db.Manager) *Manager { + manager := &Manager{ + DbClient: dbManager, + } + return manager +} + +// Get lists saved searches for current user +// (note - I don't have the user auth stuff here) +// +// @Summary Get Saved Searches for current User +// @Description get saved searches for user +// @Tags saved_searches +// @Accept json +// @Produce json +// @Success 200 {array} savedsearches.SavedSearches +// @Router /saved_searches [get] +func (m *Manager) Get(w http.ResponseWriter, r *http.Request) { + userId, err := strconv.Atoi(r.URL.Query().Get("user_id")) + if err != nil { + fmt.Println("error:", err) + writeStatus(w, http.StatusBadRequest) + return + } + + dbSavedSearches := m.DbClient.GetSavedSearches(userId) + dbEligibilities := m.DbClient.GetEligibilitiesByIDs(getEligibilityIdsFromDbSavedSearches(dbSavedSearches)) + dbCategories := m.DbClient.GetCategoriesByIDs(getCategoryIdsFromDbSavedSearches(dbSavedSearches)) + + eligibilityIdToName := make(map[int]string) + for _, dbEligibility := range dbEligibilities { + eligibilityIdToName[dbEligibility.Id] = dbEligibility.Name.String + } + categoryIdToName := make(map[int]string) + for _, dbCategory := range dbCategories { + categoryIdToName[dbCategory.Id] = dbCategory.Name + } + + response := SavedSearches{ + SavedSearches: FromDBTypeArray(dbSavedSearches, eligibilityIdToName, categoryIdToName), + } + writeJson(w, response) +} + +// Create saved search for current user +// (note - I don't have the user auth stuff here) +// +// @Summary Create SavedSearch for current User +// @Description new saved search for user +// @Tags saved_searches +// @Accept json +// @Produce json +// @Success 200 {object} savedsearches.SavedSearch +// @Router /saved_searches [post] +func (m *Manager) Post(w http.ResponseWriter, r *http.Request) { + defer r.Body.Close() + body, _ := ioutil.ReadAll(r.Body) + + savedSearch := &SavedSearch{} + err := json.Unmarshal(body, savedSearch) + if err != nil { + writeStatus(w, http.StatusInternalServerError) + } + + var dbEligibilityIds []int + if len(savedSearch.Search.Eligibilities) > 0 { + dbEligibilities := m.DbClient.GetEligibilitiesByNames(savedSearch.Search.Eligibilities) + if len(dbEligibilities) != len(savedSearch.Search.Eligibilities) { + var dbEligibilityNames []string + for _, dbEligibility := range dbEligibilities { + dbEligibilityNames = append(dbEligibilityNames, dbEligibility.Name.String) + } + missingEligibilities := diffStringSlices(savedSearch.Search.Eligibilities, dbEligibilityNames) + writeStatus(w, http.StatusBadRequest) + w.Write([]byte(fmt.Sprintf("Invalid eligibilities: %s", strings.Join(missingEligibilities, ", ")))) + return + } + for _, dbEligibility := range dbEligibilities { + dbEligibilityIds = append(dbEligibilityIds, dbEligibility.Id) + } + } + + var dbCategoryIds []int + if len(savedSearch.Search.Categories) > 0 { + dbCategories := m.DbClient.GetCategoriesByNames(savedSearch.Search.Categories) + if len(dbCategories) != len(savedSearch.Search.Categories) { + var dbCategoryNames []string + for _, dbCategory := range dbCategories { + dbCategoryNames = append(dbCategoryNames, dbCategory.Name) + } + missingCategories := diffStringSlices(savedSearch.Search.Categories, dbCategoryNames) + writeStatus(w, http.StatusBadRequest) + w.Write([]byte(fmt.Sprintf("Invalid categories: %s", strings.Join(missingCategories, ", ")))) + return + } + for _, dbCategory := range dbCategories { + dbCategoryIds = append(dbCategoryIds, dbCategory.Id) + } + } + + dbSavedSearch := &db.SavedSearch{ + UserId: savedSearch.UserId, + Name: savedSearch.Name, + Search: db.SavedSearchQuery{ + Eligibilities: dbEligibilityIds, + Categories: dbCategoryIds, + Lat: savedSearch.Search.Lat, + Lng: savedSearch.Search.Lng, + Query: savedSearch.Search.Query, + }, + } + + savedSearchId, err := m.DbClient.CreateSavedSearch(dbSavedSearch) + if err != nil { + log.Print(err) + writeStatus(w, http.StatusInternalServerError) + return + } + + dbSavedSearch = m.DbClient.GetSavedSearchById(savedSearchId) + if dbSavedSearch == nil { + // This really shouldn't happen, since we just created it. + writeStatus(w, http.StatusInternalServerError) + return + } + eligibilityIds := getEligibilityIdsFromDbSavedSearches([]*db.SavedSearch{dbSavedSearch}) + dbEligibilities := m.DbClient.GetEligibilitiesByIDs(eligibilityIds) + categoryIds := getCategoryIdsFromDbSavedSearches([]*db.SavedSearch{dbSavedSearch}) + dbCategories := m.DbClient.GetCategoriesByIDs(categoryIds) + + eligibilityIdToName := make(map[int]string) + for _, dbEligibility := range dbEligibilities { + eligibilityIdToName[dbEligibility.Id] = dbEligibility.Name.String + } + categoryIdToName := make(map[int]string) + for _, dbCategory := range dbCategories { + categoryIdToName[dbCategory.Id] = dbCategory.Name + } + + writeStatus(w, http.StatusCreated) + writeJson(w, FromDBType(dbSavedSearch, eligibilityIdToName, categoryIdToName)) +} + +// Get saved searches for current user +// (note - I don't have the user auth stuff here) +// +// @Summary Get Saved Search for current User +// @Description get saved searches for user +// @Tags saved_searches +// @Accept json +// @Produce json +// @Success 200 {array} savedsearches.SavedSearch +// @Router /saved_searches [get] +func (m *Manager) GetByID(w http.ResponseWriter, r *http.Request) { + savedSearchId, err := strconv.Atoi(chi.URLParam(r, "id")) + if err != nil { + fmt.Println("error:", err) + writeStatus(w, http.StatusBadRequest) + return + } + + dbSavedSearch := m.DbClient.GetSavedSearchById(savedSearchId) + eligibilityIds := getEligibilityIdsFromDbSavedSearches([]*db.SavedSearch{dbSavedSearch}) + dbEligibilities := m.DbClient.GetEligibilitiesByIDs(eligibilityIds) + categoryIds := getCategoryIdsFromDbSavedSearches([]*db.SavedSearch{dbSavedSearch}) + dbCategories := m.DbClient.GetCategoriesByIDs(categoryIds) + + eligibilityIdToName := make(map[int]string) + for _, dbEligibility := range dbEligibilities { + eligibilityIdToName[dbEligibility.Id] = dbEligibility.Name.String + } + categoryIdToName := make(map[int]string) + for _, dbCategory := range dbCategories { + categoryIdToName[dbCategory.Id] = dbCategory.Name + } + + response := FromDBType(dbSavedSearch, eligibilityIdToName, categoryIdToName) + + writeJson(w, response) +} + +// Delete saved search by ID +// not done +// (note - I don't have the user auth stuff here) +// +// @Summary Delete saved search by ID +// @Description delete a saved search for user +// @Tags saved_searches +// @Accept json +// @Produce json +// @Success 200 {object} savedsearches.SavedSearch +// @Router /saved_searches/{id} [delete] +func (m *Manager) Delete(w http.ResponseWriter, r *http.Request) { + id, err := strconv.Atoi(chi.URLParam(r, "id")) + if err != nil { + log.Printf("%v", err) + } + err = m.DbClient.DeleteSavedSearchById(id) + if err != nil { + log.Print(err) + writeStatus(w, http.StatusInternalServerError) + } + + writeStatus(w, http.StatusOK) +} + +func writeJson(w http.ResponseWriter, object interface{}) { + output, err := json.Marshal(object) + if err != nil { + fmt.Println("error:", err) + } + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + _, err = w.Write(output) + if err != nil { + panic(err) + } +} + +func writeStatus(w http.ResponseWriter, responseStatus int) { + w.WriteHeader(responseStatus) +} + +// Create a flattened, uniquified list of eligibility IDs. +func getEligibilityIdsFromDbSavedSearches(dbSavedSearches []*db.SavedSearch) []int { + eligibilityIdMap := make(map[int]bool) + for _, dbSavedSearch := range dbSavedSearches { + for _, id := range dbSavedSearch.Search.Eligibilities { + eligibilityIdMap[id] = true + } + } + var eligibilityIds []int + for id := range eligibilityIdMap { + eligibilityIds = append(eligibilityIds, id) + } + return eligibilityIds +} + +// Create a flattened, uniquified list of category IDs. +func getCategoryIdsFromDbSavedSearches(dbSavedSearches []*db.SavedSearch) []int { + categoryIdMap := make(map[int]bool) + for _, dbSavedSearch := range dbSavedSearches { + for _, id := range dbSavedSearch.Search.Categories { + categoryIdMap[id] = true + } + } + var categoryIds []int + for id := range categoryIdMap { + categoryIds = append(categoryIds, id) + } + return categoryIds +} + +// Return the elements of a that are not in b +func diffStringSlices(a []string, b []string) []string { + bMap := make(map[string]bool) + for _, s := range b { + bMap[s] = true + } + var results []string + seen := make(map[string]bool) + for _, s := range a { + _, inB := bMap[s] + if inB { + continue + } + _, inSeen := seen[s] + if inSeen { + continue + } + seen[s] = true + results = append(results, s) + } + return results +} diff --git a/internal/savedsearches/types.go b/internal/savedsearches/types.go new file mode 100644 index 00000000..005644c0 --- /dev/null +++ b/internal/savedsearches/types.go @@ -0,0 +1,66 @@ +package savedsearches + +import "github.com/sheltertechsf/sheltertech-go/internal/db" + +type SavedSearchQuery struct { + Eligibilities []string `json:"eligibilities"` + Categories []string `json:"categories"` + Lat *float64 `json:"lat"` + Lng *float64 `json:"lng"` + Query string `json:"query"` +} + +type SavedSearch struct { + Id int `json:"id"` + UserId int `json:"user_id"` + Name string `json:"name"` + Search SavedSearchQuery `json:"search"` +} + +type SavedSearches struct { + SavedSearches []*SavedSearch `json:"saved_searches"` +} + +// Convert DB SavedSearch to API SavedSearch +func FromDBType(dbSavedSearch *db.SavedSearch, eligibilityIdToName map[int]string, categoryIdToName map[int]string) *SavedSearch { + var eligibilityNames []string + for _, eligibilityId := range dbSavedSearch.Search.Eligibilities { + name, ok := eligibilityIdToName[eligibilityId] + if !ok { + // Return an error? + return nil + } + eligibilityNames = append(eligibilityNames, name) + } + var categoryNames []string + for _, categoryId := range dbSavedSearch.Search.Categories { + name, ok := categoryIdToName[categoryId] + if !ok { + // Return an error? + return nil + } + categoryNames = append(categoryNames, name) + } + + savedSearch := &SavedSearch{ + Id: dbSavedSearch.Id, + Name: dbSavedSearch.Name, + UserId: dbSavedSearch.UserId, + Search: SavedSearchQuery{ + Eligibilities: eligibilityNames, + Categories: categoryNames, + Lat: dbSavedSearch.Search.Lat, + Lng: dbSavedSearch.Search.Lng, + Query: dbSavedSearch.Search.Query, + }, + } + return savedSearch +} + +func FromDBTypeArray(dbSavedSearches []*db.SavedSearch, eligibilityIdToName map[int]string, categoryIdToName map[int]string) []*SavedSearch { + savedSearches := []*SavedSearch{} + for _, dbSavedSearch := range dbSavedSearches { + savedSearches = append(savedSearches, FromDBType(dbSavedSearch, eligibilityIdToName, categoryIdToName)) + } + return savedSearches +} diff --git a/internal/services/manager.go b/internal/services/manager.go index d08b6096..937c08f8 100644 --- a/internal/services/manager.go +++ b/internal/services/manager.go @@ -50,7 +50,7 @@ func (m *Manager) GetByID(w http.ResponseWriter, r *http.Request) { response.Categories = categories.FromDBTypeArray(m.DbClient.GetCategoriesByServiceID(serviceId)) response.Notes = notes.FromNoteDBTypeArray(m.DbClient.GetNotesByServiceID(serviceId)) response.Addresses = addresses.FromAddressesDBTypeArray(m.DbClient.GetAddressesByServiceID(serviceId)) - response.Eligibilities = eligibilities.FromEligibilitiesDBTypeArray(m.DbClient.GetEligibitiesByServiceID(serviceId)) + response.Eligibilities = eligibilities.FromEligibilitiesDBTypeArray(m.DbClient.GetEligibilitiesByServiceID(serviceId)) response.Instructions = instructions.FromInstructionDBTypeArray(m.DbClient.GetInstructionsByServiceID(serviceId)) response.Documents = documents.FromDocumentDBTypeArray(m.DbClient.GetDocumentsByServiceID(serviceId)) response.Schedule = schedules.FromDBType(m.DbClient.GetScheduleByServiceId(serviceId))