From cbbc48f4aa0956667ee232d4f016c36dd643514b Mon Sep 17 00:00:00 2001 From: Clank OpenClaw Date: Mon, 20 Jul 2026 13:28:24 -0400 Subject: [PATCH] Add Face Reality ingredient list filter --- README.md | 6 ++++++ examples/chekit-core-client.js | 6 +++++- scripts/smoke.js | 27 +++++++++++++++++++++++++++ src/db.js | 5 +++-- src/server.js | 24 ++++++++++++++++++++++-- 5 files changed, 63 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 94d9b99..7016fe0 100644 --- a/README.md +++ b/README.md @@ -175,6 +175,12 @@ Returns service health. Returns SQLite ingredient rows and their synonyms. +Use `faceReality=true` to return only ingredients flagged by Face Reality: + +```bash +curl -s "http://127.0.0.1:3333/ingredients?faceReality=true&limit=100" +``` + ### `POST /api/check` Checks an ingredient list. `/check` is also registered locally, but `/api/check` is the recommended hosted endpoint. diff --git a/examples/chekit-core-client.js b/examples/chekit-core-client.js index f2ae36b..d8b0349 100644 --- a/examples/chekit-core-client.js +++ b/examples/chekit-core-client.js @@ -37,12 +37,16 @@ export function createChekItCoreClient({ baseUrl = DEFAULT_BASE_URL, fetchImpl = return request('/health'); }, - listIngredients({ limit = 100, offset = 0 } = {}) { + listIngredients({ limit = 100, offset = 0, faceReality = false } = {}) { const params = new URLSearchParams({ limit: String(limit), offset: String(offset) }); + if (faceReality) { + params.set('faceReality', 'true'); + } + return request(`/ingredients?${params.toString()}`); }, diff --git a/scripts/smoke.js b/scripts/smoke.js index 64b27c3..977ed7d 100644 --- a/scripts/smoke.js +++ b/scripts/smoke.js @@ -18,6 +18,14 @@ upsertIngredients([ description: 'Seed row for local smoke testing.', comedogenic_score: 4, face_reality: true + }, + { + id: 2, + name: 'myristate', + causes_acne: true, + description: 'Non-Face Reality smoke test row.', + comedogenic_score: 4, + face_reality: false } ]); @@ -47,6 +55,25 @@ try { } } + const faceRealityResponse = await server.inject({ + method: 'GET', + url: '/ingredients?faceReality=true' + }); + + if (faceRealityResponse.statusCode !== 200) { + throw new Error(faceRealityResponse.body); + } + + const faceRealityBody = JSON.parse(faceRealityResponse.body); + if ( + faceRealityBody.onlyFaceReality !== true + || faceRealityBody.data.length !== 1 + || faceRealityBody.data[0].name !== 'coconut oil' + || faceRealityBody.data.some((ingredient) => ingredient.faceReality !== true) + ) { + throw new Error(JSON.stringify(faceRealityBody, null, 2)); + } + console.log('Smoke test passed.'); } finally { await server.close(); diff --git a/src/db.js b/src/db.js index 25d3c6e..6960dca 100644 --- a/src/db.js +++ b/src/db.js @@ -145,16 +145,17 @@ export function seedKnownSynonyms() { transaction(); } -export function listIngredients({ limit = 100, offset = 0 } = {}) { +export function listIngredients({ limit = 100, offset = 0, onlyFaceReality = false } = {}) { const database = initDb(); const rows = database .prepare(` SELECT i.* FROM ingredients i + WHERE (? = 0 OR i.face_reality = 1) ORDER BY i.name ASC LIMIT ? OFFSET ? `) - .all(limit, offset); + .all(onlyFaceReality ? 1 : 0, limit, offset); const synonymsForIngredient = database.prepare(` SELECT synonym FROM ingredient_synonyms WHERE ingredient_id = ? ORDER BY synonym ASC `); diff --git a/src/server.js b/src/server.js index a43cbe4..961704a 100644 --- a/src/server.js +++ b/src/server.js @@ -23,11 +23,15 @@ export function buildServer() { fastify.get('/ingredients', async (request) => { const limit = Math.min(Number(request.query.limit || 100), 500); const offset = Math.max(Number(request.query.offset || 0), 0); + const onlyFaceReality = parseBooleanQuery( + request.query.faceReality ?? request.query.onlyFaceReality + ); return { - data: listIngredients({ limit, offset }), + data: listIngredients({ limit, offset, onlyFaceReality }), limit, - offset + offset, + onlyFaceReality }; }); @@ -59,6 +63,22 @@ export function buildServer() { return fastify; } +function parseBooleanQuery(value) { + if (Array.isArray(value)) { + return parseBooleanQuery(value[0]); + } + + if (typeof value === 'boolean') { + return value; + } + + if (typeof value !== 'string') { + return false; + } + + return ['1', 'true', 'yes'].includes(value.toLowerCase()); +} + if (import.meta.url === `file://${process.argv[1]}`) { const server = buildServer(); await server.listen({