staticql
Version:
Type-safe query engine for static content including Markdown, YAML, JSON, and more.
169 lines (168 loc) • 5.89 kB
JavaScript
import { SourceConfigResolver as Resolver } from "../SourceConfigResolver.js";
import { parsePrefixDict } from "../utils/normalize.js";
import { joinPath, toI, toP } from "../utils/path.js";
/**
* FetchRepository: A browser-compatible StorageRepository implementation.
*
* This implementation uses `fetch()` to load files under a public directory.
*
* ⚠ Write, delete, and full file listing are not supported in browser environments.
*/
export class FetchRepository {
constructor(baseUrl = "/") {
this.baseUrl = baseUrl.replace(/\/+$/, "") + "/";
}
setResolver(resolver) {
this.resolver = resolver;
}
/**
* Reads a file from the public directory using fetch.
*
* @param path - Relative path from base URL.
* @returns The file contents as text.
* @throws If fetch fails or response is not OK.
*/
async readFile(path) {
const url = this.baseUrl + path.replace(/^\/+/, "");
const res = await fetch(url);
if (!res.ok)
throw new Error(`Failed to fetch: ${url}`);
return await res.text();
}
/**
* Checks if a file exists by sending a HEAD request.
*
* @param path - Relative path from base URL.
* @returns True if the file is accessible; false otherwise.
*/
async exists(path) {
const url = this.baseUrl + path.replace(/^\/+/, "");
const res = await fetch(url, { method: "HEAD" });
return res.ok;
}
/**
* Retrieves a list of file paths matching a pattern.
*
* This works by:
* - Inferring the source name from the pattern.
* - Using the resolved source config to locate the slug index file.
* - Converting slugs to full paths using the resolver.
*
* @param pattern - A glob-style pattern like "herbs/*.md" or "states.yaml".
* @returns List of matching file paths (relative to base).
*/
async listFiles(pattern) {
const allRSCs = this.resolver?.resolveAll() ?? [];
const rsc = allRSCs.find((r) => pattern.startsWith(r.pattern));
if (!rsc)
return [];
const indexDir = `index/${rsc.name}.slug`;
const prefixIndexLines = await this.readAllIndexesRemote(indexDir);
const slugs = prefixIndexLines.map((line) => line.v).filter(Boolean);
let paths;
if (pattern.includes("*")) {
paths = Resolver.getSourcePathsBySlugs(pattern, slugs);
}
else {
paths = slugs.map((slug) => rsc.pattern.replace("*", slug));
}
return paths;
}
/**
* Not supported in browser.
*/
async writeFile(path, data) {
throw new Error("writeFile is not supported in browser environment");
}
/**
* Not supported in browser.
*/
async removeFile(path) {
throw new Error("removeFile is not supported in browser environment");
}
/**
* Not supported in browser.
*/
async removeDir(path) {
throw new Error("removeFile is not supported in browser environment");
}
/**
* Internal helper to fetch a JSON index file (typically a list of slugs).
*
* @param indexPath - Relative or absolute path to index file.
* @returns Parsed slug list or empty array on failure.
*/
async fetchIndexFile(indexPath) {
const url = indexPath.startsWith("/")
? this.baseUrl + indexPath.replace(/^\/+/, "")
: this.baseUrl + indexPath;
const res = await fetch(url);
if (!res.ok)
return [];
return await res.json();
}
/**
* Opens a file as a ReadableStream.
*
* @param path - Relative path to the file (from the repository base directory)
* @returns Promise that resolves to a ReadableStream for the file contents
*/
async openFileStream(path) {
const res = await fetch(`${this.baseUrl}${path}`);
if (!res.ok)
throw new Error(`Failed to fetch ${path}`);
return res.body;
}
/**
* Remote (fetch-based) version of recursive prefix index traversal.
*/
async readAllIndexesRemote(dir) {
const results = [];
try {
const indexUrl = toI(this.baseUrl, dir);
const indexRes = await fetch(indexUrl);
if (indexRes.ok) {
const indexData = await indexRes.text();
const flattened = this.flatPrefixIndexLine(indexData
.split("\n")
.map((line) => line.trim())
.filter(Boolean)
.map((line) => JSON.parse(line)));
results.push(...flattened);
}
}
catch { }
try {
const prefixesUrl = toP(this.baseUrl, dir);
const prefixesRes = await fetch(prefixesUrl);
if (prefixesRes.ok) {
const prefixesData = await prefixesRes.text();
const prefixes = parsePrefixDict(prefixesData);
for (const prefix of prefixes) {
const subdir = joinPath(dir, prefix);
const subResults = await this.readAllIndexesRemote(subdir);
results.push(...subResults);
}
}
}
catch { }
return results;
}
flatPrefixIndexLine(unflattened) {
const seen = new Set();
const flattened = [];
for (const item of unflattened) {
for (const [key, value] of Object.entries(item.ref)) {
if (!seen.has(key)) {
seen.add(key);
flattened.push({
v: item.v,
vs: item.vs,
ref: { [key]: value },
});
}
}
}
return flattened;
}
}