feat: fruit search by name/synonym and type filter (story #06)
Backend: List repo query gains name (ILIKE with wildcard escaping on
name + synonym LEFT JOIN, SELECT DISTINCT) and types (ANY cast) params;
handler parses ?name= and ?type=, resolves combined-type aliases from
domain.FruitTypeAliases, validates plain types against validFruitTypes
to prevent Postgres enum cast errors. ORDER BY f.name, f.id for stable
pagination.
Frontend: listFruits gains optional {name, type} params; fruitStore adds
searchName/searchType state and setSearch action (resets offset, refetches);
FruitList.vue wires text input (debounced 300 ms + Enter) and flat type
dropdown (17 enum values + 4 aliases per spec §5 order); debounce timer
cleared on unmount.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/jackc/pgx/v5"
|
||||
"github.com/jackc/pgx/v5/pgconn"
|
||||
@@ -33,15 +34,35 @@ func mapPgError(err error) error {
|
||||
return err
|
||||
}
|
||||
|
||||
func (r *FruitRepo) List(ctx context.Context, limit, offset int) ([]domain.Fruit, int, error) {
|
||||
func escapeLike(s string) string {
|
||||
s = strings.ReplaceAll(s, `\`, `\\`)
|
||||
s = strings.ReplaceAll(s, `%`, `\%`)
|
||||
s = strings.ReplaceAll(s, `_`, `\_`)
|
||||
return s
|
||||
}
|
||||
|
||||
func (r *FruitRepo) List(ctx context.Context, limit, offset int, name string, types []string) ([]domain.Fruit, int, error) {
|
||||
escaped := escapeLike(name)
|
||||
countRow := r.pool.QueryRow(ctx,
|
||||
`SELECT COUNT(DISTINCT f.id)
|
||||
FROM fruits f
|
||||
LEFT JOIN fruit_synonyms fs ON fs.fruit_id = f.id
|
||||
WHERE ($1 = '' OR f.name ILIKE '%' || $1 || '%' ESCAPE '\' OR fs.synonym ILIKE '%' || $1 || '%' ESCAPE '\')
|
||||
AND ($2::fruit_type[] IS NULL OR f.fruit_type = ANY($2::fruit_type[]))`,
|
||||
escaped, types)
|
||||
var total int
|
||||
if err := r.pool.QueryRow(ctx, "SELECT COUNT(*) FROM fruits").Scan(&total); err != nil {
|
||||
if err := countRow.Scan(&total); err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
rows, err := r.pool.Query(ctx,
|
||||
`SELECT id, name, osdb_number, comment, fruit_type, created_at, updated_at
|
||||
FROM fruits ORDER BY id LIMIT $1 OFFSET $2`, limit, offset)
|
||||
`SELECT DISTINCT f.id, f.name, f.osdb_number, f.comment, f.fruit_type, f.created_at, f.updated_at
|
||||
FROM fruits f
|
||||
LEFT JOIN fruit_synonyms fs ON fs.fruit_id = f.id
|
||||
WHERE ($1 = '' OR f.name ILIKE '%' || $1 || '%' ESCAPE '\' OR fs.synonym ILIKE '%' || $1 || '%' ESCAPE '\')
|
||||
AND ($2::fruit_type[] IS NULL OR f.fruit_type = ANY($2::fruit_type[]))
|
||||
ORDER BY f.name, f.id LIMIT $3 OFFSET $4`,
|
||||
escaped, types, limit, offset)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user