Foundation: core types, DB schema, connector SDK, API contract, infra docs
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
47 changed files +10,323 −0
added
.env.example
+19 −0
@@ -0,0 +1,19 @@ | ||
| 1 | +# DataCenterIndex — variables d'environnement (jamais de secrets dans git) | |
| 2 | +DATABASE_URL=postgres://dci:dci@127.0.0.1:5432/dci | |
| 3 | +REDIS_URL=redis://127.0.0.1:6379/0 | |
| 4 | +CLICKHOUSE_URL=http://127.0.0.1:8123 | |
| 5 | +CLICKHOUSE_DB=dci | |
| 6 | +S3_ENDPOINT=http://127.0.0.1:9000 | |
| 7 | +S3_BUCKET=dci-raw | |
| 8 | +S3_ACCESS_KEY=dci | |
| 9 | +S3_SECRET_KEY=change-me | |
| 10 | +SCRAPFLY_API_KEY= | |
| 11 | +FIRECRAWL_API_KEY= | |
| 12 | +DCI_USER_AGENT="DataCenterIndexBot/0.1 (+https://www.datacenterindex.io/bot; contact@spboucher.ai)" | |
| 13 | +DCI_ADMIN_TOKEN=change-me | |
| 14 | +API_PORT=8311 | |
| 15 | +WEB_PORT=8310 | |
| 16 | +NEXT_PUBLIC_API_URL=http://127.0.0.1:8311 | |
| 17 | +NEXT_PUBLIC_SITE_URL=https://www.datacenterindex.io | |
| 18 | +DCI_SCRAPFLY_DAILY_BUDGET=400 | |
| 19 | +DCI_FIRECRAWL_DAILY_BUDGET=200 | |
added
.gitignore
+17 −0
@@ -0,0 +1,17 @@ | ||
| 1 | +node_modules | |
| 2 | +dist | |
| 3 | +.next | |
| 4 | +*.log | |
| 5 | +.env | |
| 6 | +.env.* | |
| 7 | +!.env.example | |
| 8 | +data/ | |
| 9 | +deploy/*.secret* | |
| 10 | +deploy/.env* | |
| 11 | +!deploy/.env.example | |
| 12 | +.DS_Store | |
| 13 | +coverage | |
| 14 | +.turbo | |
| 15 | +tsconfig.tsbuildinfo | |
| 16 | +*.tsbuildinfo | |
| 17 | +.admin-token.local | |
added
CLAUDE.md
+43 −0
@@ -0,0 +1,43 @@ | ||
| 1 | +# DataCenterIndex.io — repo guide (read first) | |
| 2 | + | |
| 3 | +**Product**: the global index of data center infrastructure (facilities, campuses, operators, cloud regions, metros, countries, projects, IXPs, power, connectivity) with a live change feed. Feels like Our World in Data × a Bloomberg terminal for digital infrastructure × a global infrastructure map. **The product is the structured graph and its history, not the crawler.** | |
| 4 | + | |
| 5 | +**Pipeline**: discover → fetch → archive → extract → normalize → reconcile → validate → version → detect changes → publish. Never `scrape → dump → show`. | |
| 6 | + | |
| 7 | +## Non-negotiables | |
| 8 | +- **Never invent data.** No fake facilities, capacities, coordinates, dates. A figure that is an estimate is flagged `isEstimate`. City-level coordinates are `geo_precision = city`, never `exact`. | |
| 9 | +- **Provenance on every field** (`provenance` table: source, URL, first/last observed, retrieved, confidence, method). | |
| 10 | +- **Legal crawling**: robots.txt respected, no auth/paywall/CAPTCHA bypass, rate limits per host, conditional GET, content hashing — never refetch unchanged pages with premium credits. Attribution + license recorded per source (`sources.license/attribution`). | |
| 11 | +- **Escalation**: L1 direct HTTP → L2 direct + browser identity → L3 Firecrawl → L4 Scrapfly rendered. Escalate only when blocked / JS shell. Premium daily budgets (`DCI_SCRAPFLY_DAILY_BUDGET`, `DCI_FIRECRAWL_DAILY_BUDGET`). | |
| 12 | +- **Secrets** only in env / deploy secrets, never in git, never sent to the browser. | |
| 13 | +- **Mobile-first** UI (320–430 px first), then tablet/desktop. Industrial, premium, restrained: no neon, no fake terminal, no giant hero, no card soup. | |
| 14 | + | |
| 15 | +## Layout | |
| 16 | +``` | |
| 17 | +packages/core types (entities, provenance, events), ids, hashing, normalize (MW, dates, status, money), geo, confidence, diff, ssrf, api-types (API contract) | |
| 18 | +packages/db Drizzle schema (Postgres) + plain SQL migrations (migrations/*.sql, runner src/migrate.ts) + ClickHouse client/DDL | |
| 19 | +packages/connectors connector SDK: types, YAML config schema, fetchers (direct/firecrawl/scrapfly + escalation), robots, ratelimit, discovery (sitemap/rss/links), extract (JSON-LD, embedded JSON, selectors, PDF), classify, registry (parsers/implementations), GenericConnector | |
| 20 | +config/connectors/*.yaml one file per source (declarative; `parser:` or `implementation:` for code-backed logic) | |
| 21 | +apps/worker runtime: ConnectorContext, document registry + storage (MinIO), change detection, ingest (reconcile/persist/events), scheduler (BullMQ), rankings/metrics, CLI `pnpm dci …` | |
| 22 | +apps/api Fastify REST `/api/v1/*` (public) + `/api/admin/*` (token) — see packages/core/src/api-types.ts | |
| 23 | +apps/web Next 16 (App Router, Tailwind v4, MapLibre) — SSR for content, lazy map/charts | |
| 24 | +deploy/ Dockerfiles, compose (data node + crawl node), edge Caddy, backups, monitoring | |
| 25 | +docs/ ARCHITECTURE.md, CONNECTORS.md (authoring guide), DEPLOY.md | |
| 26 | +``` | |
| 27 | + | |
| 28 | +## Conventions | |
| 29 | +- pnpm workspaces, TypeScript strict, ESM, `verbatimModuleSyntax` (use `import type`). Run `pnpm typecheck` before finishing. | |
| 30 | +- Ids: prefixed (`fac_`, `op_`, `prj_`, `evt_`, `doc_`…) via `newId()` / `stableId()` in `@dci/core`. Slugs unique per table. | |
| 31 | +- Drizzle: `casing: "snake_case"`. `sql\`${arr}\`` spreads arrays → use `textArray()` or `sql.raw`. `db.execute` returns timestamps as strings. | |
| 32 | +- Partial dates are strings: `2027`, `2027-06`, `2027-06-15`, `2027-Q2`, `2027-H1` (`parsePartialDate`, `formatPartialDate`). | |
| 33 | +- Connector keys are connector-scoped and stable (`peeringdb:fac:12`), mapped in `entity_keys` for idempotent re-ingestion. | |
| 34 | +- Events are deduplicated by `fingerprint` (entity + type + new value + day). | |
| 35 | +- API envelope `{ data, meta, sources }`. Web reads `NEXT_PUBLIC_API_URL` server-side (`API_URL_INTERNAL` preferred when set). | |
| 36 | +- Ports: web 8310 (Next, loopback in prod), api 8311, edge 8300 (Caddy in compose, bound to the WireGuard IP). Public: https://www.datacenterindex.io via BHS64 gateway. | |
| 37 | +- Local dev: Postgres `postgres://dci:dci@127.0.0.1:5432/dci`, Redis 6379, ClickHouse 8123, MinIO 9000 (see `.env.example`, `deploy/dev-compose.yml`). | |
| 38 | + | |
| 39 | +## Connector authoring (short) | |
| 40 | +1. `config/connectors/<id>.yaml` (id, name, domain, kind, license/attribution, fetch levels, discovery, schedule, extractors). | |
| 41 | +2. Declarative fields when possible; otherwise a parser in `apps/worker/src/parsers/<id>.ts` registered with `registerParser`, or a full `implementation` (`registerImplementation`) for APIs/datasets. | |
| 42 | +3. Validate against LIVE pages: `pnpm dci run <id> --dry-run --limit 5` must yield valid entities (no errors in the ValidationReport). Nothing ships unverified. | |
| 43 | +4. Document the source (what, license, update cadence, quirks) in `docs/connectors/<id>.md`. | |
added
apps/worker/src/ingest/contract.ts
+39 −0
@@ -0,0 +1,39 @@ | ||
| 1 | +/** | |
| 2 | + * Boundary between the crawl runtime (context, documents, scheduling) and the ingest layer | |
| 3 | + * (reconciliation, persistence, provenance, events). Both sides implement against this file. | |
| 4 | + */ | |
| 5 | +import type { NormalizedEntity, DetectedChange } from "@dci/core"; | |
| 6 | + | |
| 7 | +export interface IngestRun { | |
| 8 | + connectorId: string; | |
| 9 | + sourceId: string; | |
| 10 | + runId: string; | |
| 11 | + sourceKind: string; | |
| 12 | + sourcePriority: number; | |
| 13 | + dryRun: boolean; | |
| 14 | +} | |
| 15 | + | |
| 16 | +export interface IngestDocRef { | |
| 17 | + documentId: string; | |
| 18 | + url: string; | |
| 19 | + pageType: string; | |
| 20 | + fetchedAt: string; | |
| 21 | +} | |
| 22 | + | |
| 23 | +export interface IngestStats { | |
| 24 | + received: number; | |
| 25 | + created: number; | |
| 26 | + updated: number; | |
| 27 | + unchanged: number; | |
| 28 | + merged: number; | |
| 29 | + pendingMatches: number; | |
| 30 | + rejected: number; | |
| 31 | + events: number; | |
| 32 | + provenanceRows: number; | |
| 33 | + byType: Record<string, number>; | |
| 34 | + changes: DetectedChange[]; | |
| 35 | + /** entity ids touched: [{type,id}] for documents.entity_refs */ | |
| 36 | + refs: Array<{ type: string; id: string }>; | |
| 37 | +} | |
| 38 | + | |
| 39 | +export type IngestFn = (run: IngestRun, entities: NormalizedEntity[], doc: IngestDocRef | null) => Promise<IngestStats>; | |
added
config/connectors/_example.yaml.disabled
+50 −0
@@ -0,0 +1,50 @@ | ||
| 1 | +# Example connector definition (disabled: file extension not .yaml). Copy to <id>.yaml. | |
| 2 | +id: example-operator | |
| 3 | +name: Example Operator | |
| 4 | +domain: example.com | |
| 5 | +kind: operator # operator | government | filing | utility | dataset | community | secondary | news | cloud_provider | registry | |
| 6 | +mode: hybrid | |
| 7 | +priority: 2 # 1 best … 5 | |
| 8 | +license: "Publicly accessible operator pages; facts only, attribution kept" | |
| 9 | +attribution: "Example Operator" | |
| 10 | +coverage: { global: true } | |
| 11 | +fetch: | |
| 12 | + level: 1 # start direct | |
| 13 | + maxLevel: 4 # allow escalation to Scrapfly when blocked | |
| 14 | + rpm: 20 | |
| 15 | + concurrency: 2 | |
| 16 | + respectRobots: true | |
| 17 | + maxCreditsPerRun: 150 | |
| 18 | +discovery: | |
| 19 | + sitemap: true | |
| 20 | + include: ["/data-centers/", "/newsroom/"] | |
| 21 | + exclude: ["/login", "\\?page="] | |
| 22 | + rss: ["/newsroom/rss.xml"] | |
| 23 | + classify: | |
| 24 | + - { pattern: "/data-centers/[a-z0-9-]+/[a-z0-9-]+$", group: facility_pages, pageType: facility_page, priority: 60 } | |
| 25 | + - { pattern: "/newsroom/", group: newsroom, pageType: press_release, priority: 70 } | |
| 26 | +schedule: | |
| 27 | + facility_pages: weekly | |
| 28 | + newsroom: daily | |
| 29 | + sitemap: weekly | |
| 30 | +extractors: | |
| 31 | + facility: | |
| 32 | + pageTypes: [facility_page] | |
| 33 | + kind: facility | |
| 34 | + key: "example:{code}" | |
| 35 | + fields: | |
| 36 | + name: "h1" | |
| 37 | + code: { selector: "h1", regex: "\\b([A-Z]{2}\\d{1,2})\\b" } | |
| 38 | + address: { jsonld: "Place", jsonPath: "address.streetAddress" } | |
| 39 | + city: { jsonld: "Place", jsonPath: "address.addressLocality" } | |
| 40 | + countryIso2: { jsonld: "Place", jsonPath: "address.addressCountry", transform: country } | |
| 41 | + lat: { jsonld: "Place", jsonPath: "geo.latitude", transform: float } | |
| 42 | + lng: { jsonld: "Place", jsonPath: "geo.longitude", transform: float } | |
| 43 | + totalPowerMw: { selector: ".specs", regex: "(\\d+(?:\\.\\d+)?\\s*MW)", transform: mw } | |
| 44 | + news: | |
| 45 | + pageTypes: [press_release, project_announcement, expansion, acquisition] | |
| 46 | + kind: news_event | |
| 47 | + parser: article_v1 | |
| 48 | +defaults: | |
| 49 | + operatorName: Example Operator | |
| 50 | + facilityType: colocation | |
added
deploy/dev-compose.yml
+23 −0
@@ -0,0 +1,23 @@ | ||
| 1 | +# Local development infra (laptop): ClickHouse + MinIO in Docker; Postgres 17 and Redis run from Homebrew. | |
| 2 | +services: | |
| 3 | + clickhouse: | |
| 4 | + image: clickhouse/clickhouse-server:25.8 | |
| 5 | + container_name: dci-dev-clickhouse | |
| 6 | + ports: ["127.0.0.1:8123:8123", "127.0.0.1:9009:9000"] | |
| 7 | + environment: | |
| 8 | + CLICKHOUSE_DB: dci | |
| 9 | + CLICKHOUSE_USER: default | |
| 10 | + CLICKHOUSE_PASSWORD: "" | |
| 11 | + CLICKHOUSE_DEFAULT_ACCESS_MANAGEMENT: "1" | |
| 12 | + ulimits: { nofile: { soft: 262144, hard: 262144 } } | |
| 13 | + volumes: ["dci-dev-clickhouse:/var/lib/clickhouse"] | |
| 14 | + minio: | |
| 15 | + image: minio/minio:latest | |
| 16 | + container_name: dci-dev-minio | |
| 17 | + command: server /data --console-address ":9001" | |
| 18 | + ports: ["127.0.0.1:9000:9000", "127.0.0.1:9001:9001"] | |
| 19 | + environment: { MINIO_ROOT_USER: dci, MINIO_ROOT_PASSWORD: dci-dev-secret } | |
| 20 | + volumes: ["dci-dev-minio:/data"] | |
| 21 | +volumes: | |
| 22 | + dci-dev-clickhouse: {} | |
| 23 | + dci-dev-minio: {} | |
added
docs/ARCHITECTURE.md
+23 −0
@@ -0,0 +1,23 @@ | ||
| 1 | +# Architecture | |
| 2 | + | |
| 3 | +## Stores | |
| 4 | +- **PostgreSQL** (truth): countries, metros, operators, campuses, facilities (+aliases, entity_keys), cloud_regions, ixps, facility_ixps, facility_tenants, projects (+timeline), sources, connectors (+runs), documents (+versions), provenance, events, news_items, entity_matches, rankings, daily_metrics, admin_sessions, system_alerts. | |
| 5 | +- **ClickHouse** (append-only analytics): crawl_log, observations, page_changes, metrics, entity_daily, api_requests. | |
| 6 | +- **Redis**: BullMQ queues (`dci:crawl`, `dci:ingest`, `dci:maintenance`), rate-limit + locks, API cache. | |
| 7 | +- **MinIO (S3)**: raw bodies `raw/<connector>/<doc_id>/<content_hash>.<ext>.zst`, PDFs, exports. | |
| 8 | + | |
| 9 | +## Crawl flow | |
| 10 | +Scheduler (per connector group interval, adaptive by `change_frequency_score`) → Connector.discover → URL registry (`documents`, fingerprint, classify, priority) → priority queue → fetch (robots, rate limit, conditional GET, escalation L1→L4, budgets) → archive raw → content fingerprint → if changed: extract → normalize → validate → reconcile (entity_matches) → persist entities + provenance + entity_keys → diff old/new field values → events (+ ClickHouse observations / page_changes) → recompute `documents.next_check`. | |
| 11 | + | |
| 12 | +## Entity resolution (facilities) | |
| 13 | +Score candidates by: same connector key (exact) → external ids → normalized name equality / facility code match (DC12, FR5) → same operator + distance ≤ radius(precision) → address similarity → alias table. ≥ 0.92 auto-merge, 0.6–0.92 → `entity_matches` pending (admin approves), < 0.6 → create. Never merge across different operators unless name+address exact. | |
| 14 | + | |
| 15 | +## Confidence | |
| 16 | +`computeConfidence()` in core: source kind base (operator/government/filing/utility/cloud/registry = high; dataset/community/secondary/news = moderate), extraction certainty, corroborations (≥2 independent primary → verified), conflicts, age. | |
| 17 | + | |
| 18 | +## Change detection | |
| 19 | +`contentFingerprint` (noise-stripped) per fetch; on change store a `document_versions` row with line diff + `detectedChanges` (field-level from normalized entity diff), significance 0–100 (status change 90, MW change 80, date change 60, operator change 85, text-only 10–30). | |
| 20 | + | |
| 21 | +## Two-node deployment | |
| 22 | +- **BHS128 (data/public)**: edge Caddy :8300 → web :8310 / api :8311; postgres, clickhouse, redis, minio bound to `10.68.0.1` (private WireGuard) + docker network. Public traffic arrives via the BHS64 gateway over WireGuard `wg1` (10.67.0.60). | |
| 23 | +- **BHS64b (crawl)**: worker + scheduler containers reaching the data services over `dci0` (10.68.0.2 → 10.68.0.1). No public ports. | |
added
package.json
+26 −0
@@ -0,0 +1,26 @@ | ||
| 1 | +{ | |
| 2 | + "name": "datacenterindex", | |
| 3 | + "version": "0.1.0", | |
| 4 | + "private": true, | |
| 5 | + "description": "DataCenterIndex.io — the global index of data center infrastructure: facilities, campuses, operators, cloud regions, markets, projects, power and connectivity, built on a homemade connector and crawling platform.", | |
| 6 | + "type": "module", | |
| 7 | + "packageManager": "pnpm@11.1.2", | |
| 8 | + "engines": { "node": ">=22.15" }, | |
| 9 | + "scripts": { | |
| 10 | + "typecheck": "pnpm -r --parallel run typecheck", | |
| 11 | + "test": "pnpm -r run test", | |
| 12 | + "build": "pnpm -r run build", | |
| 13 | + "db:migrate": "pnpm --filter @dci/db run migrate", | |
| 14 | + "db:seed": "pnpm --filter @dci/db run seed", | |
| 15 | + "dev:api": "pnpm --filter @dci/api run dev", | |
| 16 | + "dev:web": "pnpm --filter @dci/web run dev", | |
| 17 | + "dev:worker": "pnpm --filter @dci/worker run dev", | |
| 18 | + "dci": "node node_modules/tsx/dist/cli.mjs apps/worker/src/cli.ts" | |
| 19 | + }, | |
| 20 | + "devDependencies": { | |
| 21 | + "@types/node": "^24.0.0", | |
| 22 | + "tsx": "^4.20.0", | |
| 23 | + "typescript": "^5.9.3", | |
| 24 | + "vitest": "^3.2.0" | |
| 25 | + } | |
| 26 | +} | |
added
packages/connectors/package.json
+20 −0
@@ -0,0 +1,20 @@ | ||
| 1 | +{ | |
| 2 | + "name": "@dci/connectors", | |
| 3 | + "version": "0.1.0", | |
| 4 | + "private": true, | |
| 5 | + "type": "module", | |
| 6 | + "main": "./src/index.ts", | |
| 7 | + "types": "./src/index.ts", | |
| 8 | + "exports": { ".": "./src/index.ts" }, | |
| 9 | + "scripts": { "typecheck": "tsc --noEmit", "test": "vitest run --passWithNoTests" }, | |
| 10 | + "dependencies": { | |
| 11 | + "@dci/core": "workspace:*", | |
| 12 | + "cheerio": "^1.1.0", | |
| 13 | + "fast-xml-parser": "^5.2.0", | |
| 14 | + "undici": "^7.10.0", | |
| 15 | + "unpdf": "^1.0.0", | |
| 16 | + "yaml": "^2.8.0", | |
| 17 | + "zod": "^4.1.0" | |
| 18 | + }, | |
| 19 | + "devDependencies": { "@types/node": "^24.0.0", "typescript": "^5.9.3", "vitest": "^3.2.0" } | |
| 20 | +} | |
added
packages/connectors/src/classify.ts
+69 −0
@@ -0,0 +1,69 @@ | ||
| 1 | +import type { EventType, PageType } from "@dci/core"; | |
| 2 | +import { parseAllMw, inferStatus } from "@dci/core"; | |
| 3 | + | |
| 4 | +/** | |
| 5 | + * Deterministic page classification. URL rules first, then title/body keyword rules. | |
| 6 | + * An LLM may be plugged in later as an assistive classifier for "unknown" only. | |
| 7 | + */ | |
| 8 | +const URL_RULES: Array<[RegExp, PageType]> = [ | |
| 9 | + [/\/(data-?cent(er|re)s?|facilities|locations|colocation|ibx|campus(es)?)\/[a-z0-9-]+\/?$/i, "facility_page"], | |
| 10 | + [/\/(data-?cent(er|re)s?|facilities|locations|colocation)\/?$/i, "facility_index"], | |
| 11 | + [/\/(news(room)?|press(-releases?)?|media|announcements|blog)\/?$/i, "news_index"], | |
| 12 | + [/\/(news|press|media|announcements|newsroom)\/./i, "press_release"], | |
| 13 | + [/\/(regions?|global-infrastructure|locations|about\/locations|infrastructure\/regions)/i, "cloud_region"], | |
| 14 | + [/\.pdf(\?|$)/i, "planning_document"], | |
| 15 | + [/sitemap.*\.xml/i, "sitemap"], | |
| 16 | + [/\/(investors?|ir|financials?|sec-filings|annual-report|10-?k|quarterly)/i, "financial_disclosure"], | |
| 17 | + [/\/(planning|permits?|zoning|development-applications?|applications?)/i, "planning_document"], | |
| 18 | +]; | |
| 19 | + | |
| 20 | +const TEXT_RULES: Array<[RegExp, PageType]> = [ | |
| 21 | + [/\b(acquires|acquisition of|to acquire|completes acquisition|agreed to acquire|acquired)\b/i, "acquisition"], | |
| 22 | + [/\b(expansion|expands|expanding|adds? \d+ ?MW|additional (capacity|phase)|new phase)\b/i, "expansion"], | |
| 23 | + [/\b(closes?|closure|closing|decommission|shut(ting)? down|exit(s|ing)? (the )?market)\b/i, "closure"], | |
| 24 | + [/\b(planning (application|permission|commission|board)|rezon(e|ing)|site plan|conditional use|environmental (assessment|impact)|permit(ting)?)\b/i, "planning_document"], | |
| 25 | + [/\b(under construction|breaks? ground|groundbreaking|topping out|construction (update|progress|milestone)|shell (is )?complete)\b/i, "construction_update"], | |
| 26 | + [/\b(substation|transmission line|power (purchase|supply) agreement|PPA|grid connection|interconnection agreement|utility|megawatts of power|gigawatt|nuclear|SMR|gas turbine|solar farm|wind farm)\b/i, "power_infrastructure"], | |
| 27 | + [/\b(cloud region|availability zones?|launches? (a )?new region|region (now )?(available|generally available|GA)|sovereign cloud)\b/i, "cloud_region"], | |
| 28 | + [/\b(announces?|unveils?|plans (to build|for)|to build|will build|new (data ?cent(er|re)|campus|facility)|investment of|to invest|\$\d+(\.\d+)? ?(billion|million)|€\d+|£\d+)\b/i, "project_announcement"], | |
| 29 | + [/\b(press release|for immediate release|newsroom|media contact)\b/i, "press_release"], | |
| 30 | + [/\b(colocation|carrier-neutral|interconnection|meet-me room|rack|cabinet|cage|cross[- ]connect|N\+1|2N|redundan|PUE|Tier (III|IV|3|4)|uptime institute)\b/i, "facility_page"], | |
| 31 | +]; | |
| 32 | + | |
| 33 | +export interface Classification { pageType: PageType; rule: string; eventType?: EventType; mw: number[]; status?: string | null } | |
| 34 | + | |
| 35 | +export function classifyPage(url: string, title: string | null, text: string): Classification { | |
| 36 | + for (const [re, pt] of URL_RULES) if (re.test(url)) return finish(pt, `url:${re.source.slice(0, 30)}`, title, text); | |
| 37 | + const head = `${title ?? ""}\n${text.slice(0, 4000)}`; | |
| 38 | + for (const [re, pt] of TEXT_RULES) if (re.test(head)) return finish(pt, `text:${re.source.slice(0, 30)}`, title, text); | |
| 39 | + return finish("unknown", "none", title, text); | |
| 40 | +} | |
| 41 | + | |
| 42 | +function finish(pageType: PageType, rule: string, title: string | null, text: string): Classification { | |
| 43 | + const head = `${title ?? ""}\n${text.slice(0, 6000)}`; | |
| 44 | + const mw = parseAllMw(head); | |
| 45 | + const status = inferStatus(head); | |
| 46 | + const eventType = eventTypeFor(pageType, head); | |
| 47 | + return { pageType, rule, eventType, mw, status }; | |
| 48 | +} | |
| 49 | + | |
| 50 | +export function eventTypeFor(pageType: PageType, text: string): EventType | undefined { | |
| 51 | + switch (pageType) { | |
| 52 | + case "acquisition": return "acquisition"; | |
| 53 | + case "expansion": return "expansion_announced"; | |
| 54 | + case "closure": return "closure"; | |
| 55 | + case "construction_update": return /\b(breaks? ground|groundbreaking|construction (has )?(started|begun|commenced))\b/i.test(text) ? "construction_started" : "project_status_changed"; | |
| 56 | + case "planning_document": return /\b(approved|approval granted|granted permission)\b/i.test(text) ? "planning_approved" : "planning_filed"; | |
| 57 | + case "power_infrastructure": return "power_agreement"; | |
| 58 | + case "cloud_region": return /\b(announc|plans?|upcoming|coming)/i.test(text) && !/\b(now available|generally available|launched|is live|opens)\b/i.test(text) ? "cloud_region_announced" : "cloud_region_launched"; | |
| 59 | + case "project_announcement": return /\b(invest(ment|s)? of|to invest|\$\d)/i.test(text) && !/\b(data ?cent|campus|MW)\b/i.test(text) ? "investment_announced" : "project_announced"; | |
| 60 | + case "press_release": return "news"; | |
| 61 | + case "facility_page": return "facility_updated"; | |
| 62 | + default: return undefined; | |
| 63 | + } | |
| 64 | +} | |
| 65 | + | |
| 66 | +/** Is this text about data centers at all? Used to drop irrelevant newsroom items early. */ | |
| 67 | +export function isDataCenterRelevant(text: string): boolean { | |
| 68 | + return /\b(data ?cent(er|re)s?|colocation|hyperscale|cloud region|availability zone|MW|megawatt|campus|interconnection|IBX|server farm|AI infrastructure|GPU cluster|compute capacity|digital infrastructure)\b/i.test(text); | |
| 69 | +} | |
added
packages/connectors/src/config.ts
+142 −0
@@ -0,0 +1,142 @@ | ||
| 1 | +import { z } from "zod"; | |
| 2 | +import { parse as parseYaml } from "yaml"; | |
| 3 | +import { readFileSync, readdirSync } from "node:fs"; | |
| 4 | +import { join } from "node:path"; | |
| 5 | +import { SOURCE_KINDS, PAGE_TYPES } from "@dci/core"; | |
| 6 | + | |
| 7 | +/** | |
| 8 | + * Connector configuration (YAML in config/connectors/*.yaml). Declarative first; `parser:` names a | |
| 9 | + * code-backed parser from the registry when selectors are not enough. | |
| 10 | + */ | |
| 11 | +const selectorRule = z.union([ | |
| 12 | + z.string(), | |
| 13 | + z.object({ | |
| 14 | + selector: z.string().optional(), | |
| 15 | + attr: z.string().optional(), | |
| 16 | + regex: z.string().optional(), | |
| 17 | + regexFlags: z.string().optional(), | |
| 18 | + group: z.number().int().optional(), | |
| 19 | + all: z.boolean().optional(), | |
| 20 | + jsonPath: z.string().optional(), // for embedded JSON / JSON-LD ("$.address.addressLocality") | |
| 21 | + jsonld: z.string().optional(), // JSON-LD @type to look in (e.g. "Place", "LocalBusiness") | |
| 22 | + meta: z.string().optional(), // <meta property|name=…> | |
| 23 | + transform: z.enum(["mw", "sqm", "ha", "money", "date", "status", "type", "country", "int", "float", "trim", "lower", "url", "bool"]).optional(), | |
| 24 | + default: z.unknown().optional(), | |
| 25 | + }), | |
| 26 | +]); | |
| 27 | + | |
| 28 | +export const extractorSchema = z.object({ | |
| 29 | + /** page types this extractor handles */ | |
| 30 | + pageTypes: z.array(z.enum(PAGE_TYPES)).optional(), | |
| 31 | + /** which URL patterns select this extractor */ | |
| 32 | + match: z.array(z.string()).optional(), | |
| 33 | + kind: z.enum(["facility", "operator", "campus", "cloud_region", "ixp", "project", "news_event", "country"]).default("facility"), | |
| 34 | + parser: z.string().optional(), | |
| 35 | + params: z.record(z.string(), z.unknown()).optional(), | |
| 36 | + /** declarative field rules */ | |
| 37 | + fields: z.record(z.string(), selectorRule).optional(), | |
| 38 | + /** repeat over a list container: each match yields one record */ | |
| 39 | + each: z.string().optional(), | |
| 40 | + /** stable key template, e.g. "equinix:{code}" or "{url}" */ | |
| 41 | + key: z.string().optional(), | |
| 42 | + minCertainty: z.number().min(0).max(1).optional(), | |
| 43 | +}); | |
| 44 | + | |
| 45 | +export const connectorConfigSchema = z.object({ | |
| 46 | + id: z.string().regex(/^[a-z0-9][a-z0-9-]*$/), | |
| 47 | + name: z.string(), | |
| 48 | + domain: z.string(), | |
| 49 | + kind: z.enum(SOURCE_KINDS), | |
| 50 | + mode: z.enum(["html", "sitemap", "rss", "pdf", "json", "hybrid", "api", "dataset"]).default("hybrid"), | |
| 51 | + enabled: z.boolean().default(true), | |
| 52 | + priority: z.number().int().min(1).max(5).default(3), | |
| 53 | + license: z.string().optional(), | |
| 54 | + attribution: z.string().optional(), | |
| 55 | + homepage: z.string().url().optional(), | |
| 56 | + notes: z.string().optional(), | |
| 57 | + /** which countries / operators this source mostly covers (for admin + scheduling) */ | |
| 58 | + coverage: z.object({ countries: z.array(z.string()).optional(), operators: z.array(z.string()).optional(), global: z.boolean().optional() }).optional(), | |
| 59 | + fetch: z | |
| 60 | + .object({ | |
| 61 | + /** starting level; the runtime escalates on 403/429/JS-shell detection if `maxLevel` allows */ | |
| 62 | + level: z.number().int().min(1).max(4).default(1), | |
| 63 | + maxLevel: z.number().int().min(1).max(4).default(2), | |
| 64 | + renderJs: z.boolean().optional(), | |
| 65 | + country: z.string().optional(), | |
| 66 | + waitForSelector: z.string().optional(), | |
| 67 | + userAgent: z.enum(["bot", "browser"]).default("bot"), | |
| 68 | + accept: z.string().optional(), | |
| 69 | + headers: z.record(z.string(), z.string()).optional(), | |
| 70 | + timeoutMs: z.number().int().optional(), | |
| 71 | + maxBytes: z.number().int().optional(), | |
| 72 | + /** requests per minute against this domain */ | |
| 73 | + rpm: z.number().min(1).max(600).default(30), | |
| 74 | + concurrency: z.number().int().min(1).max(8).default(2), | |
| 75 | + respectRobots: z.boolean().default(true), | |
| 76 | + /** premium credit budget per run */ | |
| 77 | + maxCreditsPerRun: z.number().int().default(200), | |
| 78 | + }) | |
| 79 | + .prefault({}), | |
| 80 | + discovery: z | |
| 81 | + .object({ | |
| 82 | + sitemap: z.union([z.boolean(), z.array(z.string())]).default(false), | |
| 83 | + rss: z.array(z.string()).default([]), | |
| 84 | + seeds: z.array(z.union([z.string(), z.object({ url: z.string(), group: z.string().default("seed"), pageType: z.enum(PAGE_TYPES).optional(), minLevel: z.number().int().min(1).max(4).optional() })])).default([]), | |
| 85 | + /** follow internal links matching these patterns (regex), from index pages only */ | |
| 86 | + follow: z.array(z.string()).default([]), | |
| 87 | + include: z.array(z.string()).default([]), | |
| 88 | + exclude: z.array(z.string()).default([]), | |
| 89 | + /** map URL regex → group / pageType */ | |
| 90 | + classify: z | |
| 91 | + .array(z.object({ pattern: z.string(), group: z.string(), pageType: z.enum(PAGE_TYPES).optional(), priority: z.number().int().optional(), minLevel: z.number().int().min(1).max(4).optional() })) | |
| 92 | + .default([]), | |
| 93 | + maxUrlsPerRun: z.number().int().default(2000), | |
| 94 | + /** pagination: "?page={n}" or "/page/{n}" appended to index seeds, until no new links */ | |
| 95 | + pagination: z.object({ template: z.string(), start: z.number().int().default(1), max: z.number().int().default(20) }).optional(), | |
| 96 | + }) | |
| 97 | + .prefault({}), | |
| 98 | + schedule: z.record(z.string(), z.string()).default({ seed: "weekly" }), | |
| 99 | + extractors: z.record(z.string(), extractorSchema).default({}), | |
| 100 | + /** defaults merged into every normalized entity from this connector */ | |
| 101 | + defaults: z | |
| 102 | + .object({ operatorName: z.string().optional(), operatorKind: z.string().optional(), countryIso2: z.string().optional(), facilityType: z.string().optional(), status: z.string().optional(), isHyperscale: z.boolean().optional(), providerName: z.string().optional() }) | |
| 103 | + .optional(), | |
| 104 | + /** code-backed connector class overriding the generic pipeline (e.g. "peeringdb", "overpass") */ | |
| 105 | + implementation: z.string().optional(), | |
| 106 | + params: z.record(z.string(), z.unknown()).optional(), | |
| 107 | + parserVersion: z.string().default("v1"), | |
| 108 | +}); | |
| 109 | + | |
| 110 | +export type ConnectorConfig = z.infer<typeof connectorConfigSchema>; | |
| 111 | +export type ExtractorConfig = z.infer<typeof extractorSchema>; | |
| 112 | +export type SelectorRule = z.infer<typeof selectorRule>; | |
| 113 | + | |
| 114 | +export function parseConnectorConfig(text: string, file = "<inline>"): ConnectorConfig { | |
| 115 | + const raw = parseYaml(text); | |
| 116 | + const res = connectorConfigSchema.safeParse(raw); | |
| 117 | + if (!res.success) throw new Error(`Invalid connector config ${file}: ${res.error.issues.map((i) => `${i.path.join(".")}: ${i.message}`).join("; ")}`); | |
| 118 | + return res.data; | |
| 119 | +} | |
| 120 | + | |
| 121 | +export function loadConnectorConfigs(dir: string): ConnectorConfig[] { | |
| 122 | + const out: ConnectorConfig[] = []; | |
| 123 | + const seen = new Set<string>(); | |
| 124 | + for (const f of readdirSync(dir).filter((x) => /\.ya?ml$/.test(x)).sort()) { | |
| 125 | + const cfg = parseConnectorConfig(readFileSync(join(dir, f), "utf8"), f); | |
| 126 | + if (seen.has(cfg.id)) throw new Error(`duplicate connector id ${cfg.id} in ${f}`); | |
| 127 | + seen.add(cfg.id); | |
| 128 | + out.push(cfg); | |
| 129 | + } | |
| 130 | + return out; | |
| 131 | +} | |
| 132 | + | |
| 133 | +/** "daily" | "weekly" | "monthly" | "hourly" | "3h" | "12h" | "30m" → milliseconds */ | |
| 134 | +export function intervalMs(spec: string): number { | |
| 135 | + const s = spec.trim().toLowerCase(); | |
| 136 | + const named: Record<string, number> = { hourly: 3_600_000, daily: 86_400_000, weekly: 7 * 86_400_000, biweekly: 14 * 86_400_000, monthly: 30 * 86_400_000, quarterly: 91 * 86_400_000, never: Number.POSITIVE_INFINITY }; | |
| 137 | + if (named[s]) return named[s]!; | |
| 138 | + const m = s.match(/^(\d+)\s*(m|min|h|d|w)$/); | |
| 139 | + if (!m) throw new Error(`bad interval: ${spec}`); | |
| 140 | + const n = Number(m[1]); | |
| 141 | + return { m: 60_000, min: 60_000, h: 3_600_000, d: 86_400_000, w: 7 * 86_400_000 }[m[2]!]! * n; | |
| 142 | +} | |
added
packages/connectors/src/discovery.ts
+153 −0
@@ -0,0 +1,153 @@ | ||
| 1 | +import { XMLParser } from "fast-xml-parser"; | |
| 2 | +import { load } from "cheerio"; | |
| 3 | +import type { PageType } from "@dci/core"; | |
| 4 | +import type { ConnectorConfig } from "./config.js"; | |
| 5 | +import type { ConnectorContext, DiscoveredUrl, RawDocument } from "./types.js"; | |
| 6 | + | |
| 7 | +const xml = new XMLParser({ ignoreAttributes: false, attributeNamePrefix: "@_", textNodeName: "#text", trimValues: true, parseTagValue: false }); | |
| 8 | + | |
| 9 | +/** Parse a sitemap or sitemap index. Returns child sitemaps and URL entries. */ | |
| 10 | +export function parseSitemap(text: string): { sitemaps: string[]; urls: Array<{ loc: string; lastmod: string | null }> } { | |
| 11 | + const out = { sitemaps: [] as string[], urls: [] as Array<{ loc: string; lastmod: string | null }> }; | |
| 12 | + let doc: Record<string, unknown>; | |
| 13 | + try { doc = xml.parse(text) as Record<string, unknown>; } catch { return out; } | |
| 14 | + const idx = (doc.sitemapindex as { sitemap?: unknown })?.sitemap; | |
| 15 | + if (idx) for (const s of Array.isArray(idx) ? idx : [idx]) { const loc = (s as { loc?: string }).loc; if (loc) out.sitemaps.push(String(loc).trim()); } | |
| 16 | + const set = (doc.urlset as { url?: unknown })?.url; | |
| 17 | + if (set) for (const u of Array.isArray(set) ? set : [set]) { const e = u as { loc?: string; lastmod?: string }; if (e.loc) out.urls.push({ loc: String(e.loc).trim(), lastmod: e.lastmod ? String(e.lastmod) : null }); } | |
| 18 | + if (!idx && !set) { | |
| 19 | + // plain-text sitemap | |
| 20 | + for (const line of text.split(/\r?\n/)) { const l = line.trim(); if (/^https?:\/\//.test(l)) out.urls.push({ loc: l, lastmod: null }); } | |
| 21 | + } | |
| 22 | + return out; | |
| 23 | +} | |
| 24 | + | |
| 25 | +export interface FeedItem { title: string; link: string; published: string | null; summary: string | null; id: string | null; categories: string[] } | |
| 26 | + | |
| 27 | +/** RSS 2.0 / Atom / RDF feed parser. */ | |
| 28 | +export function parseFeed(text: string): FeedItem[] { | |
| 29 | + let doc: Record<string, unknown>; | |
| 30 | + try { doc = xml.parse(text) as Record<string, unknown>; } catch { return []; } | |
| 31 | + const items: FeedItem[] = []; | |
| 32 | + const txt = (v: unknown): string | null => { if (v == null) return null; if (typeof v === "string") return v.trim() || null; if (typeof v === "object") { const o = v as Record<string, unknown>; return txt(o["#text"] ?? o["@_href"] ?? null); } return String(v); }; | |
| 33 | + const chan = (doc.rss as { channel?: { item?: unknown } })?.channel?.item ?? (doc["rdf:RDF"] as { item?: unknown })?.item; | |
| 34 | + if (chan) for (const it of Array.isArray(chan) ? chan : [chan]) { | |
| 35 | + const o = it as Record<string, unknown>; | |
| 36 | + const link = txt(o.link) ?? txt(o.guid); | |
| 37 | + if (!link) continue; | |
| 38 | + const cats = o.category ? (Array.isArray(o.category) ? o.category : [o.category]).map((c) => txt(c)).filter((c): c is string => Boolean(c)) : []; | |
| 39 | + items.push({ title: txt(o.title) ?? link, link, published: txt(o.pubDate) ?? txt(o["dc:date"]) ?? null, summary: txt(o.description) ?? txt(o["content:encoded"]) ?? null, id: txt(o.guid) ?? null, categories: cats }); | |
| 40 | + } | |
| 41 | + const entries = (doc.feed as { entry?: unknown })?.entry; | |
| 42 | + if (entries) for (const en of Array.isArray(entries) ? entries : [entries]) { | |
| 43 | + const o = en as Record<string, unknown>; | |
| 44 | + const links = Array.isArray(o.link) ? o.link : o.link ? [o.link] : []; | |
| 45 | + const alt = (links as Array<Record<string, unknown>>).find((l) => !l["@_rel"] || l["@_rel"] === "alternate") ?? (links as Array<Record<string, unknown>>)[0]; | |
| 46 | + const link = alt ? String(alt["@_href"] ?? txt(alt) ?? "") : ""; | |
| 47 | + if (!link) continue; | |
| 48 | + const cats = o.category ? (Array.isArray(o.category) ? o.category : [o.category]).map((c) => String((c as Record<string, unknown>)["@_term"] ?? txt(c) ?? "")).filter(Boolean) : []; | |
| 49 | + items.push({ title: txt(o.title) ?? link, link, published: txt(o.published) ?? txt(o.updated) ?? null, summary: txt(o.summary) ?? txt(o.content) ?? null, id: txt(o.id) ?? null, categories: cats }); | |
| 50 | + } | |
| 51 | + return items; | |
| 52 | +} | |
| 53 | + | |
| 54 | +/** Absolute, same-site links from an HTML page (anchors only). */ | |
| 55 | +export function extractLinks(html: string, baseUrl: string, sameHostOnly = true): string[] { | |
| 56 | + const $ = load(html); | |
| 57 | + const base = new URL(baseUrl); | |
| 58 | + const out = new Set<string>(); | |
| 59 | + $("a[href]").each((_, a) => { | |
| 60 | + const href = $(a).attr("href"); | |
| 61 | + if (!href || /^(mailto:|tel:|javascript:|#)/i.test(href)) return; | |
| 62 | + try { | |
| 63 | + const u = new URL(href, base); | |
| 64 | + if (!/^https?:$/.test(u.protocol)) return; | |
| 65 | + if (sameHostOnly && u.hostname.replace(/^www\./, "") !== base.hostname.replace(/^www\./, "")) return; | |
| 66 | + u.hash = ""; | |
| 67 | + out.add(u.toString()); | |
| 68 | + } catch { /* ignore */ } | |
| 69 | + }); | |
| 70 | + return [...out]; | |
| 71 | +} | |
| 72 | + | |
| 73 | +function compile(patterns: string[]): RegExp[] { return patterns.map((p) => new RegExp(p, "i")); } | |
| 74 | + | |
| 75 | +/** Apply include/exclude/classify rules from the config to a URL. Returns null when excluded. */ | |
| 76 | +export function classifyUrl(cfg: ConnectorConfig, url: string, discoveredFrom: string | null = null): DiscoveredUrl | null { | |
| 77 | + const d = cfg.discovery; | |
| 78 | + if (d.exclude.length && compile(d.exclude).some((re) => re.test(url))) return null; | |
| 79 | + if (d.include.length && !compile(d.include).some((re) => re.test(url))) return null; | |
| 80 | + for (const rule of d.classify) { | |
| 81 | + if (new RegExp(rule.pattern, "i").test(url)) return { url, group: rule.group, pageType: rule.pageType as PageType | undefined, priority: rule.priority ?? 50, discoveredFrom, minLevel: rule.minLevel as DiscoveredUrl["minLevel"] }; | |
| 82 | + } | |
| 83 | + return { url, group: "default", priority: 30, discoveredFrom }; | |
| 84 | +} | |
| 85 | + | |
| 86 | +/** Generic discovery: sitemaps (auto or listed), RSS feeds, seeds, and link-following from index pages. */ | |
| 87 | +export async function discoverGeneric(cfg: ConnectorConfig, ctx: ConnectorContext): Promise<DiscoveredUrl[]> { | |
| 88 | + const found = new Map<string, DiscoveredUrl>(); | |
| 89 | + const add = (u: DiscoveredUrl | null) => { if (u && !found.has(u.url) && found.size < cfg.discovery.maxUrlsPerRun) found.set(u.url, u); }; | |
| 90 | + const origin = `https://${cfg.domain.replace(/^https?:\/\//, "").replace(/\/$/, "")}`; | |
| 91 | + | |
| 92 | + // seeds | |
| 93 | + for (const s of cfg.discovery.seeds) { | |
| 94 | + const seed = typeof s === "string" ? { url: s, group: "seed" as string, pageType: undefined as PageType | undefined, minLevel: undefined as number | undefined } : s; | |
| 95 | + const url = seed.url.startsWith("http") ? seed.url : origin + seed.url; | |
| 96 | + const c = classifyUrl(cfg, url, null); | |
| 97 | + add({ url, group: seed.group ?? c?.group ?? "seed", pageType: seed.pageType ?? c?.pageType, priority: 80, minLevel: (seed.minLevel ?? c?.minLevel) as DiscoveredUrl["minLevel"], discoveredFrom: null }); | |
| 98 | + } | |
| 99 | + | |
| 100 | + // sitemaps | |
| 101 | + const sitemapUrls: string[] = []; | |
| 102 | + if (cfg.discovery.sitemap === true) sitemapUrls.push(`${origin}/sitemap.xml`, `${origin}/sitemap_index.xml`, `${origin}/sitemap-index.xml`); | |
| 103 | + else if (Array.isArray(cfg.discovery.sitemap)) for (const s of cfg.discovery.sitemap) sitemapUrls.push(s.startsWith("http") ? s : origin + s); | |
| 104 | + const seenMaps = new Set<string>(); | |
| 105 | + let mapBudget = 40; | |
| 106 | + while (sitemapUrls.length && mapBudget-- > 0) { | |
| 107 | + const sm = sitemapUrls.shift()!; | |
| 108 | + if (seenMaps.has(sm)) continue; | |
| 109 | + seenMaps.add(sm); | |
| 110 | + const doc = await ctx.fetch(sm, { group: "sitemap", accept: "application/xml,text/xml,*/*", maxBytes: 60 * 1024 * 1024 }); | |
| 111 | + if (doc.error || doc.status !== 200) { ctx.log("debug", `sitemap ${sm} → ${doc.error?.code ?? doc.status}`); continue; } | |
| 112 | + const parsed = parseSitemap(doc.text); | |
| 113 | + for (const child of parsed.sitemaps) if (!seenMaps.has(child)) sitemapUrls.push(child); | |
| 114 | + for (const u of parsed.urls) { const c = classifyUrl(cfg, u.loc, sm); if (c) add({ ...c, lastmod: u.lastmod }); } | |
| 115 | + ctx.log("info", `sitemap ${sm}: ${parsed.urls.length} urls, ${parsed.sitemaps.length} children`); | |
| 116 | + } | |
| 117 | + | |
| 118 | + // rss | |
| 119 | + for (const f of cfg.discovery.rss) { | |
| 120 | + const url = f.startsWith("http") ? f : origin + f; | |
| 121 | + const doc = await ctx.fetch(url, { group: "rss", accept: "application/rss+xml,application/atom+xml,application/xml,text/xml,*/*" }); | |
| 122 | + if (doc.error || doc.status !== 200) { ctx.log("warn", `rss ${url} → ${doc.error?.code ?? doc.status}`); continue; } | |
| 123 | + const items = parseFeed(doc.text); | |
| 124 | + for (const it of items) { const c = classifyUrl(cfg, it.link, url); if (c) add({ ...c, group: c.group === "default" ? "newsroom" : c.group, pageType: c.pageType ?? "press_release", priority: 70, lastmod: it.published, meta: { title: it.title, summary: it.summary, published: it.published, categories: it.categories } }); } | |
| 125 | + ctx.log("info", `rss ${url}: ${items.length} items`); | |
| 126 | + } | |
| 127 | + | |
| 128 | + // link following from index pages (seeds + pagination) | |
| 129 | + if (cfg.discovery.follow.length) { | |
| 130 | + const follow = compile(cfg.discovery.follow); | |
| 131 | + const indexPages = [...found.values()].filter((u) => u.group === "seed" || u.pageType === "facility_index" || u.pageType === "news_index"); | |
| 132 | + const pages: string[] = indexPages.map((u) => u.url); | |
| 133 | + if (cfg.discovery.pagination) for (const ip of indexPages) for (let n = cfg.discovery.pagination.start; n <= cfg.discovery.pagination.max; n++) pages.push(ip.url.replace(/\/$/, "") + cfg.discovery.pagination.template.replace("{n}", String(n))); | |
| 134 | + let stale = 0; | |
| 135 | + for (const p of pages) { | |
| 136 | + if (found.size >= cfg.discovery.maxUrlsPerRun) break; | |
| 137 | + const doc = await ctx.fetch(p, { group: "index" }); | |
| 138 | + if (doc.error || doc.status !== 200) { if (++stale >= 3) break; continue; } | |
| 139 | + const before = found.size; | |
| 140 | + for (const l of extractLinks(doc.text, doc.finalUrl)) if (follow.some((re) => re.test(l))) add(classifyUrl(cfg, l, p)); | |
| 141 | + if (found.size === before) { if (++stale >= 3) break; } else stale = 0; | |
| 142 | + } | |
| 143 | + } | |
| 144 | + return [...found.values()]; | |
| 145 | +} | |
| 146 | + | |
| 147 | +/** Robots-declared sitemaps are useful even when discovery.sitemap is false. */ | |
| 148 | +export function pickSitemapsFromRobots(sitemaps: string[], domain: string): string[] { | |
| 149 | + const d = domain.replace(/^www\./, ""); | |
| 150 | + return sitemaps.filter((s) => { try { return new URL(s).hostname.replace(/^www\./, "").endsWith(d); } catch { return false; } }); | |
| 151 | +} | |
| 152 | + | |
| 153 | +export function docIsXml(doc: RawDocument): boolean { return /xml/i.test(doc.contentType ?? "") || /^\s*<\?xml/.test(doc.text); } | |
added
packages/connectors/src/extract.ts
+186 −0
@@ -0,0 +1,186 @@ | ||
| 1 | +import { load, type CheerioAPI } from "cheerio"; | |
| 2 | +import { extractText as pdfExtractText, getDocumentProxy } from "unpdf"; | |
| 3 | +import { parseMw, parseAreaSqm, parseAreaHa, parseMoney, parsePartialDate, normalizeStatus, inferFacilityType, countryFromText, cleanText, htmlToText } from "@dci/core"; | |
| 4 | +import type { SelectorRule } from "./config.js"; | |
| 5 | +import type { RawDocument } from "./types.js"; | |
| 6 | + | |
| 7 | +/** All JSON-LD objects on a page (flattened @graph), optionally filtered by @type. */ | |
| 8 | +export function jsonLd(html: string, type?: string): Record<string, unknown>[] { | |
| 9 | + const $ = load(html); | |
| 10 | + const out: Record<string, unknown>[] = []; | |
| 11 | + $('script[type="application/ld+json"]').each((_, el) => { | |
| 12 | + const raw = $(el).contents().text(); | |
| 13 | + try { | |
| 14 | + const parsed = JSON.parse(raw.replace(/^\s*<!--|-->\s*$/g, "")); | |
| 15 | + const arr = Array.isArray(parsed) ? parsed : [parsed]; | |
| 16 | + for (const p of arr) { | |
| 17 | + if (p && typeof p === "object" && Array.isArray((p as { "@graph"?: unknown[] })["@graph"])) out.push(...((p as { "@graph": Record<string, unknown>[] })["@graph"])); | |
| 18 | + else if (p && typeof p === "object") out.push(p as Record<string, unknown>); | |
| 19 | + } | |
| 20 | + } catch { /* malformed */ } | |
| 21 | + }); | |
| 22 | + if (!type) return out; | |
| 23 | + const t = type.toLowerCase(); | |
| 24 | + return out.filter((o) => { const ty = o["@type"]; const list = Array.isArray(ty) ? ty : [ty]; return list.some((x) => String(x).toLowerCase() === t); }); | |
| 25 | +} | |
| 26 | + | |
| 27 | +/** Embedded JSON blobs: __NEXT_DATA__, window.__INITIAL_STATE__ = {...}, application/json scripts, Nuxt/Drupal settings. */ | |
| 28 | +export function embeddedJson(html: string): Array<{ id: string; data: unknown }> { | |
| 29 | + const $ = load(html); | |
| 30 | + const out: Array<{ id: string; data: unknown }> = []; | |
| 31 | + $('script[type="application/json"], script#__NEXT_DATA__, script[data-drupal-selector="drupal-settings-json"]').each((_, el) => { | |
| 32 | + const id = $(el).attr("id") ?? $(el).attr("data-drupal-selector") ?? "json"; | |
| 33 | + try { out.push({ id, data: JSON.parse($(el).contents().text()) }); } catch { /* skip */ } | |
| 34 | + }); | |
| 35 | + const re = /(?:window\.|var\s+|const\s+|let\s+)?(__INITIAL_STATE__|__NUXT__|__PRELOADED_STATE__|__APOLLO_STATE__|__DATA__|__INITIAL_DATA__|initialData|pageData|__NEXT_DATA__)\s*=\s*(\{[\s\S]*?\})\s*;?\s*(?:<\/script>|\n)/g; | |
| 36 | + for (const m of html.matchAll(re)) { try { out.push({ id: m[1]!, data: JSON.parse(m[2]!) }); } catch { /* not plain JSON */ } } | |
| 37 | + return out; | |
| 38 | +} | |
| 39 | + | |
| 40 | +/** Tiny JSONPath subset: "$.a.b[0].c", "a.b", "[*]" not supported (use walk). */ | |
| 41 | +export function jsonPath(obj: unknown, path: string): unknown { | |
| 42 | + const parts = path.replace(/^\$\.?/, "").split(/\.|\[(\d+)\]/).filter((p) => p !== undefined && p !== ""); | |
| 43 | + let cur: unknown = obj; | |
| 44 | + for (const p of parts) { | |
| 45 | + if (cur == null) return undefined; | |
| 46 | + cur = (cur as Record<string, unknown>)[p]; | |
| 47 | + } | |
| 48 | + return cur; | |
| 49 | +} | |
| 50 | + | |
| 51 | +/** Depth-first walk collecting objects satisfying a predicate (used to find facility-like nodes in embedded JSON). */ | |
| 52 | +export function walkJson(obj: unknown, pred: (o: Record<string, unknown>) => boolean, out: Record<string, unknown>[] = [], depth = 0): Record<string, unknown>[] { | |
| 53 | + if (depth > 12 || obj == null) return out; | |
| 54 | + if (Array.isArray(obj)) { for (const x of obj) walkJson(x, pred, out, depth + 1); return out; } | |
| 55 | + if (typeof obj === "object") { | |
| 56 | + const o = obj as Record<string, unknown>; | |
| 57 | + if (pred(o)) out.push(o); | |
| 58 | + for (const v of Object.values(o)) walkJson(v, pred, out, depth + 1); | |
| 59 | + } | |
| 60 | + return out; | |
| 61 | +} | |
| 62 | + | |
| 63 | +export function applyTransform(v: unknown, t: SelectorRule extends string ? never : Exclude<SelectorRule, string>["transform"]): unknown { | |
| 64 | + if (v == null) return null; | |
| 65 | + const s = typeof v === "string" ? v : String(v); | |
| 66 | + switch (t) { | |
| 67 | + case "mw": return parseMw(s); | |
| 68 | + case "sqm": return parseAreaSqm(s); | |
| 69 | + case "ha": return parseAreaHa(s); | |
| 70 | + case "money": return parseMoney(s); | |
| 71 | + case "date": return parsePartialDate(s); | |
| 72 | + case "status": return normalizeStatus(s); | |
| 73 | + case "type": return inferFacilityType(s); | |
| 74 | + case "country": return /^[A-Z]{2}$/.test(s.trim()) ? s.trim() : countryFromText(s); | |
| 75 | + case "int": { const n = parseInt(s.replace(/[^\d-]/g, ""), 10); return Number.isFinite(n) ? n : null; } | |
| 76 | + case "float": { const n = parseFloat(s.replace(/[^\d.-]/g, "")); return Number.isFinite(n) ? n : null; } | |
| 77 | + case "trim": return cleanText(s); | |
| 78 | + case "lower": return s.trim().toLowerCase(); | |
| 79 | + case "url": return s.trim(); | |
| 80 | + case "bool": return /^(true|yes|1|y)$/i.test(s.trim()); | |
| 81 | + default: return typeof v === "string" ? cleanText(v) : v; | |
| 82 | + } | |
| 83 | +} | |
| 84 | + | |
| 85 | +/** Evaluate one declarative selector rule against a page. */ | |
| 86 | +export function evalRule($: CheerioAPI, html: string, rule: SelectorRule, scope?: ReturnType<CheerioAPI>): { value: unknown; method: string } { | |
| 87 | + const r: Exclude<SelectorRule, string> = typeof rule === "string" ? { selector: rule } : rule; | |
| 88 | + let value: unknown = null; | |
| 89 | + let method = ""; | |
| 90 | + if (r.jsonld) { | |
| 91 | + const objs = jsonLd(html, r.jsonld); | |
| 92 | + value = objs.length && r.jsonPath ? jsonPath(objs[0], r.jsonPath) : objs[0] ?? null; | |
| 93 | + method = `json-ld:${r.jsonld}${r.jsonPath ? "." + r.jsonPath : ""}`; | |
| 94 | + } else if (r.meta) { | |
| 95 | + value = $(`meta[property="${r.meta}"], meta[name="${r.meta}"]`).first().attr("content") ?? null; | |
| 96 | + method = `meta:${r.meta}`; | |
| 97 | + } else if (r.jsonPath && !r.selector) { | |
| 98 | + for (const b of embeddedJson(html)) { const v = jsonPath(b.data, r.jsonPath); if (v != null) { value = v; method = `embedded-json:${b.id}.${r.jsonPath}`; break; } } | |
| 99 | + } else if (r.selector) { | |
| 100 | + const sel = scope ? scope.find(r.selector) : $(r.selector); | |
| 101 | + if (r.all) value = sel.map((_, el) => (r.attr ? $(el).attr(r.attr) : $(el).text())).get().map((x) => cleanText(x)).filter(Boolean); | |
| 102 | + else value = r.attr ? sel.first().attr(r.attr) ?? null : cleanText(sel.first().text()); | |
| 103 | + method = `selector:${r.selector}${r.attr ? "@" + r.attr : ""}`; | |
| 104 | + } else if (r.regex) { | |
| 105 | + value = htmlToText(html); | |
| 106 | + method = "regex"; | |
| 107 | + } | |
| 108 | + if (r.regex && value != null) { | |
| 109 | + const re = new RegExp(r.regex, r.regexFlags ?? "i"); | |
| 110 | + const apply = (s: string) => { const m = s.match(re); return m ? (m[r.group ?? 1] ?? m[0]) : null; }; | |
| 111 | + value = Array.isArray(value) ? value.map((x) => apply(String(x))).filter(Boolean) : apply(String(value)); | |
| 112 | + method += `+regex:${r.regex.slice(0, 40)}`; | |
| 113 | + } | |
| 114 | + if (r.transform && value != null) { value = Array.isArray(value) ? value.map((x) => applyTransform(x, r.transform)) : applyTransform(value, r.transform); method += `>${r.transform}`; } | |
| 115 | + if ((value == null || value === "") && r.default !== undefined) value = r.default; | |
| 116 | + return { value, method }; | |
| 117 | +} | |
| 118 | + | |
| 119 | +export function pageTitle(html: string): string | null { | |
| 120 | + const $ = load(html); | |
| 121 | + return cleanText($('meta[property="og:title"]').attr("content") ?? $("title").first().text() ?? $("h1").first().text()); | |
| 122 | +} | |
| 123 | + | |
| 124 | +export function metaDescription(html: string): string | null { | |
| 125 | + const $ = load(html); | |
| 126 | + return cleanText($('meta[name="description"]').attr("content") ?? $('meta[property="og:description"]').attr("content") ?? null); | |
| 127 | +} | |
| 128 | + | |
| 129 | +/** Main text of an article-like page (heuristic: largest <article>/<main>/content block). */ | |
| 130 | +export function mainText(html: string): string { | |
| 131 | + const $ = load(html); | |
| 132 | + $("script,style,noscript,nav,header,footer,aside,form,iframe,svg").remove(); | |
| 133 | + const candidates = ["article", "main", '[role="main"]', ".article-body", ".press-release", ".entry-content", ".post-content", ".content", "#content", "body"]; | |
| 134 | + for (const c of candidates) { | |
| 135 | + const el = $(c).first(); | |
| 136 | + if (el.length) { const t = htmlToText(el.html() ?? ""); if (t.length > 300) return t; } | |
| 137 | + } | |
| 138 | + return htmlToText($("body").html() ?? html); | |
| 139 | +} | |
| 140 | + | |
| 141 | +/** Publication date from meta tags / JSON-LD / <time>. */ | |
| 142 | +export function publishedDate(html: string): string | null { | |
| 143 | + const $ = load(html); | |
| 144 | + const cands = [$('meta[property="article:published_time"]').attr("content"), $('meta[name="pubdate"]').attr("content"), $('meta[name="publish-date"]').attr("content"), $('meta[name="date"]').attr("content"), $('meta[itemprop="datePublished"]').attr("content"), $("time[datetime]").first().attr("datetime"), (jsonLd(html, "NewsArticle")[0]?.datePublished as string | undefined), (jsonLd(html, "Article")[0]?.datePublished as string | undefined)]; | |
| 145 | + for (const c of cands) if (c) { const d = parsePartialDate(String(c)); if (d) return d; } | |
| 146 | + return null; | |
| 147 | +} | |
| 148 | + | |
| 149 | +export async function pdfText(buf: Buffer, maxPages = 60): Promise<{ text: string; pages: number; title: string | null }> { | |
| 150 | + const pdf = await getDocumentProxy(new Uint8Array(buf)); | |
| 151 | + const { text, totalPages } = await pdfExtractText(pdf, { mergePages: true }); | |
| 152 | + const meta = await pdf.getMetadata().catch(() => null); | |
| 153 | + const title = (meta?.info as { Title?: string } | undefined)?.Title ?? null; | |
| 154 | + const body = Array.isArray(text) ? text.join("\n") : String(text); | |
| 155 | + return { text: body.split("\f").slice(0, maxPages).join("\n").replace(/[ \t]+/g, " ").trim(), pages: totalPages, title: cleanText(title) }; | |
| 156 | +} | |
| 157 | + | |
| 158 | +export function isPdf(doc: RawDocument): boolean { return /pdf/i.test(doc.contentType ?? "") || doc.body.subarray(0, 5).toString("latin1") === "%PDF-"; } | |
| 159 | +export function isJson(doc: RawDocument): boolean { return /json/i.test(doc.contentType ?? "") || /^\s*[\[{]/.test(doc.text.slice(0, 20)); } | |
| 160 | + | |
| 161 | +/** Loose geo extraction from any HTML: JSON-LD geo, meta geo.position / ICBM, data-lat attributes, Google Maps links. */ | |
| 162 | +export function extractGeo(html: string): { lat: number; lng: number; method: string } | null { | |
| 163 | + for (const o of jsonLd(html)) { | |
| 164 | + const g = (o.geo ?? (o.location as Record<string, unknown> | undefined)?.geo) as { latitude?: unknown; longitude?: unknown } | undefined; | |
| 165 | + if (g && g.latitude != null && g.longitude != null) { const lat = Number(g.latitude), lng = Number(g.longitude); if (Number.isFinite(lat) && Number.isFinite(lng)) return { lat, lng, method: "json-ld:geo" }; } | |
| 166 | + } | |
| 167 | + const $ = load(html); | |
| 168 | + const pos = $('meta[name="geo.position"]').attr("content") ?? $('meta[name="ICBM"]').attr("content"); | |
| 169 | + if (pos) { const m = pos.match(/(-?\d+(?:\.\d+)?)[;,\s]+(-?\d+(?:\.\d+)?)/); if (m) return { lat: Number(m[1]), lng: Number(m[2]), method: "meta:geo.position" }; } | |
| 170 | + const el = $("[data-lat][data-lng], [data-latitude][data-longitude]").first(); | |
| 171 | + if (el.length) { const lat = Number(el.attr("data-lat") ?? el.attr("data-latitude")), lng = Number(el.attr("data-lng") ?? el.attr("data-longitude")); if (Number.isFinite(lat) && Number.isFinite(lng)) return { lat, lng, method: "attr:data-lat" }; } | |
| 172 | + const gm = html.match(/maps\.google\.[a-z.]+\/(?:maps)?\?q=(-?\d+\.\d+),(-?\d+\.\d+)|google\.com\/maps[^"']*?@(-?\d+\.\d+),(-?\d+\.\d+)|!3d(-?\d+\.\d+)!4d(-?\d+\.\d+)/); | |
| 173 | + if (gm) { const lat = Number(gm[1] ?? gm[3] ?? gm[5]), lng = Number(gm[2] ?? gm[4] ?? gm[6]); if (Number.isFinite(lat) && Number.isFinite(lng)) return { lat, lng, method: "link:google-maps" }; } | |
| 174 | + return null; | |
| 175 | +} | |
| 176 | + | |
| 177 | +/** Postal address from JSON-LD (PostalAddress) on any page. */ | |
| 178 | +export function extractAddress(html: string): { street: string | null; city: string | null; region: string | null; postal: string | null; country: string | null } | null { | |
| 179 | + for (const o of jsonLd(html)) { | |
| 180 | + const a = (o.address ?? (o.location as Record<string, unknown> | undefined)?.address) as Record<string, unknown> | string | undefined; | |
| 181 | + if (!a) continue; | |
| 182 | + if (typeof a === "string") return { street: cleanText(a), city: null, region: null, postal: null, country: countryFromText(a) }; | |
| 183 | + return { street: cleanText(String(a.streetAddress ?? "")), city: cleanText(String(a.addressLocality ?? "")), region: cleanText(String(a.addressRegion ?? "")), postal: cleanText(String(a.postalCode ?? "")), country: a.addressCountry ? (typeof a.addressCountry === "string" ? (/^[A-Z]{2}$/.test(a.addressCountry) ? a.addressCountry : countryFromText(a.addressCountry)) : countryFromText(String((a.addressCountry as Record<string, unknown>).name ?? ""))) : null }; | |
| 184 | + } | |
| 185 | + return null; | |
| 186 | +} | |
added
packages/connectors/src/fetchers.ts
+230 −0
@@ -0,0 +1,230 @@ | ||
| 1 | +import { Agent, fetch as undiciFetch, type Dispatcher } from "undici"; | |
| 2 | +import { gunzipSync } from "node:zlib"; | |
| 3 | +import { assertUrlAllowed, safeLookup, UrlPolicyError } from "@dci/core"; | |
| 4 | +import type { FetchLevel } from "@dci/core"; | |
| 5 | +import type { Fetcher, FetchOptions, RawDocument } from "./types.js"; | |
| 6 | + | |
| 7 | +/** | |
| 8 | + * Fetch abstraction with escalation levels: | |
| 9 | + * 1 — direct HTTP (conditional GET, ETag / Last-Modified) | |
| 10 | + * 2 — direct HTTP + parsing hints (browser UA, accept html) [same transport, different identity] | |
| 11 | + * 3 — Firecrawl (markdown + html normalization) | |
| 12 | + * 4 — Scrapfly rendered request (anti-bot + JS rendering) | |
| 13 | + * Every URL passes the SSRF policy before any network activity, on every redirect hop. | |
| 14 | + */ | |
| 15 | + | |
| 16 | +export const DEFAULT_UA = process.env.DCI_USER_AGENT ?? "DataCenterIndexBot/0.1 (+https://www.datacenterindex.io/bot; contact@spboucher.ai)"; | |
| 17 | +export const BROWSER_UA = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.5 Safari/605.1.15"; | |
| 18 | +const DEFAULT_TIMEOUT = 30_000; | |
| 19 | +const DEFAULT_MAX_BYTES = 25 * 1024 * 1024; | |
| 20 | + | |
| 21 | +let agent: Dispatcher | null = null; | |
| 22 | +let agentH1: Dispatcher | null = null; | |
| 23 | +const h1Hosts = new Set<string>(); | |
| 24 | +function dispatcher(h1 = false): Dispatcher { | |
| 25 | + if (h1) return (agentH1 ??= new Agent({ connect: { lookup: safeLookup as never, timeout: 10_000 }, connections: 32, pipelining: 1, keepAliveTimeout: 15_000, headersTimeout: 30_000, bodyTimeout: 45_000, allowH2: false })); | |
| 26 | + return (agent ??= new Agent({ connect: { lookup: safeLookup as never, timeout: 10_000 }, connections: 64, pipelining: 1, keepAliveTimeout: 15_000, headersTimeout: 30_000, bodyTimeout: 45_000, allowH2: true })); | |
| 27 | +} | |
| 28 | +export async function closeFetchers(): Promise<void> { | |
| 29 | + await agent?.close(); | |
| 30 | + await agentH1?.close(); | |
| 31 | + agent = agentH1 = null; | |
| 32 | +} | |
| 33 | + | |
| 34 | +function hdrs(h: Headers): Record<string, string> { | |
| 35 | + const out: Record<string, string> = {}; | |
| 36 | + h.forEach((v, k) => { if (/^(content-type|content-length|etag|last-modified|cache-control|server|retry-after|date|x-robots-tag|link|location|content-disposition|x-ratelimit-[a-z-]+)$/i.test(k)) out[k.toLowerCase()] = v; }); | |
| 37 | + return out; | |
| 38 | +} | |
| 39 | + | |
| 40 | +function decodeText(body: Buffer, contentType: string | null): string { | |
| 41 | + const m = contentType?.match(/charset=([\w-]+)/i); | |
| 42 | + const cs = (m?.[1] ?? "utf-8").toLowerCase(); | |
| 43 | + try { | |
| 44 | + if (cs === "utf-8" || cs === "utf8") return body.toString("utf8"); | |
| 45 | + return new TextDecoder(cs as string).decode(body); | |
| 46 | + } catch { | |
| 47 | + return body.toString("utf8"); | |
| 48 | + } | |
| 49 | +} | |
| 50 | + | |
| 51 | +export function failDoc(url: string, level: FetchLevel, fetcher: RawDocument["fetcher"], code: string, message: string, started: number, status = 0): RawDocument { | |
| 52 | + return { url, finalUrl: url, fetchedAt: new Date().toISOString(), status, contentType: null, body: Buffer.alloc(0), text: "", headers: {}, etag: null, lastModified: null, notModified: false, fetcher, level, durationMs: Date.now() - started, credits: 0, error: { code, message } }; | |
| 53 | +} | |
| 54 | + | |
| 55 | +export class DirectFetcher implements Fetcher { | |
| 56 | + readonly name = "direct" as const; | |
| 57 | + constructor(readonly level: FetchLevel = 1) {} | |
| 58 | + available(): boolean { return true; } | |
| 59 | + async fetch(urlStr: string, opts: FetchOptions = {}): Promise<RawDocument> { | |
| 60 | + const started = Date.now(); | |
| 61 | + const level = opts.level ?? this.level; | |
| 62 | + const headers: Record<string, string> = { | |
| 63 | + "user-agent": level >= 2 ? BROWSER_UA : DEFAULT_UA, | |
| 64 | + accept: opts.accept ?? "text/html,application/xhtml+xml,application/xml;q=0.9,application/json;q=0.9,application/pdf;q=0.8,*/*;q=0.5", | |
| 65 | + "accept-language": "en-US,en;q=0.9,fr;q=0.6,de;q=0.4", | |
| 66 | + "accept-encoding": "gzip, deflate, br", | |
| 67 | + ...(level >= 2 ? { "sec-fetch-dest": "document", "sec-fetch-mode": "navigate", "sec-fetch-site": "none", "upgrade-insecure-requests": "1" } : {}), | |
| 68 | + ...opts.headers, | |
| 69 | + }; | |
| 70 | + if (opts.etag) headers["if-none-match"] = opts.etag; | |
| 71 | + if (opts.lastModified) headers["if-modified-since"] = opts.lastModified; | |
| 72 | + let current = urlStr; | |
| 73 | + let redirects = 0; | |
| 74 | + for (;;) { | |
| 75 | + try { await assertUrlAllowed(current); } catch (e) { return failDoc(urlStr, level, "direct", "ssrf_blocked", e instanceof UrlPolicyError ? e.message : String(e), started); } | |
| 76 | + const ac = new AbortController(); | |
| 77 | + const timer = setTimeout(() => ac.abort(), opts.timeoutMs ?? DEFAULT_TIMEOUT); | |
| 78 | + let res: Response; | |
| 79 | + const host = new URL(current).hostname; | |
| 80 | + try { | |
| 81 | + res = (await undiciFetch(current, { method: "GET", headers, redirect: "manual", signal: ac.signal, dispatcher: dispatcher(h1Hosts.has(host)) } as never)) as unknown as Response; | |
| 82 | + } catch (e) { | |
| 83 | + clearTimeout(timer); | |
| 84 | + const msg = e instanceof Error ? `${e.name}: ${e.message}${(e as { cause?: Error }).cause ? " — " + String((e as { cause?: Error }).cause?.message) : ""}` : String(e); | |
| 85 | + if (/NGHTTP2|HTTP\/2/i.test(msg) && !h1Hosts.has(host)) { h1Hosts.add(host); continue; } | |
| 86 | + const code = ac.signal.aborted ? "timeout" : /ENOTFOUND|EAI_AGAIN|getaddrinfo/i.test(msg) ? "dns" : /blocked/i.test(msg) ? "ssrf_blocked" : /CERT|TLS|SSL|certificate/i.test(msg) ? "tls" : /ECONNREFUSED|ECONNRESET|EHOSTUNREACH|ETIMEDOUT|socket/i.test(msg) ? "connection" : "fetch_failed"; | |
| 87 | + return failDoc(urlStr, level, "direct", code, msg, started); | |
| 88 | + } | |
| 89 | + if ([301, 302, 303, 307, 308].includes(res.status)) { | |
| 90 | + clearTimeout(timer); | |
| 91 | + const loc = res.headers.get("location"); | |
| 92 | + if (!loc) return failDoc(urlStr, level, "direct", "redirect_without_location", `HTTP ${res.status}`, started, res.status); | |
| 93 | + if (++redirects > 6) return failDoc(urlStr, level, "direct", "too_many_redirects", "more than 6 redirects", started, res.status); | |
| 94 | + try { current = new URL(loc, current).toString(); } catch { return failDoc(urlStr, level, "direct", "bad_redirect", loc, started, res.status); } | |
| 95 | + delete headers["if-none-match"]; delete headers["if-modified-since"]; | |
| 96 | + continue; | |
| 97 | + } | |
| 98 | + const h = hdrs(res.headers); | |
| 99 | + const base = { url: urlStr, finalUrl: current, fetchedAt: new Date().toISOString(), status: res.status, contentType: res.headers.get("content-type"), headers: h, etag: res.headers.get("etag"), lastModified: res.headers.get("last-modified"), fetcher: "direct" as const, level, credits: 0 }; | |
| 100 | + if (res.status === 304) { clearTimeout(timer); return { ...base, body: Buffer.alloc(0), text: "", notModified: true, durationMs: Date.now() - started }; } | |
| 101 | + const maxBytes = opts.maxBytes ?? DEFAULT_MAX_BYTES; | |
| 102 | + const declared = Number(res.headers.get("content-length") ?? 0); | |
| 103 | + if (declared > maxBytes) { clearTimeout(timer); return failDoc(urlStr, level, "direct", "too_large", `content-length ${declared}`, started, res.status); } | |
| 104 | + const chunks: Uint8Array[] = []; | |
| 105 | + let total = 0; | |
| 106 | + try { | |
| 107 | + const reader = res.body?.getReader(); | |
| 108 | + if (reader) for (;;) { const { done, value } = await reader.read(); if (done) break; total += value.byteLength; if (total > maxBytes) { await reader.cancel(); clearTimeout(timer); return failDoc(urlStr, level, "direct", "too_large", `body > ${maxBytes}`, started, res.status); } chunks.push(value); } | |
| 109 | + } catch (e) { | |
| 110 | + clearTimeout(timer); | |
| 111 | + return failDoc(urlStr, level, "direct", ac.signal.aborted ? "timeout" : "body_read_failed", (e as Error).message, started, res.status); | |
| 112 | + } | |
| 113 | + clearTimeout(timer); | |
| 114 | + let body = Buffer.concat(chunks); | |
| 115 | + if (/\.gz$/i.test(current) || (h["content-type"] ?? "").includes("gzip")) { try { body = gunzipSync(body); } catch { /* not gzip */ } } | |
| 116 | + const ct = base.contentType; | |
| 117 | + const isText = !ct || /text|json|xml|javascript|html|csv|markdown/i.test(ct); | |
| 118 | + return { ...base, body, text: isText ? decodeText(body, ct) : "", notModified: false, durationMs: Date.now() - started }; | |
| 119 | + } | |
| 120 | + } | |
| 121 | +} | |
| 122 | + | |
| 123 | +/** Daily budget bookkeeping shared by premium fetchers. */ | |
| 124 | +class Budget { | |
| 125 | + private day = ""; | |
| 126 | + private used = 0; | |
| 127 | + constructor(private readonly envKey: string, private readonly fallback: number) {} | |
| 128 | + private roll() { const d = new Date().toISOString().slice(0, 10); if (d !== this.day) { this.day = d; this.used = 0; } } | |
| 129 | + limit(): number { return Number(process.env[this.envKey] ?? this.fallback); } | |
| 130 | + remaining(): number { this.roll(); return Math.max(0, this.limit() - this.used); } | |
| 131 | + spend(n = 1) { this.roll(); this.used += n; } | |
| 132 | + snapshot() { this.roll(); return { day: this.day, used: this.used, limit: this.limit() }; } | |
| 133 | +} | |
| 134 | +export const scrapflyBudget = new Budget("DCI_SCRAPFLY_DAILY_BUDGET", 400); | |
| 135 | +export const firecrawlBudget = new Budget("DCI_FIRECRAWL_DAILY_BUDGET", 200); | |
| 136 | + | |
| 137 | +export class FirecrawlFetcher implements Fetcher { | |
| 138 | + readonly name = "firecrawl" as const; | |
| 139 | + readonly level: FetchLevel = 3; | |
| 140 | + available(): boolean { return Boolean(process.env.FIRECRAWL_API_KEY) && firecrawlBudget.remaining() > 0; } | |
| 141 | + async fetch(url: string, opts: FetchOptions = {}): Promise<RawDocument> { | |
| 142 | + const started = Date.now(); | |
| 143 | + if (!this.available()) return failDoc(url, 3, "firecrawl", "firecrawl_unavailable", "no key or daily budget exhausted", started); | |
| 144 | + try { await assertUrlAllowed(url); } catch (e) { return failDoc(url, 3, "firecrawl", "ssrf_blocked", (e as Error).message, started); } | |
| 145 | + firecrawlBudget.spend(1); | |
| 146 | + const ac = new AbortController(); | |
| 147 | + const timer = setTimeout(() => ac.abort(), opts.timeoutMs ?? 90_000); | |
| 148 | + try { | |
| 149 | + const res = await fetch("https://api.firecrawl.dev/v1/scrape", { method: "POST", signal: ac.signal, headers: { "content-type": "application/json", authorization: `Bearer ${process.env.FIRECRAWL_API_KEY}` }, body: JSON.stringify({ url, formats: ["markdown", "html"], onlyMainContent: false, waitFor: opts.waitForSelector ? 3000 : 0 }) }); | |
| 150 | + const json = (await res.json()) as { success?: boolean; data?: { markdown?: string; html?: string; metadata?: { statusCode?: number; sourceURL?: string; url?: string; title?: string } }; error?: string }; | |
| 151 | + if (!res.ok || !json.success || !json.data) return failDoc(url, 3, "firecrawl", "firecrawl_error", `${res.status} ${json.error ?? "no data"}`, started); | |
| 152 | + const html = json.data.html ?? ""; | |
| 153 | + const body = Buffer.from(html || json.data.markdown || "", "utf8"); | |
| 154 | + return { url, finalUrl: json.data.metadata?.url ?? json.data.metadata?.sourceURL ?? url, fetchedAt: new Date().toISOString(), status: json.data.metadata?.statusCode ?? 200, contentType: html ? "text/html; charset=utf-8" : "text/markdown", body, text: body.toString("utf8"), headers: {}, etag: null, lastModified: null, notModified: false, fetcher: "firecrawl", level: 3, durationMs: Date.now() - started, credits: 1, markdown: json.data.markdown ?? null }; | |
| 155 | + } catch (e) { | |
| 156 | + return failDoc(url, 3, "firecrawl", ac.signal.aborted ? "timeout" : "firecrawl_error", (e as Error).message, started); | |
| 157 | + } finally { clearTimeout(timer); } | |
| 158 | + } | |
| 159 | +} | |
| 160 | + | |
| 161 | +/** Scrapfly: https://scrapfly.io/docs/scrape-api/getting-started — GET /scrape?key&url&asp&render_js&country&cache */ | |
| 162 | +export class ScrapflyFetcher implements Fetcher { | |
| 163 | + readonly name = "scrapfly" as const; | |
| 164 | + readonly level: FetchLevel = 4; | |
| 165 | + available(): boolean { return Boolean(process.env.SCRAPFLY_API_KEY) && scrapflyBudget.remaining() > 0; } | |
| 166 | + async fetch(url: string, opts: FetchOptions = {}): Promise<RawDocument> { | |
| 167 | + const started = Date.now(); | |
| 168 | + if (!this.available()) return failDoc(url, 4, "scrapfly", "scrapfly_unavailable", "no key or daily budget exhausted", started); | |
| 169 | + try { await assertUrlAllowed(url); } catch (e) { return failDoc(url, 4, "scrapfly", "ssrf_blocked", (e as Error).message, started); } | |
| 170 | + const q = new URLSearchParams({ key: process.env.SCRAPFLY_API_KEY!, url, asp: "true", render_js: opts.renderJs === false ? "false" : "true", country: opts.country ?? "us", retry: "true", cache: "true", cache_ttl: "3600" }); | |
| 171 | + if (opts.waitForSelector) q.set("wait_for_selector", opts.waitForSelector); | |
| 172 | + if (opts.renderJs !== false) q.set("rendering_wait", "1500"); | |
| 173 | + const ac = new AbortController(); | |
| 174 | + const timer = setTimeout(() => ac.abort(), opts.timeoutMs ?? 120_000); | |
| 175 | + try { | |
| 176 | + const res = await fetch(`https://api.scrapfly.io/scrape?${q}`, { signal: ac.signal, headers: { accept: "application/json" } }); | |
| 177 | + const json = (await res.json()) as { result?: { content?: string; status_code?: number; success?: boolean; reason?: string; response_headers?: Record<string, string>; url?: string; error?: { message?: string }; format?: string }; context?: { cost?: { total?: number } }; message?: string; code?: string }; | |
| 178 | + const r = json.result; | |
| 179 | + const cost = Number(json.context?.cost?.total ?? 1) || 1; | |
| 180 | + scrapflyBudget.spend(cost); | |
| 181 | + if (!res.ok || !r) return failDoc(url, 4, "scrapfly", "scrapfly_error", `${res.status} ${json.message ?? json.code ?? "no result"}`, started); | |
| 182 | + if (!r.success) return { ...failDoc(url, 4, "scrapfly", "scrapfly_failed", `${r.status_code ?? 0} ${r.reason ?? r.error?.message ?? "upstream failed"}`, started, r.status_code ?? 0), credits: cost }; | |
| 183 | + const h = Object.fromEntries(Object.entries(r.response_headers ?? {}).map(([k, v]) => [k.toLowerCase(), String(v)])); | |
| 184 | + const body = Buffer.from(r.content ?? "", "utf8"); | |
| 185 | + return { url, finalUrl: r.url ?? url, fetchedAt: new Date().toISOString(), status: r.status_code ?? 200, contentType: h["content-type"] ?? "text/html; charset=utf-8", body, text: body.toString("utf8"), headers: h, etag: null, lastModified: h["last-modified"] ?? null, notModified: false, fetcher: "scrapfly", level: 4, durationMs: Date.now() - started, credits: cost }; | |
| 186 | + } catch (e) { | |
| 187 | + return failDoc(url, 4, "scrapfly", ac.signal.aborted ? "timeout" : "scrapfly_error", (e as Error).message, started); | |
| 188 | + } finally { clearTimeout(timer); } | |
| 189 | + } | |
| 190 | +} | |
| 191 | + | |
| 192 | +export const fetchers: Record<FetchLevel, Fetcher> = { 1: new DirectFetcher(1), 2: new DirectFetcher(2), 3: new FirecrawlFetcher(), 4: new ScrapflyFetcher() }; | |
| 193 | + | |
| 194 | +/** Heuristics: a 200 that is really an anti-bot interstitial or an empty JS shell. */ | |
| 195 | +export function looksBlockedOrEmpty(doc: RawDocument): boolean { | |
| 196 | + if (doc.status === 403 || doc.status === 429 || doc.status === 503 || doc.status === 401) return true; | |
| 197 | + if (doc.status !== 200 || !doc.contentType || !/html/i.test(doc.contentType)) return false; | |
| 198 | + const t = doc.text; | |
| 199 | + if (/(Just a moment|Attention Required|cf-browser-verification|_Incapsula_Resource|Access Denied|Request unsuccessful|challenge-platform|Please enable JavaScript to view|Enable JavaScript and cookies to continue|Pardon Our Interruption|are you a robot|verify you are human|Reference #\d+\.[0-9a-f]+)/i.test(t)) return true; | |
| 200 | + const textLen = t.replace(/<script[\s\S]*?<\/script>/gi, "").replace(/<[^>]+>/g, " ").replace(/\s+/g, " ").trim().length; | |
| 201 | + return textLen < 400 && /<(div id="(root|app|__next|__nuxt)"|app-root|noscript)/i.test(t); | |
| 202 | +} | |
| 203 | + | |
| 204 | +/** | |
| 205 | + * Escalating fetch: start at `level`, escalate up to `maxLevel` when blocked/empty. Premium fetchers are | |
| 206 | + * skipped when unavailable. Returns the best document obtained (never throws). | |
| 207 | + */ | |
| 208 | +export async function fetchWithEscalation(url: string, opts: FetchOptions & { maxLevel?: FetchLevel } = {}): Promise<RawDocument> { | |
| 209 | + let level: FetchLevel = opts.level ?? 1; | |
| 210 | + const max: FetchLevel = opts.maxLevel ?? 2; | |
| 211 | + let best: RawDocument | null = null; | |
| 212 | + const attempts: string[] = []; | |
| 213 | + while (level <= max) { | |
| 214 | + const f = fetchers[level]; | |
| 215 | + if (!f.available()) { attempts.push(`L${level}:unavailable`); level = (level + 1) as FetchLevel; continue; } | |
| 216 | + const doc = await f.fetch(url, { ...opts, level }); | |
| 217 | + doc.meta = { ...(doc.meta ?? {}), attempts: [...attempts, `L${level}:${doc.error?.code ?? doc.status}`] }; | |
| 218 | + if (doc.notModified) return doc; | |
| 219 | + if (!doc.error && !looksBlockedOrEmpty(doc)) return doc; | |
| 220 | + // hard failures that escalation cannot fix | |
| 221 | + if (doc.error && ["ssrf_blocked", "dns", "too_large"].includes(doc.error.code)) return doc; | |
| 222 | + if (!doc.error && doc.status === 404) return doc; | |
| 223 | + if (!doc.error && doc.status === 410) return doc; | |
| 224 | + best = best ?? doc; | |
| 225 | + if (!doc.error && doc.status >= 200 && doc.status < 300) best = doc; // blocked-looking 200 still better than nothing | |
| 226 | + attempts.push(`L${level}:${doc.error?.code ?? doc.status}`); | |
| 227 | + level = (level + 1) as FetchLevel; | |
| 228 | + } | |
| 229 | + return best ?? failDoc(url, max, "direct", "no_fetcher", attempts.join(","), Date.now()); | |
| 230 | +} | |
added
packages/connectors/src/generic.ts
+189 −0
@@ -0,0 +1,189 @@ | ||
| 1 | +import { load } from "cheerio"; | |
| 2 | +import type { NormalizedEntity, NormalizedFacility, NormalizedNewsEvent, NormalizedProject, ValidationReport, ValidationIssue, PageType, FacilityStatus, FacilityType } from "@dci/core"; | |
| 3 | +import { parseAllMw, parseMoney, parsePartialDate, validLatLng, inferStatus, inferFacilityType, countryFromText, cleanText, sha256, urlFingerprint } from "@dci/core"; | |
| 4 | +import type { ConnectorConfig, ExtractorConfig } from "./config.js"; | |
| 5 | +import type { Connector, ConnectorContext, DiscoveredUrl, ExtractedRecord, RawDocument } from "./types.js"; | |
| 6 | +import { discoverGeneric } from "./discovery.js"; | |
| 7 | +import { evalRule, pageTitle, mainText, publishedDate, extractGeo, extractAddress, pdfText, isPdf, metaDescription } from "./extract.js"; | |
| 8 | +import { classifyPage, isDataCenterRelevant } from "./classify.js"; | |
| 9 | +import { getParser } from "./registry.js"; | |
| 10 | + | |
| 11 | +/** | |
| 12 | + * Generic, config-driven connector. Covers: sitemap/RSS/seed discovery, escalating fetch, declarative | |
| 13 | + * field extraction (selectors / JSON-LD / embedded JSON / regex), code-backed parsers, article | |
| 14 | + * classification for newsrooms, PDF text, normalization and validation. Source-specific connectors | |
| 15 | + * either extend this via YAML or register an `implementation`. | |
| 16 | + */ | |
| 17 | +export class GenericConnector implements Connector { | |
| 18 | + id: string; sourceName: string; sourceDomain: string; sourceKind: Connector["sourceKind"]; type: Connector["type"]; parserVersion: string; schedule: Connector["schedule"]; license: string | null; attribution: string | null; priority: number; | |
| 19 | + constructor(readonly cfg: ConnectorConfig) { | |
| 20 | + this.id = cfg.id; this.sourceName = cfg.name; this.sourceDomain = cfg.domain; this.sourceKind = cfg.kind; this.type = cfg.mode; this.parserVersion = cfg.parserVersion; this.schedule = cfg.schedule; this.license = cfg.license ?? null; this.attribution = cfg.attribution ?? null; this.priority = cfg.priority; | |
| 21 | + } | |
| 22 | + | |
| 23 | + async discover(ctx: ConnectorContext): Promise<DiscoveredUrl[]> { return discoverGeneric(this.cfg, ctx); } | |
| 24 | + | |
| 25 | + async fetch(ctx: ConnectorContext, u: DiscoveredUrl): Promise<RawDocument> { | |
| 26 | + const doc = await ctx.fetch(u.url, { group: u.group, level: u.minLevel, accept: u.pageType === "planning_document" ? "application/pdf,text/html,*/*" : undefined }); | |
| 27 | + doc.group = u.group; doc.pageType = u.pageType; doc.meta = { ...(doc.meta ?? {}), ...(u.meta ?? {}) }; | |
| 28 | + return doc; | |
| 29 | + } | |
| 30 | + | |
| 31 | + /** Pick extractors whose `match` regex hits the URL (or whose pageTypes include the doc's page type). */ | |
| 32 | + selectExtractors(doc: RawDocument, pageType: PageType): Array<[string, ExtractorConfig]> { | |
| 33 | + const out: Array<[string, ExtractorConfig]> = []; | |
| 34 | + for (const [name, ex] of Object.entries(this.cfg.extractors)) { | |
| 35 | + if (ex.match?.length && !ex.match.some((m) => new RegExp(m, "i").test(doc.finalUrl) || new RegExp(m, "i").test(doc.url))) continue; | |
| 36 | + if (ex.pageTypes?.length && !ex.pageTypes.includes(pageType)) continue; | |
| 37 | + out.push([name, ex]); | |
| 38 | + } | |
| 39 | + return out; | |
| 40 | + } | |
| 41 | + | |
| 42 | + async extract(ctx: ConnectorContext, doc: RawDocument): Promise<ExtractedRecord[]> { | |
| 43 | + if (doc.error || doc.notModified || doc.status >= 400 || !doc.body.length) return []; | |
| 44 | + let html = doc.text; | |
| 45 | + let text = ""; | |
| 46 | + let title: string | null = null; | |
| 47 | + if (isPdf(doc)) { | |
| 48 | + try { const p = await pdfText(doc.body); text = p.text; title = p.title ?? doc.url.split("/").pop() ?? null; html = ""; } catch (e) { ctx.log("warn", `pdf parse failed ${doc.url}: ${(e as Error).message}`); return []; } | |
| 49 | + } else if (/html|xml/.test(doc.contentType ?? "") || /<html/i.test(html.slice(0, 2000))) { | |
| 50 | + title = pageTitle(html); | |
| 51 | + text = mainText(html); | |
| 52 | + } else if (doc.markdown) { text = doc.markdown; } | |
| 53 | + else text = doc.text; | |
| 54 | + const cls = classifyPage(doc.finalUrl, title, text); | |
| 55 | + const pageType: PageType = doc.pageType && doc.pageType !== "unknown" && !["press_release", "news_index"].includes(doc.pageType) ? doc.pageType : cls.pageType; | |
| 56 | + doc.meta = { ...(doc.meta ?? {}), title, pageType, classifier: cls.rule, textLength: text.length }; | |
| 57 | + const records: ExtractedRecord[] = []; | |
| 58 | + const extractors = this.selectExtractors(doc, pageType); | |
| 59 | + for (const [name, ex] of extractors) { | |
| 60 | + if (ex.parser) { | |
| 61 | + const parser = getParser(ex.parser); | |
| 62 | + const recs = await parser.parse(doc, ctx, ex.params); | |
| 63 | + for (const r of recs) records.push({ ...r, pageType: r.pageType ?? pageType }); | |
| 64 | + continue; | |
| 65 | + } | |
| 66 | + if (ex.fields && html) { | |
| 67 | + const $ = load(html); | |
| 68 | + const scopes = ex.each ? $(ex.each).toArray().map((el) => $(el)) : [undefined]; | |
| 69 | + for (const scope of scopes) { | |
| 70 | + const data: Record<string, unknown> = {}; | |
| 71 | + const methods: Record<string, string> = {}; | |
| 72 | + for (const [field, rule] of Object.entries(ex.fields)) { | |
| 73 | + const { value, method } = evalRule($, html, rule, scope); | |
| 74 | + if (value != null && value !== "") { data[field] = value; methods[field] = method; } | |
| 75 | + } | |
| 76 | + if (!Object.keys(data).length) continue; | |
| 77 | + const key = ex.key ? ex.key.replace(/\{(\w+)\}/g, (_, k: string) => (k === "url" ? doc.finalUrl : String(data[k] ?? ""))) : `${this.id}:${urlFingerprint(doc.finalUrl)}`; | |
| 78 | + const certainty = Math.min(1, Object.keys(data).length / Math.max(3, Object.keys(ex.fields).length * 0.6)); | |
| 79 | + if (ex.minCertainty && certainty < ex.minCertainty) continue; | |
| 80 | + records.push({ kind: ex.kind, key, data: { ...data, _title: title, _url: doc.finalUrl, _extractor: name }, methods, certainty, url: doc.finalUrl, pageType }); | |
| 81 | + } | |
| 82 | + } | |
| 83 | + } | |
| 84 | + // Newsroom / announcement pages with no dedicated extractor: produce a news_event record when relevant. | |
| 85 | + if (!records.length && ["press_release", "project_announcement", "acquisition", "expansion", "closure", "construction_update", "planning_document", "power_infrastructure", "cloud_region", "financial_disclosure"].includes(pageType) && isDataCenterRelevant(`${title ?? ""} ${text.slice(0, 5000)}`)) { | |
| 86 | + const summary = cleanText((doc.meta?.summary as string | undefined) ?? metaDescription(html) ?? text.slice(0, 400)); | |
| 87 | + const published = (doc.meta?.published as string | undefined) ? parsePartialDate(String(doc.meta?.published)) : html ? publishedDate(html) : null; | |
| 88 | + const money = parseMoney(`${title ?? ""} ${text.slice(0, 3000)}`); | |
| 89 | + records.push({ kind: "news_event", key: `${this.id}:news:${urlFingerprint(doc.finalUrl)}`, data: { title: title ?? doc.finalUrl, url: doc.finalUrl, publishedAt: published, summary, pageType, eventType: cls.eventType, mw: cls.mw, status: cls.status, money, text: text.slice(0, 20_000) }, methods: { title: "html:title", publishedAt: "meta:published", mw: "regex:mw_v1" }, certainty: 0.6, url: doc.finalUrl, pageType }); | |
| 90 | + } | |
| 91 | + return records; | |
| 92 | + } | |
| 93 | + | |
| 94 | + async normalize(ctx: ConnectorContext, records: ExtractedRecord[]): Promise<NormalizedEntity[]> { | |
| 95 | + const out: NormalizedEntity[] = []; | |
| 96 | + const d = this.cfg.defaults ?? {}; | |
| 97 | + for (const r of records) { | |
| 98 | + const prov = ctx.provenance(r.url, { method: r.methods ? Object.values(r.methods).slice(0, 3).join(",") : undefined, extractorVersion: this.parserVersion }); | |
| 99 | + const x = r.data; | |
| 100 | + const str = (k: string): string | null => { const v = x[k]; return v == null ? null : cleanText(String(v)); }; | |
| 101 | + const num = (k: string): number | null => { const v = x[k]; if (v == null || v === "") return null; const n = typeof v === "number" ? v : Number(String(v).replace(/[^\d.-]/g, "")); return Number.isFinite(n) ? n : null; }; | |
| 102 | + if (r.kind === "facility") { | |
| 103 | + const lat = num("lat"), lng = num("lng"); | |
| 104 | + const geo = validLatLng(lat, lng) ? { lat: lat!, lng: lng!, precision: (str("geoPrecision") as NormalizedFacility["geo"] extends infer G ? (G extends { precision: infer P } ? P : never) : never) ?? "exact", source: `operator:${this.id}` } : null; | |
| 105 | + const name = str("name") ?? str("_title"); | |
| 106 | + if (!name) continue; | |
| 107 | + const f: NormalizedFacility = { | |
| 108 | + entityType: "facility", key: r.key, name, aliases: Array.isArray(x.aliases) ? (x.aliases as string[]) : [], | |
| 109 | + operatorName: str("operatorName") ?? d.operatorName ?? null, campusName: str("campusName"), address: str("address"), city: str("city"), regionName: str("regionName"), | |
| 110 | + countryIso2: (str("countryIso2") ?? (str("country") ? (/^[A-Z]{2}$/.test(str("country")!) ? str("country") : countryFromText(str("country"))) : null) ?? d.countryIso2 ?? null) as string | null, | |
| 111 | + postalCode: str("postalCode"), geo, | |
| 112 | + status: ((str("status") as FacilityStatus | null) ?? (d.status as FacilityStatus | undefined) ?? inferStatus(str("statusText")) ?? "operational"), | |
| 113 | + facilityType: ((str("facilityType") as FacilityType | null) ?? (d.facilityType as FacilityType | undefined) ?? inferFacilityType(`${name} ${str("description") ?? ""}`) ?? "colocation"), | |
| 114 | + tier: str("tier"), buildingSqm: num("buildingSqm"), siteAreaHa: num("siteAreaHa"), itCapacityMw: num("itCapacityMw"), totalPowerMw: num("totalPowerMw"), plannedPowerMw: num("plannedPowerMw"), rackCount: num("rackCount"), pue: num("pue"), | |
| 115 | + coolingType: str("coolingType"), renewableClaim: str("renewableClaim"), openedOn: str("openedOn") ? parsePartialDate(str("openedOn")) : null, constructionStartedOn: str("constructionStartedOn") ? parsePartialDate(str("constructionStartedOn")) : null, announcedOn: str("announcedOn") ? parsePartialDate(str("announcedOn")) : null, | |
| 116 | + website: str("website") ?? r.url, isAi: typeof x.isAi === "boolean" ? x.isAi : null, isHyperscale: typeof x.isHyperscale === "boolean" ? x.isHyperscale : d.isHyperscale ?? null, | |
| 117 | + certifications: Array.isArray(x.certifications) ? (x.certifications as string[]) : [], carriers: Array.isArray(x.carriers) ? (x.carriers as string[]) : [], cloudProviders: Array.isArray(x.cloudProviders) ? (x.cloudProviders as string[]) : [], ixps: Array.isArray(x.ixps) ? (x.ixps as string[]) : [], | |
| 118 | + externalIds: (x.externalIds as Record<string, string | number> | undefined) ?? {}, description: str("description"), provenance: prov, | |
| 119 | + }; | |
| 120 | + // when no explicit geo but page has JSON-LD/meta geo, the extractor may already have set lat/lng; otherwise leave null (never fake coordinates) | |
| 121 | + out.push(f); | |
| 122 | + } else if (r.kind === "news_event") { | |
| 123 | + const n: NormalizedNewsEvent = { entityType: "news_event", key: r.key, title: str("title") ?? r.url, url: (str("url") ?? r.url)!, publishedAt: str("publishedAt"), summary: str("summary"), pageType: (str("pageType") as PageType | null) ?? r.pageType, eventType: (str("eventType") as NormalizedNewsEvent["eventType"]) ?? undefined, mentions: { mw: Array.isArray(x.mw) ? (x.mw as number[]) : [], operators: d.operatorName ? [d.operatorName] : [], countriesIso2: [countryFromText(`${str("title")} ${str("summary")}`) ?? ""].filter(Boolean) }, provenance: prov }; | |
| 124 | + out.push(n); | |
| 125 | + // Project candidate: announcement with MW figure + status keyword | |
| 126 | + const mw = (Array.isArray(x.mw) ? (x.mw as number[]) : parseAllMw(`${n.title} ${n.summary ?? ""}`))[0]; | |
| 127 | + const st = str("status") as FacilityStatus | null; | |
| 128 | + if (mw && st && ["announced", "proposed", "permitting", "approved", "under_construction", "expansion", "rumored", "delayed"].includes(st) && ["project_announcement", "expansion", "construction_update", "planning_document"].includes(n.pageType ?? "")) { | |
| 129 | + const money = x.money as { amount: number; currency: string } | null; | |
| 130 | + const p: NormalizedProject = { entityType: "project", key: `${r.key}:project`, name: n.title.slice(0, 160), operatorName: d.operatorName ?? null, countryIso2: n.mentions?.countriesIso2?.[0] ?? d.countryIso2 ?? null, status: st, announcedOn: n.publishedAt, plannedMw: mw, investmentUsd: money && money.currency === "USD" ? money.amount : null, description: n.summary, sourceUrl: n.url, timeline: n.publishedAt ? [{ date: n.publishedAt, type: st === "under_construction" ? "construction_started" : "project_announced", description: n.title, url: n.url }] : [], provenance: { ...prov, confidence: "moderate", method: "regex:announcement_v1" } }; | |
| 131 | + out.push(p); | |
| 132 | + } | |
| 133 | + } else if (r.kind === "project") { | |
| 134 | + const name = str("name") ?? str("_title"); if (!name) continue; | |
| 135 | + const lat = num("lat"), lng = num("lng"); | |
| 136 | + out.push({ entityType: "project", key: r.key, name, operatorName: str("operatorName") ?? d.operatorName ?? null, city: str("city"), regionName: str("regionName"), countryIso2: str("countryIso2") ?? d.countryIso2 ?? null, geo: validLatLng(lat, lng) ? { lat: lat!, lng: lng!, precision: "approximate", source: this.id } : null, status: (str("status") as FacilityStatus | null) ?? "announced", announcedOn: str("announcedOn"), expectedOpening: str("expectedOpening"), plannedMw: num("plannedMw"), investmentUsd: num("investmentUsd"), acreage: num("acreage"), phaseCount: num("phaseCount"), description: str("description"), sourceUrl: r.url, provenance: prov }); | |
| 137 | + } else if (r.kind === "operator") { | |
| 138 | + const name = str("name"); if (!name) continue; | |
| 139 | + out.push({ entityType: "operator", key: r.key, name, website: str("website"), kind: (str("kind") as NormalizedEntity extends { kind?: infer K } ? K : never) ?? null, hqCountryIso2: str("hqCountryIso2"), description: str("description"), provenance: prov }); | |
| 140 | + } else if (r.kind === "cloud_region") { | |
| 141 | + const code = str("code"), name = str("name"); if (!code || !name) continue; | |
| 142 | + const lat = num("lat"), lng = num("lng"); | |
| 143 | + out.push({ entityType: "cloud_region", key: r.key, providerName: str("providerName") ?? d.providerName ?? this.sourceName, code, name, city: str("city"), regionName: str("regionName"), countryIso2: str("countryIso2") ?? (str("country") ? countryFromText(str("country")) : null), geo: validLatLng(lat, lng) ? { lat: lat!, lng: lng!, precision: (str("geoPrecision") as "city" | "exact" | null) ?? "city", source: this.id } : null, availabilityZones: num("availabilityZones"), launchedOn: str("launchedOn") ? parsePartialDate(str("launchedOn")) : null, status: (str("status") as "announced" | "operational" | null) ?? "operational", isSovereign: Boolean(x.isSovereign), sourceUrl: r.url, provenance: prov }); | |
| 144 | + } | |
| 145 | + } | |
| 146 | + return out; | |
| 147 | + } | |
| 148 | + | |
| 149 | + async validate(_ctx: ConnectorContext, entities: NormalizedEntity[]): Promise<ValidationReport> { return validateEntities(entities); } | |
| 150 | +} | |
| 151 | + | |
| 152 | +/** Shared validation: hard errors reject the record; warnings are recorded. Never accept impossible figures. */ | |
| 153 | +export function validateEntities(entities: NormalizedEntity[]): ValidationReport { | |
| 154 | + const issues: ValidationIssue[] = []; | |
| 155 | + let rejected = 0; | |
| 156 | + for (const e of entities) { | |
| 157 | + const errs: ValidationIssue[] = []; | |
| 158 | + const warn = (field: string, message: string) => issues.push({ key: e.key, field, level: "warn", message }); | |
| 159 | + const err = (field: string, message: string) => errs.push({ key: e.key, field, level: "error", message }); | |
| 160 | + if (e.entityType === "facility") { | |
| 161 | + if (!e.name || e.name.length < 2 || e.name.length > 200) err("name", "missing or implausible name"); | |
| 162 | + if (e.geo && !validLatLng(e.geo.lat, e.geo.lng)) err("geo", "invalid coordinates"); | |
| 163 | + for (const f of ["itCapacityMw", "totalPowerMw", "plannedPowerMw"] as const) { const v = e[f]; if (v != null && (v <= 0 || v > 10_000)) err(f, `implausible MW ${v}`); } | |
| 164 | + if (e.itCapacityMw != null && e.totalPowerMw != null && e.itCapacityMw > e.totalPowerMw * 1.05) warn("itCapacityMw", "IT capacity exceeds total power"); | |
| 165 | + if (e.pue != null && (e.pue < 1 || e.pue > 3)) err("pue", `implausible PUE ${e.pue}`); | |
| 166 | + if (e.buildingSqm != null && (e.buildingSqm < 20 || e.buildingSqm > 2_000_000)) err("buildingSqm", "implausible building size"); | |
| 167 | + if (e.countryIso2 && !/^[A-Z]{2}$/.test(e.countryIso2)) err("countryIso2", "bad country code"); | |
| 168 | + if (!e.countryIso2 && !e.geo) warn("countryIso2", "no country and no coordinates"); | |
| 169 | + if (e.openedOn && Number(e.openedOn.slice(0, 4)) < 1950) err("openedOn", "opening year before 1950"); | |
| 170 | + } else if (e.entityType === "project") { | |
| 171 | + if (!e.name) err("name", "missing name"); | |
| 172 | + if (e.plannedMw != null && (e.plannedMw <= 0 || e.plannedMw > 20_000)) err("plannedMw", `implausible MW ${e.plannedMw}`); | |
| 173 | + if (e.investmentUsd != null && (e.investmentUsd < 1e5 || e.investmentUsd > 1e12)) warn("investmentUsd", "investment outside plausible range"); | |
| 174 | + if (!e.countryIso2 && !e.geo) warn("countryIso2", "no location"); | |
| 175 | + } else if (e.entityType === "cloud_region") { | |
| 176 | + if (!e.code || !e.name) err("code", "missing code/name"); | |
| 177 | + if (e.geo && !validLatLng(e.geo.lat, e.geo.lng)) err("geo", "invalid coordinates"); | |
| 178 | + } else if (e.entityType === "news_event") { | |
| 179 | + if (!/^https?:\/\//.test(e.url)) err("url", "bad url"); | |
| 180 | + if (!e.title) err("title", "missing title"); | |
| 181 | + } | |
| 182 | + if (errs.length) { rejected++; issues.push(...errs); } | |
| 183 | + } | |
| 184 | + return { total: entities.length, valid: entities.length - rejected, rejected, issues }; | |
| 185 | +} | |
| 186 | + | |
| 187 | +export function entityIsValid(report: ValidationReport, key: string): boolean { return !report.issues.some((i) => i.key === key && i.level === "error"); } | |
| 188 | + | |
| 189 | +export function newsKey(connectorId: string, url: string): string { return `${connectorId}:news:${sha256(url).slice(0, 24)}`; } | |
added
packages/connectors/src/index.ts
+10 −0
@@ -0,0 +1,10 @@ | ||
| 1 | +export * from "./types.js"; | |
| 2 | +export * from "./config.js"; | |
| 3 | +export * from "./fetchers.js"; | |
| 4 | +export * from "./robots.js"; | |
| 5 | +export * from "./ratelimit.js"; | |
| 6 | +export * from "./discovery.js"; | |
| 7 | +export * from "./extract.js"; | |
| 8 | +export * from "./classify.js"; | |
| 9 | +export * from "./registry.js"; | |
| 10 | +export * from "./generic.js"; | |
added
packages/connectors/src/ratelimit.ts
+30 −0
@@ -0,0 +1,30 @@ | ||
| 1 | +/** Per-host token bucket + concurrency gate. In-process; the worker runs one process per node so this is sufficient. */ | |
| 2 | +interface Bucket { tokens: number; last: number; rpm: number; inflight: number; concurrency: number; waiters: Array<() => void> } | |
| 3 | +const buckets = new Map<string, Bucket>(); | |
| 4 | + | |
| 5 | +export function configureHost(host: string, rpm: number, concurrency: number): void { | |
| 6 | + const b = buckets.get(host) ?? { tokens: rpm, last: Date.now(), rpm, inflight: 0, concurrency, waiters: [] }; | |
| 7 | + b.rpm = rpm; b.concurrency = concurrency; | |
| 8 | + buckets.set(host, b); | |
| 9 | +} | |
| 10 | + | |
| 11 | +export async function acquire(host: string, minDelayMs = 0): Promise<() => void> { | |
| 12 | + const b = buckets.get(host) ?? (buckets.set(host, { tokens: 30, last: Date.now(), rpm: 30, inflight: 0, concurrency: 2, waiters: [] }), buckets.get(host)!); | |
| 13 | + for (;;) { | |
| 14 | + const now = Date.now(); | |
| 15 | + b.tokens = Math.min(b.rpm, b.tokens + ((now - b.last) / 60_000) * b.rpm); | |
| 16 | + b.last = now; | |
| 17 | + if (b.tokens >= 1 && b.inflight < b.concurrency) { b.tokens -= 1; b.inflight += 1; break; } | |
| 18 | + const wait = b.tokens < 1 ? Math.ceil(((1 - b.tokens) / b.rpm) * 60_000) : 50; | |
| 19 | + await new Promise((r) => setTimeout(r, Math.max(25, Math.min(wait, 5000), minDelayMs))); | |
| 20 | + } | |
| 21 | + if (minDelayMs > 0) await new Promise((r) => setTimeout(r, minDelayMs)); | |
| 22 | + let released = false; | |
| 23 | + return () => { if (!released) { released = true; b.inflight -= 1; } }; | |
| 24 | +} | |
| 25 | + | |
| 26 | +export function hostStats(): Record<string, { rpm: number; inflight: number; tokens: number }> { | |
| 27 | + const out: Record<string, { rpm: number; inflight: number; tokens: number }> = {}; | |
| 28 | + for (const [h, b] of buckets) out[h] = { rpm: b.rpm, inflight: b.inflight, tokens: Math.round(b.tokens) }; | |
| 29 | + return out; | |
| 30 | +} | |
added
packages/connectors/src/registry.ts
+15 −0
@@ -0,0 +1,15 @@ | ||
| 1 | +import type { Connector, Parser } from "./types.js"; | |
| 2 | +import type { ConnectorConfig } from "./config.js"; | |
| 3 | + | |
| 4 | +/** Code-backed parsers (referenced by name from YAML `extractors.<x>.parser`). */ | |
| 5 | +const parsers = new Map<string, Parser>(); | |
| 6 | +export function registerParser(p: Parser): void { if (parsers.has(p.name)) throw new Error(`parser ${p.name} already registered`); parsers.set(p.name, p); } | |
| 7 | +export function getParser(name: string): Parser { const p = parsers.get(name); if (!p) throw new Error(`unknown parser ${name}`); return p; } | |
| 8 | +export function listParsers(): Parser[] { return [...parsers.values()]; } | |
| 9 | + | |
| 10 | +/** Code-backed connector implementations (referenced from YAML `implementation:`), built from the config. */ | |
| 11 | +type Factory = (cfg: ConnectorConfig) => Connector; | |
| 12 | +const implementations = new Map<string, Factory>(); | |
| 13 | +export function registerImplementation(name: string, f: Factory): void { implementations.set(name, f); } | |
| 14 | +export function getImplementation(name: string): Factory | undefined { return implementations.get(name); } | |
| 15 | +export function listImplementations(): string[] { return [...implementations.keys()]; } | |
added
packages/connectors/src/robots.ts
+65 −0
@@ -0,0 +1,65 @@ | ||
| 1 | +import { DirectFetcher } from "./fetchers.js"; | |
| 2 | + | |
| 3 | +/** Minimal robots.txt evaluator (User-agent groups, Allow/Disallow longest-match, Crawl-delay, Sitemap). Cached per host for 24 h. */ | |
| 4 | +interface Rules { allow: string[]; disallow: string[]; crawlDelay: number | null; sitemaps: string[]; fetchedAt: number } | |
| 5 | +const cache = new Map<string, Rules>(); | |
| 6 | +const TTL = 24 * 3_600_000; | |
| 7 | + | |
| 8 | +export function parseRobots(text: string, ua = "datacenterindexbot"): Rules { | |
| 9 | + const lines = text.split(/\r?\n/).map((l) => l.replace(/#.*/, "").trim()).filter(Boolean); | |
| 10 | + const groups: Array<{ agents: string[]; allow: string[]; disallow: string[]; crawlDelay: number | null }> = []; | |
| 11 | + const sitemaps: string[] = []; | |
| 12 | + let cur: (typeof groups)[number] | null = null; | |
| 13 | + let lastWasAgent = false; | |
| 14 | + for (const line of lines) { | |
| 15 | + const i = line.indexOf(":"); | |
| 16 | + if (i < 0) continue; | |
| 17 | + const k = line.slice(0, i).trim().toLowerCase(); | |
| 18 | + const v = line.slice(i + 1).trim(); | |
| 19 | + if (k === "user-agent") { | |
| 20 | + if (!cur || !lastWasAgent) { cur = { agents: [], allow: [], disallow: [], crawlDelay: null }; groups.push(cur); } | |
| 21 | + cur.agents.push(v.toLowerCase()); | |
| 22 | + lastWasAgent = true; | |
| 23 | + continue; | |
| 24 | + } | |
| 25 | + lastWasAgent = false; | |
| 26 | + if (k === "sitemap") { sitemaps.push(v); continue; } | |
| 27 | + if (!cur) continue; | |
| 28 | + if (k === "allow") cur.allow.push(v); | |
| 29 | + else if (k === "disallow") cur.disallow.push(v); | |
| 30 | + else if (k === "crawl-delay") { const d = Number(v); if (Number.isFinite(d)) cur.crawlDelay = d; } | |
| 31 | + } | |
| 32 | + const mine = groups.find((g) => g.agents.some((a) => a !== "*" && ua.includes(a))) ?? groups.find((g) => g.agents.includes("*")); | |
| 33 | + return { allow: mine?.allow ?? [], disallow: mine?.disallow ?? [], crawlDelay: mine?.crawlDelay ?? null, sitemaps, fetchedAt: Date.now() }; | |
| 34 | +} | |
| 35 | + | |
| 36 | +function matchLen(pattern: string, path: string): number { | |
| 37 | + if (!pattern) return -1; | |
| 38 | + const re = new RegExp("^" + pattern.split("*").map((s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join(".*") + (pattern.endsWith("$") ? "" : "")); | |
| 39 | + return re.test(path) ? pattern.length : -1; | |
| 40 | +} | |
| 41 | + | |
| 42 | +export function robotsAllows(rules: Rules, url: string): boolean { | |
| 43 | + const u = new URL(url); | |
| 44 | + const path = u.pathname + u.search; | |
| 45 | + let bestAllow = -1, bestDis = -1; | |
| 46 | + for (const p of rules.allow) bestAllow = Math.max(bestAllow, matchLen(p, path)); | |
| 47 | + for (const p of rules.disallow) bestDis = Math.max(bestDis, matchLen(p, path)); | |
| 48 | + if (bestDis < 0) return true; | |
| 49 | + return bestAllow >= bestDis; | |
| 50 | +} | |
| 51 | + | |
| 52 | +export async function getRobots(url: string): Promise<Rules> { | |
| 53 | + const origin = new URL(url).origin; | |
| 54 | + const hit = cache.get(origin); | |
| 55 | + if (hit && Date.now() - hit.fetchedAt < TTL) return hit; | |
| 56 | + const doc = await new DirectFetcher(1).fetch(`${origin}/robots.txt`, { timeoutMs: 15_000, maxBytes: 512 * 1024, accept: "text/plain,*/*" }); | |
| 57 | + const rules = doc.status === 200 && doc.text ? parseRobots(doc.text) : { allow: [], disallow: [], crawlDelay: null, sitemaps: [], fetchedAt: Date.now() }; | |
| 58 | + cache.set(origin, rules); | |
| 59 | + return rules; | |
| 60 | +} | |
| 61 | + | |
| 62 | +export async function isAllowedByRobots(url: string): Promise<{ allowed: boolean; crawlDelay: number | null; sitemaps: string[] }> { | |
| 63 | + const rules = await getRobots(url); | |
| 64 | + return { allowed: robotsAllows(rules, url), crawlDelay: rules.crawlDelay, sitemaps: rules.sitemaps }; | |
| 65 | +} | |
added
packages/connectors/src/types.ts
+129 −0
@@ -0,0 +1,129 @@ | ||
| 1 | +import type { FetchLevel, NormalizedEntity, PageType, SourceKind, ValidationReport, Provenance } from "@dci/core"; | |
| 2 | + | |
| 3 | +/** A URL found by discovery, with a hint about what it is and how urgently it should be crawled. */ | |
| 4 | +export interface DiscoveredUrl { | |
| 5 | + url: string; | |
| 6 | + pageType?: PageType; | |
| 7 | + /** logical group in the connector schedule: "facility_pages", "newsroom", "regions", "dataset", … */ | |
| 8 | + group: string; | |
| 9 | + priority?: number; // 0..100 (higher = sooner) | |
| 10 | + lastmod?: string | null; | |
| 11 | + discoveredFrom?: string | null; | |
| 12 | + /** minimum fetch level (a connector may know a page needs rendering) */ | |
| 13 | + minLevel?: FetchLevel; | |
| 14 | + meta?: Record<string, unknown>; | |
| 15 | +} | |
| 16 | + | |
| 17 | +export interface RawDocument { | |
| 18 | + url: string; | |
| 19 | + finalUrl: string; | |
| 20 | + fetchedAt: string; | |
| 21 | + status: number; | |
| 22 | + contentType: string | null; | |
| 23 | + body: Buffer; | |
| 24 | + /** best-effort text body (utf-8) — for JSON/HTML/XML/markdown */ | |
| 25 | + text: string; | |
| 26 | + headers: Record<string, string>; | |
| 27 | + etag: string | null; | |
| 28 | + lastModified: string | null; | |
| 29 | + notModified: boolean; | |
| 30 | + fetcher: "direct" | "firecrawl" | "scrapfly" | "cache"; | |
| 31 | + level: FetchLevel; | |
| 32 | + durationMs: number; | |
| 33 | + credits: number; // premium credits spent (Scrapfly/Firecrawl) | |
| 34 | + /** Firecrawl markdown (when used) */ | |
| 35 | + markdown?: string | null; | |
| 36 | + error?: { code: string; message: string } | null; | |
| 37 | + /** hints from discovery */ | |
| 38 | + group?: string; | |
| 39 | + pageType?: PageType; | |
| 40 | + meta?: Record<string, unknown>; | |
| 41 | +} | |
| 42 | + | |
| 43 | +/** Extracted record = one candidate entity as found on one page, before normalization. */ | |
| 44 | +export interface ExtractedRecord { | |
| 45 | + kind: NormalizedEntity["entityType"]; | |
| 46 | + key: string; // connector-scoped stable key | |
| 47 | + data: Record<string, unknown>; | |
| 48 | + /** per-field extraction method for provenance: { it_capacity_mw: "regex:mw_v1", lat: "json-ld:geo" } */ | |
| 49 | + methods?: Record<string, string>; | |
| 50 | + certainty?: number; // 0..1 overall extraction certainty | |
| 51 | + url: string; | |
| 52 | + pageType?: PageType; | |
| 53 | +} | |
| 54 | + | |
| 55 | +export interface FetchOptions { | |
| 56 | + level?: FetchLevel; | |
| 57 | + etag?: string | null; | |
| 58 | + lastModified?: string | null; | |
| 59 | + timeoutMs?: number; | |
| 60 | + maxBytes?: number; | |
| 61 | + accept?: string; | |
| 62 | + headers?: Record<string, string>; | |
| 63 | + renderJs?: boolean; | |
| 64 | + country?: string; | |
| 65 | + /** group in the connector schedule (used for rate limits / cost attribution) */ | |
| 66 | + group?: string; | |
| 67 | + waitForSelector?: string; | |
| 68 | +} | |
| 69 | + | |
| 70 | +export interface Fetcher { | |
| 71 | + readonly name: "direct" | "firecrawl" | "scrapfly"; | |
| 72 | + readonly level: FetchLevel; | |
| 73 | + available(): boolean; | |
| 74 | + fetch(url: string, options?: FetchOptions): Promise<RawDocument>; | |
| 75 | +} | |
| 76 | + | |
| 77 | +export interface ConnectorSchedule { | |
| 78 | + [group: string]: string; // "daily" | "weekly" | "monthly" | "6h" | "12h" | "3h" | cron-like interval string | |
| 79 | +} | |
| 80 | + | |
| 81 | +export interface ConnectorContext { | |
| 82 | + connectorId: string; | |
| 83 | + sourceId: string; | |
| 84 | + runId: string; | |
| 85 | + /** fetch with the connector's rate limit, robots policy, cache and escalation policy applied */ | |
| 86 | + fetch(url: string, options?: FetchOptions): Promise<RawDocument>; | |
| 87 | + /** previously stored state for this connector (cursor, seen keys, etc.) */ | |
| 88 | + getState<T = unknown>(key: string): Promise<T | null>; | |
| 89 | + setState(key: string, value: unknown): Promise<void>; | |
| 90 | + log(level: "debug" | "info" | "warn" | "error", msg: string, extra?: Record<string, unknown>): void; | |
| 91 | + /** true when this URL was fetched before and unchanged since `lastFetched` (used to skip expensive work) */ | |
| 92 | + isKnownUnchanged(url: string, contentHash: string): Promise<boolean>; | |
| 93 | + now(): string; | |
| 94 | + /** default provenance skeleton for this connector (sourceId, connectorId, retrievedAt…) */ | |
| 95 | + provenance(url: string, extra?: Partial<Provenance>): Provenance; | |
| 96 | + env: Record<string, string | undefined>; | |
| 97 | + /** dry run: nothing persisted */ | |
| 98 | + dryRun: boolean; | |
| 99 | +} | |
| 100 | + | |
| 101 | +/** | |
| 102 | + * A connector = one source, one set of discovery + extraction rules. | |
| 103 | + * Implementations are mostly config-driven (YAML) with optional code-backed parsers. | |
| 104 | + */ | |
| 105 | +export interface Connector { | |
| 106 | + id: string; | |
| 107 | + sourceName: string; | |
| 108 | + sourceDomain: string; | |
| 109 | + sourceKind: SourceKind; | |
| 110 | + type: "html" | "sitemap" | "rss" | "pdf" | "json" | "hybrid" | "api" | "dataset"; | |
| 111 | + parserVersion: string; | |
| 112 | + schedule: ConnectorSchedule; | |
| 113 | + license?: string | null; | |
| 114 | + attribution?: string | null; | |
| 115 | + priority: number; // 1 best … 5 | |
| 116 | + discover(ctx: ConnectorContext): Promise<DiscoveredUrl[]>; | |
| 117 | + fetch(ctx: ConnectorContext, url: DiscoveredUrl): Promise<RawDocument>; | |
| 118 | + extract(ctx: ConnectorContext, doc: RawDocument): Promise<ExtractedRecord[]>; | |
| 119 | + normalize(ctx: ConnectorContext, records: ExtractedRecord[]): Promise<NormalizedEntity[]>; | |
| 120 | + validate(ctx: ConnectorContext, entities: NormalizedEntity[]): Promise<ValidationReport>; | |
| 121 | +} | |
| 122 | + | |
| 123 | +/** Code-backed parser: turns a document into extracted records. Registered by name and referenced from YAML. */ | |
| 124 | +export interface Parser { | |
| 125 | + name: string; // e.g. "equinix_facility_v1" | |
| 126 | + version: string; | |
| 127 | + pageTypes?: PageType[]; | |
| 128 | + parse(doc: RawDocument, ctx: ConnectorContext, params?: Record<string, unknown>): Promise<ExtractedRecord[]> | ExtractedRecord[]; | |
| 129 | +} | |
added
packages/connectors/tsconfig.json
+1 −0
@@ -0,0 +1 @@ | ||
| 1 | +{ "extends": "../../tsconfig.base.json", "compilerOptions": { "noEmit": true, "types": ["node"], "lib": ["ES2023", "DOM"] }, "include": ["src"] } | |
added
packages/core/package.json
+12 −0
@@ -0,0 +1,12 @@ | ||
| 1 | +{ | |
| 2 | + "name": "@dci/core", | |
| 3 | + "version": "0.1.0", | |
| 4 | + "private": true, | |
| 5 | + "type": "module", | |
| 6 | + "main": "./src/index.ts", | |
| 7 | + "types": "./src/index.ts", | |
| 8 | + "exports": { ".": "./src/index.ts" }, | |
| 9 | + "scripts": { "typecheck": "tsc --noEmit", "test": "vitest run --passWithNoTests" }, | |
| 10 | + "dependencies": { "zod": "^4.1.0" }, | |
| 11 | + "devDependencies": { "@types/node": "^24.0.0", "typescript": "^5.9.3", "vitest": "^3.2.0" } | |
| 12 | +} | |
added
packages/core/src/api-types.ts
+469 −0
@@ -0,0 +1,469 @@ | ||
| 1 | +/** | |
| 2 | + * Public API contract (v1) shared by @dci/api (producer) and @dci/web (consumer). | |
| 3 | + * Every response is wrapped: { data, meta?, sources? }. Dates are ISO strings; partial dates use "YYYY", "YYYY-MM", "YYYY-Qn". | |
| 4 | + */ | |
| 5 | +import type { ConfidenceLevel, EntityType, EventType, FacilityStatus, FacilityType, GeoPrecision, OperatorKind, ReviewStatus, SourceKind } from "./types.js"; | |
| 6 | + | |
| 7 | +export interface ApiEnvelope<T> { | |
| 8 | + data: T; | |
| 9 | + meta?: { total?: number; page?: number; perPage?: number; generatedAt?: string; methodology?: string; [k: string]: unknown }; | |
| 10 | + sources?: SourceRef[]; | |
| 11 | +} | |
| 12 | + | |
| 13 | +export interface SourceRef { | |
| 14 | + id: string; | |
| 15 | + name: string; | |
| 16 | + domain: string; | |
| 17 | + kind: SourceKind; | |
| 18 | + url?: string | null; | |
| 19 | + license?: string | null; | |
| 20 | + attribution?: string | null; | |
| 21 | +} | |
| 22 | + | |
| 23 | +export interface ProvenanceDTO { | |
| 24 | + field: string; | |
| 25 | + value: unknown; | |
| 26 | + sourceId: string; | |
| 27 | + sourceName: string; | |
| 28 | + sourceKind: SourceKind; | |
| 29 | + url: string; | |
| 30 | + firstObserved: string; | |
| 31 | + lastObserved: string; | |
| 32 | + retrievedAt: string; | |
| 33 | + confidence: ConfidenceLevel; | |
| 34 | + isEstimate: boolean; | |
| 35 | + method?: string | null; | |
| 36 | +} | |
| 37 | + | |
| 38 | +export interface OperatorSummary { | |
| 39 | + id: string; | |
| 40 | + slug: string; | |
| 41 | + name: string; | |
| 42 | + kind: OperatorKind | null; | |
| 43 | + website?: string | null; | |
| 44 | + hqCountryIso2?: string | null; | |
| 45 | + facilityCount: number; | |
| 46 | + countryCount: number; | |
| 47 | + metroCount?: number; | |
| 48 | + knownMw?: number | null; | |
| 49 | + plannedMw?: number | null; | |
| 50 | + projectCount?: number; | |
| 51 | +} | |
| 52 | + | |
| 53 | +export interface OperatorDetail extends OperatorSummary { | |
| 54 | + aliases: string[]; | |
| 55 | + description?: string | null; | |
| 56 | + parent?: { id: string; slug: string; name: string } | null; | |
| 57 | + countries: Array<{ iso2: string; name: string; slug: string; facilityCount: number; knownMw: number | null }>; | |
| 58 | + metros: Array<{ id: string; slug: string; name: string; countryIso2: string; facilityCount: number }>; | |
| 59 | + statusBreakdown: Partial<Record<FacilityStatus, number>>; | |
| 60 | + facilities: FacilitySummary[]; | |
| 61 | + projects: ProjectSummary[]; | |
| 62 | + recentEvents: EventDTO[]; | |
| 63 | + sourceHistory: SourceHistoryItem[]; | |
| 64 | + externalIds: Record<string, string | number>; | |
| 65 | + updatedAt: string; | |
| 66 | +} | |
| 67 | + | |
| 68 | +export interface FacilitySummary { | |
| 69 | + id: string; | |
| 70 | + slug: string; | |
| 71 | + name: string; | |
| 72 | + operator: { id: string; slug: string; name: string } | null; | |
| 73 | + city: string | null; | |
| 74 | + regionName: string | null; | |
| 75 | + countryIso2: string | null; | |
| 76 | + countryName?: string | null; | |
| 77 | + metro: { id: string; slug: string; name: string } | null; | |
| 78 | + lat: number | null; | |
| 79 | + lng: number | null; | |
| 80 | + geoPrecision: GeoPrecision; | |
| 81 | + status: FacilityStatus; | |
| 82 | + facilityType: FacilityType; | |
| 83 | + itCapacityMw: number | null; | |
| 84 | + totalPowerMw: number | null; | |
| 85 | + plannedPowerMw: number | null; | |
| 86 | + isAi: boolean; | |
| 87 | + isHyperscale: boolean; | |
| 88 | + confidence: ConfidenceLevel; | |
| 89 | + completeness: number; // 0..100 | |
| 90 | + openedOn: string | null; | |
| 91 | + lastVerified: string | null; | |
| 92 | + carriersCount?: number | null; | |
| 93 | + ixpCount?: number | null; | |
| 94 | + networksCount?: number | null; | |
| 95 | +} | |
| 96 | + | |
| 97 | +export interface FacilityDetail extends FacilitySummary { | |
| 98 | + aliases: string[]; | |
| 99 | + owner: { id: string; slug: string; name: string } | null; | |
| 100 | + campus: { id: string; slug: string; name: string } | null; | |
| 101 | + address: string | null; | |
| 102 | + postalCode: string | null; | |
| 103 | + geoSource: string | null; | |
| 104 | + tier: string | null; | |
| 105 | + buildingSqm: number | null; | |
| 106 | + siteAreaHa: number | null; | |
| 107 | + rackCount: number | null; | |
| 108 | + pue: number | null; | |
| 109 | + coolingType: string | null; | |
| 110 | + renewableClaim: string | null; | |
| 111 | + constructionStartedOn: string | null; | |
| 112 | + announcedOn: string | null; | |
| 113 | + website: string | null; | |
| 114 | + description: string | null; | |
| 115 | + certifications: string[]; | |
| 116 | + carriers: Array<{ id: string; slug: string; name: string; asn?: number | null }>; | |
| 117 | + cloudProviders: Array<{ id: string; slug: string; name: string }>; | |
| 118 | + ixps: Array<{ id: string; slug: string; name: string }>; | |
| 119 | + cloudRegions: CloudRegionSummary[]; | |
| 120 | + nearby: FacilitySummary[]; | |
| 121 | + projects: ProjectSummary[]; | |
| 122 | + provenance: ProvenanceDTO[]; | |
| 123 | + events: EventDTO[]; | |
| 124 | + sourceHistory: SourceHistoryItem[]; | |
| 125 | + externalIds: Record<string, string | number>; | |
| 126 | + firstSeen: string; | |
| 127 | + updatedAt: string; | |
| 128 | +} | |
| 129 | + | |
| 130 | +export interface SourceHistoryItem { | |
| 131 | + date: string; | |
| 132 | + sourceId: string; | |
| 133 | + sourceName: string; | |
| 134 | + sourceKind: SourceKind; | |
| 135 | + url: string; | |
| 136 | + kind: "observed" | "updated" | "detected" | "announced" | "verified" | "changed"; | |
| 137 | + description: string; | |
| 138 | +} | |
| 139 | + | |
| 140 | +export interface CloudRegionSummary { | |
| 141 | + id: string; | |
| 142 | + slug: string; | |
| 143 | + provider: { id: string; slug: string; name: string }; | |
| 144 | + code: string; | |
| 145 | + name: string; | |
| 146 | + city: string | null; | |
| 147 | + countryIso2: string | null; | |
| 148 | + lat: number | null; | |
| 149 | + lng: number | null; | |
| 150 | + geoPrecision: GeoPrecision; | |
| 151 | + availabilityZones: number | null; | |
| 152 | + launchedOn: string | null; | |
| 153 | + status: "announced" | "operational" | "restricted" | "retired"; | |
| 154 | + isSovereign: boolean; | |
| 155 | + sourceUrl: string | null; | |
| 156 | +} | |
| 157 | + | |
| 158 | +export interface ProjectSummary { | |
| 159 | + id: string; | |
| 160 | + slug: string; | |
| 161 | + name: string; | |
| 162 | + operator: { id: string; slug: string; name: string } | null; | |
| 163 | + facility: { id: string; slug: string; name: string } | null; | |
| 164 | + city: string | null; | |
| 165 | + regionName: string | null; | |
| 166 | + countryIso2: string | null; | |
| 167 | + metro: { id: string; slug: string; name: string } | null; | |
| 168 | + lat: number | null; | |
| 169 | + lng: number | null; | |
| 170 | + status: FacilityStatus; | |
| 171 | + announcedOn: string | null; | |
| 172 | + expectedOpening: string | null; | |
| 173 | + plannedMw: number | null; | |
| 174 | + investmentUsd: number | null; | |
| 175 | + acreage: number | null; | |
| 176 | + phaseCount: number | null; | |
| 177 | + confidence: ConfidenceLevel; | |
| 178 | + lastUpdate: string; | |
| 179 | +} | |
| 180 | + | |
| 181 | +export interface ProjectDetail extends ProjectSummary { | |
| 182 | + description: string | null; | |
| 183 | + sourceUrl: string | null; | |
| 184 | + timeline: Array<{ date: string; type: string; description: string; url: string | null; sourceName: string | null }>; | |
| 185 | + statusHistory: Array<{ date: string; from: FacilityStatus | null; to: FacilityStatus; url: string | null }>; | |
| 186 | + provenance: ProvenanceDTO[]; | |
| 187 | + events: EventDTO[]; | |
| 188 | + sourceHistory: SourceHistoryItem[]; | |
| 189 | +} | |
| 190 | + | |
| 191 | +export interface EventDTO { | |
| 192 | + id: string; | |
| 193 | + entityType: EntityType; | |
| 194 | + entityId: string | null; | |
| 195 | + entity?: { slug: string; name: string } | null; | |
| 196 | + eventType: EventType; | |
| 197 | + detectedAt: string; | |
| 198 | + effectiveDate: string | null; | |
| 199 | + oldValue: unknown; | |
| 200 | + newValue: unknown; | |
| 201 | + sourceId: string; | |
| 202 | + sourceName: string; | |
| 203 | + sourceKind: SourceKind; | |
| 204 | + url: string; | |
| 205 | + title: string; | |
| 206 | + summary: string | null; | |
| 207 | + significance: number; | |
| 208 | + confidence: ConfidenceLevel; | |
| 209 | + reviewStatus: ReviewStatus; | |
| 210 | + countryIso2: string | null; | |
| 211 | + operator: { id: string; slug: string; name: string } | null; | |
| 212 | +} | |
| 213 | + | |
| 214 | +export interface CountrySummary { | |
| 215 | + iso2: string; | |
| 216 | + iso3: string | null; | |
| 217 | + slug: string; | |
| 218 | + name: string; | |
| 219 | + region: string | null; | |
| 220 | + subregion: string | null; | |
| 221 | + population: number | null; | |
| 222 | + gdpUsd: number | null; | |
| 223 | + facilityCount: number; | |
| 224 | + operationalCount: number; | |
| 225 | + constructionCount: number; | |
| 226 | + plannedCount: number; | |
| 227 | + knownMw: number | null; | |
| 228 | + constructionMw: number | null; | |
| 229 | + plannedMw: number | null; | |
| 230 | + operatorCount: number; | |
| 231 | + cloudRegionCount: number; | |
| 232 | + hyperscaleCount: number; | |
| 233 | + aiCount: number; | |
| 234 | + projectCount: number; | |
| 235 | + mwCoverage: number; // share of facilities with a known MW figure, 0..1 | |
| 236 | + lat: number | null; | |
| 237 | + lng: number | null; | |
| 238 | +} | |
| 239 | + | |
| 240 | +export interface CountryDetail extends CountrySummary { | |
| 241 | + topOperators: OperatorSummary[]; | |
| 242 | + metros: MetroSummary[]; | |
| 243 | + cloudRegions: CloudRegionSummary[]; | |
| 244 | + recentProjects: ProjectSummary[]; | |
| 245 | + recentEvents: EventDTO[]; | |
| 246 | + growth: Array<{ year: number; facilities: number; knownMw: number | null }>; | |
| 247 | + statusBreakdown: Partial<Record<FacilityStatus, number>>; | |
| 248 | + typeBreakdown: Partial<Record<FacilityType, number>>; | |
| 249 | + announcedInvestmentUsd: number | null; | |
| 250 | + renewableShare: number | null; | |
| 251 | + electricityTwh: number | null; | |
| 252 | + rankings: Array<{ key: string; label: string; rank: number; total: number; value: number | null }>; | |
| 253 | + facilities: FacilitySummary[]; | |
| 254 | +} | |
| 255 | + | |
| 256 | +export interface MetroSummary { | |
| 257 | + id: string; | |
| 258 | + slug: string; | |
| 259 | + name: string; | |
| 260 | + countryIso2: string; | |
| 261 | + countryName?: string; | |
| 262 | + regionName: string | null; | |
| 263 | + lat: number; | |
| 264 | + lng: number; | |
| 265 | + facilityCount: number; | |
| 266 | + operationalCount: number; | |
| 267 | + constructionCount: number; | |
| 268 | + plannedCount: number; | |
| 269 | + knownMw: number | null; | |
| 270 | + constructionMw: number | null; | |
| 271 | + plannedMw: number | null; | |
| 272 | + operatorCount: number; | |
| 273 | + cloudRegionCount: number; | |
| 274 | + ixpCount: number; | |
| 275 | + projectCount: number; | |
| 276 | +} | |
| 277 | + | |
| 278 | +export interface MetroDetail extends MetroSummary { | |
| 279 | + aliases: string[]; | |
| 280 | + description: string | null; | |
| 281 | + operators: OperatorSummary[]; | |
| 282 | + cloudProviders: Array<{ id: string; slug: string; name: string; regionCount: number }>; | |
| 283 | + cloudRegions: CloudRegionSummary[]; | |
| 284 | + ixps: Array<{ id: string; slug: string; name: string; networkCount: number | null }>; | |
| 285 | + facilities: FacilitySummary[]; | |
| 286 | + projects: ProjectSummary[]; | |
| 287 | + recentEvents: EventDTO[]; | |
| 288 | + constraints: Array<{ kind: "power" | "land" | "water" | "moratorium" | "grid"; summary: string; url: string; sourceName: string; date: string | null }>; | |
| 289 | + growth: Array<{ year: number; facilities: number; knownMw: number | null }>; | |
| 290 | + rankings: Array<{ key: string; label: string; rank: number; total: number; value: number | null }>; | |
| 291 | +} | |
| 292 | + | |
| 293 | +export interface IxpSummary { | |
| 294 | + id: string; | |
| 295 | + slug: string; | |
| 296 | + name: string; | |
| 297 | + nameLong: string | null; | |
| 298 | + city: string | null; | |
| 299 | + countryIso2: string | null; | |
| 300 | + website: string | null; | |
| 301 | + networkCount: number | null; | |
| 302 | + facilityCount: number; | |
| 303 | +} | |
| 304 | + | |
| 305 | +export interface MapPoint { | |
| 306 | + id: string; | |
| 307 | + slug: string; | |
| 308 | + n: string; // name | |
| 309 | + o?: string | null; // operator name | |
| 310 | + lat: number; | |
| 311 | + lng: number; | |
| 312 | + s: FacilityStatus; | |
| 313 | + t: FacilityType; | |
| 314 | + mw?: number | null; // best known MW (it, total or planned) | |
| 315 | + p: GeoPrecision; | |
| 316 | + ai?: 1; | |
| 317 | + hs?: 1; | |
| 318 | + c?: string | null; // country iso2 | |
| 319 | +} | |
| 320 | + | |
| 321 | +export interface MapCluster { | |
| 322 | + key: string; | |
| 323 | + lat: number; | |
| 324 | + lng: number; | |
| 325 | + count: number; | |
| 326 | + mw: number | null; | |
| 327 | + statuses: Partial<Record<FacilityStatus, number>>; | |
| 328 | + label?: string | null; // metro/country label at low zoom | |
| 329 | +} | |
| 330 | + | |
| 331 | +export interface MapResponse { | |
| 332 | + zoom: number; | |
| 333 | + mode: "clusters" | "points"; | |
| 334 | + clusters?: MapCluster[]; | |
| 335 | + points?: MapPoint[]; | |
| 336 | + total: number; | |
| 337 | +} | |
| 338 | + | |
| 339 | +export interface SearchHit { | |
| 340 | + type: "facility" | "operator" | "country" | "metro" | "city" | "cloud_region" | "project" | "ixp"; | |
| 341 | + id: string; | |
| 342 | + slug: string; | |
| 343 | + title: string; | |
| 344 | + subtitle: string | null; | |
| 345 | + href: string; | |
| 346 | + score: number; | |
| 347 | + meta?: Record<string, unknown>; | |
| 348 | +} | |
| 349 | + | |
| 350 | +export interface SearchResponse { | |
| 351 | + query: string; | |
| 352 | + interpreted?: { countryIso2?: string; operator?: string; status?: FacilityStatus; minMw?: number; facilityType?: FacilityType; text?: string }; | |
| 353 | + hits: SearchHit[]; | |
| 354 | + facilities?: { total: number; items: FacilitySummary[] }; | |
| 355 | +} | |
| 356 | + | |
| 357 | +export interface RankingRow { | |
| 358 | + rank: number; | |
| 359 | + id: string; | |
| 360 | + slug: string; | |
| 361 | + name: string; | |
| 362 | + href: string; | |
| 363 | + value: number | null; | |
| 364 | + secondary?: number | null; | |
| 365 | + countryIso2?: string | null; | |
| 366 | + coverage?: number | null; | |
| 367 | + delta?: number | null; | |
| 368 | +} | |
| 369 | + | |
| 370 | +export interface Ranking { | |
| 371 | + key: string; | |
| 372 | + label: string; | |
| 373 | + unit: string; | |
| 374 | + scope: "countries" | "metros" | "operators" | "facilities"; | |
| 375 | + methodology: string; | |
| 376 | + computedAt: string; | |
| 377 | + minCoverage?: number; | |
| 378 | + rows: RankingRow[]; | |
| 379 | + total: number; | |
| 380 | +} | |
| 381 | + | |
| 382 | +export interface DashboardStats { | |
| 383 | + facilities: number; | |
| 384 | + operational: number; | |
| 385 | + underConstruction: number; | |
| 386 | + planned: number; | |
| 387 | + knownOperationalMw: number; | |
| 388 | + constructionMw: number; | |
| 389 | + plannedMw: number; | |
| 390 | + mwCoverage: number; | |
| 391 | + countries: number; | |
| 392 | + metros: number; | |
| 393 | + operators: number; | |
| 394 | + cloudRegions: number; | |
| 395 | + ixps: number; | |
| 396 | + projects: number; | |
| 397 | + events24h: number; | |
| 398 | + events7d: number; | |
| 399 | + sources: number; | |
| 400 | + documents: number; | |
| 401 | + lastCrawlAt: string | null; | |
| 402 | + generatedAt: string; | |
| 403 | +} | |
| 404 | + | |
| 405 | +export interface Dashboard { | |
| 406 | + stats: DashboardStats; | |
| 407 | + capacityOverTime: Array<{ year: number; facilities: number; knownMw: number | null; cumulativeMw: number | null }>; | |
| 408 | + projectsByStatus: Array<{ status: FacilityStatus; count: number; mw: number | null }>; | |
| 409 | + topCountries: RankingRow[]; | |
| 410 | + topMetros: RankingRow[]; | |
| 411 | + topOperators: RankingRow[]; | |
| 412 | + typeBreakdown: Array<{ type: FacilityType; count: number }>; | |
| 413 | + pipelineByYear: Array<{ year: string; count: number; mw: number | null }>; | |
| 414 | + aiExpansion: { facilities: number; projects: number; plannedMw: number | null; recent: ProjectSummary[] }; | |
| 415 | + latestEvents: EventDTO[]; | |
| 416 | + newProjects: ProjectSummary[]; | |
| 417 | + recentlyVerified: FacilitySummary[]; | |
| 418 | +} | |
| 419 | + | |
| 420 | +export interface ConnectorHealthDTO { | |
| 421 | + id: string; | |
| 422 | + sourceName: string; | |
| 423 | + domain: string; | |
| 424 | + kind: SourceKind; | |
| 425 | + mode: string; | |
| 426 | + enabled: boolean; | |
| 427 | + health: "ok" | "degraded" | "failing" | "paused" | "never_run"; | |
| 428 | + parserVersion: string; | |
| 429 | + lastRunAt: string | null; | |
| 430 | + nextRunAt: string | null; | |
| 431 | + lastStatus: string | null; | |
| 432 | + discovered: number; | |
| 433 | + fetched: number; | |
| 434 | + changed: number; | |
| 435 | + failed: number; | |
| 436 | + extracted: number; | |
| 437 | + extractionSuccess: number | null; | |
| 438 | + avgResponseMs: number | null; | |
| 439 | + cost: { direct: number; firecrawl: number; scrapfly: number; credits: number }; | |
| 440 | + schedule: Record<string, string>; | |
| 441 | +} | |
| 442 | + | |
| 443 | +export interface FacilityFilters { | |
| 444 | + q?: string; | |
| 445 | + country?: string; // iso2, comma list | |
| 446 | + region?: string; | |
| 447 | + metro?: string; // slug | |
| 448 | + operator?: string; // slug | |
| 449 | + status?: string; // comma list | |
| 450 | + type?: string; // comma list | |
| 451 | + min_mw?: number; | |
| 452 | + max_mw?: number; | |
| 453 | + opened_from?: number; | |
| 454 | + opened_to?: number; | |
| 455 | + planned_from?: number; | |
| 456 | + planned_to?: number; | |
| 457 | + cloud?: string; // provider slug | |
| 458 | + hyperscale?: boolean; | |
| 459 | + colocation?: boolean; | |
| 460 | + ai?: boolean; | |
| 461 | + renewable?: boolean; | |
| 462 | + has_mw?: boolean; | |
| 463 | + has_geo?: boolean; | |
| 464 | + confidence?: string; | |
| 465 | + sort?: "name" | "mw" | "updated" | "opened" | "completeness"; | |
| 466 | + order?: "asc" | "desc"; | |
| 467 | + page?: number; | |
| 468 | + per_page?: number; | |
| 469 | +} | |
added
packages/core/src/confidence.ts
+49 −0
@@ -0,0 +1,49 @@ | ||
| 1 | +import type { ConfidenceLevel, SourceKind } from "./types.js"; | |
| 2 | + | |
| 3 | +export const CONFIDENCE_RANK: Record<ConfidenceLevel, number> = { verified: 5, high: 4, moderate: 3, estimated: 2, unverified: 1 }; | |
| 4 | + | |
| 5 | +/** Base confidence by source category (before adjustments). */ | |
| 6 | +export const SOURCE_KIND_BASE: Record<SourceKind, ConfidenceLevel> = { | |
| 7 | + operator: "high", | |
| 8 | + government: "high", | |
| 9 | + filing: "high", | |
| 10 | + utility: "high", | |
| 11 | + cloud_provider: "high", | |
| 12 | + registry: "high", | |
| 13 | + dataset: "moderate", | |
| 14 | + community: "moderate", | |
| 15 | + secondary: "moderate", | |
| 16 | + news: "moderate", | |
| 17 | +}; | |
| 18 | + | |
| 19 | +export interface ConfidenceInput { | |
| 20 | + sourceKind: SourceKind; | |
| 21 | + sourcePriority?: number; // 1 (best) … 5 | |
| 22 | + extractionCertainty?: number; // 0..1 (structured field = 1, regex on prose = 0.6, llm = 0.4) | |
| 23 | + ageDays?: number; | |
| 24 | + corroborations?: number; // number of independent sources agreeing | |
| 25 | + conflicts?: number; // independent sources disagreeing | |
| 26 | + isEstimate?: boolean; | |
| 27 | +} | |
| 28 | + | |
| 29 | +export function computeConfidence(i: ConfidenceInput): ConfidenceLevel { | |
| 30 | + if (i.isEstimate) return "estimated"; | |
| 31 | + let score = CONFIDENCE_RANK[SOURCE_KIND_BASE[i.sourceKind]]; | |
| 32 | + const cert = i.extractionCertainty ?? 1; | |
| 33 | + if (cert < 0.5) score -= 2; | |
| 34 | + else if (cert < 0.8) score -= 1; | |
| 35 | + if ((i.corroborations ?? 0) >= 2) score += 1; | |
| 36 | + if ((i.conflicts ?? 0) >= 1) score -= 1; | |
| 37 | + if ((i.ageDays ?? 0) > 730) score -= 1; | |
| 38 | + if ((i.sourcePriority ?? 3) >= 5) score -= 1; | |
| 39 | + score = Math.max(1, Math.min(5, score)); | |
| 40 | + // "verified" requires corroboration by ≥2 independent primary-grade sources | |
| 41 | + if (score === 5 && !((i.corroborations ?? 0) >= 2 && cert >= 0.8)) score = 4; | |
| 42 | + return (Object.keys(CONFIDENCE_RANK) as ConfidenceLevel[]).find((k) => CONFIDENCE_RANK[k] === score) ?? "unverified"; | |
| 43 | +} | |
| 44 | + | |
| 45 | +export function bestConfidence(levels: ConfidenceLevel[]): ConfidenceLevel { | |
| 46 | + return levels.reduce<ConfidenceLevel>((best, l) => (CONFIDENCE_RANK[l] > CONFIDENCE_RANK[best] ? l : best), "unverified"); | |
| 47 | +} | |
| 48 | + | |
| 49 | +export const CONFIDENCE_LABEL: Record<ConfidenceLevel, string> = { verified: "Verified", high: "High confidence", moderate: "Moderate confidence", estimated: "Estimated", unverified: "Unverified" }; | |
added
packages/core/src/diff.ts
+17 −0
@@ -0,0 +1,17 @@ | ||
| 1 | +/** Line-based diff (LCS) producing a compact unified-style summary. Good enough for page-change inspection. */ | |
| 2 | +export interface LineDiff { added: string[]; removed: string[]; addedCount: number; removedCount: number; ratio: number } | |
| 3 | + | |
| 4 | +export function diffLines(oldText: string, newText: string, maxLines = 400): LineDiff { | |
| 5 | + const a = oldText.split("\n").map((s) => s.trim()).filter(Boolean); | |
| 6 | + const b = newText.split("\n").map((s) => s.trim()).filter(Boolean); | |
| 7 | + const setA = new Map<string, number>(); | |
| 8 | + const setB = new Map<string, number>(); | |
| 9 | + for (const l of a) setA.set(l, (setA.get(l) ?? 0) + 1); | |
| 10 | + for (const l of b) setB.set(l, (setB.get(l) ?? 0) + 1); | |
| 11 | + const removed: string[] = []; | |
| 12 | + const added: string[] = []; | |
| 13 | + for (const [l, n] of setA) { const m = setB.get(l) ?? 0; for (let i = 0; i < n - m; i++) removed.push(l); } | |
| 14 | + for (const [l, n] of setB) { const m = setA.get(l) ?? 0; for (let i = 0; i < n - m; i++) added.push(l); } | |
| 15 | + const total = Math.max(1, a.length + b.length); | |
| 16 | + return { added: added.slice(0, maxLines), removed: removed.slice(0, maxLines), addedCount: added.length, removedCount: removed.length, ratio: (added.length + removed.length) / total }; | |
| 17 | +} | |
added
packages/core/src/geo.ts
+49 −0
@@ -0,0 +1,49 @@ | ||
| 1 | +import type { GeoPrecision } from "./types.js"; | |
| 2 | + | |
| 3 | +const R = 6371.0088; | |
| 4 | +export function haversineKm(lat1: number, lng1: number, lat2: number, lng2: number): number { | |
| 5 | + const toRad = (d: number) => (d * Math.PI) / 180; | |
| 6 | + const dLat = toRad(lat2 - lat1); | |
| 7 | + const dLng = toRad(lng2 - lng1); | |
| 8 | + const a = Math.sin(dLat / 2) ** 2 + Math.cos(toRad(lat1)) * Math.cos(toRad(lat2)) * Math.sin(dLng / 2) ** 2; | |
| 9 | + return 2 * R * Math.asin(Math.sqrt(a)); | |
| 10 | +} | |
| 11 | + | |
| 12 | +export function validLatLng(lat: unknown, lng: unknown): lat is number { | |
| 13 | + return typeof lat === "number" && typeof lng === "number" && Number.isFinite(lat) && Number.isFinite(lng) && Math.abs(lat) <= 90 && Math.abs(lng) <= 180 && !(lat === 0 && lng === 0); | |
| 14 | +} | |
| 15 | + | |
| 16 | +/** Radius (km) within which two points may be the same facility, depending on the worse precision of the pair. */ | |
| 17 | +export function matchRadiusKm(p: GeoPrecision, q: GeoPrecision): number { | |
| 18 | + const rank: Record<GeoPrecision, number> = { exact: 0.25, parcel: 0.5, street: 1, city: 15, metro: 40, approximate: 25, unknown: 25 }; | |
| 19 | + return Math.max(rank[p], rank[q]); | |
| 20 | +} | |
| 21 | + | |
| 22 | +export const PRECISION_RANK: Record<GeoPrecision, number> = { exact: 6, parcel: 5, street: 4, approximate: 3, city: 2, metro: 1, unknown: 0 }; | |
| 23 | + | |
| 24 | +/** Simple geohash (base32) for bucketing / cluster ids. */ | |
| 25 | +export function geohash(lat: number, lng: number, precision = 6): string { | |
| 26 | + const BASE32 = "0123456789bcdefghjkmnpqrstuvwxyz"; | |
| 27 | + let idx = 0, bit = 0, even = true, hash = ""; | |
| 28 | + let latMin = -90, latMax = 90, lngMin = -180, lngMax = 180; | |
| 29 | + while (hash.length < precision) { | |
| 30 | + if (even) { | |
| 31 | + const mid = (lngMin + lngMax) / 2; | |
| 32 | + if (lng >= mid) { idx = idx * 2 + 1; lngMin = mid; } else { idx *= 2; lngMax = mid; } | |
| 33 | + } else { | |
| 34 | + const mid = (latMin + latMax) / 2; | |
| 35 | + if (lat >= mid) { idx = idx * 2 + 1; latMin = mid; } else { idx *= 2; latMax = mid; } | |
| 36 | + } | |
| 37 | + even = !even; | |
| 38 | + if (++bit === 5) { hash += BASE32[idx]; bit = 0; idx = 0; } | |
| 39 | + } | |
| 40 | + return hash; | |
| 41 | +} | |
| 42 | + | |
| 43 | +/** Grid cell for map aggregation at a given zoom (degrees per cell shrinks with zoom). */ | |
| 44 | +export function gridCell(lat: number, lng: number, zoom: number): { key: string; lat: number; lng: number } { | |
| 45 | + const size = 360 / Math.pow(2, Math.max(0, Math.min(zoom, 12)) + 1); | |
| 46 | + const i = Math.floor((lng + 180) / size); | |
| 47 | + const j = Math.floor((lat + 90) / size); | |
| 48 | + return { key: `${zoom}:${i}:${j}`, lat: j * size - 90 + size / 2, lng: i * size - 180 + size / 2 }; | |
| 49 | +} | |
added
packages/core/src/hash.ts
+65 −0
@@ -0,0 +1,65 @@ | ||
| 1 | +import { createHash } from "node:crypto"; | |
| 2 | + | |
| 3 | +export function sha256(input: string | Uint8Array): string { | |
| 4 | + return createHash("sha256").update(input).digest("hex"); | |
| 5 | +} | |
| 6 | + | |
| 7 | +/** Canonical URL for fingerprinting: lowercase host, no fragment, sorted query, trailing slash trimmed, tracking params dropped. */ | |
| 8 | +export function canonicalUrl(raw: string): string { | |
| 9 | + const u = new URL(raw); | |
| 10 | + u.hash = ""; | |
| 11 | + u.hostname = u.hostname.toLowerCase(); | |
| 12 | + if ((u.protocol === "https:" && u.port === "443") || (u.protocol === "http:" && u.port === "80")) u.port = ""; | |
| 13 | + const drop = /^(utm_|fbclid|gclid|mc_cid|mc_eid|ref$|source$|_ga$|_gl$)/i; | |
| 14 | + const params = [...u.searchParams.entries()].filter(([k]) => !drop.test(k)).sort(([a], [b]) => a.localeCompare(b)); | |
| 15 | + u.search = ""; | |
| 16 | + for (const [k, v] of params) u.searchParams.append(k, v); | |
| 17 | + let s = u.toString(); | |
| 18 | + if (u.pathname !== "/" && s.endsWith("/") && !u.search) s = s.slice(0, -1); | |
| 19 | + return s; | |
| 20 | +} | |
| 21 | + | |
| 22 | +export function urlFingerprint(raw: string): string { | |
| 23 | + return sha256(canonicalUrl(raw)).slice(0, 32); | |
| 24 | +} | |
| 25 | + | |
| 26 | +/** | |
| 27 | + * Content fingerprint that ignores volatile noise: whitespace runs, HTML comments, script/style bodies, | |
| 28 | + * nonces, CSRF tokens, timestamps in attributes. Two fetches of an unchanged page should hash identically. | |
| 29 | + */ | |
| 30 | +export function contentFingerprint(body: string, contentType: string | null | undefined): string { | |
| 31 | + let s = body; | |
| 32 | + if (!contentType || /html/i.test(contentType)) { | |
| 33 | + s = s | |
| 34 | + .replace(/<!--[\s\S]*?-->/g, "") | |
| 35 | + .replace(/<script\b[^>]*>[\s\S]*?<\/script>/gi, "") | |
| 36 | + .replace(/<style\b[^>]*>[\s\S]*?<\/style>/gi, "") | |
| 37 | + .replace(/<noscript\b[^>]*>[\s\S]*?<\/noscript>/gi, "") | |
| 38 | + .replace(/\s(nonce|data-nonce|csrf-token|data-csrf|data-timestamp|data-build|data-reactroot)="[^"]*"/gi, "") | |
| 39 | + .replace(/<meta[^>]+(csrf|nonce|build|generated)[^>]*>/gi, "") | |
| 40 | + .replace(/<input[^>]+type="hidden"[^>]*>/gi, ""); | |
| 41 | + } | |
| 42 | + s = s.replace(/\s+/g, " ").trim(); | |
| 43 | + return sha256(s); | |
| 44 | +} | |
| 45 | + | |
| 46 | +/** Plain-text projection of HTML for diffing and regex extraction. */ | |
| 47 | +export function htmlToText(html: string): string { | |
| 48 | + return html | |
| 49 | + .replace(/<script\b[^>]*>[\s\S]*?<\/script>/gi, " ") | |
| 50 | + .replace(/<style\b[^>]*>[\s\S]*?<\/style>/gi, " ") | |
| 51 | + .replace(/<!--[\s\S]*?-->/g, " ") | |
| 52 | + .replace(/<\/(p|div|li|tr|h[1-6]|section|article|br|table|ul|ol|dd|dt)>/gi, "\n") | |
| 53 | + .replace(/<br\s*\/?>/gi, "\n") | |
| 54 | + .replace(/<[^>]+>/g, " ") | |
| 55 | + .replace(/ /g, " ") | |
| 56 | + .replace(/&/g, "&") | |
| 57 | + .replace(/</g, "<") | |
| 58 | + .replace(/>/g, ">") | |
| 59 | + .replace(/"/g, '"') | |
| 60 | + .replace(/'|'/g, "'") | |
| 61 | + .replace(/&#(\d+);/g, (_, d) => String.fromCodePoint(Number(d))) | |
| 62 | + .replace(/[ \t]+/g, " ") | |
| 63 | + .replace(/\n\s*\n+/g, "\n") | |
| 64 | + .trim(); | |
| 65 | +} | |
added
packages/core/src/ids.ts
+41 −0
@@ -0,0 +1,41 @@ | ||
| 1 | +import { randomBytes, createHash } from "node:crypto"; | |
| 2 | + | |
| 3 | +const ALPHABET = "0123456789abcdefghijkmnpqrstuvwxyz"; // no l/o to avoid confusion | |
| 4 | + | |
| 5 | +export const ID_PREFIXES = { | |
| 6 | + facility: "fac", | |
| 7 | + campus: "cmp", | |
| 8 | + operator: "op", | |
| 9 | + cloud_region: "cr", | |
| 10 | + metro: "met", | |
| 11 | + ixp: "ix", | |
| 12 | + project: "prj", | |
| 13 | + event: "evt", | |
| 14 | + document: "doc", | |
| 15 | + source: "src", | |
| 16 | + news: "nws", | |
| 17 | + run: "run", | |
| 18 | + match: "mtc", | |
| 19 | + provenance: "prv", | |
| 20 | + ranking: "rnk", | |
| 21 | +} as const; | |
| 22 | +export type IdKind = keyof typeof ID_PREFIXES; | |
| 23 | + | |
| 24 | +export function newId(kind: IdKind, len = 12): string { | |
| 25 | + const bytes = randomBytes(len); | |
| 26 | + let out = ""; | |
| 27 | + for (let i = 0; i < len; i++) out += ALPHABET[bytes[i]! % ALPHABET.length]; | |
| 28 | + return `${ID_PREFIXES[kind]}_${out}`; | |
| 29 | +} | |
| 30 | + | |
| 31 | +/** Deterministic id from a stable key (used for documents/provenance so re-runs are idempotent). */ | |
| 32 | +export function stableId(kind: IdKind, key: string, len = 16): string { | |
| 33 | + const h = createHash("sha256").update(key).digest("hex").slice(0, len); | |
| 34 | + return `${ID_PREFIXES[kind]}_${h}`; | |
| 35 | +} | |
| 36 | + | |
| 37 | +export function kindOfId(id: string): IdKind | null { | |
| 38 | + const p = id.split("_")[0]; | |
| 39 | + for (const [k, v] of Object.entries(ID_PREFIXES)) if (v === p) return k as IdKind; | |
| 40 | + return null; | |
| 41 | +} | |
added
packages/core/src/index.ts
+9 −0
@@ -0,0 +1,9 @@ | ||
| 1 | +export * from "./types.js"; | |
| 2 | +export * from "./ids.js"; | |
| 3 | +export * from "./hash.js"; | |
| 4 | +export * from "./normalize.js"; | |
| 5 | +export * from "./geo.js"; | |
| 6 | +export * from "./confidence.js"; | |
| 7 | +export * from "./diff.js"; | |
| 8 | +export * from "./ssrf.js"; | |
| 9 | +export * from "./api-types.js"; | |
added
packages/core/src/normalize.ts
+284 −0
@@ -0,0 +1,284 @@ | ||
| 1 | +import type { FacilityStatus, FacilityType } from "./types.js"; | |
| 2 | + | |
| 3 | +export function slugify(s: string): string { | |
| 4 | + return s | |
| 5 | + .normalize("NFKD") | |
| 6 | + .replace(/[̀-ͯ]/g, "") | |
| 7 | + .toLowerCase() | |
| 8 | + .replace(/&/g, " and ") | |
| 9 | + .replace(/[^a-z0-9]+/g, "-") | |
| 10 | + .replace(/^-+|-+$/g, "") | |
| 11 | + .slice(0, 96); | |
| 12 | +} | |
| 13 | + | |
| 14 | +const NOISE_WORDS = new Set(["data", "center", "centre", "datacenter", "datacentre", "dc", "facility", "campus", "site", "the", "of", "inc", "llc", "ltd", "limited", "corp", "corporation", "gmbh", "sa", "ag", "bv", "nv", "plc", "co", "company", "group", "holdings", "s.a.", "s.a", "srl", "sas", "pty", "kk", "k.k."]); | |
| 15 | + | |
| 16 | +/** Aggressive name normalization for entity resolution: lowercase, strip punctuation, drop noise tokens, sort-independent. */ | |
| 17 | +export function normalizeName(name: string): string { | |
| 18 | + return name | |
| 19 | + .normalize("NFKD") | |
| 20 | + .replace(/[̀-ͯ]/g, "") | |
| 21 | + .toLowerCase() | |
| 22 | + .replace(/\(.*?\)/g, " ") | |
| 23 | + .replace(/[^a-z0-9]+/g, " ") | |
| 24 | + .split(" ") | |
| 25 | + .filter((t) => t && !NOISE_WORDS.has(t)) | |
| 26 | + .join(" ") | |
| 27 | + .trim(); | |
| 28 | +} | |
| 29 | + | |
| 30 | +/** Lighter normalization keeping identifiers like "DC12", "FR5", "AM3". */ | |
| 31 | +export function normalizeNameKeepCodes(name: string): string { | |
| 32 | + return name | |
| 33 | + .normalize("NFKD") | |
| 34 | + .replace(/[̀-ͯ]/g, "") | |
| 35 | + .toLowerCase() | |
| 36 | + .replace(/\b(data\s*cent(er|re)s?|datacent(er|re)s?)\b/g, " ") | |
| 37 | + .replace(/[^a-z0-9]+/g, " ") | |
| 38 | + .replace(/\s+/g, " ") | |
| 39 | + .trim(); | |
| 40 | +} | |
| 41 | + | |
| 42 | +/** Extract facility codes such as DC12, FR5, AM3, LD8, SV1, NY9, TY11, CH1-CH4. */ | |
| 43 | +export function extractFacilityCodes(name: string): string[] { | |
| 44 | + const out = new Set<string>(); | |
| 45 | + for (const m of name.toUpperCase().matchAll(/\b([A-Z]{2,4})\s?-?(\d{1,3}[A-Z]?)\b/g)) out.add(`${m[1]}${m[2]}`); | |
| 46 | + return [...out]; | |
| 47 | +} | |
| 48 | + | |
| 49 | +export function tokenSet(s: string): Set<string> { | |
| 50 | + return new Set(normalizeName(s).split(" ").filter(Boolean)); | |
| 51 | +} | |
| 52 | + | |
| 53 | +export function jaccard(a: Set<string>, b: Set<string>): number { | |
| 54 | + if (!a.size && !b.size) return 1; | |
| 55 | + let inter = 0; | |
| 56 | + for (const t of a) if (b.has(t)) inter++; | |
| 57 | + return inter / (a.size + b.size - inter); | |
| 58 | +} | |
| 59 | + | |
| 60 | +/** Parse a power figure in MW from free text like "36 MW", "36MW", "120 megawatts", "1.2 GW", "36,000 kW". Returns MW. */ | |
| 61 | +export function parseMw(text: string | null | undefined): number | null { | |
| 62 | + if (!text) return null; | |
| 63 | + const s = text.replace(/ /g, " "); | |
| 64 | + const m = s.match(/(\d{1,3}(?:[.,]\d{3})+|\d+(?:[.,]\d+)?)\s*(gigawatts?|gw|megawatts?|mw|kilowatts?|kw)\b/i); | |
| 65 | + if (!m) return null; | |
| 66 | + let num = m[1]!; | |
| 67 | + // "36,000" → 36000 ; "1,2" (fr decimal) → 1.2 ; "1.2" → 1.2 | |
| 68 | + if (/^\d{1,3}(,\d{3})+$/.test(num)) num = num.replace(/,/g, ""); | |
| 69 | + else if (/^\d{1,3}(\.\d{3})+$/.test(num) && !/^\d+\.\d{1,2}$/.test(num)) num = num.replace(/\./g, ""); | |
| 70 | + else num = num.replace(",", "."); | |
| 71 | + const v = Number(num); | |
| 72 | + if (!Number.isFinite(v)) return null; | |
| 73 | + const unit = m[2]!.toLowerCase(); | |
| 74 | + if (unit.startsWith("g")) return round(v * 1000, 3); | |
| 75 | + if (unit.startsWith("k")) return round(v / 1000, 3); | |
| 76 | + return round(v, 3); | |
| 77 | +} | |
| 78 | + | |
| 79 | +/** All MW figures in a text, deduplicated, descending. */ | |
| 80 | +export function parseAllMw(text: string): number[] { | |
| 81 | + const out = new Set<number>(); | |
| 82 | + for (const m of text.matchAll(/(\d{1,3}(?:[.,]\d{3})+|\d+(?:[.,]\d+)?)\s*(gigawatts?|gw|megawatts?|mw)\b/gi)) { | |
| 83 | + const v = parseMw(m[0]); | |
| 84 | + if (v != null && v > 0 && v < 100_000) out.add(v); | |
| 85 | + } | |
| 86 | + return [...out].sort((a, b) => b - a); | |
| 87 | +} | |
| 88 | + | |
| 89 | +/** Square feet / square metres → m². */ | |
| 90 | +export function parseAreaSqm(text: string | null | undefined): number | null { | |
| 91 | + if (!text) return null; | |
| 92 | + const m = text.replace(/ /g, " ").match(/(\d{1,3}(?:[.,]\d{3})+|\d+(?:\.\d+)?)\s*(million\s+)?(sq\.?\s*ft|square\s*feet|sqft|ft²|sq\.?\s*m|square\s*met(?:er|re)s?|sqm|m²|m2)/i); | |
| 93 | + if (!m) return null; | |
| 94 | + let v = Number(m[1]!.replace(/,/g, "").replace(/\.(?=\d{3}\b)/g, "")); | |
| 95 | + if (m[2]) v *= 1_000_000; | |
| 96 | + if (!Number.isFinite(v)) return null; | |
| 97 | + const unit = m[3]!.toLowerCase(); | |
| 98 | + return /ft|feet/.test(unit) ? round(v * 0.09290304, 1) : round(v, 1); | |
| 99 | +} | |
| 100 | + | |
| 101 | +/** Acres / hectares → hectares. */ | |
| 102 | +export function parseAreaHa(text: string | null | undefined): number | null { | |
| 103 | + if (!text) return null; | |
| 104 | + const m = text.match(/(\d{1,3}(?:,\d{3})+|\d+(?:\.\d+)?)\s*(acres?|hectares?|ha)\b/i); | |
| 105 | + if (!m) return null; | |
| 106 | + const v = Number(m[1]!.replace(/,/g, "")); | |
| 107 | + if (!Number.isFinite(v)) return null; | |
| 108 | + return /acre/i.test(m[2]!) ? round(v * 0.40468564, 2) : round(v, 2); | |
| 109 | +} | |
| 110 | + | |
| 111 | +/** Money in USD from "$1.2 billion", "US$450 million", "€800m", "£2bn". Non-USD is returned as-is with currency flag. */ | |
| 112 | +export function parseMoney(text: string | null | undefined): { amount: number; currency: string } | null { | |
| 113 | + if (!text) return null; | |
| 114 | + const m = text.replace(/ /g, " ").match(/(US\$|USD|\$|CA\$|C\$|€|EUR|£|GBP|A\$|AUD|¥|JPY|S\$|SGD|₹|INR|CHF|SEK|NOK|DKK)\s?(\d{1,3}(?:,\d{3})+|\d+(?:\.\d+)?)\s*(trillion|billion|million|thousand|bn|m|k|b|tn)?\b/i); | |
| 115 | + if (!m) return null; | |
| 116 | + let v = Number(m[2]!.replace(/,/g, "")); | |
| 117 | + const mult = (m[3] ?? "").toLowerCase(); | |
| 118 | + if (mult === "billion" || mult === "bn" || mult === "b") v *= 1e9; | |
| 119 | + else if (mult === "million" || mult === "m") v *= 1e6; | |
| 120 | + else if (mult === "thousand" || mult === "k") v *= 1e3; | |
| 121 | + else if (mult === "trillion" || mult === "tn") v *= 1e12; | |
| 122 | + const cur = m[1]!.toUpperCase(); | |
| 123 | + const currency = cur === "$" || cur === "US$" || cur === "USD" ? "USD" : cur === "CA$" || cur === "C$" ? "CAD" : cur === "€" || cur === "EUR" ? "EUR" : cur === "£" || cur === "GBP" ? "GBP" : cur === "A$" || cur === "AUD" ? "AUD" : cur === "¥" || cur === "JPY" ? "JPY" : cur === "S$" || cur === "SGD" ? "SGD" : cur === "₹" || cur === "INR" ? "INR" : cur; | |
| 124 | + return { amount: Math.round(v), currency }; | |
| 125 | +} | |
| 126 | + | |
| 127 | +const MONTHS: Record<string, string> = { jan: "01", feb: "02", mar: "03", apr: "04", may: "05", jun: "06", jul: "07", aug: "08", sep: "09", sept: "09", oct: "10", nov: "11", dec: "12", janvier: "01", février: "02", fevrier: "02", mars: "03", avril: "04", mai: "05", juin: "06", juillet: "07", août: "08", aout: "08", septembre: "09", octobre: "10", novembre: "11", décembre: "12", decembre: "12" }; | |
| 128 | + | |
| 129 | +/** | |
| 130 | + * Parse a (possibly partial) date into ISO-like partial: "2027", "2027-Q2", "2027-06", "2027-06-15", "2027-H1". | |
| 131 | + * Accepts "Q2 2027", "second half of 2027", "mid-2027", "June 2027", "2027-06-15T…", "15 June 2027". | |
| 132 | + */ | |
| 133 | +export function parsePartialDate(text: string | null | undefined): string | null { | |
| 134 | + if (!text) return null; | |
| 135 | + const s = text.trim(); | |
| 136 | + let m = s.match(/^(\d{4})-(\d{2})-(\d{2})/); | |
| 137 | + if (m) return `${m[1]}-${m[2]}-${m[3]}`; | |
| 138 | + m = s.match(/^(\d{4})-(\d{2})$/); | |
| 139 | + if (m) return `${m[1]}-${m[2]}`; | |
| 140 | + m = s.match(/\b(?:Q|quarter\s*)([1-4])\s*(?:of\s*)?(\d{4})\b/i) ?? s.match(/\b(\d{4})\s*-?\s*Q([1-4])\b/i); | |
| 141 | + if (m) { | |
| 142 | + const [q, y] = /^\d{4}/.test(m[1]!) ? [m[2], m[1]] : [m[1], m[2]]; | |
| 143 | + return `${y}-Q${q}`; | |
| 144 | + } | |
| 145 | + m = s.match(/\b(first|second|1st|2nd)\s+half\s+(?:of\s+)?(\d{4})\b/i) ?? s.match(/\bH([12])\s*(\d{4})\b/i); | |
| 146 | + if (m) { | |
| 147 | + const h = /^(first|1st|1)$/i.test(m[1]!) ? 1 : 2; | |
| 148 | + return `${m[2]}-H${h}`; | |
| 149 | + } | |
| 150 | + m = s.match(/\b(\d{1,2})(?:st|nd|rd|th)?\s+([A-Za-zéû]+)\.?\s+(\d{4})\b/); | |
| 151 | + if (m && MONTHS[m[2]!.toLowerCase()]) return `${m[3]}-${MONTHS[m[2]!.toLowerCase()]}-${m[1]!.padStart(2, "0")}`; | |
| 152 | + m = s.match(/\b([A-Za-zéû]+)\.?\s+(\d{1,2})(?:st|nd|rd|th)?,?\s+(\d{4})\b/); | |
| 153 | + if (m && MONTHS[m[1]!.toLowerCase()]) return `${m[3]}-${MONTHS[m[1]!.toLowerCase()]}-${m[2]!.padStart(2, "0")}`; | |
| 154 | + m = s.match(/\b([A-Za-zéû]+)\.?\s+(\d{4})\b/); | |
| 155 | + if (m && MONTHS[m[1]!.toLowerCase()]) return `${m[2]}-${MONTHS[m[1]!.toLowerCase()]}`; | |
| 156 | + m = s.match(/\b(?:mid|early|late|end of|by|in)?-?\s*(20\d{2}|19\d{2})\b/); | |
| 157 | + if (m) return m[1]!; | |
| 158 | + return null; | |
| 159 | +} | |
| 160 | + | |
| 161 | +/** Sortable numeric key for a partial date (for ordering timelines): YYYY.fraction */ | |
| 162 | +export function partialDateSortKey(d: string | null | undefined): number { | |
| 163 | + if (!d) return Number.POSITIVE_INFINITY; | |
| 164 | + const y = Number(d.slice(0, 4)); | |
| 165 | + if (!Number.isFinite(y)) return Number.POSITIVE_INFINITY; | |
| 166 | + let frac = 0.5; | |
| 167 | + const rest = d.slice(5); | |
| 168 | + if (/^Q[1-4]/.test(rest)) frac = (Number(rest[1]) - 0.5) / 4; | |
| 169 | + else if (/^H[12]/.test(rest)) frac = rest[1] === "1" ? 0.25 : 0.75; | |
| 170 | + else if (/^\d{2}/.test(rest)) { | |
| 171 | + const mo = Number(rest.slice(0, 2)); | |
| 172 | + const day = /^\d{2}-\d{2}/.test(rest) ? Number(rest.slice(3, 5)) : 15; | |
| 173 | + frac = (mo - 1 + day / 31) / 12; | |
| 174 | + } | |
| 175 | + return y + frac; | |
| 176 | +} | |
| 177 | + | |
| 178 | +export function formatPartialDate(d: string | null | undefined): string { | |
| 179 | + if (!d) return "—"; | |
| 180 | + const y = d.slice(0, 4); | |
| 181 | + const rest = d.slice(5); | |
| 182 | + if (!rest) return y; | |
| 183 | + if (/^[QH]\d/.test(rest)) return `${rest} ${y}`; | |
| 184 | + const mo = Number(rest.slice(0, 2)); | |
| 185 | + const names = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]; | |
| 186 | + const mn = names[mo - 1] ?? rest.slice(0, 2); | |
| 187 | + if (/^\d{2}-\d{2}/.test(rest)) return `${mn} ${Number(rest.slice(3, 5))}, ${y}`; | |
| 188 | + return `${mn} ${y}`; | |
| 189 | +} | |
| 190 | + | |
| 191 | +const STATUS_PATTERNS: Array<[RegExp, FacilityStatus]> = [ | |
| 192 | + [/\b(cancel+ed|scrapped|abandoned|withdrawn)\b/i, "cancelled"], | |
| 193 | + [/\b(closed|decommissioned|shut\s?down|ceased operations)\b/i, "closed"], | |
| 194 | + [/\b(delayed|postponed|on hold|paused)\b/i, "delayed"], | |
| 195 | + [/\b(partially operational|phase\s*1\s*(is\s*)?(live|operational|complete))\b/i, "partially_operational"], | |
| 196 | + [/\b(under construction|being built|construction (is )?under ?way|breaking ground|broke ground|groundbreaking|topped out|construction (has )?(started|began|commenced))\b/i, "under_construction"], | |
| 197 | + [/\b(approved|approval granted|granted planning|received approval|rezoning approved|permit(s)? (were |was )?(granted|issued))\b/i, "approved"], | |
| 198 | + [/\b(planning application|permit application|rezoning (request|application)|seeking approval|applied for|filed plans|submitted plans|under review by)\b/i, "permitting"], | |
| 199 | + [/\b(announced|unveils?|unveiled|plans to build|will build|to build|to develop|reveals? plans|launch(es|ed)? plans)\b/i, "announced"], | |
| 200 | + [/\b(proposed|proposal)\b/i, "proposed"], | |
| 201 | + [/\b(rumou?red|reportedly|is said to|considering|exploring|in talks)\b/i, "rumored"], | |
| 202 | + [/\b(expansion|expanding|expands|adds? capacity|new phase|additional \d+ ?MW)\b/i, "expansion"], | |
| 203 | + [/\b(now open|opened|opens|operational|live|online|commissioned|inaugurat(ed|es|ion)|ready for service|RFS)\b/i, "operational"], | |
| 204 | +]; | |
| 205 | + | |
| 206 | +export function inferStatus(text: string | null | undefined): FacilityStatus | null { | |
| 207 | + if (!text) return null; | |
| 208 | + for (const [re, st] of STATUS_PATTERNS) if (re.test(text)) return st; | |
| 209 | + return null; | |
| 210 | +} | |
| 211 | + | |
| 212 | +export function normalizeStatus(raw: string | null | undefined): FacilityStatus | null { | |
| 213 | + if (!raw) return null; | |
| 214 | + const s = raw.toLowerCase().trim().replace(/[\s-]+/g, "_"); | |
| 215 | + const direct: Record<string, FacilityStatus> = { | |
| 216 | + operational: "operational", live: "operational", active: "operational", open: "operational", ok: "operational", operating: "operational", existing: "operational", | |
| 217 | + under_construction: "under_construction", construction: "under_construction", building: "under_construction", | |
| 218 | + planned: "announced", plan: "announced", announced: "announced", proposed: "proposed", rumored: "rumored", rumoured: "rumored", | |
| 219 | + permitting: "permitting", approved: "approved", delayed: "delayed", cancelled: "cancelled", canceled: "cancelled", closed: "closed", decommissioned: "closed", | |
| 220 | + expansion: "expansion", partially_operational: "partially_operational", partial: "partially_operational", | |
| 221 | + coming_soon: "announced", future: "announced", in_development: "announced", development: "announced", | |
| 222 | + }; | |
| 223 | + return direct[s] ?? inferStatus(raw); | |
| 224 | +} | |
| 225 | + | |
| 226 | +export function inferFacilityType(text: string | null | undefined): FacilityType | null { | |
| 227 | + if (!text) return null; | |
| 228 | + const s = text.toLowerCase(); | |
| 229 | + if (/\b(internet exchange|ixp?\b|peering exchange)/.test(s)) return "internet_exchange"; | |
| 230 | + if (/\b(carrier hotel|meet-?me room|interconnection hub)/.test(s)) return "carrier_hotel"; | |
| 231 | + if (/\b(ai (data ?center|campus|factory|cluster|infrastructure)|gpu (cluster|campus|farm)|accelerated computing|training cluster|inference)/.test(s)) return "ai"; | |
| 232 | + if (/\b(supercomput|hpc|high[- ]performance computing|exascale|petaflop)/.test(s)) return "hpc"; | |
| 233 | + if (/\b(sovereign cloud|sovereign region)/.test(s)) return "sovereign_cloud"; | |
| 234 | + if (/\b(cloud region|availability zone|aws region|azure region|gcp region)/.test(s)) return "cloud_region"; | |
| 235 | + if (/\b(hyperscale|hyperscaler)/.test(s)) return "hyperscale"; | |
| 236 | + if (/\b(edge (data ?center|site|node|facility)|micro data ?center)/.test(s)) return "edge"; | |
| 237 | + if (/\b(wholesale|build[- ]to[- ]suit|powered shell)/.test(s)) return "wholesale"; | |
| 238 | + if (/\b(government|federal|ministry|public sector|municipal)/.test(s)) return "government"; | |
| 239 | + if (/\b(university|research|laboratory|national lab)/.test(s)) return "research"; | |
| 240 | + if (/\b(colocation|colo\b|retail colocation|interconnection)/.test(s)) return "colocation"; | |
| 241 | + return null; | |
| 242 | +} | |
| 243 | + | |
| 244 | +export function round(v: number, digits = 2): number { | |
| 245 | + const f = 10 ** digits; | |
| 246 | + return Math.round(v * f) / f; | |
| 247 | +} | |
| 248 | + | |
| 249 | +export function clamp(v: number, lo: number, hi: number): number { | |
| 250 | + return Math.min(hi, Math.max(lo, v)); | |
| 251 | +} | |
| 252 | + | |
| 253 | +export function cleanText(s: string | null | undefined): string | null { | |
| 254 | + if (s == null) return null; | |
| 255 | + const t = s.replace(/\s+/g, " ").trim(); | |
| 256 | + return t || null; | |
| 257 | +} | |
| 258 | + | |
| 259 | +/** Country ISO-2 from common names / ISO-3 / demonyms found in text. */ | |
| 260 | +export const COUNTRY_ALIASES: Record<string, string> = { | |
| 261 | + "united states": "US", usa: "US", "u.s.": "US", "u.s.a.": "US", "united states of america": "US", america: "US", | |
| 262 | + canada: "CA", "united kingdom": "GB", uk: "GB", "u.k.": "GB", britain: "GB", "great britain": "GB", england: "GB", scotland: "GB", wales: "GB", | |
| 263 | + germany: "DE", deutschland: "DE", france: "FR", netherlands: "NL", "the netherlands": "NL", holland: "NL", ireland: "IE", spain: "ES", portugal: "PT", italy: "IT", | |
| 264 | + sweden: "SE", norway: "NO", denmark: "DK", finland: "FI", iceland: "IS", belgium: "BE", luxembourg: "LU", switzerland: "CH", austria: "AT", poland: "PL", | |
| 265 | + czechia: "CZ", "czech republic": "CZ", hungary: "HU", romania: "RO", bulgaria: "BG", greece: "GR", turkey: "TR", türkiye: "TR", russia: "RU", ukraine: "UA", | |
| 266 | + japan: "JP", singapore: "SG", australia: "AU", "new zealand": "NZ", india: "IN", china: "CN", "hong kong": "HK", taiwan: "TW", "south korea": "KR", korea: "KR", | |
| 267 | + malaysia: "MY", indonesia: "ID", thailand: "TH", vietnam: "VN", philippines: "PH", "united arab emirates": "AE", uae: "AE", "saudi arabia": "SA", qatar: "QA", | |
| 268 | + bahrain: "BH", oman: "OM", kuwait: "KW", israel: "IL", "south africa": "ZA", nigeria: "NG", kenya: "KE", egypt: "EG", morocco: "MA", brazil: "BR", mexico: "MX", | |
| 269 | + chile: "CL", argentina: "AR", colombia: "CO", peru: "PE", uruguay: "UY", estonia: "EE", latvia: "LV", lithuania: "LT", slovakia: "SK", slovenia: "SI", croatia: "HR", | |
| 270 | + serbia: "RS", cyprus: "CY", malta: "MT", pakistan: "PK", bangladesh: "BD", "sri lanka": "LK", kazakhstan: "KZ", ghana: "GH", tanzania: "TZ", uganda: "UG", | |
| 271 | + rwanda: "RW", ethiopia: "ET", angola: "AO", "côte d'ivoire": "CI", "ivory coast": "CI", senegal: "SN", tunisia: "TN", algeria: "DZ", jordan: "JO", panama: "PA", | |
| 272 | + "costa rica": "CR", "puerto rico": "PR", "dominican republic": "DO", ecuador: "EC", bolivia: "BO", paraguay: "PY", venezuela: "VE", guatemala: "GT", | |
| 273 | + cambodia: "KH", laos: "LA", myanmar: "MM", nepal: "NP", mongolia: "MN", macau: "MO", macao: "MO", brunei: "BN", fiji: "FJ", "papua new guinea": "PG", | |
| 274 | + georgia: "GE", armenia: "AM", azerbaijan: "AZ", uzbekistan: "UZ", belarus: "BY", moldova: "MD", albania: "AL", "north macedonia": "MK", "bosnia and herzegovina": "BA", montenegro: "ME", kosovo: "XK", | |
| 275 | + usa_: "US", | |
| 276 | +}; | |
| 277 | + | |
| 278 | +export function countryFromText(text: string | null | undefined): string | null { | |
| 279 | + if (!text) return null; | |
| 280 | + const s = text.toLowerCase(); | |
| 281 | + const keys = Object.keys(COUNTRY_ALIASES).sort((a, b) => b.length - a.length); | |
| 282 | + for (const k of keys) if (new RegExp(`(^|[^a-z])${k.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}([^a-z]|$)`, "i").test(s)) return COUNTRY_ALIASES[k]!; | |
| 283 | + return null; | |
| 284 | +} | |
added
packages/core/src/ssrf.ts
+198 −0
@@ -0,0 +1,198 @@ | ||
| 1 | +import dns from "node:dns/promises"; | |
| 2 | +import net from "node:net"; | |
| 3 | + | |
| 4 | +/** | |
| 5 | + * SSRF protection. Every URL the crawler fetches — seed, discovered or redirect hop — | |
| 6 | + * must pass `assertUrlAllowed()` before any network activity. We validate the scheme, | |
| 7 | + * the hostname, and every resolved address. `safeLookup` is used by the HTTP dispatcher | |
| 8 | + * so the socket connects only to a validated address (defeats DNS rebinding). | |
| 9 | + */ | |
| 10 | + | |
| 11 | +const BLOCKED_HOSTNAMES = new Set([ | |
| 12 | + "localhost", | |
| 13 | + "localhost.localdomain", | |
| 14 | + "ip6-localhost", | |
| 15 | + "ip6-loopback", | |
| 16 | + "metadata.google.internal", | |
| 17 | + "metadata", | |
| 18 | + "instance-data", | |
| 19 | + "kubernetes.default", | |
| 20 | + "kubernetes.default.svc", | |
| 21 | +]); | |
| 22 | + | |
| 23 | +const BLOCKED_SUFFIXES = [".localhost", ".local", ".internal", ".localdomain", ".home.arpa", ".in-addr.arpa", ".ip6.arpa", ".maclustr.io", ".ts.net"]; | |
| 24 | + | |
| 25 | +function ipv4ToInt(ip: string): number { | |
| 26 | + const p = ip.split(".").map((x) => Number(x)); | |
| 27 | + return ((p[0]! << 24) >>> 0) + (p[1]! << 16) + (p[2]! << 8) + p[3]!; | |
| 28 | +} | |
| 29 | + | |
| 30 | +function inCidr4(ip: string, cidr: string): boolean { | |
| 31 | + const [base, bitsStr] = cidr.split("/"); | |
| 32 | + const bits = Number(bitsStr); | |
| 33 | + const mask = bits === 0 ? 0 : (~0 << (32 - bits)) >>> 0; | |
| 34 | + return ((ipv4ToInt(ip) & mask) >>> 0) === ((ipv4ToInt(base!) & mask) >>> 0); | |
| 35 | +} | |
| 36 | + | |
| 37 | +const BLOCKED_V4 = [ | |
| 38 | + "0.0.0.0/8", | |
| 39 | + "10.0.0.0/8", | |
| 40 | + "100.64.0.0/10", | |
| 41 | + "127.0.0.0/8", | |
| 42 | + "169.254.0.0/16", | |
| 43 | + "172.16.0.0/12", | |
| 44 | + "192.0.0.0/24", | |
| 45 | + "192.0.2.0/24", | |
| 46 | + "192.168.0.0/16", | |
| 47 | + "198.18.0.0/15", | |
| 48 | + "198.51.100.0/24", | |
| 49 | + "203.0.113.0/24", | |
| 50 | + "224.0.0.0/4", | |
| 51 | + "240.0.0.0/4", | |
| 52 | + "255.255.255.255/32", | |
| 53 | +]; | |
| 54 | + | |
| 55 | +export function isBlockedIPv4(ip: string): boolean { | |
| 56 | + return BLOCKED_V4.some((c) => inCidr4(ip, c)); | |
| 57 | +} | |
| 58 | + | |
| 59 | +export function isBlockedIPv6(ip: string): boolean { | |
| 60 | + const lower = ip.toLowerCase(); | |
| 61 | + if (lower === "::" || lower === "::1") return true; | |
| 62 | + const mapped = lower.match(/^(?:0*:)*ffff:(\d+\.\d+\.\d+\.\d+)$/); | |
| 63 | + if (mapped) return isBlockedIPv4(mapped[1]!); | |
| 64 | + const mappedHex = lower.match(/^(?:0*:)*ffff:([0-9a-f]{1,4}):([0-9a-f]{1,4})$/); | |
| 65 | + if (mappedHex) { | |
| 66 | + const a = parseInt(mappedHex[1]!, 16); | |
| 67 | + const b = parseInt(mappedHex[2]!, 16); | |
| 68 | + return isBlockedIPv4(`${a >> 8}.${a & 255}.${b >> 8}.${b & 255}`); | |
| 69 | + } | |
| 70 | + if (/^fe[89ab]/.test(lower)) return true; // link-local | |
| 71 | + if (lower.startsWith("fc") || lower.startsWith("fd")) return true; // unique local | |
| 72 | + if (lower.startsWith("ff")) return true; // multicast | |
| 73 | + // NAT64 (RFC 6052 well-known prefix 64:ff9b::/96, and the local-use 64:ff9b:1::/48): the last 32 bits | |
| 74 | + // embed an IPv4 address — apply the IPv4 policy to it instead of blocking the whole prefix, so that | |
| 75 | + // IPv6-only networks (464XLAT hotspots) can still reach public IPv4 hosts while private IPv4 stays blocked. | |
| 76 | + const nat64 = lower.match(/^64:ff9b:(?:1:[0-9a-f]{1,4}:[0-9a-f]{1,4}:)?(?:0*:)*([0-9a-f]{1,4}):([0-9a-f]{1,4})$/); | |
| 77 | + if (nat64) { | |
| 78 | + const a = parseInt(nat64[1]!, 16); | |
| 79 | + const b = parseInt(nat64[2]!, 16); | |
| 80 | + return isBlockedIPv4(`${a >> 8}.${a & 255}.${b >> 8}.${b & 255}`); | |
| 81 | + } | |
| 82 | + if (lower.startsWith("64:ff9b:")) return true; // malformed NAT64 form | |
| 83 | + if (lower.startsWith("2001:db8:")) return true; // documentation | |
| 84 | + return false; | |
| 85 | +} | |
| 86 | + | |
| 87 | +export function isBlockedIP(ip: string): boolean { | |
| 88 | + const v = net.isIP(ip); | |
| 89 | + if (v === 4) return isBlockedIPv4(ip); | |
| 90 | + if (v === 6) return isBlockedIPv6(ip); | |
| 91 | + return true; | |
| 92 | +} | |
| 93 | + | |
| 94 | +export function isBlockedHostname(hostname: string): boolean { | |
| 95 | + const h = hostname.toLowerCase().replace(/\.$/, ""); | |
| 96 | + if (BLOCKED_HOSTNAMES.has(h)) return true; | |
| 97 | + if (BLOCKED_SUFFIXES.some((s) => h.endsWith(s))) return true; | |
| 98 | + if (!h.includes(".") && net.isIP(h) === 0) return true; // bare single-label hosts | |
| 99 | + return false; | |
| 100 | +} | |
| 101 | + | |
| 102 | +export class UrlPolicyError extends Error { | |
| 103 | + constructor( | |
| 104 | + message: string, | |
| 105 | + public readonly reason: string, | |
| 106 | + ) { | |
| 107 | + super(message); | |
| 108 | + this.name = "UrlPolicyError"; | |
| 109 | + } | |
| 110 | +} | |
| 111 | + | |
| 112 | +export interface AllowedUrl { | |
| 113 | + url: URL; | |
| 114 | + hostname: string; | |
| 115 | + addresses: string[]; | |
| 116 | +} | |
| 117 | + | |
| 118 | +const DNS_FALLBACK_SERVERS = ["1.1.1.1", "8.8.8.8", "9.9.9.9"]; | |
| 119 | + | |
| 120 | +/** Resolve all A/AAAA records, falling back to public resolvers when the system resolver fails. */ | |
| 121 | +export async function resolveAll(hostname: string): Promise<string[]> { | |
| 122 | + try { | |
| 123 | + const res = await dns.lookup(hostname, { all: true, verbatim: true }); | |
| 124 | + const addrs = res.map((r) => r.address); | |
| 125 | + if (addrs.length) return addrs; | |
| 126 | + } catch { | |
| 127 | + // fall through | |
| 128 | + } | |
| 129 | + const r = new dns.Resolver({ timeout: 4000, tries: 2 }); | |
| 130 | + r.setServers(DNS_FALLBACK_SERVERS); | |
| 131 | + const out: string[] = []; | |
| 132 | + const [a, aaaa] = await Promise.allSettled([r.resolve4(hostname), r.resolve6(hostname)]); | |
| 133 | + if (a.status === "fulfilled") out.push(...a.value); | |
| 134 | + if (aaaa.status === "fulfilled") out.push(...aaaa.value); | |
| 135 | + return out; | |
| 136 | +} | |
| 137 | + | |
| 138 | +export interface UrlPolicyOptions { | |
| 139 | + /** Resolve DNS and validate each address. Default true. */ | |
| 140 | + resolve?: boolean; | |
| 141 | + /** Allow http:// (default true). */ | |
| 142 | + allowHttp?: boolean; | |
| 143 | +} | |
| 144 | + | |
| 145 | +/** Validate a URL (scheme, host, resolved addresses). Throws UrlPolicyError. */ | |
| 146 | +export async function assertUrlAllowed(input: string | URL, opts: UrlPolicyOptions = {}): Promise<AllowedUrl> { | |
| 147 | + let url: URL; | |
| 148 | + try { | |
| 149 | + url = typeof input === "string" ? new URL(input) : input; | |
| 150 | + } catch { | |
| 151 | + throw new UrlPolicyError("Malformed URL.", "malformed"); | |
| 152 | + } | |
| 153 | + if (url.protocol !== "https:" && !(url.protocol === "http:" && (opts.allowHttp ?? true))) { | |
| 154 | + throw new UrlPolicyError(`Scheme ${url.protocol} not allowed.`, "scheme"); | |
| 155 | + } | |
| 156 | + if (url.username || url.password) throw new UrlPolicyError("Credentials in URL are not allowed.", "credentials"); | |
| 157 | + const hostname = url.hostname.replace(/^\[|\]$/g, ""); | |
| 158 | + if (!hostname) throw new UrlPolicyError("Missing host.", "host"); | |
| 159 | + if (isBlockedHostname(hostname)) throw new UrlPolicyError(`Host ${hostname} is not allowed.`, "blocked_host"); | |
| 160 | + if (net.isIP(hostname)) { | |
| 161 | + if (isBlockedIP(hostname)) throw new UrlPolicyError(`Address ${hostname} is not allowed.`, "blocked_ip"); | |
| 162 | + return { url, hostname, addresses: [hostname] }; | |
| 163 | + } | |
| 164 | + if (opts.resolve === false) return { url, hostname, addresses: [] }; | |
| 165 | + const addresses = await resolveAll(hostname); | |
| 166 | + if (!addresses.length) throw new UrlPolicyError(`DNS resolution failed for ${hostname}.`, "dns"); | |
| 167 | + const bad = addresses.find((a) => isBlockedIP(a)); | |
| 168 | + if (bad) throw new UrlPolicyError(`Host ${hostname} resolves to a blocked address (${bad}).`, "blocked_ip"); | |
| 169 | + return { url, hostname, addresses }; | |
| 170 | +} | |
| 171 | + | |
| 172 | +/** | |
| 173 | + * `lookup` implementation for net.connect / undici connect options: only returns | |
| 174 | + * addresses that pass the policy, so the TCP connection can never reach a private range | |
| 175 | + * even if DNS answers differently than during validation (rebinding). | |
| 176 | + */ | |
| 177 | +export function safeLookup( | |
| 178 | + hostname: string, | |
| 179 | + options: unknown, | |
| 180 | + callback: (err: NodeJS.ErrnoException | null, address: string | { address: string; family: number }[], family?: number) => void, | |
| 181 | +): void { | |
| 182 | + const all = typeof options === "object" && options !== null && (options as { all?: boolean }).all === true; | |
| 183 | + if (isBlockedHostname(hostname)) { | |
| 184 | + callback(Object.assign(new Error(`blocked host ${hostname}`), { code: "EBLOCKED" }), all ? [] : "", 4); | |
| 185 | + return; | |
| 186 | + } | |
| 187 | + resolveAll(hostname) | |
| 188 | + .then((addrs) => { | |
| 189 | + const ok = addrs.filter((a) => !isBlockedIP(a)); | |
| 190 | + if (!ok.length) { | |
| 191 | + callback(Object.assign(new Error(`no allowed address for ${hostname}`), { code: "EBLOCKED" }), all ? [] : "", 4); | |
| 192 | + return; | |
| 193 | + } | |
| 194 | + if (all) callback(null, ok.map((a) => ({ address: a, family: net.isIP(a) }))); | |
| 195 | + else callback(null, ok[0]!, net.isIP(ok[0]!)); | |
| 196 | + }) | |
| 197 | + .catch((err) => callback(err, all ? [] : "", 4)); | |
| 198 | +} | |
added
packages/core/src/types.ts
+375 −0
@@ -0,0 +1,375 @@ | ||
| 1 | +/** | |
| 2 | + * DataCenterIndex — shared domain types. | |
| 3 | + * The product is the structured graph + its history: every value carries provenance and a confidence. | |
| 4 | + */ | |
| 5 | + | |
| 6 | +export const FACILITY_STATUSES = [ | |
| 7 | + "rumored", | |
| 8 | + "proposed", | |
| 9 | + "announced", | |
| 10 | + "permitting", | |
| 11 | + "approved", | |
| 12 | + "under_construction", | |
| 13 | + "partially_operational", | |
| 14 | + "operational", | |
| 15 | + "expansion", | |
| 16 | + "delayed", | |
| 17 | + "cancelled", | |
| 18 | + "closed", | |
| 19 | + "unknown", | |
| 20 | +] as const; | |
| 21 | +export type FacilityStatus = (typeof FACILITY_STATUSES)[number]; | |
| 22 | + | |
| 23 | +/** Statuses that count as "pipeline" (not yet fully operational). */ | |
| 24 | +export const PIPELINE_STATUSES: readonly FacilityStatus[] = ["rumored", "proposed", "announced", "permitting", "approved", "under_construction", "partially_operational", "delayed"]; | |
| 25 | + | |
| 26 | +export const FACILITY_TYPES = [ | |
| 27 | + "colocation", | |
| 28 | + "hyperscale", | |
| 29 | + "cloud_region", | |
| 30 | + "edge", | |
| 31 | + "carrier_hotel", | |
| 32 | + "internet_exchange", | |
| 33 | + "ai", | |
| 34 | + "hpc", | |
| 35 | + "sovereign_cloud", | |
| 36 | + "government", | |
| 37 | + "research", | |
| 38 | + "enterprise", | |
| 39 | + "telecom", | |
| 40 | + "wholesale", | |
| 41 | + "unknown", | |
| 42 | +] as const; | |
| 43 | +export type FacilityType = (typeof FACILITY_TYPES)[number]; | |
| 44 | + | |
| 45 | +export const OPERATOR_KINDS = ["colocation", "hyperscaler", "cloud", "carrier", "enterprise", "government", "research", "developer", "investor", "ixp_operator", "other"] as const; | |
| 46 | +export type OperatorKind = (typeof OPERATOR_KINDS)[number]; | |
| 47 | + | |
| 48 | +export const GEO_PRECISIONS = ["exact", "parcel", "street", "city", "metro", "approximate", "unknown"] as const; | |
| 49 | +export type GeoPrecision = (typeof GEO_PRECISIONS)[number]; | |
| 50 | + | |
| 51 | +export const CONFIDENCE_LEVELS = ["verified", "high", "moderate", "estimated", "unverified"] as const; | |
| 52 | +export type ConfidenceLevel = (typeof CONFIDENCE_LEVELS)[number]; | |
| 53 | + | |
| 54 | +export const SOURCE_KINDS = ["operator", "government", "filing", "utility", "dataset", "community", "secondary", "news", "cloud_provider", "registry"] as const; | |
| 55 | +export type SourceKind = (typeof SOURCE_KINDS)[number]; | |
| 56 | + | |
| 57 | +export const ENTITY_TYPES = [ | |
| 58 | + "facility", | |
| 59 | + "campus", | |
| 60 | + "operator", | |
| 61 | + "company", | |
| 62 | + "cloud_region", | |
| 63 | + "metro", | |
| 64 | + "city", | |
| 65 | + "country", | |
| 66 | + "power_project", | |
| 67 | + "substation", | |
| 68 | + "ixp", | |
| 69 | + "fiber_route", | |
| 70 | + "carrier", | |
| 71 | + "project", | |
| 72 | + "construction_phase", | |
| 73 | + "acquisition", | |
| 74 | + "expansion", | |
| 75 | + "incident", | |
| 76 | + "news_event", | |
| 77 | + "source", | |
| 78 | +] as const; | |
| 79 | +export type EntityType = (typeof ENTITY_TYPES)[number]; | |
| 80 | + | |
| 81 | +export const PAGE_TYPES = [ | |
| 82 | + "facility_page", | |
| 83 | + "facility_index", | |
| 84 | + "operator_page", | |
| 85 | + "project_announcement", | |
| 86 | + "press_release", | |
| 87 | + "planning_document", | |
| 88 | + "construction_update", | |
| 89 | + "acquisition", | |
| 90 | + "expansion", | |
| 91 | + "closure", | |
| 92 | + "cloud_region", | |
| 93 | + "power_infrastructure", | |
| 94 | + "utility_announcement", | |
| 95 | + "financial_disclosure", | |
| 96 | + "dataset", | |
| 97 | + "news_index", | |
| 98 | + "sitemap", | |
| 99 | + "unknown", | |
| 100 | +] as const; | |
| 101 | +export type PageType = (typeof PAGE_TYPES)[number]; | |
| 102 | + | |
| 103 | +export const EVENT_TYPES = [ | |
| 104 | + "facility_discovered", | |
| 105 | + "facility_updated", | |
| 106 | + "status_changed", | |
| 107 | + "capacity_changed", | |
| 108 | + "planned_capacity_changed", | |
| 109 | + "opening_date_changed", | |
| 110 | + "operator_changed", | |
| 111 | + "owner_changed", | |
| 112 | + "project_announced", | |
| 113 | + "project_status_changed", | |
| 114 | + "phase_announced", | |
| 115 | + "planning_filed", | |
| 116 | + "planning_approved", | |
| 117 | + "construction_started", | |
| 118 | + "facility_opened", | |
| 119 | + "expansion_announced", | |
| 120 | + "acquisition", | |
| 121 | + "closure", | |
| 122 | + "cloud_region_announced", | |
| 123 | + "cloud_region_launched", | |
| 124 | + "cloud_region_updated", | |
| 125 | + "power_agreement", | |
| 126 | + "investment_announced", | |
| 127 | + "incident", | |
| 128 | + "page_changed", | |
| 129 | + "news", | |
| 130 | +] as const; | |
| 131 | +export type EventType = (typeof EVENT_TYPES)[number]; | |
| 132 | + | |
| 133 | +export type ReviewStatus = "auto" | "pending" | "approved" | "rejected"; | |
| 134 | + | |
| 135 | +/** Fetch escalation levels — Level 1 direct HTTP … Level 4 Scrapfly rendered. */ | |
| 136 | +export type FetchLevel = 1 | 2 | 3 | 4; | |
| 137 | + | |
| 138 | +export interface Provenance { | |
| 139 | + sourceId: string; | |
| 140 | + connectorId: string; | |
| 141 | + url: string; | |
| 142 | + documentId?: string | null; | |
| 143 | + firstObserved: string; // ISO | |
| 144 | + lastObserved: string; // ISO | |
| 145 | + retrievedAt: string; // ISO | |
| 146 | + confidence: ConfidenceLevel; | |
| 147 | + isEstimate?: boolean; | |
| 148 | + method?: string; // e.g. "json-ld", "html-selector:.capacity", "regex:mw_v1", "dataset-field:latitude" | |
| 149 | + extractorVersion?: string; | |
| 150 | + note?: string | null; | |
| 151 | +} | |
| 152 | + | |
| 153 | +/** A single extracted value with provenance. */ | |
| 154 | +export interface Fact<T = unknown> { | |
| 155 | + field: string; | |
| 156 | + value: T; | |
| 157 | + provenance: Provenance; | |
| 158 | +} | |
| 159 | + | |
| 160 | +export interface GeoPoint { | |
| 161 | + lat: number; | |
| 162 | + lng: number; | |
| 163 | + precision: GeoPrecision; | |
| 164 | + source: string; // "operator" | "dataset:peeringdb" | "geocoder:nominatim" | "planning" | … | |
| 165 | +} | |
| 166 | + | |
| 167 | +/** Normalized entity payloads produced by connectors (before reconciliation). */ | |
| 168 | +export interface NormalizedFacility { | |
| 169 | + entityType: "facility"; | |
| 170 | + key: string; // connector-scoped stable key, e.g. "peeringdb:fac:1" | |
| 171 | + name: string; | |
| 172 | + aliases?: string[]; | |
| 173 | + operatorName?: string | null; | |
| 174 | + operatorKey?: string | null; | |
| 175 | + ownerName?: string | null; | |
| 176 | + campusName?: string | null; | |
| 177 | + campusKey?: string | null; | |
| 178 | + address?: string | null; | |
| 179 | + city?: string | null; | |
| 180 | + regionName?: string | null; // state / province | |
| 181 | + countryIso2?: string | null; | |
| 182 | + postalCode?: string | null; | |
| 183 | + geo?: GeoPoint | null; | |
| 184 | + status?: FacilityStatus | null; | |
| 185 | + facilityType?: FacilityType | null; | |
| 186 | + tier?: string | null; | |
| 187 | + buildingSqm?: number | null; | |
| 188 | + siteAreaHa?: number | null; | |
| 189 | + itCapacityMw?: number | null; | |
| 190 | + totalPowerMw?: number | null; | |
| 191 | + plannedPowerMw?: number | null; | |
| 192 | + rackCount?: number | null; | |
| 193 | + pue?: number | null; | |
| 194 | + coolingType?: string | null; | |
| 195 | + renewableClaim?: string | null; | |
| 196 | + openedOn?: string | null; // ISO date or partial YYYY / YYYY-MM | |
| 197 | + constructionStartedOn?: string | null; | |
| 198 | + announcedOn?: string | null; | |
| 199 | + website?: string | null; | |
| 200 | + isAi?: boolean | null; | |
| 201 | + isHyperscale?: boolean | null; | |
| 202 | + certifications?: string[]; | |
| 203 | + carriers?: string[]; | |
| 204 | + cloudProviders?: string[]; | |
| 205 | + ixps?: string[]; | |
| 206 | + externalIds?: Record<string, string | number>; // { peeringdb_fac: 1, osm: "way/123", wikidata: "Q42" } | |
| 207 | + description?: string | null; | |
| 208 | + /** per-field provenance overrides; default provenance applies to all other fields */ | |
| 209 | + facts?: Fact[]; | |
| 210 | + provenance: Provenance; | |
| 211 | +} | |
| 212 | + | |
| 213 | +export interface NormalizedOperator { | |
| 214 | + entityType: "operator"; | |
| 215 | + key: string; | |
| 216 | + name: string; | |
| 217 | + aliases?: string[]; | |
| 218 | + kind?: OperatorKind | null; | |
| 219 | + website?: string | null; | |
| 220 | + hqCountryIso2?: string | null; | |
| 221 | + hqCity?: string | null; | |
| 222 | + parentName?: string | null; | |
| 223 | + description?: string | null; | |
| 224 | + externalIds?: Record<string, string | number>; | |
| 225 | + provenance: Provenance; | |
| 226 | +} | |
| 227 | + | |
| 228 | +export interface NormalizedCampus { | |
| 229 | + entityType: "campus"; | |
| 230 | + key: string; | |
| 231 | + name: string; | |
| 232 | + operatorName?: string | null; | |
| 233 | + city?: string | null; | |
| 234 | + countryIso2?: string | null; | |
| 235 | + geo?: GeoPoint | null; | |
| 236 | + externalIds?: Record<string, string | number>; | |
| 237 | + provenance: Provenance; | |
| 238 | +} | |
| 239 | + | |
| 240 | +export interface NormalizedCloudRegion { | |
| 241 | + entityType: "cloud_region"; | |
| 242 | + key: string; | |
| 243 | + providerName: string; // AWS, Microsoft Azure, Google Cloud, … | |
| 244 | + code: string; // us-east-1 | |
| 245 | + name: string; // US East (N. Virginia) | |
| 246 | + city?: string | null; | |
| 247 | + regionName?: string | null; | |
| 248 | + countryIso2?: string | null; | |
| 249 | + geo?: GeoPoint | null; | |
| 250 | + availabilityZones?: number | null; | |
| 251 | + launchedOn?: string | null; | |
| 252 | + status?: "announced" | "operational" | "restricted" | "retired" | null; | |
| 253 | + isSovereign?: boolean | null; | |
| 254 | + sourceUrl?: string | null; | |
| 255 | + externalIds?: Record<string, string | number>; | |
| 256 | + provenance: Provenance; | |
| 257 | +} | |
| 258 | + | |
| 259 | +export interface NormalizedIxp { | |
| 260 | + entityType: "ixp"; | |
| 261 | + key: string; | |
| 262 | + name: string; | |
| 263 | + nameLong?: string | null; | |
| 264 | + city?: string | null; | |
| 265 | + countryIso2?: string | null; | |
| 266 | + regionContinent?: string | null; | |
| 267 | + website?: string | null; | |
| 268 | + networkCount?: number | null; | |
| 269 | + facilityKeys?: string[]; // NormalizedFacility keys where this IX is present | |
| 270 | + externalIds?: Record<string, string | number>; | |
| 271 | + provenance: Provenance; | |
| 272 | +} | |
| 273 | + | |
| 274 | +export interface NormalizedProject { | |
| 275 | + entityType: "project"; | |
| 276 | + key: string; | |
| 277 | + name: string; | |
| 278 | + operatorName?: string | null; | |
| 279 | + facilityKey?: string | null; | |
| 280 | + city?: string | null; | |
| 281 | + regionName?: string | null; | |
| 282 | + countryIso2?: string | null; | |
| 283 | + geo?: GeoPoint | null; | |
| 284 | + status?: FacilityStatus | null; | |
| 285 | + announcedOn?: string | null; | |
| 286 | + expectedOpening?: string | null; | |
| 287 | + plannedMw?: number | null; | |
| 288 | + investmentUsd?: number | null; | |
| 289 | + acreage?: number | null; | |
| 290 | + phaseCount?: number | null; | |
| 291 | + description?: string | null; | |
| 292 | + sourceUrl?: string | null; | |
| 293 | + timeline?: Array<{ date: string; type: EventType | string; description: string; url?: string }>; | |
| 294 | + externalIds?: Record<string, string | number>; | |
| 295 | + provenance: Provenance; | |
| 296 | +} | |
| 297 | + | |
| 298 | +export interface NormalizedNewsEvent { | |
| 299 | + entityType: "news_event"; | |
| 300 | + key: string; // url hash | |
| 301 | + title: string; | |
| 302 | + url: string; | |
| 303 | + publishedAt?: string | null; | |
| 304 | + summary?: string | null; | |
| 305 | + pageType?: PageType; | |
| 306 | + eventType?: EventType; | |
| 307 | + mentions?: { operators?: string[]; countriesIso2?: string[]; cities?: string[]; mw?: number[]; facilities?: string[] }; | |
| 308 | + provenance: Provenance; | |
| 309 | +} | |
| 310 | + | |
| 311 | +export interface NormalizedCountryStat { | |
| 312 | + entityType: "country"; | |
| 313 | + key: string; // iso2 | |
| 314 | + iso2: string; | |
| 315 | + population?: number | null; | |
| 316 | + gdpUsd?: number | null; | |
| 317 | + electricityTwh?: number | null; | |
| 318 | + renewableShare?: number | null; | |
| 319 | + year?: number | null; | |
| 320 | + provenance: Provenance; | |
| 321 | +} | |
| 322 | + | |
| 323 | +export type NormalizedEntity = | |
| 324 | + | NormalizedFacility | |
| 325 | + | NormalizedOperator | |
| 326 | + | NormalizedCampus | |
| 327 | + | NormalizedCloudRegion | |
| 328 | + | NormalizedIxp | |
| 329 | + | NormalizedProject | |
| 330 | + | NormalizedNewsEvent | |
| 331 | + | NormalizedCountryStat; | |
| 332 | + | |
| 333 | +export interface ValidationIssue { | |
| 334 | + key: string; | |
| 335 | + field?: string; | |
| 336 | + level: "error" | "warn"; | |
| 337 | + message: string; | |
| 338 | +} | |
| 339 | +export interface ValidationReport { | |
| 340 | + total: number; | |
| 341 | + valid: number; | |
| 342 | + rejected: number; | |
| 343 | + issues: ValidationIssue[]; | |
| 344 | +} | |
| 345 | + | |
| 346 | +/** Structured change detected between two versions of the same entity/page. */ | |
| 347 | +export interface DetectedChange { | |
| 348 | + field: string; | |
| 349 | + oldValue: unknown; | |
| 350 | + newValue: unknown; | |
| 351 | + significance: number; // 0..100 | |
| 352 | + eventType: EventType; | |
| 353 | + description: string; | |
| 354 | +} | |
| 355 | + | |
| 356 | +export interface EventRecord { | |
| 357 | + id: string; | |
| 358 | + entityType: EntityType; | |
| 359 | + entityId: string | null; | |
| 360 | + eventType: EventType; | |
| 361 | + detectedAt: string; | |
| 362 | + effectiveDate?: string | null; | |
| 363 | + oldValue?: unknown; | |
| 364 | + newValue?: unknown; | |
| 365 | + sourceId: string; | |
| 366 | + documentId?: string | null; | |
| 367 | + url: string; | |
| 368 | + title: string; | |
| 369 | + summary?: string | null; | |
| 370 | + significance: number; | |
| 371 | + confidence: ConfidenceLevel; | |
| 372 | + reviewStatus: ReviewStatus; | |
| 373 | + countryIso2?: string | null; | |
| 374 | + operatorId?: string | null; | |
| 375 | +} | |
added
packages/core/tsconfig.json
+1 −0
@@ -0,0 +1 @@ | ||
| 1 | +{ "extends": "../../tsconfig.base.json", "compilerOptions": { "noEmit": true, "types": ["node"] }, "include": ["src"] } | |
added
packages/db/drizzle.config.ts
+2 −0
@@ -0,0 +1,2 @@ | ||
| 1 | +import type { Config } from "drizzle-kit"; | |
| 2 | +export default { schema: "./src/schema.ts", out: "./migrations", dialect: "postgresql", dbCredentials: { url: process.env.DATABASE_URL ?? "postgres://dci:dci@127.0.0.1:5432/dci" }, casing: "snake_case" } satisfies Config; | |
added
packages/db/migrations/0000_init.sql
+532 −0
@@ -0,0 +1,532 @@ | ||
| 1 | +CREATE TABLE "admin_sessions" ( | |
| 2 | + "id" text PRIMARY KEY NOT NULL, | |
| 3 | + "created_at" timestamp with time zone DEFAULT now() NOT NULL, | |
| 4 | + "expires_at" timestamp with time zone NOT NULL, | |
| 5 | + "ip" text, | |
| 6 | + "user_agent" text | |
| 7 | +); | |
| 8 | +--> statement-breakpoint | |
| 9 | +CREATE TABLE "campuses" ( | |
| 10 | + "id" text PRIMARY KEY NOT NULL, | |
| 11 | + "slug" text NOT NULL, | |
| 12 | + "name" text NOT NULL, | |
| 13 | + "operator_id" text, | |
| 14 | + "metro_id" text, | |
| 15 | + "country_iso2" text, | |
| 16 | + "city" text, | |
| 17 | + "lat" double precision, | |
| 18 | + "lng" double precision, | |
| 19 | + "external_ids" jsonb DEFAULT '{}'::jsonb NOT NULL, | |
| 20 | + "created_at" timestamp with time zone DEFAULT now() NOT NULL, | |
| 21 | + "updated_at" timestamp with time zone DEFAULT now() NOT NULL, | |
| 22 | + CONSTRAINT "campuses_slug_unique" UNIQUE("slug") | |
| 23 | +); | |
| 24 | +--> statement-breakpoint | |
| 25 | +CREATE TABLE "cloud_regions" ( | |
| 26 | + "id" text PRIMARY KEY NOT NULL, | |
| 27 | + "slug" text NOT NULL, | |
| 28 | + "provider_id" text NOT NULL, | |
| 29 | + "code" text NOT NULL, | |
| 30 | + "name" text NOT NULL, | |
| 31 | + "city" text, | |
| 32 | + "region_name" text, | |
| 33 | + "country_iso2" text, | |
| 34 | + "metro_id" text, | |
| 35 | + "lat" double precision, | |
| 36 | + "lng" double precision, | |
| 37 | + "geo_precision" text DEFAULT 'city' NOT NULL, | |
| 38 | + "availability_zones" integer, | |
| 39 | + "launched_on" text, | |
| 40 | + "status" text DEFAULT 'operational' NOT NULL, | |
| 41 | + "is_sovereign" boolean DEFAULT false NOT NULL, | |
| 42 | + "source_url" text, | |
| 43 | + "external_ids" jsonb DEFAULT '{}'::jsonb NOT NULL, | |
| 44 | + "created_at" timestamp with time zone DEFAULT now() NOT NULL, | |
| 45 | + "updated_at" timestamp with time zone DEFAULT now() NOT NULL, | |
| 46 | + CONSTRAINT "cloud_regions_slug_unique" UNIQUE("slug") | |
| 47 | +); | |
| 48 | +--> statement-breakpoint | |
| 49 | +CREATE TABLE "connector_runs" ( | |
| 50 | + "id" text PRIMARY KEY NOT NULL, | |
| 51 | + "connector_id" text NOT NULL, | |
| 52 | + "task" text DEFAULT 'crawl' NOT NULL, | |
| 53 | + "started_at" timestamp with time zone DEFAULT now() NOT NULL, | |
| 54 | + "finished_at" timestamp with time zone, | |
| 55 | + "status" text DEFAULT 'running' NOT NULL, | |
| 56 | + "stats" jsonb DEFAULT '{}'::jsonb NOT NULL, | |
| 57 | + "error" text, | |
| 58 | + "log" jsonb DEFAULT '[]'::jsonb NOT NULL | |
| 59 | +); | |
| 60 | +--> statement-breakpoint | |
| 61 | +CREATE TABLE "connectors" ( | |
| 62 | + "id" text PRIMARY KEY NOT NULL, | |
| 63 | + "source_name" text NOT NULL, | |
| 64 | + "domain" text NOT NULL, | |
| 65 | + "kind" text NOT NULL, | |
| 66 | + "mode" text NOT NULL, | |
| 67 | + "enabled" boolean DEFAULT true NOT NULL, | |
| 68 | + "paused" boolean DEFAULT false NOT NULL, | |
| 69 | + "config" jsonb DEFAULT '{}'::jsonb NOT NULL, | |
| 70 | + "parser_version" text DEFAULT 'v1' NOT NULL, | |
| 71 | + "schedule" jsonb DEFAULT '{}'::jsonb NOT NULL, | |
| 72 | + "health" text DEFAULT 'never_run' NOT NULL, | |
| 73 | + "last_run_at" timestamp with time zone, | |
| 74 | + "last_status" text, | |
| 75 | + "last_error" text, | |
| 76 | + "next_run_at" timestamp with time zone, | |
| 77 | + "stats" jsonb DEFAULT '{}'::jsonb NOT NULL, | |
| 78 | + "created_at" timestamp with time zone DEFAULT now() NOT NULL, | |
| 79 | + "updated_at" timestamp with time zone DEFAULT now() NOT NULL | |
| 80 | +); | |
| 81 | +--> statement-breakpoint | |
| 82 | +CREATE TABLE "countries" ( | |
| 83 | + "iso2" text PRIMARY KEY NOT NULL, | |
| 84 | + "iso3" text, | |
| 85 | + "slug" text NOT NULL, | |
| 86 | + "name" text NOT NULL, | |
| 87 | + "region" text, | |
| 88 | + "subregion" text, | |
| 89 | + "population" bigint, | |
| 90 | + "gdp_usd" double precision, | |
| 91 | + "electricity_twh" double precision, | |
| 92 | + "renewable_share" double precision, | |
| 93 | + "stats_year" integer, | |
| 94 | + "lat" double precision, | |
| 95 | + "lng" double precision, | |
| 96 | + "stats" jsonb DEFAULT '{}'::jsonb NOT NULL, | |
| 97 | + "updated_at" timestamp with time zone DEFAULT now() NOT NULL, | |
| 98 | + CONSTRAINT "countries_slug_unique" UNIQUE("slug") | |
| 99 | +); | |
| 100 | +--> statement-breakpoint | |
| 101 | +CREATE TABLE "daily_metrics" ( | |
| 102 | + "day" date NOT NULL, | |
| 103 | + "metric" text NOT NULL, | |
| 104 | + "dim" text DEFAULT 'global' NOT NULL, | |
| 105 | + "value" double precision NOT NULL, | |
| 106 | + CONSTRAINT "daily_metrics_day_metric_dim_pk" PRIMARY KEY("day","metric","dim") | |
| 107 | +); | |
| 108 | +--> statement-breakpoint | |
| 109 | +CREATE TABLE "document_versions" ( | |
| 110 | + "id" text PRIMARY KEY NOT NULL, | |
| 111 | + "document_id" text NOT NULL, | |
| 112 | + "content_hash" text NOT NULL, | |
| 113 | + "fetched_at" timestamp with time zone DEFAULT now() NOT NULL, | |
| 114 | + "fetch_level" integer DEFAULT 1 NOT NULL, | |
| 115 | + "status_code" integer, | |
| 116 | + "size_bytes" integer, | |
| 117 | + "storage_key" text, | |
| 118 | + "diff_summary" jsonb, | |
| 119 | + "detected_changes" jsonb DEFAULT '[]'::jsonb NOT NULL, | |
| 120 | + "significance" integer DEFAULT 0 NOT NULL | |
| 121 | +); | |
| 122 | +--> statement-breakpoint | |
| 123 | +CREATE TABLE "documents" ( | |
| 124 | + "id" text PRIMARY KEY NOT NULL, | |
| 125 | + "connector_id" text NOT NULL, | |
| 126 | + "source_id" text NOT NULL, | |
| 127 | + "url" text NOT NULL, | |
| 128 | + "canonical_url" text NOT NULL, | |
| 129 | + "url_fingerprint" text NOT NULL, | |
| 130 | + "page_type" text DEFAULT 'unknown' NOT NULL, | |
| 131 | + "classifier" text, | |
| 132 | + "fetch_level" integer DEFAULT 1 NOT NULL, | |
| 133 | + "priority" integer DEFAULT 50 NOT NULL, | |
| 134 | + "content_hash" text, | |
| 135 | + "etag" text, | |
| 136 | + "last_modified" text, | |
| 137 | + "status_code" integer, | |
| 138 | + "content_type" text, | |
| 139 | + "size_bytes" integer, | |
| 140 | + "title" text, | |
| 141 | + "storage_key" text, | |
| 142 | + "extractor_version" text, | |
| 143 | + "extract_ok" boolean, | |
| 144 | + "extract_count" integer DEFAULT 0 NOT NULL, | |
| 145 | + "error" text, | |
| 146 | + "error_count" integer DEFAULT 0 NOT NULL, | |
| 147 | + "first_seen" timestamp with time zone DEFAULT now() NOT NULL, | |
| 148 | + "last_fetched" timestamp with time zone, | |
| 149 | + "last_changed" timestamp with time zone, | |
| 150 | + "last_checked" timestamp with time zone, | |
| 151 | + "next_check" timestamp with time zone, | |
| 152 | + "change_frequency_score" real DEFAULT 0.5 NOT NULL, | |
| 153 | + "fetch_count" integer DEFAULT 0 NOT NULL, | |
| 154 | + "change_count" integer DEFAULT 0 NOT NULL, | |
| 155 | + "discovered_from" text, | |
| 156 | + "quarantined" boolean DEFAULT false NOT NULL, | |
| 157 | + "entity_refs" jsonb DEFAULT '[]'::jsonb NOT NULL, | |
| 158 | + "updated_at" timestamp with time zone DEFAULT now() NOT NULL | |
| 159 | +); | |
| 160 | +--> statement-breakpoint | |
| 161 | +CREATE TABLE "entity_keys" ( | |
| 162 | + "key" text PRIMARY KEY NOT NULL, | |
| 163 | + "entity_type" text NOT NULL, | |
| 164 | + "entity_id" text NOT NULL, | |
| 165 | + "connector_id" text NOT NULL, | |
| 166 | + "created_at" timestamp with time zone DEFAULT now() NOT NULL | |
| 167 | +); | |
| 168 | +--> statement-breakpoint | |
| 169 | +CREATE TABLE "entity_matches" ( | |
| 170 | + "id" text PRIMARY KEY NOT NULL, | |
| 171 | + "connector_id" text NOT NULL, | |
| 172 | + "candidate_key" text NOT NULL, | |
| 173 | + "candidate" jsonb NOT NULL, | |
| 174 | + "matched_facility_id" text, | |
| 175 | + "score" real NOT NULL, | |
| 176 | + "reasons" text[] DEFAULT '{}'::text[] NOT NULL, | |
| 177 | + "status" text DEFAULT 'pending' NOT NULL, | |
| 178 | + "decided_by" text, | |
| 179 | + "decided_at" timestamp with time zone, | |
| 180 | + "created_at" timestamp with time zone DEFAULT now() NOT NULL | |
| 181 | +); | |
| 182 | +--> statement-breakpoint | |
| 183 | +CREATE TABLE "events" ( | |
| 184 | + "id" text PRIMARY KEY NOT NULL, | |
| 185 | + "entity_type" text NOT NULL, | |
| 186 | + "entity_id" text, | |
| 187 | + "event_type" text NOT NULL, | |
| 188 | + "detected_at" timestamp with time zone DEFAULT now() NOT NULL, | |
| 189 | + "effective_date" text, | |
| 190 | + "old_value" jsonb, | |
| 191 | + "new_value" jsonb, | |
| 192 | + "source_id" text NOT NULL, | |
| 193 | + "document_id" text, | |
| 194 | + "url" text NOT NULL, | |
| 195 | + "title" text NOT NULL, | |
| 196 | + "summary" text, | |
| 197 | + "significance" integer DEFAULT 30 NOT NULL, | |
| 198 | + "confidence" text DEFAULT 'moderate' NOT NULL, | |
| 199 | + "review_status" text DEFAULT 'auto' NOT NULL, | |
| 200 | + "country_iso2" text, | |
| 201 | + "operator_id" text, | |
| 202 | + "metro_id" text, | |
| 203 | + "project_id" text, | |
| 204 | + "fingerprint" text NOT NULL | |
| 205 | +); | |
| 206 | +--> statement-breakpoint | |
| 207 | +CREATE TABLE "facilities" ( | |
| 208 | + "id" text PRIMARY KEY NOT NULL, | |
| 209 | + "slug" text NOT NULL, | |
| 210 | + "name" text NOT NULL, | |
| 211 | + "normalized_name" text NOT NULL, | |
| 212 | + "operator_id" text, | |
| 213 | + "owner_id" text, | |
| 214 | + "campus_id" text, | |
| 215 | + "metro_id" text, | |
| 216 | + "country_iso2" text, | |
| 217 | + "city" text, | |
| 218 | + "region_name" text, | |
| 219 | + "address" text, | |
| 220 | + "postal_code" text, | |
| 221 | + "lat" double precision, | |
| 222 | + "lng" double precision, | |
| 223 | + "geo_precision" text DEFAULT 'unknown' NOT NULL, | |
| 224 | + "geo_source" text, | |
| 225 | + "geohash" text, | |
| 226 | + "status" text DEFAULT 'unknown' NOT NULL, | |
| 227 | + "facility_type" text DEFAULT 'unknown' NOT NULL, | |
| 228 | + "tier" text, | |
| 229 | + "building_sqm" double precision, | |
| 230 | + "site_area_ha" double precision, | |
| 231 | + "it_capacity_mw" double precision, | |
| 232 | + "total_power_mw" double precision, | |
| 233 | + "planned_power_mw" double precision, | |
| 234 | + "mw_is_estimate" boolean DEFAULT false NOT NULL, | |
| 235 | + "rack_count" integer, | |
| 236 | + "pue" real, | |
| 237 | + "cooling_type" text, | |
| 238 | + "renewable_claim" text, | |
| 239 | + "opened_on" text, | |
| 240 | + "construction_started_on" text, | |
| 241 | + "announced_on" text, | |
| 242 | + "website" text, | |
| 243 | + "description" text, | |
| 244 | + "is_ai" boolean DEFAULT false NOT NULL, | |
| 245 | + "is_hyperscale" boolean DEFAULT false NOT NULL, | |
| 246 | + "certifications" text[] DEFAULT '{}'::text[] NOT NULL, | |
| 247 | + "carriers_count" integer, | |
| 248 | + "ixp_count" integer, | |
| 249 | + "networks_count" integer, | |
| 250 | + "confidence" text DEFAULT 'unverified' NOT NULL, | |
| 251 | + "completeness" integer DEFAULT 0 NOT NULL, | |
| 252 | + "external_ids" jsonb DEFAULT '{}'::jsonb NOT NULL, | |
| 253 | + "source_count" integer DEFAULT 0 NOT NULL, | |
| 254 | + "first_seen" timestamp with time zone DEFAULT now() NOT NULL, | |
| 255 | + "last_verified" timestamp with time zone, | |
| 256 | + "merged_into" text, | |
| 257 | + "created_at" timestamp with time zone DEFAULT now() NOT NULL, | |
| 258 | + "updated_at" timestamp with time zone DEFAULT now() NOT NULL, | |
| 259 | + CONSTRAINT "facilities_slug_unique" UNIQUE("slug") | |
| 260 | +); | |
| 261 | +--> statement-breakpoint | |
| 262 | +CREATE TABLE "facility_aliases" ( | |
| 263 | + "facility_id" text NOT NULL, | |
| 264 | + "alias" text NOT NULL, | |
| 265 | + "normalized" text NOT NULL, | |
| 266 | + "source_id" text, | |
| 267 | + CONSTRAINT "facility_aliases_facility_id_alias_pk" PRIMARY KEY("facility_id","alias") | |
| 268 | +); | |
| 269 | +--> statement-breakpoint | |
| 270 | +CREATE TABLE "facility_ixps" ( | |
| 271 | + "facility_id" text NOT NULL, | |
| 272 | + "ixp_id" text NOT NULL, | |
| 273 | + "source_id" text, | |
| 274 | + CONSTRAINT "facility_ixps_facility_id_ixp_id_pk" PRIMARY KEY("facility_id","ixp_id") | |
| 275 | +); | |
| 276 | +--> statement-breakpoint | |
| 277 | +CREATE TABLE "facility_tenants" ( | |
| 278 | + "facility_id" text NOT NULL, | |
| 279 | + "operator_id" text NOT NULL, | |
| 280 | + "role" text DEFAULT 'carrier' NOT NULL, | |
| 281 | + "asn" integer, | |
| 282 | + "source_id" text, | |
| 283 | + CONSTRAINT "facility_tenants_facility_id_operator_id_role_pk" PRIMARY KEY("facility_id","operator_id","role") | |
| 284 | +); | |
| 285 | +--> statement-breakpoint | |
| 286 | +CREATE TABLE "ixps" ( | |
| 287 | + "id" text PRIMARY KEY NOT NULL, | |
| 288 | + "slug" text NOT NULL, | |
| 289 | + "name" text NOT NULL, | |
| 290 | + "name_long" text, | |
| 291 | + "city" text, | |
| 292 | + "country_iso2" text, | |
| 293 | + "metro_id" text, | |
| 294 | + "region_continent" text, | |
| 295 | + "website" text, | |
| 296 | + "network_count" integer, | |
| 297 | + "external_ids" jsonb DEFAULT '{}'::jsonb NOT NULL, | |
| 298 | + "created_at" timestamp with time zone DEFAULT now() NOT NULL, | |
| 299 | + "updated_at" timestamp with time zone DEFAULT now() NOT NULL, | |
| 300 | + CONSTRAINT "ixps_slug_unique" UNIQUE("slug") | |
| 301 | +); | |
| 302 | +--> statement-breakpoint | |
| 303 | +CREATE TABLE "metros" ( | |
| 304 | + "id" text PRIMARY KEY NOT NULL, | |
| 305 | + "slug" text NOT NULL, | |
| 306 | + "name" text NOT NULL, | |
| 307 | + "country_iso2" text NOT NULL, | |
| 308 | + "region_name" text, | |
| 309 | + "lat" double precision NOT NULL, | |
| 310 | + "lng" double precision NOT NULL, | |
| 311 | + "radius_km" real DEFAULT 60 NOT NULL, | |
| 312 | + "aliases" text[] DEFAULT '{}'::text[] NOT NULL, | |
| 313 | + "description" text, | |
| 314 | + "stats" jsonb DEFAULT '{}'::jsonb NOT NULL, | |
| 315 | + "created_at" timestamp with time zone DEFAULT now() NOT NULL, | |
| 316 | + "updated_at" timestamp with time zone DEFAULT now() NOT NULL, | |
| 317 | + CONSTRAINT "metros_slug_unique" UNIQUE("slug") | |
| 318 | +); | |
| 319 | +--> statement-breakpoint | |
| 320 | +CREATE TABLE "news_items" ( | |
| 321 | + "id" text PRIMARY KEY NOT NULL, | |
| 322 | + "source_id" text NOT NULL, | |
| 323 | + "connector_id" text NOT NULL, | |
| 324 | + "url" text NOT NULL, | |
| 325 | + "title" text NOT NULL, | |
| 326 | + "published_at" timestamp with time zone, | |
| 327 | + "summary" text, | |
| 328 | + "page_type" text DEFAULT 'unknown' NOT NULL, | |
| 329 | + "event_type" text, | |
| 330 | + "mentions" jsonb DEFAULT '{}'::jsonb NOT NULL, | |
| 331 | + "operator_ids" text[] DEFAULT '{}'::text[] NOT NULL, | |
| 332 | + "country_iso2s" text[] DEFAULT '{}'::text[] NOT NULL, | |
| 333 | + "facility_ids" text[] DEFAULT '{}'::text[] NOT NULL, | |
| 334 | + "project_id" text, | |
| 335 | + "mw" double precision, | |
| 336 | + "significance" integer DEFAULT 20 NOT NULL, | |
| 337 | + "created_at" timestamp with time zone DEFAULT now() NOT NULL, | |
| 338 | + CONSTRAINT "news_items_url_unique" UNIQUE("url") | |
| 339 | +); | |
| 340 | +--> statement-breakpoint | |
| 341 | +CREATE TABLE "operators" ( | |
| 342 | + "id" text PRIMARY KEY NOT NULL, | |
| 343 | + "slug" text NOT NULL, | |
| 344 | + "name" text NOT NULL, | |
| 345 | + "normalized_name" text NOT NULL, | |
| 346 | + "kind" text, | |
| 347 | + "website" text, | |
| 348 | + "hq_country_iso2" text, | |
| 349 | + "hq_city" text, | |
| 350 | + "parent_id" text, | |
| 351 | + "description" text, | |
| 352 | + "aliases" text[] DEFAULT '{}'::text[] NOT NULL, | |
| 353 | + "external_ids" jsonb DEFAULT '{}'::jsonb NOT NULL, | |
| 354 | + "is_cloud_provider" boolean DEFAULT false NOT NULL, | |
| 355 | + "is_carrier" boolean DEFAULT false NOT NULL, | |
| 356 | + "stats" jsonb DEFAULT '{}'::jsonb NOT NULL, | |
| 357 | + "created_at" timestamp with time zone DEFAULT now() NOT NULL, | |
| 358 | + "updated_at" timestamp with time zone DEFAULT now() NOT NULL, | |
| 359 | + CONSTRAINT "operators_slug_unique" UNIQUE("slug") | |
| 360 | +); | |
| 361 | +--> statement-breakpoint | |
| 362 | +CREATE TABLE "project_timeline" ( | |
| 363 | + "id" text PRIMARY KEY NOT NULL, | |
| 364 | + "project_id" text NOT NULL, | |
| 365 | + "event_date" text NOT NULL, | |
| 366 | + "event_type" text NOT NULL, | |
| 367 | + "description" text NOT NULL, | |
| 368 | + "source_id" text, | |
| 369 | + "document_id" text, | |
| 370 | + "url" text, | |
| 371 | + "created_at" timestamp with time zone DEFAULT now() NOT NULL | |
| 372 | +); | |
| 373 | +--> statement-breakpoint | |
| 374 | +CREATE TABLE "projects" ( | |
| 375 | + "id" text PRIMARY KEY NOT NULL, | |
| 376 | + "slug" text NOT NULL, | |
| 377 | + "name" text NOT NULL, | |
| 378 | + "normalized_name" text NOT NULL, | |
| 379 | + "operator_id" text, | |
| 380 | + "facility_id" text, | |
| 381 | + "metro_id" text, | |
| 382 | + "country_iso2" text, | |
| 383 | + "city" text, | |
| 384 | + "region_name" text, | |
| 385 | + "lat" double precision, | |
| 386 | + "lng" double precision, | |
| 387 | + "geo_precision" text DEFAULT 'unknown' NOT NULL, | |
| 388 | + "status" text DEFAULT 'announced' NOT NULL, | |
| 389 | + "announced_on" text, | |
| 390 | + "expected_opening" text, | |
| 391 | + "planned_mw" double precision, | |
| 392 | + "investment_usd" double precision, | |
| 393 | + "investment_currency" text, | |
| 394 | + "investment_original" double precision, | |
| 395 | + "acreage" double precision, | |
| 396 | + "phase_count" integer, | |
| 397 | + "is_ai" boolean DEFAULT false NOT NULL, | |
| 398 | + "description" text, | |
| 399 | + "source_url" text, | |
| 400 | + "confidence" text DEFAULT 'moderate' NOT NULL, | |
| 401 | + "external_ids" jsonb DEFAULT '{}'::jsonb NOT NULL, | |
| 402 | + "last_update" timestamp with time zone DEFAULT now() NOT NULL, | |
| 403 | + "created_at" timestamp with time zone DEFAULT now() NOT NULL, | |
| 404 | + "updated_at" timestamp with time zone DEFAULT now() NOT NULL, | |
| 405 | + CONSTRAINT "projects_slug_unique" UNIQUE("slug") | |
| 406 | +); | |
| 407 | +--> statement-breakpoint | |
| 408 | +CREATE TABLE "provenance" ( | |
| 409 | + "id" text PRIMARY KEY NOT NULL, | |
| 410 | + "entity_type" text NOT NULL, | |
| 411 | + "entity_id" text NOT NULL, | |
| 412 | + "field" text NOT NULL, | |
| 413 | + "value" jsonb, | |
| 414 | + "source_id" text NOT NULL, | |
| 415 | + "connector_id" text NOT NULL, | |
| 416 | + "document_id" text, | |
| 417 | + "url" text NOT NULL, | |
| 418 | + "first_observed" timestamp with time zone DEFAULT now() NOT NULL, | |
| 419 | + "last_observed" timestamp with time zone DEFAULT now() NOT NULL, | |
| 420 | + "retrieved_at" timestamp with time zone DEFAULT now() NOT NULL, | |
| 421 | + "confidence" text DEFAULT 'moderate' NOT NULL, | |
| 422 | + "is_estimate" boolean DEFAULT false NOT NULL, | |
| 423 | + "method" text, | |
| 424 | + "extractor_version" text, | |
| 425 | + "is_current" boolean DEFAULT true NOT NULL, | |
| 426 | + "note" text | |
| 427 | +); | |
| 428 | +--> statement-breakpoint | |
| 429 | +CREATE TABLE "rankings" ( | |
| 430 | + "id" text PRIMARY KEY NOT NULL, | |
| 431 | + "key" text NOT NULL, | |
| 432 | + "scope" text NOT NULL, | |
| 433 | + "label" text NOT NULL, | |
| 434 | + "unit" text NOT NULL, | |
| 435 | + "methodology" text NOT NULL, | |
| 436 | + "min_coverage" real, | |
| 437 | + "rows" jsonb NOT NULL, | |
| 438 | + "total" integer NOT NULL, | |
| 439 | + "computed_at" timestamp with time zone DEFAULT now() NOT NULL, | |
| 440 | + "is_current" boolean DEFAULT true NOT NULL | |
| 441 | +); | |
| 442 | +--> statement-breakpoint | |
| 443 | +CREATE TABLE "sources" ( | |
| 444 | + "id" text PRIMARY KEY NOT NULL, | |
| 445 | + "connector_id" text NOT NULL, | |
| 446 | + "name" text NOT NULL, | |
| 447 | + "domain" text NOT NULL, | |
| 448 | + "kind" text NOT NULL, | |
| 449 | + "priority" integer DEFAULT 3 NOT NULL, | |
| 450 | + "url" text, | |
| 451 | + "license" text, | |
| 452 | + "attribution" text, | |
| 453 | + "robots_allowed" boolean, | |
| 454 | + "notes" text, | |
| 455 | + "created_at" timestamp with time zone DEFAULT now() NOT NULL, | |
| 456 | + "updated_at" timestamp with time zone DEFAULT now() NOT NULL | |
| 457 | +); | |
| 458 | +--> statement-breakpoint | |
| 459 | +CREATE TABLE "system_alerts" ( | |
| 460 | + "id" text PRIMARY KEY NOT NULL, | |
| 461 | + "level" text NOT NULL, | |
| 462 | + "component" text NOT NULL, | |
| 463 | + "message" text NOT NULL, | |
| 464 | + "details" jsonb, | |
| 465 | + "resolved_at" timestamp with time zone, | |
| 466 | + "created_at" timestamp with time zone DEFAULT now() NOT NULL | |
| 467 | +); | |
| 468 | +--> statement-breakpoint | |
| 469 | +ALTER TABLE "campuses" ADD CONSTRAINT "campuses_operator_id_operators_id_fk" FOREIGN KEY ("operator_id") REFERENCES "public"."operators"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint | |
| 470 | +ALTER TABLE "campuses" ADD CONSTRAINT "campuses_metro_id_metros_id_fk" FOREIGN KEY ("metro_id") REFERENCES "public"."metros"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint | |
| 471 | +ALTER TABLE "cloud_regions" ADD CONSTRAINT "cloud_regions_provider_id_operators_id_fk" FOREIGN KEY ("provider_id") REFERENCES "public"."operators"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint | |
| 472 | +ALTER TABLE "cloud_regions" ADD CONSTRAINT "cloud_regions_country_iso2_countries_iso2_fk" FOREIGN KEY ("country_iso2") REFERENCES "public"."countries"("iso2") ON DELETE no action ON UPDATE no action;--> statement-breakpoint | |
| 473 | +ALTER TABLE "cloud_regions" ADD CONSTRAINT "cloud_regions_metro_id_metros_id_fk" FOREIGN KEY ("metro_id") REFERENCES "public"."metros"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint | |
| 474 | +ALTER TABLE "document_versions" ADD CONSTRAINT "document_versions_document_id_documents_id_fk" FOREIGN KEY ("document_id") REFERENCES "public"."documents"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint | |
| 475 | +ALTER TABLE "facilities" ADD CONSTRAINT "facilities_operator_id_operators_id_fk" FOREIGN KEY ("operator_id") REFERENCES "public"."operators"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint | |
| 476 | +ALTER TABLE "facilities" ADD CONSTRAINT "facilities_owner_id_operators_id_fk" FOREIGN KEY ("owner_id") REFERENCES "public"."operators"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint | |
| 477 | +ALTER TABLE "facilities" ADD CONSTRAINT "facilities_campus_id_campuses_id_fk" FOREIGN KEY ("campus_id") REFERENCES "public"."campuses"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint | |
| 478 | +ALTER TABLE "facilities" ADD CONSTRAINT "facilities_metro_id_metros_id_fk" FOREIGN KEY ("metro_id") REFERENCES "public"."metros"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint | |
| 479 | +ALTER TABLE "facilities" ADD CONSTRAINT "facilities_country_iso2_countries_iso2_fk" FOREIGN KEY ("country_iso2") REFERENCES "public"."countries"("iso2") ON DELETE no action ON UPDATE no action;--> statement-breakpoint | |
| 480 | +ALTER TABLE "facility_aliases" ADD CONSTRAINT "facility_aliases_facility_id_facilities_id_fk" FOREIGN KEY ("facility_id") REFERENCES "public"."facilities"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint | |
| 481 | +ALTER TABLE "facility_ixps" ADD CONSTRAINT "facility_ixps_facility_id_facilities_id_fk" FOREIGN KEY ("facility_id") REFERENCES "public"."facilities"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint | |
| 482 | +ALTER TABLE "facility_ixps" ADD CONSTRAINT "facility_ixps_ixp_id_ixps_id_fk" FOREIGN KEY ("ixp_id") REFERENCES "public"."ixps"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint | |
| 483 | +ALTER TABLE "facility_tenants" ADD CONSTRAINT "facility_tenants_facility_id_facilities_id_fk" FOREIGN KEY ("facility_id") REFERENCES "public"."facilities"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint | |
| 484 | +ALTER TABLE "facility_tenants" ADD CONSTRAINT "facility_tenants_operator_id_operators_id_fk" FOREIGN KEY ("operator_id") REFERENCES "public"."operators"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint | |
| 485 | +ALTER TABLE "ixps" ADD CONSTRAINT "ixps_country_iso2_countries_iso2_fk" FOREIGN KEY ("country_iso2") REFERENCES "public"."countries"("iso2") ON DELETE no action ON UPDATE no action;--> statement-breakpoint | |
| 486 | +ALTER TABLE "ixps" ADD CONSTRAINT "ixps_metro_id_metros_id_fk" FOREIGN KEY ("metro_id") REFERENCES "public"."metros"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint | |
| 487 | +ALTER TABLE "metros" ADD CONSTRAINT "metros_country_iso2_countries_iso2_fk" FOREIGN KEY ("country_iso2") REFERENCES "public"."countries"("iso2") ON DELETE no action ON UPDATE no action;--> statement-breakpoint | |
| 488 | +ALTER TABLE "project_timeline" ADD CONSTRAINT "project_timeline_project_id_projects_id_fk" FOREIGN KEY ("project_id") REFERENCES "public"."projects"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint | |
| 489 | +ALTER TABLE "projects" ADD CONSTRAINT "projects_operator_id_operators_id_fk" FOREIGN KEY ("operator_id") REFERENCES "public"."operators"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint | |
| 490 | +ALTER TABLE "projects" ADD CONSTRAINT "projects_facility_id_facilities_id_fk" FOREIGN KEY ("facility_id") REFERENCES "public"."facilities"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint | |
| 491 | +ALTER TABLE "projects" ADD CONSTRAINT "projects_metro_id_metros_id_fk" FOREIGN KEY ("metro_id") REFERENCES "public"."metros"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint | |
| 492 | +ALTER TABLE "projects" ADD CONSTRAINT "projects_country_iso2_countries_iso2_fk" FOREIGN KEY ("country_iso2") REFERENCES "public"."countries"("iso2") ON DELETE no action ON UPDATE no action;--> statement-breakpoint | |
| 493 | +CREATE UNIQUE INDEX "cloud_regions_provider_code_uq" ON "cloud_regions" USING btree ("provider_id","code");--> statement-breakpoint | |
| 494 | +CREATE INDEX "cloud_regions_country_idx" ON "cloud_regions" USING btree ("country_iso2");--> statement-breakpoint | |
| 495 | +CREATE INDEX "connector_runs_connector_idx" ON "connector_runs" USING btree ("connector_id","started_at");--> statement-breakpoint | |
| 496 | +CREATE INDEX "document_versions_doc_idx" ON "document_versions" USING btree ("document_id","fetched_at");--> statement-breakpoint | |
| 497 | +CREATE UNIQUE INDEX "documents_fingerprint_uq" ON "documents" USING btree ("url_fingerprint");--> statement-breakpoint | |
| 498 | +CREATE INDEX "documents_connector_idx" ON "documents" USING btree ("connector_id");--> statement-breakpoint | |
| 499 | +CREATE INDEX "documents_next_check_idx" ON "documents" USING btree ("next_check");--> statement-breakpoint | |
| 500 | +CREATE INDEX "documents_page_type_idx" ON "documents" USING btree ("page_type");--> statement-breakpoint | |
| 501 | +CREATE INDEX "entity_keys_entity_idx" ON "entity_keys" USING btree ("entity_type","entity_id");--> statement-breakpoint | |
| 502 | +CREATE INDEX "entity_matches_status_idx" ON "entity_matches" USING btree ("status");--> statement-breakpoint | |
| 503 | +CREATE INDEX "entity_matches_key_idx" ON "entity_matches" USING btree ("candidate_key");--> statement-breakpoint | |
| 504 | +CREATE UNIQUE INDEX "events_fingerprint_uq" ON "events" USING btree ("fingerprint");--> statement-breakpoint | |
| 505 | +CREATE INDEX "events_detected_idx" ON "events" USING btree ("detected_at");--> statement-breakpoint | |
| 506 | +CREATE INDEX "events_entity_idx" ON "events" USING btree ("entity_type","entity_id");--> statement-breakpoint | |
| 507 | +CREATE INDEX "events_country_idx" ON "events" USING btree ("country_iso2");--> statement-breakpoint | |
| 508 | +CREATE INDEX "events_operator_idx" ON "events" USING btree ("operator_id");--> statement-breakpoint | |
| 509 | +CREATE INDEX "events_type_idx" ON "events" USING btree ("event_type");--> statement-breakpoint | |
| 510 | +CREATE INDEX "facilities_country_idx" ON "facilities" USING btree ("country_iso2");--> statement-breakpoint | |
| 511 | +CREATE INDEX "facilities_operator_idx" ON "facilities" USING btree ("operator_id");--> statement-breakpoint | |
| 512 | +CREATE INDEX "facilities_metro_idx" ON "facilities" USING btree ("metro_id");--> statement-breakpoint | |
| 513 | +CREATE INDEX "facilities_status_idx" ON "facilities" USING btree ("status");--> statement-breakpoint | |
| 514 | +CREATE INDEX "facilities_type_idx" ON "facilities" USING btree ("facility_type");--> statement-breakpoint | |
| 515 | +CREATE INDEX "facilities_geohash_idx" ON "facilities" USING btree ("geohash");--> statement-breakpoint | |
| 516 | +CREATE INDEX "facilities_norm_idx" ON "facilities" USING btree ("normalized_name");--> statement-breakpoint | |
| 517 | +CREATE INDEX "facilities_updated_idx" ON "facilities" USING btree ("updated_at");--> statement-breakpoint | |
| 518 | +CREATE INDEX "facility_aliases_norm_idx" ON "facility_aliases" USING btree ("normalized");--> statement-breakpoint | |
| 519 | +CREATE INDEX "facility_tenants_operator_idx" ON "facility_tenants" USING btree ("operator_id");--> statement-breakpoint | |
| 520 | +CREATE INDEX "metros_country_idx" ON "metros" USING btree ("country_iso2");--> statement-breakpoint | |
| 521 | +CREATE INDEX "news_items_published_idx" ON "news_items" USING btree ("published_at");--> statement-breakpoint | |
| 522 | +CREATE INDEX "operators_norm_idx" ON "operators" USING btree ("normalized_name");--> statement-breakpoint | |
| 523 | +CREATE INDEX "operators_kind_idx" ON "operators" USING btree ("kind");--> statement-breakpoint | |
| 524 | +CREATE INDEX "project_timeline_project_idx" ON "project_timeline" USING btree ("project_id");--> statement-breakpoint | |
| 525 | +CREATE INDEX "projects_country_idx" ON "projects" USING btree ("country_iso2");--> statement-breakpoint | |
| 526 | +CREATE INDEX "projects_operator_idx" ON "projects" USING btree ("operator_id");--> statement-breakpoint | |
| 527 | +CREATE INDEX "projects_status_idx" ON "projects" USING btree ("status");--> statement-breakpoint | |
| 528 | +CREATE INDEX "projects_norm_idx" ON "projects" USING btree ("normalized_name");--> statement-breakpoint | |
| 529 | +CREATE INDEX "provenance_entity_idx" ON "provenance" USING btree ("entity_type","entity_id");--> statement-breakpoint | |
| 530 | +CREATE INDEX "provenance_source_idx" ON "provenance" USING btree ("source_id");--> statement-breakpoint | |
| 531 | +CREATE UNIQUE INDEX "provenance_uq" ON "provenance" USING btree ("entity_type","entity_id","field","source_id","url");--> statement-breakpoint | |
| 532 | +CREATE INDEX "rankings_key_idx" ON "rankings" USING btree ("key","is_current"); | |
| \ No newline at end of file | ||
added
packages/db/migrations/0001_search.sql
+26 −0
@@ -0,0 +1,26 @@ | ||
| 1 | +-- Extensions + search/geo indexes | |
| 2 | +CREATE EXTENSION IF NOT EXISTS pg_trgm; | |
| 3 | +CREATE EXTENSION IF NOT EXISTS unaccent; | |
| 4 | +--> statement-breakpoint | |
| 5 | +ALTER TABLE facilities ADD COLUMN IF NOT EXISTS search tsvector | |
| 6 | + GENERATED ALWAYS AS ( | |
| 7 | + setweight(to_tsvector('simple'::regconfig, coalesce(name,'')), 'A') || | |
| 8 | + setweight(to_tsvector('simple'::regconfig, coalesce(city,'') || ' ' || coalesce(region_name,'') || ' ' || coalesce(address,'')), 'B') || | |
| 9 | + setweight(to_tsvector('simple'::regconfig, coalesce(description,'')), 'C') | |
| 10 | + ) STORED; | |
| 11 | +--> statement-breakpoint | |
| 12 | +CREATE INDEX IF NOT EXISTS facilities_search_idx ON facilities USING gin (search); | |
| 13 | +--> statement-breakpoint | |
| 14 | +CREATE INDEX IF NOT EXISTS facilities_name_trgm_idx ON facilities USING gin (name gin_trgm_ops); | |
| 15 | +--> statement-breakpoint | |
| 16 | +CREATE INDEX IF NOT EXISTS facilities_city_trgm_idx ON facilities USING gin (city gin_trgm_ops); | |
| 17 | +--> statement-breakpoint | |
| 18 | +CREATE INDEX IF NOT EXISTS operators_name_trgm_idx ON operators USING gin (name gin_trgm_ops); | |
| 19 | +--> statement-breakpoint | |
| 20 | +CREATE INDEX IF NOT EXISTS metros_name_trgm_idx ON metros USING gin (name gin_trgm_ops); | |
| 21 | +--> statement-breakpoint | |
| 22 | +CREATE INDEX IF NOT EXISTS projects_name_trgm_idx ON projects USING gin (name gin_trgm_ops); | |
| 23 | +--> statement-breakpoint | |
| 24 | +CREATE INDEX IF NOT EXISTS facilities_latlng_idx ON facilities (lat, lng) WHERE lat IS NOT NULL; | |
| 25 | +--> statement-breakpoint | |
| 26 | +CREATE INDEX IF NOT EXISTS facilities_live_idx ON facilities (status, country_iso2) WHERE merged_into IS NULL; | |
added
packages/db/migrations/meta/0000_snapshot.json
+3840 −0
@@ -0,0 +1,3840 @@ | ||
| 1 | +{ | |
| 2 | + "id": "299a3c4c-177d-48f1-8ad6-24538455fa02", | |
| 3 | + "prevId": "00000000-0000-0000-0000-000000000000", | |
| 4 | + "version": "7", | |
| 5 | + "dialect": "postgresql", | |
| 6 | + "tables": { | |
| 7 | + "public.admin_sessions": { | |
| 8 | + "name": "admin_sessions", | |
| 9 | + "schema": "", | |
| 10 | + "columns": { | |
| 11 | + "id": { | |
| 12 | + "name": "id", | |
| 13 | + "type": "text", | |
| 14 | + "primaryKey": true, | |
| 15 | + "notNull": true | |
| 16 | + }, | |
| 17 | + "created_at": { | |
| 18 | + "name": "created_at", | |
| 19 | + "type": "timestamp with time zone", | |
| 20 | + "primaryKey": false, | |
| 21 | + "notNull": true, | |
| 22 | + "default": "now()" | |
| 23 | + }, | |
| 24 | + "expires_at": { | |
| 25 | + "name": "expires_at", | |
| 26 | + "type": "timestamp with time zone", | |
| 27 | + "primaryKey": false, | |
| 28 | + "notNull": true | |
| 29 | + }, | |
| 30 | + "ip": { | |
| 31 | + "name": "ip", | |
| 32 | + "type": "text", | |
| 33 | + "primaryKey": false, | |
| 34 | + "notNull": false | |
| 35 | + }, | |
| 36 | + "user_agent": { | |
| 37 | + "name": "user_agent", | |
| 38 | + "type": "text", | |
| 39 | + "primaryKey": false, | |
| 40 | + "notNull": false | |
| 41 | + } | |
| 42 | + }, | |
| 43 | + "indexes": {}, | |
| 44 | + "foreignKeys": {}, | |
| 45 | + "compositePrimaryKeys": {}, | |
| 46 | + "uniqueConstraints": {}, | |
| 47 | + "policies": {}, | |
| 48 | + "checkConstraints": {}, | |
| 49 | + "isRLSEnabled": false | |
| 50 | + }, | |
| 51 | + "public.campuses": { | |
| 52 | + "name": "campuses", | |
| 53 | + "schema": "", | |
| 54 | + "columns": { | |
| 55 | + "id": { | |
| 56 | + "name": "id", | |
| 57 | + "type": "text", | |
| 58 | + "primaryKey": true, | |
| 59 | + "notNull": true | |
| 60 | + }, | |
| 61 | + "slug": { | |
| 62 | + "name": "slug", | |
| 63 | + "type": "text", | |
| 64 | + "primaryKey": false, | |
| 65 | + "notNull": true | |
| 66 | + }, | |
| 67 | + "name": { | |
| 68 | + "name": "name", | |
| 69 | + "type": "text", | |
| 70 | + "primaryKey": false, | |
| 71 | + "notNull": true | |
| 72 | + }, | |
| 73 | + "operator_id": { | |
| 74 | + "name": "operator_id", | |
| 75 | + "type": "text", | |
| 76 | + "primaryKey": false, | |
| 77 | + "notNull": false | |
| 78 | + }, | |
| 79 | + "metro_id": { | |
| 80 | + "name": "metro_id", | |
| 81 | + "type": "text", | |
| 82 | + "primaryKey": false, | |
| 83 | + "notNull": false | |
| 84 | + }, | |
| 85 | + "country_iso2": { | |
| 86 | + "name": "country_iso2", | |
| 87 | + "type": "text", | |
| 88 | + "primaryKey": false, | |
| 89 | + "notNull": false | |
| 90 | + }, | |
| 91 | + "city": { | |
| 92 | + "name": "city", | |
| 93 | + "type": "text", | |
| 94 | + "primaryKey": false, | |
| 95 | + "notNull": false | |
| 96 | + }, | |
| 97 | + "lat": { | |
| 98 | + "name": "lat", | |
| 99 | + "type": "double precision", | |
| 100 | + "primaryKey": false, | |
| 101 | + "notNull": false | |
| 102 | + }, | |
| 103 | + "lng": { | |
| 104 | + "name": "lng", | |
| 105 | + "type": "double precision", | |
| 106 | + "primaryKey": false, | |
| 107 | + "notNull": false | |
| 108 | + }, | |
| 109 | + "external_ids": { | |
| 110 | + "name": "external_ids", | |
| 111 | + "type": "jsonb", | |
| 112 | + "primaryKey": false, | |
| 113 | + "notNull": true, | |
| 114 | + "default": "'{}'::jsonb" | |
| 115 | + }, | |
| 116 | + "created_at": { | |
| 117 | + "name": "created_at", | |
| 118 | + "type": "timestamp with time zone", | |
| 119 | + "primaryKey": false, | |
| 120 | + "notNull": true, | |
| 121 | + "default": "now()" | |
| 122 | + }, | |
| 123 | + "updated_at": { | |
| 124 | + "name": "updated_at", | |
| 125 | + "type": "timestamp with time zone", | |
| 126 | + "primaryKey": false, | |
| 127 | + "notNull": true, | |
| 128 | + "default": "now()" | |
| 129 | + } | |
| 130 | + }, | |
| 131 | + "indexes": {}, | |
| 132 | + "foreignKeys": { | |
| 133 | + "campuses_operator_id_operators_id_fk": { | |
| 134 | + "name": "campuses_operator_id_operators_id_fk", | |
| 135 | + "tableFrom": "campuses", | |
| 136 | + "tableTo": "operators", | |
| 137 | + "columnsFrom": [ | |
| 138 | + "operator_id" | |
| 139 | + ], | |
| 140 | + "columnsTo": [ | |
| 141 | + "id" | |
| 142 | + ], | |
| 143 | + "onDelete": "no action", | |
| 144 | + "onUpdate": "no action" | |
| 145 | + }, | |
| 146 | + "campuses_metro_id_metros_id_fk": { | |
| 147 | + "name": "campuses_metro_id_metros_id_fk", | |
| 148 | + "tableFrom": "campuses", | |
| 149 | + "tableTo": "metros", | |
| 150 | + "columnsFrom": [ | |
| 151 | + "metro_id" | |
| 152 | + ], | |
| 153 | + "columnsTo": [ | |
| 154 | + "id" | |
| 155 | + ], | |
| 156 | + "onDelete": "no action", | |
| 157 | + "onUpdate": "no action" | |
| 158 | + } | |
| 159 | + }, | |
| 160 | + "compositePrimaryKeys": {}, | |
| 161 | + "uniqueConstraints": { | |
| 162 | + "campuses_slug_unique": { | |
| 163 | + "name": "campuses_slug_unique", | |
| 164 | + "nullsNotDistinct": false, | |
| 165 | + "columns": [ | |
| 166 | + "slug" | |
| 167 | + ] | |
| 168 | + } | |
| 169 | + }, | |
| 170 | + "policies": {}, | |
| 171 | + "checkConstraints": {}, | |
| 172 | + "isRLSEnabled": false | |
| 173 | + }, | |
| 174 | + "public.cloud_regions": { | |
| 175 | + "name": "cloud_regions", | |
| 176 | + "schema": "", | |
| 177 | + "columns": { | |
| 178 | + "id": { | |
| 179 | + "name": "id", | |
| 180 | + "type": "text", | |
| 181 | + "primaryKey": true, | |
| 182 | + "notNull": true | |
| 183 | + }, | |
| 184 | + "slug": { | |
| 185 | + "name": "slug", | |
| 186 | + "type": "text", | |
| 187 | + "primaryKey": false, | |
| 188 | + "notNull": true | |
| 189 | + }, | |
| 190 | + "provider_id": { | |
| 191 | + "name": "provider_id", | |
| 192 | + "type": "text", | |
| 193 | + "primaryKey": false, | |
| 194 | + "notNull": true | |
| 195 | + }, | |
| 196 | + "code": { | |
| 197 | + "name": "code", | |
| 198 | + "type": "text", | |
| 199 | + "primaryKey": false, | |
| 200 | + "notNull": true | |
| 201 | + }, | |
| 202 | + "name": { | |
| 203 | + "name": "name", | |
| 204 | + "type": "text", | |
| 205 | + "primaryKey": false, | |
| 206 | + "notNull": true | |
| 207 | + }, | |
| 208 | + "city": { | |
| 209 | + "name": "city", | |
| 210 | + "type": "text", | |
| 211 | + "primaryKey": false, | |
| 212 | + "notNull": false | |
| 213 | + }, | |
| 214 | + "region_name": { | |
| 215 | + "name": "region_name", | |
| 216 | + "type": "text", | |
| 217 | + "primaryKey": false, | |
| 218 | + "notNull": false | |
| 219 | + }, | |
| 220 | + "country_iso2": { | |
| 221 | + "name": "country_iso2", | |
| 222 | + "type": "text", | |
| 223 | + "primaryKey": false, | |
| 224 | + "notNull": false | |
| 225 | + }, | |
| 226 | + "metro_id": { | |
| 227 | + "name": "metro_id", | |
| 228 | + "type": "text", | |
| 229 | + "primaryKey": false, | |
| 230 | + "notNull": false | |
| 231 | + }, | |
| 232 | + "lat": { | |
| 233 | + "name": "lat", | |
| 234 | + "type": "double precision", | |
| 235 | + "primaryKey": false, | |
| 236 | + "notNull": false | |
| 237 | + }, | |
| 238 | + "lng": { | |
| 239 | + "name": "lng", | |
| 240 | + "type": "double precision", | |
| 241 | + "primaryKey": false, | |
| 242 | + "notNull": false | |
| 243 | + }, | |
| 244 | + "geo_precision": { | |
| 245 | + "name": "geo_precision", | |
| 246 | + "type": "text", | |
| 247 | + "primaryKey": false, | |
| 248 | + "notNull": true, | |
| 249 | + "default": "'city'" | |
| 250 | + }, | |
| 251 | + "availability_zones": { | |
| 252 | + "name": "availability_zones", | |
| 253 | + "type": "integer", | |
| 254 | + "primaryKey": false, | |
| 255 | + "notNull": false | |
| 256 | + }, | |
| 257 | + "launched_on": { | |
| 258 | + "name": "launched_on", | |
| 259 | + "type": "text", | |
| 260 | + "primaryKey": false, | |
| 261 | + "notNull": false | |
| 262 | + }, | |
| 263 | + "status": { | |
| 264 | + "name": "status", | |
| 265 | + "type": "text", | |
| 266 | + "primaryKey": false, | |
| 267 | + "notNull": true, | |
| 268 | + "default": "'operational'" | |
| 269 | + }, | |
| 270 | + "is_sovereign": { | |
| 271 | + "name": "is_sovereign", | |
| 272 | + "type": "boolean", | |
| 273 | + "primaryKey": false, | |
| 274 | + "notNull": true, | |
| 275 | + "default": false | |
| 276 | + }, | |
| 277 | + "source_url": { | |
| 278 | + "name": "source_url", | |
| 279 | + "type": "text", | |
| 280 | + "primaryKey": false, | |
| 281 | + "notNull": false | |
| 282 | + }, | |
| 283 | + "external_ids": { | |
| 284 | + "name": "external_ids", | |
| 285 | + "type": "jsonb", | |
| 286 | + "primaryKey": false, | |
| 287 | + "notNull": true, | |
| 288 | + "default": "'{}'::jsonb" | |
| 289 | + }, | |
| 290 | + "created_at": { | |
| 291 | + "name": "created_at", | |
| 292 | + "type": "timestamp with time zone", | |
| 293 | + "primaryKey": false, | |
| 294 | + "notNull": true, | |
| 295 | + "default": "now()" | |
| 296 | + }, | |
| 297 | + "updated_at": { | |
| 298 | + "name": "updated_at", | |
| 299 | + "type": "timestamp with time zone", | |
| 300 | + "primaryKey": false, | |
| 301 | + "notNull": true, | |
| 302 | + "default": "now()" | |
| 303 | + } | |
| 304 | + }, | |
| 305 | + "indexes": { | |
| 306 | + "cloud_regions_provider_code_uq": { | |
| 307 | + "name": "cloud_regions_provider_code_uq", | |
| 308 | + "columns": [ | |
| 309 | + { | |
| 310 | + "expression": "provider_id", | |
| 311 | + "isExpression": false, | |
| 312 | + "asc": true, | |
| 313 | + "nulls": "last" | |
| 314 | + }, | |
| 315 | + { | |
| 316 | + "expression": "code", | |
| 317 | + "isExpression": false, | |
| 318 | + "asc": true, | |
| 319 | + "nulls": "last" | |
| 320 | + } | |
| 321 | + ], | |
| 322 | + "isUnique": true, | |
| 323 | + "concurrently": false, | |
| 324 | + "method": "btree", | |
| 325 | + "with": {} | |
| 326 | + }, | |
| 327 | + "cloud_regions_country_idx": { | |
| 328 | + "name": "cloud_regions_country_idx", | |
| 329 | + "columns": [ | |
| 330 | + { | |
| 331 | + "expression": "country_iso2", | |
| 332 | + "isExpression": false, | |
| 333 | + "asc": true, | |
| 334 | + "nulls": "last" | |
| 335 | + } | |
| 336 | + ], | |
| 337 | + "isUnique": false, | |
| 338 | + "concurrently": false, | |
| 339 | + "method": "btree", | |
| 340 | + "with": {} | |
| 341 | + } | |
| 342 | + }, | |
| 343 | + "foreignKeys": { | |
| 344 | + "cloud_regions_provider_id_operators_id_fk": { | |
| 345 | + "name": "cloud_regions_provider_id_operators_id_fk", | |
| 346 | + "tableFrom": "cloud_regions", | |
| 347 | + "tableTo": "operators", | |
| 348 | + "columnsFrom": [ | |
| 349 | + "provider_id" | |
| 350 | + ], | |
| 351 | + "columnsTo": [ | |
| 352 | + "id" | |
| 353 | + ], | |
| 354 | + "onDelete": "no action", | |
| 355 | + "onUpdate": "no action" | |
| 356 | + }, | |
| 357 | + "cloud_regions_country_iso2_countries_iso2_fk": { | |
| 358 | + "name": "cloud_regions_country_iso2_countries_iso2_fk", | |
| 359 | + "tableFrom": "cloud_regions", | |
| 360 | + "tableTo": "countries", | |
| 361 | + "columnsFrom": [ | |
| 362 | + "country_iso2" | |
| 363 | + ], | |
| 364 | + "columnsTo": [ | |
| 365 | + "iso2" | |
| 366 | + ], | |
| 367 | + "onDelete": "no action", | |
| 368 | + "onUpdate": "no action" | |
| 369 | + }, | |
| 370 | + "cloud_regions_metro_id_metros_id_fk": { | |
| 371 | + "name": "cloud_regions_metro_id_metros_id_fk", | |
| 372 | + "tableFrom": "cloud_regions", | |
| 373 | + "tableTo": "metros", | |
| 374 | + "columnsFrom": [ | |
| 375 | + "metro_id" | |
| 376 | + ], | |
| 377 | + "columnsTo": [ | |
| 378 | + "id" | |
| 379 | + ], | |
| 380 | + "onDelete": "no action", | |
| 381 | + "onUpdate": "no action" | |
| 382 | + } | |
| 383 | + }, | |
| 384 | + "compositePrimaryKeys": {}, | |
| 385 | + "uniqueConstraints": { | |
| 386 | + "cloud_regions_slug_unique": { | |
| 387 | + "name": "cloud_regions_slug_unique", | |
| 388 | + "nullsNotDistinct": false, | |
| 389 | + "columns": [ | |
| 390 | + "slug" | |
| 391 | + ] | |
| 392 | + } | |
| 393 | + }, | |
| 394 | + "policies": {}, | |
| 395 | + "checkConstraints": {}, | |
| 396 | + "isRLSEnabled": false | |
| 397 | + }, | |
| 398 | + "public.connector_runs": { | |
| 399 | + "name": "connector_runs", | |
| 400 | + "schema": "", | |
| 401 | + "columns": { | |
| 402 | + "id": { | |
| 403 | + "name": "id", | |
| 404 | + "type": "text", | |
| 405 | + "primaryKey": true, | |
| 406 | + "notNull": true | |
| 407 | + }, | |
| 408 | + "connector_id": { | |
| 409 | + "name": "connector_id", | |
| 410 | + "type": "text", | |
| 411 | + "primaryKey": false, | |
| 412 | + "notNull": true | |
| 413 | + }, | |
| 414 | + "task": { | |
| 415 | + "name": "task", | |
| 416 | + "type": "text", | |
| 417 | + "primaryKey": false, | |
| 418 | + "notNull": true, | |
| 419 | + "default": "'crawl'" | |
| 420 | + }, | |
| 421 | + "started_at": { | |
| 422 | + "name": "started_at", | |
| 423 | + "type": "timestamp with time zone", | |
| 424 | + "primaryKey": false, | |
| 425 | + "notNull": true, | |
| 426 | + "default": "now()" | |
| 427 | + }, | |
| 428 | + "finished_at": { | |
| 429 | + "name": "finished_at", | |
| 430 | + "type": "timestamp with time zone", | |
| 431 | + "primaryKey": false, | |
| 432 | + "notNull": false | |
| 433 | + }, | |
| 434 | + "status": { | |
| 435 | + "name": "status", | |
| 436 | + "type": "text", | |
| 437 | + "primaryKey": false, | |
| 438 | + "notNull": true, | |
| 439 | + "default": "'running'" | |
| 440 | + }, | |
| 441 | + "stats": { | |
| 442 | + "name": "stats", | |
| 443 | + "type": "jsonb", | |
| 444 | + "primaryKey": false, | |
| 445 | + "notNull": true, | |
| 446 | + "default": "'{}'::jsonb" | |
| 447 | + }, | |
| 448 | + "error": { | |
| 449 | + "name": "error", | |
| 450 | + "type": "text", | |
| 451 | + "primaryKey": false, | |
| 452 | + "notNull": false | |
| 453 | + }, | |
| 454 | + "log": { | |
| 455 | + "name": "log", | |
| 456 | + "type": "jsonb", | |
| 457 | + "primaryKey": false, | |
| 458 | + "notNull": true, | |
| 459 | + "default": "'[]'::jsonb" | |
| 460 | + } | |
| 461 | + }, | |
| 462 | + "indexes": { | |
| 463 | + "connector_runs_connector_idx": { | |
| 464 | + "name": "connector_runs_connector_idx", | |
| 465 | + "columns": [ | |
| 466 | + { | |
| 467 | + "expression": "connector_id", | |
| 468 | + "isExpression": false, | |
| 469 | + "asc": true, | |
| 470 | + "nulls": "last" | |
| 471 | + }, | |
| 472 | + { | |
| 473 | + "expression": "started_at", | |
| 474 | + "isExpression": false, | |
| 475 | + "asc": true, | |
| 476 | + "nulls": "last" | |
| 477 | + } | |
| 478 | + ], | |
| 479 | + "isUnique": false, | |
| 480 | + "concurrently": false, | |
| 481 | + "method": "btree", | |
| 482 | + "with": {} | |
| 483 | + } | |
| 484 | + }, | |
| 485 | + "foreignKeys": {}, | |
| 486 | + "compositePrimaryKeys": {}, | |
| 487 | + "uniqueConstraints": {}, | |
| 488 | + "policies": {}, | |
| 489 | + "checkConstraints": {}, | |
| 490 | + "isRLSEnabled": false | |
| 491 | + }, | |
| 492 | + "public.connectors": { | |
| 493 | + "name": "connectors", | |
| 494 | + "schema": "", | |
| 495 | + "columns": { | |
| 496 | + "id": { | |
| 497 | + "name": "id", | |
| 498 | + "type": "text", | |
| 499 | + "primaryKey": true, | |
| 500 | + "notNull": true | |
| 501 | + }, | |
| 502 | + "source_name": { | |
| 503 | + "name": "source_name", | |
| 504 | + "type": "text", | |
| 505 | + "primaryKey": false, | |
| 506 | + "notNull": true | |
| 507 | + }, | |
| 508 | + "domain": { | |
| 509 | + "name": "domain", | |
| 510 | + "type": "text", | |
| 511 | + "primaryKey": false, | |
| 512 | + "notNull": true | |
| 513 | + }, | |
| 514 | + "kind": { | |
| 515 | + "name": "kind", | |
| 516 | + "type": "text", | |
| 517 | + "primaryKey": false, | |
| 518 | + "notNull": true | |
| 519 | + }, | |
| 520 | + "mode": { | |
| 521 | + "name": "mode", | |
| 522 | + "type": "text", | |
| 523 | + "primaryKey": false, | |
| 524 | + "notNull": true | |
| 525 | + }, | |
| 526 | + "enabled": { | |
| 527 | + "name": "enabled", | |
| 528 | + "type": "boolean", | |
| 529 | + "primaryKey": false, | |
| 530 | + "notNull": true, | |
| 531 | + "default": true | |
| 532 | + }, | |
| 533 | + "paused": { | |
| 534 | + "name": "paused", | |
| 535 | + "type": "boolean", | |
| 536 | + "primaryKey": false, | |
| 537 | + "notNull": true, | |
| 538 | + "default": false | |
| 539 | + }, | |
| 540 | + "config": { | |
| 541 | + "name": "config", | |
| 542 | + "type": "jsonb", | |
| 543 | + "primaryKey": false, | |
| 544 | + "notNull": true, | |
| 545 | + "default": "'{}'::jsonb" | |
| 546 | + }, | |
| 547 | + "parser_version": { | |
| 548 | + "name": "parser_version", | |
| 549 | + "type": "text", | |
| 550 | + "primaryKey": false, | |
| 551 | + "notNull": true, | |
| 552 | + "default": "'v1'" | |
| 553 | + }, | |
| 554 | + "schedule": { | |
| 555 | + "name": "schedule", | |
| 556 | + "type": "jsonb", | |
| 557 | + "primaryKey": false, | |
| 558 | + "notNull": true, | |
| 559 | + "default": "'{}'::jsonb" | |
| 560 | + }, | |
| 561 | + "health": { | |
| 562 | + "name": "health", | |
| 563 | + "type": "text", | |
| 564 | + "primaryKey": false, | |
| 565 | + "notNull": true, | |
| 566 | + "default": "'never_run'" | |
| 567 | + }, | |
| 568 | + "last_run_at": { | |
| 569 | + "name": "last_run_at", | |
| 570 | + "type": "timestamp with time zone", | |
| 571 | + "primaryKey": false, | |
| 572 | + "notNull": false | |
| 573 | + }, | |
| 574 | + "last_status": { | |
| 575 | + "name": "last_status", | |
| 576 | + "type": "text", | |
| 577 | + "primaryKey": false, | |
| 578 | + "notNull": false | |
| 579 | + }, | |
| 580 | + "last_error": { | |
| 581 | + "name": "last_error", | |
| 582 | + "type": "text", | |
| 583 | + "primaryKey": false, | |
| 584 | + "notNull": false | |
| 585 | + }, | |
| 586 | + "next_run_at": { | |
| 587 | + "name": "next_run_at", | |
| 588 | + "type": "timestamp with time zone", | |
| 589 | + "primaryKey": false, | |
| 590 | + "notNull": false | |
| 591 | + }, | |
| 592 | + "stats": { | |
| 593 | + "name": "stats", | |
| 594 | + "type": "jsonb", | |
| 595 | + "primaryKey": false, | |
| 596 | + "notNull": true, | |
| 597 | + "default": "'{}'::jsonb" | |
| 598 | + }, | |
| 599 | + "created_at": { | |
| 600 | + "name": "created_at", | |
| 601 | + "type": "timestamp with time zone", | |
| 602 | + "primaryKey": false, | |
| 603 | + "notNull": true, | |
| 604 | + "default": "now()" | |
| 605 | + }, | |
| 606 | + "updated_at": { | |
| 607 | + "name": "updated_at", | |
| 608 | + "type": "timestamp with time zone", | |
| 609 | + "primaryKey": false, | |
| 610 | + "notNull": true, | |
| 611 | + "default": "now()" | |
| 612 | + } | |
| 613 | + }, | |
| 614 | + "indexes": {}, | |
| 615 | + "foreignKeys": {}, | |
| 616 | + "compositePrimaryKeys": {}, | |
| 617 | + "uniqueConstraints": {}, | |
| 618 | + "policies": {}, | |
| 619 | + "checkConstraints": {}, | |
| 620 | + "isRLSEnabled": false | |
| 621 | + }, | |
| 622 | + "public.countries": { | |
| 623 | + "name": "countries", | |
| 624 | + "schema": "", | |
| 625 | + "columns": { | |
| 626 | + "iso2": { | |
| 627 | + "name": "iso2", | |
| 628 | + "type": "text", | |
| 629 | + "primaryKey": true, | |
| 630 | + "notNull": true | |
| 631 | + }, | |
| 632 | + "iso3": { | |
| 633 | + "name": "iso3", | |
| 634 | + "type": "text", | |
| 635 | + "primaryKey": false, | |
| 636 | + "notNull": false | |
| 637 | + }, | |
| 638 | + "slug": { | |
| 639 | + "name": "slug", | |
| 640 | + "type": "text", | |
| 641 | + "primaryKey": false, | |
| 642 | + "notNull": true | |
| 643 | + }, | |
| 644 | + "name": { | |
| 645 | + "name": "name", | |
| 646 | + "type": "text", | |
| 647 | + "primaryKey": false, | |
| 648 | + "notNull": true | |
| 649 | + }, | |
| 650 | + "region": { | |
| 651 | + "name": "region", | |
| 652 | + "type": "text", | |
| 653 | + "primaryKey": false, | |
| 654 | + "notNull": false | |
| 655 | + }, | |
| 656 | + "subregion": { | |
| 657 | + "name": "subregion", | |
| 658 | + "type": "text", | |
| 659 | + "primaryKey": false, | |
| 660 | + "notNull": false | |
| 661 | + }, | |
| 662 | + "population": { | |
| 663 | + "name": "population", | |
| 664 | + "type": "bigint", | |
| 665 | + "primaryKey": false, | |
| 666 | + "notNull": false | |
| 667 | + }, | |
| 668 | + "gdp_usd": { | |
| 669 | + "name": "gdp_usd", | |
| 670 | + "type": "double precision", | |
| 671 | + "primaryKey": false, | |
| 672 | + "notNull": false | |
| 673 | + }, | |
| 674 | + "electricity_twh": { | |
| 675 | + "name": "electricity_twh", | |
| 676 | + "type": "double precision", | |
| 677 | + "primaryKey": false, | |
| 678 | + "notNull": false | |
| 679 | + }, | |
| 680 | + "renewable_share": { | |
| 681 | + "name": "renewable_share", | |
| 682 | + "type": "double precision", | |
| 683 | + "primaryKey": false, | |
| 684 | + "notNull": false | |
| 685 | + }, | |
| 686 | + "stats_year": { | |
| 687 | + "name": "stats_year", | |
| 688 | + "type": "integer", | |
| 689 | + "primaryKey": false, | |
| 690 | + "notNull": false | |
| 691 | + }, | |
| 692 | + "lat": { | |
| 693 | + "name": "lat", | |
| 694 | + "type": "double precision", | |
| 695 | + "primaryKey": false, | |
| 696 | + "notNull": false | |
| 697 | + }, | |
| 698 | + "lng": { | |
| 699 | + "name": "lng", | |
| 700 | + "type": "double precision", | |
| 701 | + "primaryKey": false, | |
| 702 | + "notNull": false | |
| 703 | + }, | |
| 704 | + "stats": { | |
| 705 | + "name": "stats", | |
| 706 | + "type": "jsonb", | |
| 707 | + "primaryKey": false, | |
| 708 | + "notNull": true, | |
| 709 | + "default": "'{}'::jsonb" | |
| 710 | + }, | |
| 711 | + "updated_at": { | |
| 712 | + "name": "updated_at", | |
| 713 | + "type": "timestamp with time zone", | |
| 714 | + "primaryKey": false, | |
| 715 | + "notNull": true, | |
| 716 | + "default": "now()" | |
| 717 | + } | |
| 718 | + }, | |
| 719 | + "indexes": {}, | |
| 720 | + "foreignKeys": {}, | |
| 721 | + "compositePrimaryKeys": {}, | |
| 722 | + "uniqueConstraints": { | |
| 723 | + "countries_slug_unique": { | |
| 724 | + "name": "countries_slug_unique", | |
| 725 | + "nullsNotDistinct": false, | |
| 726 | + "columns": [ | |
| 727 | + "slug" | |
| 728 | + ] | |
| 729 | + } | |
| 730 | + }, | |
| 731 | + "policies": {}, | |
| 732 | + "checkConstraints": {}, | |
| 733 | + "isRLSEnabled": false | |
| 734 | + }, | |
| 735 | + "public.daily_metrics": { | |
| 736 | + "name": "daily_metrics", | |
| 737 | + "schema": "", | |
| 738 | + "columns": { | |
| 739 | + "day": { | |
| 740 | + "name": "day", | |
| 741 | + "type": "date", | |
| 742 | + "primaryKey": false, | |
| 743 | + "notNull": true | |
| 744 | + }, | |
| 745 | + "metric": { | |
| 746 | + "name": "metric", | |
| 747 | + "type": "text", | |
| 748 | + "primaryKey": false, | |
| 749 | + "notNull": true | |
| 750 | + }, | |
| 751 | + "dim": { | |
| 752 | + "name": "dim", | |
| 753 | + "type": "text", | |
| 754 | + "primaryKey": false, | |
| 755 | + "notNull": true, | |
| 756 | + "default": "'global'" | |
| 757 | + }, | |
| 758 | + "value": { | |
| 759 | + "name": "value", | |
| 760 | + "type": "double precision", | |
| 761 | + "primaryKey": false, | |
| 762 | + "notNull": true | |
| 763 | + } | |
| 764 | + }, | |
| 765 | + "indexes": {}, | |
| 766 | + "foreignKeys": {}, | |
| 767 | + "compositePrimaryKeys": { | |
| 768 | + "daily_metrics_day_metric_dim_pk": { | |
| 769 | + "name": "daily_metrics_day_metric_dim_pk", | |
| 770 | + "columns": [ | |
| 771 | + "day", | |
| 772 | + "metric", | |
| 773 | + "dim" | |
| 774 | + ] | |
| 775 | + } | |
| 776 | + }, | |
| 777 | + "uniqueConstraints": {}, | |
| 778 | + "policies": {}, | |
| 779 | + "checkConstraints": {}, | |
| 780 | + "isRLSEnabled": false | |
| 781 | + }, | |
| 782 | + "public.document_versions": { | |
| 783 | + "name": "document_versions", | |
| 784 | + "schema": "", | |
| 785 | + "columns": { | |
| 786 | + "id": { | |
| 787 | + "name": "id", | |
| 788 | + "type": "text", | |
| 789 | + "primaryKey": true, | |
| 790 | + "notNull": true | |
| 791 | + }, | |
| 792 | + "document_id": { | |
| 793 | + "name": "document_id", | |
| 794 | + "type": "text", | |
| 795 | + "primaryKey": false, | |
| 796 | + "notNull": true | |
| 797 | + }, | |
| 798 | + "content_hash": { | |
| 799 | + "name": "content_hash", | |
| 800 | + "type": "text", | |
| 801 | + "primaryKey": false, | |
| 802 | + "notNull": true | |
| 803 | + }, | |
| 804 | + "fetched_at": { | |
| 805 | + "name": "fetched_at", | |
| 806 | + "type": "timestamp with time zone", | |
| 807 | + "primaryKey": false, | |
| 808 | + "notNull": true, | |
| 809 | + "default": "now()" | |
| 810 | + }, | |
| 811 | + "fetch_level": { | |
| 812 | + "name": "fetch_level", | |
| 813 | + "type": "integer", | |
| 814 | + "primaryKey": false, | |
| 815 | + "notNull": true, | |
| 816 | + "default": 1 | |
| 817 | + }, | |
| 818 | + "status_code": { | |
| 819 | + "name": "status_code", | |
| 820 | + "type": "integer", | |
| 821 | + "primaryKey": false, | |
| 822 | + "notNull": false | |
| 823 | + }, | |
| 824 | + "size_bytes": { | |
| 825 | + "name": "size_bytes", | |
| 826 | + "type": "integer", | |
| 827 | + "primaryKey": false, | |
| 828 | + "notNull": false | |
| 829 | + }, | |
| 830 | + "storage_key": { | |
| 831 | + "name": "storage_key", | |
| 832 | + "type": "text", | |
| 833 | + "primaryKey": false, | |
| 834 | + "notNull": false | |
| 835 | + }, | |
| 836 | + "diff_summary": { | |
| 837 | + "name": "diff_summary", | |
| 838 | + "type": "jsonb", | |
| 839 | + "primaryKey": false, | |
| 840 | + "notNull": false | |
| 841 | + }, | |
| 842 | + "detected_changes": { | |
| 843 | + "name": "detected_changes", | |
| 844 | + "type": "jsonb", | |
| 845 | + "primaryKey": false, | |
| 846 | + "notNull": true, | |
| 847 | + "default": "'[]'::jsonb" | |
| 848 | + }, | |
| 849 | + "significance": { | |
| 850 | + "name": "significance", | |
| 851 | + "type": "integer", | |
| 852 | + "primaryKey": false, | |
| 853 | + "notNull": true, | |
| 854 | + "default": 0 | |
| 855 | + } | |
| 856 | + }, | |
| 857 | + "indexes": { | |
| 858 | + "document_versions_doc_idx": { | |
| 859 | + "name": "document_versions_doc_idx", | |
| 860 | + "columns": [ | |
| 861 | + { | |
| 862 | + "expression": "document_id", | |
| 863 | + "isExpression": false, | |
| 864 | + "asc": true, | |
| 865 | + "nulls": "last" | |
| 866 | + }, | |
| 867 | + { | |
| 868 | + "expression": "fetched_at", | |
| 869 | + "isExpression": false, | |
| 870 | + "asc": true, | |
| 871 | + "nulls": "last" | |
| 872 | + } | |
| 873 | + ], | |
| 874 | + "isUnique": false, | |
| 875 | + "concurrently": false, | |
| 876 | + "method": "btree", | |
| 877 | + "with": {} | |
| 878 | + } | |
| 879 | + }, | |
| 880 | + "foreignKeys": { | |
| 881 | + "document_versions_document_id_documents_id_fk": { | |
| 882 | + "name": "document_versions_document_id_documents_id_fk", | |
| 883 | + "tableFrom": "document_versions", | |
| 884 | + "tableTo": "documents", | |
| 885 | + "columnsFrom": [ | |
| 886 | + "document_id" | |
| 887 | + ], | |
| 888 | + "columnsTo": [ | |
| 889 | + "id" | |
| 890 | + ], | |
| 891 | + "onDelete": "cascade", | |
| 892 | + "onUpdate": "no action" | |
| 893 | + } | |
| 894 | + }, | |
| 895 | + "compositePrimaryKeys": {}, | |
| 896 | + "uniqueConstraints": {}, | |
| 897 | + "policies": {}, | |
| 898 | + "checkConstraints": {}, | |
| 899 | + "isRLSEnabled": false | |
| 900 | + }, | |
| 901 | + "public.documents": { | |
| 902 | + "name": "documents", | |
| 903 | + "schema": "", | |
| 904 | + "columns": { | |
| 905 | + "id": { | |
| 906 | + "name": "id", | |
| 907 | + "type": "text", | |
| 908 | + "primaryKey": true, | |
| 909 | + "notNull": true | |
| 910 | + }, | |
| 911 | + "connector_id": { | |
| 912 | + "name": "connector_id", | |
| 913 | + "type": "text", | |
| 914 | + "primaryKey": false, | |
| 915 | + "notNull": true | |
| 916 | + }, | |
| 917 | + "source_id": { | |
| 918 | + "name": "source_id", | |
| 919 | + "type": "text", | |
| 920 | + "primaryKey": false, | |
| 921 | + "notNull": true | |
| 922 | + }, | |
| 923 | + "url": { | |
| 924 | + "name": "url", | |
| 925 | + "type": "text", | |
| 926 | + "primaryKey": false, | |
| 927 | + "notNull": true | |
| 928 | + }, | |
| 929 | + "canonical_url": { | |
| 930 | + "name": "canonical_url", | |
| 931 | + "type": "text", | |
| 932 | + "primaryKey": false, | |
| 933 | + "notNull": true | |
| 934 | + }, | |
| 935 | + "url_fingerprint": { | |
| 936 | + "name": "url_fingerprint", | |
| 937 | + "type": "text", | |
| 938 | + "primaryKey": false, | |
| 939 | + "notNull": true | |
| 940 | + }, | |
| 941 | + "page_type": { | |
| 942 | + "name": "page_type", | |
| 943 | + "type": "text", | |
| 944 | + "primaryKey": false, | |
| 945 | + "notNull": true, | |
| 946 | + "default": "'unknown'" | |
| 947 | + }, | |
| 948 | + "classifier": { | |
| 949 | + "name": "classifier", | |
| 950 | + "type": "text", | |
| 951 | + "primaryKey": false, | |
| 952 | + "notNull": false | |
| 953 | + }, | |
| 954 | + "fetch_level": { | |
| 955 | + "name": "fetch_level", | |
| 956 | + "type": "integer", | |
| 957 | + "primaryKey": false, | |
| 958 | + "notNull": true, | |
| 959 | + "default": 1 | |
| 960 | + }, | |
| 961 | + "priority": { | |
| 962 | + "name": "priority", | |
| 963 | + "type": "integer", | |
| 964 | + "primaryKey": false, | |
| 965 | + "notNull": true, | |
| 966 | + "default": 50 | |
| 967 | + }, | |
| 968 | + "content_hash": { | |
| 969 | + "name": "content_hash", | |
| 970 | + "type": "text", | |
| 971 | + "primaryKey": false, | |
| 972 | + "notNull": false | |
| 973 | + }, | |
| 974 | + "etag": { | |
| 975 | + "name": "etag", | |
| 976 | + "type": "text", | |
| 977 | + "primaryKey": false, | |
| 978 | + "notNull": false | |
| 979 | + }, | |
| 980 | + "last_modified": { | |
| 981 | + "name": "last_modified", | |
| 982 | + "type": "text", | |
| 983 | + "primaryKey": false, | |
| 984 | + "notNull": false | |
| 985 | + }, | |
| 986 | + "status_code": { | |
| 987 | + "name": "status_code", | |
| 988 | + "type": "integer", | |
| 989 | + "primaryKey": false, | |
| 990 | + "notNull": false | |
| 991 | + }, | |
| 992 | + "content_type": { | |
| 993 | + "name": "content_type", | |
| 994 | + "type": "text", | |
| 995 | + "primaryKey": false, | |
| 996 | + "notNull": false | |
| 997 | + }, | |
| 998 | + "size_bytes": { | |
| 999 | + "name": "size_bytes", | |
| 1000 | + "type": "integer", | |
| 1001 | + "primaryKey": false, | |
| 1002 | + "notNull": false | |
| 1003 | + }, | |
| 1004 | + "title": { | |
| 1005 | + "name": "title", | |
| 1006 | + "type": "text", | |
| 1007 | + "primaryKey": false, | |
| 1008 | + "notNull": false | |
| 1009 | + }, | |
| 1010 | + "storage_key": { | |
| 1011 | + "name": "storage_key", | |
| 1012 | + "type": "text", | |
| 1013 | + "primaryKey": false, | |
| 1014 | + "notNull": false | |
| 1015 | + }, | |
| 1016 | + "extractor_version": { | |
| 1017 | + "name": "extractor_version", | |
| 1018 | + "type": "text", | |
| 1019 | + "primaryKey": false, | |
| 1020 | + "notNull": false | |
| 1021 | + }, | |
| 1022 | + "extract_ok": { | |
| 1023 | + "name": "extract_ok", | |
| 1024 | + "type": "boolean", | |
| 1025 | + "primaryKey": false, | |
| 1026 | + "notNull": false | |
| 1027 | + }, | |
| 1028 | + "extract_count": { | |
| 1029 | + "name": "extract_count", | |
| 1030 | + "type": "integer", | |
| 1031 | + "primaryKey": false, | |
| 1032 | + "notNull": true, | |
| 1033 | + "default": 0 | |
| 1034 | + }, | |
| 1035 | + "error": { | |
| 1036 | + "name": "error", | |
| 1037 | + "type": "text", | |
| 1038 | + "primaryKey": false, | |
| 1039 | + "notNull": false | |
| 1040 | + }, | |
| 1041 | + "error_count": { | |
| 1042 | + "name": "error_count", | |
| 1043 | + "type": "integer", | |
| 1044 | + "primaryKey": false, | |
| 1045 | + "notNull": true, | |
| 1046 | + "default": 0 | |
| 1047 | + }, | |
| 1048 | + "first_seen": { | |
| 1049 | + "name": "first_seen", | |
| 1050 | + "type": "timestamp with time zone", | |
| 1051 | + "primaryKey": false, | |
| 1052 | + "notNull": true, | |
| 1053 | + "default": "now()" | |
| 1054 | + }, | |
| 1055 | + "last_fetched": { | |
| 1056 | + "name": "last_fetched", | |
| 1057 | + "type": "timestamp with time zone", | |
| 1058 | + "primaryKey": false, | |
| 1059 | + "notNull": false | |
| 1060 | + }, | |
| 1061 | + "last_changed": { | |
| 1062 | + "name": "last_changed", | |
| 1063 | + "type": "timestamp with time zone", | |
| 1064 | + "primaryKey": false, | |
| 1065 | + "notNull": false | |
| 1066 | + }, | |
| 1067 | + "last_checked": { | |
| 1068 | + "name": "last_checked", | |
| 1069 | + "type": "timestamp with time zone", | |
| 1070 | + "primaryKey": false, | |
| 1071 | + "notNull": false | |
| 1072 | + }, | |
| 1073 | + "next_check": { | |
| 1074 | + "name": "next_check", | |
| 1075 | + "type": "timestamp with time zone", | |
| 1076 | + "primaryKey": false, | |
| 1077 | + "notNull": false | |
| 1078 | + }, | |
| 1079 | + "change_frequency_score": { | |
| 1080 | + "name": "change_frequency_score", | |
| 1081 | + "type": "real", | |
| 1082 | + "primaryKey": false, | |
| 1083 | + "notNull": true, | |
| 1084 | + "default": 0.5 | |
| 1085 | + }, | |
| 1086 | + "fetch_count": { | |
| 1087 | + "name": "fetch_count", | |
| 1088 | + "type": "integer", | |
| 1089 | + "primaryKey": false, | |
| 1090 | + "notNull": true, | |
| 1091 | + "default": 0 | |
| 1092 | + }, | |
| 1093 | + "change_count": { | |
| 1094 | + "name": "change_count", | |
| 1095 | + "type": "integer", | |
| 1096 | + "primaryKey": false, | |
| 1097 | + "notNull": true, | |
| 1098 | + "default": 0 | |
| 1099 | + }, | |
| 1100 | + "discovered_from": { | |
| 1101 | + "name": "discovered_from", | |
| 1102 | + "type": "text", | |
| 1103 | + "primaryKey": false, | |
| 1104 | + "notNull": false | |
| 1105 | + }, | |
| 1106 | + "quarantined": { | |
| 1107 | + "name": "quarantined", | |
| 1108 | + "type": "boolean", | |
| 1109 | + "primaryKey": false, | |
| 1110 | + "notNull": true, | |
| 1111 | + "default": false | |
| 1112 | + }, | |
| 1113 | + "entity_refs": { | |
| 1114 | + "name": "entity_refs", | |
| 1115 | + "type": "jsonb", | |
| 1116 | + "primaryKey": false, | |
| 1117 | + "notNull": true, | |
| 1118 | + "default": "'[]'::jsonb" | |
| 1119 | + }, | |
| 1120 | + "updated_at": { | |
| 1121 | + "name": "updated_at", | |
| 1122 | + "type": "timestamp with time zone", | |
| 1123 | + "primaryKey": false, | |
| 1124 | + "notNull": true, | |
| 1125 | + "default": "now()" | |
| 1126 | + } | |
| 1127 | + }, | |
| 1128 | + "indexes": { | |
| 1129 | + "documents_fingerprint_uq": { | |
| 1130 | + "name": "documents_fingerprint_uq", | |
| 1131 | + "columns": [ | |
| 1132 | + { | |
| 1133 | + "expression": "url_fingerprint", | |
| 1134 | + "isExpression": false, | |
| 1135 | + "asc": true, | |
| 1136 | + "nulls": "last" | |
| 1137 | + } | |
| 1138 | + ], | |
| 1139 | + "isUnique": true, | |
| 1140 | + "concurrently": false, | |
| 1141 | + "method": "btree", | |
| 1142 | + "with": {} | |
| 1143 | + }, | |
| 1144 | + "documents_connector_idx": { | |
| 1145 | + "name": "documents_connector_idx", | |
| 1146 | + "columns": [ | |
| 1147 | + { | |
| 1148 | + "expression": "connector_id", | |
| 1149 | + "isExpression": false, | |
| 1150 | + "asc": true, | |
| 1151 | + "nulls": "last" | |
| 1152 | + } | |
| 1153 | + ], | |
| 1154 | + "isUnique": false, | |
| 1155 | + "concurrently": false, | |
| 1156 | + "method": "btree", | |
| 1157 | + "with": {} | |
| 1158 | + }, | |
| 1159 | + "documents_next_check_idx": { | |
| 1160 | + "name": "documents_next_check_idx", | |
| 1161 | + "columns": [ | |
| 1162 | + { | |
| 1163 | + "expression": "next_check", | |
| 1164 | + "isExpression": false, | |
| 1165 | + "asc": true, | |
| 1166 | + "nulls": "last" | |
| 1167 | + } | |
| 1168 | + ], | |
| 1169 | + "isUnique": false, | |
| 1170 | + "concurrently": false, | |
| 1171 | + "method": "btree", | |
| 1172 | + "with": {} | |
| 1173 | + }, | |
| 1174 | + "documents_page_type_idx": { | |
| 1175 | + "name": "documents_page_type_idx", | |
| 1176 | + "columns": [ | |
| 1177 | + { | |
| 1178 | + "expression": "page_type", | |
| 1179 | + "isExpression": false, | |
| 1180 | + "asc": true, | |
| 1181 | + "nulls": "last" | |
| 1182 | + } | |
| 1183 | + ], | |
| 1184 | + "isUnique": false, | |
| 1185 | + "concurrently": false, | |
| 1186 | + "method": "btree", | |
| 1187 | + "with": {} | |
| 1188 | + } | |
| 1189 | + }, | |
| 1190 | + "foreignKeys": {}, | |
| 1191 | + "compositePrimaryKeys": {}, | |
| 1192 | + "uniqueConstraints": {}, | |
| 1193 | + "policies": {}, | |
| 1194 | + "checkConstraints": {}, | |
| 1195 | + "isRLSEnabled": false | |
| 1196 | + }, | |
| 1197 | + "public.entity_keys": { | |
| 1198 | + "name": "entity_keys", | |
| 1199 | + "schema": "", | |
| 1200 | + "columns": { | |
| 1201 | + "key": { | |
| 1202 | + "name": "key", | |
| 1203 | + "type": "text", | |
| 1204 | + "primaryKey": true, | |
| 1205 | + "notNull": true | |
| 1206 | + }, | |
| 1207 | + "entity_type": { | |
| 1208 | + "name": "entity_type", | |
| 1209 | + "type": "text", | |
| 1210 | + "primaryKey": false, | |
| 1211 | + "notNull": true | |
| 1212 | + }, | |
| 1213 | + "entity_id": { | |
| 1214 | + "name": "entity_id", | |
| 1215 | + "type": "text", | |
| 1216 | + "primaryKey": false, | |
| 1217 | + "notNull": true | |
| 1218 | + }, | |
| 1219 | + "connector_id": { | |
| 1220 | + "name": "connector_id", | |
| 1221 | + "type": "text", | |
| 1222 | + "primaryKey": false, | |
| 1223 | + "notNull": true | |
| 1224 | + }, | |
| 1225 | + "created_at": { | |
| 1226 | + "name": "created_at", | |
| 1227 | + "type": "timestamp with time zone", | |
| 1228 | + "primaryKey": false, | |
| 1229 | + "notNull": true, | |
| 1230 | + "default": "now()" | |
| 1231 | + } | |
| 1232 | + }, | |
| 1233 | + "indexes": { | |
| 1234 | + "entity_keys_entity_idx": { | |
| 1235 | + "name": "entity_keys_entity_idx", | |
| 1236 | + "columns": [ | |
| 1237 | + { | |
| 1238 | + "expression": "entity_type", | |
| 1239 | + "isExpression": false, | |
| 1240 | + "asc": true, | |
| 1241 | + "nulls": "last" | |
| 1242 | + }, | |
| 1243 | + { | |
| 1244 | + "expression": "entity_id", | |
| 1245 | + "isExpression": false, | |
| 1246 | + "asc": true, | |
| 1247 | + "nulls": "last" | |
| 1248 | + } | |
| 1249 | + ], | |
| 1250 | + "isUnique": false, | |
| 1251 | + "concurrently": false, | |
| 1252 | + "method": "btree", | |
| 1253 | + "with": {} | |
| 1254 | + } | |
| 1255 | + }, | |
| 1256 | + "foreignKeys": {}, | |
| 1257 | + "compositePrimaryKeys": {}, | |
| 1258 | + "uniqueConstraints": {}, | |
| 1259 | + "policies": {}, | |
| 1260 | + "checkConstraints": {}, | |
| 1261 | + "isRLSEnabled": false | |
| 1262 | + }, | |
| 1263 | + "public.entity_matches": { | |
| 1264 | + "name": "entity_matches", | |
| 1265 | + "schema": "", | |
| 1266 | + "columns": { | |
| 1267 | + "id": { | |
| 1268 | + "name": "id", | |
| 1269 | + "type": "text", | |
| 1270 | + "primaryKey": true, | |
| 1271 | + "notNull": true | |
| 1272 | + }, | |
| 1273 | + "connector_id": { | |
| 1274 | + "name": "connector_id", | |
| 1275 | + "type": "text", | |
| 1276 | + "primaryKey": false, | |
| 1277 | + "notNull": true | |
| 1278 | + }, | |
| 1279 | + "candidate_key": { | |
| 1280 | + "name": "candidate_key", | |
| 1281 | + "type": "text", | |
| 1282 | + "primaryKey": false, | |
| 1283 | + "notNull": true | |
| 1284 | + }, | |
| 1285 | + "candidate": { | |
| 1286 | + "name": "candidate", | |
| 1287 | + "type": "jsonb", | |
| 1288 | + "primaryKey": false, | |
| 1289 | + "notNull": true | |
| 1290 | + }, | |
| 1291 | + "matched_facility_id": { | |
| 1292 | + "name": "matched_facility_id", | |
| 1293 | + "type": "text", | |
| 1294 | + "primaryKey": false, | |
| 1295 | + "notNull": false | |
| 1296 | + }, | |
| 1297 | + "score": { | |
| 1298 | + "name": "score", | |
| 1299 | + "type": "real", | |
| 1300 | + "primaryKey": false, | |
| 1301 | + "notNull": true | |
| 1302 | + }, | |
| 1303 | + "reasons": { | |
| 1304 | + "name": "reasons", | |
| 1305 | + "type": "text[]", | |
| 1306 | + "primaryKey": false, | |
| 1307 | + "notNull": true, | |
| 1308 | + "default": "'{}'::text[]" | |
| 1309 | + }, | |
| 1310 | + "status": { | |
| 1311 | + "name": "status", | |
| 1312 | + "type": "text", | |
| 1313 | + "primaryKey": false, | |
| 1314 | + "notNull": true, | |
| 1315 | + "default": "'pending'" | |
| 1316 | + }, | |
| 1317 | + "decided_by": { | |
| 1318 | + "name": "decided_by", | |
| 1319 | + "type": "text", | |
| 1320 | + "primaryKey": false, | |
| 1321 | + "notNull": false | |
| 1322 | + }, | |
| 1323 | + "decided_at": { | |
| 1324 | + "name": "decided_at", | |
| 1325 | + "type": "timestamp with time zone", | |
| 1326 | + "primaryKey": false, | |
| 1327 | + "notNull": false | |
| 1328 | + }, | |
| 1329 | + "created_at": { | |
| 1330 | + "name": "created_at", | |
| 1331 | + "type": "timestamp with time zone", | |
| 1332 | + "primaryKey": false, | |
| 1333 | + "notNull": true, | |
| 1334 | + "default": "now()" | |
| 1335 | + } | |
| 1336 | + }, | |
| 1337 | + "indexes": { | |
| 1338 | + "entity_matches_status_idx": { | |
| 1339 | + "name": "entity_matches_status_idx", | |
| 1340 | + "columns": [ | |
| 1341 | + { | |
| 1342 | + "expression": "status", | |
| 1343 | + "isExpression": false, | |
| 1344 | + "asc": true, | |
| 1345 | + "nulls": "last" | |
| 1346 | + } | |
| 1347 | + ], | |
| 1348 | + "isUnique": false, | |
| 1349 | + "concurrently": false, | |
| 1350 | + "method": "btree", | |
| 1351 | + "with": {} | |
| 1352 | + }, | |
| 1353 | + "entity_matches_key_idx": { | |
| 1354 | + "name": "entity_matches_key_idx", | |
| 1355 | + "columns": [ | |
| 1356 | + { | |
| 1357 | + "expression": "candidate_key", | |
| 1358 | + "isExpression": false, | |
| 1359 | + "asc": true, | |
| 1360 | + "nulls": "last" | |
| 1361 | + } | |
| 1362 | + ], | |
| 1363 | + "isUnique": false, | |
| 1364 | + "concurrently": false, | |
| 1365 | + "method": "btree", | |
| 1366 | + "with": {} | |
| 1367 | + } | |
| 1368 | + }, | |
| 1369 | + "foreignKeys": {}, | |
| 1370 | + "compositePrimaryKeys": {}, | |
| 1371 | + "uniqueConstraints": {}, | |
| 1372 | + "policies": {}, | |
| 1373 | + "checkConstraints": {}, | |
| 1374 | + "isRLSEnabled": false | |
| 1375 | + }, | |
| 1376 | + "public.events": { | |
| 1377 | + "name": "events", | |
| 1378 | + "schema": "", | |
| 1379 | + "columns": { | |
| 1380 | + "id": { | |
| 1381 | + "name": "id", | |
| 1382 | + "type": "text", | |
| 1383 | + "primaryKey": true, | |
| 1384 | + "notNull": true | |
| 1385 | + }, | |
| 1386 | + "entity_type": { | |
| 1387 | + "name": "entity_type", | |
| 1388 | + "type": "text", | |
| 1389 | + "primaryKey": false, | |
| 1390 | + "notNull": true | |
| 1391 | + }, | |
| 1392 | + "entity_id": { | |
| 1393 | + "name": "entity_id", | |
| 1394 | + "type": "text", | |
| 1395 | + "primaryKey": false, | |
| 1396 | + "notNull": false | |
| 1397 | + }, | |
| 1398 | + "event_type": { | |
| 1399 | + "name": "event_type", | |
| 1400 | + "type": "text", | |
| 1401 | + "primaryKey": false, | |
| 1402 | + "notNull": true | |
| 1403 | + }, | |
| 1404 | + "detected_at": { | |
| 1405 | + "name": "detected_at", | |
| 1406 | + "type": "timestamp with time zone", | |
| 1407 | + "primaryKey": false, | |
| 1408 | + "notNull": true, | |
| 1409 | + "default": "now()" | |
| 1410 | + }, | |
| 1411 | + "effective_date": { | |
| 1412 | + "name": "effective_date", | |
| 1413 | + "type": "text", | |
| 1414 | + "primaryKey": false, | |
| 1415 | + "notNull": false | |
| 1416 | + }, | |
| 1417 | + "old_value": { | |
| 1418 | + "name": "old_value", | |
| 1419 | + "type": "jsonb", | |
| 1420 | + "primaryKey": false, | |
| 1421 | + "notNull": false | |
| 1422 | + }, | |
| 1423 | + "new_value": { | |
| 1424 | + "name": "new_value", | |
| 1425 | + "type": "jsonb", | |
| 1426 | + "primaryKey": false, | |
| 1427 | + "notNull": false | |
| 1428 | + }, | |
| 1429 | + "source_id": { | |
| 1430 | + "name": "source_id", | |
| 1431 | + "type": "text", | |
| 1432 | + "primaryKey": false, | |
| 1433 | + "notNull": true | |
| 1434 | + }, | |
| 1435 | + "document_id": { | |
| 1436 | + "name": "document_id", | |
| 1437 | + "type": "text", | |
| 1438 | + "primaryKey": false, | |
| 1439 | + "notNull": false | |
| 1440 | + }, | |
| 1441 | + "url": { | |
| 1442 | + "name": "url", | |
| 1443 | + "type": "text", | |
| 1444 | + "primaryKey": false, | |
| 1445 | + "notNull": true | |
| 1446 | + }, | |
| 1447 | + "title": { | |
| 1448 | + "name": "title", | |
| 1449 | + "type": "text", | |
| 1450 | + "primaryKey": false, | |
| 1451 | + "notNull": true | |
| 1452 | + }, | |
| 1453 | + "summary": { | |
| 1454 | + "name": "summary", | |
| 1455 | + "type": "text", | |
| 1456 | + "primaryKey": false, | |
| 1457 | + "notNull": false | |
| 1458 | + }, | |
| 1459 | + "significance": { | |
| 1460 | + "name": "significance", | |
| 1461 | + "type": "integer", | |
| 1462 | + "primaryKey": false, | |
| 1463 | + "notNull": true, | |
| 1464 | + "default": 30 | |
| 1465 | + }, | |
| 1466 | + "confidence": { | |
| 1467 | + "name": "confidence", | |
| 1468 | + "type": "text", | |
| 1469 | + "primaryKey": false, | |
| 1470 | + "notNull": true, | |
| 1471 | + "default": "'moderate'" | |
| 1472 | + }, | |
| 1473 | + "review_status": { | |
| 1474 | + "name": "review_status", | |
| 1475 | + "type": "text", | |
| 1476 | + "primaryKey": false, | |
| 1477 | + "notNull": true, | |
| 1478 | + "default": "'auto'" | |
| 1479 | + }, | |
| 1480 | + "country_iso2": { | |
| 1481 | + "name": "country_iso2", | |
| 1482 | + "type": "text", | |
| 1483 | + "primaryKey": false, | |
| 1484 | + "notNull": false | |
| 1485 | + }, | |
| 1486 | + "operator_id": { | |
| 1487 | + "name": "operator_id", | |
| 1488 | + "type": "text", | |
| 1489 | + "primaryKey": false, | |
| 1490 | + "notNull": false | |
| 1491 | + }, | |
| 1492 | + "metro_id": { | |
| 1493 | + "name": "metro_id", | |
| 1494 | + "type": "text", | |
| 1495 | + "primaryKey": false, | |
| 1496 | + "notNull": false | |
| 1497 | + }, | |
| 1498 | + "project_id": { | |
| 1499 | + "name": "project_id", | |
| 1500 | + "type": "text", | |
| 1501 | + "primaryKey": false, | |
| 1502 | + "notNull": false | |
| 1503 | + }, | |
| 1504 | + "fingerprint": { | |
| 1505 | + "name": "fingerprint", | |
| 1506 | + "type": "text", | |
| 1507 | + "primaryKey": false, | |
| 1508 | + "notNull": true | |
| 1509 | + } | |
| 1510 | + }, | |
| 1511 | + "indexes": { | |
| 1512 | + "events_fingerprint_uq": { | |
| 1513 | + "name": "events_fingerprint_uq", | |
| 1514 | + "columns": [ | |
| 1515 | + { | |
| 1516 | + "expression": "fingerprint", | |
| 1517 | + "isExpression": false, | |
| 1518 | + "asc": true, | |
| 1519 | + "nulls": "last" | |
| 1520 | + } | |
| 1521 | + ], | |
| 1522 | + "isUnique": true, | |
| 1523 | + "concurrently": false, | |
| 1524 | + "method": "btree", | |
| 1525 | + "with": {} | |
| 1526 | + }, | |
| 1527 | + "events_detected_idx": { | |
| 1528 | + "name": "events_detected_idx", | |
| 1529 | + "columns": [ | |
| 1530 | + { | |
| 1531 | + "expression": "detected_at", | |
| 1532 | + "isExpression": false, | |
| 1533 | + "asc": true, | |
| 1534 | + "nulls": "last" | |
| 1535 | + } | |
| 1536 | + ], | |
| 1537 | + "isUnique": false, | |
| 1538 | + "concurrently": false, | |
| 1539 | + "method": "btree", | |
| 1540 | + "with": {} | |
| 1541 | + }, | |
| 1542 | + "events_entity_idx": { | |
| 1543 | + "name": "events_entity_idx", | |
| 1544 | + "columns": [ | |
| 1545 | + { | |
| 1546 | + "expression": "entity_type", | |
| 1547 | + "isExpression": false, | |
| 1548 | + "asc": true, | |
| 1549 | + "nulls": "last" | |
| 1550 | + }, | |
| 1551 | + { | |
| 1552 | + "expression": "entity_id", | |
| 1553 | + "isExpression": false, | |
| 1554 | + "asc": true, | |
| 1555 | + "nulls": "last" | |
| 1556 | + } | |
| 1557 | + ], | |
| 1558 | + "isUnique": false, | |
| 1559 | + "concurrently": false, | |
| 1560 | + "method": "btree", | |
| 1561 | + "with": {} | |
| 1562 | + }, | |
| 1563 | + "events_country_idx": { | |
| 1564 | + "name": "events_country_idx", | |
| 1565 | + "columns": [ | |
| 1566 | + { | |
| 1567 | + "expression": "country_iso2", | |
| 1568 | + "isExpression": false, | |
| 1569 | + "asc": true, | |
| 1570 | + "nulls": "last" | |
| 1571 | + } | |
| 1572 | + ], | |
| 1573 | + "isUnique": false, | |
| 1574 | + "concurrently": false, | |
| 1575 | + "method": "btree", | |
| 1576 | + "with": {} | |
| 1577 | + }, | |
| 1578 | + "events_operator_idx": { | |
| 1579 | + "name": "events_operator_idx", | |
| 1580 | + "columns": [ | |
| 1581 | + { | |
| 1582 | + "expression": "operator_id", | |
| 1583 | + "isExpression": false, | |
| 1584 | + "asc": true, | |
| 1585 | + "nulls": "last" | |
| 1586 | + } | |
| 1587 | + ], | |
| 1588 | + "isUnique": false, | |
| 1589 | + "concurrently": false, | |
| 1590 | + "method": "btree", | |
| 1591 | + "with": {} | |
| 1592 | + }, | |
| 1593 | + "events_type_idx": { | |
| 1594 | + "name": "events_type_idx", | |
| 1595 | + "columns": [ | |
| 1596 | + { | |
| 1597 | + "expression": "event_type", | |
| 1598 | + "isExpression": false, | |
| 1599 | + "asc": true, | |
| 1600 | + "nulls": "last" | |
| 1601 | + } | |
| 1602 | + ], | |
| 1603 | + "isUnique": false, | |
| 1604 | + "concurrently": false, | |
| 1605 | + "method": "btree", | |
| 1606 | + "with": {} | |
| 1607 | + } | |
| 1608 | + }, | |
| 1609 | + "foreignKeys": {}, | |
| 1610 | + "compositePrimaryKeys": {}, | |
| 1611 | + "uniqueConstraints": {}, | |
| 1612 | + "policies": {}, | |
| 1613 | + "checkConstraints": {}, | |
| 1614 | + "isRLSEnabled": false | |
| 1615 | + }, | |
| 1616 | + "public.facilities": { | |
| 1617 | + "name": "facilities", | |
| 1618 | + "schema": "", | |
| 1619 | + "columns": { | |
| 1620 | + "id": { | |
| 1621 | + "name": "id", | |
| 1622 | + "type": "text", | |
| 1623 | + "primaryKey": true, | |
| 1624 | + "notNull": true | |
| 1625 | + }, | |
| 1626 | + "slug": { | |
| 1627 | + "name": "slug", | |
| 1628 | + "type": "text", | |
| 1629 | + "primaryKey": false, | |
| 1630 | + "notNull": true | |
| 1631 | + }, | |
| 1632 | + "name": { | |
| 1633 | + "name": "name", | |
| 1634 | + "type": "text", | |
| 1635 | + "primaryKey": false, | |
| 1636 | + "notNull": true | |
| 1637 | + }, | |
| 1638 | + "normalized_name": { | |
| 1639 | + "name": "normalized_name", | |
| 1640 | + "type": "text", | |
| 1641 | + "primaryKey": false, | |
| 1642 | + "notNull": true | |
| 1643 | + }, | |
| 1644 | + "operator_id": { | |
| 1645 | + "name": "operator_id", | |
| 1646 | + "type": "text", | |
| 1647 | + "primaryKey": false, | |
| 1648 | + "notNull": false | |
| 1649 | + }, | |
| 1650 | + "owner_id": { | |
| 1651 | + "name": "owner_id", | |
| 1652 | + "type": "text", | |
| 1653 | + "primaryKey": false, | |
| 1654 | + "notNull": false | |
| 1655 | + }, | |
| 1656 | + "campus_id": { | |
| 1657 | + "name": "campus_id", | |
| 1658 | + "type": "text", | |
| 1659 | + "primaryKey": false, | |
| 1660 | + "notNull": false | |
| 1661 | + }, | |
| 1662 | + "metro_id": { | |
| 1663 | + "name": "metro_id", | |
| 1664 | + "type": "text", | |
| 1665 | + "primaryKey": false, | |
| 1666 | + "notNull": false | |
| 1667 | + }, | |
| 1668 | + "country_iso2": { | |
| 1669 | + "name": "country_iso2", | |
| 1670 | + "type": "text", | |
| 1671 | + "primaryKey": false, | |
| 1672 | + "notNull": false | |
| 1673 | + }, | |
| 1674 | + "city": { | |
| 1675 | + "name": "city", | |
| 1676 | + "type": "text", | |
| 1677 | + "primaryKey": false, | |
| 1678 | + "notNull": false | |
| 1679 | + }, | |
| 1680 | + "region_name": { | |
| 1681 | + "name": "region_name", | |
| 1682 | + "type": "text", | |
| 1683 | + "primaryKey": false, | |
| 1684 | + "notNull": false | |
| 1685 | + }, | |
| 1686 | + "address": { | |
| 1687 | + "name": "address", | |
| 1688 | + "type": "text", | |
| 1689 | + "primaryKey": false, | |
| 1690 | + "notNull": false | |
| 1691 | + }, | |
| 1692 | + "postal_code": { | |
| 1693 | + "name": "postal_code", | |
| 1694 | + "type": "text", | |
| 1695 | + "primaryKey": false, | |
| 1696 | + "notNull": false | |
| 1697 | + }, | |
| 1698 | + "lat": { | |
| 1699 | + "name": "lat", | |
| 1700 | + "type": "double precision", | |
| 1701 | + "primaryKey": false, | |
| 1702 | + "notNull": false | |
| 1703 | + }, | |
| 1704 | + "lng": { | |
| 1705 | + "name": "lng", | |
| 1706 | + "type": "double precision", | |
| 1707 | + "primaryKey": false, | |
| 1708 | + "notNull": false | |
| 1709 | + }, | |
| 1710 | + "geo_precision": { | |
| 1711 | + "name": "geo_precision", | |
| 1712 | + "type": "text", | |
| 1713 | + "primaryKey": false, | |
| 1714 | + "notNull": true, | |
| 1715 | + "default": "'unknown'" | |
| 1716 | + }, | |
| 1717 | + "geo_source": { | |
| 1718 | + "name": "geo_source", | |
| 1719 | + "type": "text", | |
| 1720 | + "primaryKey": false, | |
| 1721 | + "notNull": false | |
| 1722 | + }, | |
| 1723 | + "geohash": { | |
| 1724 | + "name": "geohash", | |
| 1725 | + "type": "text", | |
| 1726 | + "primaryKey": false, | |
| 1727 | + "notNull": false | |
| 1728 | + }, | |
| 1729 | + "status": { | |
| 1730 | + "name": "status", | |
| 1731 | + "type": "text", | |
| 1732 | + "primaryKey": false, | |
| 1733 | + "notNull": true, | |
| 1734 | + "default": "'unknown'" | |
| 1735 | + }, | |
| 1736 | + "facility_type": { | |
| 1737 | + "name": "facility_type", | |
| 1738 | + "type": "text", | |
| 1739 | + "primaryKey": false, | |
| 1740 | + "notNull": true, | |
| 1741 | + "default": "'unknown'" | |
| 1742 | + }, | |
| 1743 | + "tier": { | |
| 1744 | + "name": "tier", | |
| 1745 | + "type": "text", | |
| 1746 | + "primaryKey": false, | |
| 1747 | + "notNull": false | |
| 1748 | + }, | |
| 1749 | + "building_sqm": { | |
| 1750 | + "name": "building_sqm", | |
| 1751 | + "type": "double precision", | |
| 1752 | + "primaryKey": false, | |
| 1753 | + "notNull": false | |
| 1754 | + }, | |
| 1755 | + "site_area_ha": { | |
| 1756 | + "name": "site_area_ha", | |
| 1757 | + "type": "double precision", | |
| 1758 | + "primaryKey": false, | |
| 1759 | + "notNull": false | |
| 1760 | + }, | |
| 1761 | + "it_capacity_mw": { | |
| 1762 | + "name": "it_capacity_mw", | |
| 1763 | + "type": "double precision", | |
| 1764 | + "primaryKey": false, | |
| 1765 | + "notNull": false | |
| 1766 | + }, | |
| 1767 | + "total_power_mw": { | |
| 1768 | + "name": "total_power_mw", | |
| 1769 | + "type": "double precision", | |
| 1770 | + "primaryKey": false, | |
| 1771 | + "notNull": false | |
| 1772 | + }, | |
| 1773 | + "planned_power_mw": { | |
| 1774 | + "name": "planned_power_mw", | |
| 1775 | + "type": "double precision", | |
| 1776 | + "primaryKey": false, | |
| 1777 | + "notNull": false | |
| 1778 | + }, | |
| 1779 | + "mw_is_estimate": { | |
| 1780 | + "name": "mw_is_estimate", | |
| 1781 | + "type": "boolean", | |
| 1782 | + "primaryKey": false, | |
| 1783 | + "notNull": true, | |
| 1784 | + "default": false | |
| 1785 | + }, | |
| 1786 | + "rack_count": { | |
| 1787 | + "name": "rack_count", | |
| 1788 | + "type": "integer", | |
| 1789 | + "primaryKey": false, | |
| 1790 | + "notNull": false | |
| 1791 | + }, | |
| 1792 | + "pue": { | |
| 1793 | + "name": "pue", | |
| 1794 | + "type": "real", | |
| 1795 | + "primaryKey": false, | |
| 1796 | + "notNull": false | |
| 1797 | + }, | |
| 1798 | + "cooling_type": { | |
| 1799 | + "name": "cooling_type", | |
| 1800 | + "type": "text", | |
| 1801 | + "primaryKey": false, | |
| 1802 | + "notNull": false | |
| 1803 | + }, | |
| 1804 | + "renewable_claim": { | |
| 1805 | + "name": "renewable_claim", | |
| 1806 | + "type": "text", | |
| 1807 | + "primaryKey": false, | |
| 1808 | + "notNull": false | |
| 1809 | + }, | |
| 1810 | + "opened_on": { | |
| 1811 | + "name": "opened_on", | |
| 1812 | + "type": "text", | |
| 1813 | + "primaryKey": false, | |
| 1814 | + "notNull": false | |
| 1815 | + }, | |
| 1816 | + "construction_started_on": { | |
| 1817 | + "name": "construction_started_on", | |
| 1818 | + "type": "text", | |
| 1819 | + "primaryKey": false, | |
| 1820 | + "notNull": false | |
| 1821 | + }, | |
| 1822 | + "announced_on": { | |
| 1823 | + "name": "announced_on", | |
| 1824 | + "type": "text", | |
| 1825 | + "primaryKey": false, | |
| 1826 | + "notNull": false | |
| 1827 | + }, | |
| 1828 | + "website": { | |
| 1829 | + "name": "website", | |
| 1830 | + "type": "text", | |
| 1831 | + "primaryKey": false, | |
| 1832 | + "notNull": false | |
| 1833 | + }, | |
| 1834 | + "description": { | |
| 1835 | + "name": "description", | |
| 1836 | + "type": "text", | |
| 1837 | + "primaryKey": false, | |
| 1838 | + "notNull": false | |
| 1839 | + }, | |
| 1840 | + "is_ai": { | |
| 1841 | + "name": "is_ai", | |
| 1842 | + "type": "boolean", | |
| 1843 | + "primaryKey": false, | |
| 1844 | + "notNull": true, | |
| 1845 | + "default": false | |
| 1846 | + }, | |
| 1847 | + "is_hyperscale": { | |
| 1848 | + "name": "is_hyperscale", | |
| 1849 | + "type": "boolean", | |
| 1850 | + "primaryKey": false, | |
| 1851 | + "notNull": true, | |
| 1852 | + "default": false | |
| 1853 | + }, | |
| 1854 | + "certifications": { | |
| 1855 | + "name": "certifications", | |
| 1856 | + "type": "text[]", | |
| 1857 | + "primaryKey": false, | |
| 1858 | + "notNull": true, | |
| 1859 | + "default": "'{}'::text[]" | |
| 1860 | + }, | |
| 1861 | + "carriers_count": { | |
| 1862 | + "name": "carriers_count", | |
| 1863 | + "type": "integer", | |
| 1864 | + "primaryKey": false, | |
| 1865 | + "notNull": false | |
| 1866 | + }, | |
| 1867 | + "ixp_count": { | |
| 1868 | + "name": "ixp_count", | |
| 1869 | + "type": "integer", | |
| 1870 | + "primaryKey": false, | |
| 1871 | + "notNull": false | |
| 1872 | + }, | |
| 1873 | + "networks_count": { | |
| 1874 | + "name": "networks_count", | |
| 1875 | + "type": "integer", | |
| 1876 | + "primaryKey": false, | |
| 1877 | + "notNull": false | |
| 1878 | + }, | |
| 1879 | + "confidence": { | |
| 1880 | + "name": "confidence", | |
| 1881 | + "type": "text", | |
| 1882 | + "primaryKey": false, | |
| 1883 | + "notNull": true, | |
| 1884 | + "default": "'unverified'" | |
| 1885 | + }, | |
| 1886 | + "completeness": { | |
| 1887 | + "name": "completeness", | |
| 1888 | + "type": "integer", | |
| 1889 | + "primaryKey": false, | |
| 1890 | + "notNull": true, | |
| 1891 | + "default": 0 | |
| 1892 | + }, | |
| 1893 | + "external_ids": { | |
| 1894 | + "name": "external_ids", | |
| 1895 | + "type": "jsonb", | |
| 1896 | + "primaryKey": false, | |
| 1897 | + "notNull": true, | |
| 1898 | + "default": "'{}'::jsonb" | |
| 1899 | + }, | |
| 1900 | + "source_count": { | |
| 1901 | + "name": "source_count", | |
| 1902 | + "type": "integer", | |
| 1903 | + "primaryKey": false, | |
| 1904 | + "notNull": true, | |
| 1905 | + "default": 0 | |
| 1906 | + }, | |
| 1907 | + "first_seen": { | |
| 1908 | + "name": "first_seen", | |
| 1909 | + "type": "timestamp with time zone", | |
| 1910 | + "primaryKey": false, | |
| 1911 | + "notNull": true, | |
| 1912 | + "default": "now()" | |
| 1913 | + }, | |
| 1914 | + "last_verified": { | |
| 1915 | + "name": "last_verified", | |
| 1916 | + "type": "timestamp with time zone", | |
| 1917 | + "primaryKey": false, | |
| 1918 | + "notNull": false | |
| 1919 | + }, | |
| 1920 | + "merged_into": { | |
| 1921 | + "name": "merged_into", | |
| 1922 | + "type": "text", | |
| 1923 | + "primaryKey": false, | |
| 1924 | + "notNull": false | |
| 1925 | + }, | |
| 1926 | + "created_at": { | |
| 1927 | + "name": "created_at", | |
| 1928 | + "type": "timestamp with time zone", | |
| 1929 | + "primaryKey": false, | |
| 1930 | + "notNull": true, | |
| 1931 | + "default": "now()" | |
| 1932 | + }, | |
| 1933 | + "updated_at": { | |
| 1934 | + "name": "updated_at", | |
| 1935 | + "type": "timestamp with time zone", | |
| 1936 | + "primaryKey": false, | |
| 1937 | + "notNull": true, | |
| 1938 | + "default": "now()" | |
| 1939 | + } | |
| 1940 | + }, | |
| 1941 | + "indexes": { | |
| 1942 | + "facilities_country_idx": { | |
| 1943 | + "name": "facilities_country_idx", | |
| 1944 | + "columns": [ | |
| 1945 | + { | |
| 1946 | + "expression": "country_iso2", | |
| 1947 | + "isExpression": false, | |
| 1948 | + "asc": true, | |
| 1949 | + "nulls": "last" | |
| 1950 | + } | |
| 1951 | + ], | |
| 1952 | + "isUnique": false, | |
| 1953 | + "concurrently": false, | |
| 1954 | + "method": "btree", | |
| 1955 | + "with": {} | |
| 1956 | + }, | |
| 1957 | + "facilities_operator_idx": { | |
| 1958 | + "name": "facilities_operator_idx", | |
| 1959 | + "columns": [ | |
| 1960 | + { | |
| 1961 | + "expression": "operator_id", | |
| 1962 | + "isExpression": false, | |
| 1963 | + "asc": true, | |
| 1964 | + "nulls": "last" | |
| 1965 | + } | |
| 1966 | + ], | |
| 1967 | + "isUnique": false, | |
| 1968 | + "concurrently": false, | |
| 1969 | + "method": "btree", | |
| 1970 | + "with": {} | |
| 1971 | + }, | |
| 1972 | + "facilities_metro_idx": { | |
| 1973 | + "name": "facilities_metro_idx", | |
| 1974 | + "columns": [ | |
| 1975 | + { | |
| 1976 | + "expression": "metro_id", | |
| 1977 | + "isExpression": false, | |
| 1978 | + "asc": true, | |
| 1979 | + "nulls": "last" | |
| 1980 | + } | |
| 1981 | + ], | |
| 1982 | + "isUnique": false, | |
| 1983 | + "concurrently": false, | |
| 1984 | + "method": "btree", | |
| 1985 | + "with": {} | |
| 1986 | + }, | |
| 1987 | + "facilities_status_idx": { | |
| 1988 | + "name": "facilities_status_idx", | |
| 1989 | + "columns": [ | |
| 1990 | + { | |
| 1991 | + "expression": "status", | |
| 1992 | + "isExpression": false, | |
| 1993 | + "asc": true, | |
| 1994 | + "nulls": "last" | |
| 1995 | + } | |
| 1996 | + ], | |
| 1997 | + "isUnique": false, | |
| 1998 | + "concurrently": false, | |
| 1999 | + "method": "btree", | |
| 2000 | + "with": {} | |
| 2001 | + }, | |
| 2002 | + "facilities_type_idx": { | |
| 2003 | + "name": "facilities_type_idx", | |
| 2004 | + "columns": [ | |
| 2005 | + { | |
| 2006 | + "expression": "facility_type", | |
| 2007 | + "isExpression": false, | |
| 2008 | + "asc": true, | |
| 2009 | + "nulls": "last" | |
| 2010 | + } | |
| 2011 | + ], | |
| 2012 | + "isUnique": false, | |
| 2013 | + "concurrently": false, | |
| 2014 | + "method": "btree", | |
| 2015 | + "with": {} | |
| 2016 | + }, | |
| 2017 | + "facilities_geohash_idx": { | |
| 2018 | + "name": "facilities_geohash_idx", | |
| 2019 | + "columns": [ | |
| 2020 | + { | |
| 2021 | + "expression": "geohash", | |
| 2022 | + "isExpression": false, | |
| 2023 | + "asc": true, | |
| 2024 | + "nulls": "last" | |
| 2025 | + } | |
| 2026 | + ], | |
| 2027 | + "isUnique": false, | |
| 2028 | + "concurrently": false, | |
| 2029 | + "method": "btree", | |
| 2030 | + "with": {} | |
| 2031 | + }, | |
| 2032 | + "facilities_norm_idx": { | |
| 2033 | + "name": "facilities_norm_idx", | |
| 2034 | + "columns": [ | |
| 2035 | + { | |
| 2036 | + "expression": "normalized_name", | |
| 2037 | + "isExpression": false, | |
| 2038 | + "asc": true, | |
| 2039 | + "nulls": "last" | |
| 2040 | + } | |
| 2041 | + ], | |
| 2042 | + "isUnique": false, | |
| 2043 | + "concurrently": false, | |
| 2044 | + "method": "btree", | |
| 2045 | + "with": {} | |
| 2046 | + }, | |
| 2047 | + "facilities_updated_idx": { | |
| 2048 | + "name": "facilities_updated_idx", | |
| 2049 | + "columns": [ | |
| 2050 | + { | |
| 2051 | + "expression": "updated_at", | |
| 2052 | + "isExpression": false, | |
| 2053 | + "asc": true, | |
| 2054 | + "nulls": "last" | |
| 2055 | + } | |
| 2056 | + ], | |
| 2057 | + "isUnique": false, | |
| 2058 | + "concurrently": false, | |
| 2059 | + "method": "btree", | |
| 2060 | + "with": {} | |
| 2061 | + } | |
| 2062 | + }, | |
| 2063 | + "foreignKeys": { | |
| 2064 | + "facilities_operator_id_operators_id_fk": { | |
| 2065 | + "name": "facilities_operator_id_operators_id_fk", | |
| 2066 | + "tableFrom": "facilities", | |
| 2067 | + "tableTo": "operators", | |
| 2068 | + "columnsFrom": [ | |
| 2069 | + "operator_id" | |
| 2070 | + ], | |
| 2071 | + "columnsTo": [ | |
| 2072 | + "id" | |
| 2073 | + ], | |
| 2074 | + "onDelete": "no action", | |
| 2075 | + "onUpdate": "no action" | |
| 2076 | + }, | |
| 2077 | + "facilities_owner_id_operators_id_fk": { | |
| 2078 | + "name": "facilities_owner_id_operators_id_fk", | |
| 2079 | + "tableFrom": "facilities", | |
| 2080 | + "tableTo": "operators", | |
| 2081 | + "columnsFrom": [ | |
| 2082 | + "owner_id" | |
| 2083 | + ], | |
| 2084 | + "columnsTo": [ | |
| 2085 | + "id" | |
| 2086 | + ], | |
| 2087 | + "onDelete": "no action", | |
| 2088 | + "onUpdate": "no action" | |
| 2089 | + }, | |
| 2090 | + "facilities_campus_id_campuses_id_fk": { | |
| 2091 | + "name": "facilities_campus_id_campuses_id_fk", | |
| 2092 | + "tableFrom": "facilities", | |
| 2093 | + "tableTo": "campuses", | |
| 2094 | + "columnsFrom": [ | |
| 2095 | + "campus_id" | |
| 2096 | + ], | |
| 2097 | + "columnsTo": [ | |
| 2098 | + "id" | |
| 2099 | + ], | |
| 2100 | + "onDelete": "no action", | |
| 2101 | + "onUpdate": "no action" | |
| 2102 | + }, | |
| 2103 | + "facilities_metro_id_metros_id_fk": { | |
| 2104 | + "name": "facilities_metro_id_metros_id_fk", | |
| 2105 | + "tableFrom": "facilities", | |
| 2106 | + "tableTo": "metros", | |
| 2107 | + "columnsFrom": [ | |
| 2108 | + "metro_id" | |
| 2109 | + ], | |
| 2110 | + "columnsTo": [ | |
| 2111 | + "id" | |
| 2112 | + ], | |
| 2113 | + "onDelete": "no action", | |
| 2114 | + "onUpdate": "no action" | |
| 2115 | + }, | |
| 2116 | + "facilities_country_iso2_countries_iso2_fk": { | |
| 2117 | + "name": "facilities_country_iso2_countries_iso2_fk", | |
| 2118 | + "tableFrom": "facilities", | |
| 2119 | + "tableTo": "countries", | |
| 2120 | + "columnsFrom": [ | |
| 2121 | + "country_iso2" | |
| 2122 | + ], | |
| 2123 | + "columnsTo": [ | |
| 2124 | + "iso2" | |
| 2125 | + ], | |
| 2126 | + "onDelete": "no action", | |
| 2127 | + "onUpdate": "no action" | |
| 2128 | + } | |
| 2129 | + }, | |
| 2130 | + "compositePrimaryKeys": {}, | |
| 2131 | + "uniqueConstraints": { | |
| 2132 | + "facilities_slug_unique": { | |
| 2133 | + "name": "facilities_slug_unique", | |
| 2134 | + "nullsNotDistinct": false, | |
| 2135 | + "columns": [ | |
| 2136 | + "slug" | |
| 2137 | + ] | |
| 2138 | + } | |
| 2139 | + }, | |
| 2140 | + "policies": {}, | |
| 2141 | + "checkConstraints": {}, | |
| 2142 | + "isRLSEnabled": false | |
| 2143 | + }, | |
| 2144 | + "public.facility_aliases": { | |
| 2145 | + "name": "facility_aliases", | |
| 2146 | + "schema": "", | |
| 2147 | + "columns": { | |
| 2148 | + "facility_id": { | |
| 2149 | + "name": "facility_id", | |
| 2150 | + "type": "text", | |
| 2151 | + "primaryKey": false, | |
| 2152 | + "notNull": true | |
| 2153 | + }, | |
| 2154 | + "alias": { | |
| 2155 | + "name": "alias", | |
| 2156 | + "type": "text", | |
| 2157 | + "primaryKey": false, | |
| 2158 | + "notNull": true | |
| 2159 | + }, | |
| 2160 | + "normalized": { | |
| 2161 | + "name": "normalized", | |
| 2162 | + "type": "text", | |
| 2163 | + "primaryKey": false, | |
| 2164 | + "notNull": true | |
| 2165 | + }, | |
| 2166 | + "source_id": { | |
| 2167 | + "name": "source_id", | |
| 2168 | + "type": "text", | |
| 2169 | + "primaryKey": false, | |
| 2170 | + "notNull": false | |
| 2171 | + } | |
| 2172 | + }, | |
| 2173 | + "indexes": { | |
| 2174 | + "facility_aliases_norm_idx": { | |
| 2175 | + "name": "facility_aliases_norm_idx", | |
| 2176 | + "columns": [ | |
| 2177 | + { | |
| 2178 | + "expression": "normalized", | |
| 2179 | + "isExpression": false, | |
| 2180 | + "asc": true, | |
| 2181 | + "nulls": "last" | |
| 2182 | + } | |
| 2183 | + ], | |
| 2184 | + "isUnique": false, | |
| 2185 | + "concurrently": false, | |
| 2186 | + "method": "btree", | |
| 2187 | + "with": {} | |
| 2188 | + } | |
| 2189 | + }, | |
| 2190 | + "foreignKeys": { | |
| 2191 | + "facility_aliases_facility_id_facilities_id_fk": { | |
| 2192 | + "name": "facility_aliases_facility_id_facilities_id_fk", | |
| 2193 | + "tableFrom": "facility_aliases", | |
| 2194 | + "tableTo": "facilities", | |
| 2195 | + "columnsFrom": [ | |
| 2196 | + "facility_id" | |
| 2197 | + ], | |
| 2198 | + "columnsTo": [ | |
| 2199 | + "id" | |
| 2200 | + ], | |
| 2201 | + "onDelete": "cascade", | |
| 2202 | + "onUpdate": "no action" | |
| 2203 | + } | |
| 2204 | + }, | |
| 2205 | + "compositePrimaryKeys": { | |
| 2206 | + "facility_aliases_facility_id_alias_pk": { | |
| 2207 | + "name": "facility_aliases_facility_id_alias_pk", | |
| 2208 | + "columns": [ | |
| 2209 | + "facility_id", | |
| 2210 | + "alias" | |
| 2211 | + ] | |
| 2212 | + } | |
| 2213 | + }, | |
| 2214 | + "uniqueConstraints": {}, | |
| 2215 | + "policies": {}, | |
| 2216 | + "checkConstraints": {}, | |
| 2217 | + "isRLSEnabled": false | |
| 2218 | + }, | |
| 2219 | + "public.facility_ixps": { | |
| 2220 | + "name": "facility_ixps", | |
| 2221 | + "schema": "", | |
| 2222 | + "columns": { | |
| 2223 | + "facility_id": { | |
| 2224 | + "name": "facility_id", | |
| 2225 | + "type": "text", | |
| 2226 | + "primaryKey": false, | |
| 2227 | + "notNull": true | |
| 2228 | + }, | |
| 2229 | + "ixp_id": { | |
| 2230 | + "name": "ixp_id", | |
| 2231 | + "type": "text", | |
| 2232 | + "primaryKey": false, | |
| 2233 | + "notNull": true | |
| 2234 | + }, | |
| 2235 | + "source_id": { | |
| 2236 | + "name": "source_id", | |
| 2237 | + "type": "text", | |
| 2238 | + "primaryKey": false, | |
| 2239 | + "notNull": false | |
| 2240 | + } | |
| 2241 | + }, | |
| 2242 | + "indexes": {}, | |
| 2243 | + "foreignKeys": { | |
| 2244 | + "facility_ixps_facility_id_facilities_id_fk": { | |
| 2245 | + "name": "facility_ixps_facility_id_facilities_id_fk", | |
| 2246 | + "tableFrom": "facility_ixps", | |
| 2247 | + "tableTo": "facilities", | |
| 2248 | + "columnsFrom": [ | |
| 2249 | + "facility_id" | |
| 2250 | + ], | |
| 2251 | + "columnsTo": [ | |
| 2252 | + "id" | |
| 2253 | + ], | |
| 2254 | + "onDelete": "cascade", | |
| 2255 | + "onUpdate": "no action" | |
| 2256 | + }, | |
| 2257 | + "facility_ixps_ixp_id_ixps_id_fk": { | |
| 2258 | + "name": "facility_ixps_ixp_id_ixps_id_fk", | |
| 2259 | + "tableFrom": "facility_ixps", | |
| 2260 | + "tableTo": "ixps", | |
| 2261 | + "columnsFrom": [ | |
| 2262 | + "ixp_id" | |
| 2263 | + ], | |
| 2264 | + "columnsTo": [ | |
| 2265 | + "id" | |
| 2266 | + ], | |
| 2267 | + "onDelete": "cascade", | |
| 2268 | + "onUpdate": "no action" | |
| 2269 | + } | |
| 2270 | + }, | |
| 2271 | + "compositePrimaryKeys": { | |
| 2272 | + "facility_ixps_facility_id_ixp_id_pk": { | |
| 2273 | + "name": "facility_ixps_facility_id_ixp_id_pk", | |
| 2274 | + "columns": [ | |
| 2275 | + "facility_id", | |
| 2276 | + "ixp_id" | |
| 2277 | + ] | |
| 2278 | + } | |
| 2279 | + }, | |
| 2280 | + "uniqueConstraints": {}, | |
| 2281 | + "policies": {}, | |
| 2282 | + "checkConstraints": {}, | |
| 2283 | + "isRLSEnabled": false | |
| 2284 | + }, | |
| 2285 | + "public.facility_tenants": { | |
| 2286 | + "name": "facility_tenants", | |
| 2287 | + "schema": "", | |
| 2288 | + "columns": { | |
| 2289 | + "facility_id": { | |
| 2290 | + "name": "facility_id", | |
| 2291 | + "type": "text", | |
| 2292 | + "primaryKey": false, | |
| 2293 | + "notNull": true | |
| 2294 | + }, | |
| 2295 | + "operator_id": { | |
| 2296 | + "name": "operator_id", | |
| 2297 | + "type": "text", | |
| 2298 | + "primaryKey": false, | |
| 2299 | + "notNull": true | |
| 2300 | + }, | |
| 2301 | + "role": { | |
| 2302 | + "name": "role", | |
| 2303 | + "type": "text", | |
| 2304 | + "primaryKey": false, | |
| 2305 | + "notNull": true, | |
| 2306 | + "default": "'carrier'" | |
| 2307 | + }, | |
| 2308 | + "asn": { | |
| 2309 | + "name": "asn", | |
| 2310 | + "type": "integer", | |
| 2311 | + "primaryKey": false, | |
| 2312 | + "notNull": false | |
| 2313 | + }, | |
| 2314 | + "source_id": { | |
| 2315 | + "name": "source_id", | |
| 2316 | + "type": "text", | |
| 2317 | + "primaryKey": false, | |
| 2318 | + "notNull": false | |
| 2319 | + } | |
| 2320 | + }, | |
| 2321 | + "indexes": { | |
| 2322 | + "facility_tenants_operator_idx": { | |
| 2323 | + "name": "facility_tenants_operator_idx", | |
| 2324 | + "columns": [ | |
| 2325 | + { | |
| 2326 | + "expression": "operator_id", | |
| 2327 | + "isExpression": false, | |
| 2328 | + "asc": true, | |
| 2329 | + "nulls": "last" | |
| 2330 | + } | |
| 2331 | + ], | |
| 2332 | + "isUnique": false, | |
| 2333 | + "concurrently": false, | |
| 2334 | + "method": "btree", | |
| 2335 | + "with": {} | |
| 2336 | + } | |
| 2337 | + }, | |
| 2338 | + "foreignKeys": { | |
| 2339 | + "facility_tenants_facility_id_facilities_id_fk": { | |
| 2340 | + "name": "facility_tenants_facility_id_facilities_id_fk", | |
| 2341 | + "tableFrom": "facility_tenants", | |
| 2342 | + "tableTo": "facilities", | |
| 2343 | + "columnsFrom": [ | |
| 2344 | + "facility_id" | |
| 2345 | + ], | |
| 2346 | + "columnsTo": [ | |
| 2347 | + "id" | |
| 2348 | + ], | |
| 2349 | + "onDelete": "cascade", | |
| 2350 | + "onUpdate": "no action" | |
| 2351 | + }, | |
| 2352 | + "facility_tenants_operator_id_operators_id_fk": { | |
| 2353 | + "name": "facility_tenants_operator_id_operators_id_fk", | |
| 2354 | + "tableFrom": "facility_tenants", | |
| 2355 | + "tableTo": "operators", | |
| 2356 | + "columnsFrom": [ | |
| 2357 | + "operator_id" | |
| 2358 | + ], | |
| 2359 | + "columnsTo": [ | |
| 2360 | + "id" | |
| 2361 | + ], | |
| 2362 | + "onDelete": "cascade", | |
| 2363 | + "onUpdate": "no action" | |
| 2364 | + } | |
| 2365 | + }, | |
| 2366 | + "compositePrimaryKeys": { | |
| 2367 | + "facility_tenants_facility_id_operator_id_role_pk": { | |
| 2368 | + "name": "facility_tenants_facility_id_operator_id_role_pk", | |
| 2369 | + "columns": [ | |
| 2370 | + "facility_id", | |
| 2371 | + "operator_id", | |
| 2372 | + "role" | |
| 2373 | + ] | |
| 2374 | + } | |
| 2375 | + }, | |
| 2376 | + "uniqueConstraints": {}, | |
| 2377 | + "policies": {}, | |
| 2378 | + "checkConstraints": {}, | |
| 2379 | + "isRLSEnabled": false | |
| 2380 | + }, | |
| 2381 | + "public.ixps": { | |
| 2382 | + "name": "ixps", | |
| 2383 | + "schema": "", | |
| 2384 | + "columns": { | |
| 2385 | + "id": { | |
| 2386 | + "name": "id", | |
| 2387 | + "type": "text", | |
| 2388 | + "primaryKey": true, | |
| 2389 | + "notNull": true | |
| 2390 | + }, | |
| 2391 | + "slug": { | |
| 2392 | + "name": "slug", | |
| 2393 | + "type": "text", | |
| 2394 | + "primaryKey": false, | |
| 2395 | + "notNull": true | |
| 2396 | + }, | |
| 2397 | + "name": { | |
| 2398 | + "name": "name", | |
| 2399 | + "type": "text", | |
| 2400 | + "primaryKey": false, | |
| 2401 | + "notNull": true | |
| 2402 | + }, | |
| 2403 | + "name_long": { | |
| 2404 | + "name": "name_long", | |
| 2405 | + "type": "text", | |
| 2406 | + "primaryKey": false, | |
| 2407 | + "notNull": false | |
| 2408 | + }, | |
| 2409 | + "city": { | |
| 2410 | + "name": "city", | |
| 2411 | + "type": "text", | |
| 2412 | + "primaryKey": false, | |
| 2413 | + "notNull": false | |
| 2414 | + }, | |
| 2415 | + "country_iso2": { | |
| 2416 | + "name": "country_iso2", | |
| 2417 | + "type": "text", | |
| 2418 | + "primaryKey": false, | |
| 2419 | + "notNull": false | |
| 2420 | + }, | |
| 2421 | + "metro_id": { | |
| 2422 | + "name": "metro_id", | |
| 2423 | + "type": "text", | |
| 2424 | + "primaryKey": false, | |
| 2425 | + "notNull": false | |
| 2426 | + }, | |
| 2427 | + "region_continent": { | |
| 2428 | + "name": "region_continent", | |
| 2429 | + "type": "text", | |
| 2430 | + "primaryKey": false, | |
| 2431 | + "notNull": false | |
| 2432 | + }, | |
| 2433 | + "website": { | |
| 2434 | + "name": "website", | |
| 2435 | + "type": "text", | |
| 2436 | + "primaryKey": false, | |
| 2437 | + "notNull": false | |
| 2438 | + }, | |
| 2439 | + "network_count": { | |
| 2440 | + "name": "network_count", | |
| 2441 | + "type": "integer", | |
| 2442 | + "primaryKey": false, | |
| 2443 | + "notNull": false | |
| 2444 | + }, | |
| 2445 | + "external_ids": { | |
| 2446 | + "name": "external_ids", | |
| 2447 | + "type": "jsonb", | |
| 2448 | + "primaryKey": false, | |
| 2449 | + "notNull": true, | |
| 2450 | + "default": "'{}'::jsonb" | |
| 2451 | + }, | |
| 2452 | + "created_at": { | |
| 2453 | + "name": "created_at", | |
| 2454 | + "type": "timestamp with time zone", | |
| 2455 | + "primaryKey": false, | |
| 2456 | + "notNull": true, | |
| 2457 | + "default": "now()" | |
| 2458 | + }, | |
| 2459 | + "updated_at": { | |
| 2460 | + "name": "updated_at", | |
| 2461 | + "type": "timestamp with time zone", | |
| 2462 | + "primaryKey": false, | |
| 2463 | + "notNull": true, | |
| 2464 | + "default": "now()" | |
| 2465 | + } | |
| 2466 | + }, | |
| 2467 | + "indexes": {}, | |
| 2468 | + "foreignKeys": { | |
| 2469 | + "ixps_country_iso2_countries_iso2_fk": { | |
| 2470 | + "name": "ixps_country_iso2_countries_iso2_fk", | |
| 2471 | + "tableFrom": "ixps", | |
| 2472 | + "tableTo": "countries", | |
| 2473 | + "columnsFrom": [ | |
| 2474 | + "country_iso2" | |
| 2475 | + ], | |
| 2476 | + "columnsTo": [ | |
| 2477 | + "iso2" | |
| 2478 | + ], | |
| 2479 | + "onDelete": "no action", | |
| 2480 | + "onUpdate": "no action" | |
| 2481 | + }, | |
| 2482 | + "ixps_metro_id_metros_id_fk": { | |
| 2483 | + "name": "ixps_metro_id_metros_id_fk", | |
| 2484 | + "tableFrom": "ixps", | |
| 2485 | + "tableTo": "metros", | |
| 2486 | + "columnsFrom": [ | |
| 2487 | + "metro_id" | |
| 2488 | + ], | |
| 2489 | + "columnsTo": [ | |
| 2490 | + "id" | |
| 2491 | + ], | |
| 2492 | + "onDelete": "no action", | |
| 2493 | + "onUpdate": "no action" | |
| 2494 | + } | |
| 2495 | + }, | |
| 2496 | + "compositePrimaryKeys": {}, | |
| 2497 | + "uniqueConstraints": { | |
| 2498 | + "ixps_slug_unique": { | |
| 2499 | + "name": "ixps_slug_unique", | |
| 2500 | + "nullsNotDistinct": false, | |
| 2501 | + "columns": [ | |
| 2502 | + "slug" | |
| 2503 | + ] | |
| 2504 | + } | |
| 2505 | + }, | |
| 2506 | + "policies": {}, | |
| 2507 | + "checkConstraints": {}, | |
| 2508 | + "isRLSEnabled": false | |
| 2509 | + }, | |
| 2510 | + "public.metros": { | |
| 2511 | + "name": "metros", | |
| 2512 | + "schema": "", | |
| 2513 | + "columns": { | |
| 2514 | + "id": { | |
| 2515 | + "name": "id", | |
| 2516 | + "type": "text", | |
| 2517 | + "primaryKey": true, | |
| 2518 | + "notNull": true | |
| 2519 | + }, | |
| 2520 | + "slug": { | |
| 2521 | + "name": "slug", | |
| 2522 | + "type": "text", | |
| 2523 | + "primaryKey": false, | |
| 2524 | + "notNull": true | |
| 2525 | + }, | |
| 2526 | + "name": { | |
| 2527 | + "name": "name", | |
| 2528 | + "type": "text", | |
| 2529 | + "primaryKey": false, | |
| 2530 | + "notNull": true | |
| 2531 | + }, | |
| 2532 | + "country_iso2": { | |
| 2533 | + "name": "country_iso2", | |
| 2534 | + "type": "text", | |
| 2535 | + "primaryKey": false, | |
| 2536 | + "notNull": true | |
| 2537 | + }, | |
| 2538 | + "region_name": { | |
| 2539 | + "name": "region_name", | |
| 2540 | + "type": "text", | |
| 2541 | + "primaryKey": false, | |
| 2542 | + "notNull": false | |
| 2543 | + }, | |
| 2544 | + "lat": { | |
| 2545 | + "name": "lat", | |
| 2546 | + "type": "double precision", | |
| 2547 | + "primaryKey": false, | |
| 2548 | + "notNull": true | |
| 2549 | + }, | |
| 2550 | + "lng": { | |
| 2551 | + "name": "lng", | |
| 2552 | + "type": "double precision", | |
| 2553 | + "primaryKey": false, | |
| 2554 | + "notNull": true | |
| 2555 | + }, | |
| 2556 | + "radius_km": { | |
| 2557 | + "name": "radius_km", | |
| 2558 | + "type": "real", | |
| 2559 | + "primaryKey": false, | |
| 2560 | + "notNull": true, | |
| 2561 | + "default": 60 | |
| 2562 | + }, | |
| 2563 | + "aliases": { | |
| 2564 | + "name": "aliases", | |
| 2565 | + "type": "text[]", | |
| 2566 | + "primaryKey": false, | |
| 2567 | + "notNull": true, | |
| 2568 | + "default": "'{}'::text[]" | |
| 2569 | + }, | |
| 2570 | + "description": { | |
| 2571 | + "name": "description", | |
| 2572 | + "type": "text", | |
| 2573 | + "primaryKey": false, | |
| 2574 | + "notNull": false | |
| 2575 | + }, | |
| 2576 | + "stats": { | |
| 2577 | + "name": "stats", | |
| 2578 | + "type": "jsonb", | |
| 2579 | + "primaryKey": false, | |
| 2580 | + "notNull": true, | |
| 2581 | + "default": "'{}'::jsonb" | |
| 2582 | + }, | |
| 2583 | + "created_at": { | |
| 2584 | + "name": "created_at", | |
| 2585 | + "type": "timestamp with time zone", | |
| 2586 | + "primaryKey": false, | |
| 2587 | + "notNull": true, | |
| 2588 | + "default": "now()" | |
| 2589 | + }, | |
| 2590 | + "updated_at": { | |
| 2591 | + "name": "updated_at", | |
| 2592 | + "type": "timestamp with time zone", | |
| 2593 | + "primaryKey": false, | |
| 2594 | + "notNull": true, | |
| 2595 | + "default": "now()" | |
| 2596 | + } | |
| 2597 | + }, | |
| 2598 | + "indexes": { | |
| 2599 | + "metros_country_idx": { | |
| 2600 | + "name": "metros_country_idx", | |
| 2601 | + "columns": [ | |
| 2602 | + { | |
| 2603 | + "expression": "country_iso2", | |
| 2604 | + "isExpression": false, | |
| 2605 | + "asc": true, | |
| 2606 | + "nulls": "last" | |
| 2607 | + } | |
| 2608 | + ], | |
| 2609 | + "isUnique": false, | |
| 2610 | + "concurrently": false, | |
| 2611 | + "method": "btree", | |
| 2612 | + "with": {} | |
| 2613 | + } | |
| 2614 | + }, | |
| 2615 | + "foreignKeys": { | |
| 2616 | + "metros_country_iso2_countries_iso2_fk": { | |
| 2617 | + "name": "metros_country_iso2_countries_iso2_fk", | |
| 2618 | + "tableFrom": "metros", | |
| 2619 | + "tableTo": "countries", | |
| 2620 | + "columnsFrom": [ | |
| 2621 | + "country_iso2" | |
| 2622 | + ], | |
| 2623 | + "columnsTo": [ | |
| 2624 | + "iso2" | |
| 2625 | + ], | |
| 2626 | + "onDelete": "no action", | |
| 2627 | + "onUpdate": "no action" | |
| 2628 | + } | |
| 2629 | + }, | |
| 2630 | + "compositePrimaryKeys": {}, | |
| 2631 | + "uniqueConstraints": { | |
| 2632 | + "metros_slug_unique": { | |
| 2633 | + "name": "metros_slug_unique", | |
| 2634 | + "nullsNotDistinct": false, | |
| 2635 | + "columns": [ | |
| 2636 | + "slug" | |
| 2637 | + ] | |
| 2638 | + } | |
| 2639 | + }, | |
| 2640 | + "policies": {}, | |
| 2641 | + "checkConstraints": {}, | |
| 2642 | + "isRLSEnabled": false | |
| 2643 | + }, | |
| 2644 | + "public.news_items": { | |
| 2645 | + "name": "news_items", | |
| 2646 | + "schema": "", | |
| 2647 | + "columns": { | |
| 2648 | + "id": { | |
| 2649 | + "name": "id", | |
| 2650 | + "type": "text", | |
| 2651 | + "primaryKey": true, | |
| 2652 | + "notNull": true | |
| 2653 | + }, | |
| 2654 | + "source_id": { | |
| 2655 | + "name": "source_id", | |
| 2656 | + "type": "text", | |
| 2657 | + "primaryKey": false, | |
| 2658 | + "notNull": true | |
| 2659 | + }, | |
| 2660 | + "connector_id": { | |
| 2661 | + "name": "connector_id", | |
| 2662 | + "type": "text", | |
| 2663 | + "primaryKey": false, | |
| 2664 | + "notNull": true | |
| 2665 | + }, | |
| 2666 | + "url": { | |
| 2667 | + "name": "url", | |
| 2668 | + "type": "text", | |
| 2669 | + "primaryKey": false, | |
| 2670 | + "notNull": true | |
| 2671 | + }, | |
| 2672 | + "title": { | |
| 2673 | + "name": "title", | |
| 2674 | + "type": "text", | |
| 2675 | + "primaryKey": false, | |
| 2676 | + "notNull": true | |
| 2677 | + }, | |
| 2678 | + "published_at": { | |
| 2679 | + "name": "published_at", | |
| 2680 | + "type": "timestamp with time zone", | |
| 2681 | + "primaryKey": false, | |
| 2682 | + "notNull": false | |
| 2683 | + }, | |
| 2684 | + "summary": { | |
| 2685 | + "name": "summary", | |
| 2686 | + "type": "text", | |
| 2687 | + "primaryKey": false, | |
| 2688 | + "notNull": false | |
| 2689 | + }, | |
| 2690 | + "page_type": { | |
| 2691 | + "name": "page_type", | |
| 2692 | + "type": "text", | |
| 2693 | + "primaryKey": false, | |
| 2694 | + "notNull": true, | |
| 2695 | + "default": "'unknown'" | |
| 2696 | + }, | |
| 2697 | + "event_type": { | |
| 2698 | + "name": "event_type", | |
| 2699 | + "type": "text", | |
| 2700 | + "primaryKey": false, | |
| 2701 | + "notNull": false | |
| 2702 | + }, | |
| 2703 | + "mentions": { | |
| 2704 | + "name": "mentions", | |
| 2705 | + "type": "jsonb", | |
| 2706 | + "primaryKey": false, | |
| 2707 | + "notNull": true, | |
| 2708 | + "default": "'{}'::jsonb" | |
| 2709 | + }, | |
| 2710 | + "operator_ids": { | |
| 2711 | + "name": "operator_ids", | |
| 2712 | + "type": "text[]", | |
| 2713 | + "primaryKey": false, | |
| 2714 | + "notNull": true, | |
| 2715 | + "default": "'{}'::text[]" | |
| 2716 | + }, | |
| 2717 | + "country_iso2s": { | |
| 2718 | + "name": "country_iso2s", | |
| 2719 | + "type": "text[]", | |
| 2720 | + "primaryKey": false, | |
| 2721 | + "notNull": true, | |
| 2722 | + "default": "'{}'::text[]" | |
| 2723 | + }, | |
| 2724 | + "facility_ids": { | |
| 2725 | + "name": "facility_ids", | |
| 2726 | + "type": "text[]", | |
| 2727 | + "primaryKey": false, | |
| 2728 | + "notNull": true, | |
| 2729 | + "default": "'{}'::text[]" | |
| 2730 | + }, | |
| 2731 | + "project_id": { | |
| 2732 | + "name": "project_id", | |
| 2733 | + "type": "text", | |
| 2734 | + "primaryKey": false, | |
| 2735 | + "notNull": false | |
| 2736 | + }, | |
| 2737 | + "mw": { | |
| 2738 | + "name": "mw", | |
| 2739 | + "type": "double precision", | |
| 2740 | + "primaryKey": false, | |
| 2741 | + "notNull": false | |
| 2742 | + }, | |
| 2743 | + "significance": { | |
| 2744 | + "name": "significance", | |
| 2745 | + "type": "integer", | |
| 2746 | + "primaryKey": false, | |
| 2747 | + "notNull": true, | |
| 2748 | + "default": 20 | |
| 2749 | + }, | |
| 2750 | + "created_at": { | |
| 2751 | + "name": "created_at", | |
| 2752 | + "type": "timestamp with time zone", | |
| 2753 | + "primaryKey": false, | |
| 2754 | + "notNull": true, | |
| 2755 | + "default": "now()" | |
| 2756 | + } | |
| 2757 | + }, | |
| 2758 | + "indexes": { | |
| 2759 | + "news_items_published_idx": { | |
| 2760 | + "name": "news_items_published_idx", | |
| 2761 | + "columns": [ | |
| 2762 | + { | |
| 2763 | + "expression": "published_at", | |
| 2764 | + "isExpression": false, | |
| 2765 | + "asc": true, | |
| 2766 | + "nulls": "last" | |
| 2767 | + } | |
| 2768 | + ], | |
| 2769 | + "isUnique": false, | |
| 2770 | + "concurrently": false, | |
| 2771 | + "method": "btree", | |
| 2772 | + "with": {} | |
| 2773 | + } | |
| 2774 | + }, | |
| 2775 | + "foreignKeys": {}, | |
| 2776 | + "compositePrimaryKeys": {}, | |
| 2777 | + "uniqueConstraints": { | |
| 2778 | + "news_items_url_unique": { | |
| 2779 | + "name": "news_items_url_unique", | |
| 2780 | + "nullsNotDistinct": false, | |
| 2781 | + "columns": [ | |
| 2782 | + "url" | |
| 2783 | + ] | |
| 2784 | + } | |
| 2785 | + }, | |
| 2786 | + "policies": {}, | |
| 2787 | + "checkConstraints": {}, | |
| 2788 | + "isRLSEnabled": false | |
| 2789 | + }, | |
| 2790 | + "public.operators": { | |
| 2791 | + "name": "operators", | |
| 2792 | + "schema": "", | |
| 2793 | + "columns": { | |
| 2794 | + "id": { | |
| 2795 | + "name": "id", | |
| 2796 | + "type": "text", | |
| 2797 | + "primaryKey": true, | |
| 2798 | + "notNull": true | |
| 2799 | + }, | |
| 2800 | + "slug": { | |
| 2801 | + "name": "slug", | |
| 2802 | + "type": "text", | |
| 2803 | + "primaryKey": false, | |
| 2804 | + "notNull": true | |
| 2805 | + }, | |
| 2806 | + "name": { | |
| 2807 | + "name": "name", | |
| 2808 | + "type": "text", | |
| 2809 | + "primaryKey": false, | |
| 2810 | + "notNull": true | |
| 2811 | + }, | |
| 2812 | + "normalized_name": { | |
| 2813 | + "name": "normalized_name", | |
| 2814 | + "type": "text", | |
| 2815 | + "primaryKey": false, | |
| 2816 | + "notNull": true | |
| 2817 | + }, | |
| 2818 | + "kind": { | |
| 2819 | + "name": "kind", | |
| 2820 | + "type": "text", | |
| 2821 | + "primaryKey": false, | |
| 2822 | + "notNull": false | |
| 2823 | + }, | |
| 2824 | + "website": { | |
| 2825 | + "name": "website", | |
| 2826 | + "type": "text", | |
| 2827 | + "primaryKey": false, | |
| 2828 | + "notNull": false | |
| 2829 | + }, | |
| 2830 | + "hq_country_iso2": { | |
| 2831 | + "name": "hq_country_iso2", | |
| 2832 | + "type": "text", | |
| 2833 | + "primaryKey": false, | |
| 2834 | + "notNull": false | |
| 2835 | + }, | |
| 2836 | + "hq_city": { | |
| 2837 | + "name": "hq_city", | |
| 2838 | + "type": "text", | |
| 2839 | + "primaryKey": false, | |
| 2840 | + "notNull": false | |
| 2841 | + }, | |
| 2842 | + "parent_id": { | |
| 2843 | + "name": "parent_id", | |
| 2844 | + "type": "text", | |
| 2845 | + "primaryKey": false, | |
| 2846 | + "notNull": false | |
| 2847 | + }, | |
| 2848 | + "description": { | |
| 2849 | + "name": "description", | |
| 2850 | + "type": "text", | |
| 2851 | + "primaryKey": false, | |
| 2852 | + "notNull": false | |
| 2853 | + }, | |
| 2854 | + "aliases": { | |
| 2855 | + "name": "aliases", | |
| 2856 | + "type": "text[]", | |
| 2857 | + "primaryKey": false, | |
| 2858 | + "notNull": true, | |
| 2859 | + "default": "'{}'::text[]" | |
| 2860 | + }, | |
| 2861 | + "external_ids": { | |
| 2862 | + "name": "external_ids", | |
| 2863 | + "type": "jsonb", | |
| 2864 | + "primaryKey": false, | |
| 2865 | + "notNull": true, | |
| 2866 | + "default": "'{}'::jsonb" | |
| 2867 | + }, | |
| 2868 | + "is_cloud_provider": { | |
| 2869 | + "name": "is_cloud_provider", | |
| 2870 | + "type": "boolean", | |
| 2871 | + "primaryKey": false, | |
| 2872 | + "notNull": true, | |
| 2873 | + "default": false | |
| 2874 | + }, | |
| 2875 | + "is_carrier": { | |
| 2876 | + "name": "is_carrier", | |
| 2877 | + "type": "boolean", | |
| 2878 | + "primaryKey": false, | |
| 2879 | + "notNull": true, | |
| 2880 | + "default": false | |
| 2881 | + }, | |
| 2882 | + "stats": { | |
| 2883 | + "name": "stats", | |
| 2884 | + "type": "jsonb", | |
| 2885 | + "primaryKey": false, | |
| 2886 | + "notNull": true, | |
| 2887 | + "default": "'{}'::jsonb" | |
| 2888 | + }, | |
| 2889 | + "created_at": { | |
| 2890 | + "name": "created_at", | |
| 2891 | + "type": "timestamp with time zone", | |
| 2892 | + "primaryKey": false, | |
| 2893 | + "notNull": true, | |
| 2894 | + "default": "now()" | |
| 2895 | + }, | |
| 2896 | + "updated_at": { | |
| 2897 | + "name": "updated_at", | |
| 2898 | + "type": "timestamp with time zone", | |
| 2899 | + "primaryKey": false, | |
| 2900 | + "notNull": true, | |
| 2901 | + "default": "now()" | |
| 2902 | + } | |
| 2903 | + }, | |
| 2904 | + "indexes": { | |
| 2905 | + "operators_norm_idx": { | |
| 2906 | + "name": "operators_norm_idx", | |
| 2907 | + "columns": [ | |
| 2908 | + { | |
| 2909 | + "expression": "normalized_name", | |
| 2910 | + "isExpression": false, | |
| 2911 | + "asc": true, | |
| 2912 | + "nulls": "last" | |
| 2913 | + } | |
| 2914 | + ], | |
| 2915 | + "isUnique": false, | |
| 2916 | + "concurrently": false, | |
| 2917 | + "method": "btree", | |
| 2918 | + "with": {} | |
| 2919 | + }, | |
| 2920 | + "operators_kind_idx": { | |
| 2921 | + "name": "operators_kind_idx", | |
| 2922 | + "columns": [ | |
| 2923 | + { | |
| 2924 | + "expression": "kind", | |
| 2925 | + "isExpression": false, | |
| 2926 | + "asc": true, | |
| 2927 | + "nulls": "last" | |
| 2928 | + } | |
| 2929 | + ], | |
| 2930 | + "isUnique": false, | |
| 2931 | + "concurrently": false, | |
| 2932 | + "method": "btree", | |
| 2933 | + "with": {} | |
| 2934 | + } | |
| 2935 | + }, | |
| 2936 | + "foreignKeys": {}, | |
| 2937 | + "compositePrimaryKeys": {}, | |
| 2938 | + "uniqueConstraints": { | |
| 2939 | + "operators_slug_unique": { | |
| 2940 | + "name": "operators_slug_unique", | |
| 2941 | + "nullsNotDistinct": false, | |
| 2942 | + "columns": [ | |
| 2943 | + "slug" | |
| 2944 | + ] | |
| 2945 | + } | |
| 2946 | + }, | |
| 2947 | + "policies": {}, | |
| 2948 | + "checkConstraints": {}, | |
| 2949 | + "isRLSEnabled": false | |
| 2950 | + }, | |
| 2951 | + "public.project_timeline": { | |
| 2952 | + "name": "project_timeline", | |
| 2953 | + "schema": "", | |
| 2954 | + "columns": { | |
| 2955 | + "id": { | |
| 2956 | + "name": "id", | |
| 2957 | + "type": "text", | |
| 2958 | + "primaryKey": true, | |
| 2959 | + "notNull": true | |
| 2960 | + }, | |
| 2961 | + "project_id": { | |
| 2962 | + "name": "project_id", | |
| 2963 | + "type": "text", | |
| 2964 | + "primaryKey": false, | |
| 2965 | + "notNull": true | |
| 2966 | + }, | |
| 2967 | + "event_date": { | |
| 2968 | + "name": "event_date", | |
| 2969 | + "type": "text", | |
| 2970 | + "primaryKey": false, | |
| 2971 | + "notNull": true | |
| 2972 | + }, | |
| 2973 | + "event_type": { | |
| 2974 | + "name": "event_type", | |
| 2975 | + "type": "text", | |
| 2976 | + "primaryKey": false, | |
| 2977 | + "notNull": true | |
| 2978 | + }, | |
| 2979 | + "description": { | |
| 2980 | + "name": "description", | |
| 2981 | + "type": "text", | |
| 2982 | + "primaryKey": false, | |
| 2983 | + "notNull": true | |
| 2984 | + }, | |
| 2985 | + "source_id": { | |
| 2986 | + "name": "source_id", | |
| 2987 | + "type": "text", | |
| 2988 | + "primaryKey": false, | |
| 2989 | + "notNull": false | |
| 2990 | + }, | |
| 2991 | + "document_id": { | |
| 2992 | + "name": "document_id", | |
| 2993 | + "type": "text", | |
| 2994 | + "primaryKey": false, | |
| 2995 | + "notNull": false | |
| 2996 | + }, | |
| 2997 | + "url": { | |
| 2998 | + "name": "url", | |
| 2999 | + "type": "text", | |
| 3000 | + "primaryKey": false, | |
| 3001 | + "notNull": false | |
| 3002 | + }, | |
| 3003 | + "created_at": { | |
| 3004 | + "name": "created_at", | |
| 3005 | + "type": "timestamp with time zone", | |
| 3006 | + "primaryKey": false, | |
| 3007 | + "notNull": true, | |
| 3008 | + "default": "now()" | |
| 3009 | + } | |
| 3010 | + }, | |
| 3011 | + "indexes": { | |
| 3012 | + "project_timeline_project_idx": { | |
| 3013 | + "name": "project_timeline_project_idx", | |
| 3014 | + "columns": [ | |
| 3015 | + { | |
| 3016 | + "expression": "project_id", | |
| 3017 | + "isExpression": false, | |
| 3018 | + "asc": true, | |
| 3019 | + "nulls": "last" | |
| 3020 | + } | |
| 3021 | + ], | |
| 3022 | + "isUnique": false, | |
| 3023 | + "concurrently": false, | |
| 3024 | + "method": "btree", | |
| 3025 | + "with": {} | |
| 3026 | + } | |
| 3027 | + }, | |
| 3028 | + "foreignKeys": { | |
| 3029 | + "project_timeline_project_id_projects_id_fk": { | |
| 3030 | + "name": "project_timeline_project_id_projects_id_fk", | |
| 3031 | + "tableFrom": "project_timeline", | |
| 3032 | + "tableTo": "projects", | |
| 3033 | + "columnsFrom": [ | |
| 3034 | + "project_id" | |
| 3035 | + ], | |
| 3036 | + "columnsTo": [ | |
| 3037 | + "id" | |
| 3038 | + ], | |
| 3039 | + "onDelete": "cascade", | |
| 3040 | + "onUpdate": "no action" | |
| 3041 | + } | |
| 3042 | + }, | |
| 3043 | + "compositePrimaryKeys": {}, | |
| 3044 | + "uniqueConstraints": {}, | |
| 3045 | + "policies": {}, | |
| 3046 | + "checkConstraints": {}, | |
| 3047 | + "isRLSEnabled": false | |
| 3048 | + }, | |
| 3049 | + "public.projects": { | |
| 3050 | + "name": "projects", | |
| 3051 | + "schema": "", | |
| 3052 | + "columns": { | |
| 3053 | + "id": { | |
| 3054 | + "name": "id", | |
| 3055 | + "type": "text", | |
| 3056 | + "primaryKey": true, | |
| 3057 | + "notNull": true | |
| 3058 | + }, | |
| 3059 | + "slug": { | |
| 3060 | + "name": "slug", | |
| 3061 | + "type": "text", | |
| 3062 | + "primaryKey": false, | |
| 3063 | + "notNull": true | |
| 3064 | + }, | |
| 3065 | + "name": { | |
| 3066 | + "name": "name", | |
| 3067 | + "type": "text", | |
| 3068 | + "primaryKey": false, | |
| 3069 | + "notNull": true | |
| 3070 | + }, | |
| 3071 | + "normalized_name": { | |
| 3072 | + "name": "normalized_name", | |
| 3073 | + "type": "text", | |
| 3074 | + "primaryKey": false, | |
| 3075 | + "notNull": true | |
| 3076 | + }, | |
| 3077 | + "operator_id": { | |
| 3078 | + "name": "operator_id", | |
| 3079 | + "type": "text", | |
| 3080 | + "primaryKey": false, | |
| 3081 | + "notNull": false | |
| 3082 | + }, | |
| 3083 | + "facility_id": { | |
| 3084 | + "name": "facility_id", | |
| 3085 | + "type": "text", | |
| 3086 | + "primaryKey": false, | |
| 3087 | + "notNull": false | |
| 3088 | + }, | |
| 3089 | + "metro_id": { | |
| 3090 | + "name": "metro_id", | |
| 3091 | + "type": "text", | |
| 3092 | + "primaryKey": false, | |
| 3093 | + "notNull": false | |
| 3094 | + }, | |
| 3095 | + "country_iso2": { | |
| 3096 | + "name": "country_iso2", | |
| 3097 | + "type": "text", | |
| 3098 | + "primaryKey": false, | |
| 3099 | + "notNull": false | |
| 3100 | + }, | |
| 3101 | + "city": { | |
| 3102 | + "name": "city", | |
| 3103 | + "type": "text", | |
| 3104 | + "primaryKey": false, | |
| 3105 | + "notNull": false | |
| 3106 | + }, | |
| 3107 | + "region_name": { | |
| 3108 | + "name": "region_name", | |
| 3109 | + "type": "text", | |
| 3110 | + "primaryKey": false, | |
| 3111 | + "notNull": false | |
| 3112 | + }, | |
| 3113 | + "lat": { | |
| 3114 | + "name": "lat", | |
| 3115 | + "type": "double precision", | |
| 3116 | + "primaryKey": false, | |
| 3117 | + "notNull": false | |
| 3118 | + }, | |
| 3119 | + "lng": { | |
| 3120 | + "name": "lng", | |
| 3121 | + "type": "double precision", | |
| 3122 | + "primaryKey": false, | |
| 3123 | + "notNull": false | |
| 3124 | + }, | |
| 3125 | + "geo_precision": { | |
| 3126 | + "name": "geo_precision", | |
| 3127 | + "type": "text", | |
| 3128 | + "primaryKey": false, | |
| 3129 | + "notNull": true, | |
| 3130 | + "default": "'unknown'" | |
| 3131 | + }, | |
| 3132 | + "status": { | |
| 3133 | + "name": "status", | |
| 3134 | + "type": "text", | |
| 3135 | + "primaryKey": false, | |
| 3136 | + "notNull": true, | |
| 3137 | + "default": "'announced'" | |
| 3138 | + }, | |
| 3139 | + "announced_on": { | |
| 3140 | + "name": "announced_on", | |
| 3141 | + "type": "text", | |
| 3142 | + "primaryKey": false, | |
| 3143 | + "notNull": false | |
| 3144 | + }, | |
| 3145 | + "expected_opening": { | |
| 3146 | + "name": "expected_opening", | |
| 3147 | + "type": "text", | |
| 3148 | + "primaryKey": false, | |
| 3149 | + "notNull": false | |
| 3150 | + }, | |
| 3151 | + "planned_mw": { | |
| 3152 | + "name": "planned_mw", | |
| 3153 | + "type": "double precision", | |
| 3154 | + "primaryKey": false, | |
| 3155 | + "notNull": false | |
| 3156 | + }, | |
| 3157 | + "investment_usd": { | |
| 3158 | + "name": "investment_usd", | |
| 3159 | + "type": "double precision", | |
| 3160 | + "primaryKey": false, | |
| 3161 | + "notNull": false | |
| 3162 | + }, | |
| 3163 | + "investment_currency": { | |
| 3164 | + "name": "investment_currency", | |
| 3165 | + "type": "text", | |
| 3166 | + "primaryKey": false, | |
| 3167 | + "notNull": false | |
| 3168 | + }, | |
| 3169 | + "investment_original": { | |
| 3170 | + "name": "investment_original", | |
| 3171 | + "type": "double precision", | |
| 3172 | + "primaryKey": false, | |
| 3173 | + "notNull": false | |
| 3174 | + }, | |
| 3175 | + "acreage": { | |
| 3176 | + "name": "acreage", | |
| 3177 | + "type": "double precision", | |
| 3178 | + "primaryKey": false, | |
| 3179 | + "notNull": false | |
| 3180 | + }, | |
| 3181 | + "phase_count": { | |
| 3182 | + "name": "phase_count", | |
| 3183 | + "type": "integer", | |
| 3184 | + "primaryKey": false, | |
| 3185 | + "notNull": false | |
| 3186 | + }, | |
| 3187 | + "is_ai": { | |
| 3188 | + "name": "is_ai", | |
| 3189 | + "type": "boolean", | |
| 3190 | + "primaryKey": false, | |
| 3191 | + "notNull": true, | |
| 3192 | + "default": false | |
| 3193 | + }, | |
| 3194 | + "description": { | |
| 3195 | + "name": "description", | |
| 3196 | + "type": "text", | |
| 3197 | + "primaryKey": false, | |
| 3198 | + "notNull": false | |
| 3199 | + }, | |
| 3200 | + "source_url": { | |
| 3201 | + "name": "source_url", | |
| 3202 | + "type": "text", | |
| 3203 | + "primaryKey": false, | |
| 3204 | + "notNull": false | |
| 3205 | + }, | |
| 3206 | + "confidence": { | |
| 3207 | + "name": "confidence", | |
| 3208 | + "type": "text", | |
| 3209 | + "primaryKey": false, | |
| 3210 | + "notNull": true, | |
| 3211 | + "default": "'moderate'" | |
| 3212 | + }, | |
| 3213 | + "external_ids": { | |
| 3214 | + "name": "external_ids", | |
| 3215 | + "type": "jsonb", | |
| 3216 | + "primaryKey": false, | |
| 3217 | + "notNull": true, | |
| 3218 | + "default": "'{}'::jsonb" | |
| 3219 | + }, | |
| 3220 | + "last_update": { | |
| 3221 | + "name": "last_update", | |
| 3222 | + "type": "timestamp with time zone", | |
| 3223 | + "primaryKey": false, | |
| 3224 | + "notNull": true, | |
| 3225 | + "default": "now()" | |
| 3226 | + }, | |
| 3227 | + "created_at": { | |
| 3228 | + "name": "created_at", | |
| 3229 | + "type": "timestamp with time zone", | |
| 3230 | + "primaryKey": false, | |
| 3231 | + "notNull": true, | |
| 3232 | + "default": "now()" | |
| 3233 | + }, | |
| 3234 | + "updated_at": { | |
| 3235 | + "name": "updated_at", | |
| 3236 | + "type": "timestamp with time zone", | |
| 3237 | + "primaryKey": false, | |
| 3238 | + "notNull": true, | |
| 3239 | + "default": "now()" | |
| 3240 | + } | |
| 3241 | + }, | |
| 3242 | + "indexes": { | |
| 3243 | + "projects_country_idx": { | |
| 3244 | + "name": "projects_country_idx", | |
| 3245 | + "columns": [ | |
| 3246 | + { | |
| 3247 | + "expression": "country_iso2", | |
| 3248 | + "isExpression": false, | |
| 3249 | + "asc": true, | |
| 3250 | + "nulls": "last" | |
| 3251 | + } | |
| 3252 | + ], | |
| 3253 | + "isUnique": false, | |
| 3254 | + "concurrently": false, | |
| 3255 | + "method": "btree", | |
| 3256 | + "with": {} | |
| 3257 | + }, | |
| 3258 | + "projects_operator_idx": { | |
| 3259 | + "name": "projects_operator_idx", | |
| 3260 | + "columns": [ | |
| 3261 | + { | |
| 3262 | + "expression": "operator_id", | |
| 3263 | + "isExpression": false, | |
| 3264 | + "asc": true, | |
| 3265 | + "nulls": "last" | |
| 3266 | + } | |
| 3267 | + ], | |
| 3268 | + "isUnique": false, | |
| 3269 | + "concurrently": false, | |
| 3270 | + "method": "btree", | |
| 3271 | + "with": {} | |
| 3272 | + }, | |
| 3273 | + "projects_status_idx": { | |
| 3274 | + "name": "projects_status_idx", | |
| 3275 | + "columns": [ | |
| 3276 | + { | |
| 3277 | + "expression": "status", | |
| 3278 | + "isExpression": false, | |
| 3279 | + "asc": true, | |
| 3280 | + "nulls": "last" | |
| 3281 | + } | |
| 3282 | + ], | |
| 3283 | + "isUnique": false, | |
| 3284 | + "concurrently": false, | |
| 3285 | + "method": "btree", | |
| 3286 | + "with": {} | |
| 3287 | + }, | |
| 3288 | + "projects_norm_idx": { | |
| 3289 | + "name": "projects_norm_idx", | |
| 3290 | + "columns": [ | |
| 3291 | + { | |
| 3292 | + "expression": "normalized_name", | |
| 3293 | + "isExpression": false, | |
| 3294 | + "asc": true, | |
| 3295 | + "nulls": "last" | |
| 3296 | + } | |
| 3297 | + ], | |
| 3298 | + "isUnique": false, | |
| 3299 | + "concurrently": false, | |
| 3300 | + "method": "btree", | |
| 3301 | + "with": {} | |
| 3302 | + } | |
| 3303 | + }, | |
| 3304 | + "foreignKeys": { | |
| 3305 | + "projects_operator_id_operators_id_fk": { | |
| 3306 | + "name": "projects_operator_id_operators_id_fk", | |
| 3307 | + "tableFrom": "projects", | |
| 3308 | + "tableTo": "operators", | |
| 3309 | + "columnsFrom": [ | |
| 3310 | + "operator_id" | |
| 3311 | + ], | |
| 3312 | + "columnsTo": [ | |
| 3313 | + "id" | |
| 3314 | + ], | |
| 3315 | + "onDelete": "no action", | |
| 3316 | + "onUpdate": "no action" | |
| 3317 | + }, | |
| 3318 | + "projects_facility_id_facilities_id_fk": { | |
| 3319 | + "name": "projects_facility_id_facilities_id_fk", | |
| 3320 | + "tableFrom": "projects", | |
| 3321 | + "tableTo": "facilities", | |
| 3322 | + "columnsFrom": [ | |
| 3323 | + "facility_id" | |
| 3324 | + ], | |
| 3325 | + "columnsTo": [ | |
| 3326 | + "id" | |
| 3327 | + ], | |
| 3328 | + "onDelete": "no action", | |
| 3329 | + "onUpdate": "no action" | |
| 3330 | + }, | |
| 3331 | + "projects_metro_id_metros_id_fk": { | |
| 3332 | + "name": "projects_metro_id_metros_id_fk", | |
| 3333 | + "tableFrom": "projects", | |
| 3334 | + "tableTo": "metros", | |
| 3335 | + "columnsFrom": [ | |
| 3336 | + "metro_id" | |
| 3337 | + ], | |
| 3338 | + "columnsTo": [ | |
| 3339 | + "id" | |
| 3340 | + ], | |
| 3341 | + "onDelete": "no action", | |
| 3342 | + "onUpdate": "no action" | |
| 3343 | + }, | |
| 3344 | + "projects_country_iso2_countries_iso2_fk": { | |
| 3345 | + "name": "projects_country_iso2_countries_iso2_fk", | |
| 3346 | + "tableFrom": "projects", | |
| 3347 | + "tableTo": "countries", | |
| 3348 | + "columnsFrom": [ | |
| 3349 | + "country_iso2" | |
| 3350 | + ], | |
| 3351 | + "columnsTo": [ | |
| 3352 | + "iso2" | |
| 3353 | + ], | |
| 3354 | + "onDelete": "no action", | |
| 3355 | + "onUpdate": "no action" | |
| 3356 | + } | |
| 3357 | + }, | |
| 3358 | + "compositePrimaryKeys": {}, | |
| 3359 | + "uniqueConstraints": { | |
| 3360 | + "projects_slug_unique": { | |
| 3361 | + "name": "projects_slug_unique", | |
| 3362 | + "nullsNotDistinct": false, | |
| 3363 | + "columns": [ | |
| 3364 | + "slug" | |
| 3365 | + ] | |
| 3366 | + } | |
| 3367 | + }, | |
| 3368 | + "policies": {}, | |
| 3369 | + "checkConstraints": {}, | |
| 3370 | + "isRLSEnabled": false | |
| 3371 | + }, | |
| 3372 | + "public.provenance": { | |
| 3373 | + "name": "provenance", | |
| 3374 | + "schema": "", | |
| 3375 | + "columns": { | |
| 3376 | + "id": { | |
| 3377 | + "name": "id", | |
| 3378 | + "type": "text", | |
| 3379 | + "primaryKey": true, | |
| 3380 | + "notNull": true | |
| 3381 | + }, | |
| 3382 | + "entity_type": { | |
| 3383 | + "name": "entity_type", | |
| 3384 | + "type": "text", | |
| 3385 | + "primaryKey": false, | |
| 3386 | + "notNull": true | |
| 3387 | + }, | |
| 3388 | + "entity_id": { | |
| 3389 | + "name": "entity_id", | |
| 3390 | + "type": "text", | |
| 3391 | + "primaryKey": false, | |
| 3392 | + "notNull": true | |
| 3393 | + }, | |
| 3394 | + "field": { | |
| 3395 | + "name": "field", | |
| 3396 | + "type": "text", | |
| 3397 | + "primaryKey": false, | |
| 3398 | + "notNull": true | |
| 3399 | + }, | |
| 3400 | + "value": { | |
| 3401 | + "name": "value", | |
| 3402 | + "type": "jsonb", | |
| 3403 | + "primaryKey": false, | |
| 3404 | + "notNull": false | |
| 3405 | + }, | |
| 3406 | + "source_id": { | |
| 3407 | + "name": "source_id", | |
| 3408 | + "type": "text", | |
| 3409 | + "primaryKey": false, | |
| 3410 | + "notNull": true | |
| 3411 | + }, | |
| 3412 | + "connector_id": { | |
| 3413 | + "name": "connector_id", | |
| 3414 | + "type": "text", | |
| 3415 | + "primaryKey": false, | |
| 3416 | + "notNull": true | |
| 3417 | + }, | |
| 3418 | + "document_id": { | |
| 3419 | + "name": "document_id", | |
| 3420 | + "type": "text", | |
| 3421 | + "primaryKey": false, | |
| 3422 | + "notNull": false | |
| 3423 | + }, | |
| 3424 | + "url": { | |
| 3425 | + "name": "url", | |
| 3426 | + "type": "text", | |
| 3427 | + "primaryKey": false, | |
| 3428 | + "notNull": true | |
| 3429 | + }, | |
| 3430 | + "first_observed": { | |
| 3431 | + "name": "first_observed", | |
| 3432 | + "type": "timestamp with time zone", | |
| 3433 | + "primaryKey": false, | |
| 3434 | + "notNull": true, | |
| 3435 | + "default": "now()" | |
| 3436 | + }, | |
| 3437 | + "last_observed": { | |
| 3438 | + "name": "last_observed", | |
| 3439 | + "type": "timestamp with time zone", | |
| 3440 | + "primaryKey": false, | |
| 3441 | + "notNull": true, | |
| 3442 | + "default": "now()" | |
| 3443 | + }, | |
| 3444 | + "retrieved_at": { | |
| 3445 | + "name": "retrieved_at", | |
| 3446 | + "type": "timestamp with time zone", | |
| 3447 | + "primaryKey": false, | |
| 3448 | + "notNull": true, | |
| 3449 | + "default": "now()" | |
| 3450 | + }, | |
| 3451 | + "confidence": { | |
| 3452 | + "name": "confidence", | |
| 3453 | + "type": "text", | |
| 3454 | + "primaryKey": false, | |
| 3455 | + "notNull": true, | |
| 3456 | + "default": "'moderate'" | |
| 3457 | + }, | |
| 3458 | + "is_estimate": { | |
| 3459 | + "name": "is_estimate", | |
| 3460 | + "type": "boolean", | |
| 3461 | + "primaryKey": false, | |
| 3462 | + "notNull": true, | |
| 3463 | + "default": false | |
| 3464 | + }, | |
| 3465 | + "method": { | |
| 3466 | + "name": "method", | |
| 3467 | + "type": "text", | |
| 3468 | + "primaryKey": false, | |
| 3469 | + "notNull": false | |
| 3470 | + }, | |
| 3471 | + "extractor_version": { | |
| 3472 | + "name": "extractor_version", | |
| 3473 | + "type": "text", | |
| 3474 | + "primaryKey": false, | |
| 3475 | + "notNull": false | |
| 3476 | + }, | |
| 3477 | + "is_current": { | |
| 3478 | + "name": "is_current", | |
| 3479 | + "type": "boolean", | |
| 3480 | + "primaryKey": false, | |
| 3481 | + "notNull": true, | |
| 3482 | + "default": true | |
| 3483 | + }, | |
| 3484 | + "note": { | |
| 3485 | + "name": "note", | |
| 3486 | + "type": "text", | |
| 3487 | + "primaryKey": false, | |
| 3488 | + "notNull": false | |
| 3489 | + } | |
| 3490 | + }, | |
| 3491 | + "indexes": { | |
| 3492 | + "provenance_entity_idx": { | |
| 3493 | + "name": "provenance_entity_idx", | |
| 3494 | + "columns": [ | |
| 3495 | + { | |
| 3496 | + "expression": "entity_type", | |
| 3497 | + "isExpression": false, | |
| 3498 | + "asc": true, | |
| 3499 | + "nulls": "last" | |
| 3500 | + }, | |
| 3501 | + { | |
| 3502 | + "expression": "entity_id", | |
| 3503 | + "isExpression": false, | |
| 3504 | + "asc": true, | |
| 3505 | + "nulls": "last" | |
| 3506 | + } | |
| 3507 | + ], | |
| 3508 | + "isUnique": false, | |
| 3509 | + "concurrently": false, | |
| 3510 | + "method": "btree", | |
| 3511 | + "with": {} | |
| 3512 | + }, | |
| 3513 | + "provenance_source_idx": { | |
| 3514 | + "name": "provenance_source_idx", | |
| 3515 | + "columns": [ | |
| 3516 | + { | |
| 3517 | + "expression": "source_id", | |
| 3518 | + "isExpression": false, | |
| 3519 | + "asc": true, | |
| 3520 | + "nulls": "last" | |
| 3521 | + } | |
| 3522 | + ], | |
| 3523 | + "isUnique": false, | |
| 3524 | + "concurrently": false, | |
| 3525 | + "method": "btree", | |
| 3526 | + "with": {} | |
| 3527 | + }, | |
| 3528 | + "provenance_uq": { | |
| 3529 | + "name": "provenance_uq", | |
| 3530 | + "columns": [ | |
| 3531 | + { | |
| 3532 | + "expression": "entity_type", | |
| 3533 | + "isExpression": false, | |
| 3534 | + "asc": true, | |
| 3535 | + "nulls": "last" | |
| 3536 | + }, | |
| 3537 | + { | |
| 3538 | + "expression": "entity_id", | |
| 3539 | + "isExpression": false, | |
| 3540 | + "asc": true, | |
| 3541 | + "nulls": "last" | |
| 3542 | + }, | |
| 3543 | + { | |
| 3544 | + "expression": "field", | |
| 3545 | + "isExpression": false, | |
| 3546 | + "asc": true, | |
| 3547 | + "nulls": "last" | |
| 3548 | + }, | |
| 3549 | + { | |
| 3550 | + "expression": "source_id", | |
| 3551 | + "isExpression": false, | |
| 3552 | + "asc": true, | |
| 3553 | + "nulls": "last" | |
| 3554 | + }, | |
| 3555 | + { | |
| 3556 | + "expression": "url", | |
| 3557 | + "isExpression": false, | |
| 3558 | + "asc": true, | |
| 3559 | + "nulls": "last" | |
| 3560 | + } | |
| 3561 | + ], | |
| 3562 | + "isUnique": true, | |
| 3563 | + "concurrently": false, | |
| 3564 | + "method": "btree", | |
| 3565 | + "with": {} | |
| 3566 | + } | |
| 3567 | + }, | |
| 3568 | + "foreignKeys": {}, | |
| 3569 | + "compositePrimaryKeys": {}, | |
| 3570 | + "uniqueConstraints": {}, | |
| 3571 | + "policies": {}, | |
| 3572 | + "checkConstraints": {}, | |
| 3573 | + "isRLSEnabled": false | |
| 3574 | + }, | |
| 3575 | + "public.rankings": { | |
| 3576 | + "name": "rankings", | |
| 3577 | + "schema": "", | |
| 3578 | + "columns": { | |
| 3579 | + "id": { | |
| 3580 | + "name": "id", | |
| 3581 | + "type": "text", | |
| 3582 | + "primaryKey": true, | |
| 3583 | + "notNull": true | |
| 3584 | + }, | |
| 3585 | + "key": { | |
| 3586 | + "name": "key", | |
| 3587 | + "type": "text", | |
| 3588 | + "primaryKey": false, | |
| 3589 | + "notNull": true | |
| 3590 | + }, | |
| 3591 | + "scope": { | |
| 3592 | + "name": "scope", | |
| 3593 | + "type": "text", | |
| 3594 | + "primaryKey": false, | |
| 3595 | + "notNull": true | |
| 3596 | + }, | |
| 3597 | + "label": { | |
| 3598 | + "name": "label", | |
| 3599 | + "type": "text", | |
| 3600 | + "primaryKey": false, | |
| 3601 | + "notNull": true | |
| 3602 | + }, | |
| 3603 | + "unit": { | |
| 3604 | + "name": "unit", | |
| 3605 | + "type": "text", | |
| 3606 | + "primaryKey": false, | |
| 3607 | + "notNull": true | |
| 3608 | + }, | |
| 3609 | + "methodology": { | |
| 3610 | + "name": "methodology", | |
| 3611 | + "type": "text", | |
| 3612 | + "primaryKey": false, | |
| 3613 | + "notNull": true | |
| 3614 | + }, | |
| 3615 | + "min_coverage": { | |
| 3616 | + "name": "min_coverage", | |
| 3617 | + "type": "real", | |
| 3618 | + "primaryKey": false, | |
| 3619 | + "notNull": false | |
| 3620 | + }, | |
| 3621 | + "rows": { | |
| 3622 | + "name": "rows", | |
| 3623 | + "type": "jsonb", | |
| 3624 | + "primaryKey": false, | |
| 3625 | + "notNull": true | |
| 3626 | + }, | |
| 3627 | + "total": { | |
| 3628 | + "name": "total", | |
| 3629 | + "type": "integer", | |
| 3630 | + "primaryKey": false, | |
| 3631 | + "notNull": true | |
| 3632 | + }, | |
| 3633 | + "computed_at": { | |
| 3634 | + "name": "computed_at", | |
| 3635 | + "type": "timestamp with time zone", | |
| 3636 | + "primaryKey": false, | |
| 3637 | + "notNull": true, | |
| 3638 | + "default": "now()" | |
| 3639 | + }, | |
| 3640 | + "is_current": { | |
| 3641 | + "name": "is_current", | |
| 3642 | + "type": "boolean", | |
| 3643 | + "primaryKey": false, | |
| 3644 | + "notNull": true, | |
| 3645 | + "default": true | |
| 3646 | + } | |
| 3647 | + }, | |
| 3648 | + "indexes": { | |
| 3649 | + "rankings_key_idx": { | |
| 3650 | + "name": "rankings_key_idx", | |
| 3651 | + "columns": [ | |
| 3652 | + { | |
| 3653 | + "expression": "key", | |
| 3654 | + "isExpression": false, | |
| 3655 | + "asc": true, | |
| 3656 | + "nulls": "last" | |
| 3657 | + }, | |
| 3658 | + { | |
| 3659 | + "expression": "is_current", | |
| 3660 | + "isExpression": false, | |
| 3661 | + "asc": true, | |
| 3662 | + "nulls": "last" | |
| 3663 | + } | |
| 3664 | + ], | |
| 3665 | + "isUnique": false, | |
| 3666 | + "concurrently": false, | |
| 3667 | + "method": "btree", | |
| 3668 | + "with": {} | |
| 3669 | + } | |
| 3670 | + }, | |
| 3671 | + "foreignKeys": {}, | |
| 3672 | + "compositePrimaryKeys": {}, | |
| 3673 | + "uniqueConstraints": {}, | |
| 3674 | + "policies": {}, | |
| 3675 | + "checkConstraints": {}, | |
| 3676 | + "isRLSEnabled": false | |
| 3677 | + }, | |
| 3678 | + "public.sources": { | |
| 3679 | + "name": "sources", | |
| 3680 | + "schema": "", | |
| 3681 | + "columns": { | |
| 3682 | + "id": { | |
| 3683 | + "name": "id", | |
| 3684 | + "type": "text", | |
| 3685 | + "primaryKey": true, | |
| 3686 | + "notNull": true | |
| 3687 | + }, | |
| 3688 | + "connector_id": { | |
| 3689 | + "name": "connector_id", | |
| 3690 | + "type": "text", | |
| 3691 | + "primaryKey": false, | |
| 3692 | + "notNull": true | |
| 3693 | + }, | |
| 3694 | + "name": { | |
| 3695 | + "name": "name", | |
| 3696 | + "type": "text", | |
| 3697 | + "primaryKey": false, | |
| 3698 | + "notNull": true | |
| 3699 | + }, | |
| 3700 | + "domain": { | |
| 3701 | + "name": "domain", | |
| 3702 | + "type": "text", | |
| 3703 | + "primaryKey": false, | |
| 3704 | + "notNull": true | |
| 3705 | + }, | |
| 3706 | + "kind": { | |
| 3707 | + "name": "kind", | |
| 3708 | + "type": "text", | |
| 3709 | + "primaryKey": false, | |
| 3710 | + "notNull": true | |
| 3711 | + }, | |
| 3712 | + "priority": { | |
| 3713 | + "name": "priority", | |
| 3714 | + "type": "integer", | |
| 3715 | + "primaryKey": false, | |
| 3716 | + "notNull": true, | |
| 3717 | + "default": 3 | |
| 3718 | + }, | |
| 3719 | + "url": { | |
| 3720 | + "name": "url", | |
| 3721 | + "type": "text", | |
| 3722 | + "primaryKey": false, | |
| 3723 | + "notNull": false | |
| 3724 | + }, | |
| 3725 | + "license": { | |
| 3726 | + "name": "license", | |
| 3727 | + "type": "text", | |
| 3728 | + "primaryKey": false, | |
| 3729 | + "notNull": false | |
| 3730 | + }, | |
| 3731 | + "attribution": { | |
| 3732 | + "name": "attribution", | |
| 3733 | + "type": "text", | |
| 3734 | + "primaryKey": false, | |
| 3735 | + "notNull": false | |
| 3736 | + }, | |
| 3737 | + "robots_allowed": { | |
| 3738 | + "name": "robots_allowed", | |
| 3739 | + "type": "boolean", | |
| 3740 | + "primaryKey": false, | |
| 3741 | + "notNull": false | |
| 3742 | + }, | |
| 3743 | + "notes": { | |
| 3744 | + "name": "notes", | |
| 3745 | + "type": "text", | |
| 3746 | + "primaryKey": false, | |
| 3747 | + "notNull": false | |
| 3748 | + }, | |
| 3749 | + "created_at": { | |
| 3750 | + "name": "created_at", | |
| 3751 | + "type": "timestamp with time zone", | |
| 3752 | + "primaryKey": false, | |
| 3753 | + "notNull": true, | |
| 3754 | + "default": "now()" | |
| 3755 | + }, | |
| 3756 | + "updated_at": { | |
| 3757 | + "name": "updated_at", | |
| 3758 | + "type": "timestamp with time zone", | |
| 3759 | + "primaryKey": false, | |
| 3760 | + "notNull": true, | |
| 3761 | + "default": "now()" | |
| 3762 | + } | |
| 3763 | + }, | |
| 3764 | + "indexes": {}, | |
| 3765 | + "foreignKeys": {}, | |
| 3766 | + "compositePrimaryKeys": {}, | |
| 3767 | + "uniqueConstraints": {}, | |
| 3768 | + "policies": {}, | |
| 3769 | + "checkConstraints": {}, | |
| 3770 | + "isRLSEnabled": false | |
| 3771 | + }, | |
| 3772 | + "public.system_alerts": { | |
| 3773 | + "name": "system_alerts", | |
| 3774 | + "schema": "", | |
| 3775 | + "columns": { | |
| 3776 | + "id": { | |
| 3777 | + "name": "id", | |
| 3778 | + "type": "text", | |
| 3779 | + "primaryKey": true, | |
| 3780 | + "notNull": true | |
| 3781 | + }, | |
| 3782 | + "level": { | |
| 3783 | + "name": "level", | |
| 3784 | + "type": "text", | |
| 3785 | + "primaryKey": false, | |
| 3786 | + "notNull": true | |
| 3787 | + }, | |
| 3788 | + "component": { | |
| 3789 | + "name": "component", | |
| 3790 | + "type": "text", | |
| 3791 | + "primaryKey": false, | |
| 3792 | + "notNull": true | |
| 3793 | + }, | |
| 3794 | + "message": { | |
| 3795 | + "name": "message", | |
| 3796 | + "type": "text", | |
| 3797 | + "primaryKey": false, | |
| 3798 | + "notNull": true | |
| 3799 | + }, | |
| 3800 | + "details": { | |
| 3801 | + "name": "details", | |
| 3802 | + "type": "jsonb", | |
| 3803 | + "primaryKey": false, | |
| 3804 | + "notNull": false | |
| 3805 | + }, | |
| 3806 | + "resolved_at": { | |
| 3807 | + "name": "resolved_at", | |
| 3808 | + "type": "timestamp with time zone", | |
| 3809 | + "primaryKey": false, | |
| 3810 | + "notNull": false | |
| 3811 | + }, | |
| 3812 | + "created_at": { | |
| 3813 | + "name": "created_at", | |
| 3814 | + "type": "timestamp with time zone", | |
| 3815 | + "primaryKey": false, | |
| 3816 | + "notNull": true, | |
| 3817 | + "default": "now()" | |
| 3818 | + } | |
| 3819 | + }, | |
| 3820 | + "indexes": {}, | |
| 3821 | + "foreignKeys": {}, | |
| 3822 | + "compositePrimaryKeys": {}, | |
| 3823 | + "uniqueConstraints": {}, | |
| 3824 | + "policies": {}, | |
| 3825 | + "checkConstraints": {}, | |
| 3826 | + "isRLSEnabled": false | |
| 3827 | + } | |
| 3828 | + }, | |
| 3829 | + "enums": {}, | |
| 3830 | + "schemas": {}, | |
| 3831 | + "sequences": {}, | |
| 3832 | + "roles": {}, | |
| 3833 | + "policies": {}, | |
| 3834 | + "views": {}, | |
| 3835 | + "_meta": { | |
| 3836 | + "columns": {}, | |
| 3837 | + "schemas": {}, | |
| 3838 | + "tables": {} | |
| 3839 | + } | |
| 3840 | +} | |
| \ No newline at end of file | ||
added
packages/db/migrations/meta/_journal.json
+13 −0
@@ -0,0 +1,13 @@ | ||
| 1 | +{ | |
| 2 | + "version": "7", | |
| 3 | + "dialect": "postgresql", | |
| 4 | + "entries": [ | |
| 5 | + { | |
| 6 | + "idx": 0, | |
| 7 | + "version": "7", | |
| 8 | + "when": 1789108278691, | |
| 9 | + "tag": "0000_init", | |
| 10 | + "breakpoints": true | |
| 11 | + } | |
| 12 | + ] | |
| 13 | +} | |
| \ No newline at end of file | ||
added
packages/db/package.json
+23 −0
@@ -0,0 +1,23 @@ | ||
| 1 | +{ | |
| 2 | + "name": "@dci/db", | |
| 3 | + "version": "0.1.0", | |
| 4 | + "private": true, | |
| 5 | + "type": "module", | |
| 6 | + "main": "./src/index.ts", | |
| 7 | + "types": "./src/index.ts", | |
| 8 | + "exports": { ".": "./src/index.ts", "./schema": "./src/schema.ts", "./clickhouse": "./src/clickhouse.ts" }, | |
| 9 | + "scripts": { | |
| 10 | + "typecheck": "tsc --noEmit", | |
| 11 | + "test": "vitest run --passWithNoTests", | |
| 12 | + "generate": "drizzle-kit generate", | |
| 13 | + "migrate": "node ../../node_modules/tsx/dist/cli.mjs src/migrate.ts", | |
| 14 | + "seed": "node ../../node_modules/tsx/dist/cli.mjs src/seed.ts" | |
| 15 | + }, | |
| 16 | + "dependencies": { | |
| 17 | + "@clickhouse/client": "^1.11.0", | |
| 18 | + "@dci/core": "workspace:*", | |
| 19 | + "drizzle-orm": "^0.44.0", | |
| 20 | + "postgres": "^3.4.7" | |
| 21 | + }, | |
| 22 | + "devDependencies": { "@types/node": "^24.0.0", "drizzle-kit": "^0.31.0", "typescript": "^5.9.3", "vitest": "^3.2.0" } | |
| 23 | +} | |
added
packages/db/src/clickhouse.ts
+114 −0
@@ -0,0 +1,114 @@ | ||
| 1 | +import { createClient, type ClickHouseClient } from "@clickhouse/client"; | |
| 2 | + | |
| 3 | +/** | |
| 4 | + * ClickHouse: append-only analytics store — crawl history, extracted observations, page changes, | |
| 5 | + * connector metrics and daily time series. Postgres remains the transactional source of truth. | |
| 6 | + */ | |
| 7 | +let _ch: ClickHouseClient | null = null; | |
| 8 | +export function getClickHouse(): ClickHouseClient { | |
| 9 | + if (!_ch) { | |
| 10 | + _ch = createClient({ | |
| 11 | + url: process.env.CLICKHOUSE_URL ?? "http://127.0.0.1:8123", | |
| 12 | + database: process.env.CLICKHOUSE_DB ?? "dci", | |
| 13 | + username: process.env.CLICKHOUSE_USER ?? "default", | |
| 14 | + password: process.env.CLICKHOUSE_PASSWORD ?? "", | |
| 15 | + request_timeout: 30_000, | |
| 16 | + clickhouse_settings: { async_insert: 1, wait_for_async_insert: 0 }, | |
| 17 | + }); | |
| 18 | + } | |
| 19 | + return _ch; | |
| 20 | +} | |
| 21 | + | |
| 22 | +export const CLICKHOUSE_DDL: string[] = [ | |
| 23 | + `CREATE TABLE IF NOT EXISTS crawl_log ( | |
| 24 | + ts DateTime64(3) DEFAULT now64(3), | |
| 25 | + connector_id LowCardinality(String), | |
| 26 | + source_id LowCardinality(String), | |
| 27 | + document_id String, | |
| 28 | + url String, | |
| 29 | + fetch_level UInt8, | |
| 30 | + fetcher LowCardinality(String), | |
| 31 | + status_code UInt16, | |
| 32 | + duration_ms UInt32, | |
| 33 | + bytes UInt32, | |
| 34 | + changed UInt8, | |
| 35 | + not_modified UInt8, | |
| 36 | + error LowCardinality(String), | |
| 37 | + credits Float32, | |
| 38 | + run_id String | |
| 39 | + ) ENGINE = MergeTree ORDER BY (connector_id, ts) TTL toDateTime(ts) + INTERVAL 400 DAY`, | |
| 40 | + `CREATE TABLE IF NOT EXISTS observations ( | |
| 41 | + ts DateTime64(3) DEFAULT now64(3), | |
| 42 | + connector_id LowCardinality(String), | |
| 43 | + source_id LowCardinality(String), | |
| 44 | + document_id String, | |
| 45 | + entity_type LowCardinality(String), | |
| 46 | + entity_key String, | |
| 47 | + entity_id String, | |
| 48 | + field LowCardinality(String), | |
| 49 | + value String, | |
| 50 | + value_num Nullable(Float64), | |
| 51 | + confidence LowCardinality(String), | |
| 52 | + is_estimate UInt8, | |
| 53 | + method LowCardinality(String), | |
| 54 | + extractor_version LowCardinality(String), | |
| 55 | + run_id String | |
| 56 | + ) ENGINE = MergeTree ORDER BY (entity_type, entity_key, field, ts)`, | |
| 57 | + `CREATE TABLE IF NOT EXISTS page_changes ( | |
| 58 | + ts DateTime64(3) DEFAULT now64(3), | |
| 59 | + connector_id LowCardinality(String), | |
| 60 | + document_id String, | |
| 61 | + url String, | |
| 62 | + old_hash String, | |
| 63 | + new_hash String, | |
| 64 | + added_lines UInt32, | |
| 65 | + removed_lines UInt32, | |
| 66 | + ratio Float32, | |
| 67 | + significance UInt8, | |
| 68 | + fields Array(String), | |
| 69 | + event_types Array(String) | |
| 70 | + ) ENGINE = MergeTree ORDER BY (connector_id, ts)`, | |
| 71 | + `CREATE TABLE IF NOT EXISTS metrics ( | |
| 72 | + ts DateTime DEFAULT now(), | |
| 73 | + metric LowCardinality(String), | |
| 74 | + dim LowCardinality(String), | |
| 75 | + value Float64 | |
| 76 | + ) ENGINE = MergeTree ORDER BY (metric, dim, ts) TTL ts + INTERVAL 400 DAY`, | |
| 77 | + `CREATE TABLE IF NOT EXISTS entity_daily ( | |
| 78 | + day Date, | |
| 79 | + metric LowCardinality(String), | |
| 80 | + dim LowCardinality(String), | |
| 81 | + value Float64 | |
| 82 | + ) ENGINE = ReplacingMergeTree ORDER BY (metric, dim, day)`, | |
| 83 | + `CREATE TABLE IF NOT EXISTS api_requests ( | |
| 84 | + ts DateTime DEFAULT now(), | |
| 85 | + route LowCardinality(String), | |
| 86 | + status UInt16, | |
| 87 | + duration_ms UInt32, | |
| 88 | + cached UInt8 | |
| 89 | + ) ENGINE = MergeTree ORDER BY (route, ts) TTL ts + INTERVAL 90 DAY`, | |
| 90 | +]; | |
| 91 | + | |
| 92 | +export async function ensureClickHouse(): Promise<void> { | |
| 93 | + const db = process.env.CLICKHOUSE_DB ?? "dci"; | |
| 94 | + const admin = createClient({ url: process.env.CLICKHOUSE_URL ?? "http://127.0.0.1:8123", username: process.env.CLICKHOUSE_USER ?? "default", password: process.env.CLICKHOUSE_PASSWORD ?? "" }); | |
| 95 | + await admin.command({ query: `CREATE DATABASE IF NOT EXISTS ${db}` }); | |
| 96 | + await admin.close(); | |
| 97 | + const ch = getClickHouse(); | |
| 98 | + for (const ddl of CLICKHOUSE_DDL) await ch.command({ query: ddl }); | |
| 99 | +} | |
| 100 | + | |
| 101 | +export async function chInsert(table: string, rows: Record<string, unknown>[]): Promise<void> { | |
| 102 | + if (!rows.length) return; | |
| 103 | + try { | |
| 104 | + await getClickHouse().insert({ table, values: rows, format: "JSONEachRow" }); | |
| 105 | + } catch (e) { | |
| 106 | + if (process.env.DCI_CLICKHOUSE_OPTIONAL !== "0") console.warn(`[clickhouse] insert ${table} failed: ${(e as Error).message}`); | |
| 107 | + else throw e; | |
| 108 | + } | |
| 109 | +} | |
| 110 | + | |
| 111 | +export async function chQuery<T = Record<string, unknown>>(query: string, params: Record<string, unknown> = {}): Promise<T[]> { | |
| 112 | + const rs = await getClickHouse().query({ query, query_params: params, format: "JSONEachRow" }); | |
| 113 | + return (await rs.json()) as T[]; | |
| 114 | +} | |
added
packages/db/src/index.ts
+36 −0
@@ -0,0 +1,36 @@ | ||
| 1 | +import { drizzle, type PostgresJsDatabase } from "drizzle-orm/postgres-js"; | |
| 2 | +import postgres from "postgres"; | |
| 3 | +import * as schema from "./schema.js"; | |
| 4 | + | |
| 5 | +export * from "./schema.js"; | |
| 6 | +export { schema }; | |
| 7 | +export { sql, eq, and, or, desc, asc, inArray, isNull, isNotNull, gte, lte, lt, gt, ilike, like, ne, count, sum, avg, max, min, notInArray } from "drizzle-orm"; | |
| 8 | + | |
| 9 | +export type Db = PostgresJsDatabase<typeof schema>; | |
| 10 | + | |
| 11 | +let _client: ReturnType<typeof postgres> | null = null; | |
| 12 | +let _db: Db | null = null; | |
| 13 | + | |
| 14 | +export function getSql(): ReturnType<typeof postgres> { | |
| 15 | + if (!_client) { | |
| 16 | + const url = process.env.DATABASE_URL ?? "postgres://dci:dci@127.0.0.1:5432/dci"; | |
| 17 | + _client = postgres(url, { max: Number(process.env.PG_POOL_MAX ?? 16), idle_timeout: 30, connect_timeout: 15, prepare: true, onnotice: () => {} }); | |
| 18 | + } | |
| 19 | + return _client; | |
| 20 | +} | |
| 21 | + | |
| 22 | +export function getDb(): Db { | |
| 23 | + if (!_db) _db = drizzle(getSql(), { schema, casing: "snake_case" }); | |
| 24 | + return _db; | |
| 25 | +} | |
| 26 | + | |
| 27 | +export async function closeDb(): Promise<void> { | |
| 28 | + if (_client) await _client.end({ timeout: 5 }); | |
| 29 | + _client = null; | |
| 30 | + _db = null; | |
| 31 | +} | |
| 32 | + | |
| 33 | +/** Postgres `text[]` literal helper — drizzle `sql\`${arr}\`` would spread arrays into tuples. */ | |
| 34 | +export function textArray(values: string[]): string { | |
| 35 | + return `{${values.map((v) => `"${v.replace(/(["\\])/g, "\\$1")}"`).join(",")}}`; | |
| 36 | +} | |
added
packages/db/src/migrate.ts
+26 −0
@@ -0,0 +1,26 @@ | ||
| 1 | +import { readdirSync, readFileSync } from "node:fs"; | |
| 2 | +import { join, dirname } from "node:path"; | |
| 3 | +import { fileURLToPath } from "node:url"; | |
| 4 | +import { getSql, closeDb } from "./index.js"; | |
| 5 | + | |
| 6 | +/** Plain-SQL migrations in ./migrations, applied in lexical order, tracked in _dci_migrations. Idempotent. */ | |
| 7 | +async function main() { | |
| 8 | + const sql = getSql(); | |
| 9 | + const dir = join(dirname(fileURLToPath(import.meta.url)), "..", "migrations"); | |
| 10 | + await sql`create table if not exists _dci_migrations (name text primary key, applied_at timestamptz not null default now())`; | |
| 11 | + const done = new Set((await sql`select name from _dci_migrations`).map((r) => r.name as string)); | |
| 12 | + const files = readdirSync(dir).filter((f) => f.endsWith(".sql")).sort(); | |
| 13 | + for (const f of files) { | |
| 14 | + if (done.has(f)) continue; | |
| 15 | + const body = readFileSync(join(dir, f), "utf8"); | |
| 16 | + process.stdout.write(`applying ${f} … `); | |
| 17 | + await sql.begin(async (tx) => { | |
| 18 | + await tx.unsafe(body); | |
| 19 | + await tx`insert into _dci_migrations (name) values (${f})`; | |
| 20 | + }); | |
| 21 | + console.log("ok"); | |
| 22 | + } | |
| 23 | + console.log(`migrations: ${files.length} total, ${files.length - done.size} applied now`); | |
| 24 | + await closeDb(); | |
| 25 | +} | |
| 26 | +main().catch((e) => { console.error(e); process.exit(1); }); | |
added
packages/db/src/schema.ts
+575 −0
@@ -0,0 +1,575 @@ | ||
| 1 | +/** | |
| 2 | + * PostgreSQL schema (source of truth for entities, provenance, events, connector state). | |
| 3 | + * ClickHouse (see clickhouse.ts) holds crawl history, observations, page changes and time series. | |
| 4 | + * Column naming: snake_case via drizzle `casing`. | |
| 5 | + */ | |
| 6 | +import { boolean, doublePrecision, index, integer, jsonb, pgTable, primaryKey, real, text, timestamp, uniqueIndex, date, bigint } from "drizzle-orm/pg-core"; | |
| 7 | +import { sql } from "drizzle-orm"; | |
| 8 | + | |
| 9 | +const ts = (name: string) => timestamp(name, { withTimezone: true, mode: "string" }); | |
| 10 | +const createdAt = () => ts("created_at").notNull().defaultNow(); | |
| 11 | +const updatedAt = () => ts("updated_at").notNull().defaultNow(); | |
| 12 | + | |
| 13 | +export const countries = pgTable("countries", { | |
| 14 | + iso2: text("iso2").primaryKey(), | |
| 15 | + iso3: text("iso3"), | |
| 16 | + slug: text("slug").notNull().unique(), | |
| 17 | + name: text("name").notNull(), | |
| 18 | + region: text("region"), | |
| 19 | + subregion: text("subregion"), | |
| 20 | + population: bigint("population", { mode: "number" }), | |
| 21 | + gdpUsd: doublePrecision("gdp_usd"), | |
| 22 | + electricityTwh: doublePrecision("electricity_twh"), | |
| 23 | + renewableShare: doublePrecision("renewable_share"), | |
| 24 | + statsYear: integer("stats_year"), | |
| 25 | + lat: doublePrecision("lat"), | |
| 26 | + lng: doublePrecision("lng"), | |
| 27 | + stats: jsonb("stats").$type<Record<string, unknown>>().notNull().default(sql`'{}'::jsonb`), | |
| 28 | + updatedAt: updatedAt(), | |
| 29 | +}); | |
| 30 | + | |
| 31 | +export const metros = pgTable( | |
| 32 | + "metros", | |
| 33 | + { | |
| 34 | + id: text("id").primaryKey(), | |
| 35 | + slug: text("slug").notNull().unique(), | |
| 36 | + name: text("name").notNull(), | |
| 37 | + countryIso2: text("country_iso2").notNull().references(() => countries.iso2), | |
| 38 | + regionName: text("region_name"), | |
| 39 | + lat: doublePrecision("lat").notNull(), | |
| 40 | + lng: doublePrecision("lng").notNull(), | |
| 41 | + radiusKm: real("radius_km").notNull().default(60), | |
| 42 | + aliases: text("aliases").array().notNull().default(sql`'{}'::text[]`), | |
| 43 | + description: text("description"), | |
| 44 | + stats: jsonb("stats").$type<Record<string, unknown>>().notNull().default(sql`'{}'::jsonb`), | |
| 45 | + createdAt: createdAt(), | |
| 46 | + updatedAt: updatedAt(), | |
| 47 | + }, | |
| 48 | + (t) => [index("metros_country_idx").on(t.countryIso2)], | |
| 49 | +); | |
| 50 | + | |
| 51 | +export const operators = pgTable( | |
| 52 | + "operators", | |
| 53 | + { | |
| 54 | + id: text("id").primaryKey(), | |
| 55 | + slug: text("slug").notNull().unique(), | |
| 56 | + name: text("name").notNull(), | |
| 57 | + normalizedName: text("normalized_name").notNull(), | |
| 58 | + kind: text("kind"), // OperatorKind | |
| 59 | + website: text("website"), | |
| 60 | + hqCountryIso2: text("hq_country_iso2"), | |
| 61 | + hqCity: text("hq_city"), | |
| 62 | + parentId: text("parent_id"), | |
| 63 | + description: text("description"), | |
| 64 | + aliases: text("aliases").array().notNull().default(sql`'{}'::text[]`), | |
| 65 | + externalIds: jsonb("external_ids").$type<Record<string, string | number>>().notNull().default(sql`'{}'::jsonb`), | |
| 66 | + isCloudProvider: boolean("is_cloud_provider").notNull().default(false), | |
| 67 | + isCarrier: boolean("is_carrier").notNull().default(false), | |
| 68 | + stats: jsonb("stats").$type<Record<string, unknown>>().notNull().default(sql`'{}'::jsonb`), | |
| 69 | + createdAt: createdAt(), | |
| 70 | + updatedAt: updatedAt(), | |
| 71 | + }, | |
| 72 | + (t) => [index("operators_norm_idx").on(t.normalizedName), index("operators_kind_idx").on(t.kind)], | |
| 73 | +); | |
| 74 | + | |
| 75 | +export const campuses = pgTable("campuses", { | |
| 76 | + id: text("id").primaryKey(), | |
| 77 | + slug: text("slug").notNull().unique(), | |
| 78 | + name: text("name").notNull(), | |
| 79 | + operatorId: text("operator_id").references(() => operators.id), | |
| 80 | + metroId: text("metro_id").references(() => metros.id), | |
| 81 | + countryIso2: text("country_iso2"), | |
| 82 | + city: text("city"), | |
| 83 | + lat: doublePrecision("lat"), | |
| 84 | + lng: doublePrecision("lng"), | |
| 85 | + externalIds: jsonb("external_ids").$type<Record<string, string | number>>().notNull().default(sql`'{}'::jsonb`), | |
| 86 | + createdAt: createdAt(), | |
| 87 | + updatedAt: updatedAt(), | |
| 88 | +}); | |
| 89 | + | |
| 90 | +export const facilities = pgTable( | |
| 91 | + "facilities", | |
| 92 | + { | |
| 93 | + id: text("id").primaryKey(), | |
| 94 | + slug: text("slug").notNull().unique(), | |
| 95 | + name: text("name").notNull(), | |
| 96 | + normalizedName: text("normalized_name").notNull(), | |
| 97 | + operatorId: text("operator_id").references(() => operators.id), | |
| 98 | + ownerId: text("owner_id").references(() => operators.id), | |
| 99 | + campusId: text("campus_id").references(() => campuses.id), | |
| 100 | + metroId: text("metro_id").references(() => metros.id), | |
| 101 | + countryIso2: text("country_iso2").references(() => countries.iso2), | |
| 102 | + city: text("city"), | |
| 103 | + regionName: text("region_name"), | |
| 104 | + address: text("address"), | |
| 105 | + postalCode: text("postal_code"), | |
| 106 | + lat: doublePrecision("lat"), | |
| 107 | + lng: doublePrecision("lng"), | |
| 108 | + geoPrecision: text("geo_precision").notNull().default("unknown"), | |
| 109 | + geoSource: text("geo_source"), | |
| 110 | + geohash: text("geohash"), | |
| 111 | + status: text("status").notNull().default("unknown"), | |
| 112 | + facilityType: text("facility_type").notNull().default("unknown"), | |
| 113 | + tier: text("tier"), | |
| 114 | + buildingSqm: doublePrecision("building_sqm"), | |
| 115 | + siteAreaHa: doublePrecision("site_area_ha"), | |
| 116 | + itCapacityMw: doublePrecision("it_capacity_mw"), | |
| 117 | + totalPowerMw: doublePrecision("total_power_mw"), | |
| 118 | + plannedPowerMw: doublePrecision("planned_power_mw"), | |
| 119 | + mwIsEstimate: boolean("mw_is_estimate").notNull().default(false), | |
| 120 | + rackCount: integer("rack_count"), | |
| 121 | + pue: real("pue"), | |
| 122 | + coolingType: text("cooling_type"), | |
| 123 | + renewableClaim: text("renewable_claim"), | |
| 124 | + openedOn: text("opened_on"), // partial date | |
| 125 | + constructionStartedOn: text("construction_started_on"), | |
| 126 | + announcedOn: text("announced_on"), | |
| 127 | + website: text("website"), | |
| 128 | + description: text("description"), | |
| 129 | + isAi: boolean("is_ai").notNull().default(false), | |
| 130 | + isHyperscale: boolean("is_hyperscale").notNull().default(false), | |
| 131 | + certifications: text("certifications").array().notNull().default(sql`'{}'::text[]`), | |
| 132 | + carriersCount: integer("carriers_count"), | |
| 133 | + ixpCount: integer("ixp_count"), | |
| 134 | + networksCount: integer("networks_count"), | |
| 135 | + confidence: text("confidence").notNull().default("unverified"), | |
| 136 | + completeness: integer("completeness").notNull().default(0), | |
| 137 | + externalIds: jsonb("external_ids").$type<Record<string, string | number>>().notNull().default(sql`'{}'::jsonb`), | |
| 138 | + sourceCount: integer("source_count").notNull().default(0), | |
| 139 | + firstSeen: ts("first_seen").notNull().defaultNow(), | |
| 140 | + lastVerified: ts("last_verified"), | |
| 141 | + mergedInto: text("merged_into"), | |
| 142 | + createdAt: createdAt(), | |
| 143 | + updatedAt: updatedAt(), | |
| 144 | + }, | |
| 145 | + (t) => [ | |
| 146 | + index("facilities_country_idx").on(t.countryIso2), | |
| 147 | + index("facilities_operator_idx").on(t.operatorId), | |
| 148 | + index("facilities_metro_idx").on(t.metroId), | |
| 149 | + index("facilities_status_idx").on(t.status), | |
| 150 | + index("facilities_type_idx").on(t.facilityType), | |
| 151 | + index("facilities_geohash_idx").on(t.geohash), | |
| 152 | + index("facilities_norm_idx").on(t.normalizedName), | |
| 153 | + index("facilities_updated_idx").on(t.updatedAt), | |
| 154 | + ], | |
| 155 | +); | |
| 156 | + | |
| 157 | +export const facilityAliases = pgTable( | |
| 158 | + "facility_aliases", | |
| 159 | + { | |
| 160 | + facilityId: text("facility_id").notNull().references(() => facilities.id, { onDelete: "cascade" }), | |
| 161 | + alias: text("alias").notNull(), | |
| 162 | + normalized: text("normalized").notNull(), | |
| 163 | + sourceId: text("source_id"), | |
| 164 | + }, | |
| 165 | + (t) => [primaryKey({ columns: [t.facilityId, t.alias] }), index("facility_aliases_norm_idx").on(t.normalized)], | |
| 166 | +); | |
| 167 | + | |
| 168 | +/** connector-scoped key → facility id (idempotent re-ingestion, e.g. "peeringdb:fac:1"). */ | |
| 169 | +export const entityKeys = pgTable( | |
| 170 | + "entity_keys", | |
| 171 | + { | |
| 172 | + key: text("key").primaryKey(), | |
| 173 | + entityType: text("entity_type").notNull(), | |
| 174 | + entityId: text("entity_id").notNull(), | |
| 175 | + connectorId: text("connector_id").notNull(), | |
| 176 | + createdAt: createdAt(), | |
| 177 | + }, | |
| 178 | + (t) => [index("entity_keys_entity_idx").on(t.entityType, t.entityId)], | |
| 179 | +); | |
| 180 | + | |
| 181 | +export const cloudRegions = pgTable( | |
| 182 | + "cloud_regions", | |
| 183 | + { | |
| 184 | + id: text("id").primaryKey(), | |
| 185 | + slug: text("slug").notNull().unique(), | |
| 186 | + providerId: text("provider_id").notNull().references(() => operators.id), | |
| 187 | + code: text("code").notNull(), | |
| 188 | + name: text("name").notNull(), | |
| 189 | + city: text("city"), | |
| 190 | + regionName: text("region_name"), | |
| 191 | + countryIso2: text("country_iso2").references(() => countries.iso2), | |
| 192 | + metroId: text("metro_id").references(() => metros.id), | |
| 193 | + lat: doublePrecision("lat"), | |
| 194 | + lng: doublePrecision("lng"), | |
| 195 | + geoPrecision: text("geo_precision").notNull().default("city"), | |
| 196 | + availabilityZones: integer("availability_zones"), | |
| 197 | + launchedOn: text("launched_on"), | |
| 198 | + status: text("status").notNull().default("operational"), | |
| 199 | + isSovereign: boolean("is_sovereign").notNull().default(false), | |
| 200 | + sourceUrl: text("source_url"), | |
| 201 | + externalIds: jsonb("external_ids").$type<Record<string, string | number>>().notNull().default(sql`'{}'::jsonb`), | |
| 202 | + createdAt: createdAt(), | |
| 203 | + updatedAt: updatedAt(), | |
| 204 | + }, | |
| 205 | + (t) => [uniqueIndex("cloud_regions_provider_code_uq").on(t.providerId, t.code), index("cloud_regions_country_idx").on(t.countryIso2)], | |
| 206 | +); | |
| 207 | + | |
| 208 | +export const ixps = pgTable("ixps", { | |
| 209 | + id: text("id").primaryKey(), | |
| 210 | + slug: text("slug").notNull().unique(), | |
| 211 | + name: text("name").notNull(), | |
| 212 | + nameLong: text("name_long"), | |
| 213 | + city: text("city"), | |
| 214 | + countryIso2: text("country_iso2").references(() => countries.iso2), | |
| 215 | + metroId: text("metro_id").references(() => metros.id), | |
| 216 | + regionContinent: text("region_continent"), | |
| 217 | + website: text("website"), | |
| 218 | + networkCount: integer("network_count"), | |
| 219 | + externalIds: jsonb("external_ids").$type<Record<string, string | number>>().notNull().default(sql`'{}'::jsonb`), | |
| 220 | + createdAt: createdAt(), | |
| 221 | + updatedAt: updatedAt(), | |
| 222 | +}); | |
| 223 | + | |
| 224 | +export const facilityIxps = pgTable( | |
| 225 | + "facility_ixps", | |
| 226 | + { | |
| 227 | + facilityId: text("facility_id").notNull().references(() => facilities.id, { onDelete: "cascade" }), | |
| 228 | + ixpId: text("ixp_id").notNull().references(() => ixps.id, { onDelete: "cascade" }), | |
| 229 | + sourceId: text("source_id"), | |
| 230 | + }, | |
| 231 | + (t) => [primaryKey({ columns: [t.facilityId, t.ixpId] })], | |
| 232 | +); | |
| 233 | + | |
| 234 | +/** Carriers / networks / cloud providers present at a facility (operators with kind carrier|cloud|hyperscaler). */ | |
| 235 | +export const facilityTenants = pgTable( | |
| 236 | + "facility_tenants", | |
| 237 | + { | |
| 238 | + facilityId: text("facility_id").notNull().references(() => facilities.id, { onDelete: "cascade" }), | |
| 239 | + operatorId: text("operator_id").notNull().references(() => operators.id, { onDelete: "cascade" }), | |
| 240 | + role: text("role").notNull().default("carrier"), // carrier | cloud | network | tenant | |
| 241 | + asn: integer("asn"), | |
| 242 | + sourceId: text("source_id"), | |
| 243 | + }, | |
| 244 | + (t) => [primaryKey({ columns: [t.facilityId, t.operatorId, t.role] }), index("facility_tenants_operator_idx").on(t.operatorId)], | |
| 245 | +); | |
| 246 | + | |
| 247 | +export const projects = pgTable( | |
| 248 | + "projects", | |
| 249 | + { | |
| 250 | + id: text("id").primaryKey(), | |
| 251 | + slug: text("slug").notNull().unique(), | |
| 252 | + name: text("name").notNull(), | |
| 253 | + normalizedName: text("normalized_name").notNull(), | |
| 254 | + operatorId: text("operator_id").references(() => operators.id), | |
| 255 | + facilityId: text("facility_id").references(() => facilities.id), | |
| 256 | + metroId: text("metro_id").references(() => metros.id), | |
| 257 | + countryIso2: text("country_iso2").references(() => countries.iso2), | |
| 258 | + city: text("city"), | |
| 259 | + regionName: text("region_name"), | |
| 260 | + lat: doublePrecision("lat"), | |
| 261 | + lng: doublePrecision("lng"), | |
| 262 | + geoPrecision: text("geo_precision").notNull().default("unknown"), | |
| 263 | + status: text("status").notNull().default("announced"), | |
| 264 | + announcedOn: text("announced_on"), | |
| 265 | + expectedOpening: text("expected_opening"), | |
| 266 | + plannedMw: doublePrecision("planned_mw"), | |
| 267 | + investmentUsd: doublePrecision("investment_usd"), | |
| 268 | + investmentCurrency: text("investment_currency"), | |
| 269 | + investmentOriginal: doublePrecision("investment_original"), | |
| 270 | + acreage: doublePrecision("acreage"), | |
| 271 | + phaseCount: integer("phase_count"), | |
| 272 | + isAi: boolean("is_ai").notNull().default(false), | |
| 273 | + description: text("description"), | |
| 274 | + sourceUrl: text("source_url"), | |
| 275 | + confidence: text("confidence").notNull().default("moderate"), | |
| 276 | + externalIds: jsonb("external_ids").$type<Record<string, string | number>>().notNull().default(sql`'{}'::jsonb`), | |
| 277 | + lastUpdate: ts("last_update").notNull().defaultNow(), | |
| 278 | + createdAt: createdAt(), | |
| 279 | + updatedAt: updatedAt(), | |
| 280 | + }, | |
| 281 | + (t) => [index("projects_country_idx").on(t.countryIso2), index("projects_operator_idx").on(t.operatorId), index("projects_status_idx").on(t.status), index("projects_norm_idx").on(t.normalizedName)], | |
| 282 | +); | |
| 283 | + | |
| 284 | +export const projectTimeline = pgTable( | |
| 285 | + "project_timeline", | |
| 286 | + { | |
| 287 | + id: text("id").primaryKey(), | |
| 288 | + projectId: text("project_id").notNull().references(() => projects.id, { onDelete: "cascade" }), | |
| 289 | + eventDate: text("event_date").notNull(), // partial date | |
| 290 | + eventType: text("event_type").notNull(), | |
| 291 | + description: text("description").notNull(), | |
| 292 | + sourceId: text("source_id"), | |
| 293 | + documentId: text("document_id"), | |
| 294 | + url: text("url"), | |
| 295 | + createdAt: createdAt(), | |
| 296 | + }, | |
| 297 | + (t) => [index("project_timeline_project_idx").on(t.projectId)], | |
| 298 | +); | |
| 299 | + | |
| 300 | +/** Sources = websites / datasets a connector reads from. */ | |
| 301 | +export const sources = pgTable("sources", { | |
| 302 | + id: text("id").primaryKey(), | |
| 303 | + connectorId: text("connector_id").notNull(), | |
| 304 | + name: text("name").notNull(), | |
| 305 | + domain: text("domain").notNull(), | |
| 306 | + kind: text("kind").notNull(), // SourceKind | |
| 307 | + priority: integer("priority").notNull().default(3), | |
| 308 | + url: text("url"), | |
| 309 | + license: text("license"), | |
| 310 | + attribution: text("attribution"), | |
| 311 | + robotsAllowed: boolean("robots_allowed"), | |
| 312 | + notes: text("notes"), | |
| 313 | + createdAt: createdAt(), | |
| 314 | + updatedAt: updatedAt(), | |
| 315 | +}); | |
| 316 | + | |
| 317 | +/** Connector runtime state (config is loaded from YAML and mirrored here for the admin UI). */ | |
| 318 | +export const connectors = pgTable("connectors", { | |
| 319 | + id: text("id").primaryKey(), | |
| 320 | + sourceName: text("source_name").notNull(), | |
| 321 | + domain: text("domain").notNull(), | |
| 322 | + kind: text("kind").notNull(), | |
| 323 | + mode: text("mode").notNull(), | |
| 324 | + enabled: boolean("enabled").notNull().default(true), | |
| 325 | + paused: boolean("paused").notNull().default(false), | |
| 326 | + config: jsonb("config").$type<Record<string, unknown>>().notNull().default(sql`'{}'::jsonb`), | |
| 327 | + parserVersion: text("parser_version").notNull().default("v1"), | |
| 328 | + schedule: jsonb("schedule").$type<Record<string, string>>().notNull().default(sql`'{}'::jsonb`), | |
| 329 | + health: text("health").notNull().default("never_run"), | |
| 330 | + lastRunAt: ts("last_run_at"), | |
| 331 | + lastStatus: text("last_status"), | |
| 332 | + lastError: text("last_error"), | |
| 333 | + nextRunAt: ts("next_run_at"), | |
| 334 | + stats: jsonb("stats").$type<Record<string, number>>().notNull().default(sql`'{}'::jsonb`), | |
| 335 | + createdAt: createdAt(), | |
| 336 | + updatedAt: updatedAt(), | |
| 337 | +}); | |
| 338 | + | |
| 339 | +export const connectorRuns = pgTable( | |
| 340 | + "connector_runs", | |
| 341 | + { | |
| 342 | + id: text("id").primaryKey(), | |
| 343 | + connectorId: text("connector_id").notNull(), | |
| 344 | + task: text("task").notNull().default("crawl"), // crawl | discover | reprocess | manual | |
| 345 | + startedAt: ts("started_at").notNull().defaultNow(), | |
| 346 | + finishedAt: ts("finished_at"), | |
| 347 | + status: text("status").notNull().default("running"), // running | ok | partial | failed | aborted | |
| 348 | + stats: jsonb("stats").$type<Record<string, number>>().notNull().default(sql`'{}'::jsonb`), | |
| 349 | + error: text("error"), | |
| 350 | + log: jsonb("log").$type<Array<{ t: string; level: string; msg: string }>>().notNull().default(sql`'[]'::jsonb`), | |
| 351 | + }, | |
| 352 | + (t) => [index("connector_runs_connector_idx").on(t.connectorId, t.startedAt)], | |
| 353 | +); | |
| 354 | + | |
| 355 | +/** Crawled documents (URL registry with scheduling + change-tracking state). Raw bodies live in object storage. */ | |
| 356 | +export const documents = pgTable( | |
| 357 | + "documents", | |
| 358 | + { | |
| 359 | + id: text("id").primaryKey(), // stableId("document", canonicalUrl) | |
| 360 | + connectorId: text("connector_id").notNull(), | |
| 361 | + sourceId: text("source_id").notNull(), | |
| 362 | + url: text("url").notNull(), | |
| 363 | + canonicalUrl: text("canonical_url").notNull(), | |
| 364 | + urlFingerprint: text("url_fingerprint").notNull(), | |
| 365 | + pageType: text("page_type").notNull().default("unknown"), | |
| 366 | + classifier: text("classifier"), // rule id that classified it | |
| 367 | + fetchLevel: integer("fetch_level").notNull().default(1), | |
| 368 | + priority: integer("priority").notNull().default(50), | |
| 369 | + contentHash: text("content_hash"), | |
| 370 | + etag: text("etag"), | |
| 371 | + lastModified: text("last_modified"), | |
| 372 | + statusCode: integer("status_code"), | |
| 373 | + contentType: text("content_type"), | |
| 374 | + sizeBytes: integer("size_bytes"), | |
| 375 | + title: text("title"), | |
| 376 | + storageKey: text("storage_key"), | |
| 377 | + extractorVersion: text("extractor_version"), | |
| 378 | + extractOk: boolean("extract_ok"), | |
| 379 | + extractCount: integer("extract_count").notNull().default(0), | |
| 380 | + error: text("error"), | |
| 381 | + errorCount: integer("error_count").notNull().default(0), | |
| 382 | + firstSeen: ts("first_seen").notNull().defaultNow(), | |
| 383 | + lastFetched: ts("last_fetched"), | |
| 384 | + lastChanged: ts("last_changed"), | |
| 385 | + lastChecked: ts("last_checked"), | |
| 386 | + nextCheck: ts("next_check"), | |
| 387 | + changeFrequencyScore: real("change_frequency_score").notNull().default(0.5), | |
| 388 | + fetchCount: integer("fetch_count").notNull().default(0), | |
| 389 | + changeCount: integer("change_count").notNull().default(0), | |
| 390 | + discoveredFrom: text("discovered_from"), | |
| 391 | + quarantined: boolean("quarantined").notNull().default(false), | |
| 392 | + entityRefs: jsonb("entity_refs").$type<Array<{ type: string; id: string }>>().notNull().default(sql`'[]'::jsonb`), | |
| 393 | + updatedAt: updatedAt(), | |
| 394 | + }, | |
| 395 | + (t) => [ | |
| 396 | + uniqueIndex("documents_fingerprint_uq").on(t.urlFingerprint), | |
| 397 | + index("documents_connector_idx").on(t.connectorId), | |
| 398 | + index("documents_next_check_idx").on(t.nextCheck), | |
| 399 | + index("documents_page_type_idx").on(t.pageType), | |
| 400 | + ], | |
| 401 | +); | |
| 402 | + | |
| 403 | +export const documentVersions = pgTable( | |
| 404 | + "document_versions", | |
| 405 | + { | |
| 406 | + id: text("id").primaryKey(), | |
| 407 | + documentId: text("document_id").notNull().references(() => documents.id, { onDelete: "cascade" }), | |
| 408 | + contentHash: text("content_hash").notNull(), | |
| 409 | + fetchedAt: ts("fetched_at").notNull().defaultNow(), | |
| 410 | + fetchLevel: integer("fetch_level").notNull().default(1), | |
| 411 | + statusCode: integer("status_code"), | |
| 412 | + sizeBytes: integer("size_bytes"), | |
| 413 | + storageKey: text("storage_key"), | |
| 414 | + diffSummary: jsonb("diff_summary").$type<{ addedCount: number; removedCount: number; ratio: number; added: string[]; removed: string[] }>(), | |
| 415 | + detectedChanges: jsonb("detected_changes").$type<Array<Record<string, unknown>>>().notNull().default(sql`'[]'::jsonb`), | |
| 416 | + significance: integer("significance").notNull().default(0), | |
| 417 | + }, | |
| 418 | + (t) => [index("document_versions_doc_idx").on(t.documentId, t.fetchedAt)], | |
| 419 | +); | |
| 420 | + | |
| 421 | +/** Per-field provenance for every entity value. */ | |
| 422 | +export const provenance = pgTable( | |
| 423 | + "provenance", | |
| 424 | + { | |
| 425 | + id: text("id").primaryKey(), | |
| 426 | + entityType: text("entity_type").notNull(), | |
| 427 | + entityId: text("entity_id").notNull(), | |
| 428 | + field: text("field").notNull(), | |
| 429 | + value: jsonb("value"), | |
| 430 | + sourceId: text("source_id").notNull(), | |
| 431 | + connectorId: text("connector_id").notNull(), | |
| 432 | + documentId: text("document_id"), | |
| 433 | + url: text("url").notNull(), | |
| 434 | + firstObserved: ts("first_observed").notNull().defaultNow(), | |
| 435 | + lastObserved: ts("last_observed").notNull().defaultNow(), | |
| 436 | + retrievedAt: ts("retrieved_at").notNull().defaultNow(), | |
| 437 | + confidence: text("confidence").notNull().default("moderate"), | |
| 438 | + isEstimate: boolean("is_estimate").notNull().default(false), | |
| 439 | + method: text("method"), | |
| 440 | + extractorVersion: text("extractor_version"), | |
| 441 | + isCurrent: boolean("is_current").notNull().default(true), | |
| 442 | + note: text("note"), | |
| 443 | + }, | |
| 444 | + (t) => [ | |
| 445 | + index("provenance_entity_idx").on(t.entityType, t.entityId), | |
| 446 | + index("provenance_source_idx").on(t.sourceId), | |
| 447 | + uniqueIndex("provenance_uq").on(t.entityType, t.entityId, t.field, t.sourceId, t.url), | |
| 448 | + ], | |
| 449 | +); | |
| 450 | + | |
| 451 | +export const events = pgTable( | |
| 452 | + "events", | |
| 453 | + { | |
| 454 | + id: text("id").primaryKey(), | |
| 455 | + entityType: text("entity_type").notNull(), | |
| 456 | + entityId: text("entity_id"), | |
| 457 | + eventType: text("event_type").notNull(), | |
| 458 | + detectedAt: ts("detected_at").notNull().defaultNow(), | |
| 459 | + effectiveDate: text("effective_date"), | |
| 460 | + oldValue: jsonb("old_value"), | |
| 461 | + newValue: jsonb("new_value"), | |
| 462 | + sourceId: text("source_id").notNull(), | |
| 463 | + documentId: text("document_id"), | |
| 464 | + url: text("url").notNull(), | |
| 465 | + title: text("title").notNull(), | |
| 466 | + summary: text("summary"), | |
| 467 | + significance: integer("significance").notNull().default(30), | |
| 468 | + confidence: text("confidence").notNull().default("moderate"), | |
| 469 | + reviewStatus: text("review_status").notNull().default("auto"), | |
| 470 | + countryIso2: text("country_iso2"), | |
| 471 | + operatorId: text("operator_id"), | |
| 472 | + metroId: text("metro_id"), | |
| 473 | + projectId: text("project_id"), | |
| 474 | + fingerprint: text("fingerprint").notNull(), | |
| 475 | + }, | |
| 476 | + (t) => [ | |
| 477 | + uniqueIndex("events_fingerprint_uq").on(t.fingerprint), | |
| 478 | + index("events_detected_idx").on(t.detectedAt), | |
| 479 | + index("events_entity_idx").on(t.entityType, t.entityId), | |
| 480 | + index("events_country_idx").on(t.countryIso2), | |
| 481 | + index("events_operator_idx").on(t.operatorId), | |
| 482 | + index("events_type_idx").on(t.eventType), | |
| 483 | + ], | |
| 484 | +); | |
| 485 | + | |
| 486 | +export const newsItems = pgTable( | |
| 487 | + "news_items", | |
| 488 | + { | |
| 489 | + id: text("id").primaryKey(), | |
| 490 | + sourceId: text("source_id").notNull(), | |
| 491 | + connectorId: text("connector_id").notNull(), | |
| 492 | + url: text("url").notNull().unique(), | |
| 493 | + title: text("title").notNull(), | |
| 494 | + publishedAt: ts("published_at"), | |
| 495 | + summary: text("summary"), | |
| 496 | + pageType: text("page_type").notNull().default("unknown"), | |
| 497 | + eventType: text("event_type"), | |
| 498 | + mentions: jsonb("mentions").$type<Record<string, unknown>>().notNull().default(sql`'{}'::jsonb`), | |
| 499 | + operatorIds: text("operator_ids").array().notNull().default(sql`'{}'::text[]`), | |
| 500 | + countryIso2s: text("country_iso2s").array().notNull().default(sql`'{}'::text[]`), | |
| 501 | + facilityIds: text("facility_ids").array().notNull().default(sql`'{}'::text[]`), | |
| 502 | + projectId: text("project_id"), | |
| 503 | + mw: doublePrecision("mw"), | |
| 504 | + significance: integer("significance").notNull().default(20), | |
| 505 | + createdAt: createdAt(), | |
| 506 | + }, | |
| 507 | + (t) => [index("news_items_published_idx").on(t.publishedAt)], | |
| 508 | +); | |
| 509 | + | |
| 510 | +/** Reconciliation queue: candidate records whose match to an existing facility was ambiguous. */ | |
| 511 | +export const entityMatches = pgTable( | |
| 512 | + "entity_matches", | |
| 513 | + { | |
| 514 | + id: text("id").primaryKey(), | |
| 515 | + connectorId: text("connector_id").notNull(), | |
| 516 | + candidateKey: text("candidate_key").notNull(), | |
| 517 | + candidate: jsonb("candidate").$type<Record<string, unknown>>().notNull(), | |
| 518 | + matchedFacilityId: text("matched_facility_id"), | |
| 519 | + score: real("score").notNull(), | |
| 520 | + reasons: text("reasons").array().notNull().default(sql`'{}'::text[]`), | |
| 521 | + status: text("status").notNull().default("pending"), // pending | approved | rejected | auto_merged | auto_created | |
| 522 | + decidedBy: text("decided_by"), | |
| 523 | + decidedAt: ts("decided_at"), | |
| 524 | + createdAt: createdAt(), | |
| 525 | + }, | |
| 526 | + (t) => [index("entity_matches_status_idx").on(t.status), index("entity_matches_key_idx").on(t.candidateKey)], | |
| 527 | +); | |
| 528 | + | |
| 529 | +export const rankings = pgTable( | |
| 530 | + "rankings", | |
| 531 | + { | |
| 532 | + id: text("id").primaryKey(), | |
| 533 | + key: text("key").notNull(), | |
| 534 | + scope: text("scope").notNull(), | |
| 535 | + label: text("label").notNull(), | |
| 536 | + unit: text("unit").notNull(), | |
| 537 | + methodology: text("methodology").notNull(), | |
| 538 | + minCoverage: real("min_coverage"), | |
| 539 | + rows: jsonb("rows").$type<Array<Record<string, unknown>>>().notNull(), | |
| 540 | + total: integer("total").notNull(), | |
| 541 | + computedAt: ts("computed_at").notNull().defaultNow(), | |
| 542 | + isCurrent: boolean("is_current").notNull().default(true), | |
| 543 | + }, | |
| 544 | + (t) => [index("rankings_key_idx").on(t.key, t.isCurrent)], | |
| 545 | +); | |
| 546 | + | |
| 547 | +/** Daily aggregate snapshots (also mirrored to ClickHouse) for growth charts. */ | |
| 548 | +export const dailyMetrics = pgTable( | |
| 549 | + "daily_metrics", | |
| 550 | + { | |
| 551 | + day: date("day").notNull(), | |
| 552 | + metric: text("metric").notNull(), // facilities_total, known_mw, construction_mw, planned_mw, events, … | |
| 553 | + dim: text("dim").notNull().default("global"), // "global" | "country:US" | "operator:op_x" | "metro:met_x" | |
| 554 | + value: doublePrecision("value").notNull(), | |
| 555 | + }, | |
| 556 | + (t) => [primaryKey({ columns: [t.day, t.metric, t.dim] })], | |
| 557 | +); | |
| 558 | + | |
| 559 | +export const adminSessions = pgTable("admin_sessions", { | |
| 560 | + id: text("id").primaryKey(), | |
| 561 | + createdAt: createdAt(), | |
| 562 | + expiresAt: ts("expires_at").notNull(), | |
| 563 | + ip: text("ip"), | |
| 564 | + userAgent: text("user_agent"), | |
| 565 | +}); | |
| 566 | + | |
| 567 | +export const systemAlerts = pgTable("system_alerts", { | |
| 568 | + id: text("id").primaryKey(), | |
| 569 | + level: text("level").notNull(), // info | warn | error | |
| 570 | + component: text("component").notNull(), | |
| 571 | + message: text("message").notNull(), | |
| 572 | + details: jsonb("details").$type<Record<string, unknown>>(), | |
| 573 | + resolvedAt: ts("resolved_at"), | |
| 574 | + createdAt: createdAt(), | |
| 575 | +}); | |
added
packages/db/tsconfig.json
+1 −0
@@ -0,0 +1 @@ | ||
| 1 | +{ "extends": "../../tsconfig.base.json", "compilerOptions": { "noEmit": true, "types": ["node"] }, "include": ["src"] } | |
added
pnpm-lock.yaml
+2058 −0
@@ -0,0 +1,2058 @@ | ||
| 1 | +lockfileVersion: '9.0' | |
| 2 | + | |
| 3 | +settings: | |
| 4 | + autoInstallPeers: true | |
| 5 | + excludeLinksFromLockfile: false | |
| 6 | + | |
| 7 | +importers: | |
| 8 | + | |
| 9 | + .: | |
| 10 | + devDependencies: | |
| 11 | + '@types/node': | |
| 12 | + specifier: ^24.0.0 | |
| 13 | + version: 24.13.4 | |
| 14 | + tsx: | |
| 15 | + specifier: ^4.20.0 | |
| 16 | + version: 4.23.13 | |
| 17 | + typescript: | |
| 18 | + specifier: ^5.9.3 | |
| 19 | + version: 5.9.3 | |
| 20 | + vitest: | |
| 21 | + specifier: ^3.2.0 | |
| 22 | + version: 3.2.7(@types/node@24.13.4)(tsx@4.23.13)(yaml@2.9.0) | |
| 23 | + | |
| 24 | + packages/connectors: | |
| 25 | + dependencies: | |
| 26 | + '@dci/core': | |
| 27 | + specifier: workspace:* | |
| 28 | + version: link:../core | |
| 29 | + cheerio: | |
| 30 | + specifier: ^1.1.0 | |
| 31 | + version: 1.2.0 | |
| 32 | + fast-xml-parser: | |
| 33 | + specifier: ^5.2.0 | |
| 34 | + version: 5.11.1 | |
| 35 | + undici: | |
| 36 | + specifier: ^7.10.0 | |
| 37 | + version: 7.29.1 | |
| 38 | + unpdf: | |
| 39 | + specifier: ^1.0.0 | |
| 40 | + version: 1.8.1 | |
| 41 | + yaml: | |
| 42 | + specifier: ^2.8.0 | |
| 43 | + version: 2.9.0 | |
| 44 | + zod: | |
| 45 | + specifier: ^4.1.0 | |
| 46 | + version: 4.6.1 | |
| 47 | + devDependencies: | |
| 48 | + '@types/node': | |
| 49 | + specifier: ^24.0.0 | |
| 50 | + version: 24.13.4 | |
| 51 | + typescript: | |
| 52 | + specifier: ^5.9.3 | |
| 53 | + version: 5.9.3 | |
| 54 | + vitest: | |
| 55 | + specifier: ^3.2.0 | |
| 56 | + version: 3.2.7(@types/node@24.13.4)(tsx@4.23.13)(yaml@2.9.0) | |
| 57 | + | |
| 58 | + packages/core: | |
| 59 | + dependencies: | |
| 60 | + zod: | |
| 61 | + specifier: ^4.1.0 | |
| 62 | + version: 4.6.1 | |
| 63 | + devDependencies: | |
| 64 | + '@types/node': | |
| 65 | + specifier: ^24.0.0 | |
| 66 | + version: 24.13.4 | |
| 67 | + typescript: | |
| 68 | + specifier: ^5.9.3 | |
| 69 | + version: 5.9.3 | |
| 70 | + vitest: | |
| 71 | + specifier: ^3.2.0 | |
| 72 | + version: 3.2.7(@types/node@24.13.4)(tsx@4.23.13)(yaml@2.9.0) | |
| 73 | + | |
| 74 | + packages/db: | |
| 75 | + dependencies: | |
| 76 | + '@clickhouse/client': | |
| 77 | + specifier: ^1.11.0 | |
| 78 | + version: 1.23.1 | |
| 79 | + '@dci/core': | |
| 80 | + specifier: workspace:* | |
| 81 | + version: link:../core | |
| 82 | + drizzle-orm: | |
| 83 | + specifier: ^0.44.0 | |
| 84 | + version: 0.44.7(postgres@3.4.9) | |
| 85 | + postgres: | |
| 86 | + specifier: ^3.4.7 | |
| 87 | + version: 3.4.9 | |
| 88 | + devDependencies: | |
| 89 | + '@types/node': | |
| 90 | + specifier: ^24.0.0 | |
| 91 | + version: 24.13.4 | |
| 92 | + drizzle-kit: | |
| 93 | + specifier: ^0.31.0 | |
| 94 | + version: 0.31.10 | |
| 95 | + typescript: | |
| 96 | + specifier: ^5.9.3 | |
| 97 | + version: 5.9.3 | |
| 98 | + vitest: | |
| 99 | + specifier: ^3.2.0 | |
| 100 | + version: 3.2.7(@types/node@24.13.4)(tsx@4.23.13)(yaml@2.9.0) | |
| 101 | + | |
| 102 | +packages: | |
| 103 | + | |
| 104 | + '@clickhouse/client@1.23.1': | |
| 105 | + resolution: {integrity: sha512-vs3/Zc1dHvT171btW5nMoPsPCJ6QVJ5pp7obxzO5sjqwFx/jjz9wwCAqcFOdc2DhprugDBaVn+4dVY8hG3A9nw==} | |
| 106 | + engines: {node: '>=20'} | |
| 107 | + | |
| 108 | + '@drizzle-team/brocli@0.10.2': | |
| 109 | + resolution: {integrity: sha512-z33Il7l5dKjUgGULTqBsQBQwckHh5AbIuxhdsIxDDiZAzBOrZO6q9ogcWC65kU382AfynTfgNumVcNIjuIua6w==} | |
| 110 | + | |
| 111 | + '@esbuild-kit/core-utils@3.3.2': | |
| 112 | + resolution: {integrity: sha512-sPRAnw9CdSsRmEtnsl2WXWdyquogVpB3yZ3dgwJfe8zrOzTsV7cJvmwrKVa+0ma5BoiGJ+BoqkMvawbayKUsqQ==} | |
| 113 | + deprecated: 'Merged into tsx: https://tsx.hirok.io' | |
| 114 | + | |
| 115 | + '@esbuild-kit/esm-loader@2.6.5': | |
| 116 | + resolution: {integrity: sha512-FxEMIkJKnodyA1OaCUoEvbYRkoZlLZ4d/eXFu9Fh8CbBBgP5EmZxrfTRyN0qpXZ4vOvqnE5YdRdcrmUUXuU+dA==} | |
| 117 | + deprecated: 'Merged into tsx: https://tsx.hirok.io' | |
| 118 | + | |
| 119 | + '@esbuild/aix-ppc64@0.25.12': | |
| 120 | + resolution: {integrity: sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==} | |
| 121 | + engines: {node: '>=18'} | |
| 122 | + cpu: [ppc64] | |
| 123 | + os: [aix] | |
| 124 | + | |
| 125 | + '@esbuild/aix-ppc64@0.28.2': | |
| 126 | + resolution: {integrity: sha512-XExcO+dvLKvVtNTibSTBej1NCAbaGhWn9Ww1ZPx80qsahhPFe/8jgWP0IchNe0F3HwkU7n8ejhH8bjonqht8mQ==} | |
| 127 | + engines: {node: '>=18'} | |
| 128 | + cpu: [ppc64] | |
| 129 | + os: [aix] | |
| 130 | + | |
| 131 | + '@esbuild/android-arm64@0.18.20': | |
| 132 | + resolution: {integrity: sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ==} | |
| 133 | + engines: {node: '>=12'} | |
| 134 | + cpu: [arm64] | |
| 135 | + os: [android] | |
| 136 | + | |
| 137 | + '@esbuild/android-arm64@0.25.12': | |
| 138 | + resolution: {integrity: sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==} | |
| 139 | + engines: {node: '>=18'} | |
| 140 | + cpu: [arm64] | |
| 141 | + os: [android] | |
| 142 | + | |
| 143 | + '@esbuild/android-arm64@0.28.2': | |
| 144 | + resolution: {integrity: sha512-5YfKeeI8qWfBZIX+u2xZC3Zlb3Os/gLS2sbEKM+I4ZOcsWmHS2WLysCcQZDAFRslDUU5Oiq44gf6PYN1vGwG5A==} | |
| 145 | + engines: {node: '>=18'} | |
| 146 | + cpu: [arm64] | |
| 147 | + os: [android] | |
| 148 | + | |
| 149 | + '@esbuild/android-arm@0.18.20': | |
| 150 | + resolution: {integrity: sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw==} | |
| 151 | + engines: {node: '>=12'} | |
| 152 | + cpu: [arm] | |
| 153 | + os: [android] | |
| 154 | + | |
| 155 | + '@esbuild/android-arm@0.25.12': | |
| 156 | + resolution: {integrity: sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==} | |
| 157 | + engines: {node: '>=18'} | |
| 158 | + cpu: [arm] | |
| 159 | + os: [android] | |
| 160 | + | |
| 161 | + '@esbuild/android-arm@0.28.2': | |
| 162 | + resolution: {integrity: sha512-kXXoiPVVGQcnIYGOeaovwOURpniDBpSq4A03qkQ+BMQqtGG6HYap3xne9C1O1yo4TR3qxlCX5IqqmX6fFo2Lqg==} | |
| 163 | + engines: {node: '>=18'} | |
| 164 | + cpu: [arm] | |
| 165 | + os: [android] | |
| 166 | + | |
| 167 | + '@esbuild/android-x64@0.18.20': | |
| 168 | + resolution: {integrity: sha512-8GDdlePJA8D6zlZYJV/jnrRAi6rOiNaCC/JclcXpB+KIuvfBN4owLtgzY2bsxnx666XjJx2kDPUmnTtR8qKQUg==} | |
| 169 | + engines: {node: '>=12'} | |
| 170 | + cpu: [x64] | |
| 171 | + os: [android] | |
| 172 | + | |
| 173 | + '@esbuild/android-x64@0.25.12': | |
| 174 | + resolution: {integrity: sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==} | |
| 175 | + engines: {node: '>=18'} | |
| 176 | + cpu: [x64] | |
| 177 | + os: [android] | |
| 178 | + | |
| 179 | + '@esbuild/android-x64@0.28.2': | |
| 180 | + resolution: {integrity: sha512-O387ite7SzUyCcy3JQX4P4bLtEA7bLLkx+esve5JHnyYfNTxcVpXZo9jhdB0lTKN44gztELTdU7nS8Nr16Fs1Q==} | |
| 181 | + engines: {node: '>=18'} | |
| 182 | + cpu: [x64] | |
| 183 | + os: [android] | |
| 184 | + | |
| 185 | + '@esbuild/darwin-arm64@0.18.20': | |
| 186 | + resolution: {integrity: sha512-bxRHW5kHU38zS2lPTPOyuyTm+S+eobPUnTNkdJEfAddYgEcll4xkT8DB9d2008DtTbl7uJag2HuE5NZAZgnNEA==} | |
| 187 | + engines: {node: '>=12'} | |
| 188 | + cpu: [arm64] | |
| 189 | + os: [darwin] | |
| 190 | + | |
| 191 | + '@esbuild/darwin-arm64@0.25.12': | |
| 192 | + resolution: {integrity: sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==} | |
| 193 | + engines: {node: '>=18'} | |
| 194 | + cpu: [arm64] | |
| 195 | + os: [darwin] | |
| 196 | + | |
| 197 | + '@esbuild/darwin-arm64@0.28.2': | |
| 198 | + resolution: {integrity: sha512-n4KqkOQrraxHJcgjM1RvwbigfQKIKJVpM7xp+KsxiyUSrRdIXnt73VhrPAx0fV44hgfmIVKjxMN9J1t5jySVkw==} | |
| 199 | + engines: {node: '>=18'} | |
| 200 | + cpu: [arm64] | |
| 201 | + os: [darwin] | |
| 202 | + | |
| 203 | + '@esbuild/darwin-x64@0.18.20': | |
| 204 | + resolution: {integrity: sha512-pc5gxlMDxzm513qPGbCbDukOdsGtKhfxD1zJKXjCCcU7ju50O7MeAZ8c4krSJcOIJGFR+qx21yMMVYwiQvyTyQ==} | |
| 205 | + engines: {node: '>=12'} | |
| 206 | + cpu: [x64] | |
| 207 | + os: [darwin] | |
| 208 | + | |
| 209 | + '@esbuild/darwin-x64@0.25.12': | |
| 210 | + resolution: {integrity: sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==} | |
| 211 | + engines: {node: '>=18'} | |
| 212 | + cpu: [x64] | |
| 213 | + os: [darwin] | |
| 214 | + | |
| 215 | + '@esbuild/darwin-x64@0.28.2': | |
| 216 | + resolution: {integrity: sha512-uq6suIWYP37qzGddBKPw5QEQPi6HiLGsO7UmkpfyaYNQ3D+rN6w6WfwH+nuqcGXWvawGwxOEroO4YGnFh95azw==} | |
| 217 | + engines: {node: '>=18'} | |
| 218 | + cpu: [x64] | |
| 219 | + os: [darwin] | |
| 220 | + | |
| 221 | + '@esbuild/freebsd-arm64@0.18.20': | |
| 222 | + resolution: {integrity: sha512-yqDQHy4QHevpMAaxhhIwYPMv1NECwOvIpGCZkECn8w2WFHXjEwrBn3CeNIYsibZ/iZEUemj++M26W3cNR5h+Tw==} | |
| 223 | + engines: {node: '>=12'} | |
| 224 | + cpu: [arm64] | |
| 225 | + os: [freebsd] | |
| 226 | + | |
| 227 | + '@esbuild/freebsd-arm64@0.25.12': | |
| 228 | + resolution: {integrity: sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==} | |
| 229 | + engines: {node: '>=18'} | |
| 230 | + cpu: [arm64] | |
| 231 | + os: [freebsd] | |
| 232 | + | |
| 233 | + '@esbuild/freebsd-arm64@0.28.2': | |
| 234 | + resolution: {integrity: sha512-n+I0BTSRIoy+d6RPKnEVwql5UwBJolytvY4mAOIEJorKlqgPII8ix6slVVrfZ5Tnj7glIZvloylbB/EJPMWEXw==} | |
| 235 | + engines: {node: '>=18'} | |
| 236 | + cpu: [arm64] | |
| 237 | + os: [freebsd] | |
| 238 | + | |
| 239 | + '@esbuild/freebsd-x64@0.18.20': | |
| 240 | + resolution: {integrity: sha512-tgWRPPuQsd3RmBZwarGVHZQvtzfEBOreNuxEMKFcd5DaDn2PbBxfwLcj4+aenoh7ctXcbXmOQIn8HI6mCSw5MQ==} | |
| 241 | + engines: {node: '>=12'} | |
| 242 | + cpu: [x64] | |
| 243 | + os: [freebsd] | |
| 244 | + | |
| 245 | + '@esbuild/freebsd-x64@0.25.12': | |
| 246 | + resolution: {integrity: sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==} | |
| 247 | + engines: {node: '>=18'} | |
| 248 | + cpu: [x64] | |
| 249 | + os: [freebsd] | |
| 250 | + | |
| 251 | + '@esbuild/freebsd-x64@0.28.2': | |
| 252 | + resolution: {integrity: sha512-78XJTJkvPs0kz2w61301PJjXl4g7q3JqiYMZ/M/yVI73EHBrCRTgkhu9oqG7vPqq+a/yadEW8aD+agKlk5xrmg==} | |
| 253 | + engines: {node: '>=18'} | |
| 254 | + cpu: [x64] | |
| 255 | + os: [freebsd] | |
| 256 | + | |
| 257 | + '@esbuild/linux-arm64@0.18.20': | |
| 258 | + resolution: {integrity: sha512-2YbscF+UL7SQAVIpnWvYwM+3LskyDmPhe31pE7/aoTMFKKzIc9lLbyGUpmmb8a8AixOL61sQ/mFh3jEjHYFvdA==} | |
| 259 | + engines: {node: '>=12'} | |
| 260 | + cpu: [arm64] | |
| 261 | + os: [linux] | |
| 262 | + | |
| 263 | + '@esbuild/linux-arm64@0.25.12': | |
| 264 | + resolution: {integrity: sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==} | |
| 265 | + engines: {node: '>=18'} | |
| 266 | + cpu: [arm64] | |
| 267 | + os: [linux] | |
| 268 | + | |
| 269 | + '@esbuild/linux-arm64@0.28.2': | |
| 270 | + resolution: {integrity: sha512-pW4AC0P3it8c7do9MVM4p51FzHzdM/TZrerurgRcHJ2WTa1VQ1CIq18xncfpBJw4ojkiZZrKW2yIBWBP92j6Ug==} | |
| 271 | + engines: {node: '>=18'} | |
| 272 | + cpu: [arm64] | |
| 273 | + os: [linux] | |
| 274 | + | |
| 275 | + '@esbuild/linux-arm@0.18.20': | |
| 276 | + resolution: {integrity: sha512-/5bHkMWnq1EgKr1V+Ybz3s1hWXok7mDFUMQ4cG10AfW3wL02PSZi5kFpYKrptDsgb2WAJIvRcDm+qIvXf/apvg==} | |
| 277 | + engines: {node: '>=12'} | |
| 278 | + cpu: [arm] | |
| 279 | + os: [linux] | |
| 280 | + | |
| 281 | + '@esbuild/linux-arm@0.25.12': | |
| 282 | + resolution: {integrity: sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==} | |
| 283 | + engines: {node: '>=18'} | |
| 284 | + cpu: [arm] | |
| 285 | + os: [linux] | |
| 286 | + | |
| 287 | + '@esbuild/linux-arm@0.28.2': | |
| 288 | + resolution: {integrity: sha512-XlDnu2q5yoqems+xay6wSAcg9DDD7K9RLKZEBOMZm3ckNpJBvOX20tSfby8KfrrhINDyv9V2YVZKY/SpoGJI8w==} | |
| 289 | + engines: {node: '>=18'} | |
| 290 | + cpu: [arm] | |
| 291 | + os: [linux] | |
| 292 | + | |
| 293 | + '@esbuild/linux-ia32@0.18.20': | |
| 294 | + resolution: {integrity: sha512-P4etWwq6IsReT0E1KHU40bOnzMHoH73aXp96Fs8TIT6z9Hu8G6+0SHSw9i2isWrD2nbx2qo5yUqACgdfVGx7TA==} | |
| 295 | + engines: {node: '>=12'} | |
| 296 | + cpu: [ia32] | |
| 297 | + os: [linux] | |
| 298 | + | |
| 299 | + '@esbuild/linux-ia32@0.25.12': | |
| 300 | + resolution: {integrity: sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==} | |
| 301 | + engines: {node: '>=18'} | |
| 302 | + cpu: [ia32] | |
| 303 | + os: [linux] | |
| 304 | + | |
| 305 | + '@esbuild/linux-ia32@0.28.2': | |
| 306 | + resolution: {integrity: sha512-CYbnj78HsIeA+DhgUKgFCfvNsTHFhMMrinUrMZpDXJXKN8T3XViTZ/+wtHeVxEWY8ewSzTFN+nRmSwO2tZaLUQ==} | |
| 307 | + engines: {node: '>=18'} | |
| 308 | + cpu: [ia32] | |
| 309 | + os: [linux] | |
| 310 | + | |
| 311 | + '@esbuild/linux-loong64@0.18.20': | |
| 312 | + resolution: {integrity: sha512-nXW8nqBTrOpDLPgPY9uV+/1DjxoQ7DoB2N8eocyq8I9XuqJ7BiAMDMf9n1xZM9TgW0J8zrquIb/A7s3BJv7rjg==} | |
| 313 | + engines: {node: '>=12'} | |
| 314 | + cpu: [loong64] | |
| 315 | + os: [linux] | |
| 316 | + | |
| 317 | + '@esbuild/linux-loong64@0.25.12': | |
| 318 | + resolution: {integrity: sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==} | |
| 319 | + engines: {node: '>=18'} | |
| 320 | + cpu: [loong64] | |
| 321 | + os: [linux] | |
| 322 | + | |
| 323 | + '@esbuild/linux-loong64@0.28.2': | |
| 324 | + resolution: {integrity: sha512-buwkd8nsph4R+ajRvw0qM5Hja/TXQow3ptzWO2EbG/cqcIkHloRrdlBtQlshyYGTNFvfkfJ5tpPLVkY4DtsPfQ==} | |
| 325 | + engines: {node: '>=18'} | |
| 326 | + cpu: [loong64] | |
| 327 | + os: [linux] | |
| 328 | + | |
| 329 | + '@esbuild/linux-mips64el@0.18.20': | |
| 330 | + resolution: {integrity: sha512-d5NeaXZcHp8PzYy5VnXV3VSd2D328Zb+9dEq5HE6bw6+N86JVPExrA6O68OPwobntbNJ0pzCpUFZTo3w0GyetQ==} | |
| 331 | + engines: {node: '>=12'} | |
| 332 | + cpu: [mips64el] | |
| 333 | + os: [linux] | |
| 334 | + | |
| 335 | + '@esbuild/linux-mips64el@0.25.12': | |
| 336 | + resolution: {integrity: sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==} | |
| 337 | + engines: {node: '>=18'} | |
| 338 | + cpu: [mips64el] | |
| 339 | + os: [linux] | |
| 340 | + | |
| 341 | + '@esbuild/linux-mips64el@0.28.2': | |
| 342 | + resolution: {integrity: sha512-ZVykbDyk7519VwiNb9Lcj9m8XM6v5V9uKPvrEMkkEedVewf+0itkhahp4HDpgERXhwLRpWFypsGbG/J8s0QjJA==} | |
| 343 | + engines: {node: '>=18'} | |
| 344 | + cpu: [mips64el] | |
| 345 | + os: [linux] | |
| 346 | + | |
| 347 | + '@esbuild/linux-ppc64@0.18.20': | |
| 348 | + resolution: {integrity: sha512-WHPyeScRNcmANnLQkq6AfyXRFr5D6N2sKgkFo2FqguP44Nw2eyDlbTdZwd9GYk98DZG9QItIiTlFLHJHjxP3FA==} | |
| 349 | + engines: {node: '>=12'} | |
| 350 | + cpu: [ppc64] | |
| 351 | + os: [linux] | |
| 352 | + | |
| 353 | + '@esbuild/linux-ppc64@0.25.12': | |
| 354 | + resolution: {integrity: sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==} | |
| 355 | + engines: {node: '>=18'} | |
| 356 | + cpu: [ppc64] | |
| 357 | + os: [linux] | |
| 358 | + | |
| 359 | + '@esbuild/linux-ppc64@0.28.2': | |
| 360 | + resolution: {integrity: sha512-CAXl+Dtd9UUuJd8pKKdwh6MLm3MUMiqMPmhZ3tTSXPqfyQ3vDl6R5hZdZ/kYojK4ofXtdfSv1tFq8XzWx3heNQ==} | |
| 361 | + engines: {node: '>=18'} | |
| 362 | + cpu: [ppc64] | |
| 363 | + os: [linux] | |
| 364 | + | |
| 365 | + '@esbuild/linux-riscv64@0.18.20': | |
| 366 | + resolution: {integrity: sha512-WSxo6h5ecI5XH34KC7w5veNnKkju3zBRLEQNY7mv5mtBmrP/MjNBCAlsM2u5hDBlS3NGcTQpoBvRzqBcRtpq1A==} | |
| 367 | + engines: {node: '>=12'} | |
| 368 | + cpu: [riscv64] | |
| 369 | + os: [linux] | |
| 370 | + | |
| 371 | + '@esbuild/linux-riscv64@0.25.12': | |
| 372 | + resolution: {integrity: sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==} | |
| 373 | + engines: {node: '>=18'} | |
| 374 | + cpu: [riscv64] | |
| 375 | + os: [linux] | |
| 376 | + | |
| 377 | + '@esbuild/linux-riscv64@0.28.2': | |
| 378 | + resolution: {integrity: sha512-GeXCej4IQtU1B+QlDV8W/RRvbzI3O/Stss+/bCXv4lZls5WGRtu2a+3JkA3i4qIUlMXpcHebWpF8AkJhATowuA==} | |
| 379 | + engines: {node: '>=18'} | |
| 380 | + cpu: [riscv64] | |
| 381 | + os: [linux] | |
| 382 | + | |
| 383 | + '@esbuild/linux-s390x@0.18.20': | |
| 384 | + resolution: {integrity: sha512-+8231GMs3mAEth6Ja1iK0a1sQ3ohfcpzpRLH8uuc5/KVDFneH6jtAJLFGafpzpMRO6DzJ6AvXKze9LfFMrIHVQ==} | |
| 385 | + engines: {node: '>=12'} | |
| 386 | + cpu: [s390x] | |
| 387 | + os: [linux] | |
| 388 | + | |
| 389 | + '@esbuild/linux-s390x@0.25.12': | |
| 390 | + resolution: {integrity: sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==} | |
| 391 | + engines: {node: '>=18'} | |
| 392 | + cpu: [s390x] | |
| 393 | + os: [linux] | |
| 394 | + | |
| 395 | + '@esbuild/linux-s390x@0.28.2': | |
| 396 | + resolution: {integrity: sha512-3H1weTYZPxt/WOhByszQZybS9w5lKzUn1FDMsgEChbHWQwHYQQRfBxgCcZvPhjHfKyJjIievvMmEUawJrdY9Dg==} | |
| 397 | + engines: {node: '>=18'} | |
| 398 | + cpu: [s390x] | |
| 399 | + os: [linux] | |
| 400 | + | |
| 401 | + '@esbuild/linux-x64@0.18.20': | |
| 402 | + resolution: {integrity: sha512-UYqiqemphJcNsFEskc73jQ7B9jgwjWrSayxawS6UVFZGWrAAtkzjxSqnoclCXxWtfwLdzU+vTpcNYhpn43uP1w==} | |
| 403 | + engines: {node: '>=12'} | |
| 404 | + cpu: [x64] | |
| 405 | + os: [linux] | |
| 406 | + | |
| 407 | + '@esbuild/linux-x64@0.25.12': | |
| 408 | + resolution: {integrity: sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==} | |
| 409 | + engines: {node: '>=18'} | |
| 410 | + cpu: [x64] | |
| 411 | + os: [linux] | |
| 412 | + | |
| 413 | + '@esbuild/linux-x64@0.28.2': | |
| 414 | + resolution: {integrity: sha512-4xTZr1FUmSoQW4XIWmit3tzQrUTZM+N3P0XV8xROKYF50XfI7xeO90+1bZvNwxIufQ9hDQVRJH5YhgPVF8A/HQ==} | |
| 415 | + engines: {node: '>=18'} | |
| 416 | + cpu: [x64] | |
| 417 | + os: [linux] | |
| 418 | + | |
| 419 | + '@esbuild/netbsd-arm64@0.25.12': | |
| 420 | + resolution: {integrity: sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==} | |
| 421 | + engines: {node: '>=18'} | |
| 422 | + cpu: [arm64] | |
| 423 | + os: [netbsd] | |
| 424 | + | |
| 425 | + '@esbuild/netbsd-arm64@0.28.2': | |
| 426 | + resolution: {integrity: sha512-sSATRjPeDBg3pdgHoQfoYBob11Kk1FGa9lui5RIHZCoCkJa9QKlvl3/vKz2usCmYYjs7ymJR/2Nnsqe+Hjt5nw==} | |
| 427 | + engines: {node: '>=18'} | |
| 428 | + cpu: [arm64] | |
| 429 | + os: [netbsd] | |
| 430 | + | |
| 431 | + '@esbuild/netbsd-x64@0.18.20': | |
| 432 | + resolution: {integrity: sha512-iO1c++VP6xUBUmltHZoMtCUdPlnPGdBom6IrO4gyKPFFVBKioIImVooR5I83nTew5UOYrk3gIJhbZh8X44y06A==} | |
| 433 | + engines: {node: '>=12'} | |
| 434 | + cpu: [x64] | |
| 435 | + os: [netbsd] | |
| 436 | + | |
| 437 | + '@esbuild/netbsd-x64@0.25.12': | |
| 438 | + resolution: {integrity: sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==} | |
| 439 | + engines: {node: '>=18'} | |
| 440 | + cpu: [x64] | |
| 441 | + os: [netbsd] | |
| 442 | + | |
| 443 | + '@esbuild/netbsd-x64@0.28.2': | |
| 444 | + resolution: {integrity: sha512-lqnzCV+mM0gIADaKihiCg6ifgfU2L3h5E33rNQBN1Y4MaVGnzryzmvvf7UHxprpQdE8hpqLolJ9Rl+SkIRDpyw==} | |
| 445 | + engines: {node: '>=18'} | |
| 446 | + cpu: [x64] | |
| 447 | + os: [netbsd] | |
| 448 | + | |
| 449 | + '@esbuild/openbsd-arm64@0.25.12': | |
| 450 | + resolution: {integrity: sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==} | |
| 451 | + engines: {node: '>=18'} | |
| 452 | + cpu: [arm64] | |
| 453 | + os: [openbsd] | |
| 454 | + | |
| 455 | + '@esbuild/openbsd-arm64@0.28.2': | |
| 456 | + resolution: {integrity: sha512-AL2qJILH7lNjrDmCQDvdxMfAUIv8KMNZOvrwAQ8i8//ntL9FflhOyMJ8OZSMBb8/AWXe3/5v5S20y3zCoZWKoQ==} | |
| 457 | + engines: {node: '>=18'} | |
| 458 | + cpu: [arm64] | |
| 459 | + os: [openbsd] | |
| 460 | + | |
| 461 | + '@esbuild/openbsd-x64@0.18.20': | |
| 462 | + resolution: {integrity: sha512-e5e4YSsuQfX4cxcygw/UCPIEP6wbIL+se3sxPdCiMbFLBWu0eiZOJ7WoD+ptCLrmjZBK1Wk7I6D/I3NglUGOxg==} | |
| 463 | + engines: {node: '>=12'} | |
| 464 | + cpu: [x64] | |
| 465 | + os: [openbsd] | |
| 466 | + | |
| 467 | + '@esbuild/openbsd-x64@0.25.12': | |
| 468 | + resolution: {integrity: sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==} | |
| 469 | + engines: {node: '>=18'} | |
| 470 | + cpu: [x64] | |
| 471 | + os: [openbsd] | |
| 472 | + | |
| 473 | + '@esbuild/openbsd-x64@0.28.2': | |
| 474 | + resolution: {integrity: sha512-QtiuPytchRyC4rwUKhexJdQKvDuZ6hWloi3igqPQNUJCS1/v9EiO3UTOXR6A3FoMo4fnAKbWJdqaIwhOzh8qEw==} | |
| 475 | + engines: {node: '>=18'} | |
| 476 | + cpu: [x64] | |
| 477 | + os: [openbsd] | |
| 478 | + | |
| 479 | + '@esbuild/openharmony-arm64@0.25.12': | |
| 480 | + resolution: {integrity: sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==} | |
| 481 | + engines: {node: '>=18'} | |
| 482 | + cpu: [arm64] | |
| 483 | + os: [openharmony] | |
| 484 | + | |
| 485 | + '@esbuild/openharmony-arm64@0.28.2': | |
| 486 | + resolution: {integrity: sha512-WkhYDmpTjLvGlScA1rwjRUmhl4k8oXR3cIbtqWmELgU/dFeHHlEllxDvdWcNJV9rbzCexB5vz8gtNewWLgCT7Q==} | |
| 487 | + engines: {node: '>=18'} | |
| 488 | + cpu: [arm64] | |
| 489 | + os: [openharmony] | |
| 490 | + | |
| 491 | + '@esbuild/sunos-x64@0.18.20': | |
| 492 | + resolution: {integrity: sha512-kDbFRFp0YpTQVVrqUd5FTYmWo45zGaXe0X8E1G/LKFC0v8x0vWrhOWSLITcCn63lmZIxfOMXtCfti/RxN/0wnQ==} | |
| 493 | + engines: {node: '>=12'} | |
| 494 | + cpu: [x64] | |
| 495 | + os: [sunos] | |
| 496 | + | |
| 497 | + '@esbuild/sunos-x64@0.25.12': | |
| 498 | + resolution: {integrity: sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==} | |
| 499 | + engines: {node: '>=18'} | |
| 500 | + cpu: [x64] | |
| 501 | + os: [sunos] | |
| 502 | + | |
| 503 | + '@esbuild/sunos-x64@0.28.2': | |
| 504 | + resolution: {integrity: sha512-GPMSkTOtMnv2U2F8gxe4Io6qmVs+YKyp832Etqqxr0hFngmXQ3rzwytelm3GIn7T4VviRUlf3sOgBOiTdvaf7g==} | |
| 505 | + engines: {node: '>=18'} | |
| 506 | + cpu: [x64] | |
| 507 | + os: [sunos] | |
| 508 | + | |
| 509 | + '@esbuild/win32-arm64@0.18.20': | |
| 510 | + resolution: {integrity: sha512-ddYFR6ItYgoaq4v4JmQQaAI5s7npztfV4Ag6NrhiaW0RrnOXqBkgwZLofVTlq1daVTQNhtI5oieTvkRPfZrePg==} | |
| 511 | + engines: {node: '>=12'} | |
| 512 | + cpu: [arm64] | |
| 513 | + os: [win32] | |
| 514 | + | |
| 515 | + '@esbuild/win32-arm64@0.25.12': | |
| 516 | + resolution: {integrity: sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==} | |
| 517 | + engines: {node: '>=18'} | |
| 518 | + cpu: [arm64] | |
| 519 | + os: [win32] | |
| 520 | + | |
| 521 | + '@esbuild/win32-arm64@0.28.2': | |
| 522 | + resolution: {integrity: sha512-PIhhEkE9uPBleRBrQEJpUn7MBnibZzbGzYWPmY3x+YoVg/95zbjB4CxPPOQ8l5tYYM4mMaCthF8/1DIfBQQyWQ==} | |
| 523 | + engines: {node: '>=18'} | |
| 524 | + cpu: [arm64] | |
| 525 | + os: [win32] | |
| 526 | + | |
| 527 | + '@esbuild/win32-ia32@0.18.20': | |
| 528 | + resolution: {integrity: sha512-Wv7QBi3ID/rROT08SABTS7eV4hX26sVduqDOTe1MvGMjNd3EjOz4b7zeexIR62GTIEKrfJXKL9LFxTYgkyeu7g==} | |
| 529 | + engines: {node: '>=12'} | |
| 530 | + cpu: [ia32] | |
| 531 | + os: [win32] | |
| 532 | + | |
| 533 | + '@esbuild/win32-ia32@0.25.12': | |
| 534 | + resolution: {integrity: sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==} | |
| 535 | + engines: {node: '>=18'} | |
| 536 | + cpu: [ia32] | |
| 537 | + os: [win32] | |
| 538 | + | |
| 539 | + '@esbuild/win32-ia32@0.28.2': | |
| 540 | + resolution: {integrity: sha512-YmJbfTlvU7Sdn9BB+4PRES4oB6pxgS37MAONj+hBr/cpXS1aBPKXxNnDbu+QCWPj0o9dgyxeq79g6c5P8KeuYA==} | |
| 541 | + engines: {node: '>=18'} | |
| 542 | + cpu: [ia32] | |
| 543 | + os: [win32] | |
| 544 | + | |
| 545 | + '@esbuild/win32-x64@0.18.20': | |
| 546 | + resolution: {integrity: sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ==} | |
| 547 | + engines: {node: '>=12'} | |
| 548 | + cpu: [x64] | |
| 549 | + os: [win32] | |
| 550 | + | |
| 551 | + '@esbuild/win32-x64@0.25.12': | |
| 552 | + resolution: {integrity: sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==} | |
| 553 | + engines: {node: '>=18'} | |
| 554 | + cpu: [x64] | |
| 555 | + os: [win32] | |
| 556 | + | |
| 557 | + '@esbuild/win32-x64@0.28.2': | |
| 558 | + resolution: {integrity: sha512-5ebpxr3nWMzrL/rnUI755Jkuee0bHL/Gq0WTF9lvcpv73wAp5eu8MfBUgWK9bhWvZjj7yX8etf/8tI8Ney695g==} | |
| 559 | + engines: {node: '>=18'} | |
| 560 | + cpu: [x64] | |
| 561 | + os: [win32] | |
| 562 | + | |
| 563 | + '@jridgewell/sourcemap-codec@1.6.0': | |
| 564 | + resolution: {integrity: sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw==} | |
| 565 | + | |
| 566 | + '@napi-rs/lzma-linux-x64-gnu@1.5.1': | |
| 567 | + resolution: {integrity: sha512-oTXEIha4SsuXdTA4Iyskj0kpdx2yVXdhd75c2v3xGrHFfVMsbhTPZU/nMPL4sWKo4pBHm3aucLaqGlF696dTyQ==} | |
| 568 | + engines: {node: ^22.20 || ^24.12 || >=25} | |
| 569 | + cpu: [x64] | |
| 570 | + os: [linux] | |
| 571 | + libc: [glibc] | |
| 572 | + | |
| 573 | + '@nodable/entities@3.0.0': | |
| 574 | + resolution: {integrity: sha512-8L9xFeTYKhm49xfIypoe2W5wV1m/3Z58kT+7kR9A8OyFxcPduI4VmxaUMQyKYrRjUoLLSXv6EKKID5Tvj9cUVw==} | |
| 575 | + | |
| 576 | + '@rollup/rollup-android-arm-eabi@4.63.1': | |
| 577 | + resolution: {integrity: sha512-UZ8sUxPTiHWYX9QNdJedb1kDZSpS1t/VPWBWGSgqHNi9w3Cu6IXvu2mzbhiTiPvtrqgTQJ+zqiAq2iPIPilpaQ==} | |
| 578 | + cpu: [arm] | |
| 579 | + os: [android] | |
| 580 | + | |
| 581 | + '@rollup/rollup-android-arm64@4.63.1': | |
| 582 | + resolution: {integrity: sha512-cQ4nFQABN5cDvDpbvJ7bMStCpnaVxynZrRMfUJYgxcIk9Sh54FIO1vtfkg0B69REjER77ioZ/ov+eAApx/KmLQ==} | |
| 583 | + cpu: [arm64] | |
| 584 | + os: [android] | |
| 585 | + | |
| 586 | + '@rollup/rollup-darwin-arm64@4.63.1': | |
| 587 | + resolution: {integrity: sha512-FQNqd1lRy/0QhDk3xeRIkSBiCpXCiDnZO3YLVdcDKN1UBiKToNftCzcXYNLshmPDUMlu2TdeS8tGcsU6f3YF1Q==} | |
| 588 | + cpu: [arm64] | |
| 589 | + os: [darwin] | |
| 590 | + | |
| 591 | + '@rollup/rollup-darwin-x64@4.63.1': | |
| 592 | + resolution: {integrity: sha512-pvD16V939D3CloK0+qikpGaxiPrDUXTe7Y5cWOMkMSy7m1cawa8EGy/kXYi/G/cKAC4HDAbSnzCIk1WmsoOKXg==} | |
| 593 | + cpu: [x64] | |
| 594 | + os: [darwin] | |
| 595 | + | |
| 596 | + '@rollup/rollup-freebsd-arm64@4.63.1': | |
| 597 | + resolution: {integrity: sha512-pcFGeL2345VwdTnJhA6zLbew+YgWB0qBG2+dMtXjCicf6+rm6kO6cOoh5VnTe0ZMrMRgRyuHmCJxZWrIdzYuOw==} | |
| 598 | + cpu: [arm64] | |
| 599 | + os: [freebsd] | |
| 600 | + | |
| 601 | + '@rollup/rollup-freebsd-x64@4.63.1': | |
| 602 | + resolution: {integrity: sha512-mRJlqSRulVzcKq/LKA6ICSIc3K/l4fzlVn/gePn2nXIHy8seRi5z/eeRE0d/XMBxcMldiXtQTSpRj0tkkC3g8Q==} | |
| 603 | + cpu: [x64] | |
| 604 | + os: [freebsd] | |
| 605 | + | |
| 606 | + '@rollup/rollup-linux-arm-gnueabihf@4.63.1': | |
| 607 | + resolution: {integrity: sha512-YDUNvVM85TI3g/1OpnqKP1h4NeW/j64DfWMf+G3M809xNk1bJSnpFp4sh83NpmVE5DXnkh8ULor4LTVZKoYLHw==} | |
| 608 | + cpu: [arm] | |
| 609 | + os: [linux] | |
| 610 | + libc: [glibc] | |
| 611 | + | |
| 612 | + '@rollup/rollup-linux-arm-musleabihf@4.63.1': | |
| 613 | + resolution: {integrity: sha512-7Mcn71p9ZuQFAj+h+dhQXy/yeLePRS2yKRnmW1DijA9thKO5qap0GNOIQK4yQ6iP3SU0Mrb/yWo8h8vgRba8lw==} | |
| 614 | + cpu: [arm] | |
| 615 | + os: [linux] | |
| 616 | + libc: [musl] | |
| 617 | + | |
| 618 | + '@rollup/rollup-linux-arm64-gnu@4.63.1': | |
| 619 | + resolution: {integrity: sha512-4YiLQTX6U4CSl0L9cluep9A9W6UmTfqBDc2/CH6wlu54pl4E7Jn3cOD8oxzvBDEGk/JMKgJ47C8g+radF7mwvg==} | |
| 620 | + cpu: [arm64] | |
| 621 | + os: [linux] | |
| 622 | + libc: [glibc] | |
| 623 | + | |
| 624 | + '@rollup/rollup-linux-arm64-musl@4.63.1': | |
| 625 | + resolution: {integrity: sha512-2ra8F7w8OquwZN9z2/fKFnli69wa8PLwaVzRMIPGb13ByMJwC28Fbp8YcVGoUhlYMTt7j5j9bNgpysrN2UM+vw==} | |
| 626 | + cpu: [arm64] | |
| 627 | + os: [linux] | |
| 628 | + libc: [musl] | |
| 629 | + | |
| 630 | + '@rollup/rollup-linux-loong64-gnu@4.63.1': | |
| 631 | + resolution: {integrity: sha512-Sy20ncyhjmBP0Ml+UvQbimjlk6VFgjW5uNP+qqwHB00mTE8Bl2C1TuHTlRwK2YoXeZbee5lP2XevBWVkAQAtSQ==} | |
| 632 | + cpu: [loong64] | |
| 633 | + os: [linux] | |
| 634 | + libc: [glibc] | |
| 635 | + | |
| 636 | + '@rollup/rollup-linux-loong64-musl@4.63.1': | |
| 637 | + resolution: {integrity: sha512-noITLp8oNjYliPnGWmLyelIHwULGqbHloQHGw1rtxbWhTuWooRpnZarZQJ1y9EUC4szuCusCc+HEpUtxpIwYvA==} | |
| 638 | + cpu: [loong64] | |
| 639 | + os: [linux] | |
| 640 | + libc: [musl] | |
| 641 | + | |
| 642 | + '@rollup/rollup-linux-ppc64-gnu@4.63.1': | |
| 643 | + resolution: {integrity: sha512-hlxxXd+F1mWiAcaFR7Sv9ZQT6m6UfI8+Vy/kFJzztq2pDMU/0wZ9sish0iszNZvsQDo8Gc0i5yuFEOz5dDf6fA==} | |
| 644 | + cpu: [ppc64] | |
| 645 | + os: [linux] | |
| 646 | + libc: [glibc] | |
| 647 | + | |
| 648 | + '@rollup/rollup-linux-ppc64-musl@4.63.1': | |
| 649 | + resolution: {integrity: sha512-EF7OpqQTQ/BvGqLzUi4rEHuagCV9MugAUXSHemwPW5vxZ75RR+jxO/2j95Ph2dalMpFHSVECjRoioHZgA9zOYA==} | |
| 650 | + cpu: [ppc64] | |
| 651 | + os: [linux] | |
| 652 | + libc: [musl] | |
| 653 | + | |
| 654 | + '@rollup/rollup-linux-riscv64-gnu@4.63.1': | |
| 655 | + resolution: {integrity: sha512-wQO3JesW9PRkwlabQ27y7sPfVOOTLRG73I4F2UYHG5PXun3J9U3y+b7ezVKSYbsvSKGQ1k1cq8Qlun4C9kLt3w==} | |
| 656 | + cpu: [riscv64] | |
| 657 | + os: [linux] | |
| 658 | + libc: [glibc] | |
| 659 | + | |
| 660 | + '@rollup/rollup-linux-riscv64-musl@4.63.1': | |
| 661 | + resolution: {integrity: sha512-ouAGwhO6wHRXdnOVCOsB0tRFkA7nhNB2Nwax6oECXN0YiN8EYUTBAOudADOB1PI+yDL61TeNx/u7MVCzksNbkQ==} | |
| 662 | + cpu: [riscv64] | |
| 663 | + os: [linux] | |
| 664 | + libc: [musl] | |
| 665 | + | |
| 666 | + '@rollup/rollup-linux-s390x-gnu@4.63.1': | |
| 667 | + resolution: {integrity: sha512-q2R38Sn+1J8RxhfJ+T54wSWmyKXWec+9jgDfqO2AtArEqHO5R2aeayp5H5OYLr5UYDVGsVaZPEFUooMhYCdz5A==} | |
| 668 | + cpu: [s390x] | |
| 669 | + os: [linux] | |
| 670 | + libc: [glibc] | |
| 671 | + | |
| 672 | + '@rollup/rollup-linux-x64-gnu@4.63.1': | |
| 673 | + resolution: {integrity: sha512-gfI5T24WLLuFfSKw7Go/zDXjAAV0fny0swTaDv+WjK7vqcw4cRhFfdsyKL1n+ukI+ooBxn3bVQnyrn06WpI50w==} | |
| 674 | + cpu: [x64] | |
| 675 | + os: [linux] | |
| 676 | + libc: [glibc] | |
| 677 | + | |
| 678 | + '@rollup/rollup-linux-x64-musl@4.63.1': | |
| 679 | + resolution: {integrity: sha512-4h6XqthmB4Hspji84wvgk+ElodTsGj+dbZqHJHHtKxj4mYq0ANSEEPX9ys3moJueqsRjwpaJYH7874Itwnj2ow==} | |
| 680 | + cpu: [x64] | |
| 681 | + os: [linux] | |
| 682 | + libc: [musl] | |
| 683 | + | |
| 684 | + '@rollup/rollup-openbsd-x64@4.63.1': | |
| 685 | + resolution: {integrity: sha512-dlfCOa87o1VAYegLQ9EKilx2JCeRofiyPGhTCmqnuXZ6bMPiycO1rq1+sKoulAp7pGLIsTIw+1x5R+zgh5LhhA==} | |
| 686 | + cpu: [x64] | |
| 687 | + os: [openbsd] | |
| 688 | + | |
| 689 | + '@rollup/rollup-openharmony-arm64@4.63.1': | |
| 690 | + resolution: {integrity: sha512-cjkLbOlfcm3QGhMM1J5zaZjsw1GggbN6rw9UTSSRrPrR1KkcXnN7Uq9rPw34xImQ9VOY9GN+6u2Zj80B9ptkcw==} | |
| 691 | + cpu: [arm64] | |
| 692 | + os: [openharmony] | |
| 693 | + | |
| 694 | + '@rollup/rollup-win32-arm64-msvc@4.63.1': | |
| 695 | + resolution: {integrity: sha512-Li1KdUnWGE4N3e1F/B4RTB1ms+nG4WBgjByO46pkeBVX/2UBsY53xf5vK9WygVmnH3RwncIST7lkSdLSY6P9lg==} | |
| 696 | + cpu: [arm64] | |
| 697 | + os: [win32] | |
| 698 | + | |
| 699 | + '@rollup/rollup-win32-ia32-msvc@4.63.1': | |
| 700 | + resolution: {integrity: sha512-t4ZYOSoLTgwhuFMrmTMLx/+i1DQVK7HYqMc6kY46EApwi8X0nIVphzdNoThU3xt6n+N5urG1/gxBdCaKDLavfg==} | |
| 701 | + cpu: [ia32] | |
| 702 | + os: [win32] | |
| 703 | + | |
| 704 | + '@rollup/rollup-win32-x64-gnu@4.63.1': | |
| 705 | + resolution: {integrity: sha512-RgroPfMmKlD1RzSDxvwgcPiy2HNQKoYV7OmwIXDsk73uKW5t6B/V8KIy27SMv/FNXFo/oSBtWc9J0X7t91ezZg==} | |
| 706 | + cpu: [x64] | |
| 707 | + os: [win32] | |
| 708 | + | |
| 709 | + '@rollup/rollup-win32-x64-msvc@4.63.1': | |
| 710 | + resolution: {integrity: sha512-at8QVep6S3h5Y6gSbdGU06bRY5WJkf6WUduM9YtvYMbYhB1MOFfUgc6kehitQXzOtMSaT70q7f9ydPhpqu821w==} | |
| 711 | + cpu: [x64] | |
| 712 | + os: [win32] | |
| 713 | + | |
| 714 | + '@types/chai@5.2.3': | |
| 715 | + resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==} | |
| 716 | + | |
| 717 | + '@types/deep-eql@4.0.2': | |
| 718 | + resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==} | |
| 719 | + | |
| 720 | + '@types/estree@1.0.9': | |
| 721 | + resolution: {integrity: sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==} | |
| 722 | + | |
| 723 | + '@types/node@24.13.4': | |
| 724 | + resolution: {integrity: sha512-YJ7EqCstVTzIr0fMr7qul/977en+pQHrfmuKIo6Zr9i75Be21dr3MovcfvGtyvi2HAUrRerWps5sMO9I7WaxDw==} | |
| 725 | + | |
| 726 | + '@vitest/expect@3.2.7': | |
| 727 | + resolution: {integrity: sha512-E8eBXaKibuvH2pSZErOjdVb5vF4PbKYcrnluBTYxEk1l/VhhwZg1kZQsdtjq+CsF5CFydf2Rdkz7jDHKSisi3w==} | |
| 728 | + | |
| 729 | + '@vitest/mocker@3.2.7': | |
| 730 | + resolution: {integrity: sha512-Trr0hYO9CM3Wj6ksWHRhK9IZpIY6wTMO5u/MqXurMxT57sWBaOPEtP3Oq60ihZuh5JsiagKfz95OcxdEP6dBrA==} | |
| 731 | + peerDependencies: | |
| 732 | + msw: ^2.4.9 | |
| 733 | + vite: ^5.0.0 || ^6.0.0 || ^7.0.0-0 | |
| 734 | + peerDependenciesMeta: | |
| 735 | + msw: | |
| 736 | + optional: true | |
| 737 | + vite: | |
| 738 | + optional: true | |
| 739 | + | |
| 740 | + '@vitest/pretty-format@3.2.7': | |
| 741 | + resolution: {integrity: sha512-KUHlwqVu0sRlhCdyPdQ/wBoTfRahjUky1MubOmYw9fWfIZy1gNoHpuaaQBPAaMaVYdQYHJLurzj8ECCj5OwTqA==} | |
| 742 | + | |
| 743 | + '@vitest/runner@3.2.7': | |
| 744 | + resolution: {integrity: sha512-sB9y4ovltoQP+WaUPwmSxO9WIg9Ig694Di5PalVPsYHklAdE027mehpWF2SQSVq+k6sFgaivbTjTJwZLSHbedA==} | |
| 745 | + | |
| 746 | + '@vitest/snapshot@3.2.7': | |
| 747 | + resolution: {integrity: sha512-7C+MwShwtBSI5Buwoyg3s/iY1eHL9PKAf+O1wVh/TdnjXUtkoL/9YQtre90i4MtNXM6edP1wJ2zOBpfCyhIS7g==} | |
| 748 | + | |
| 749 | + '@vitest/spy@3.2.7': | |
| 750 | + resolution: {integrity: sha512-Q2eQGI6d2L/hBtZ0qNuKcAGid68XK6cv1xsoaIma6PaJhHPoqcEJhYpXZ/5myCMqkNgtP6UKuBhbc0nHKnrkuQ==} | |
| 751 | + | |
| 752 | + '@vitest/utils@3.2.7': | |
| 753 | + resolution: {integrity: sha512-x6BDOd7dyo3PFLY3I9/HJ25X/6OurhGXk2/B9gOZNPF7XDVjeBK4k01lQE5uvDpbuheErh91qYuE1E2OEjK3Rw==} | |
| 754 | + | |
| 755 | + anynum@1.0.1: | |
| 756 | + resolution: {integrity: sha512-N6//FLET/tXYNM/F6ABca1oH6fWB+KlTt909Le28WMDBk8oaT4vY17DCrwg2MvmuqUKt3Ni4N5dGJ/EoBgcO6A==} | |
| 757 | + | |
| 758 | + assertion-error@2.0.1: | |
| 759 | + resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} | |
| 760 | + engines: {node: '>=12'} | |
| 761 | + | |
| 762 | + boolbase@1.0.0: | |
| 763 | + resolution: {integrity: sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==} | |
| 764 | + | |
| 765 | + buffer-from@1.1.2: | |
| 766 | + resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} | |
| 767 | + | |
| 768 | + cac@6.7.14: | |
| 769 | + resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==} | |
| 770 | + engines: {node: '>=8'} | |
| 771 | + | |
| 772 | + chai@5.3.3: | |
| 773 | + resolution: {integrity: sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==} | |
| 774 | + engines: {node: '>=18'} | |
| 775 | + | |
| 776 | + check-error@2.1.3: | |
| 777 | + resolution: {integrity: sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==} | |
| 778 | + engines: {node: '>= 16'} | |
| 779 | + | |
| 780 | + cheerio-select@2.1.0: | |
| 781 | + resolution: {integrity: sha512-9v9kG0LvzrlcungtnJtpGNxY+fzECQKhK4EGJX2vByejiMX84MFNQw4UxPJl3bFbTMw+Dfs37XaIkCwTZfLh4g==} | |
| 782 | + | |
| 783 | + cheerio@1.2.0: | |
| 784 | + resolution: {integrity: sha512-WDrybc/gKFpTYQutKIK6UvfcuxijIZfMfXaYm8NMsPQxSYvf+13fXUJ4rztGGbJcBQ/GF55gvrZ0Bc0bj/mqvg==} | |
| 785 | + engines: {node: '>=20.18.1'} | |
| 786 | + | |
| 787 | + css-select@5.2.2: | |
| 788 | + resolution: {integrity: sha512-TizTzUddG/xYLA3NXodFM0fSbNizXjOKhqiQQwvhlspadZokn1KDy0NZFS0wuEubIYAV5/c1/lAr0TaaFXEXzw==} | |
| 789 | + | |
| 790 | + css-what@6.2.2: | |
| 791 | + resolution: {integrity: sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA==} | |
| 792 | + engines: {node: '>= 6'} | |
| 793 | + | |
| 794 | + debug@4.4.3: | |
| 795 | + resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} | |
| 796 | + engines: {node: '>=6.0'} | |
| 797 | + peerDependencies: | |
| 798 | + supports-color: '*' | |
| 799 | + peerDependenciesMeta: | |
| 800 | + supports-color: | |
| 801 | + optional: true | |
| 802 | + | |
| 803 | + deep-eql@5.0.2: | |
| 804 | + resolution: {integrity: sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==} | |
| 805 | + engines: {node: '>=6'} | |
| 806 | + | |
| 807 | + dom-serializer@2.0.0: | |
| 808 | + resolution: {integrity: sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==} | |
| 809 | + | |
| 810 | + domelementtype@2.3.0: | |
| 811 | + resolution: {integrity: sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==} | |
| 812 | + | |
| 813 | + domhandler@5.0.3: | |
| 814 | + resolution: {integrity: sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==} | |
| 815 | + engines: {node: '>= 4'} | |
| 816 | + | |
| 817 | + domutils@3.2.2: | |
| 818 | + resolution: {integrity: sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==} | |
| 819 | + | |
| 820 | + drizzle-kit@0.31.10: | |
| 821 | + resolution: {integrity: sha512-7OZcmQUrdGI+DUNNsKBn1aW8qSoKuTH7d0mYgSP8bAzdFzKoovxEFnoGQp2dVs82EOJeYycqRtciopszwUf8bw==} | |
| 822 | + hasBin: true | |
| 823 | + | |
| 824 | + drizzle-orm@0.44.7: | |
| 825 | + resolution: {integrity: sha512-quIpnYznjU9lHshEOAYLoZ9s3jweleHlZIAWR/jX9gAWNg/JhQ1wj0KGRf7/Zm+obRrYd9GjPVJg790QY9N5AQ==} | |
| 826 | + peerDependencies: | |
| 827 | + '@aws-sdk/client-rds-data': '>=3' | |
| 828 | + '@cloudflare/workers-types': '>=4' | |
| 829 | + '@electric-sql/pglite': '>=0.2.0' | |
| 830 | + '@libsql/client': '>=0.10.0' | |
| 831 | + '@libsql/client-wasm': '>=0.10.0' | |
| 832 | + '@neondatabase/serverless': '>=0.10.0' | |
| 833 | + '@op-engineering/op-sqlite': '>=2' | |
| 834 | + '@opentelemetry/api': ^1.4.1 | |
| 835 | + '@planetscale/database': '>=1.13' | |
| 836 | + '@prisma/client': '*' | |
| 837 | + '@tidbcloud/serverless': '*' | |
| 838 | + '@types/better-sqlite3': '*' | |
| 839 | + '@types/pg': '*' | |
| 840 | + '@types/sql.js': '*' | |
| 841 | + '@upstash/redis': '>=1.34.7' | |
| 842 | + '@vercel/postgres': '>=0.8.0' | |
| 843 | + '@xata.io/client': '*' | |
| 844 | + better-sqlite3: '>=7' | |
| 845 | + bun-types: '*' | |
| 846 | + expo-sqlite: '>=14.0.0' | |
| 847 | + gel: '>=2' | |
| 848 | + knex: '*' | |
| 849 | + kysely: '*' | |
| 850 | + mysql2: '>=2' | |
| 851 | + pg: '>=8' | |
| 852 | + postgres: '>=3' | |
| 853 | + prisma: '*' | |
| 854 | + sql.js: '>=1' | |
| 855 | + sqlite3: '>=5' | |
| 856 | + peerDependenciesMeta: | |
| 857 | + '@aws-sdk/client-rds-data': | |
| 858 | + optional: true | |
| 859 | + '@cloudflare/workers-types': | |
| 860 | + optional: true | |
| 861 | + '@electric-sql/pglite': | |
| 862 | + optional: true | |
| 863 | + '@libsql/client': | |
| 864 | + optional: true | |
| 865 | + '@libsql/client-wasm': | |
| 866 | + optional: true | |
| 867 | + '@neondatabase/serverless': | |
| 868 | + optional: true | |
| 869 | + '@op-engineering/op-sqlite': | |
| 870 | + optional: true | |
| 871 | + '@opentelemetry/api': | |
| 872 | + optional: true | |
| 873 | + '@planetscale/database': | |
| 874 | + optional: true | |
| 875 | + '@prisma/client': | |
| 876 | + optional: true | |
| 877 | + '@tidbcloud/serverless': | |
| 878 | + optional: true | |
| 879 | + '@types/better-sqlite3': | |
| 880 | + optional: true | |
| 881 | + '@types/pg': | |
| 882 | + optional: true | |
| 883 | + '@types/sql.js': | |
| 884 | + optional: true | |
| 885 | + '@upstash/redis': | |
| 886 | + optional: true | |
| 887 | + '@vercel/postgres': | |
| 888 | + optional: true | |
| 889 | + '@xata.io/client': | |
| 890 | + optional: true | |
| 891 | + better-sqlite3: | |
| 892 | + optional: true | |
| 893 | + bun-types: | |
| 894 | + optional: true | |
| 895 | + expo-sqlite: | |
| 896 | + optional: true | |
| 897 | + gel: | |
| 898 | + optional: true | |
| 899 | + knex: | |
| 900 | + optional: true | |
| 901 | + kysely: | |
| 902 | + optional: true | |
| 903 | + mysql2: | |
| 904 | + optional: true | |
| 905 | + pg: | |
| 906 | + optional: true | |
| 907 | + postgres: | |
| 908 | + optional: true | |
| 909 | + prisma: | |
| 910 | + optional: true | |
| 911 | + sql.js: | |
| 912 | + optional: true | |
| 913 | + sqlite3: | |
| 914 | + optional: true | |
| 915 | + | |
| 916 | + encoding-sniffer@0.2.1: | |
| 917 | + resolution: {integrity: sha512-5gvq20T6vfpekVtqrYQsSCFZ1wEg5+wW0/QaZMWkFr6BqD3NfKs0rLCx4rrVlSWJeZb5NBJgVLswK/w2MWU+Gw==} | |
| 918 | + | |
| 919 | + entities@4.5.0: | |
| 920 | + resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==} | |
| 921 | + engines: {node: '>=0.12'} | |
| 922 | + | |
| 923 | + entities@6.0.1: | |
| 924 | + resolution: {integrity: sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==} | |
| 925 | + engines: {node: '>=0.12'} | |
| 926 | + | |
| 927 | + entities@7.0.1: | |
| 928 | + resolution: {integrity: sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==} | |
| 929 | + engines: {node: '>=0.12'} | |
| 930 | + | |
| 931 | + es-module-lexer@1.7.0: | |
| 932 | + resolution: {integrity: sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==} | |
| 933 | + | |
| 934 | + esbuild@0.18.20: | |
| 935 | + resolution: {integrity: sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA==} | |
| 936 | + engines: {node: '>=12'} | |
| 937 | + hasBin: true | |
| 938 | + | |
| 939 | + esbuild@0.25.12: | |
| 940 | + resolution: {integrity: sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==} | |
| 941 | + engines: {node: '>=18'} | |
| 942 | + hasBin: true | |
| 943 | + | |
| 944 | + esbuild@0.28.2: | |
| 945 | + resolution: {integrity: sha512-HKVLS8dvII+xoKW9kmqxbRKrnWEXfJJr/FZhhJmiqIB0e053QNYFqOBouTMO/k5sID4MvCiUCvv8b9M4h32wIA==} | |
| 946 | + engines: {node: '>=18'} | |
| 947 | + hasBin: true | |
| 948 | + | |
| 949 | + estree-walker@3.0.3: | |
| 950 | + resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} | |
| 951 | + | |
| 952 | + expect-type@1.4.0: | |
| 953 | + resolution: {integrity: sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==} | |
| 954 | + engines: {node: '>=12.0.0'} | |
| 955 | + | |
| 956 | + fast-xml-builder@1.3.1: | |
| 957 | + resolution: {integrity: sha512-pIM/1n3ntFXKYrUZwW7QCK0gAW7XY+wzj1YMIV3tLDvPj/V+zTGJK5e3/4WJfwj0qWw2ElNXiTixda/R+3YSug==} | |
| 958 | + | |
| 959 | + fast-xml-parser@5.11.1: | |
| 960 | + resolution: {integrity: sha512-TBw6K/fxoQGGjCmZDw9w/ZwP3uDcnTM4YH/g+PFRWr8sbe5idXtxNN6vITh4+1ruCZaho6uBFurElsA7F0zzgw==} | |
| 961 | + hasBin: true | |
| 962 | + | |
| 963 | + fdir@6.5.0: | |
| 964 | + resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} | |
| 965 | + engines: {node: '>=12.0.0'} | |
| 966 | + peerDependencies: | |
| 967 | + picomatch: ^3 || ^4 | |
| 968 | + peerDependenciesMeta: | |
| 969 | + picomatch: | |
| 970 | + optional: true | |
| 971 | + | |
| 972 | + fsevents@2.3.3: | |
| 973 | + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} | |
| 974 | + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} | |
| 975 | + os: [darwin] | |
| 976 | + | |
| 977 | + get-tsconfig@4.14.3: | |
| 978 | + resolution: {integrity: sha512-++QEw4DIY7WGoukz+/+A/8dGYPT9l9yIadnmSgZ8Rjr3YVSVDipQSO9CdnJo9ePqFqUUqh+wk9uIaoiAwsiPkA==} | |
| 979 | + | |
| 980 | + htmlparser2@10.1.0: | |
| 981 | + resolution: {integrity: sha512-VTZkM9GWRAtEpveh7MSF6SjjrpNVNNVJfFup7xTY3UpFtm67foy9HDVXneLtFVt4pMz5kZtgNcvCniNFb1hlEQ==} | |
| 982 | + | |
| 983 | + iconv-lite@0.6.3: | |
| 984 | + resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==} | |
| 985 | + engines: {node: '>=0.10.0'} | |
| 986 | + | |
| 987 | + is-unsafe@2.0.2: | |
| 988 | + resolution: {integrity: sha512-HgbIHPBH0KHHCcjLfGsCvhtPTVxjaAZlXjwdz7/GQC40SjSe4sfQsar8J5VFo8JOSbarkpV0OLG95bbaNd9aAQ==} | |
| 989 | + | |
| 990 | + js-tokens@9.0.1: | |
| 991 | + resolution: {integrity: sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==} | |
| 992 | + | |
| 993 | + loupe@3.2.1: | |
| 994 | + resolution: {integrity: sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==} | |
| 995 | + | |
| 996 | + magic-string@0.30.21: | |
| 997 | + resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} | |
| 998 | + | |
| 999 | + ms@2.1.3: | |
| 1000 | + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} | |
| 1001 | + | |
| 1002 | + nanoid@3.3.18: | |
| 1003 | + resolution: {integrity: sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w==} | |
| 1004 | + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} | |
| 1005 | + hasBin: true | |
| 1006 | + | |
| 1007 | + nth-check@2.1.1: | |
| 1008 | + resolution: {integrity: sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==} | |
| 1009 | + | |
| 1010 | + parse5-htmlparser2-tree-adapter@7.1.0: | |
| 1011 | + resolution: {integrity: sha512-ruw5xyKs6lrpo9x9rCZqZZnIUntICjQAd0Wsmp396Ul9lN/h+ifgVV1x1gZHi8euej6wTfpqX8j+BFQxF0NS/g==} | |
| 1012 | + | |
| 1013 | + parse5-parser-stream@7.1.2: | |
| 1014 | + resolution: {integrity: sha512-JyeQc9iwFLn5TbvvqACIF/VXG6abODeB3Fwmv/TGdLk2LfbWkaySGY72at4+Ty7EkPZj854u4CrICqNk2qIbow==} | |
| 1015 | + | |
| 1016 | + parse5@7.3.0: | |
| 1017 | + resolution: {integrity: sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==} | |
| 1018 | + | |
| 1019 | + path-expression-matcher@1.6.2: | |
| 1020 | + resolution: {integrity: sha512-enSlaiat05iasnzmgNxRj8reFdj3puY2QpNgP1aPIaVfT6nn9ICuPoFlKHk8EN22HcwewshO+mN2DGbkCEOtqQ==} | |
| 1021 | + engines: {node: '>=14.0.0'} | |
| 1022 | + | |
| 1023 | + pathe@2.0.3: | |
| 1024 | + resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} | |
| 1025 | + | |
| 1026 | + pathval@2.0.1: | |
| 1027 | + resolution: {integrity: sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==} | |
| 1028 | + engines: {node: '>= 14.16'} | |
| 1029 | + | |
| 1030 | + picocolors@1.1.1: | |
| 1031 | + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} | |
| 1032 | + | |
| 1033 | + picomatch@4.0.7: | |
| 1034 | + resolution: {integrity: sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==} | |
| 1035 | + engines: {node: '>=12'} | |
| 1036 | + | |
| 1037 | + postcss@8.5.28: | |
| 1038 | + resolution: {integrity: sha512-RRuzqDtt5Y9h3quz5hWhK+TPnsmVs6WwSU6LkJMeY4HstUEDuYTG8UJSdawMRzmzAtV+KEoG8N3Qg2qLy5vM/A==} | |
| 1039 | + engines: {node: ^10 || ^12 || >=14} | |
| 1040 | + | |
| 1041 | + postgres@3.4.9: | |
| 1042 | + resolution: {integrity: sha512-GD3qdB0x1z9xgFI6cdRD6xu2Sp2WCOEoe3mtnyB5Ee0XrrL5Pe+e4CCnJrRMnL1zYtRDZmQQVbvOttLnKDLnaw==} | |
| 1043 | + engines: {node: '>=12'} | |
| 1044 | + | |
| 1045 | + resolve-pkg-maps@1.0.0: | |
| 1046 | + resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==} | |
| 1047 | + | |
| 1048 | + rollup@4.63.1: | |
| 1049 | + resolution: {integrity: sha512-3Df9jsstwhccuEfmAMi9l8XUh/GOkVObmFTU7CCVBysEbcOZLl84jCtaAZMcPiMz2EGKsATzQcU+Xr3n/wU6cg==} | |
| 1050 | + engines: {node: '>=18.0.0', npm: '>=8.0.0'} | |
| 1051 | + hasBin: true | |
| 1052 | + | |
| 1053 | + safer-buffer@2.1.2: | |
| 1054 | + resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} | |
| 1055 | + | |
| 1056 | + siginfo@2.0.0: | |
| 1057 | + resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} | |
| 1058 | + | |
| 1059 | + source-map-js@1.2.1: | |
| 1060 | + resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} | |
| 1061 | + engines: {node: '>=0.10.0'} | |
| 1062 | + | |
| 1063 | + source-map-support@0.5.21: | |
| 1064 | + resolution: {integrity: sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==} | |
| 1065 | + | |
| 1066 | + source-map@0.6.1: | |
| 1067 | + resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} | |
| 1068 | + engines: {node: '>=0.10.0'} | |
| 1069 | + | |
| 1070 | + stackback@0.0.2: | |
| 1071 | + resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} | |
| 1072 | + | |
| 1073 | + std-env@3.10.0: | |
| 1074 | + resolution: {integrity: sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==} | |
| 1075 | + | |
| 1076 | + strip-literal@3.1.0: | |
| 1077 | + resolution: {integrity: sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg==} | |
| 1078 | + | |
| 1079 | + strnum@2.4.2: | |
| 1080 | + resolution: {integrity: sha512-rDG3Ah4TV0k1hWvLSzkZtMmLN9+eS+h3knq4MP6A42Y3Yh5qGNnOUs1jJkoSr8FG5dsL28c7KgkIBzSEykqtuw==} | |
| 1081 | + | |
| 1082 | + tinybench@2.9.0: | |
| 1083 | + resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} | |
| 1084 | + | |
| 1085 | + tinyexec@0.3.2: | |
| 1086 | + resolution: {integrity: sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==} | |
| 1087 | + | |
| 1088 | + tinyglobby@0.2.17: | |
| 1089 | + resolution: {integrity: sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==} | |
| 1090 | + engines: {node: '>=12.0.0'} | |
| 1091 | + | |
| 1092 | + tinypool@1.1.1: | |
| 1093 | + resolution: {integrity: sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==} | |
| 1094 | + engines: {node: ^18.0.0 || >=20.0.0} | |
| 1095 | + | |
| 1096 | + tinyrainbow@2.0.0: | |
| 1097 | + resolution: {integrity: sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==} | |
| 1098 | + engines: {node: '>=14.0.0'} | |
| 1099 | + | |
| 1100 | + tinyspy@4.0.6: | |
| 1101 | + resolution: {integrity: sha512-u8KszXvGfU68hVcZpRHKG28T0krMuv2G5nDhiHaMLen/gIuFEgIJhaJuO69qjnXg5paSrbPMFfx3brNuN8eVSg==} | |
| 1102 | + engines: {node: '>=14.0.0'} | |
| 1103 | + | |
| 1104 | + tsx@4.23.13: | |
| 1105 | + resolution: {integrity: sha512-BL5MGkRln6aDYhb0xbQlEAGw743BaZYWdbWtdJOBriYJboKgUUYCadFp2/FpBBZquBC/ezNBn7wMMPx7FDZUDw==} | |
| 1106 | + engines: {node: '>=18.0.0'} | |
| 1107 | + hasBin: true | |
| 1108 | + | |
| 1109 | + typescript@5.9.3: | |
| 1110 | + resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} | |
| 1111 | + engines: {node: '>=14.17'} | |
| 1112 | + hasBin: true | |
| 1113 | + | |
| 1114 | + undici-types@7.18.2: | |
| 1115 | + resolution: {integrity: sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==} | |
| 1116 | + | |
| 1117 | + undici@7.29.1: | |
| 1118 | + resolution: {integrity: sha512-RYONW2MeafgYlkVOKYKkA/Ag7BmXqgIWCa8t1m0JcxrQg9pI9lEqRhAOruOBCbAohOa/gkCF+iPi9hrgvTzu6Q==} | |
| 1119 | + engines: {node: '>=20.18.1'} | |
| 1120 | + | |
| 1121 | + unpdf@1.8.1: | |
| 1122 | + resolution: {integrity: sha512-xkURhy2SoGpOIH0a1gLHNkASPIQYonadDJs2AQwPEfUakafeD9EA1WTWWsaR++gfTCXJpV27W7tU1nXuk82UKQ==} | |
| 1123 | + engines: {node: '>=22'} | |
| 1124 | + peerDependencies: | |
| 1125 | + '@napi-rs/canvas': ^0.1.69 || ^1.0.0 | |
| 1126 | + peerDependenciesMeta: | |
| 1127 | + '@napi-rs/canvas': | |
| 1128 | + optional: true | |
| 1129 | + | |
| 1130 | + vite-node@3.2.4: | |
| 1131 | + resolution: {integrity: sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==} | |
| 1132 | + engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} | |
| 1133 | + hasBin: true | |
| 1134 | + | |
| 1135 | + vite@7.3.6: | |
| 1136 | + resolution: {integrity: sha512-4XP60spRGjSZFf1qYH+dJIkK2znL3zQfl9KkOV9MkkRR/3Dls0dxaBsQPTloEc5BLXWPL9vsOxopxyKoMmDueg==} | |
| 1137 | + engines: {node: ^20.19.0 || >=22.12.0} | |
| 1138 | + hasBin: true | |
| 1139 | + peerDependencies: | |
| 1140 | + '@types/node': ^20.19.0 || >=22.12.0 | |
| 1141 | + jiti: '>=1.21.0' | |
| 1142 | + less: ^4.0.0 | |
| 1143 | + lightningcss: ^1.21.0 | |
| 1144 | + sass: ^1.70.0 | |
| 1145 | + sass-embedded: ^1.70.0 | |
| 1146 | + stylus: '>=0.54.8' | |
| 1147 | + sugarss: ^5.0.0 | |
| 1148 | + terser: ^5.16.0 | |
| 1149 | + tsx: ^4.8.1 | |
| 1150 | + yaml: ^2.4.2 | |
| 1151 | + peerDependenciesMeta: | |
| 1152 | + '@types/node': | |
| 1153 | + optional: true | |
| 1154 | + jiti: | |
| 1155 | + optional: true | |
| 1156 | + less: | |
| 1157 | + optional: true | |
| 1158 | + lightningcss: | |
| 1159 | + optional: true | |
| 1160 | + sass: | |
| 1161 | + optional: true | |
| 1162 | + sass-embedded: | |
| 1163 | + optional: true | |
| 1164 | + stylus: | |
| 1165 | + optional: true | |
| 1166 | + sugarss: | |
| 1167 | + optional: true | |
| 1168 | + terser: | |
| 1169 | + optional: true | |
| 1170 | + tsx: | |
| 1171 | + optional: true | |
| 1172 | + yaml: | |
| 1173 | + optional: true | |
| 1174 | + | |
| 1175 | + vitest@3.2.7: | |
| 1176 | + resolution: {integrity: sha512-KrxIJ62Fd89gfysR4WotlgZABiz2dqFPgqGzX7s+CwsqLFomRH7777ZcrOD6+WVAh7khPQP41A+BKbpcJFrdEg==} | |
| 1177 | + engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} | |
| 1178 | + hasBin: true | |
| 1179 | + peerDependencies: | |
| 1180 | + '@edge-runtime/vm': '*' | |
| 1181 | + '@types/debug': ^4.1.12 | |
| 1182 | + '@types/node': ^18.0.0 || ^20.0.0 || >=22.0.0 | |
| 1183 | + '@vitest/browser': 3.2.7 | |
| 1184 | + '@vitest/ui': 3.2.7 | |
| 1185 | + happy-dom: '*' | |
| 1186 | + jsdom: '*' | |
| 1187 | + peerDependenciesMeta: | |
| 1188 | + '@edge-runtime/vm': | |
| 1189 | + optional: true | |
| 1190 | + '@types/debug': | |
| 1191 | + optional: true | |
| 1192 | + '@types/node': | |
| 1193 | + optional: true | |
| 1194 | + '@vitest/browser': | |
| 1195 | + optional: true | |
| 1196 | + '@vitest/ui': | |
| 1197 | + optional: true | |
| 1198 | + happy-dom: | |
| 1199 | + optional: true | |
| 1200 | + jsdom: | |
| 1201 | + optional: true | |
| 1202 | + | |
| 1203 | + whatwg-encoding@3.1.1: | |
| 1204 | + resolution: {integrity: sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==} | |
| 1205 | + engines: {node: '>=18'} | |
| 1206 | + deprecated: Use @exodus/bytes instead for a more spec-conformant and faster implementation | |
| 1207 | + | |
| 1208 | + whatwg-mimetype@4.0.0: | |
| 1209 | + resolution: {integrity: sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==} | |
| 1210 | + engines: {node: '>=18'} | |
| 1211 | + | |
| 1212 | + why-is-node-running@2.3.0: | |
| 1213 | + resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==} | |
| 1214 | + engines: {node: '>=8'} | |
| 1215 | + hasBin: true | |
| 1216 | + | |
| 1217 | + xml-naming@0.3.0: | |
| 1218 | + resolution: {integrity: sha512-ghig2TBE/H11aOVgmahA3MhimvkBr6JIYknH/Dhdk10nXwdbIqBJsbfMxpvFPG8bAw77gN29aQWvKpmVoPlvPQ==} | |
| 1219 | + engines: {node: '>=16.0.0'} | |
| 1220 | + | |
| 1221 | + yaml@2.9.0: | |
| 1222 | + resolution: {integrity: sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA==} | |
| 1223 | + engines: {node: '>= 14.6'} | |
| 1224 | + hasBin: true | |
| 1225 | + | |
| 1226 | + zod@4.6.1: | |
| 1227 | + resolution: {integrity: sha512-341aRWQsve0rvronKNTqZpjmzdbUDlFuzHaI/XLg/Ej82qffDJRRfBTCuv7+9q/rMjB6LSLyEBnW4InJeMtt/Q==} | |
| 1228 | + | |
| 1229 | +snapshots: | |
| 1230 | + | |
| 1231 | + '@clickhouse/client@1.23.1': {} | |
| 1232 | + | |
| 1233 | + '@drizzle-team/brocli@0.10.2': {} | |
| 1234 | + | |
| 1235 | + '@esbuild-kit/core-utils@3.3.2': | |
| 1236 | + dependencies: | |
| 1237 | + esbuild: 0.18.20 | |
| 1238 | + source-map-support: 0.5.21 | |
| 1239 | + | |
| 1240 | + '@esbuild-kit/esm-loader@2.6.5': | |
| 1241 | + dependencies: | |
| 1242 | + '@esbuild-kit/core-utils': 3.3.2 | |
| 1243 | + get-tsconfig: 4.14.3 | |
| 1244 | + | |
| 1245 | + '@esbuild/aix-ppc64@0.25.12': | |
| 1246 | + optional: true | |
| 1247 | + | |
| 1248 | + '@esbuild/aix-ppc64@0.28.2': | |
| 1249 | + optional: true | |
| 1250 | + | |
| 1251 | + '@esbuild/android-arm64@0.18.20': | |
| 1252 | + optional: true | |
| 1253 | + | |
| 1254 | + '@esbuild/android-arm64@0.25.12': | |
| 1255 | + optional: true | |
| 1256 | + | |
| 1257 | + '@esbuild/android-arm64@0.28.2': | |
| 1258 | + optional: true | |
| 1259 | + | |
| 1260 | + '@esbuild/android-arm@0.18.20': | |
| 1261 | + optional: true | |
| 1262 | + | |
| 1263 | + '@esbuild/android-arm@0.25.12': | |
| 1264 | + optional: true | |
| 1265 | + | |
| 1266 | + '@esbuild/android-arm@0.28.2': | |
| 1267 | + optional: true | |
| 1268 | + | |
| 1269 | + '@esbuild/android-x64@0.18.20': | |
| 1270 | + optional: true | |
| 1271 | + | |
| 1272 | + '@esbuild/android-x64@0.25.12': | |
| 1273 | + optional: true | |
| 1274 | + | |
| 1275 | + '@esbuild/android-x64@0.28.2': | |
| 1276 | + optional: true | |
| 1277 | + | |
| 1278 | + '@esbuild/darwin-arm64@0.18.20': | |
| 1279 | + optional: true | |
| 1280 | + | |
| 1281 | + '@esbuild/darwin-arm64@0.25.12': | |
| 1282 | + optional: true | |
| 1283 | + | |
| 1284 | + '@esbuild/darwin-arm64@0.28.2': | |
| 1285 | + optional: true | |
| 1286 | + | |
| 1287 | + '@esbuild/darwin-x64@0.18.20': | |
| 1288 | + optional: true | |
| 1289 | + | |
| 1290 | + '@esbuild/darwin-x64@0.25.12': | |
| 1291 | + optional: true | |
| 1292 | + | |
| 1293 | + '@esbuild/darwin-x64@0.28.2': | |
| 1294 | + optional: true | |
| 1295 | + | |
| 1296 | + '@esbuild/freebsd-arm64@0.18.20': | |
| 1297 | + optional: true | |
| 1298 | + | |
| 1299 | + '@esbuild/freebsd-arm64@0.25.12': | |
| 1300 | + optional: true | |
| 1301 | + | |
| 1302 | + '@esbuild/freebsd-arm64@0.28.2': | |
| 1303 | + optional: true | |
| 1304 | + | |
| 1305 | + '@esbuild/freebsd-x64@0.18.20': | |
| 1306 | + optional: true | |
| 1307 | + | |
| 1308 | + '@esbuild/freebsd-x64@0.25.12': | |
| 1309 | + optional: true | |
| 1310 | + | |
| 1311 | + '@esbuild/freebsd-x64@0.28.2': | |
| 1312 | + optional: true | |
| 1313 | + | |
| 1314 | + '@esbuild/linux-arm64@0.18.20': | |
| 1315 | + optional: true | |
| 1316 | + | |
| 1317 | + '@esbuild/linux-arm64@0.25.12': | |
| 1318 | + optional: true | |
| 1319 | + | |
| 1320 | + '@esbuild/linux-arm64@0.28.2': | |
| 1321 | + optional: true | |
| 1322 | + | |
| 1323 | + '@esbuild/linux-arm@0.18.20': | |
| 1324 | + optional: true | |
| 1325 | + | |
| 1326 | + '@esbuild/linux-arm@0.25.12': | |
| 1327 | + optional: true | |
| 1328 | + | |
| 1329 | + '@esbuild/linux-arm@0.28.2': | |
| 1330 | + optional: true | |
| 1331 | + | |
| 1332 | + '@esbuild/linux-ia32@0.18.20': | |
| 1333 | + optional: true | |
| 1334 | + | |
| 1335 | + '@esbuild/linux-ia32@0.25.12': | |
| 1336 | + optional: true | |
| 1337 | + | |
| 1338 | + '@esbuild/linux-ia32@0.28.2': | |
| 1339 | + optional: true | |
| 1340 | + | |
| 1341 | + '@esbuild/linux-loong64@0.18.20': | |
| 1342 | + optional: true | |
| 1343 | + | |
| 1344 | + '@esbuild/linux-loong64@0.25.12': | |
| 1345 | + optional: true | |
| 1346 | + | |
| 1347 | + '@esbuild/linux-loong64@0.28.2': | |
| 1348 | + optional: true | |
| 1349 | + | |
| 1350 | + '@esbuild/linux-mips64el@0.18.20': | |
| 1351 | + optional: true | |
| 1352 | + | |
| 1353 | + '@esbuild/linux-mips64el@0.25.12': | |
| 1354 | + optional: true | |
| 1355 | + | |
| 1356 | + '@esbuild/linux-mips64el@0.28.2': | |
| 1357 | + optional: true | |
| 1358 | + | |
| 1359 | + '@esbuild/linux-ppc64@0.18.20': | |
| 1360 | + optional: true | |
| 1361 | + | |
| 1362 | + '@esbuild/linux-ppc64@0.25.12': | |
| 1363 | + optional: true | |
| 1364 | + | |
| 1365 | + '@esbuild/linux-ppc64@0.28.2': | |
| 1366 | + optional: true | |
| 1367 | + | |
| 1368 | + '@esbuild/linux-riscv64@0.18.20': | |
| 1369 | + optional: true | |
| 1370 | + | |
| 1371 | + '@esbuild/linux-riscv64@0.25.12': | |
| 1372 | + optional: true | |
| 1373 | + | |
| 1374 | + '@esbuild/linux-riscv64@0.28.2': | |
| 1375 | + optional: true | |
| 1376 | + | |
| 1377 | + '@esbuild/linux-s390x@0.18.20': | |
| 1378 | + optional: true | |
| 1379 | + | |
| 1380 | + '@esbuild/linux-s390x@0.25.12': | |
| 1381 | + optional: true | |
| 1382 | + | |
| 1383 | + '@esbuild/linux-s390x@0.28.2': | |
| 1384 | + optional: true | |
| 1385 | + | |
| 1386 | + '@esbuild/linux-x64@0.18.20': | |
| 1387 | + optional: true | |
| 1388 | + | |
| 1389 | + '@esbuild/linux-x64@0.25.12': | |
| 1390 | + optional: true | |
| 1391 | + | |
| 1392 | + '@esbuild/linux-x64@0.28.2': | |
| 1393 | + optional: true | |
| 1394 | + | |
| 1395 | + '@esbuild/netbsd-arm64@0.25.12': | |
| 1396 | + optional: true | |
| 1397 | + | |
| 1398 | + '@esbuild/netbsd-arm64@0.28.2': | |
| 1399 | + optional: true | |
| 1400 | + | |
| 1401 | + '@esbuild/netbsd-x64@0.18.20': | |
| 1402 | + optional: true | |
| 1403 | + | |
| 1404 | + '@esbuild/netbsd-x64@0.25.12': | |
| 1405 | + optional: true | |
| 1406 | + | |
| 1407 | + '@esbuild/netbsd-x64@0.28.2': | |
| 1408 | + optional: true | |
| 1409 | + | |
| 1410 | + '@esbuild/openbsd-arm64@0.25.12': | |
| 1411 | + optional: true | |
| 1412 | + | |
| 1413 | + '@esbuild/openbsd-arm64@0.28.2': | |
| 1414 | + optional: true | |
| 1415 | + | |
| 1416 | + '@esbuild/openbsd-x64@0.18.20': | |
| 1417 | + optional: true | |
| 1418 | + | |
| 1419 | + '@esbuild/openbsd-x64@0.25.12': | |
| 1420 | + optional: true | |
| 1421 | + | |
| 1422 | + '@esbuild/openbsd-x64@0.28.2': | |
| 1423 | + optional: true | |
| 1424 | + | |
| 1425 | + '@esbuild/openharmony-arm64@0.25.12': | |
| 1426 | + optional: true | |
| 1427 | + | |
| 1428 | + '@esbuild/openharmony-arm64@0.28.2': | |
| 1429 | + optional: true | |
| 1430 | + | |
| 1431 | + '@esbuild/sunos-x64@0.18.20': | |
| 1432 | + optional: true | |
| 1433 | + | |
| 1434 | + '@esbuild/sunos-x64@0.25.12': | |
| 1435 | + optional: true | |
| 1436 | + | |
| 1437 | + '@esbuild/sunos-x64@0.28.2': | |
| 1438 | + optional: true | |
| 1439 | + | |
| 1440 | + '@esbuild/win32-arm64@0.18.20': | |
| 1441 | + optional: true | |
| 1442 | + | |
| 1443 | + '@esbuild/win32-arm64@0.25.12': | |
| 1444 | + optional: true | |
| 1445 | + | |
| 1446 | + '@esbuild/win32-arm64@0.28.2': | |
| 1447 | + optional: true | |
| 1448 | + | |
| 1449 | + '@esbuild/win32-ia32@0.18.20': | |
| 1450 | + optional: true | |
| 1451 | + | |
| 1452 | + '@esbuild/win32-ia32@0.25.12': | |
| 1453 | + optional: true | |
| 1454 | + | |
| 1455 | + '@esbuild/win32-ia32@0.28.2': | |
| 1456 | + optional: true | |
| 1457 | + | |
| 1458 | + '@esbuild/win32-x64@0.18.20': | |
| 1459 | + optional: true | |
| 1460 | + | |
| 1461 | + '@esbuild/win32-x64@0.25.12': | |
| 1462 | + optional: true | |
| 1463 | + | |
| 1464 | + '@esbuild/win32-x64@0.28.2': | |
| 1465 | + optional: true | |
| 1466 | + | |
| 1467 | + '@jridgewell/sourcemap-codec@1.6.0': {} | |
| 1468 | + | |
| 1469 | + '@napi-rs/lzma-linux-x64-gnu@1.5.1': | |
| 1470 | + optional: true | |
| 1471 | + | |
| 1472 | + '@nodable/entities@3.0.0': {} | |
| 1473 | + | |
| 1474 | + '@rollup/rollup-android-arm-eabi@4.63.1': | |
| 1475 | + optional: true | |
| 1476 | + | |
| 1477 | + '@rollup/rollup-android-arm64@4.63.1': | |
| 1478 | + optional: true | |
| 1479 | + | |
| 1480 | + '@rollup/rollup-darwin-arm64@4.63.1': | |
| 1481 | + optional: true | |
| 1482 | + | |
| 1483 | + '@rollup/rollup-darwin-x64@4.63.1': | |
| 1484 | + optional: true | |
| 1485 | + | |
| 1486 | + '@rollup/rollup-freebsd-arm64@4.63.1': | |
| 1487 | + optional: true | |
| 1488 | + | |
| 1489 | + '@rollup/rollup-freebsd-x64@4.63.1': | |
| 1490 | + optional: true | |
| 1491 | + | |
| 1492 | + '@rollup/rollup-linux-arm-gnueabihf@4.63.1': | |
| 1493 | + optional: true | |
| 1494 | + | |
| 1495 | + '@rollup/rollup-linux-arm-musleabihf@4.63.1': | |
| 1496 | + optional: true | |
| 1497 | + | |
| 1498 | + '@rollup/rollup-linux-arm64-gnu@4.63.1': | |
| 1499 | + optional: true | |
| 1500 | + | |
| 1501 | + '@rollup/rollup-linux-arm64-musl@4.63.1': | |
| 1502 | + optional: true | |
| 1503 | + | |
| 1504 | + '@rollup/rollup-linux-loong64-gnu@4.63.1': | |
| 1505 | + optional: true | |
| 1506 | + | |
| 1507 | + '@rollup/rollup-linux-loong64-musl@4.63.1': | |
| 1508 | + optional: true | |
| 1509 | + | |
| 1510 | + '@rollup/rollup-linux-ppc64-gnu@4.63.1': | |
| 1511 | + optional: true | |
| 1512 | + | |
| 1513 | + '@rollup/rollup-linux-ppc64-musl@4.63.1': | |
| 1514 | + optional: true | |
| 1515 | + | |
| 1516 | + '@rollup/rollup-linux-riscv64-gnu@4.63.1': | |
| 1517 | + optional: true | |
| 1518 | + | |
| 1519 | + '@rollup/rollup-linux-riscv64-musl@4.63.1': | |
| 1520 | + optional: true | |
| 1521 | + | |
| 1522 | + '@rollup/rollup-linux-s390x-gnu@4.63.1': | |
| 1523 | + optional: true | |
| 1524 | + | |
| 1525 | + '@rollup/rollup-linux-x64-gnu@4.63.1': | |
| 1526 | + optional: true | |
| 1527 | + | |
| 1528 | + '@rollup/rollup-linux-x64-musl@4.63.1': | |
| 1529 | + optional: true | |
| 1530 | + | |
| 1531 | + '@rollup/rollup-openbsd-x64@4.63.1': | |
| 1532 | + optional: true | |
| 1533 | + | |
| 1534 | + '@rollup/rollup-openharmony-arm64@4.63.1': | |
| 1535 | + optional: true | |
| 1536 | + | |
| 1537 | + '@rollup/rollup-win32-arm64-msvc@4.63.1': | |
| 1538 | + optional: true | |
| 1539 | + | |
| 1540 | + '@rollup/rollup-win32-ia32-msvc@4.63.1': | |
| 1541 | + optional: true | |
| 1542 | + | |
| 1543 | + '@rollup/rollup-win32-x64-gnu@4.63.1': | |
| 1544 | + optional: true | |
| 1545 | + | |
| 1546 | + '@rollup/rollup-win32-x64-msvc@4.63.1': | |
| 1547 | + optional: true | |
| 1548 | + | |
| 1549 | + '@types/chai@5.2.3': | |
| 1550 | + dependencies: | |
| 1551 | + '@types/deep-eql': 4.0.2 | |
| 1552 | + assertion-error: 2.0.1 | |
| 1553 | + | |
| 1554 | + '@types/deep-eql@4.0.2': {} | |
| 1555 | + | |
| 1556 | + '@types/estree@1.0.9': {} | |
| 1557 | + | |
| 1558 | + '@types/node@24.13.4': | |
| 1559 | + dependencies: | |
| 1560 | + undici-types: 7.18.2 | |
| 1561 | + | |
| 1562 | + '@vitest/expect@3.2.7': | |
| 1563 | + dependencies: | |
| 1564 | + '@types/chai': 5.2.3 | |
| 1565 | + '@vitest/spy': 3.2.7 | |
| 1566 | + '@vitest/utils': 3.2.7 | |
| 1567 | + chai: 5.3.3 | |
| 1568 | + tinyrainbow: 2.0.0 | |
| 1569 | + | |
| 1570 | + '@vitest/mocker@3.2.7(vite@7.3.6(@types/node@24.13.4)(tsx@4.23.13)(yaml@2.9.0))': | |
| 1571 | + dependencies: | |
| 1572 | + '@vitest/spy': 3.2.7 | |
| 1573 | + estree-walker: 3.0.3 | |
| 1574 | + magic-string: 0.30.21 | |
| 1575 | + optionalDependencies: | |
| 1576 | + vite: 7.3.6(@types/node@24.13.4)(tsx@4.23.13)(yaml@2.9.0) | |
| 1577 | + | |
| 1578 | + '@vitest/pretty-format@3.2.7': | |
| 1579 | + dependencies: | |
| 1580 | + tinyrainbow: 2.0.0 | |
| 1581 | + | |
| 1582 | + '@vitest/runner@3.2.7': | |
| 1583 | + dependencies: | |
| 1584 | + '@vitest/utils': 3.2.7 | |
| 1585 | + pathe: 2.0.3 | |
| 1586 | + strip-literal: 3.1.0 | |
| 1587 | + | |
| 1588 | + '@vitest/snapshot@3.2.7': | |
| 1589 | + dependencies: | |
| 1590 | + '@vitest/pretty-format': 3.2.7 | |
| 1591 | + magic-string: 0.30.21 | |
| 1592 | + pathe: 2.0.3 | |
| 1593 | + | |
| 1594 | + '@vitest/spy@3.2.7': | |
| 1595 | + dependencies: | |
| 1596 | + tinyspy: 4.0.6 | |
| 1597 | + | |
| 1598 | + '@vitest/utils@3.2.7': | |
| 1599 | + dependencies: | |
| 1600 | + '@vitest/pretty-format': 3.2.7 | |
| 1601 | + loupe: 3.2.1 | |
| 1602 | + tinyrainbow: 2.0.0 | |
| 1603 | + | |
| 1604 | + anynum@1.0.1: {} | |
| 1605 | + | |
| 1606 | + assertion-error@2.0.1: {} | |
| 1607 | + | |
| 1608 | + boolbase@1.0.0: {} | |
| 1609 | + | |
| 1610 | + buffer-from@1.1.2: {} | |
| 1611 | + | |
| 1612 | + cac@6.7.14: {} | |
| 1613 | + | |
| 1614 | + chai@5.3.3: | |
| 1615 | + dependencies: | |
| 1616 | + assertion-error: 2.0.1 | |
| 1617 | + check-error: 2.1.3 | |
| 1618 | + deep-eql: 5.0.2 | |
| 1619 | + loupe: 3.2.1 | |
| 1620 | + pathval: 2.0.1 | |
| 1621 | + | |
| 1622 | + check-error@2.1.3: {} | |
| 1623 | + | |
| 1624 | + cheerio-select@2.1.0: | |
| 1625 | + dependencies: | |
| 1626 | + boolbase: 1.0.0 | |
| 1627 | + css-select: 5.2.2 | |
| 1628 | + css-what: 6.2.2 | |
| 1629 | + domelementtype: 2.3.0 | |
| 1630 | + domhandler: 5.0.3 | |
| 1631 | + domutils: 3.2.2 | |
| 1632 | + | |
| 1633 | + cheerio@1.2.0: | |
| 1634 | + dependencies: | |
| 1635 | + cheerio-select: 2.1.0 | |
| 1636 | + dom-serializer: 2.0.0 | |
| 1637 | + domhandler: 5.0.3 | |
| 1638 | + domutils: 3.2.2 | |
| 1639 | + encoding-sniffer: 0.2.1 | |
| 1640 | + htmlparser2: 10.1.0 | |
| 1641 | + parse5: 7.3.0 | |
| 1642 | + parse5-htmlparser2-tree-adapter: 7.1.0 | |
| 1643 | + parse5-parser-stream: 7.1.2 | |
| 1644 | + undici: 7.29.1 | |
| 1645 | + whatwg-mimetype: 4.0.0 | |
| 1646 | + | |
| 1647 | + css-select@5.2.2: | |
| 1648 | + dependencies: | |
| 1649 | + boolbase: 1.0.0 | |
| 1650 | + css-what: 6.2.2 | |
| 1651 | + domhandler: 5.0.3 | |
| 1652 | + domutils: 3.2.2 | |
| 1653 | + nth-check: 2.1.1 | |
| 1654 | + | |
| 1655 | + css-what@6.2.2: {} | |
| 1656 | + | |
| 1657 | + debug@4.4.3: | |
| 1658 | + dependencies: | |
| 1659 | + ms: 2.1.3 | |
| 1660 | + | |
| 1661 | + deep-eql@5.0.2: {} | |
| 1662 | + | |
| 1663 | + dom-serializer@2.0.0: | |
| 1664 | + dependencies: | |
| 1665 | + domelementtype: 2.3.0 | |
| 1666 | + domhandler: 5.0.3 | |
| 1667 | + entities: 4.5.0 | |
| 1668 | + | |
| 1669 | + domelementtype@2.3.0: {} | |
| 1670 | + | |
| 1671 | + domhandler@5.0.3: | |
| 1672 | + dependencies: | |
| 1673 | + domelementtype: 2.3.0 | |
| 1674 | + | |
| 1675 | + domutils@3.2.2: | |
| 1676 | + dependencies: | |
| 1677 | + dom-serializer: 2.0.0 | |
| 1678 | + domelementtype: 2.3.0 | |
| 1679 | + domhandler: 5.0.3 | |
| 1680 | + | |
| 1681 | + drizzle-kit@0.31.10: | |
| 1682 | + dependencies: | |
| 1683 | + '@drizzle-team/brocli': 0.10.2 | |
| 1684 | + '@esbuild-kit/esm-loader': 2.6.5 | |
| 1685 | + esbuild: 0.25.12 | |
| 1686 | + tsx: 4.23.13 | |
| 1687 | + | |
| 1688 | + drizzle-orm@0.44.7(postgres@3.4.9): | |
| 1689 | + optionalDependencies: | |
| 1690 | + postgres: 3.4.9 | |
| 1691 | + | |
| 1692 | + encoding-sniffer@0.2.1: | |
| 1693 | + dependencies: | |
| 1694 | + iconv-lite: 0.6.3 | |
| 1695 | + whatwg-encoding: 3.1.1 | |
| 1696 | + | |
| 1697 | + entities@4.5.0: {} | |
| 1698 | + | |
| 1699 | + entities@6.0.1: {} | |
| 1700 | + | |
| 1701 | + entities@7.0.1: {} | |
| 1702 | + | |
| 1703 | + es-module-lexer@1.7.0: {} | |
| 1704 | + | |
| 1705 | + esbuild@0.18.20: | |
| 1706 | + optionalDependencies: | |
| 1707 | + '@esbuild/android-arm': 0.18.20 | |
| 1708 | + '@esbuild/android-arm64': 0.18.20 | |
| 1709 | + '@esbuild/android-x64': 0.18.20 | |
| 1710 | + '@esbuild/darwin-arm64': 0.18.20 | |
| 1711 | + '@esbuild/darwin-x64': 0.18.20 | |
| 1712 | + '@esbuild/freebsd-arm64': 0.18.20 | |
| 1713 | + '@esbuild/freebsd-x64': 0.18.20 | |
| 1714 | + '@esbuild/linux-arm': 0.18.20 | |
| 1715 | + '@esbuild/linux-arm64': 0.18.20 | |
| 1716 | + '@esbuild/linux-ia32': 0.18.20 | |
| 1717 | + '@esbuild/linux-loong64': 0.18.20 | |
| 1718 | + '@esbuild/linux-mips64el': 0.18.20 | |
| 1719 | + '@esbuild/linux-ppc64': 0.18.20 | |
| 1720 | + '@esbuild/linux-riscv64': 0.18.20 | |
| 1721 | + '@esbuild/linux-s390x': 0.18.20 | |
| 1722 | + '@esbuild/linux-x64': 0.18.20 | |
| 1723 | + '@esbuild/netbsd-x64': 0.18.20 | |
| 1724 | + '@esbuild/openbsd-x64': 0.18.20 | |
| 1725 | + '@esbuild/sunos-x64': 0.18.20 | |
| 1726 | + '@esbuild/win32-arm64': 0.18.20 | |
| 1727 | + '@esbuild/win32-ia32': 0.18.20 | |
| 1728 | + '@esbuild/win32-x64': 0.18.20 | |
| 1729 | + | |
| 1730 | + esbuild@0.25.12: | |
| 1731 | + optionalDependencies: | |
| 1732 | + '@esbuild/aix-ppc64': 0.25.12 | |
| 1733 | + '@esbuild/android-arm': 0.25.12 | |
| 1734 | + '@esbuild/android-arm64': 0.25.12 | |
| 1735 | + '@esbuild/android-x64': 0.25.12 | |
| 1736 | + '@esbuild/darwin-arm64': 0.25.12 | |
| 1737 | + '@esbuild/darwin-x64': 0.25.12 | |
| 1738 | + '@esbuild/freebsd-arm64': 0.25.12 | |
| 1739 | + '@esbuild/freebsd-x64': 0.25.12 | |
| 1740 | + '@esbuild/linux-arm': 0.25.12 | |
| 1741 | + '@esbuild/linux-arm64': 0.25.12 | |
| 1742 | + '@esbuild/linux-ia32': 0.25.12 | |
| 1743 | + '@esbuild/linux-loong64': 0.25.12 | |
| 1744 | + '@esbuild/linux-mips64el': 0.25.12 | |
| 1745 | + '@esbuild/linux-ppc64': 0.25.12 | |
| 1746 | + '@esbuild/linux-riscv64': 0.25.12 | |
| 1747 | + '@esbuild/linux-s390x': 0.25.12 | |
| 1748 | + '@esbuild/linux-x64': 0.25.12 | |
| 1749 | + '@esbuild/netbsd-arm64': 0.25.12 | |
| 1750 | + '@esbuild/netbsd-x64': 0.25.12 | |
| 1751 | + '@esbuild/openbsd-arm64': 0.25.12 | |
| 1752 | + '@esbuild/openbsd-x64': 0.25.12 | |
| 1753 | + '@esbuild/openharmony-arm64': 0.25.12 | |
| 1754 | + '@esbuild/sunos-x64': 0.25.12 | |
| 1755 | + '@esbuild/win32-arm64': 0.25.12 | |
| 1756 | + '@esbuild/win32-ia32': 0.25.12 | |
| 1757 | + '@esbuild/win32-x64': 0.25.12 | |
| 1758 | + | |
| 1759 | + esbuild@0.28.2: | |
| 1760 | + optionalDependencies: | |
| 1761 | + '@esbuild/aix-ppc64': 0.28.2 | |
| 1762 | + '@esbuild/android-arm': 0.28.2 | |
| 1763 | + '@esbuild/android-arm64': 0.28.2 | |
| 1764 | + '@esbuild/android-x64': 0.28.2 | |
| 1765 | + '@esbuild/darwin-arm64': 0.28.2 | |
| 1766 | + '@esbuild/darwin-x64': 0.28.2 | |
| 1767 | + '@esbuild/freebsd-arm64': 0.28.2 | |
| 1768 | + '@esbuild/freebsd-x64': 0.28.2 | |
| 1769 | + '@esbuild/linux-arm': 0.28.2 | |
| 1770 | + '@esbuild/linux-arm64': 0.28.2 | |
| 1771 | + '@esbuild/linux-ia32': 0.28.2 | |
| 1772 | + '@esbuild/linux-loong64': 0.28.2 | |
| 1773 | + '@esbuild/linux-mips64el': 0.28.2 | |
| 1774 | + '@esbuild/linux-ppc64': 0.28.2 | |
| 1775 | + '@esbuild/linux-riscv64': 0.28.2 | |
| 1776 | + '@esbuild/linux-s390x': 0.28.2 | |
| 1777 | + '@esbuild/linux-x64': 0.28.2 | |
| 1778 | + '@esbuild/netbsd-arm64': 0.28.2 | |
| 1779 | + '@esbuild/netbsd-x64': 0.28.2 | |
| 1780 | + '@esbuild/openbsd-arm64': 0.28.2 | |
| 1781 | + '@esbuild/openbsd-x64': 0.28.2 | |
| 1782 | + '@esbuild/openharmony-arm64': 0.28.2 | |
| 1783 | + '@esbuild/sunos-x64': 0.28.2 | |
| 1784 | + '@esbuild/win32-arm64': 0.28.2 | |
| 1785 | + '@esbuild/win32-ia32': 0.28.2 | |
| 1786 | + '@esbuild/win32-x64': 0.28.2 | |
| 1787 | + | |
| 1788 | + estree-walker@3.0.3: | |
| 1789 | + dependencies: | |
| 1790 | + '@types/estree': 1.0.9 | |
| 1791 | + | |
| 1792 | + expect-type@1.4.0: {} | |
| 1793 | + | |
| 1794 | + fast-xml-builder@1.3.1: | |
| 1795 | + dependencies: | |
| 1796 | + path-expression-matcher: 1.6.2 | |
| 1797 | + xml-naming: 0.3.0 | |
| 1798 | + | |
| 1799 | + fast-xml-parser@5.11.1: | |
| 1800 | + dependencies: | |
| 1801 | + '@nodable/entities': 3.0.0 | |
| 1802 | + fast-xml-builder: 1.3.1 | |
| 1803 | + is-unsafe: 2.0.2 | |
| 1804 | + path-expression-matcher: 1.6.2 | |
| 1805 | + strnum: 2.4.2 | |
| 1806 | + xml-naming: 0.3.0 | |
| 1807 | + | |
| 1808 | + fdir@6.5.0(picomatch@4.0.7): | |
| 1809 | + optionalDependencies: | |
| 1810 | + picomatch: 4.0.7 | |
| 1811 | + | |
| 1812 | + fsevents@2.3.3: | |
| 1813 | + optional: true | |
| 1814 | + | |
| 1815 | + get-tsconfig@4.14.3: | |
| 1816 | + dependencies: | |
| 1817 | + resolve-pkg-maps: 1.0.0 | |
| 1818 | + | |
| 1819 | + htmlparser2@10.1.0: | |
| 1820 | + dependencies: | |
| 1821 | + domelementtype: 2.3.0 | |
| 1822 | + domhandler: 5.0.3 | |
| 1823 | + domutils: 3.2.2 | |
| 1824 | + entities: 7.0.1 | |
| 1825 | + | |
| 1826 | + iconv-lite@0.6.3: | |
| 1827 | + dependencies: | |
| 1828 | + safer-buffer: 2.1.2 | |
| 1829 | + | |
| 1830 | + is-unsafe@2.0.2: {} | |
| 1831 | + | |
| 1832 | + js-tokens@9.0.1: {} | |
| 1833 | + | |
| 1834 | + loupe@3.2.1: {} | |
| 1835 | + | |
| 1836 | + magic-string@0.30.21: | |
| 1837 | + dependencies: | |
| 1838 | + '@jridgewell/sourcemap-codec': 1.6.0 | |
| 1839 | + | |
| 1840 | + ms@2.1.3: {} | |
| 1841 | + | |
| 1842 | + nanoid@3.3.18: {} | |
| 1843 | + | |
| 1844 | + nth-check@2.1.1: | |
| 1845 | + dependencies: | |
| 1846 | + boolbase: 1.0.0 | |
| 1847 | + | |
| 1848 | + parse5-htmlparser2-tree-adapter@7.1.0: | |
| 1849 | + dependencies: | |
| 1850 | + domhandler: 5.0.3 | |
| 1851 | + parse5: 7.3.0 | |
| 1852 | + | |
| 1853 | + parse5-parser-stream@7.1.2: | |
| 1854 | + dependencies: | |
| 1855 | + parse5: 7.3.0 | |
| 1856 | + | |
| 1857 | + parse5@7.3.0: | |
| 1858 | + dependencies: | |
| 1859 | + entities: 6.0.1 | |
| 1860 | + | |
| 1861 | + path-expression-matcher@1.6.2: {} | |
| 1862 | + | |
| 1863 | + pathe@2.0.3: {} | |
| 1864 | + | |
| 1865 | + pathval@2.0.1: {} | |
| 1866 | + | |
| 1867 | + picocolors@1.1.1: {} | |
| 1868 | + | |
| 1869 | + picomatch@4.0.7: {} | |
| 1870 | + | |
| 1871 | + postcss@8.5.28: | |
| 1872 | + dependencies: | |
| 1873 | + nanoid: 3.3.18 | |
| 1874 | + picocolors: 1.1.1 | |
| 1875 | + source-map-js: 1.2.1 | |
| 1876 | + | |
| 1877 | + postgres@3.4.9: {} | |
| 1878 | + | |
| 1879 | + resolve-pkg-maps@1.0.0: {} | |
| 1880 | + | |
| 1881 | + rollup@4.63.1: | |
| 1882 | + dependencies: | |
| 1883 | + '@types/estree': 1.0.9 | |
| 1884 | + optionalDependencies: | |
| 1885 | + '@napi-rs/lzma-linux-x64-gnu': 1.5.1 | |
| 1886 | + '@rollup/rollup-android-arm-eabi': 4.63.1 | |
| 1887 | + '@rollup/rollup-android-arm64': 4.63.1 | |
| 1888 | + '@rollup/rollup-darwin-arm64': 4.63.1 | |
| 1889 | + '@rollup/rollup-darwin-x64': 4.63.1 | |
| 1890 | + '@rollup/rollup-freebsd-arm64': 4.63.1 | |
| 1891 | + '@rollup/rollup-freebsd-x64': 4.63.1 | |
| 1892 | + '@rollup/rollup-linux-arm-gnueabihf': 4.63.1 | |
| 1893 | + '@rollup/rollup-linux-arm-musleabihf': 4.63.1 | |
| 1894 | + '@rollup/rollup-linux-arm64-gnu': 4.63.1 | |
| 1895 | + '@rollup/rollup-linux-arm64-musl': 4.63.1 | |
| 1896 | + '@rollup/rollup-linux-loong64-gnu': 4.63.1 | |
| 1897 | + '@rollup/rollup-linux-loong64-musl': 4.63.1 | |
| 1898 | + '@rollup/rollup-linux-ppc64-gnu': 4.63.1 | |
| 1899 | + '@rollup/rollup-linux-ppc64-musl': 4.63.1 | |
| 1900 | + '@rollup/rollup-linux-riscv64-gnu': 4.63.1 | |
| 1901 | + '@rollup/rollup-linux-riscv64-musl': 4.63.1 | |
| 1902 | + '@rollup/rollup-linux-s390x-gnu': 4.63.1 | |
| 1903 | + '@rollup/rollup-linux-x64-gnu': 4.63.1 | |
| 1904 | + '@rollup/rollup-linux-x64-musl': 4.63.1 | |
| 1905 | + '@rollup/rollup-openbsd-x64': 4.63.1 | |
| 1906 | + '@rollup/rollup-openharmony-arm64': 4.63.1 | |
| 1907 | + '@rollup/rollup-win32-arm64-msvc': 4.63.1 | |
| 1908 | + '@rollup/rollup-win32-ia32-msvc': 4.63.1 | |
| 1909 | + '@rollup/rollup-win32-x64-gnu': 4.63.1 | |
| 1910 | + '@rollup/rollup-win32-x64-msvc': 4.63.1 | |
| 1911 | + fsevents: 2.3.3 | |
| 1912 | + | |
| 1913 | + safer-buffer@2.1.2: {} | |
| 1914 | + | |
| 1915 | + siginfo@2.0.0: {} | |
| 1916 | + | |
| 1917 | + source-map-js@1.2.1: {} | |
| 1918 | + | |
| 1919 | + source-map-support@0.5.21: | |
| 1920 | + dependencies: | |
| 1921 | + buffer-from: 1.1.2 | |
| 1922 | + source-map: 0.6.1 | |
| 1923 | + | |
| 1924 | + source-map@0.6.1: {} | |
| 1925 | + | |
| 1926 | + stackback@0.0.2: {} | |
| 1927 | + | |
| 1928 | + std-env@3.10.0: {} | |
| 1929 | + | |
| 1930 | + strip-literal@3.1.0: | |
| 1931 | + dependencies: | |
| 1932 | + js-tokens: 9.0.1 | |
| 1933 | + | |
| 1934 | + strnum@2.4.2: | |
| 1935 | + dependencies: | |
| 1936 | + anynum: 1.0.1 | |
| 1937 | + | |
| 1938 | + tinybench@2.9.0: {} | |
| 1939 | + | |
| 1940 | + tinyexec@0.3.2: {} | |
| 1941 | + | |
| 1942 | + tinyglobby@0.2.17: | |
| 1943 | + dependencies: | |
| 1944 | + fdir: 6.5.0(picomatch@4.0.7) | |
| 1945 | + picomatch: 4.0.7 | |
| 1946 | + | |
| 1947 | + tinypool@1.1.1: {} | |
| 1948 | + | |
| 1949 | + tinyrainbow@2.0.0: {} | |
| 1950 | + | |
| 1951 | + tinyspy@4.0.6: {} | |
| 1952 | + | |
| 1953 | + tsx@4.23.13: | |
| 1954 | + dependencies: | |
| 1955 | + esbuild: 0.28.2 | |
| 1956 | + optionalDependencies: | |
| 1957 | + fsevents: 2.3.3 | |
| 1958 | + | |
| 1959 | + typescript@5.9.3: {} | |
| 1960 | + | |
| 1961 | + undici-types@7.18.2: {} | |
| 1962 | + | |
| 1963 | + undici@7.29.1: {} | |
| 1964 | + | |
| 1965 | + unpdf@1.8.1: {} | |
| 1966 | + | |
| 1967 | + vite-node@3.2.4(@types/node@24.13.4)(tsx@4.23.13)(yaml@2.9.0): | |
| 1968 | + dependencies: | |
| 1969 | + cac: 6.7.14 | |
| 1970 | + debug: 4.4.3 | |
| 1971 | + es-module-lexer: 1.7.0 | |
| 1972 | + pathe: 2.0.3 | |
| 1973 | + vite: 7.3.6(@types/node@24.13.4)(tsx@4.23.13)(yaml@2.9.0) | |
| 1974 | + transitivePeerDependencies: | |
| 1975 | + - '@types/node' | |
| 1976 | + - jiti | |
| 1977 | + - less | |
| 1978 | + - lightningcss | |
| 1979 | + - sass | |
| 1980 | + - sass-embedded | |
| 1981 | + - stylus | |
| 1982 | + - sugarss | |
| 1983 | + - supports-color | |
| 1984 | + - terser | |
| 1985 | + - tsx | |
| 1986 | + - yaml | |
| 1987 | + | |
| 1988 | + vite@7.3.6(@types/node@24.13.4)(tsx@4.23.13)(yaml@2.9.0): | |
| 1989 | + dependencies: | |
| 1990 | + esbuild: 0.28.2 | |
| 1991 | + fdir: 6.5.0(picomatch@4.0.7) | |
| 1992 | + picomatch: 4.0.7 | |
| 1993 | + postcss: 8.5.28 | |
| 1994 | + rollup: 4.63.1 | |
| 1995 | + tinyglobby: 0.2.17 | |
| 1996 | + optionalDependencies: | |
| 1997 | + '@types/node': 24.13.4 | |
| 1998 | + fsevents: 2.3.3 | |
| 1999 | + tsx: 4.23.13 | |
| 2000 | + yaml: 2.9.0 | |
| 2001 | + | |
| 2002 | + vitest@3.2.7(@types/node@24.13.4)(tsx@4.23.13)(yaml@2.9.0): | |
| 2003 | + dependencies: | |
| 2004 | + '@types/chai': 5.2.3 | |
| 2005 | + '@vitest/expect': 3.2.7 | |
| 2006 | + '@vitest/mocker': 3.2.7(vite@7.3.6(@types/node@24.13.4)(tsx@4.23.13)(yaml@2.9.0)) | |
| 2007 | + '@vitest/pretty-format': 3.2.7 | |
| 2008 | + '@vitest/runner': 3.2.7 | |
| 2009 | + '@vitest/snapshot': 3.2.7 | |
| 2010 | + '@vitest/spy': 3.2.7 | |
| 2011 | + '@vitest/utils': 3.2.7 | |
| 2012 | + chai: 5.3.3 | |
| 2013 | + debug: 4.4.3 | |
| 2014 | + expect-type: 1.4.0 | |
| 2015 | + magic-string: 0.30.21 | |
| 2016 | + pathe: 2.0.3 | |
| 2017 | + picomatch: 4.0.7 | |
| 2018 | + std-env: 3.10.0 | |
| 2019 | + tinybench: 2.9.0 | |
| 2020 | + tinyexec: 0.3.2 | |
| 2021 | + tinyglobby: 0.2.17 | |
| 2022 | + tinypool: 1.1.1 | |
| 2023 | + tinyrainbow: 2.0.0 | |
| 2024 | + vite: 7.3.6(@types/node@24.13.4)(tsx@4.23.13)(yaml@2.9.0) | |
| 2025 | + vite-node: 3.2.4(@types/node@24.13.4)(tsx@4.23.13)(yaml@2.9.0) | |
| 2026 | + why-is-node-running: 2.3.0 | |
| 2027 | + optionalDependencies: | |
| 2028 | + '@types/node': 24.13.4 | |
| 2029 | + transitivePeerDependencies: | |
| 2030 | + - jiti | |
| 2031 | + - less | |
| 2032 | + - lightningcss | |
| 2033 | + - msw | |
| 2034 | + - sass | |
| 2035 | + - sass-embedded | |
| 2036 | + - stylus | |
| 2037 | + - sugarss | |
| 2038 | + - supports-color | |
| 2039 | + - terser | |
| 2040 | + - tsx | |
| 2041 | + - yaml | |
| 2042 | + | |
| 2043 | + whatwg-encoding@3.1.1: | |
| 2044 | + dependencies: | |
| 2045 | + iconv-lite: 0.6.3 | |
| 2046 | + | |
| 2047 | + whatwg-mimetype@4.0.0: {} | |
| 2048 | + | |
| 2049 | + why-is-node-running@2.3.0: | |
| 2050 | + dependencies: | |
| 2051 | + siginfo: 2.0.0 | |
| 2052 | + stackback: 0.0.2 | |
| 2053 | + | |
| 2054 | + xml-naming@0.3.0: {} | |
| 2055 | + | |
| 2056 | + yaml@2.9.0: {} | |
| 2057 | + | |
| 2058 | + zod@4.6.1: {} | |
added
pnpm-workspace.yaml
+10 −0
@@ -0,0 +1,10 @@ | ||
| 1 | +packages: | |
| 2 | + - "apps/*" | |
| 3 | + - "packages/*" | |
| 4 | +onlyBuiltDependencies: | |
| 5 | + - esbuild | |
| 6 | + - sharp | |
| 7 | + - '@tailwindcss/oxide' | |
| 8 | + - unrs-resolver | |
| 9 | + - bufferutil | |
| 10 | + - utf-8-validate | |
added
tsconfig.base.json
+19 −0
@@ -0,0 +1,19 @@ | ||
| 1 | +{ | |
| 2 | + "compilerOptions": { | |
| 3 | + "target": "ES2023", | |
| 4 | + "module": "NodeNext", | |
| 5 | + "moduleResolution": "NodeNext", | |
| 6 | + "lib": ["ES2023"], | |
| 7 | + "strict": true, | |
| 8 | + "noUncheckedIndexedAccess": true, | |
| 9 | + "exactOptionalPropertyTypes": false, | |
| 10 | + "esModuleInterop": true, | |
| 11 | + "skipLibCheck": true, | |
| 12 | + "resolveJsonModule": true, | |
| 13 | + "declaration": false, | |
| 14 | + "sourceMap": true, | |
| 15 | + "forceConsistentCasingInFileNames": true, | |
| 16 | + "isolatedModules": true, | |
| 17 | + "verbatimModuleSyntax": true | |
| 18 | + } | |
| 19 | +} | |
| 20 | ||