Skip to content
This repository has been archived by the owner on Aug 26, 2022. It is now read-only.

Enable searching customers by their IDs and optimize # of database queries #236

Merged
merged 1 commit into from
Oct 12, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion api/client.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -206,10 +206,16 @@ paths:
type: string
- name: count
in: query
description: Optional parameter for searching for customers by specifying the amount to return
description: Optional parameter for searching by specifying the amount to return
example: 20
schema:
type: string
- name: customerIDs
in: query
description: Optional parameter for searching by customers' IDs
example: e210a9d6-d755-4455-9bd2-9577ea7e1081,970ef15d-a4e1-473f-b5d7-da38163b0ba3
schema:
type: string
responses:
'200':
description: Customers were successfully retrieved
Expand Down
13 changes: 11 additions & 2 deletions pkg/client/api/openapi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -230,8 +230,8 @@ paths:
schema:
type: string
style: form
- description: Optional parameter for searching for customers by specifying
the amount to return
- description: Optional parameter for searching by specifying the amount to
alovak marked this conversation as resolved.
Show resolved Hide resolved
return
example: 20
explode: true
in: query
Expand All @@ -240,6 +240,15 @@ paths:
schema:
type: string
style: form
- description: Optional parameter for searching by customers' IDs
example: e210a9d6-d755-4455-9bd2-9577ea7e1081,970ef15d-a4e1-473f-b5d7-da38163b0ba3
explode: true
in: query
name: customerIDs
required: false
schema:
type: string
style: form
responses:
"200":
content:
Expand Down
19 changes: 12 additions & 7 deletions pkg/client/api_customers.go
Original file line number Diff line number Diff line change
Expand Up @@ -2517,12 +2517,13 @@ func (a *CustomersApiService) ReplaceCustomerMetadata(ctx _context.Context, cust

// SearchCustomersOpts Optional parameters for the method 'SearchCustomers'
type SearchCustomersOpts struct {
Query optional.String
Email optional.String
Status optional.String
Type_ optional.String
Skip optional.String
Count optional.String
Query optional.String
Email optional.String
Status optional.String
Type_ optional.String
Skip optional.String
Count optional.String
CustomerIDs optional.String
}

/*
Expand All @@ -2535,7 +2536,8 @@ Search for customers using different filter parameters
* @param "Status" (optional.String) - Optional parameter for searching by customer status
* @param "Type_" (optional.String) - Optional parameter for searching by customer type
* @param "Skip" (optional.String) - Optional parameter for searching for customers by skipping over an initial group
* @param "Count" (optional.String) - Optional parameter for searching for customers by specifying the amount to return
* @param "Count" (optional.String) - Optional parameter for searching by specifying the amount to return
* @param "CustomerIDs" (optional.String) - Optional parameter for searching by customers' IDs
@return []Customer
*/
func (a *CustomersApiService) SearchCustomers(ctx _context.Context, localVarOptionals *SearchCustomersOpts) ([]Customer, *_nethttp.Response, error) {
Expand Down Expand Up @@ -2572,6 +2574,9 @@ func (a *CustomersApiService) SearchCustomers(ctx _context.Context, localVarOpti
if localVarOptionals != nil && localVarOptionals.Count.IsSet() {
localVarQueryParams.Add("count", parameterToString(localVarOptionals.Count.Value(), ""))
}
if localVarOptionals != nil && localVarOptionals.CustomerIDs.IsSet() {
localVarQueryParams.Add("customerIDs", parameterToString(localVarOptionals.CustomerIDs.Value(), ""))
}
// to determine the Content-Type header
localVarHTTPContentTypes := []string{}

Expand Down
3 changes: 2 additions & 1 deletion pkg/client/docs/CustomersApi.md
Original file line number Diff line number Diff line change
Expand Up @@ -1218,7 +1218,8 @@ Name | Type | Description | Notes
**status** | **optional.String**| Optional parameter for searching by customer status |
**type_** | **optional.String**| Optional parameter for searching by customer type |
**skip** | **optional.String**| Optional parameter for searching for customers by skipping over an initial group |
**count** | **optional.String**| Optional parameter for searching for customers by specifying the amount to return |
**count** | **optional.String**| Optional parameter for searching by specifying the amount to return |
**customerIDs** | **optional.String**| Optional parameter for searching by customers' IDs |

### Return type

Expand Down
226 changes: 200 additions & 26 deletions pkg/customers/customer_search.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
package customers

import (
"database/sql"
"encoding/json"
"fmt"
"net/http"
Expand All @@ -13,6 +14,7 @@ import (
moovhttp "github.com/moov-io/base/http"

"github.com/moov-io/base/log"

"github.com/moov-io/customers/pkg/client"
"github.com/moov-io/customers/pkg/route"
)
Expand All @@ -26,7 +28,7 @@ func searchCustomers(logger log.Logger, repo CustomerRepository) http.HandlerFun
return
}

params, err := readSearchParams(r)
params, err := parseSearchParams(r)
if err != nil {
moovhttp.Problem(w, err)
return
Expand Down Expand Up @@ -55,15 +57,25 @@ type SearchParams struct {
Type string
Skip int64
Count int64
CustomerIDs []string
}

func readSearchParams(r *http.Request) (SearchParams, error) {
func parseSearchParams(r *http.Request) (SearchParams, error) {
queryParams := r.URL.Query()
getQueryParam := func(key string) string {
alovak marked this conversation as resolved.
Show resolved Hide resolved
return strings.ToLower(strings.TrimSpace(queryParams.Get(key)))
}
params := SearchParams{
Query: strings.ToLower(strings.TrimSpace(r.URL.Query().Get("query"))),
Email: strings.ToLower(strings.TrimSpace(r.URL.Query().Get("email"))),
Status: strings.ToLower(strings.TrimSpace(r.URL.Query().Get("status"))),
Type: strings.ToLower(strings.TrimSpace(r.URL.Query().Get("type"))),
Query: getQueryParam("query"),
Email: getQueryParam("email"),
Status: getQueryParam("status"),
Type: getQueryParam("type"),
}
customerIDsInput := getQueryParam("customerIDs")
if customerIDsInput != "" {
params.CustomerIDs = strings.Split(customerIDsInput, ",")
}

skip, count, exists, err := moovhttp.GetSkipAndCount(r)
if exists && err != nil {
return params, err
Expand All @@ -84,54 +96,103 @@ func (r *sqlCustomerRepository) searchCustomers(params SearchParams) ([]*client.
}
defer stmt.Close()

var customerIDs []string
customers := make([]*client.Customer, 0)

// grab customerIDs
rows, err := stmt.Query(args...)
if err != nil {
return customers, err
return nil, err
}
for rows.Next() {
var customerID string
if err := rows.Scan(&customerID); err == nil {
customerIDs = append(customerIDs, customerID)
} else {
return customers, err
var c client.Customer
var birthDate *string
err := rows.Scan(
&c.CustomerID,
&c.FirstName,
&c.MiddleName,
&c.LastName,
&c.NickName,
&c.Suffix,
&c.Type,
&birthDate,
&c.Status,
&c.Email,
&c.CreatedAt,
&c.LastModified,
)
if err != nil {
return nil, err
}
customers = append(customers, &c)
}

// Read each Customer
for i := range customerIDs {
cust, err := r.GetCustomer(customerIDs[i])
if err != nil {
return customers, fmt.Errorf("search: customerID=%s error=%v", customerIDs[i], err)
}
customers = append(customers, cust)
if len(customers) == 0 {
return customers, nil
}

var customerIDs []string
for _, c := range customers {
customerIDs = append(customerIDs, c.CustomerID)
}

phonesByCustomerID, err := r.getPhones(customerIDs)
if err != nil {
return nil, fmt.Errorf("fetching customer phones: %v", err)
}
addressesByCustomerID, err := r.getAddresses(customerIDs)
if err != nil {
return nil, fmt.Errorf("fetching customer addresses: %v", err)
}
metadataByCustomerID, err := r.getMetadata(customerIDs)
if err != nil {
return nil, fmt.Errorf("fetching customer metadata: %v", err)
}

for _, c := range customers {
c.Phones = phonesByCustomerID[c.CustomerID]
c.Addresses = addressesByCustomerID[c.CustomerID]
c.Metadata = metadataByCustomerID[c.CustomerID].Metadata
}

return customers, nil
}

func buildSearchQuery(params SearchParams) (string, []interface{}) {
var args []interface{}
query := `select customer_id from customers where deleted_at is null and organization = ?`
args = append(args, params.Organization)
query := `select customer_id, first_name, middle_name, last_name, nick_name, suffix, type, birth_date, status, email, created_at, last_modified
from customers where deleted_at is null`

if params.Organization != "" {
query += " and organization = ?"
args = append(args, params.Organization)
}

if params.Query != "" {
query += " and lower(first_name) || \" \" || lower(last_name) LIKE ?"
args = append(args, "%"+params.Query+"%")
// warning: this will ONLY work for MySQL
alovak marked this conversation as resolved.
Show resolved Hide resolved
query += " and lower(concat(first_name,' ', last_name)) LIKE ?"
args = append(args, fmt.Sprintf("%%%s%%", params.Query))
}

if params.Email != "" {
query += " and lower(email) like ?"
args = append(args, "%"+params.Email)
}

if params.Status != "" {
query += " and status like ?"
args = append(args, "%"+params.Status)
}

if params.Type != "" {
query += " and type like ?"
args = append(args, "%"+params.Type)
}

if len(params.CustomerIDs) > 0 {
query += fmt.Sprintf(" and customer_id in (?%s)", strings.Repeat(",?", len(params.CustomerIDs)-1))
for _, id := range params.CustomerIDs {
args = append(args, id)
}
}

query += " order by created_at desc limit ?"
args = append(args, fmt.Sprintf("%d", params.Count))

Expand All @@ -142,3 +203,116 @@ func buildSearchQuery(params SearchParams) (string, []interface{}) {
query += ";"
return query, args
}

func (r *sqlCustomerRepository) getPhones(customerIDs []string) (map[string][]client.Phone, error) {
query := fmt.Sprintf(
"select customer_id, number, valid, type from customers_phones where customer_id in (?%s)",
strings.Repeat(",?", len(customerIDs)-1),
)

rows, err := r.queryRowsByCustomerIDs(query, customerIDs)
if err != nil {
return nil, err
}
defer rows.Close()

ret := make(map[string][]client.Phone)
alovak marked this conversation as resolved.
Show resolved Hide resolved
for rows.Next() {
var p client.Phone
var customerID string
err := rows.Scan(
&customerID,
&p.Number,
&p.Valid,
&p.Type,
)
if err != nil {
return nil, fmt.Errorf("scanning row: %v", err)
}
ret[customerID] = append(ret[customerID], p)
}

return ret, nil
}

func (r *sqlCustomerRepository) getAddresses(customerIDs []string) (map[string][]client.CustomerAddress, error) {
query := fmt.Sprintf(
"select customer_id, address_id, type, address1, address2, city, state, postal_code, country, validated from customers_addresses where customer_id in (?%s) and deleted_at is null;",
strings.Repeat(",?", len(customerIDs)-1),
)
rows, err := r.queryRowsByCustomerIDs(query, customerIDs)
if err != nil {
return nil, err
}
defer rows.Close()

ret := make(map[string][]client.CustomerAddress)
for rows.Next() {
var a client.CustomerAddress
var customerID string
if err := rows.Scan(
&customerID,
&a.AddressID,
&a.Type,
&a.Address1,
&a.Address2,
&a.City,
&a.State,
&a.PostalCode,
&a.Country,
&a.Validated,
); err != nil {
return nil, fmt.Errorf("scanning row: %v", err)
}
ret[customerID] = append(ret[customerID], a)
}

return ret, nil
}

func (r *sqlCustomerRepository) getMetadata(customerIDs []string) (map[string]client.CustomerMetadata, error) {
query := fmt.Sprintf(
"select customer_id, meta_key, meta_value from customer_metadata where customer_id in (?%s);",
strings.Repeat(",?", len(customerIDs)-1),
)
rows, err := r.queryRowsByCustomerIDs(query, customerIDs)
if err != nil {
return nil, err
}
defer rows.Close()

ret := make(map[string]client.CustomerMetadata)
for rows.Next() {
m := client.CustomerMetadata{
Metadata: make(map[string]string),
}
var customerID string
var k, v string
if err := rows.Scan(&customerID, &k, &v); err != nil {
return nil, fmt.Errorf("scanning row: %v", err)
}
m.Metadata[k] = v
ret[customerID] = m
}
return ret, nil
}

func (r *sqlCustomerRepository) queryRowsByCustomerIDs(query string, customerIDs []string) (*sql.Rows, error) {
stmt, err := r.db.Prepare(query)
if err != nil {
return nil, fmt.Errorf("preparing query: %v", err)
}
defer stmt.Close()

var args []interface{}
for _, id := range customerIDs {
args = append(args, id)
}

rows, err := stmt.Query(args...)
if err != nil {
return nil, fmt.Errorf("executing query: %v", err)
}

return rows, nil
}
Loading
You are viewing a condensed version of this merge commit. You can view the full changes here.