UNPKG

staticql

Version:

Type-safe query engine for static content including Markdown, YAML, JSON, and more.

145 lines (144 loc) 5.61 kB
import { parseByType } from "./parser/index.js"; import { SourceConfigResolver as Resolver, } from "./SourceConfigResolver.js"; import { InMemoryCacheProvider } from "./cache/InMemoryCacheProvider.js"; /** * Responsible for loading and validating content from static sources. */ export class SourceLoader { constructor(repository, resolver, validator) { this.repository = repository; this.resolver = resolver; this.validator = validator; this.cache = new InMemoryCacheProvider(); } /** * Loads all records for a given source name. * * @param sourceName - The name of the source defined in config. * @returns An array of validated records. */ async loadBySourceName(sourceName) { const rsc = this.resolver.resolveOne(sourceName); const filePaths = await this.repository.listFiles(rsc.pattern); const data = []; for (const filePath of filePaths) { data.push(await this.load(filePath, rsc)); } const flattened = Array.isArray(data) && Array.isArray(data[0]) ? data.flat() : data; return flattened; } /** * Loads and validates content from a specific file path. * * @param filePath - The path to the file. * @param rsc - The resolved source configuration. * @returns Parsed and validated content. */ async load(filePath, rsc) { let rawContent; try { rawContent = await this.repository.readFile(filePath); } catch { throw new Error(`Target Source [${filePath}] is not found.`); } const parsed = await parseByType(rsc.type, { rawContent }); let validated = []; if (Array.isArray(parsed)) { parsed.map((p) => this.validator.validate(p, rsc.schema, rsc.name)); validated = parsed.flat(); } else { parsed.slug = Resolver.getSlugFromPath(rsc.pattern, filePath); this.validator.validate(parsed, rsc.schema, rsc.name); validated = parsed; } return validated; } /** * Loads and validates a single record by source name and slug. * If the file contains an array of records, the one matching the slug is returned. * * @param sourceName - The name of the source defined in config. * @param slug - The unique slug identifier. * @returns The matching validated record. * @throws If the source or slug is not found or fails validation. */ async loadBySlug(sourceName, slug) { const rsc = this.resolver.resolveOne(sourceName); if (!rsc) throw new Error(`Unknown source: ${sourceName}`); let filePath; if (rsc.pattern.includes("*")) { filePath = Resolver.getSourcePathsBySlugs(rsc.pattern, [slug])[0]; } else { filePath = rsc.pattern; } try { const { parsed, raw } = await this.parseFile(filePath, rsc); if (Array.isArray(parsed)) { const found = parsed.find((item) => item && item.slug === slug); if (!found) throw new Error(`Slug '${slug}' not found in file: ${filePath}`); this.validator.validate(found, rsc.schema, rsc.name); found.raw = raw; return found; } else { this.validator.validate(parsed, rsc.schema, rsc.name); parsed.raw = raw; return parsed; } } catch (err) { throw new Error(`Failed to loadBySlug: ${filePath}${err}`); } } /** * Loads and validates multiple records by slugs for the given source. * * @param sourceName - The name of the source. * @param slugs - Array of slug identifiers. * @returns An array of matched and validated records. */ async loadBySlugs(sourceName, slugs) { const unique = [...new Set(slugs)]; return Promise.all(unique.map((slug) => this.loadBySlug(sourceName, slug))); } /** * Parses and validates a single file. * Ensures slug consistency if the file name pattern contains wildcards. * * @param filePath - Logical file path (may include pattern). * @param rsc - The resolved source configuration. * @param fullPath - Actual resolved file path. * @returns Parsed and validated record and raw data. * @throws If the slug is inconsistent or unsupported type. */ async parseFile(filePath, rsc) { if (await this.cache.has(filePath)) { const cached = await this.cache.get(filePath); if (cached) return cached; } let raw = await this.repository.readFile(filePath); let parsed = await parseByType(rsc.type, { rawContent: raw }); if (rsc.pattern.includes("*") && !Array.isArray(parsed) && typeof parsed === "object" && parsed !== null) { const slugFromPath = Resolver.getSlugFromPath(rsc.pattern, filePath); const parsedObj = parsed; if (!parsedObj.slug) { parsedObj.slug = slugFromPath; } else if (!slugFromPath.includes(String(parsedObj.slug))) { throw new Error(`Slug mismatch: expected "${slugFromPath}", got "${parsedObj.slug}" in ${filePath}`); } parsed = parsedObj; } await this.cache.set(filePath, { parsed, raw }); return { parsed, raw }; } }