← back to Ventura Claw

server/connectors/cadata.js

64 lines

// California Open Data (Socrata, hosted on data.ca.gov). No auth needed.
// Catalog: https://data.ca.gov/dataset
// Useful datasets:
//   - CA SOS Business Filings:     8ek2-eknp (SOS active business search)
//   - CalEnviroScreen 4.0:         <varies>
//   - State Contracts Inventory:   ng9z-2spp
//   - Active Civil Cases:          c9b6-2v9a
const BASE = "https://data.ca.gov/api/3/action/datastore_search";
async function call(qs) {
  const r = await fetch(`${BASE}?${qs}`, {
    headers: { Accept: "application/json", "User-Agent": "VenturaClaw/1.0" },
    signal: AbortSignal.timeout(20_000),
  });
  if (!r.ok) throw new Error(`cadata ${r.status}`);
  return r.json();
}
async function packageList() {
  const r = await fetch("https://data.ca.gov/api/3/action/package_list?limit=10", {
    headers: { Accept: "application/json", "User-Agent": "VenturaClaw/1.0" },
    signal: AbortSignal.timeout(15_000),
  });
  if (!r.ok) throw new Error(`cadata package_list ${r.status}`);
  return r.json();
}
module.exports = {
  meta: { id: "cadata", name: "California Open Data", category: "data", docsUrl: "https://data.ca.gov/dataset", auth: "none", realImpl: true },
  fields: [],
  configured() { return true; },
  async health() {
    try {
      const d = await packageList();
      return { ok: true, label: `live · CKAN datastore · ${(d.result || []).length}+ packages sampled` };
    } catch (e) { return { ok: false, reason: e.message }; }
  },
  actions: {
    async "search_packages"({ q, rows }) {
      if (!q) throw new Error("q required");
      const qs = new URLSearchParams({ q, rows: String(Math.min(rows || 20, 100)) });
      const r = await fetch(`https://data.ca.gov/api/3/action/package_search?${qs}`, {
        headers: { Accept: "application/json", "User-Agent": "VenturaClaw/1.0" },
        signal: AbortSignal.timeout(15_000),
      });
      if (!r.ok) throw new Error(`cadata search ${r.status}`);
      const d = await r.json();
      return {
        count: d.result?.count || 0,
        packages: (d.result?.results || []).map(p => ({
          id: p.id, name: p.name, title: p.title, organization: p.organization?.title,
          notes: p.notes?.slice(0, 240),
          resources: (p.resources || []).map(r => ({ id: r.id, name: r.name, format: r.format, url: r.url })).slice(0, 5),
        })),
      };
    },
    async "datastore_query"({ resource_id, q, limit, offset }) {
      if (!resource_id) throw new Error("resource_id required");
      const qs = new URLSearchParams({ resource_id, limit: String(Math.min(limit || 50, 500)) });
      if (offset) qs.set("offset", String(offset));
      if (q) qs.set("q", q);
      const d = await call(qs.toString());
      return { total: d.result?.total, records: d.result?.records || [] };
    },
  },
};