SPB Git forge
38commits 1branches 0releases
338.7 MBsize
maindefault branch
6 h agolast push
HTML 53.9% TypeScript 44.5% JavaScript 0.6% SQL 0.5%
7.1 KB · 135 lines typescript
Raw Blame History
1import type { Parser, ExtractedRecord, RawDocument } from "@dci/connectors";2import { countryFromText, cleanText, slugify } from "@dci/core";3import { loadHtml, tableRows, resolvePlace, regionRecord, type Place } from "./shared.js";4import { countryFromStateName, stateDisplayName, lookupCity } from "./city-coords.js";56/**7 * Google Cloud regions — https://cloud.google.com/compute/docs/regions-zones (static docs table):8 *   Zones | Location | Machine types | CPUs | … ; zone "europe-west1-b", location "St. Ghislain, Belgium, Europe".9 * Regions are derived by grouping zones on their prefix; the zone count is the availability-zone count.10 * (The marketing page /about/locations is a JS shell fed by data.json which carries no geography → not used.)11 */12const CONTINENTS = new Set(["europe", "apac", "north america", "south america", "middle east", "africa", "asia"]);1314/** "Montréal, Québec, North America" → { city, region, iso } */15function gcpLocation(loc: string): { city: string; region: string | null; iso: string | null } {16  const parts = loc.split(",").map((s) => cleanText(s) ?? "").filter((s) => s && !CONTINENTS.has(s.toLowerCase()));17  const city = parts[0] ?? loc;18  let region: string | null = null;19  let iso: string | null = null;20  for (const p of parts.slice(1)) {21    if (countryFromStateName(p)) { region = stateDisplayName(p); iso = iso ?? countryFromStateName(p); continue; }22    if (/^england|scotland|wales$/i.test(p)) { region = p; iso = "GB"; continue; }23    iso = iso ?? countryFromText(p);24    if (!countryFromText(p)) region = region ?? p;25  }26  if (!iso) iso = lookupCity(city)?.iso ?? countryFromText(city);27  return { city, region, iso };28}2930export const gcpRegionsParser: Parser = {31  name: "cloud_gcp_regions_v1",32  version: "1.0.0",33  pageTypes: ["cloud_region"],34  parse(doc: RawDocument, ctx): ExtractedRecord[] {35    const $ = loadHtml(doc);36    const regions = new Map<string, { zones: Set<string>; location: string }>();37    for (const row of tableRows($, $("table"))) {38      const zone = row[0]?.trim();39      const location = row[1];40      if (!zone || !location || !/^[a-z]+-[a-z]+\d+-[a-z]$/.test(zone)) continue;41      const code = zone.replace(/-[a-z]$/, "");42      const r = regions.get(code) ?? { zones: new Set<string>(), location };43      r.zones.add(zone);44      regions.set(code, r);45    }46    const out: ExtractedRecord[] = [];47    for (const [code, r] of regions) {48      const loc = gcpLocation(r.location);49      let place: Place = resolvePlace(loc.city, loc.iso, loc.region);50      if (!place.countryIso2) place.countryIso2 = loc.iso;51      if (!place.city) place.city = loc.city;52      if (!place.regionName && loc.region) place.regionName = loc.region;53      out.push(54        regionRecord({55          connectorId: ctx.connectorId,56          keyPrefix: "gcp:region",57          providerName: "Google Cloud",58          code,59          name: `${loc.city} (${code})`,60          place,61          availabilityZones: r.zones.size,62          status: "operational",63          externalIds: { gcp_region: code, gcp_zones: [...r.zones].sort().join(","), published_location: r.location },64          url: doc.finalUrl,65          methods: { code: "html:table.Zones(prefix)", availabilityZones: "count:zones", city: "html:table.Location+curated-city-table" },66        }),67      );68    }69    ctx.log("info", `gcp: ${out.length} regions from ${[...regions.values()].reduce((n, r) => n + r.zones.size, 0)} zones`);70    return out;71  },72};7374/**75 * Google's own data center sites — https://www.google.com/about/datacenters/locations/76 * The list is embedded server-side in <div id="data-centers-data"> as a comma-joined list of JSON objects:77 *   { location: "Council Bluffs, Iowa", latitude, longitude (often null), inDevelopment, continent, detailPage }78 * → facilities operated by Google (hyperscale). `inDevelopment: true` is Google's own label; we map it to79 *   "announced" (pipeline) because the page does not say whether construction has started.80 */81interface GoogleSite { location?: string; latitude?: number | null; longitude?: number | null; inDevelopment?: boolean; continent?: string; detailPage?: string }8283export const googleDatacentersParser: Parser = {84  name: "cloud_google_datacenters_v1",85  version: "1.0.0",86  parse(doc: RawDocument, ctx): ExtractedRecord[] {87    const $ = loadHtml(doc);88    const raw = $("#data-centers-data").text().trim();89    if (!raw) { ctx.log("warn", "google datacenters: #data-centers-data not found"); return []; }90    let sites: GoogleSite[] = [];91    try { sites = JSON.parse(raw.startsWith("[") ? raw : `[${raw}]`) as GoogleSite[]; } catch (e) { ctx.log("warn", `google datacenters: bad JSON ${(e as Error).message}`); return []; }92    const out: ExtractedRecord[] = [];93    for (const s of sites) {94      if (!s.location) continue;95      const [cityPart, ...rest] = s.location.split(",").map((x) => x.trim());96      const tail = rest.join(", ");97      const isoFromTail = countryFromStateName(tail) ?? countryFromText(tail) ?? null;98      let place: Place = resolvePlace(cityPart!, isoFromTail, countryFromStateName(tail) ? tail : null);99      if (!place.city) place.city = cityPart!;100      if (!place.countryIso2) place.countryIso2 = isoFromTail ?? lookupCity(cityPart!)?.iso ?? countryFromText(s.location);101      if (!place.regionName && tail && countryFromStateName(tail)) place.regionName = stateDisplayName(tail);102      // Google's own coordinates (when present) are used only when the curated table has none; they are site-area markers → "approximate"103      if (place.lat == null && typeof s.latitude === "number" && typeof s.longitude === "number") place = { ...place, lat: s.latitude, lng: s.longitude, geoPrecision: "approximate" };104      const detail = s.detailPage ? new URL(s.detailPage.replace(/^\.\/(\.\.\/)*/, "/about/datacenters/"), "https://www.google.com").toString() : null;105      const slug = slugify(s.location);106      out.push({107        kind: "facility",108        key: `google:dc:${slug}`,109        url: doc.finalUrl,110        pageType: "facility_index",111        certainty: 0.9,112        methods: { name: "embedded-json:location", status: "embedded-json:inDevelopment", lat: place.geoPrecision === "approximate" ? "embedded-json:latitude" : "curated-city-table" },113        data: {114          name: `Google ${s.location} data center`,115          operatorName: "Google",116          city: place.city,117          regionName: place.regionName,118          countryIso2: place.countryIso2,119          lat: place.lat,120          lng: place.lng,121          geoPrecision: place.geoPrecision ?? "city",122          status: s.inDevelopment ? "announced" : "operational",123          facilityType: "hyperscale",124          isHyperscale: true,125          website: detail ?? doc.finalUrl,126          description: s.inDevelopment ? `Listed by Google as a data center location "in development" (${s.continent ?? ""}).` : `Listed by Google as an operating data center location (${s.continent ?? ""}).`,127          externalIds: { google_location: s.location, ...(detail ? { google_detail_page: detail } : {}) },128        },129      });130    }131    ctx.log("info", `google datacenters: ${out.length} sites (${out.filter((r) => r.data.status === "announced").length} in development)`);132    return out;133  },134};135