SPB Git forge

spb/websensor

Public
33commits 1branches 0releases
3.4 MBsize
maindefault branch
10 days agolast push
TypeScript 55.4% Python 43.2% SQL 1.2%
3.7 KB · 47 lines typescript
Raw Blame History
1import YAML from "yaml";2import { db, sql } from "@websensor/db";34/**5 * Export factory-created sources and accepted sensors as a registry fragment (reviewable YAML). Graduating a6 * fragment into `config/sources.d/` makes the registry sync own those sensors (`seed: true`) — the Factory never7 * re-creates them because their URLs already exist.8 */9export async function exportFactoryFragment(opts: { sector?: string; sinceDays?: number; includeShadow?: boolean } = {}): Promise<string> {10  const rows = await db.execute<Record<string, unknown>>(sql`11    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,12      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 sensors13    from sources so14    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})15    where (so.origin = 'factory' or exists (select 1 from sensors y where y.source_id = so.id and (y.config->>'factory') = 'true'))16      ${opts.sector ? sql`and so.sector = ${opts.sector}` : sql``}17      ${opts.sinceDays ? sql`and so.updated_at >= now() - make_interval(days => ${opts.sinceDays})` : sql``}18    group by so.id order by so.sector, so.id`);19  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;20  const aliasBySource = new Map<string, string[]>();21  for (const e of entities) aliasBySource.set(e.source_id, [...(aliasBySource.get(e.source_id) ?? []), e.alias]);22  const out: Record<string, unknown>[] = [];23  for (const r of rows.rows) {24    const sensors = (r.sensors as Record<string, unknown>[]).map((s) => {25      const cfg = s.config as Record<string, unknown>;26      const o: Record<string, unknown> = { name: s.name, url: s.url, type: s.type, connector: s.connector, tier: s.tier };27      if (cfg && Object.keys(cfg).length) o.config = cfg;28      return o;29    });30    if (!sensors.length) continue;31    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());32    if (r.origin === "factory") {33      const src: Record<string, unknown> = { id: r.id, name: r.name, domain: r.domain, categories: r.categories, tier: r.tier };34      if (Number(r.importance_weight) !== 1) src.weight = Number(r.importance_weight);35      if (aliases.length) src.aliases = aliases;36      if (r.country) src.country = r.country;37      if (r.language) src.language = r.language;38      if (r.first_party === false) src.first_party = false;39      if (r.notes) src.notes = r.notes;40      src.sensors = sensors;41      out.push(src);42    } else out.push({ id: r.id, extend: true, sensors });43  }44  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`;45  return header + YAML.stringify({ sources: out }, { lineWidth: 0, defaultStringType: "PLAIN", defaultKeyType: "PLAIN" });46}47