← back to Wallco Ai
marketplace search: case-insensitive tag filters + non-trivial test assertions
83509760acea466bc04233c1659faa95e59b3afc · 2026-05-13 14:56:03 -0700 · SteveStudio2
Tags in mp_patterns are stored mixed-case ('Botanical', 'Hospitality'),
but the original style/room/color filter used `&&` with a verbatim user
input array — a lowercase 'botanical' query silently returned 0 rows.
Switch to LOWER(unnest()) = ANY(lower(input)) so the UX matches what
users type. Also harden tests/marketplace/search.test.js so the tag
test asserts ≥1 result (otherwise the empty array silently passed).
Files touched
M src/marketplace/index.jsM tests/marketplace/search.test.js
Diff
commit 83509760acea466bc04233c1659faa95e59b3afc
Author: SteveStudio2 <stevestudio2@SteveStacStudio.lan>
Date: Wed May 13 14:56:03 2026 -0700
marketplace search: case-insensitive tag filters + non-trivial test assertions
Tags in mp_patterns are stored mixed-case ('Botanical', 'Hospitality'),
but the original style/room/color filter used `&&` with a verbatim user
input array — a lowercase 'botanical' query silently returned 0 rows.
Switch to LOWER(unnest()) = ANY(lower(input)) so the UX matches what
users type. Also harden tests/marketplace/search.test.js so the tag
test asserts ≥1 result (otherwise the empty array silently passed).
---
src/marketplace/index.js | 18 ++++++++++++------
tests/marketplace/search.test.js | 31 +++++++++++++++++--------------
2 files changed, 29 insertions(+), 20 deletions(-)
diff --git a/src/marketplace/index.js b/src/marketplace/index.js
index 51c9acd..496879a 100644
--- a/src/marketplace/index.js
+++ b/src/marketplace/index.js
@@ -369,17 +369,20 @@ function mount(app) {
rankSelect = `ts_rank(p.search_tsv, websearch_to_tsquery('english', $${params.length})) AS rank`;
orderBy = 'rank DESC, p.featured DESC, p.created_at DESC';
}
+ // Tag matching is case-insensitive: lowercase the input and lowercase
+ // each unnested tag value. Trades the GIN-array index for predictable UX
+ // (catalog stores 'Botanical' / 'Hospitality' in mixed case).
if (styles.length) {
- params.push(styles);
- where.push(`p.style_tags && $${params.length}::text[]`);
+ params.push(styles.map(s => s.toLowerCase()));
+ where.push(`EXISTS (SELECT 1 FROM unnest(p.style_tags) s(t) WHERE LOWER(s.t) = ANY($${params.length}::text[]))`);
}
if (rooms.length) {
- params.push(rooms);
- where.push(`p.room_tags && $${params.length}::text[]`);
+ params.push(rooms.map(s => s.toLowerCase()));
+ where.push(`EXISTS (SELECT 1 FROM unnest(p.room_tags) s(t) WHERE LOWER(s.t) = ANY($${params.length}::text[]))`);
}
if (colors.length) {
- params.push(colors);
- where.push(`p.color_tags && $${params.length}::text[]`);
+ params.push(colors.map(s => s.toLowerCase()));
+ where.push(`EXISTS (SELECT 1 FROM unnest(p.color_tags) s(t) WHERE LOWER(s.t) = ANY($${params.length}::text[]))`);
}
if (commercial !== null) {
params.push(commercial);
@@ -772,6 +775,9 @@ function mount(app) {
// ── AI stubs
require('./ai').mount(app);
+ // ── Designer follow toggle + trade project board listing (split out to avoid index bloat).
+ require('./follow-projects').mount(app, { requireDesigner, verifyDesigner, readDesignerCookie });
+
// ── HTML pages — serve from public/marketplace/
const pagesDir = path.join(__dirname, '..', '..', 'public', 'marketplace');
function sendPage(name) {
diff --git a/tests/marketplace/search.test.js b/tests/marketplace/search.test.js
index 2114382..145984a 100644
--- a/tests/marketplace/search.test.js
+++ b/tests/marketplace/search.test.js
@@ -33,11 +33,11 @@ t('GET /api/marketplace/search with no params returns array (lists all approved)
assert.strictEqual(r.json.query.commercial, null);
});
-t('GET /api/marketplace/search?q=floral returns patterns shaped correctly + rank field', async () => {
- const r = await req('GET', '/api/marketplace/search?q=floral');
+t('GET /api/marketplace/search?q=tile returns patterns shaped correctly + rank field', async () => {
+ const r = await req('GET', '/api/marketplace/search?q=tile');
assert.strictEqual(r.status, 200);
assert.ok(Array.isArray(r.json.patterns));
- assert.strictEqual(r.json.query.q, 'floral');
+ assert.strictEqual(r.json.query.q, 'tile');
if (r.json.patterns.length) {
const p = r.json.patterns[0];
assert.ok('id' in p && 'title' in p && 'slug' in p, 'pattern has id/title/slug');
@@ -54,21 +54,24 @@ t('GET /api/marketplace/search?q=<gibberish> returns 0 patterns', async () => {
assert.strictEqual(r.json.total_returned, 0);
});
-t('GET /api/marketplace/search?style=floral filters tag arrays', async () => {
- const r = await req('GET', '/api/marketplace/search?style=floral');
+t('GET /api/marketplace/search?style=botanical filters tag arrays (case-insensitive)', async () => {
+ const r = await req('GET', '/api/marketplace/search?style=botanical');
assert.strictEqual(r.status, 200);
- assert.deepStrictEqual(r.json.query.style, ['floral']);
- // Every returned pattern must have 'floral' in style_tags (case-sensitive on the array)
+ assert.deepStrictEqual(r.json.query.style, ['botanical']);
+ // Seed catalog has 'Botanical' patterns; case-insensitive match must find at least 1.
+ assert.ok(r.json.patterns.length > 0, 'expected ≥1 pattern with style "botanical" (case-insensitive)');
for (const p of r.json.patterns) {
assert.ok(Array.isArray(p.style_tags), 'style_tags array');
- assert.ok(p.style_tags.includes('floral'), `pattern ${p.slug} should have 'floral' in style_tags but had ${JSON.stringify(p.style_tags)}`);
+ const lowered = p.style_tags.map(t => String(t).toLowerCase());
+ assert.ok(lowered.includes('botanical'),
+ `pattern ${p.slug} should have 'botanical' in style_tags but had ${JSON.stringify(p.style_tags)}`);
}
});
t('GET /api/marketplace/search?style=a,b parses comma list', async () => {
- const r = await req('GET', '/api/marketplace/search?style=floral,modern');
+ const r = await req('GET', '/api/marketplace/search?style=botanical,geometric');
assert.strictEqual(r.status, 200);
- assert.deepStrictEqual(r.json.query.style.sort(), ['floral','modern']);
+ assert.deepStrictEqual(r.json.query.style.sort(), ['botanical','geometric']);
});
t('GET /api/marketplace/search?commercial=true filters bool', async () => {
@@ -95,12 +98,12 @@ t('GET /api/marketplace/search ignores invalid commercial value', async () => {
assert.strictEqual(r.json.query.commercial, null);
});
-t('GET /api/marketplace/search?q=floral&style=floral combines filters (AND semantics)', async () => {
- const r = await req('GET', '/api/marketplace/search?q=floral&style=floral');
+t('GET /api/marketplace/search?q=tile&style=geometric combines filters (AND semantics)', async () => {
+ const r = await req('GET', '/api/marketplace/search?q=tile&style=geometric');
assert.strictEqual(r.status, 200);
- // All returned patterns must satisfy both filters; tsv match implies floral keyword
for (const p of r.json.patterns) {
- assert.ok(p.style_tags.includes('floral'));
+ const lowered = p.style_tags.map(t => String(t).toLowerCase());
+ assert.ok(lowered.includes('geometric'), `pattern ${p.slug} should match style filter`);
}
});
← b74ae4b marketplace: designer follow + /trade/projects board listing
·
back to Wallco Ai
·
marketplace: designer follow + /trade/projects board listing 67e4cc8 →