@bluefields/db 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (76) hide show
  1. package/LICENSE +202 -0
  2. package/dist/client.d.ts +6 -0
  3. package/dist/client.js +13 -0
  4. package/dist/client.js.map +1 -0
  5. package/dist/index.d.ts +19 -0
  6. package/dist/index.js +20 -0
  7. package/dist/index.js.map +1 -0
  8. package/dist/migrate.d.ts +11 -0
  9. package/dist/migrate.js +48 -0
  10. package/dist/migrate.js.map +1 -0
  11. package/dist/schema/api-keys.d.ts +145 -0
  12. package/dist/schema/api-keys.js +18 -0
  13. package/dist/schema/api-keys.js.map +1 -0
  14. package/dist/schema/audit-log.d.ts +228 -0
  15. package/dist/schema/audit-log.js +49 -0
  16. package/dist/schema/audit-log.js.map +1 -0
  17. package/dist/schema/cost-records.d.ts +190 -0
  18. package/dist/schema/cost-records.js +55 -0
  19. package/dist/schema/cost-records.js.map +1 -0
  20. package/dist/schema/crawl-job-pages.d.ts +186 -0
  21. package/dist/schema/crawl-job-pages.js +31 -0
  22. package/dist/schema/crawl-job-pages.js.map +1 -0
  23. package/dist/schema/crawl-jobs.d.ts +271 -0
  24. package/dist/schema/crawl-jobs.js +40 -0
  25. package/dist/schema/crawl-jobs.js.map +1 -0
  26. package/dist/schema/customers.d.ts +145 -0
  27. package/dist/schema/customers.js +19 -0
  28. package/dist/schema/customers.js.map +1 -0
  29. package/dist/schema/event-feedback.d.ts +170 -0
  30. package/dist/schema/event-feedback.js +35 -0
  31. package/dist/schema/event-feedback.js.map +1 -0
  32. package/dist/schema/events.d.ts +291 -0
  33. package/dist/schema/events.js +58 -0
  34. package/dist/schema/events.js.map +1 -0
  35. package/dist/schema/extractions.d.ts +327 -0
  36. package/dist/schema/extractions.js +56 -0
  37. package/dist/schema/extractions.js.map +1 -0
  38. package/dist/schema/host-extractors.d.ts +131 -0
  39. package/dist/schema/host-extractors.js +18 -0
  40. package/dist/schema/host-extractors.js.map +1 -0
  41. package/dist/schema/host-records.d.ts +237 -0
  42. package/dist/schema/host-records.js +42 -0
  43. package/dist/schema/host-records.js.map +1 -0
  44. package/dist/schema/notification-preferences.d.ts +195 -0
  45. package/dist/schema/notification-preferences.js +49 -0
  46. package/dist/schema/notification-preferences.js.map +1 -0
  47. package/dist/schema/scrape-cache.d.ts +136 -0
  48. package/dist/schema/scrape-cache.js +23 -0
  49. package/dist/schema/scrape-cache.js.map +1 -0
  50. package/dist/schema/sessions.d.ts +153 -0
  51. package/dist/schema/sessions.js +27 -0
  52. package/dist/schema/sessions.js.map +1 -0
  53. package/dist/schema/snapshots.d.ts +382 -0
  54. package/dist/schema/snapshots.js +60 -0
  55. package/dist/schema/snapshots.js.map +1 -0
  56. package/dist/schema/subscriptions.d.ts +604 -0
  57. package/dist/schema/subscriptions.js +79 -0
  58. package/dist/schema/subscriptions.js.map +1 -0
  59. package/dist/schema/usage-periods.d.ts +446 -0
  60. package/dist/schema/usage-periods.js +71 -0
  61. package/dist/schema/usage-periods.js.map +1 -0
  62. package/dist/tsconfig.tsbuildinfo +1 -0
  63. package/package.json +54 -0
  64. package/src/migrations/0001_initial.sql +398 -0
  65. package/src/migrations/0002_pg_cron.sql +109 -0
  66. package/src/migrations/0003_webhook_secret_cleanup.sql +55 -0
  67. package/src/migrations/0004_events_pg_notify.sql +39 -0
  68. package/src/migrations/0005_subscription_consecutive_failures.sql +19 -0
  69. package/src/migrations/0006_notification_preferences.sql +27 -0
  70. package/src/migrations/0007_crawl_jobs.sql +65 -0
  71. package/src/migrations/0008_sessions.sql +38 -0
  72. package/src/migrations/0009_scrape_cache.sql +34 -0
  73. package/src/migrations/0010_neon_defaults.sql +99 -0
  74. package/src/migrations/0011_credits_consumed_numeric.sql +12 -0
  75. package/src/migrations/0012_crawl_page_attestation.sql +10 -0
  76. package/src/migrations/0013_customer_deleted_at.sql +5 -0
@@ -0,0 +1,65 @@
1
+ -- BlueFields Phase C — crawl jobs.
2
+ --
3
+ -- A "crawl job" is a long-running BFS over a domain. The route returns
4
+ -- a job_id immediately; an Inngest function does the actual BFS,
5
+ -- writing per-page rows as it goes. Customers poll GET /crawl/:id or
6
+ -- subscribe to GET /crawl/:id/stream (SSE) for progress.
7
+ --
8
+ -- The same schema serves /batch/scrape + /extract in Phase D — both
9
+ -- are also "jobs with per-URL results."
10
+
11
+ CREATE TABLE IF NOT EXISTS crawl_jobs (
12
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
13
+ customer_id UUID NOT NULL REFERENCES customers(id),
14
+ -- Discriminator: 'crawl' | 'batch_scrape' | 'extract'. One table for
15
+ -- all three keeps the polling/stream/webhook paths uniform.
16
+ job_kind TEXT NOT NULL,
17
+ -- For crawl: the seed URL. For batch_scrape/extract: null (per-URL
18
+ -- list lives in `params.urls`).
19
+ start_url TEXT,
20
+ url_canonical TEXT,
21
+ status TEXT NOT NULL DEFAULT 'pending',
22
+ -- All request-time params (maxDepth, maxPages, includes, excludes,
23
+ -- scrapeOptions, urls list, schema, ...) — kept opaque here so the
24
+ -- worker can evolve without schema migrations.
25
+ params JSONB NOT NULL DEFAULT '{}'::jsonb,
26
+ total_pages_discovered INT NOT NULL DEFAULT 0,
27
+ total_pages_scraped INT NOT NULL DEFAULT 0,
28
+ -- Optional: webhook to call when status transitions to terminal.
29
+ webhook_url TEXT,
30
+ webhook_secret TEXT,
31
+ created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
32
+ started_at TIMESTAMPTZ,
33
+ completed_at TIMESTAMPTZ,
34
+ -- On failure: short human-readable reason.
35
+ error TEXT
36
+ );
37
+
38
+ CREATE INDEX IF NOT EXISTS idx_crawl_jobs_customer_time
39
+ ON crawl_jobs (customer_id, created_at DESC);
40
+ CREATE INDEX IF NOT EXISTS idx_crawl_jobs_status_pending
41
+ ON crawl_jobs (status, created_at)
42
+ WHERE status IN ('pending', 'running');
43
+
44
+ -- Per-page results. Composite PK on (job_id, url_canonical) so a
45
+ -- job can't double-record the same URL (BFS dedup safety net).
46
+ CREATE TABLE IF NOT EXISTS crawl_job_pages (
47
+ job_id UUID NOT NULL REFERENCES crawl_jobs(id) ON DELETE CASCADE,
48
+ url_canonical TEXT NOT NULL,
49
+ -- BFS depth from start_url. 0 for the seed; null for batch_scrape/extract.
50
+ depth INT,
51
+ -- Result fields: subset of FetchResult + extraction metadata.
52
+ content_hash TEXT,
53
+ response_status INT,
54
+ fetched_at TIMESTAMPTZ NOT NULL DEFAULT now(),
55
+ extraction_id UUID,
56
+ -- 'ok' | 'fetch_failed' | 'extract_failed' | 'excluded' | 'depth_limit'
57
+ status TEXT NOT NULL DEFAULT 'ok',
58
+ status_detail TEXT,
59
+ PRIMARY KEY (job_id, url_canonical)
60
+ );
61
+
62
+ CREATE INDEX IF NOT EXISTS idx_crawl_pages_job_time
63
+ ON crawl_job_pages (job_id, fetched_at DESC);
64
+ CREATE INDEX IF NOT EXISTS idx_crawl_pages_job_status
65
+ ON crawl_job_pages (job_id, status);
@@ -0,0 +1,38 @@
1
+ -- BlueFields Phase G — named sessions for cookie/header reuse across scrapes.
2
+ --
3
+ -- One row per (customer_id, name). The cookies + headers blobs are
4
+ -- AES-256-GCM ciphertext (key from SESSION_ENCRYPTION_KEY env). Each
5
+ -- payload is a versioned JSON envelope: `{ v: 1, iv, tag, ciphertext }`
6
+ -- — the encryption layer lives in @bluefields/control/sessions.ts.
7
+ --
8
+ -- Expiry: caller-supplied or default 30 days. A row past expires_at is
9
+ -- treated as missing by `loadSession`; a daily cleanup cron is wired in
10
+ -- chunk-22-style if/when needed (volume should be low — names are
11
+ -- intentional, not request-scoped).
12
+
13
+ CREATE TABLE IF NOT EXISTS sessions (
14
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
15
+ customer_id UUID NOT NULL REFERENCES customers(id) ON DELETE CASCADE,
16
+ -- Human-friendly identifier scoped per customer. NOT globally unique;
17
+ -- two customers can both have a session named 'github-prod'.
18
+ name TEXT NOT NULL,
19
+ -- AES-256-GCM ciphertext (versioned JSON envelope, stored as TEXT for
20
+ -- easy inspection in psql; not a BYTEA because Drizzle's text() type
21
+ -- works cleanly here).
22
+ cookies_ciphertext TEXT NOT NULL,
23
+ -- Optional default headers (Authorization, custom UA) — same envelope.
24
+ headers_ciphertext TEXT,
25
+ -- When this session stops being usable. NULL == no expiry (rare).
26
+ expires_at TIMESTAMPTZ,
27
+ created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
28
+ updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
29
+ -- Per-customer uniqueness so save/use lookups are O(1).
30
+ UNIQUE (customer_id, name)
31
+ );
32
+
33
+ CREATE INDEX IF NOT EXISTS idx_sessions_customer_name
34
+ ON sessions (customer_id, name);
35
+ -- Partial index for the cleanup job — only scan rows that have an expiry.
36
+ CREATE INDEX IF NOT EXISTS idx_sessions_expires_at
37
+ ON sessions (expires_at)
38
+ WHERE expires_at IS NOT NULL;
@@ -0,0 +1,34 @@
1
+ -- BlueFields Phase A.5 — per-customer scrape cache for /scrape maxAge.
2
+ --
3
+ -- One row per (customer_id, url_canonical). Holds enough to short-circuit
4
+ -- the next /scrape for the same URL without re-running the fetch +
5
+ -- extractor pipeline.
6
+ --
7
+ -- We store an extraction_id pointer rather than denormalising markdown /
8
+ -- structured_data — extractions can be 100KB+ and the cache is a small,
9
+ -- hot lookup. The actual data is one extra SELECT on the extractions
10
+ -- table when the cache hits.
11
+ --
12
+ -- Cross-customer sharing happens at the extractor layer (Review #6 cache
13
+ -- on content_hash + schema_hash + extractor_version). This table is a
14
+ -- per-customer URL-level layer on top of that.
15
+
16
+ CREATE TABLE IF NOT EXISTS scrape_cache (
17
+ customer_id UUID NOT NULL REFERENCES customers(id) ON DELETE CASCADE,
18
+ url_canonical TEXT NOT NULL,
19
+ -- The fresh-fetched extraction. NULL only when the call was html-only
20
+ -- (no extractor invoked) — those cache rows can still answer html
21
+ -- requests when wired to a snapshot. v1: store NULL when no extractor
22
+ -- ran; serve only html-format requests from cache.
23
+ extraction_id UUID,
24
+ content_hash TEXT NOT NULL,
25
+ final_url TEXT NOT NULL,
26
+ response_status INT NOT NULL,
27
+ fetched_at TIMESTAMPTZ NOT NULL DEFAULT now(),
28
+ PRIMARY KEY (customer_id, url_canonical)
29
+ );
30
+
31
+ -- Lookup by url across customers is rare — main read is the PK lookup
32
+ -- per /scrape call. A by-time index would help if we add a cleanup cron
33
+ -- later, but for v1 we keep the table lean and rely on customer-scoped
34
+ -- TTL behavior (rows are overwritten by upsert on each fresh scrape).
@@ -0,0 +1,99 @@
1
+ -- 0010 — Neon-friendly default partitions + missing usage_period seed.
2
+ --
3
+ -- Why:
4
+ -- Migration 0001 creates partitioned parent tables (snapshots, events,
5
+ -- cost_records, audit_log) and calls partman.create_parent() to spin
6
+ -- up the per-month child partitions. Neon doesn't have pg_partman, so
7
+ -- the partman call is wrapped in a DO-block that no-ops when the
8
+ -- extension is missing. Result: the parent tables exist, but no child
9
+ -- partitions — any INSERT errors out with:
10
+ -- ERROR: no partition of relation "cost_records" found for row
11
+ --
12
+ -- For Neon (and any pg-partman-less Postgres), add a single DEFAULT
13
+ -- partition per parent. New rows route there until/unless the
14
+ -- operator later layers in a real partitioning strategy.
15
+ --
16
+ -- Also seeds a usage_periods row for any customer that lacks one in
17
+ -- the current month — covers the bootstrap admin case where the
18
+ -- customer was inserted via raw SQL without going through the Stripe
19
+ -- checkout webhook that normally creates the period.
20
+ --
21
+ -- Safe to re-run: all statements are IF NOT EXISTS / ON CONFLICT.
22
+
23
+ -- ── Default partitions ─────────────────────────────────────────────────
24
+
25
+ CREATE TABLE IF NOT EXISTS snapshots_default
26
+ PARTITION OF snapshots DEFAULT;
27
+
28
+ CREATE TABLE IF NOT EXISTS events_default
29
+ PARTITION OF events DEFAULT;
30
+
31
+ CREATE TABLE IF NOT EXISTS cost_records_default
32
+ PARTITION OF cost_records DEFAULT;
33
+
34
+ CREATE TABLE IF NOT EXISTS audit_log_default
35
+ PARTITION OF audit_log DEFAULT;
36
+
37
+ -- ── Backfill usage_periods for customers without an active row ─────────
38
+ --
39
+ -- For each customer whose latest period_end is before today (or who has
40
+ -- no period at all), insert a period covering the current calendar
41
+ -- month with the customer's plan-derived quotas. Numbers below match
42
+ -- the application layer's free-tier plan defaults; customers
43
+ -- on higher tiers should still get a usable period (overstating their
44
+ -- quota slightly is harmless — the Stripe checkout flow re-snapshots
45
+ -- at the next month rollover).
46
+
47
+ INSERT INTO usage_periods (
48
+ customer_id, period_start, period_end, plan,
49
+ events_quota, scrapes_quota, active_subs_quota, overage_cap_events,
50
+ credits_quota
51
+ )
52
+ SELECT
53
+ c.id,
54
+ date_trunc('month', now())::date AS period_start,
55
+ (date_trunc('month', now()) + interval '1 month - 1 day')::date AS period_end,
56
+ c.plan,
57
+ CASE c.plan
58
+ WHEN 'free' THEN 100
59
+ WHEN 'starter' THEN 5000
60
+ WHEN 'growth' THEN 50000
61
+ WHEN 'scale' THEN 500000
62
+ ELSE 100
63
+ END AS events_quota,
64
+ CASE c.plan
65
+ WHEN 'free' THEN 100
66
+ WHEN 'starter' THEN 5000
67
+ WHEN 'growth' THEN 50000
68
+ WHEN 'scale' THEN 500000
69
+ ELSE 100
70
+ END AS scrapes_quota,
71
+ CASE c.plan
72
+ WHEN 'free' THEN 10
73
+ WHEN 'starter' THEN 50
74
+ WHEN 'growth' THEN 500
75
+ WHEN 'scale' THEN 5000
76
+ ELSE 10
77
+ END AS active_subs_quota,
78
+ CASE c.plan
79
+ WHEN 'free' THEN 100
80
+ WHEN 'starter' THEN 7500
81
+ WHEN 'growth' THEN 75000
82
+ WHEN 'scale' THEN 750000
83
+ ELSE 100
84
+ END AS overage_cap_events,
85
+ CASE c.plan
86
+ WHEN 'free' THEN 500
87
+ WHEN 'starter' THEN 25000
88
+ WHEN 'growth' THEN 100000
89
+ WHEN 'scale' THEN 500000
90
+ ELSE 500
91
+ END AS credits_quota
92
+ FROM customers c
93
+ WHERE NOT EXISTS (
94
+ SELECT 1 FROM usage_periods up
95
+ WHERE up.customer_id = c.id
96
+ AND up.period_start <= now()::date
97
+ AND up.period_end >= now()::date
98
+ )
99
+ ON CONFLICT (customer_id, period_start) DO NOTHING;
@@ -0,0 +1,12 @@
1
+ -- 0011_credits_consumed_numeric.sql
2
+ --
3
+ -- Cache-hit fractional charge (0.2 credit — Option A pricing, 2026-06-16)
4
+ -- needs a sub-integer credits column. Widen usage_periods.credits_consumed from
5
+ -- INTEGER to NUMERIC(14,4): up to 10 integer digits (>> any plan's credit total)
6
+ -- plus 4 decimal places. Lossless for existing integer values; not destructive.
7
+
8
+ ALTER TABLE usage_periods
9
+ ALTER COLUMN credits_consumed TYPE NUMERIC(14, 4) USING credits_consumed::numeric(14, 4);
10
+
11
+ ALTER TABLE usage_periods
12
+ ALTER COLUMN credits_consumed SET DEFAULT 0;
@@ -0,0 +1,10 @@
1
+ -- 0012_crawl_page_attestation.sql
2
+ --
3
+ -- Per-page attestation for crawl / batch / extract (2026-06-21).
4
+ -- Stores the signed v1 manifest ({ manifest, signature }) for each crawled page
5
+ -- when ATTEST_SYNC_ROUTES is enabled, so GET /crawl/:id can return it. Nullable
6
+ -- + additive: existing rows stay NULL and the default-off path is unaffected.
7
+ -- Idempotent (IF NOT EXISTS) so the runner can safely re-apply it.
8
+
9
+ ALTER TABLE crawl_job_pages
10
+ ADD COLUMN IF NOT EXISTS attestation jsonb;
@@ -0,0 +1,5 @@
1
+ -- Account deletion (GDPR right to erasure): mark a customer as deleted/anonymized.
2
+ -- The row is kept (anonymized in place by @bluefields/control deleteAccount) for
3
+ -- billing/audit integrity; NULL = active. Additive + idempotent (the migrate
4
+ -- runner re-applies every .sql file, so this MUST stay re-runnable).
5
+ ALTER TABLE customers ADD COLUMN IF NOT EXISTS deleted_at timestamptz;