import YAML from "yaml"; import { db, sql } from "@websensor/db"; /** * Export factory-created sources and accepted sensors as a registry fragment (reviewable YAML). Graduating a * fragment into `config/sources.d/` makes the registry sync own those sensors (`seed: true`) — the Factory never * re-creates them because their URLs already exist. */ export async function exportFactoryFragment(opts: { sector?: string; sinceDays?: number; includeShadow?: boolean } = {}): Promise { const rows = await db.execute>(sql` select so.id, so.name, so.domain, so.homepage, so.categories, so.tier, so.importance_weight, so.country, so.language, so.first_party, so.origin, so.sector, so.notes, coalesce(json_agg(json_build_object('name', s.name, 'url', s.url, 'type', s.type, 'connector', s.connector, 'tier', s.tier, 'config', s.config - 'factory' - 'shadow' - 'seed' - 'candidate' - 'seedId' - 'shadowSince' - 'targetPriority' - 'acceptedAt' - 'shadowReport' - 'kind') order by s.name) filter (where s.id is not null), '[]'::json) as sensors from sources so left join sensors s on s.source_id = so.id and s.enabled and (s.config->>'factory') = 'true' and (s.status <> 'SHADOW' or ${opts.includeShadow ?? false}) where (so.origin = 'factory' or exists (select 1 from sensors y where y.source_id = so.id and (y.config->>'factory') = 'true')) ${opts.sector ? sql`and so.sector = ${opts.sector}` : sql``} ${opts.sinceDays ? sql`and so.updated_at >= now() - make_interval(days => ${opts.sinceDays})` : sql``} group by so.id order by so.sector, so.id`); const entities = (await db.execute<{ source_id: string; alias: string }>(sql`select se.source_id, ea.alias from source_entities se join entity_aliases ea on ea.entity_id = se.entity_id where se.entity_id like 'org_%'`)).rows; const aliasBySource = new Map(); for (const e of entities) aliasBySource.set(e.source_id, [...(aliasBySource.get(e.source_id) ?? []), e.alias]); const out: Record[] = []; for (const r of rows.rows) { const sensors = (r.sensors as Record[]).map((s) => { const cfg = s.config as Record; const o: Record = { name: s.name, url: s.url, type: s.type, connector: s.connector, tier: s.tier }; if (cfg && Object.keys(cfg).length) o.config = cfg; return o; }); if (!sensors.length) continue; const aliases = (aliasBySource.get(String(r.id)) ?? []).filter((a) => a !== String(r.name).toLowerCase() && a !== String(r.domain).toLowerCase() && a !== String(r.domain).replace(/^www\./, "").toLowerCase()); if (r.origin === "factory") { const src: Record = { id: r.id, name: r.name, domain: r.domain, categories: r.categories, tier: r.tier }; if (Number(r.importance_weight) !== 1) src.weight = Number(r.importance_weight); if (aliases.length) src.aliases = aliases; if (r.country) src.country = r.country; if (r.language) src.language = r.language; if (r.first_party === false) src.first_party = false; if (r.notes) src.notes = r.notes; src.sensors = sensors; out.push(src); } else out.push({ id: r.id, extend: true, sensors }); } const header = `# WebSensor — Source Factory export (${new Date().toISOString().slice(0, 10)}${opts.sector ? `, sector ${opts.sector}` : ""}): ${out.length} organizations, ${out.reduce((n, s) => n + (s.sensors as unknown[]).length, 0)} accepted sensors.\n# Generated by \`cli.ts factory export\`; review, then place in config/sources.d/ to let the registry own these sensors.\n`; return header + YAML.stringify({ sources: out }, { lineWidth: 0, defaultStringType: "PLAIN", defaultKeyType: "PLAIN" }); }