← back to Ventura Claw

server/connectors/lacity.js

56 lines

// LA City Open Data (Socrata). Read-only, no auth required for ≤1000 req/hr.
// Catalog: https://data.lacity.org/browse
// Useful datasets:
//   - Active Business Tax Registration: 6rrh-rzua
//   - Crime Data 2020-Present:           2nrs-mtv8
//   - Building & Safety Permits:         pi9x-tg5x
//   - Code Violations:                   855j-wf32
const BASE = "https://data.lacity.org/resource";
async function call(dataset, qs) {
  const url = `${BASE}/${dataset}.json` + (qs ? `?${qs}` : "");
  const r = await fetch(url, {
    headers: { Accept: "application/json", "User-Agent": "VenturaClaw/1.0", "X-App-Token": "" },
    signal: AbortSignal.timeout(20_000),
  });
  if (!r.ok) throw new Error(`lacity ${r.status}`);
  return r.json();
}
const KNOWN = {
  business_tax_registry: "6rrh-rzua",
  building_permits: "pi9x-tg5x",
  crime_data: "2nrs-mtv8",
  code_violations: "855j-wf32",
};
module.exports = {
  meta: { id: "lacity", name: "LA City Open Data", category: "data", docsUrl: "https://data.lacity.org/browse", auth: "none", realImpl: true },
  fields: [],
  configured() { return true; },
  async health() {
    try {
      const d = await call(KNOWN.business_tax_registry, "$limit=1");
      return { ok: true, label: `live · ${Object.keys(KNOWN).length} curated datasets (Socrata SoQL)` };
    } catch (e) { return { ok: false, reason: e.message }; }
  },
  actions: {
    async "datasets"() { return { datasets: KNOWN }; },
    async "query"({ dataset, where, select, order, limit, q }) {
      const id = KNOWN[dataset] || dataset;
      if (!id || !/^[a-z0-9-]+$/i.test(id)) throw new Error("dataset id required");
      const qs = new URLSearchParams();
      if (where) qs.set("$where", where);
      if (select) qs.set("$select", select);
      if (order) qs.set("$order", order);
      if (q) qs.set("$q", q);
      qs.set("$limit", String(Math.min(limit || 50, 1000)));
      return { rows: await call(id, qs.toString()) };
    },
    async "business_search"({ name, address, limit }) {
      const where = [];
      if (name) where.push(`upper(business_name) like upper('%${String(name).replace(/'/g, "''")}%')`);
      if (address) where.push(`upper(street_address) like upper('%${String(address).replace(/'/g, "''")}%')`);
      const qs = new URLSearchParams({ $where: where.join(" AND ") || "1=1", $limit: String(Math.min(limit || 25, 200)) });
      return { rows: await call(KNOWN.business_tax_registry, qs.toString()) };
    },
  },
};