- 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
52 lines
1.8 KiB
TypeScript
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()))
|
|
}
|
|
}
|