Merge branch 'main' into feat/pet-photo-upload-gh93

This commit is contained in:
groombook-ceo[bot]
2026-03-22 19:45:35 +00:00
committed by GitHub
32 changed files with 1121 additions and 96 deletions
+70
View File
@@ -0,0 +1,70 @@
import { Hono } from "hono";
import { and, eq, getDb, clients, ilike, or, pets } from "@groombook/db";
export const searchRouter = new Hono();
const LIMIT = 10;
/** Escape %, _, and \ in user input before wrapping with ILIKE wildcards. */
function escapeLike(s: string): string {
return `%${s.replace(/[%_\\]/g, "\\$&")}%`;
}
/**
* GET /api/search?q={query}
*
* Returns up to 10 matching active clients and up to 10 matching pets.
* Clients are matched on name, email, or phone.
* Pets are matched on name or breed; includes owner name.
*/
searchRouter.get("/", async (c) => {
const q = c.req.query("q");
if (!q || q.trim().length === 0) {
return c.json({ error: "Query parameter q is required" }, 400);
}
const pattern = escapeLike(q.trim());
const db = getDb();
const [matchingClients, matchingPets] = await Promise.all([
db
.select({
id: clients.id,
name: clients.name,
email: clients.email,
phone: clients.phone,
})
.from(clients)
.where(
and(
eq(clients.status, "active"),
or(
ilike(clients.name, pattern),
ilike(clients.email, pattern),
ilike(clients.phone, pattern)
)
)
)
.limit(LIMIT),
db
.select({
id: pets.id,
name: pets.name,
breed: pets.breed,
clientId: pets.clientId,
ownerName: clients.name,
})
.from(pets)
.innerJoin(clients, and(eq(pets.clientId, clients.id), eq(clients.status, "active")))
.where(
or(
ilike(pets.name, pattern),
ilike(pets.breed, pattern)
)
)
.limit(LIMIT),
]);
return c.json({ clients: matchingClients, pets: matchingPets });
});