← back to Lifestyle Asset Intel
yolo tick #31: /api/audit/recent ?route= substring filter (case-insensitive, composes with asset_slug)
01bd2e3179b515b008ac3a2fdaf827c4cf4c1ea6 · 2026-05-14 08:16:22 -0700 · Steve Abrams
Files touched
M lib/audit.jsM routes/api.jsM tests/audit.test.js
Diff
commit 01bd2e3179b515b008ac3a2fdaf827c4cf4c1ea6
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Thu May 14 08:16:22 2026 -0700
yolo tick #31: /api/audit/recent ?route= substring filter (case-insensitive, composes with asset_slug)
---
lib/audit.js | 28 ++++++++++++++--------------
routes/api.js | 10 ++++++++--
tests/audit.test.js | 31 +++++++++++++++++++++++++++++++
3 files changed, 53 insertions(+), 16 deletions(-)
diff --git a/lib/audit.js b/lib/audit.js
index b4d28f9..bf4a09f 100644
--- a/lib/audit.js
+++ b/lib/audit.js
@@ -89,29 +89,29 @@ function auditMiddleware(req, res, next) {
next();
}
-async function recentCalls({ limit = 100, asset_slug = null } = {}) {
+async function recentCalls({ limit = 100, asset_slug = null, route = null } = {}) {
const lim = Math.max(1, Math.min(1000, parseInt(limit, 10) || 100));
+ const where = [];
+ const params = [];
if (asset_slug) {
- const r = await pool.query(
- `SELECT id, called_at, caller_ip::text AS caller_ip,
- route, method, asset_slug, response_status,
- methodology_version, latency_ms, user_agent, request_id
- FROM valuation_calls
- WHERE asset_slug = $1
- ORDER BY called_at DESC
- LIMIT $2`,
- [asset_slug, lim]
- );
- return r.rows;
+ params.push(asset_slug);
+ where.push(`asset_slug = $${params.length}`);
}
+ if (route && typeof route === 'string' && route.trim()) {
+ params.push(`%${route.trim().toLowerCase()}%`);
+ where.push(`LOWER(route) LIKE $${params.length}`);
+ }
+ params.push(lim);
+ const whereSql = where.length ? `WHERE ${where.join(' AND ')}` : '';
const r = await pool.query(
`SELECT id, called_at, caller_ip::text AS caller_ip,
route, method, asset_slug, response_status,
methodology_version, latency_ms, user_agent, request_id
FROM valuation_calls
+ ${whereSql}
ORDER BY called_at DESC
- LIMIT $1`,
- [lim]
+ LIMIT $${params.length}`,
+ params
);
return r.rows;
}
diff --git a/routes/api.js b/routes/api.js
index 27721b9..6728b89 100644
--- a/routes/api.js
+++ b/routes/api.js
@@ -241,8 +241,14 @@ router.get('/audit/recent', setCache(NO_STORE), async (req, res, next) => {
try {
const limit = req.query.limit;
const asset_slug = req.query.asset_slug || null;
- const rows = await recentCalls({ limit, asset_slug });
- res.json({ count: rows.length, calls: rows });
+ const route = typeof req.query.route === 'string' ? req.query.route.trim() : '';
+ const rows = await recentCalls({ limit, asset_slug, route: route || null });
+ res.json({
+ count: rows.length,
+ asset_slug: asset_slug || null,
+ route: route || null,
+ calls: rows
+ });
} catch (e) { next(e); }
});
diff --git a/tests/audit.test.js b/tests/audit.test.js
index 39e30b7..bc56b89 100644
--- a/tests/audit.test.js
+++ b/tests/audit.test.js
@@ -132,6 +132,37 @@ test('GET /api/audit/recent?asset_slug filters', async () => {
}
});
+test('GET /api/audit/recent?route filters by route substring (case-insensitive) and composes with asset_slug', async () => {
+ // Prime the table with at least one /api/valuation row + one non-matching row.
+ await getJson('/api/valuation/birkin-30-togo-gold-ghw-us');
+ await getJson('/api/assets');
+ await new Promise((res) => setTimeout(res, 250));
+
+ // Substring match — uppercase input proves case-insensitivity.
+ const r = await getJson('/api/audit/recent?route=VALUATION&limit=50');
+ assert.equal(r.status, 200);
+ assert.equal(r.json.route, 'VALUATION');
+ assert.ok(r.json.calls.length >= 1, 'at least one /api/valuation row');
+ for (const c of r.json.calls) {
+ assert.match(c.route.toLowerCase(), /valuation/);
+ }
+
+ // Composes cleanly with asset_slug.
+ const r2 = await getJson(
+ '/api/audit/recent?route=valuation&asset_slug=birkin-30-togo-gold-ghw-us'
+ );
+ assert.equal(r2.status, 200);
+ for (const c of r2.json.calls) {
+ assert.match(c.route, /\/api\/valuation/);
+ assert.equal(c.asset_slug, 'birkin-30-togo-gold-ghw-us');
+ }
+
+ // Empty string route should NOT filter (behaves like no param).
+ const r3 = await getJson('/api/audit/recent?route=&limit=5');
+ assert.equal(r3.status, 200);
+ assert.equal(r3.json.route, null);
+});
+
test('valuation_calls has no UPDATE or DELETE called by app code', async () => {
// Sanity: app should never mutate the table. A grep would verify
// this at code-review time, but the fastest check at runtime is that
← 456f827 yolo tick #30: /api/sources ?q= filter (slug/name/kind/notes
·
back to Lifestyle Asset Intel
·
yolo tick #32: /api/audit/recent ?method= filter (HTTP verb, 369b470 →