Files
pc_builder/app/controllers/scraped_parts_controller.ts
T
Kevin L'Hereec 497ae797ca
Build & Deploy / build (push) Failing after 5s
Build & Deploy / docker (push) Skipped
feat: add 24h cache freshness with auto-refresh on search
- part_scraper_service.ts: new CACHE_TTL_HOURS (24), isCategoryStale()
  checks the most recent scrapedAt for a given ComponentType's mapped
  category, ensureFreshCategory() re-scrapes it synchronously if missing
  or stale (errors are logged, not thrown, so search still falls back to
  whatever is already cached)
- ScrapedPartsController#index: when a typeId filter is provided, the
  corresponding category cache is refreshed eagerly if stale before
  querying results. Free-text searches without a typeId skip this to
  avoid re-scraping every mapped category on every keystroke -
  'node ace scrape:parts' still refreshes everything at once on demand
- 4 new tests covering staleness detection (missing cache, <24h, >24h)
  and that unmapped types never trigger a scrape/network call
2026-07-23 13:58:37 +02:00

52 lines
1.8 KiB
TypeScript

import type { HttpContext } from '@adonisjs/core/http'
import vine from '@vinejs/vine'
import ScrapedPart from '#models/scraped_part'
import ComponentType from '#models/component_type'
import { ensureFreshCategory } from '#services/part_scraper_service'
const searchValidator = vine.compile(
vine.object({
q: vine.string().trim().minLength(1).optional(),
typeId: vine.number().min(1).optional(),
})
)
export default class ScrapedPartsController {
/**
* Search the new-parts cache (scraped from pc-builder.io) by name and/or
* component type. Read-only, authenticated (the search UI lives inside
* the authenticated "create component" flow).
*
* When a typeId is given, the corresponding category cache is refreshed
* synchronously first if it's missing or older than 24h (see
* part_scraper_service.CACHE_TTL_HOURS), so results stay reasonably
* fresh without needing an external cron job. Free-text searches across
* all types skip this eager refresh to avoid re-scraping every mapped
* category on every keystroke; run `node ace scrape:parts` to refresh
* everything at once.
*/
async index({ request, auth, response }: HttpContext) {
await auth.authenticate()
const { q, typeId } = await request.validateUsing(searchValidator)
if (typeId) {
const componentType = await ComponentType.find(typeId)
if (componentType) {
await ensureFreshCategory(componentType)
}
}
const query = ScrapedPart.query().preload('type').orderBy('name')
if (q) {
query.whereRaw('LOWER(name) LIKE ?', [`%${q.toLowerCase()}%`])
}
if (typeId) {
query.where('typeId', typeId)
}
const parts = await query.limit(30)
return response.json(parts.map((part) => part.serialize()))
}
}