feat: add public search controls
Quality / test (push) Canceled after 0s

Add public category lookup options, allowlisted sort ordering, URL-backed catalog controls, and strict search route validation with regression tests.
This commit is contained in:
2026-09-09 01:44:28 -04:00
parent 67ce29a93e
commit f4e4685ecc
10 changed files with 71 additions and 12 deletions
@@ -0,0 +1,7 @@
export async function registerPublicLookupRoutes(app, options) {
const { catalogLookupRepository } = options;
app.get('/public/lookups/categories', async () => ({
data: await catalogLookupRepository.listCategories(),
}));
}
@@ -53,6 +53,7 @@ export function createListingRepository(db) {
page = 1,
limit = 20,
includeSold = false,
sort = 'recent',
} = filters;
const conditions = [
@@ -96,6 +97,11 @@ export function createListingRepository(db) {
}
const offset = (page - 1) * limit;
const sortOrder = sort === 'price_low'
? listings.askingPriceMinor
: sort === 'price_high'
? desc(listings.askingPriceMinor)
: desc(listings.createdAt);
const [countResult, results] = await Promise.all([
db
@@ -110,7 +116,7 @@ export function createListingRepository(db) {
.innerJoin(inventoryItems, eq(inventoryItems.id, listings.inventoryItemId))
.innerJoin(products, eq(products.id, inventoryItems.productId))
.where(and(...conditions))
.orderBy(desc(listings.createdAt))
.orderBy(sortOrder, desc(listings.createdAt))
.limit(limit)
.offset(offset),
]);
@@ -23,6 +23,7 @@ export function createSearchQuery({
page = 1,
limit = 20,
includeSold = false,
sort = 'recent',
}) {
const text = normalizeSearchQuery(query);
@@ -50,6 +51,10 @@ export function createSearchQuery({
throw new Error('minPriceMinor must be <= maxPriceMinor');
}
if (!['recent', 'price_low', 'price_high'].includes(sort)) {
throw new Error('sort must be recent, price_low, or price_high');
}
return {
text,
...(typeof category === 'string' && category.trim().length > 0 ? { category: category.trim() } : {}),
@@ -58,6 +63,7 @@ export function createSearchQuery({
page,
limit,
includeSold,
...(sort !== 'recent' ? { sort } : {}),
};
}
@@ -32,6 +32,7 @@ export async function registerPublicSearchRoutes(app, options) {
page: coerceNumber(request.query.page, 'page', 1),
limit: coerceNumber(request.query.limit, 'limit', 20),
includeSold: request.query.includeSold === 'true' || request.query.includeSold === true,
sort: request.query.sort ?? 'recent',
};
return { data: await searchService.searchPublic(filters) };