@secondlayer/shared 10.0.1 → 11.0.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.
- package/dist/src/db/index.js.map +1 -1
- package/dist/src/db/queries/chain-reorgs.js.map +1 -1
- package/dist/src/db/queries/subgraphs.d.ts +1 -8
- package/dist/src/db/queries/subgraphs.js +1 -9
- package/dist/src/db/queries/subgraphs.js.map +3 -3
- package/dist/src/errors.d.ts +5 -2
- package/dist/src/errors.js +5 -5
- package/dist/src/errors.js.map +3 -3
- package/dist/src/index.d.ts +14 -8
- package/dist/src/index.js +12 -13
- package/dist/src/index.js.map +6 -6
- package/dist/src/runtime/index.js +2 -2
- package/dist/src/runtime/index.js.map +3 -3
- package/dist/src/schemas/index.d.ts +0 -6
- package/dist/src/schemas/index.js +2 -3
- package/dist/src/schemas/index.js.map +3 -3
- package/dist/src/schemas/subgraphs.d.ts +0 -6
- package/dist/src/schemas/subgraphs.js +2 -3
- package/dist/src/schemas/subgraphs.js.map +3 -3
- package/dist/src/subgraphs/spec.d.ts +9 -0
- package/dist/src/subgraphs/spec.js +7 -7
- package/dist/src/subgraphs/spec.js.map +3 -3
- package/package.json +1 -1
package/dist/src/db/index.js.map
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
"/**\n * Instance modes for the Secondlayer platform.\n *\n * - `oss`: self-hosted, single-tenant. No auth middleware, no platform routes\n * (projects, admin). Everything runs against a single `DATABASE_URL`.\n * Intended for `docker compose up`.\n *\n * - `platform`: the metered archive deployment. Magic-link auth, API keys,\n * accounts, credits, and metered `/v1` reads (Index + Streams + subgraphs)\n * against a single shared DB. No paid tier ladder — one metered `free`\n * tier plus first-party internal service credentials.\n */\n\nexport type InstanceMode = \"oss\" | \"platform\";\n\n/** What the operator wrote in the env — `archive` is the honest name for the\n * metered public-archive deployment and aliases `platform` behaviorally. */\nexport type DeclaredInstanceMode = InstanceMode | \"archive\";\n\nconst VALID_MODES: readonly DeclaredInstanceMode[] = [\n\t\"oss\",\n\t\"platform\",\n\t\"archive\",\n];\n\nfunction declaredMode(): DeclaredInstanceMode {\n\tconst raw = process.env.INSTANCE_MODE?.trim().toLowerCase();\n\tif (raw && (VALID_MODES as readonly string[]).includes(raw)) {\n\t\treturn raw as DeclaredInstanceMode;\n\t}\n\treturn \"oss\";\n}\n\n/**\n * Resolve the active instance mode from `process.env.INSTANCE_MODE`.\n * Defaults to `\"oss\"` — the safest default for self-hosters who deploy\n * without setting the variable. `archive` normalizes to `platform` here so\n * every behavioral branch stays a two-way choice; use\n * `getDeclaredInstanceMode()` for display surfaces.\n */\nexport function getInstanceMode(): InstanceMode {\n\tconst declared = declaredMode();\n\treturn declared === \"archive\" ? \"platform\" : declared;\n}\n\n/** The mode as declared in the env, for status/identity surfaces. */\nexport function getDeclaredInstanceMode(): DeclaredInstanceMode {\n\treturn declaredMode();\n}\n\n/**\n * True when public reads are credit-metered (the free window, retention, and\n * credits gates arm). Platform/archive deployments meter by default;\n * self-host never does unless the operator explicitly sets\n * `METERED_READS=true`. `METERED_READS=false` disarms a metered deployment.\n */\nexport function isMeteredReads(): boolean {\n\tconst override = process.env.METERED_READS?.trim().toLowerCase();\n\tif (override === \"true\") return true;\n\tif (override === \"false\") return false;\n\treturn getInstanceMode() === \"platform\";\n}\n\n/** True when the active mode is `\"platform\"` (shared multi-tenant). */\nexport function isPlatformMode(): boolean {\n\treturn getInstanceMode() === \"platform\";\n}\n\n/** True when the active mode is `\"oss\"` (self-hosted). */\nexport function isOssMode(): boolean {\n\treturn getInstanceMode() === \"oss\";\n}\n",
|
|
8
8
|
"import { type RawBuilder, sql } from \"kysely\";\n\n/**\n * Safely encode a JS value as a JSONB literal for Kysely inserts/updates.\n * Kysely + postgres.js double-encodes JSON when using parameterized queries\n * with ::jsonb casts. This uses sql.raw to inline a properly escaped literal.\n *\n * Generic parameter lets callers set the RawBuilder's output type so they\n * don't need to cast at the insert site. Default is `unknown` — widen at\n * the call site when the column type is narrower, e.g. `jsonb<MyShape>(...)`.\n */\nexport function jsonb<T = unknown>(value: T): RawBuilder<T> {\n\tconst escaped = JSON.stringify(value, (_k, v) =>\n\t\ttypeof v === \"bigint\" ? v.toString() : v,\n\t).replace(/'/g, \"''\");\n\treturn sql`${sql.raw(`'${escaped}'::jsonb`)}`;\n}\n\n/**\n * Safely parse a JSONB value from the database.\n * Handles double-encoded strings where postgres.js returns a JSON string\n * instead of a parsed object.\n */\nexport function parseJsonb<T = unknown>(value: unknown): T {\n\tif (typeof value === \"string\") {\n\t\ttry {\n\t\t\treturn JSON.parse(value) as T;\n\t\t} catch {\n\t\t\treturn value as T;\n\t\t}\n\t}\n\treturn (value ?? {}) as T;\n}\n",
|
|
9
9
|
"import { Kysely } from \"kysely\";\nimport { PostgresJSDialect } from \"kysely-postgres-js\";\nimport postgres from \"postgres\";\nimport { isProductionEnv } from \"../env.ts\";\nimport { logger } from \"../logger.ts\";\nimport { isPlatformMode } from \"../mode.ts\";\nimport type { Database } from \"./types.ts\";\n\nexport type { Database } from \"./types.ts\";\n\nconst DEFAULT_URL =\n\t\"postgres://postgres:postgres@localhost:5432/secondlayer_dev\";\n\ninterface PoolEntry {\n\tdb: Kysely<Database>;\n\trawClient: ReturnType<typeof postgres>;\n\t/** Last access (ms) — drives LRU eviction of BYO pools. */\n\tlastUsed: number;\n\t/** Monotonic counter as a tiebreaker; Date.now() can repeat under load. */\n\tseq: number;\n}\n\n/**\n * Cache of Kysely + raw postgres.js pools keyed by resolved URL.\n * Two getters resolving to the same URL share one entry (single pool) —\n * this is the single-DB backward-compat contract: when only `DATABASE_URL`\n * is set, `getSourceDb() === getTargetDb()` (zero regression vs. pre-dual-DB).\n *\n * The BYO data plane adds one pool per user-owned DB. To stop N user DBs from\n * exhausting connections/FDs, the map is bounded (`DATABASE_MAX_POOLS`, default\n * 25) with LRU eviction — the hot source/target pools are never evicted.\n */\nconst pools = new Map<string, PoolEntry>();\nlet poolSeq = 0;\n\nfunction maxPools(): number {\n\treturn Number.parseInt(process.env.DATABASE_MAX_POOLS ?? \"25\", 10);\n}\n\n/** Close the least-recently-used non-(source/target) pool when over the cap. */\nfunction evictIfNeeded(): void {\n\tif (pools.size <= maxPools()) return;\n\tconst protectedUrls = new Set([resolveSourceUrl(), resolveTargetUrl()]);\n\tlet lruUrl: string | undefined;\n\tlet lruEntry: PoolEntry | undefined;\n\tfor (const [url, entry] of pools) {\n\t\tif (protectedUrls.has(url)) continue;\n\t\tif (\n\t\t\t!lruEntry ||\n\t\t\tentry.lastUsed < lruEntry.lastUsed ||\n\t\t\t(entry.lastUsed === lruEntry.lastUsed && entry.seq < lruEntry.seq)\n\t\t) {\n\t\t\tlruUrl = url;\n\t\t\tlruEntry = entry;\n\t\t}\n\t}\n\tif (!lruUrl || !lruEntry) return;\n\tpools.delete(lruUrl);\n\tconst evicted = lruEntry;\n\t// Close in the background — never block pool creation on teardown.\n\tvoid evicted.db\n\t\t.destroy()\n\t\t.catch(() => {})\n\t\t.then(() => evicted.rawClient.end({ timeout: 5 }))\n\t\t.catch(() => {});\n}\n\n/** OSS: one application URL. SOURCE_/TARGET_ are hosted split only. */\nexport function applicationDatabaseUrl(): string {\n\treturn process.env.DATABASE_URL || DEFAULT_URL;\n}\n\nfunction resolveSourceUrl(): string {\n\tif (!isPlatformMode()) return applicationDatabaseUrl();\n\treturn (\n\t\tprocess.env.SOURCE_DATABASE_URL || process.env.DATABASE_URL || DEFAULT_URL\n\t);\n}\n\nfunction resolveTargetUrl(): string {\n\tif (!isPlatformMode()) return applicationDatabaseUrl();\n\treturn (\n\t\tprocess.env.TARGET_DATABASE_URL || process.env.DATABASE_URL || DEFAULT_URL\n\t);\n}\n\n/** Host[:port]/dbname for a connection URL — credentials stripped. */\nfunction describeDbUrl(url: string): string {\n\ttry {\n\t\tconst u = new URL(url);\n\t\treturn `${u.hostname}${u.port ? `:${u.port}` : \"\"}${u.pathname}`;\n\t} catch {\n\t\treturn \"invalid-url\";\n\t}\n}\n\n/** Warn-once latch — one line per process, not one per import or connection. */\nlet warnedDefaultUrlFallthrough = false;\n\n/**\n * Test-run guard for the plain \"no DB env at all\" case, which `assertDbSplit`\n * deliberately does not cover (it only fires when a SPLIT is requested).\n *\n * Without any of the three URL vars, every getter resolves to the built-in\n * dev `DEFAULT_URL`. In a test run that means one of two silent outcomes: the\n * suite's own `skipIf(!process.env.DATABASE_URL)` guard disables it entirely,\n * or an unguarded test connects to whatever Postgres happens to be on\n * localhost:5432. Both exit 0 and read as \"tests passed\".\n *\n * Fires at MODULE LOAD, not from a getter: DB-backed suites guard `getDb()`\n * behind their own `skipIf`, so when the env is missing no getter is ever\n * called — import is the only point where this is still observable.\n *\n * Gated to `NODE_ENV=test` on purpose. `@secondlayer/shared` is published, and\n * end users running `sl local` / `sl dev` rely on `DEFAULT_URL` legitimately —\n * it matches what the CLI provisions. They must see nothing. Fail-soft: warns,\n * never throws, matching `assertDbSplit`'s contract.\n */\nfunction warnIfTestRunFallsThroughToDefault(): void {\n\tif (warnedDefaultUrlFallthrough) return;\n\t// Runtime read — inlining `process.env.NODE_ENV` gets constant-folded into\n\t// the shipped dist, freezing this to a build-time literal. See env.ts.\n\t// biome-ignore lint/complexity/useLiteralKeys: bracket access is deliberate — dot-access gets constant-folded by the bundler.\n\tif (process.env[\"NODE_ENV\"] !== \"test\") return;\n\tif (\n\t\tprocess.env.DATABASE_URL ||\n\t\tprocess.env.SOURCE_DATABASE_URL ||\n\t\tprocess.env.TARGET_DATABASE_URL\n\t) {\n\t\treturn;\n\t}\n\twarnedDefaultUrlFallthrough = true;\n\tconsole.warn(\n\t\t`⚠️ DATABASE_URL unset — falling through to the built-in DEFAULT_URL (${describeDbUrl(\n\t\t\tDEFAULT_URL,\n\t\t)}). Database-backed tests will skip or hit the wrong Postgres. Copy .env.example to .env in this package (see \\`bun run db\\`).`,\n\t);\n}\n\nwarnIfTestRunFallsThroughToDefault();\n\nexport interface DbSplitStatus {\n\t/** \"split\" when source/target resolve to different DBs, else \"single\". */\n\tmode: \"split\" | \"single\";\n\t/** True when the chain/control split is live (distinct DBs). */\n\tactive: boolean;\n\t/** SOURCE host[:port]/dbname (no credentials). */\n\tsourceDb: string;\n\t/** TARGET host[:port]/dbname (no credentials). */\n\ttargetDb: string;\n}\n\n/**\n * Resolved source/target DB identity for status/health surfaces. Lets operators\n * see whether the chain/control split is live or dormant (collapsed to one DB)\n * without shelling in. Credentials are never exposed — host/db only.\n *\n * `active` is a STRING-IDENTITY check on the two URLs: it can't catch two\n * distinct URLs that alias the same physical instance (e.g. `postgres` vs\n * `127.0.0.1`, or a swapped/wrong host). Treat `active: true` as \"configured\n * for split\", not a proof of physical isolation.\n */\nexport function getDbSplitStatus(): DbSplitStatus {\n\tconst source = resolveSourceUrl();\n\tconst target = resolveTargetUrl();\n\tconst active = source !== target;\n\treturn {\n\t\tmode: active ? \"split\" : \"single\",\n\t\tactive,\n\t\tsourceDb: describeDbUrl(source),\n\t\ttargetDb: describeDbUrl(target),\n\t};\n}\n\n/**\n * Boot guard for the chain/control DB split. Surfaces three misconfigurations\n * loudly (fail-soft — logs, never throws, so a misconfig can't brick startup):\n *\n * 1. Silent wrong-DB: a split is requested (one of SOURCE_/TARGET_ set) but\n * `DATABASE_URL` is absent (the split-prod default) and the OTHER var is\n * unset, so it falls through to the built-in dev `DEFAULT_URL` — about to\n * read/write a real-but-wrong database. This is the failure mode the\n * remove-DATABASE_URL decision creates; catch it.\n * 2. Collapsed split: both vars set but resolve to the same DB (typo, or a\n * stray `DATABASE_URL` masking the intent).\n * 3. Dormant split (prod only): neither var set, so all services share one\n * Postgres failure domain. Not an error, but no longer silent.\n */\nexport function assertDbSplit(): void {\n\t// Runtime read — inlining `process.env.NODE_ENV` gets constant-folded in the\n\t// shipped dist (this prod branch would die silently). See env.ts.\n\tconst isProd = isProductionEnv();\n\tconst wantsSplit = !!(\n\t\tprocess.env.SOURCE_DATABASE_URL || process.env.TARGET_DATABASE_URL\n\t);\n\tconst databaseUrlSet = !!process.env.DATABASE_URL;\n\tconst source = resolveSourceUrl();\n\tconst target = resolveTargetUrl();\n\n\tif (\n\t\twantsSplit &&\n\t\t!databaseUrlSet &&\n\t\t(source === DEFAULT_URL || target === DEFAULT_URL)\n\t) {\n\t\tconst which =\n\t\t\tsource === DEFAULT_URL ? \"SOURCE_DATABASE_URL\" : \"TARGET_DATABASE_URL\";\n\t\tconst msg = `${which} unset and DATABASE_URL absent — resolving to built-in DEFAULT_URL; refusing to silently use the wrong database`;\n\t\tif (isProd) console.error(`❌ ${msg}`);\n\t\telse console.warn(`⚠️ ${msg}`);\n\t\treturn;\n\t}\n\n\tif (!wantsSplit) {\n\t\tif (isProd) {\n\t\t\tconsole.warn(\n\t\t\t\t\"⚠️ DB split dormant — all services share one Postgres failure domain (SOURCE_/TARGET_DATABASE_URL unset)\",\n\t\t\t);\n\t\t}\n\t\treturn;\n\t}\n\n\tif (source === target) {\n\t\tconst msg =\n\t\t\t\"DB split requested but SOURCE_DATABASE_URL === TARGET_DATABASE_URL (check for a typo or a stray DATABASE_URL fallback)\";\n\t\tif (isProd) console.error(`❌ ${msg}`);\n\t\telse console.warn(`⚠️ ${msg}`);\n\t}\n}\n\nfunction getOrCreatePool(url: string): PoolEntry {\n\tconst existing = pools.get(url);\n\tif (existing) {\n\t\texisting.lastUsed = Date.now();\n\t\texisting.seq = poolSeq++;\n\t\treturn existing;\n\t}\n\n\t// \"Local\" = we skip TLS. Any Docker service alias (single-label hostname\n\t// with no dots) is on an internal network and won't serve TLS.\n\tconst host = (() => {\n\t\ttry {\n\t\t\treturn new URL(url).hostname;\n\t\t} catch {\n\t\t\treturn \"\";\n\t\t}\n\t})();\n\tconst isLocal =\n\t\thost === \"localhost\" || host === \"127.0.0.1\" || !host.includes(\".\");\n\tconst poolMax = Number.parseInt(process.env.DATABASE_POOL_MAX ?? \"20\", 10);\n\t// Close idle connections so a fleet of BYO pools doesn't pin connections it\n\t// no longer needs (0 = never; postgres.js default).\n\tconst idleTimeout = Number.parseInt(\n\t\tprocess.env.DATABASE_IDLE_TIMEOUT ?? \"300\",\n\t\t10,\n\t);\n\tconst rawClient = postgres(url, {\n\t\tmax: poolMax,\n\t\tidle_timeout: idleTimeout,\n\t\tssl: isLocal\n\t\t\t? undefined\n\t\t\t: {\n\t\t\t\t\trejectUnauthorized: process.env.NODE_TLS_REJECT_UNAUTHORIZED !== \"0\",\n\t\t\t\t},\n\t});\n\tconst db = new Kysely<Database>({\n\t\tdialect: new PostgresJSDialect({ postgres: rawClient }),\n\t\t// Diagnostic hook: surface the failing SQL whenever postgres rejects\n\t\t// with code 42P10 (ON CONFLICT target doesn't match any unique\n\t\t// constraint). Temporary — remove once we've caught the culprit\n\t\t// query in prod logs and fixed the schema drift.\n\t\tlog: (event) => {\n\t\t\tif (event.level !== \"error\") return;\n\t\t\tconst err = event.error as {\n\t\t\t\tcode?: string;\n\t\t\t\tmessage?: string;\n\t\t\t} | null;\n\t\t\tif (err?.code !== \"42P10\") return;\n\t\t\tlogger.warn(\"db.on_conflict_constraint_missing\", {\n\t\t\t\tcode: err.code,\n\t\t\t\tmessage: err.message,\n\t\t\t\tsql: event.query.sql,\n\t\t\t\tparams: event.query.parameters,\n\t\t\t});\n\t\t},\n\t});\n\tconst entry: PoolEntry = {\n\t\tdb,\n\t\trawClient,\n\t\tlastUsed: Date.now(),\n\t\tseq: poolSeq++,\n\t};\n\tpools.set(url, entry);\n\tevictIfNeeded();\n\treturn entry;\n}\n\n/**\n * Kysely instance for the SOURCE DB (block/tx/event reads from the shared\n * indexer). Resolution: `SOURCE_DATABASE_URL || DATABASE_URL`.\n */\nexport function getSourceDb(): Kysely<Database> {\n\treturn getOrCreatePool(resolveSourceUrl()).db;\n}\n\n/**\n * Kysely instance for the TARGET DB (subgraph schemas, subgraphs table,\n * account-scoped data — tenant-side writes). Resolution:\n * `TARGET_DATABASE_URL || DATABASE_URL`.\n */\nexport function getTargetDb(): Kysely<Database> {\n\treturn getOrCreatePool(resolveTargetUrl()).db;\n}\n\n/**\n * Backward-compat alias for `getTargetDb()`. Accepts an optional\n * `connectionString` override used by seed/test helpers — when supplied,\n * bypasses env resolution and uses the provided URL directly (still cached).\n */\nexport function getDb(connectionString?: string): Kysely<Database> {\n\tif (connectionString) return getOrCreatePool(connectionString).db;\n\treturn getTargetDb();\n}\n\n/**\n * Raw postgres.js client for dynamic schema DDL (CREATE SCHEMA, DROP, etc.).\n * Defaults to the target role (tenant schemas live in the target DB).\n */\nexport function getRawClient(\n\trole: \"source\" | \"target\" = \"target\",\n): ReturnType<typeof postgres> {\n\tconst url = role === \"source\" ? resolveSourceUrl() : resolveTargetUrl();\n\treturn getOrCreatePool(url).rawClient;\n}\n\n/**\n * Raw postgres.js client for an arbitrary connection string (cached by URL).\n * Used by the BYO data plane to run DDL / serving queries against a\n * user-owned Postgres. Distinct from {@link getRawClient}, which only knows the\n * source/target roles resolved from env.\n */\nexport function getRawClientFor(url: string): ReturnType<typeof postgres> {\n\treturn getOrCreatePool(url).rawClient;\n}\n\n/** Close all DB connection pools. Call in CLI commands to allow process exit. */\nexport async function closeDb(): Promise<void> {\n\tfor (const entry of pools.values()) {\n\t\tawait entry.db.destroy();\n\t\tawait entry.rawClient.end();\n\t}\n\tpools.clear();\n}\n\nimport { sql } from \"kysely\";\nexport { sql };\nexport * from \"./types.ts\";\nexport type { DbReadRow, NumericAsText } from \"./read-row.ts\";\nexport {\n\tSOURCE_READ_COLUMNS,\n\tSOURCE_READ_TYPES,\n\tSOURCE_READ_PKS,\n\ttype IndexColumn,\n\ttype IndexColumnType,\n\ttype IndexPrimaryKey,\n} from \"./source-read-columns.ts\";\nexport { jsonb, parseJsonb } from \"./jsonb.ts\";\nexport {\n\tgetMigrationRole,\n\tonChainPlane,\n\tonControlPlane,\n\tsetMigrationRole,\n} from \"./migration-role.ts\";\nexport type { MigrationRole } from \"./migration-role.ts\";\nexport { TABLE_TO_DB } from \"./table-plane.ts\";\nexport type { DbPlane } from \"./table-plane.ts\";\n",
|
|
10
|
-
"import type { Database } from \"./types.ts\";\n\n// The physical source-table columns the public read API (packages/api) actually\n// reads via getSourceDb(). This is the read contract between the indexer (which\n// owns the write schema) and the API (which reads it raw). A column rename or\n// removal on the producer side that isn't reflected here is caught by the\n// snapshot drift test; the `satisfies` clause guarantees every entry is a real\n// table + column today.\n//\n// Computed/aliased read expressions are intentionally excluded — they are not\n// part of the producer contract: `cursor`/`block_time` where derived in SQL\n// rather than stored, window ranks (stream_event_index), and JSONB extractions\n// from `events.data`. Generated bookkeeping columns (created_at/updated_at) are\n// excluded unless a read path selects them.\nexport const SOURCE_READ_COLUMNS = {\n\tblocks: [\n\t\t\"burn_block_hash\",\n\t\t\"burn_block_height\",\n\t\t\"canonical\",\n\t\t\"hash\",\n\t\t\"height\",\n\t\t\"parent_hash\",\n\t\t\"timestamp\",\n\t],\n\tdecoded_events: [\n\t\t\"amount\",\n\t\t\"asset_identifier\",\n\t\t\"block_height\",\n\t\t\"canonical\",\n\t\t\"contract_id\",\n\t\t\"cursor\",\n\t\t\"event_index\",\n\t\t\"event_type\",\n\t\t\"memo\",\n\t\t\"payload\",\n\t\t\"recipient\",\n\t\t\"sender\",\n\t\t\"tx_id\",\n\t\t\"tx_index\",\n\t\t\"value\",\n\t],\n\ttransactions: [\n\t\t\"block_height\",\n\t\t\"contract_id\",\n\t\t\"function_args\",\n\t\t\"function_name\",\n\t\t\"raw_result\",\n\t\t\"raw_tx\",\n\t\t\"sender\",\n\t\t\"status\",\n\t\t\"tx_id\",\n\t\t\"tx_index\",\n\t\t\"type\",\n\t],\n\tmempool_transactions: [\n\t\t\"contract_id\",\n\t\t\"function_args\",\n\t\t\"function_name\",\n\t\t\"raw_tx\",\n\t\t\"received_at\",\n\t\t\"sender\",\n\t\t\"seq\",\n\t\t\"tx_id\",\n\t\t\"type\",\n\t],\n\tpox4_calls: [\n\t\t\"aggregated_amount_ustx\",\n\t\t\"aggregated_signer_index\",\n\t\t\"amount_ustx\",\n\t\t\"auth_allowed\",\n\t\t\"auth_id\",\n\t\t\"auth_period\",\n\t\t\"auth_topic\",\n\t\t\"block_height\",\n\t\t\"block_time\",\n\t\t\"burn_block_height\",\n\t\t\"caller\",\n\t\t\"canonical\",\n\t\t\"cursor\",\n\t\t\"delegate_to\",\n\t\t\"end_cycle\",\n\t\t\"function_name\",\n\t\t\"lock_period\",\n\t\t\"max_amount\",\n\t\t\"pox_addr_btc\",\n\t\t\"pox_addr_hashbytes\",\n\t\t\"pox_addr_version\",\n\t\t\"result_ok\",\n\t\t\"reward_cycle\",\n\t\t\"signer_key\",\n\t\t\"signer_signature\",\n\t\t\"source_cursor\",\n\t\t\"stacker\",\n\t\t\"start_cycle\",\n\t\t\"tx_id\",\n\t\t\"tx_index\",\n\t],\n\tpox5_events: [\n\t\t\"amount_sats\",\n\t\t\"amount_ustx\",\n\t\t\"block_height\",\n\t\t\"block_time\",\n\t\t\"bond_index\",\n\t\t\"canonical\",\n\t\t\"cursor\",\n\t\t\"data\",\n\t\t\"event_index\",\n\t\t\"first_reward_cycle\",\n\t\t\"is_l1_lock\",\n\t\t\"reward_cycle\",\n\t\t\"signer\",\n\t\t\"signer_key\",\n\t\t\"signer_manager\",\n\t\t\"staker\",\n\t\t\"topic\",\n\t\t\"tx_id\",\n\t\t\"tx_index\",\n\t\t\"unlock_burn_height\",\n\t\t\"unlock_cycle\",\n\t],\n\tsbtc_events: [\n\t\t\"amount\",\n\t\t\"bitcoin_txid\",\n\t\t\"block_height\",\n\t\t\"block_time\",\n\t\t\"burn_hash\",\n\t\t\"burn_height\",\n\t\t\"canonical\",\n\t\t\"cursor\",\n\t\t\"event_index\",\n\t\t\"fee\",\n\t\t\"governance_contract_type\",\n\t\t\"governance_new_contract\",\n\t\t\"max_fee\",\n\t\t\"output_index\",\n\t\t\"recipient_btc_hashbytes\",\n\t\t\"recipient_btc_version\",\n\t\t\"request_id\",\n\t\t\"sender\",\n\t\t\"signer_address\",\n\t\t\"signer_aggregate_pubkey\",\n\t\t\"signer_bitmap\",\n\t\t\"signer_keys_count\",\n\t\t\"signer_threshold\",\n\t\t\"sweep_txid\",\n\t\t\"topic\",\n\t\t\"tx_id\",\n\t\t\"tx_index\",\n\t],\n\tsbtc_token_events: [\n\t\t\"amount\",\n\t\t\"block_height\",\n\t\t\"block_time\",\n\t\t\"canonical\",\n\t\t\"cursor\",\n\t\t\"event_index\",\n\t\t\"event_type\",\n\t\t\"memo\",\n\t\t\"recipient\",\n\t\t\"sender\",\n\t\t\"tx_id\",\n\t\t\"tx_index\",\n\t],\n\tbns_name_events: [\n\t\t\"block_height\",\n\t\t\"block_time\",\n\t\t\"bns_id\",\n\t\t\"canonical\",\n\t\t\"cursor\",\n\t\t\"event_index\",\n\t\t\"fqn\",\n\t\t\"hashed_salted_fqn_preorder\",\n\t\t\"imported_at\",\n\t\t\"name\",\n\t\t\"namespace\",\n\t\t\"owner\",\n\t\t\"preordered_by\",\n\t\t\"registered_at\",\n\t\t\"renewal_height\",\n\t\t\"stx_burn\",\n\t\t\"topic\",\n\t\t\"tx_id\",\n\t\t\"tx_index\",\n\t],\n\tbns_namespace_events: [\n\t\t\"block_height\",\n\t\t\"block_time\",\n\t\t\"canonical\",\n\t\t\"cursor\",\n\t\t\"event_index\",\n\t\t\"launched_at\",\n\t\t\"lifetime\",\n\t\t\"manager\",\n\t\t\"manager_frozen\",\n\t\t\"manager_transfers_disabled\",\n\t\t\"namespace\",\n\t\t\"price_frozen\",\n\t\t\"price_function\",\n\t\t\"revealed_at\",\n\t\t\"status\",\n\t\t\"tx_id\",\n\t\t\"tx_index\",\n\t],\n\tbns_marketplace_events: [\n\t\t\"action\",\n\t\t\"block_height\",\n\t\t\"block_time\",\n\t\t\"bns_id\",\n\t\t\"canonical\",\n\t\t\"commission\",\n\t\t\"cursor\",\n\t\t\"event_index\",\n\t\t\"price_ustx\",\n\t\t\"tx_id\",\n\t\t\"tx_index\",\n\t],\n\tbns_names: [\n\t\t\"bns_id\",\n\t\t\"fqn\",\n\t\t\"last_event_at\",\n\t\t\"last_event_cursor\",\n\t\t\"name\",\n\t\t\"namespace\",\n\t\t\"owner\",\n\t\t\"registered_at\",\n\t\t\"renewal_height\",\n\t],\n\tbns_namespaces: [\n\t\t\"last_event_at\",\n\t\t\"last_event_cursor\",\n\t\t\"launched_at\",\n\t\t\"lifetime\",\n\t\t\"manager\",\n\t\t\"manager_frozen\",\n\t\t\"name_count\",\n\t\t\"namespace\",\n\t\t\"price_frozen\",\n\t],\n\tburn_block_rewards: [\n\t\t\"amount_sats\",\n\t\t\"burn_amount\",\n\t\t\"burn_block_hash\",\n\t\t\"burn_block_height\",\n\t\t\"cursor\",\n\t\t\"recipient_btc\",\n\t\t\"reward_index\",\n\t],\n\tburn_block_reward_slots: [\n\t\t\"burn_block_hash\",\n\t\t\"burn_block_height\",\n\t\t\"cursor\",\n\t\t\"holder_btc\",\n\t\t\"slot_index\",\n\t],\n\tevents: [\"block_height\", \"data\", \"event_index\", \"tx_id\", \"type\"],\n\tchain_reorgs: [\"detected_at\"],\n} as const satisfies {\n\treadonly [T in keyof Database]?: readonly (keyof Database[T])[];\n};\n\n/** Portable column type for the Index read domain (a lean, ORM-agnostic vocab). */\nexport type IndexColumnType =\n\t| \"text\"\n\t| \"int\"\n\t| \"boolean\"\n\t| \"timestamp\"\n\t| \"jsonb\";\n\nexport interface IndexColumn {\n\ttype: IndexColumnType;\n\t/** SELECT type includes `| null`. Omitted = NOT NULL. */\n\tnullable?: boolean;\n}\n\n// The portable TS/SQL type for every column in SOURCE_READ_COLUMNS, single-sourced\n// from the `Database` interface (the producer's typed schema). This is what lets\n// `sl index codegen` emit a typed ORM/JSON-Schema for the public Index domain so a\n// BYO database mirror is fully typed and can't drift from the API. The drift test\n// (source-read-columns.test.ts) asserts this covers exactly SOURCE_READ_COLUMNS;\n// the `satisfies` clause guarantees a type entry for every read table.\n//\n// `string` amount/value columns (e.g. amount_ustx, fee, price_ustx) are NUMERIC in\n// Postgres surfaced as strings — `text` here is lossless for a read mirror. `number`\n// columns map to `int`. Enum string-unions (topic, status, …) map to `text`.\nexport const SOURCE_READ_TYPES = {\n\tblocks: {\n\t\tburn_block_hash: { type: \"text\", nullable: true },\n\t\tburn_block_height: { type: \"int\" },\n\t\tcanonical: { type: \"boolean\" },\n\t\thash: { type: \"text\" },\n\t\theight: { type: \"int\" },\n\t\tparent_hash: { type: \"text\" },\n\t\ttimestamp: { type: \"int\" },\n\t},\n\tdecoded_events: {\n\t\tamount: { type: \"text\", nullable: true },\n\t\tasset_identifier: { type: \"text\", nullable: true },\n\t\tblock_height: { type: \"int\" },\n\t\tcanonical: { type: \"boolean\" },\n\t\tcontract_id: { type: \"text\", nullable: true },\n\t\tcursor: { type: \"text\" },\n\t\tevent_index: { type: \"int\" },\n\t\tevent_type: { type: \"text\" },\n\t\tmemo: { type: \"text\", nullable: true },\n\t\tpayload: { type: \"jsonb\", nullable: true },\n\t\trecipient: { type: \"text\", nullable: true },\n\t\tsender: { type: \"text\", nullable: true },\n\t\ttx_id: { type: \"text\" },\n\t\ttx_index: { type: \"int\" },\n\t\tvalue: { type: \"text\", nullable: true },\n\t},\n\ttransactions: {\n\t\tblock_height: { type: \"int\" },\n\t\tcontract_id: { type: \"text\", nullable: true },\n\t\tfunction_args: { type: \"jsonb\", nullable: true },\n\t\tfunction_name: { type: \"text\", nullable: true },\n\t\traw_result: { type: \"text\", nullable: true },\n\t\traw_tx: { type: \"text\" },\n\t\tsender: { type: \"text\" },\n\t\tstatus: { type: \"text\" },\n\t\ttx_id: { type: \"text\" },\n\t\ttx_index: { type: \"int\" },\n\t\ttype: { type: \"text\" },\n\t},\n\tmempool_transactions: {\n\t\tcontract_id: { type: \"text\", nullable: true },\n\t\tfunction_args: { type: \"jsonb\", nullable: true },\n\t\tfunction_name: { type: \"text\", nullable: true },\n\t\traw_tx: { type: \"text\" },\n\t\treceived_at: { type: \"timestamp\" },\n\t\tsender: { type: \"text\" },\n\t\tseq: { type: \"text\" },\n\t\ttx_id: { type: \"text\" },\n\t\ttype: { type: \"text\" },\n\t},\n\tpox4_calls: {\n\t\taggregated_amount_ustx: { type: \"text\", nullable: true },\n\t\taggregated_signer_index: { type: \"int\", nullable: true },\n\t\tamount_ustx: { type: \"text\", nullable: true },\n\t\tauth_allowed: { type: \"boolean\", nullable: true },\n\t\tauth_id: { type: \"text\", nullable: true },\n\t\tauth_period: { type: \"int\", nullable: true },\n\t\tauth_topic: { type: \"text\", nullable: true },\n\t\tblock_height: { type: \"int\" },\n\t\tblock_time: { type: \"timestamp\" },\n\t\tburn_block_height: { type: \"int\" },\n\t\tcaller: { type: \"text\" },\n\t\tcanonical: { type: \"boolean\" },\n\t\tcursor: { type: \"text\" },\n\t\tdelegate_to: { type: \"text\", nullable: true },\n\t\tend_cycle: { type: \"int\", nullable: true },\n\t\tfunction_name: { type: \"text\" },\n\t\tlock_period: { type: \"int\", nullable: true },\n\t\tmax_amount: { type: \"text\", nullable: true },\n\t\tpox_addr_btc: { type: \"text\", nullable: true },\n\t\tpox_addr_hashbytes: { type: \"text\", nullable: true },\n\t\tpox_addr_version: { type: \"int\", nullable: true },\n\t\tresult_ok: { type: \"boolean\" },\n\t\treward_cycle: { type: \"int\", nullable: true },\n\t\tsigner_key: { type: \"text\", nullable: true },\n\t\tsigner_signature: { type: \"text\", nullable: true },\n\t\tsource_cursor: { type: \"text\" },\n\t\tstacker: { type: \"text\", nullable: true },\n\t\tstart_cycle: { type: \"int\", nullable: true },\n\t\ttx_id: { type: \"text\" },\n\t\ttx_index: { type: \"int\" },\n\t},\n\tpox5_events: {\n\t\tamount_sats: { type: \"text\", nullable: true },\n\t\tamount_ustx: { type: \"text\", nullable: true },\n\t\tblock_height: { type: \"int\" },\n\t\tblock_time: { type: \"timestamp\" },\n\t\tbond_index: { type: \"int\", nullable: true },\n\t\tcanonical: { type: \"boolean\" },\n\t\tcursor: { type: \"text\" },\n\t\tdata: { type: \"jsonb\", nullable: true },\n\t\tevent_index: { type: \"int\" },\n\t\tfirst_reward_cycle: { type: \"int\", nullable: true },\n\t\tis_l1_lock: { type: \"boolean\", nullable: true },\n\t\treward_cycle: { type: \"int\", nullable: true },\n\t\tsigner: { type: \"text\", nullable: true },\n\t\tsigner_key: { type: \"text\", nullable: true },\n\t\tsigner_manager: { type: \"text\", nullable: true },\n\t\tstaker: { type: \"text\", nullable: true },\n\t\ttopic: { type: \"text\" },\n\t\ttx_id: { type: \"text\" },\n\t\ttx_index: { type: \"int\" },\n\t\tunlock_burn_height: { type: \"int\", nullable: true },\n\t\tunlock_cycle: { type: \"int\", nullable: true },\n\t},\n\tsbtc_events: {\n\t\tamount: { type: \"text\", nullable: true },\n\t\tbitcoin_txid: { type: \"text\", nullable: true },\n\t\tblock_height: { type: \"int\" },\n\t\tblock_time: { type: \"timestamp\" },\n\t\tburn_hash: { type: \"text\", nullable: true },\n\t\tburn_height: { type: \"int\", nullable: true },\n\t\tcanonical: { type: \"boolean\" },\n\t\tcursor: { type: \"text\" },\n\t\tevent_index: { type: \"int\" },\n\t\tfee: { type: \"text\", nullable: true },\n\t\tgovernance_contract_type: { type: \"int\", nullable: true },\n\t\tgovernance_new_contract: { type: \"text\", nullable: true },\n\t\tmax_fee: { type: \"text\", nullable: true },\n\t\toutput_index: { type: \"int\", nullable: true },\n\t\trecipient_btc_hashbytes: { type: \"text\", nullable: true },\n\t\trecipient_btc_version: { type: \"int\", nullable: true },\n\t\trequest_id: { type: \"int\", nullable: true },\n\t\tsender: { type: \"text\", nullable: true },\n\t\tsigner_address: { type: \"text\", nullable: true },\n\t\tsigner_aggregate_pubkey: { type: \"text\", nullable: true },\n\t\tsigner_bitmap: { type: \"text\", nullable: true },\n\t\tsigner_keys_count: { type: \"int\", nullable: true },\n\t\tsigner_threshold: { type: \"int\", nullable: true },\n\t\tsweep_txid: { type: \"text\", nullable: true },\n\t\ttopic: { type: \"text\" },\n\t\ttx_id: { type: \"text\" },\n\t\ttx_index: { type: \"int\" },\n\t},\n\tsbtc_token_events: {\n\t\tamount: { type: \"text\" },\n\t\tblock_height: { type: \"int\" },\n\t\tblock_time: { type: \"timestamp\" },\n\t\tcanonical: { type: \"boolean\" },\n\t\tcursor: { type: \"text\" },\n\t\tevent_index: { type: \"int\" },\n\t\tevent_type: { type: \"text\" },\n\t\tmemo: { type: \"text\", nullable: true },\n\t\trecipient: { type: \"text\", nullable: true },\n\t\tsender: { type: \"text\", nullable: true },\n\t\ttx_id: { type: \"text\" },\n\t\ttx_index: { type: \"int\" },\n\t},\n\tbns_name_events: {\n\t\tblock_height: { type: \"int\" },\n\t\tblock_time: { type: \"timestamp\" },\n\t\tbns_id: { type: \"text\" },\n\t\tcanonical: { type: \"boolean\" },\n\t\tcursor: { type: \"text\" },\n\t\tevent_index: { type: \"int\" },\n\t\tfqn: { type: \"text\" },\n\t\thashed_salted_fqn_preorder: { type: \"text\", nullable: true },\n\t\timported_at: { type: \"int\", nullable: true },\n\t\tname: { type: \"text\" },\n\t\tnamespace: { type: \"text\" },\n\t\towner: { type: \"text\", nullable: true },\n\t\tpreordered_by: { type: \"text\", nullable: true },\n\t\tregistered_at: { type: \"int\", nullable: true },\n\t\trenewal_height: { type: \"int\", nullable: true },\n\t\tstx_burn: { type: \"text\", nullable: true },\n\t\ttopic: { type: \"text\" },\n\t\ttx_id: { type: \"text\" },\n\t\ttx_index: { type: \"int\" },\n\t},\n\tbns_namespace_events: {\n\t\tblock_height: { type: \"int\" },\n\t\tblock_time: { type: \"timestamp\" },\n\t\tcanonical: { type: \"boolean\" },\n\t\tcursor: { type: \"text\" },\n\t\tevent_index: { type: \"int\" },\n\t\tlaunched_at: { type: \"int\", nullable: true },\n\t\tlifetime: { type: \"int\", nullable: true },\n\t\tmanager: { type: \"text\", nullable: true },\n\t\tmanager_frozen: { type: \"boolean\", nullable: true },\n\t\tmanager_transfers_disabled: { type: \"boolean\", nullable: true },\n\t\tnamespace: { type: \"text\" },\n\t\tprice_frozen: { type: \"boolean\", nullable: true },\n\t\tprice_function: { type: \"text\", nullable: true },\n\t\trevealed_at: { type: \"int\", nullable: true },\n\t\tstatus: { type: \"text\" },\n\t\ttx_id: { type: \"text\" },\n\t\ttx_index: { type: \"int\" },\n\t},\n\tbns_marketplace_events: {\n\t\taction: { type: \"text\" },\n\t\tblock_height: { type: \"int\" },\n\t\tblock_time: { type: \"timestamp\" },\n\t\tbns_id: { type: \"text\" },\n\t\tcanonical: { type: \"boolean\" },\n\t\tcommission: { type: \"text\", nullable: true },\n\t\tcursor: { type: \"text\" },\n\t\tevent_index: { type: \"int\" },\n\t\tprice_ustx: { type: \"text\", nullable: true },\n\t\ttx_id: { type: \"text\" },\n\t\ttx_index: { type: \"int\" },\n\t},\n\tbns_names: {\n\t\tbns_id: { type: \"text\" },\n\t\tfqn: { type: \"text\" },\n\t\tlast_event_at: { type: \"timestamp\" },\n\t\tlast_event_cursor: { type: \"text\" },\n\t\tname: { type: \"text\" },\n\t\tnamespace: { type: \"text\" },\n\t\towner: { type: \"text\" },\n\t\tregistered_at: { type: \"int\", nullable: true },\n\t\trenewal_height: { type: \"int\", nullable: true },\n\t},\n\tbns_namespaces: {\n\t\tlast_event_at: { type: \"timestamp\" },\n\t\tlast_event_cursor: { type: \"text\" },\n\t\tlaunched_at: { type: \"int\", nullable: true },\n\t\tlifetime: { type: \"int\", nullable: true },\n\t\tmanager: { type: \"text\", nullable: true },\n\t\tmanager_frozen: { type: \"boolean\" },\n\t\tname_count: { type: \"int\" },\n\t\tnamespace: { type: \"text\" },\n\t\tprice_frozen: { type: \"boolean\" },\n\t},\n\tburn_block_rewards: {\n\t\tamount_sats: { type: \"text\" },\n\t\tburn_amount: { type: \"text\" },\n\t\tburn_block_hash: { type: \"text\" },\n\t\tburn_block_height: { type: \"int\" },\n\t\tcursor: { type: \"text\" },\n\t\trecipient_btc: { type: \"text\" },\n\t\treward_index: { type: \"int\" },\n\t},\n\tburn_block_reward_slots: {\n\t\tburn_block_hash: { type: \"text\" },\n\t\tburn_block_height: { type: \"int\" },\n\t\tcursor: { type: \"text\" },\n\t\tholder_btc: { type: \"text\" },\n\t\tslot_index: { type: \"int\" },\n\t},\n\tevents: {\n\t\tblock_height: { type: \"int\" },\n\t\tdata: { type: \"jsonb\" },\n\t\tevent_index: { type: \"int\" },\n\t\ttx_id: { type: \"text\" },\n\t\ttype: { type: \"text\" },\n\t},\n\tchain_reorgs: {\n\t\tdetected_at: { type: \"timestamp\" },\n\t},\n} as const satisfies Record<\n\tkeyof typeof SOURCE_READ_COLUMNS,\n\tRecord<string, IndexColumn>\n>;\n\nexport type IndexPrimaryKey = readonly string[] | null;\n\n// Primary key per read table, for codegen targets that require a model identity\n// (Prisma). Every listed column must be a read column (drift-tested). `null` = no\n// usable read-column key — the physical PK is a synthetic id excluded from the\n// read contract (chain_reorgs) — so Prisma omits the table. `events` uses its\n// natural unique (tx_id, event_index); its physical PK is a synthetic uuid not\n// surfaced in the read set.\nexport const SOURCE_READ_PKS = {\n\tblocks: [\"height\"],\n\tdecoded_events: [\"cursor\"],\n\ttransactions: [\"tx_id\"],\n\tmempool_transactions: [\"seq\"],\n\tpox4_calls: [\"cursor\"],\n\tpox5_events: [\"cursor\"],\n\tsbtc_events: [\"cursor\"],\n\tsbtc_token_events: [\"cursor\"],\n\tbns_name_events: [\"cursor\"],\n\tbns_namespace_events: [\"cursor\"],\n\tbns_marketplace_events: [\"cursor\"],\n\tbns_names: [\"fqn\"],\n\tbns_namespaces: [\"namespace\"],\n\tburn_block_rewards: [\"cursor\"],\n\tburn_block_reward_slots: [\"cursor\"],\n\tevents: [\"tx_id\", \"event_index\"],\n\tchain_reorgs: null,\n} as const satisfies Record<keyof typeof SOURCE_READ_COLUMNS, IndexPrimaryKey>;\n",
|
|
10
|
+
"import type { Database } from \"./types.ts\";\n\n// The physical source-table columns the public read API (packages/api) actually\n// reads via getSourceDb(). This is the read contract between the indexer (which\n// owns the write schema) and the API (which reads it raw). A column rename or\n// removal on the producer side that isn't reflected here is caught by the\n// snapshot drift test; the `satisfies` clause guarantees every entry is a real\n// table + column today.\n//\n// Computed/aliased read expressions are intentionally excluded — they are not\n// part of the producer contract: `cursor`/`block_time` where derived in SQL\n// rather than stored, window ranks (stream_event_index), and JSONB extractions\n// from `events.data`. Generated bookkeeping columns (created_at/updated_at) are\n// excluded unless a read path selects them.\nexport const SOURCE_READ_COLUMNS = {\n\tblocks: [\n\t\t\"burn_block_hash\",\n\t\t\"burn_block_height\",\n\t\t\"canonical\",\n\t\t\"hash\",\n\t\t\"height\",\n\t\t\"parent_hash\",\n\t\t\"timestamp\",\n\t],\n\tdecoded_events: [\n\t\t\"amount\",\n\t\t\"asset_identifier\",\n\t\t\"block_height\",\n\t\t\"canonical\",\n\t\t\"contract_id\",\n\t\t\"cursor\",\n\t\t\"event_index\",\n\t\t\"event_type\",\n\t\t\"memo\",\n\t\t\"payload\",\n\t\t\"recipient\",\n\t\t\"sender\",\n\t\t\"tx_id\",\n\t\t\"tx_index\",\n\t\t\"value\",\n\t],\n\ttransactions: [\n\t\t\"block_height\",\n\t\t\"contract_id\",\n\t\t\"function_args\",\n\t\t\"function_name\",\n\t\t\"raw_result\",\n\t\t\"raw_tx\",\n\t\t\"sender\",\n\t\t\"status\",\n\t\t\"tx_id\",\n\t\t\"tx_index\",\n\t\t\"type\",\n\t],\n\tmempool_transactions: [\n\t\t\"contract_id\",\n\t\t\"function_args\",\n\t\t\"function_name\",\n\t\t\"raw_tx\",\n\t\t\"received_at\",\n\t\t\"sender\",\n\t\t\"seq\",\n\t\t\"tx_id\",\n\t\t\"type\",\n\t],\n\tpox4_calls: [\n\t\t\"aggregated_amount_ustx\",\n\t\t\"aggregated_signer_index\",\n\t\t\"amount_ustx\",\n\t\t\"auth_allowed\",\n\t\t\"auth_id\",\n\t\t\"auth_period\",\n\t\t\"auth_topic\",\n\t\t\"block_height\",\n\t\t\"block_time\",\n\t\t\"burn_block_height\",\n\t\t\"caller\",\n\t\t\"canonical\",\n\t\t\"cursor\",\n\t\t\"delegate_to\",\n\t\t\"end_cycle\",\n\t\t\"function_name\",\n\t\t\"lock_period\",\n\t\t\"max_amount\",\n\t\t\"pox_addr_btc\",\n\t\t\"pox_addr_hashbytes\",\n\t\t\"pox_addr_version\",\n\t\t\"result_ok\",\n\t\t\"reward_cycle\",\n\t\t\"signer_key\",\n\t\t\"signer_signature\",\n\t\t\"source_cursor\",\n\t\t\"stacker\",\n\t\t\"start_cycle\",\n\t\t\"tx_id\",\n\t\t\"tx_index\",\n\t],\n\tpox5_events: [\n\t\t\"amount_sats\",\n\t\t\"amount_ustx\",\n\t\t\"block_height\",\n\t\t\"block_time\",\n\t\t\"bond_index\",\n\t\t\"canonical\",\n\t\t\"cursor\",\n\t\t\"data\",\n\t\t\"event_index\",\n\t\t\"first_reward_cycle\",\n\t\t\"is_l1_lock\",\n\t\t\"reward_cycle\",\n\t\t\"signer\",\n\t\t\"signer_key\",\n\t\t\"signer_manager\",\n\t\t\"staker\",\n\t\t\"topic\",\n\t\t\"tx_id\",\n\t\t\"tx_index\",\n\t\t\"unlock_burn_height\",\n\t\t\"unlock_cycle\",\n\t],\n\tsbtc_events: [\n\t\t\"amount\",\n\t\t\"bitcoin_txid\",\n\t\t\"block_height\",\n\t\t\"block_time\",\n\t\t\"burn_hash\",\n\t\t\"burn_height\",\n\t\t\"canonical\",\n\t\t\"cursor\",\n\t\t\"event_index\",\n\t\t\"fee\",\n\t\t\"governance_contract_type\",\n\t\t\"governance_new_contract\",\n\t\t\"max_fee\",\n\t\t\"output_index\",\n\t\t\"recipient_btc_hashbytes\",\n\t\t\"recipient_btc_version\",\n\t\t\"request_id\",\n\t\t\"sender\",\n\t\t\"signer_address\",\n\t\t\"signer_aggregate_pubkey\",\n\t\t\"signer_bitmap\",\n\t\t\"signer_keys_count\",\n\t\t\"signer_threshold\",\n\t\t\"sweep_txid\",\n\t\t\"topic\",\n\t\t\"tx_id\",\n\t\t\"tx_index\",\n\t],\n\tsbtc_token_events: [\n\t\t\"amount\",\n\t\t\"block_height\",\n\t\t\"block_time\",\n\t\t\"canonical\",\n\t\t\"cursor\",\n\t\t\"event_index\",\n\t\t\"event_type\",\n\t\t\"memo\",\n\t\t\"recipient\",\n\t\t\"sender\",\n\t\t\"tx_id\",\n\t\t\"tx_index\",\n\t],\n\tbns_name_events: [\n\t\t\"block_height\",\n\t\t\"block_time\",\n\t\t\"bns_id\",\n\t\t\"canonical\",\n\t\t\"cursor\",\n\t\t\"event_index\",\n\t\t\"fqn\",\n\t\t\"hashed_salted_fqn_preorder\",\n\t\t\"imported_at\",\n\t\t\"name\",\n\t\t\"namespace\",\n\t\t\"owner\",\n\t\t\"preordered_by\",\n\t\t\"registered_at\",\n\t\t\"renewal_height\",\n\t\t\"stx_burn\",\n\t\t\"topic\",\n\t\t\"tx_id\",\n\t\t\"tx_index\",\n\t],\n\tbns_namespace_events: [\n\t\t\"block_height\",\n\t\t\"block_time\",\n\t\t\"canonical\",\n\t\t\"cursor\",\n\t\t\"event_index\",\n\t\t\"launched_at\",\n\t\t\"lifetime\",\n\t\t\"manager\",\n\t\t\"manager_frozen\",\n\t\t\"manager_transfers_disabled\",\n\t\t\"namespace\",\n\t\t\"price_frozen\",\n\t\t\"price_function\",\n\t\t\"revealed_at\",\n\t\t\"status\",\n\t\t\"tx_id\",\n\t\t\"tx_index\",\n\t],\n\tbns_marketplace_events: [\n\t\t\"action\",\n\t\t\"block_height\",\n\t\t\"block_time\",\n\t\t\"bns_id\",\n\t\t\"canonical\",\n\t\t\"commission\",\n\t\t\"cursor\",\n\t\t\"event_index\",\n\t\t\"price_ustx\",\n\t\t\"tx_id\",\n\t\t\"tx_index\",\n\t],\n\tbns_names: [\n\t\t\"bns_id\",\n\t\t\"fqn\",\n\t\t\"last_event_at\",\n\t\t\"last_event_cursor\",\n\t\t\"name\",\n\t\t\"namespace\",\n\t\t\"owner\",\n\t\t\"registered_at\",\n\t\t\"renewal_height\",\n\t],\n\tbns_namespaces: [\n\t\t\"last_event_at\",\n\t\t\"last_event_cursor\",\n\t\t\"launched_at\",\n\t\t\"lifetime\",\n\t\t\"manager\",\n\t\t\"manager_frozen\",\n\t\t\"name_count\",\n\t\t\"namespace\",\n\t\t\"price_frozen\",\n\t],\n\tburn_block_rewards: [\n\t\t\"amount_sats\",\n\t\t\"burn_amount\",\n\t\t\"burn_block_hash\",\n\t\t\"burn_block_height\",\n\t\t\"cursor\",\n\t\t\"recipient_btc\",\n\t\t\"reward_index\",\n\t],\n\tburn_block_reward_slots: [\n\t\t\"burn_block_hash\",\n\t\t\"burn_block_height\",\n\t\t\"cursor\",\n\t\t\"holder_btc\",\n\t\t\"slot_index\",\n\t],\n\tevents: [\"block_height\", \"data\", \"event_index\", \"tx_id\", \"type\"],\n\tchain_reorgs: [\"detected_at\"],\n} as const satisfies {\n\treadonly [T in keyof Database]?: readonly (keyof Database[T])[];\n};\n\n/** Portable column type for the Index read domain (a lean, ORM-agnostic vocab). */\nexport type IndexColumnType =\n\t| \"text\"\n\t| \"int\"\n\t| \"boolean\"\n\t| \"timestamp\"\n\t| \"jsonb\";\n\nexport interface IndexColumn {\n\ttype: IndexColumnType;\n\t/** SELECT type includes `| null`. Omitted = NOT NULL. */\n\tnullable?: boolean;\n}\n\n// The portable TS/SQL type for every column in SOURCE_READ_COLUMNS, single-sourced\n// from the `Database` interface (the producer's typed schema). This is what lets\n// `sl codegen index` emit a typed ORM/JSON-Schema for the public Index domain so a\n// BYO database mirror is fully typed and can't drift from the API. The drift test\n// (source-read-columns.test.ts) asserts this covers exactly SOURCE_READ_COLUMNS;\n// the `satisfies` clause guarantees a type entry for every read table.\n//\n// `string` amount/value columns (e.g. amount_ustx, fee, price_ustx) are NUMERIC in\n// Postgres surfaced as strings — `text` here is lossless for a read mirror. `number`\n// columns map to `int`. Enum string-unions (topic, status, …) map to `text`.\nexport const SOURCE_READ_TYPES = {\n\tblocks: {\n\t\tburn_block_hash: { type: \"text\", nullable: true },\n\t\tburn_block_height: { type: \"int\" },\n\t\tcanonical: { type: \"boolean\" },\n\t\thash: { type: \"text\" },\n\t\theight: { type: \"int\" },\n\t\tparent_hash: { type: \"text\" },\n\t\ttimestamp: { type: \"int\" },\n\t},\n\tdecoded_events: {\n\t\tamount: { type: \"text\", nullable: true },\n\t\tasset_identifier: { type: \"text\", nullable: true },\n\t\tblock_height: { type: \"int\" },\n\t\tcanonical: { type: \"boolean\" },\n\t\tcontract_id: { type: \"text\", nullable: true },\n\t\tcursor: { type: \"text\" },\n\t\tevent_index: { type: \"int\" },\n\t\tevent_type: { type: \"text\" },\n\t\tmemo: { type: \"text\", nullable: true },\n\t\tpayload: { type: \"jsonb\", nullable: true },\n\t\trecipient: { type: \"text\", nullable: true },\n\t\tsender: { type: \"text\", nullable: true },\n\t\ttx_id: { type: \"text\" },\n\t\ttx_index: { type: \"int\" },\n\t\tvalue: { type: \"text\", nullable: true },\n\t},\n\ttransactions: {\n\t\tblock_height: { type: \"int\" },\n\t\tcontract_id: { type: \"text\", nullable: true },\n\t\tfunction_args: { type: \"jsonb\", nullable: true },\n\t\tfunction_name: { type: \"text\", nullable: true },\n\t\traw_result: { type: \"text\", nullable: true },\n\t\traw_tx: { type: \"text\" },\n\t\tsender: { type: \"text\" },\n\t\tstatus: { type: \"text\" },\n\t\ttx_id: { type: \"text\" },\n\t\ttx_index: { type: \"int\" },\n\t\ttype: { type: \"text\" },\n\t},\n\tmempool_transactions: {\n\t\tcontract_id: { type: \"text\", nullable: true },\n\t\tfunction_args: { type: \"jsonb\", nullable: true },\n\t\tfunction_name: { type: \"text\", nullable: true },\n\t\traw_tx: { type: \"text\" },\n\t\treceived_at: { type: \"timestamp\" },\n\t\tsender: { type: \"text\" },\n\t\tseq: { type: \"text\" },\n\t\ttx_id: { type: \"text\" },\n\t\ttype: { type: \"text\" },\n\t},\n\tpox4_calls: {\n\t\taggregated_amount_ustx: { type: \"text\", nullable: true },\n\t\taggregated_signer_index: { type: \"int\", nullable: true },\n\t\tamount_ustx: { type: \"text\", nullable: true },\n\t\tauth_allowed: { type: \"boolean\", nullable: true },\n\t\tauth_id: { type: \"text\", nullable: true },\n\t\tauth_period: { type: \"int\", nullable: true },\n\t\tauth_topic: { type: \"text\", nullable: true },\n\t\tblock_height: { type: \"int\" },\n\t\tblock_time: { type: \"timestamp\" },\n\t\tburn_block_height: { type: \"int\" },\n\t\tcaller: { type: \"text\" },\n\t\tcanonical: { type: \"boolean\" },\n\t\tcursor: { type: \"text\" },\n\t\tdelegate_to: { type: \"text\", nullable: true },\n\t\tend_cycle: { type: \"int\", nullable: true },\n\t\tfunction_name: { type: \"text\" },\n\t\tlock_period: { type: \"int\", nullable: true },\n\t\tmax_amount: { type: \"text\", nullable: true },\n\t\tpox_addr_btc: { type: \"text\", nullable: true },\n\t\tpox_addr_hashbytes: { type: \"text\", nullable: true },\n\t\tpox_addr_version: { type: \"int\", nullable: true },\n\t\tresult_ok: { type: \"boolean\" },\n\t\treward_cycle: { type: \"int\", nullable: true },\n\t\tsigner_key: { type: \"text\", nullable: true },\n\t\tsigner_signature: { type: \"text\", nullable: true },\n\t\tsource_cursor: { type: \"text\" },\n\t\tstacker: { type: \"text\", nullable: true },\n\t\tstart_cycle: { type: \"int\", nullable: true },\n\t\ttx_id: { type: \"text\" },\n\t\ttx_index: { type: \"int\" },\n\t},\n\tpox5_events: {\n\t\tamount_sats: { type: \"text\", nullable: true },\n\t\tamount_ustx: { type: \"text\", nullable: true },\n\t\tblock_height: { type: \"int\" },\n\t\tblock_time: { type: \"timestamp\" },\n\t\tbond_index: { type: \"int\", nullable: true },\n\t\tcanonical: { type: \"boolean\" },\n\t\tcursor: { type: \"text\" },\n\t\tdata: { type: \"jsonb\", nullable: true },\n\t\tevent_index: { type: \"int\" },\n\t\tfirst_reward_cycle: { type: \"int\", nullable: true },\n\t\tis_l1_lock: { type: \"boolean\", nullable: true },\n\t\treward_cycle: { type: \"int\", nullable: true },\n\t\tsigner: { type: \"text\", nullable: true },\n\t\tsigner_key: { type: \"text\", nullable: true },\n\t\tsigner_manager: { type: \"text\", nullable: true },\n\t\tstaker: { type: \"text\", nullable: true },\n\t\ttopic: { type: \"text\" },\n\t\ttx_id: { type: \"text\" },\n\t\ttx_index: { type: \"int\" },\n\t\tunlock_burn_height: { type: \"int\", nullable: true },\n\t\tunlock_cycle: { type: \"int\", nullable: true },\n\t},\n\tsbtc_events: {\n\t\tamount: { type: \"text\", nullable: true },\n\t\tbitcoin_txid: { type: \"text\", nullable: true },\n\t\tblock_height: { type: \"int\" },\n\t\tblock_time: { type: \"timestamp\" },\n\t\tburn_hash: { type: \"text\", nullable: true },\n\t\tburn_height: { type: \"int\", nullable: true },\n\t\tcanonical: { type: \"boolean\" },\n\t\tcursor: { type: \"text\" },\n\t\tevent_index: { type: \"int\" },\n\t\tfee: { type: \"text\", nullable: true },\n\t\tgovernance_contract_type: { type: \"int\", nullable: true },\n\t\tgovernance_new_contract: { type: \"text\", nullable: true },\n\t\tmax_fee: { type: \"text\", nullable: true },\n\t\toutput_index: { type: \"int\", nullable: true },\n\t\trecipient_btc_hashbytes: { type: \"text\", nullable: true },\n\t\trecipient_btc_version: { type: \"int\", nullable: true },\n\t\trequest_id: { type: \"int\", nullable: true },\n\t\tsender: { type: \"text\", nullable: true },\n\t\tsigner_address: { type: \"text\", nullable: true },\n\t\tsigner_aggregate_pubkey: { type: \"text\", nullable: true },\n\t\tsigner_bitmap: { type: \"text\", nullable: true },\n\t\tsigner_keys_count: { type: \"int\", nullable: true },\n\t\tsigner_threshold: { type: \"int\", nullable: true },\n\t\tsweep_txid: { type: \"text\", nullable: true },\n\t\ttopic: { type: \"text\" },\n\t\ttx_id: { type: \"text\" },\n\t\ttx_index: { type: \"int\" },\n\t},\n\tsbtc_token_events: {\n\t\tamount: { type: \"text\" },\n\t\tblock_height: { type: \"int\" },\n\t\tblock_time: { type: \"timestamp\" },\n\t\tcanonical: { type: \"boolean\" },\n\t\tcursor: { type: \"text\" },\n\t\tevent_index: { type: \"int\" },\n\t\tevent_type: { type: \"text\" },\n\t\tmemo: { type: \"text\", nullable: true },\n\t\trecipient: { type: \"text\", nullable: true },\n\t\tsender: { type: \"text\", nullable: true },\n\t\ttx_id: { type: \"text\" },\n\t\ttx_index: { type: \"int\" },\n\t},\n\tbns_name_events: {\n\t\tblock_height: { type: \"int\" },\n\t\tblock_time: { type: \"timestamp\" },\n\t\tbns_id: { type: \"text\" },\n\t\tcanonical: { type: \"boolean\" },\n\t\tcursor: { type: \"text\" },\n\t\tevent_index: { type: \"int\" },\n\t\tfqn: { type: \"text\" },\n\t\thashed_salted_fqn_preorder: { type: \"text\", nullable: true },\n\t\timported_at: { type: \"int\", nullable: true },\n\t\tname: { type: \"text\" },\n\t\tnamespace: { type: \"text\" },\n\t\towner: { type: \"text\", nullable: true },\n\t\tpreordered_by: { type: \"text\", nullable: true },\n\t\tregistered_at: { type: \"int\", nullable: true },\n\t\trenewal_height: { type: \"int\", nullable: true },\n\t\tstx_burn: { type: \"text\", nullable: true },\n\t\ttopic: { type: \"text\" },\n\t\ttx_id: { type: \"text\" },\n\t\ttx_index: { type: \"int\" },\n\t},\n\tbns_namespace_events: {\n\t\tblock_height: { type: \"int\" },\n\t\tblock_time: { type: \"timestamp\" },\n\t\tcanonical: { type: \"boolean\" },\n\t\tcursor: { type: \"text\" },\n\t\tevent_index: { type: \"int\" },\n\t\tlaunched_at: { type: \"int\", nullable: true },\n\t\tlifetime: { type: \"int\", nullable: true },\n\t\tmanager: { type: \"text\", nullable: true },\n\t\tmanager_frozen: { type: \"boolean\", nullable: true },\n\t\tmanager_transfers_disabled: { type: \"boolean\", nullable: true },\n\t\tnamespace: { type: \"text\" },\n\t\tprice_frozen: { type: \"boolean\", nullable: true },\n\t\tprice_function: { type: \"text\", nullable: true },\n\t\trevealed_at: { type: \"int\", nullable: true },\n\t\tstatus: { type: \"text\" },\n\t\ttx_id: { type: \"text\" },\n\t\ttx_index: { type: \"int\" },\n\t},\n\tbns_marketplace_events: {\n\t\taction: { type: \"text\" },\n\t\tblock_height: { type: \"int\" },\n\t\tblock_time: { type: \"timestamp\" },\n\t\tbns_id: { type: \"text\" },\n\t\tcanonical: { type: \"boolean\" },\n\t\tcommission: { type: \"text\", nullable: true },\n\t\tcursor: { type: \"text\" },\n\t\tevent_index: { type: \"int\" },\n\t\tprice_ustx: { type: \"text\", nullable: true },\n\t\ttx_id: { type: \"text\" },\n\t\ttx_index: { type: \"int\" },\n\t},\n\tbns_names: {\n\t\tbns_id: { type: \"text\" },\n\t\tfqn: { type: \"text\" },\n\t\tlast_event_at: { type: \"timestamp\" },\n\t\tlast_event_cursor: { type: \"text\" },\n\t\tname: { type: \"text\" },\n\t\tnamespace: { type: \"text\" },\n\t\towner: { type: \"text\" },\n\t\tregistered_at: { type: \"int\", nullable: true },\n\t\trenewal_height: { type: \"int\", nullable: true },\n\t},\n\tbns_namespaces: {\n\t\tlast_event_at: { type: \"timestamp\" },\n\t\tlast_event_cursor: { type: \"text\" },\n\t\tlaunched_at: { type: \"int\", nullable: true },\n\t\tlifetime: { type: \"int\", nullable: true },\n\t\tmanager: { type: \"text\", nullable: true },\n\t\tmanager_frozen: { type: \"boolean\" },\n\t\tname_count: { type: \"int\" },\n\t\tnamespace: { type: \"text\" },\n\t\tprice_frozen: { type: \"boolean\" },\n\t},\n\tburn_block_rewards: {\n\t\tamount_sats: { type: \"text\" },\n\t\tburn_amount: { type: \"text\" },\n\t\tburn_block_hash: { type: \"text\" },\n\t\tburn_block_height: { type: \"int\" },\n\t\tcursor: { type: \"text\" },\n\t\trecipient_btc: { type: \"text\" },\n\t\treward_index: { type: \"int\" },\n\t},\n\tburn_block_reward_slots: {\n\t\tburn_block_hash: { type: \"text\" },\n\t\tburn_block_height: { type: \"int\" },\n\t\tcursor: { type: \"text\" },\n\t\tholder_btc: { type: \"text\" },\n\t\tslot_index: { type: \"int\" },\n\t},\n\tevents: {\n\t\tblock_height: { type: \"int\" },\n\t\tdata: { type: \"jsonb\" },\n\t\tevent_index: { type: \"int\" },\n\t\ttx_id: { type: \"text\" },\n\t\ttype: { type: \"text\" },\n\t},\n\tchain_reorgs: {\n\t\tdetected_at: { type: \"timestamp\" },\n\t},\n} as const satisfies Record<\n\tkeyof typeof SOURCE_READ_COLUMNS,\n\tRecord<string, IndexColumn>\n>;\n\nexport type IndexPrimaryKey = readonly string[] | null;\n\n// Primary key per read table, for codegen targets that require a model identity\n// (Prisma). Every listed column must be a read column (drift-tested). `null` = no\n// usable read-column key — the physical PK is a synthetic id excluded from the\n// read contract (chain_reorgs) — so Prisma omits the table. `events` uses its\n// natural unique (tx_id, event_index); its physical PK is a synthetic uuid not\n// surfaced in the read set.\nexport const SOURCE_READ_PKS = {\n\tblocks: [\"height\"],\n\tdecoded_events: [\"cursor\"],\n\ttransactions: [\"tx_id\"],\n\tmempool_transactions: [\"seq\"],\n\tpox4_calls: [\"cursor\"],\n\tpox5_events: [\"cursor\"],\n\tsbtc_events: [\"cursor\"],\n\tsbtc_token_events: [\"cursor\"],\n\tbns_name_events: [\"cursor\"],\n\tbns_namespace_events: [\"cursor\"],\n\tbns_marketplace_events: [\"cursor\"],\n\tbns_names: [\"fqn\"],\n\tbns_namespaces: [\"namespace\"],\n\tburn_block_rewards: [\"cursor\"],\n\tburn_block_reward_slots: [\"cursor\"],\n\tevents: [\"tx_id\", \"event_index\"],\n\tchain_reorgs: null,\n} as const satisfies Record<keyof typeof SOURCE_READ_COLUMNS, IndexPrimaryKey>;\n",
|
|
11
11
|
"/**\n * Per-migration DB-plane gating for the source/target split.\n *\n * Under the split, SOURCE (chain DB) holds only chain+decoded tables — the\n * control-plane tables were dropped to reclaim space — while TARGET (platform\n * DB) holds the control plane. `migrate.ts` still runs EVERY migration on EVERY\n * database (kysely integrity: the provider set must never be filtered per-DB, or\n * kysely throws \"previously executed migration is missing\" because each DB's\n * `kysely_migration` already records all of them). Instead, `migrate.ts` sets a\n * role before each pass and a migration gates its DDL with the helpers below: a\n * control migration's `up()` is a no-op on SOURCE but is still recorded applied,\n * so there is no missing-migration error and no re-run.\n *\n * `'both'` is single-DB / collapsed-split mode (dev / OSS / CI): every helper\n * runs, identical to pre-split behavior.\n *\n * Authoring a new migration under the split:\n * export async function up(db: Kysely<unknown>): Promise<void> {\n * await onControlPlane(() => sql`ALTER TABLE accounts ADD COLUMN …`.execute(db));\n * await onChainPlane(() => sql`ALTER TABLE blocks ADD COLUMN …`.execute(db));\n * // schema-wide / mixed statements: leave unwrapped (run on both).\n * }\n */\n\nexport type MigrationRole = \"source\" | \"target\" | \"both\";\n\n// Module-level singleton: migrate runs sequentially in one process, one target\n// at a time, so there is no concurrency to guard.\nlet currentRole: MigrationRole = \"both\";\n\nexport function setMigrationRole(role: MigrationRole): void {\n\tcurrentRole = role;\n}\n\nexport function getMigrationRole(): MigrationRole {\n\treturn currentRole;\n}\n\n/** Run control-plane (TARGET) DDL only when this pass targets the control plane. */\nexport async function onControlPlane(fn: () => Promise<void>): Promise<void> {\n\tif (currentRole === \"target\" || currentRole === \"both\") await fn();\n}\n\n/** Run chain/decoded (SOURCE) DDL only when this pass targets the chain plane. */\nexport async function onChainPlane(fn: () => Promise<void>): Promise<void> {\n\tif (currentRole === \"source\" || currentRole === \"both\") await fn();\n}\n",
|
|
12
12
|
"import type { Database } from \"./types.ts\";\n\nexport type DbPlane = \"source\" | \"target\" | \"both\";\n\n/**\n * Canonical table → DB-plane mapping for the source/target split.\n *\n * SOURCE = chain + decoded (the `postgres` instance); TARGET = control plane\n * (the `postgres-platform` instance). `both` = present/used on both planes\n * (`service_heartbeats`: the indexer writes its row on SOURCE, the\n * subgraph-processor writes its row on TARGET).\n *\n * `satisfies Record<keyof Database, DbPlane>` makes adding a table to `Database`\n * without classifying it here a COMPILE error. This is the single source of\n * truth for the split — `docker/SCHEMA_SPLIT.md` and the cutover script's\n * `CONTROL_TABLES` mirror the `target` set (guarded by `table-plane.test.ts`).\n * Use it to decide which migration helper a table's DDL belongs in\n * (`onControlPlane` for `target`, `onChainPlane` for `source`).\n *\n * Note: `kysely_migration` / `kysely_migration_lock` are kysely-managed (not in\n * `Database`) and exist on both — they are intentionally not listed here.\n */\nexport const TABLE_TO_DB = {\n\t// ── SOURCE: raw chain ──\n\tblocks: \"source\",\n\ttransactions: \"source\",\n\tevents: \"source\",\n\ttransactions_archive: \"source\",\n\tevents_archive: \"source\",\n\tdead_letter_events: \"source\",\n\tmempool_transactions: \"source\",\n\tindex_progress: \"source\",\n\tcontracts: \"source\",\n\tchain_reorgs: \"source\",\n\t// Chain-derived read memo — keyed on index_block_hash, lives with the\n\t// blocks it pins to.\n\tchain_read_cache: \"source\",\n\t// Staged fork contenders — raw chain data, resolved during ingest.\n\tpending_fork_blocks: \"source\",\n\t// Exact observer callback receipts — the replay authority sits with chain data.\n\tobserver_journal: \"source\",\n\t// Declared scope — belongs with the chain whose start it declares.\n\tsync_scopes: \"source\",\n\t// Coverage kernel — attests the chain it sits next to.\n\tstage_registry: \"source\",\n\tstage_runs: \"source\",\n\tstage_block_receipts: \"source\",\n\tcoverage_segments: \"source\",\n\tstage_failures: \"source\",\n\t// ── SOURCE: decoded ──\n\tdecoded_events: \"source\",\n\tdecoder_checkpoints: \"source\",\n\tpox4_calls: \"source\",\n\tpox4_cycles_daily: \"source\",\n\tpox4_signers_daily: \"source\",\n\tpox5_events: \"source\",\n\tburn_block_rewards: \"source\",\n\tburn_block_reward_slots: \"source\",\n\tsbtc_events: \"source\",\n\tsbtc_token_events: \"source\",\n\tsbtc_settlements: \"source\",\n\tsbtc_supply_snapshots: \"source\",\n\tbns_name_events: \"source\",\n\tbns_namespace_events: \"source\",\n\tbns_marketplace_events: \"source\",\n\tbns_names: \"source\",\n\tbns_namespaces: \"source\",\n\t// ── TARGET: accounts / auth / billing ──\n\tinstances: \"target\",\n\taccounts: \"target\",\n\tapi_keys: \"target\",\n\tsessions: \"target\",\n\tmagic_links: \"target\",\n\taccount_spend_caps: \"target\",\n\tprocessed_stripe_events: \"target\",\n\t// ── TARGET: subscriptions ──\n\tsubscriptions: \"target\",\n\tsubscription_outbox: \"target\",\n\tsubscription_deliveries: \"target\",\n\ttrigger_evaluator_state: \"target\",\n\t// ── TARGET: subgraphs + metadata ──\n\tsubgraphs: \"target\",\n\tsubgraph_operations: \"target\",\n\tsubgraph_health_snapshots: \"target\",\n\tsubgraph_gaps: \"target\",\n\tsubgraph_processing_stats: \"target\",\n\tsubgraph_table_snapshots: \"target\",\n\t// ── TARGET: x402 payment rail ──\n\tx402_payments: \"target\",\n\tx402_balances: \"target\",\n\taccount_credits: \"target\",\n\t// ── TARGET: archive fetch gate (design-f089) ──\n\tarchive_fetches: \"target\",\n\t// ── BOTH ──\n\tservice_heartbeats: \"both\",\n} satisfies Record<keyof Database, DbPlane>;\n"
|
|
13
13
|
],
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
"/**\n * Instance modes for the Secondlayer platform.\n *\n * - `oss`: self-hosted, single-tenant. No auth middleware, no platform routes\n * (projects, admin). Everything runs against a single `DATABASE_URL`.\n * Intended for `docker compose up`.\n *\n * - `platform`: the metered archive deployment. Magic-link auth, API keys,\n * accounts, credits, and metered `/v1` reads (Index + Streams + subgraphs)\n * against a single shared DB. No paid tier ladder — one metered `free`\n * tier plus first-party internal service credentials.\n */\n\nexport type InstanceMode = \"oss\" | \"platform\";\n\n/** What the operator wrote in the env — `archive` is the honest name for the\n * metered public-archive deployment and aliases `platform` behaviorally. */\nexport type DeclaredInstanceMode = InstanceMode | \"archive\";\n\nconst VALID_MODES: readonly DeclaredInstanceMode[] = [\n\t\"oss\",\n\t\"platform\",\n\t\"archive\",\n];\n\nfunction declaredMode(): DeclaredInstanceMode {\n\tconst raw = process.env.INSTANCE_MODE?.trim().toLowerCase();\n\tif (raw && (VALID_MODES as readonly string[]).includes(raw)) {\n\t\treturn raw as DeclaredInstanceMode;\n\t}\n\treturn \"oss\";\n}\n\n/**\n * Resolve the active instance mode from `process.env.INSTANCE_MODE`.\n * Defaults to `\"oss\"` — the safest default for self-hosters who deploy\n * without setting the variable. `archive` normalizes to `platform` here so\n * every behavioral branch stays a two-way choice; use\n * `getDeclaredInstanceMode()` for display surfaces.\n */\nexport function getInstanceMode(): InstanceMode {\n\tconst declared = declaredMode();\n\treturn declared === \"archive\" ? \"platform\" : declared;\n}\n\n/** The mode as declared in the env, for status/identity surfaces. */\nexport function getDeclaredInstanceMode(): DeclaredInstanceMode {\n\treturn declaredMode();\n}\n\n/**\n * True when public reads are credit-metered (the free window, retention, and\n * credits gates arm). Platform/archive deployments meter by default;\n * self-host never does unless the operator explicitly sets\n * `METERED_READS=true`. `METERED_READS=false` disarms a metered deployment.\n */\nexport function isMeteredReads(): boolean {\n\tconst override = process.env.METERED_READS?.trim().toLowerCase();\n\tif (override === \"true\") return true;\n\tif (override === \"false\") return false;\n\treturn getInstanceMode() === \"platform\";\n}\n\n/** True when the active mode is `\"platform\"` (shared multi-tenant). */\nexport function isPlatformMode(): boolean {\n\treturn getInstanceMode() === \"platform\";\n}\n\n/** True when the active mode is `\"oss\"` (self-hosted). */\nexport function isOssMode(): boolean {\n\treturn getInstanceMode() === \"oss\";\n}\n",
|
|
8
8
|
"import { type RawBuilder, sql } from \"kysely\";\n\n/**\n * Safely encode a JS value as a JSONB literal for Kysely inserts/updates.\n * Kysely + postgres.js double-encodes JSON when using parameterized queries\n * with ::jsonb casts. This uses sql.raw to inline a properly escaped literal.\n *\n * Generic parameter lets callers set the RawBuilder's output type so they\n * don't need to cast at the insert site. Default is `unknown` — widen at\n * the call site when the column type is narrower, e.g. `jsonb<MyShape>(...)`.\n */\nexport function jsonb<T = unknown>(value: T): RawBuilder<T> {\n\tconst escaped = JSON.stringify(value, (_k, v) =>\n\t\ttypeof v === \"bigint\" ? v.toString() : v,\n\t).replace(/'/g, \"''\");\n\treturn sql`${sql.raw(`'${escaped}'::jsonb`)}`;\n}\n\n/**\n * Safely parse a JSONB value from the database.\n * Handles double-encoded strings where postgres.js returns a JSON string\n * instead of a parsed object.\n */\nexport function parseJsonb<T = unknown>(value: unknown): T {\n\tif (typeof value === \"string\") {\n\t\ttry {\n\t\t\treturn JSON.parse(value) as T;\n\t\t} catch {\n\t\t\treturn value as T;\n\t\t}\n\t}\n\treturn (value ?? {}) as T;\n}\n",
|
|
9
9
|
"import { Kysely } from \"kysely\";\nimport { PostgresJSDialect } from \"kysely-postgres-js\";\nimport postgres from \"postgres\";\nimport { isProductionEnv } from \"../env.ts\";\nimport { logger } from \"../logger.ts\";\nimport { isPlatformMode } from \"../mode.ts\";\nimport type { Database } from \"./types.ts\";\n\nexport type { Database } from \"./types.ts\";\n\nconst DEFAULT_URL =\n\t\"postgres://postgres:postgres@localhost:5432/secondlayer_dev\";\n\ninterface PoolEntry {\n\tdb: Kysely<Database>;\n\trawClient: ReturnType<typeof postgres>;\n\t/** Last access (ms) — drives LRU eviction of BYO pools. */\n\tlastUsed: number;\n\t/** Monotonic counter as a tiebreaker; Date.now() can repeat under load. */\n\tseq: number;\n}\n\n/**\n * Cache of Kysely + raw postgres.js pools keyed by resolved URL.\n * Two getters resolving to the same URL share one entry (single pool) —\n * this is the single-DB backward-compat contract: when only `DATABASE_URL`\n * is set, `getSourceDb() === getTargetDb()` (zero regression vs. pre-dual-DB).\n *\n * The BYO data plane adds one pool per user-owned DB. To stop N user DBs from\n * exhausting connections/FDs, the map is bounded (`DATABASE_MAX_POOLS`, default\n * 25) with LRU eviction — the hot source/target pools are never evicted.\n */\nconst pools = new Map<string, PoolEntry>();\nlet poolSeq = 0;\n\nfunction maxPools(): number {\n\treturn Number.parseInt(process.env.DATABASE_MAX_POOLS ?? \"25\", 10);\n}\n\n/** Close the least-recently-used non-(source/target) pool when over the cap. */\nfunction evictIfNeeded(): void {\n\tif (pools.size <= maxPools()) return;\n\tconst protectedUrls = new Set([resolveSourceUrl(), resolveTargetUrl()]);\n\tlet lruUrl: string | undefined;\n\tlet lruEntry: PoolEntry | undefined;\n\tfor (const [url, entry] of pools) {\n\t\tif (protectedUrls.has(url)) continue;\n\t\tif (\n\t\t\t!lruEntry ||\n\t\t\tentry.lastUsed < lruEntry.lastUsed ||\n\t\t\t(entry.lastUsed === lruEntry.lastUsed && entry.seq < lruEntry.seq)\n\t\t) {\n\t\t\tlruUrl = url;\n\t\t\tlruEntry = entry;\n\t\t}\n\t}\n\tif (!lruUrl || !lruEntry) return;\n\tpools.delete(lruUrl);\n\tconst evicted = lruEntry;\n\t// Close in the background — never block pool creation on teardown.\n\tvoid evicted.db\n\t\t.destroy()\n\t\t.catch(() => {})\n\t\t.then(() => evicted.rawClient.end({ timeout: 5 }))\n\t\t.catch(() => {});\n}\n\n/** OSS: one application URL. SOURCE_/TARGET_ are hosted split only. */\nexport function applicationDatabaseUrl(): string {\n\treturn process.env.DATABASE_URL || DEFAULT_URL;\n}\n\nfunction resolveSourceUrl(): string {\n\tif (!isPlatformMode()) return applicationDatabaseUrl();\n\treturn (\n\t\tprocess.env.SOURCE_DATABASE_URL || process.env.DATABASE_URL || DEFAULT_URL\n\t);\n}\n\nfunction resolveTargetUrl(): string {\n\tif (!isPlatformMode()) return applicationDatabaseUrl();\n\treturn (\n\t\tprocess.env.TARGET_DATABASE_URL || process.env.DATABASE_URL || DEFAULT_URL\n\t);\n}\n\n/** Host[:port]/dbname for a connection URL — credentials stripped. */\nfunction describeDbUrl(url: string): string {\n\ttry {\n\t\tconst u = new URL(url);\n\t\treturn `${u.hostname}${u.port ? `:${u.port}` : \"\"}${u.pathname}`;\n\t} catch {\n\t\treturn \"invalid-url\";\n\t}\n}\n\n/** Warn-once latch — one line per process, not one per import or connection. */\nlet warnedDefaultUrlFallthrough = false;\n\n/**\n * Test-run guard for the plain \"no DB env at all\" case, which `assertDbSplit`\n * deliberately does not cover (it only fires when a SPLIT is requested).\n *\n * Without any of the three URL vars, every getter resolves to the built-in\n * dev `DEFAULT_URL`. In a test run that means one of two silent outcomes: the\n * suite's own `skipIf(!process.env.DATABASE_URL)` guard disables it entirely,\n * or an unguarded test connects to whatever Postgres happens to be on\n * localhost:5432. Both exit 0 and read as \"tests passed\".\n *\n * Fires at MODULE LOAD, not from a getter: DB-backed suites guard `getDb()`\n * behind their own `skipIf`, so when the env is missing no getter is ever\n * called — import is the only point where this is still observable.\n *\n * Gated to `NODE_ENV=test` on purpose. `@secondlayer/shared` is published, and\n * end users running `sl local` / `sl dev` rely on `DEFAULT_URL` legitimately —\n * it matches what the CLI provisions. They must see nothing. Fail-soft: warns,\n * never throws, matching `assertDbSplit`'s contract.\n */\nfunction warnIfTestRunFallsThroughToDefault(): void {\n\tif (warnedDefaultUrlFallthrough) return;\n\t// Runtime read — inlining `process.env.NODE_ENV` gets constant-folded into\n\t// the shipped dist, freezing this to a build-time literal. See env.ts.\n\t// biome-ignore lint/complexity/useLiteralKeys: bracket access is deliberate — dot-access gets constant-folded by the bundler.\n\tif (process.env[\"NODE_ENV\"] !== \"test\") return;\n\tif (\n\t\tprocess.env.DATABASE_URL ||\n\t\tprocess.env.SOURCE_DATABASE_URL ||\n\t\tprocess.env.TARGET_DATABASE_URL\n\t) {\n\t\treturn;\n\t}\n\twarnedDefaultUrlFallthrough = true;\n\tconsole.warn(\n\t\t`⚠️ DATABASE_URL unset — falling through to the built-in DEFAULT_URL (${describeDbUrl(\n\t\t\tDEFAULT_URL,\n\t\t)}). Database-backed tests will skip or hit the wrong Postgres. Copy .env.example to .env in this package (see \\`bun run db\\`).`,\n\t);\n}\n\nwarnIfTestRunFallsThroughToDefault();\n\nexport interface DbSplitStatus {\n\t/** \"split\" when source/target resolve to different DBs, else \"single\". */\n\tmode: \"split\" | \"single\";\n\t/** True when the chain/control split is live (distinct DBs). */\n\tactive: boolean;\n\t/** SOURCE host[:port]/dbname (no credentials). */\n\tsourceDb: string;\n\t/** TARGET host[:port]/dbname (no credentials). */\n\ttargetDb: string;\n}\n\n/**\n * Resolved source/target DB identity for status/health surfaces. Lets operators\n * see whether the chain/control split is live or dormant (collapsed to one DB)\n * without shelling in. Credentials are never exposed — host/db only.\n *\n * `active` is a STRING-IDENTITY check on the two URLs: it can't catch two\n * distinct URLs that alias the same physical instance (e.g. `postgres` vs\n * `127.0.0.1`, or a swapped/wrong host). Treat `active: true` as \"configured\n * for split\", not a proof of physical isolation.\n */\nexport function getDbSplitStatus(): DbSplitStatus {\n\tconst source = resolveSourceUrl();\n\tconst target = resolveTargetUrl();\n\tconst active = source !== target;\n\treturn {\n\t\tmode: active ? \"split\" : \"single\",\n\t\tactive,\n\t\tsourceDb: describeDbUrl(source),\n\t\ttargetDb: describeDbUrl(target),\n\t};\n}\n\n/**\n * Boot guard for the chain/control DB split. Surfaces three misconfigurations\n * loudly (fail-soft — logs, never throws, so a misconfig can't brick startup):\n *\n * 1. Silent wrong-DB: a split is requested (one of SOURCE_/TARGET_ set) but\n * `DATABASE_URL` is absent (the split-prod default) and the OTHER var is\n * unset, so it falls through to the built-in dev `DEFAULT_URL` — about to\n * read/write a real-but-wrong database. This is the failure mode the\n * remove-DATABASE_URL decision creates; catch it.\n * 2. Collapsed split: both vars set but resolve to the same DB (typo, or a\n * stray `DATABASE_URL` masking the intent).\n * 3. Dormant split (prod only): neither var set, so all services share one\n * Postgres failure domain. Not an error, but no longer silent.\n */\nexport function assertDbSplit(): void {\n\t// Runtime read — inlining `process.env.NODE_ENV` gets constant-folded in the\n\t// shipped dist (this prod branch would die silently). See env.ts.\n\tconst isProd = isProductionEnv();\n\tconst wantsSplit = !!(\n\t\tprocess.env.SOURCE_DATABASE_URL || process.env.TARGET_DATABASE_URL\n\t);\n\tconst databaseUrlSet = !!process.env.DATABASE_URL;\n\tconst source = resolveSourceUrl();\n\tconst target = resolveTargetUrl();\n\n\tif (\n\t\twantsSplit &&\n\t\t!databaseUrlSet &&\n\t\t(source === DEFAULT_URL || target === DEFAULT_URL)\n\t) {\n\t\tconst which =\n\t\t\tsource === DEFAULT_URL ? \"SOURCE_DATABASE_URL\" : \"TARGET_DATABASE_URL\";\n\t\tconst msg = `${which} unset and DATABASE_URL absent — resolving to built-in DEFAULT_URL; refusing to silently use the wrong database`;\n\t\tif (isProd) console.error(`❌ ${msg}`);\n\t\telse console.warn(`⚠️ ${msg}`);\n\t\treturn;\n\t}\n\n\tif (!wantsSplit) {\n\t\tif (isProd) {\n\t\t\tconsole.warn(\n\t\t\t\t\"⚠️ DB split dormant — all services share one Postgres failure domain (SOURCE_/TARGET_DATABASE_URL unset)\",\n\t\t\t);\n\t\t}\n\t\treturn;\n\t}\n\n\tif (source === target) {\n\t\tconst msg =\n\t\t\t\"DB split requested but SOURCE_DATABASE_URL === TARGET_DATABASE_URL (check for a typo or a stray DATABASE_URL fallback)\";\n\t\tif (isProd) console.error(`❌ ${msg}`);\n\t\telse console.warn(`⚠️ ${msg}`);\n\t}\n}\n\nfunction getOrCreatePool(url: string): PoolEntry {\n\tconst existing = pools.get(url);\n\tif (existing) {\n\t\texisting.lastUsed = Date.now();\n\t\texisting.seq = poolSeq++;\n\t\treturn existing;\n\t}\n\n\t// \"Local\" = we skip TLS. Any Docker service alias (single-label hostname\n\t// with no dots) is on an internal network and won't serve TLS.\n\tconst host = (() => {\n\t\ttry {\n\t\t\treturn new URL(url).hostname;\n\t\t} catch {\n\t\t\treturn \"\";\n\t\t}\n\t})();\n\tconst isLocal =\n\t\thost === \"localhost\" || host === \"127.0.0.1\" || !host.includes(\".\");\n\tconst poolMax = Number.parseInt(process.env.DATABASE_POOL_MAX ?? \"20\", 10);\n\t// Close idle connections so a fleet of BYO pools doesn't pin connections it\n\t// no longer needs (0 = never; postgres.js default).\n\tconst idleTimeout = Number.parseInt(\n\t\tprocess.env.DATABASE_IDLE_TIMEOUT ?? \"300\",\n\t\t10,\n\t);\n\tconst rawClient = postgres(url, {\n\t\tmax: poolMax,\n\t\tidle_timeout: idleTimeout,\n\t\tssl: isLocal\n\t\t\t? undefined\n\t\t\t: {\n\t\t\t\t\trejectUnauthorized: process.env.NODE_TLS_REJECT_UNAUTHORIZED !== \"0\",\n\t\t\t\t},\n\t});\n\tconst db = new Kysely<Database>({\n\t\tdialect: new PostgresJSDialect({ postgres: rawClient }),\n\t\t// Diagnostic hook: surface the failing SQL whenever postgres rejects\n\t\t// with code 42P10 (ON CONFLICT target doesn't match any unique\n\t\t// constraint). Temporary — remove once we've caught the culprit\n\t\t// query in prod logs and fixed the schema drift.\n\t\tlog: (event) => {\n\t\t\tif (event.level !== \"error\") return;\n\t\t\tconst err = event.error as {\n\t\t\t\tcode?: string;\n\t\t\t\tmessage?: string;\n\t\t\t} | null;\n\t\t\tif (err?.code !== \"42P10\") return;\n\t\t\tlogger.warn(\"db.on_conflict_constraint_missing\", {\n\t\t\t\tcode: err.code,\n\t\t\t\tmessage: err.message,\n\t\t\t\tsql: event.query.sql,\n\t\t\t\tparams: event.query.parameters,\n\t\t\t});\n\t\t},\n\t});\n\tconst entry: PoolEntry = {\n\t\tdb,\n\t\trawClient,\n\t\tlastUsed: Date.now(),\n\t\tseq: poolSeq++,\n\t};\n\tpools.set(url, entry);\n\tevictIfNeeded();\n\treturn entry;\n}\n\n/**\n * Kysely instance for the SOURCE DB (block/tx/event reads from the shared\n * indexer). Resolution: `SOURCE_DATABASE_URL || DATABASE_URL`.\n */\nexport function getSourceDb(): Kysely<Database> {\n\treturn getOrCreatePool(resolveSourceUrl()).db;\n}\n\n/**\n * Kysely instance for the TARGET DB (subgraph schemas, subgraphs table,\n * account-scoped data — tenant-side writes). Resolution:\n * `TARGET_DATABASE_URL || DATABASE_URL`.\n */\nexport function getTargetDb(): Kysely<Database> {\n\treturn getOrCreatePool(resolveTargetUrl()).db;\n}\n\n/**\n * Backward-compat alias for `getTargetDb()`. Accepts an optional\n * `connectionString` override used by seed/test helpers — when supplied,\n * bypasses env resolution and uses the provided URL directly (still cached).\n */\nexport function getDb(connectionString?: string): Kysely<Database> {\n\tif (connectionString) return getOrCreatePool(connectionString).db;\n\treturn getTargetDb();\n}\n\n/**\n * Raw postgres.js client for dynamic schema DDL (CREATE SCHEMA, DROP, etc.).\n * Defaults to the target role (tenant schemas live in the target DB).\n */\nexport function getRawClient(\n\trole: \"source\" | \"target\" = \"target\",\n): ReturnType<typeof postgres> {\n\tconst url = role === \"source\" ? resolveSourceUrl() : resolveTargetUrl();\n\treturn getOrCreatePool(url).rawClient;\n}\n\n/**\n * Raw postgres.js client for an arbitrary connection string (cached by URL).\n * Used by the BYO data plane to run DDL / serving queries against a\n * user-owned Postgres. Distinct from {@link getRawClient}, which only knows the\n * source/target roles resolved from env.\n */\nexport function getRawClientFor(url: string): ReturnType<typeof postgres> {\n\treturn getOrCreatePool(url).rawClient;\n}\n\n/** Close all DB connection pools. Call in CLI commands to allow process exit. */\nexport async function closeDb(): Promise<void> {\n\tfor (const entry of pools.values()) {\n\t\tawait entry.db.destroy();\n\t\tawait entry.rawClient.end();\n\t}\n\tpools.clear();\n}\n\nimport { sql } from \"kysely\";\nexport { sql };\nexport * from \"./types.ts\";\nexport type { DbReadRow, NumericAsText } from \"./read-row.ts\";\nexport {\n\tSOURCE_READ_COLUMNS,\n\tSOURCE_READ_TYPES,\n\tSOURCE_READ_PKS,\n\ttype IndexColumn,\n\ttype IndexColumnType,\n\ttype IndexPrimaryKey,\n} from \"./source-read-columns.ts\";\nexport { jsonb, parseJsonb } from \"./jsonb.ts\";\nexport {\n\tgetMigrationRole,\n\tonChainPlane,\n\tonControlPlane,\n\tsetMigrationRole,\n} from \"./migration-role.ts\";\nexport type { MigrationRole } from \"./migration-role.ts\";\nexport { TABLE_TO_DB } from \"./table-plane.ts\";\nexport type { DbPlane } from \"./table-plane.ts\";\n",
|
|
10
|
-
"import type { Database } from \"./types.ts\";\n\n// The physical source-table columns the public read API (packages/api) actually\n// reads via getSourceDb(). This is the read contract between the indexer (which\n// owns the write schema) and the API (which reads it raw). A column rename or\n// removal on the producer side that isn't reflected here is caught by the\n// snapshot drift test; the `satisfies` clause guarantees every entry is a real\n// table + column today.\n//\n// Computed/aliased read expressions are intentionally excluded — they are not\n// part of the producer contract: `cursor`/`block_time` where derived in SQL\n// rather than stored, window ranks (stream_event_index), and JSONB extractions\n// from `events.data`. Generated bookkeeping columns (created_at/updated_at) are\n// excluded unless a read path selects them.\nexport const SOURCE_READ_COLUMNS = {\n\tblocks: [\n\t\t\"burn_block_hash\",\n\t\t\"burn_block_height\",\n\t\t\"canonical\",\n\t\t\"hash\",\n\t\t\"height\",\n\t\t\"parent_hash\",\n\t\t\"timestamp\",\n\t],\n\tdecoded_events: [\n\t\t\"amount\",\n\t\t\"asset_identifier\",\n\t\t\"block_height\",\n\t\t\"canonical\",\n\t\t\"contract_id\",\n\t\t\"cursor\",\n\t\t\"event_index\",\n\t\t\"event_type\",\n\t\t\"memo\",\n\t\t\"payload\",\n\t\t\"recipient\",\n\t\t\"sender\",\n\t\t\"tx_id\",\n\t\t\"tx_index\",\n\t\t\"value\",\n\t],\n\ttransactions: [\n\t\t\"block_height\",\n\t\t\"contract_id\",\n\t\t\"function_args\",\n\t\t\"function_name\",\n\t\t\"raw_result\",\n\t\t\"raw_tx\",\n\t\t\"sender\",\n\t\t\"status\",\n\t\t\"tx_id\",\n\t\t\"tx_index\",\n\t\t\"type\",\n\t],\n\tmempool_transactions: [\n\t\t\"contract_id\",\n\t\t\"function_args\",\n\t\t\"function_name\",\n\t\t\"raw_tx\",\n\t\t\"received_at\",\n\t\t\"sender\",\n\t\t\"seq\",\n\t\t\"tx_id\",\n\t\t\"type\",\n\t],\n\tpox4_calls: [\n\t\t\"aggregated_amount_ustx\",\n\t\t\"aggregated_signer_index\",\n\t\t\"amount_ustx\",\n\t\t\"auth_allowed\",\n\t\t\"auth_id\",\n\t\t\"auth_period\",\n\t\t\"auth_topic\",\n\t\t\"block_height\",\n\t\t\"block_time\",\n\t\t\"burn_block_height\",\n\t\t\"caller\",\n\t\t\"canonical\",\n\t\t\"cursor\",\n\t\t\"delegate_to\",\n\t\t\"end_cycle\",\n\t\t\"function_name\",\n\t\t\"lock_period\",\n\t\t\"max_amount\",\n\t\t\"pox_addr_btc\",\n\t\t\"pox_addr_hashbytes\",\n\t\t\"pox_addr_version\",\n\t\t\"result_ok\",\n\t\t\"reward_cycle\",\n\t\t\"signer_key\",\n\t\t\"signer_signature\",\n\t\t\"source_cursor\",\n\t\t\"stacker\",\n\t\t\"start_cycle\",\n\t\t\"tx_id\",\n\t\t\"tx_index\",\n\t],\n\tpox5_events: [\n\t\t\"amount_sats\",\n\t\t\"amount_ustx\",\n\t\t\"block_height\",\n\t\t\"block_time\",\n\t\t\"bond_index\",\n\t\t\"canonical\",\n\t\t\"cursor\",\n\t\t\"data\",\n\t\t\"event_index\",\n\t\t\"first_reward_cycle\",\n\t\t\"is_l1_lock\",\n\t\t\"reward_cycle\",\n\t\t\"signer\",\n\t\t\"signer_key\",\n\t\t\"signer_manager\",\n\t\t\"staker\",\n\t\t\"topic\",\n\t\t\"tx_id\",\n\t\t\"tx_index\",\n\t\t\"unlock_burn_height\",\n\t\t\"unlock_cycle\",\n\t],\n\tsbtc_events: [\n\t\t\"amount\",\n\t\t\"bitcoin_txid\",\n\t\t\"block_height\",\n\t\t\"block_time\",\n\t\t\"burn_hash\",\n\t\t\"burn_height\",\n\t\t\"canonical\",\n\t\t\"cursor\",\n\t\t\"event_index\",\n\t\t\"fee\",\n\t\t\"governance_contract_type\",\n\t\t\"governance_new_contract\",\n\t\t\"max_fee\",\n\t\t\"output_index\",\n\t\t\"recipient_btc_hashbytes\",\n\t\t\"recipient_btc_version\",\n\t\t\"request_id\",\n\t\t\"sender\",\n\t\t\"signer_address\",\n\t\t\"signer_aggregate_pubkey\",\n\t\t\"signer_bitmap\",\n\t\t\"signer_keys_count\",\n\t\t\"signer_threshold\",\n\t\t\"sweep_txid\",\n\t\t\"topic\",\n\t\t\"tx_id\",\n\t\t\"tx_index\",\n\t],\n\tsbtc_token_events: [\n\t\t\"amount\",\n\t\t\"block_height\",\n\t\t\"block_time\",\n\t\t\"canonical\",\n\t\t\"cursor\",\n\t\t\"event_index\",\n\t\t\"event_type\",\n\t\t\"memo\",\n\t\t\"recipient\",\n\t\t\"sender\",\n\t\t\"tx_id\",\n\t\t\"tx_index\",\n\t],\n\tbns_name_events: [\n\t\t\"block_height\",\n\t\t\"block_time\",\n\t\t\"bns_id\",\n\t\t\"canonical\",\n\t\t\"cursor\",\n\t\t\"event_index\",\n\t\t\"fqn\",\n\t\t\"hashed_salted_fqn_preorder\",\n\t\t\"imported_at\",\n\t\t\"name\",\n\t\t\"namespace\",\n\t\t\"owner\",\n\t\t\"preordered_by\",\n\t\t\"registered_at\",\n\t\t\"renewal_height\",\n\t\t\"stx_burn\",\n\t\t\"topic\",\n\t\t\"tx_id\",\n\t\t\"tx_index\",\n\t],\n\tbns_namespace_events: [\n\t\t\"block_height\",\n\t\t\"block_time\",\n\t\t\"canonical\",\n\t\t\"cursor\",\n\t\t\"event_index\",\n\t\t\"launched_at\",\n\t\t\"lifetime\",\n\t\t\"manager\",\n\t\t\"manager_frozen\",\n\t\t\"manager_transfers_disabled\",\n\t\t\"namespace\",\n\t\t\"price_frozen\",\n\t\t\"price_function\",\n\t\t\"revealed_at\",\n\t\t\"status\",\n\t\t\"tx_id\",\n\t\t\"tx_index\",\n\t],\n\tbns_marketplace_events: [\n\t\t\"action\",\n\t\t\"block_height\",\n\t\t\"block_time\",\n\t\t\"bns_id\",\n\t\t\"canonical\",\n\t\t\"commission\",\n\t\t\"cursor\",\n\t\t\"event_index\",\n\t\t\"price_ustx\",\n\t\t\"tx_id\",\n\t\t\"tx_index\",\n\t],\n\tbns_names: [\n\t\t\"bns_id\",\n\t\t\"fqn\",\n\t\t\"last_event_at\",\n\t\t\"last_event_cursor\",\n\t\t\"name\",\n\t\t\"namespace\",\n\t\t\"owner\",\n\t\t\"registered_at\",\n\t\t\"renewal_height\",\n\t],\n\tbns_namespaces: [\n\t\t\"last_event_at\",\n\t\t\"last_event_cursor\",\n\t\t\"launched_at\",\n\t\t\"lifetime\",\n\t\t\"manager\",\n\t\t\"manager_frozen\",\n\t\t\"name_count\",\n\t\t\"namespace\",\n\t\t\"price_frozen\",\n\t],\n\tburn_block_rewards: [\n\t\t\"amount_sats\",\n\t\t\"burn_amount\",\n\t\t\"burn_block_hash\",\n\t\t\"burn_block_height\",\n\t\t\"cursor\",\n\t\t\"recipient_btc\",\n\t\t\"reward_index\",\n\t],\n\tburn_block_reward_slots: [\n\t\t\"burn_block_hash\",\n\t\t\"burn_block_height\",\n\t\t\"cursor\",\n\t\t\"holder_btc\",\n\t\t\"slot_index\",\n\t],\n\tevents: [\"block_height\", \"data\", \"event_index\", \"tx_id\", \"type\"],\n\tchain_reorgs: [\"detected_at\"],\n} as const satisfies {\n\treadonly [T in keyof Database]?: readonly (keyof Database[T])[];\n};\n\n/** Portable column type for the Index read domain (a lean, ORM-agnostic vocab). */\nexport type IndexColumnType =\n\t| \"text\"\n\t| \"int\"\n\t| \"boolean\"\n\t| \"timestamp\"\n\t| \"jsonb\";\n\nexport interface IndexColumn {\n\ttype: IndexColumnType;\n\t/** SELECT type includes `| null`. Omitted = NOT NULL. */\n\tnullable?: boolean;\n}\n\n// The portable TS/SQL type for every column in SOURCE_READ_COLUMNS, single-sourced\n// from the `Database` interface (the producer's typed schema). This is what lets\n// `sl index codegen` emit a typed ORM/JSON-Schema for the public Index domain so a\n// BYO database mirror is fully typed and can't drift from the API. The drift test\n// (source-read-columns.test.ts) asserts this covers exactly SOURCE_READ_COLUMNS;\n// the `satisfies` clause guarantees a type entry for every read table.\n//\n// `string` amount/value columns (e.g. amount_ustx, fee, price_ustx) are NUMERIC in\n// Postgres surfaced as strings — `text` here is lossless for a read mirror. `number`\n// columns map to `int`. Enum string-unions (topic, status, …) map to `text`.\nexport const SOURCE_READ_TYPES = {\n\tblocks: {\n\t\tburn_block_hash: { type: \"text\", nullable: true },\n\t\tburn_block_height: { type: \"int\" },\n\t\tcanonical: { type: \"boolean\" },\n\t\thash: { type: \"text\" },\n\t\theight: { type: \"int\" },\n\t\tparent_hash: { type: \"text\" },\n\t\ttimestamp: { type: \"int\" },\n\t},\n\tdecoded_events: {\n\t\tamount: { type: \"text\", nullable: true },\n\t\tasset_identifier: { type: \"text\", nullable: true },\n\t\tblock_height: { type: \"int\" },\n\t\tcanonical: { type: \"boolean\" },\n\t\tcontract_id: { type: \"text\", nullable: true },\n\t\tcursor: { type: \"text\" },\n\t\tevent_index: { type: \"int\" },\n\t\tevent_type: { type: \"text\" },\n\t\tmemo: { type: \"text\", nullable: true },\n\t\tpayload: { type: \"jsonb\", nullable: true },\n\t\trecipient: { type: \"text\", nullable: true },\n\t\tsender: { type: \"text\", nullable: true },\n\t\ttx_id: { type: \"text\" },\n\t\ttx_index: { type: \"int\" },\n\t\tvalue: { type: \"text\", nullable: true },\n\t},\n\ttransactions: {\n\t\tblock_height: { type: \"int\" },\n\t\tcontract_id: { type: \"text\", nullable: true },\n\t\tfunction_args: { type: \"jsonb\", nullable: true },\n\t\tfunction_name: { type: \"text\", nullable: true },\n\t\traw_result: { type: \"text\", nullable: true },\n\t\traw_tx: { type: \"text\" },\n\t\tsender: { type: \"text\" },\n\t\tstatus: { type: \"text\" },\n\t\ttx_id: { type: \"text\" },\n\t\ttx_index: { type: \"int\" },\n\t\ttype: { type: \"text\" },\n\t},\n\tmempool_transactions: {\n\t\tcontract_id: { type: \"text\", nullable: true },\n\t\tfunction_args: { type: \"jsonb\", nullable: true },\n\t\tfunction_name: { type: \"text\", nullable: true },\n\t\traw_tx: { type: \"text\" },\n\t\treceived_at: { type: \"timestamp\" },\n\t\tsender: { type: \"text\" },\n\t\tseq: { type: \"text\" },\n\t\ttx_id: { type: \"text\" },\n\t\ttype: { type: \"text\" },\n\t},\n\tpox4_calls: {\n\t\taggregated_amount_ustx: { type: \"text\", nullable: true },\n\t\taggregated_signer_index: { type: \"int\", nullable: true },\n\t\tamount_ustx: { type: \"text\", nullable: true },\n\t\tauth_allowed: { type: \"boolean\", nullable: true },\n\t\tauth_id: { type: \"text\", nullable: true },\n\t\tauth_period: { type: \"int\", nullable: true },\n\t\tauth_topic: { type: \"text\", nullable: true },\n\t\tblock_height: { type: \"int\" },\n\t\tblock_time: { type: \"timestamp\" },\n\t\tburn_block_height: { type: \"int\" },\n\t\tcaller: { type: \"text\" },\n\t\tcanonical: { type: \"boolean\" },\n\t\tcursor: { type: \"text\" },\n\t\tdelegate_to: { type: \"text\", nullable: true },\n\t\tend_cycle: { type: \"int\", nullable: true },\n\t\tfunction_name: { type: \"text\" },\n\t\tlock_period: { type: \"int\", nullable: true },\n\t\tmax_amount: { type: \"text\", nullable: true },\n\t\tpox_addr_btc: { type: \"text\", nullable: true },\n\t\tpox_addr_hashbytes: { type: \"text\", nullable: true },\n\t\tpox_addr_version: { type: \"int\", nullable: true },\n\t\tresult_ok: { type: \"boolean\" },\n\t\treward_cycle: { type: \"int\", nullable: true },\n\t\tsigner_key: { type: \"text\", nullable: true },\n\t\tsigner_signature: { type: \"text\", nullable: true },\n\t\tsource_cursor: { type: \"text\" },\n\t\tstacker: { type: \"text\", nullable: true },\n\t\tstart_cycle: { type: \"int\", nullable: true },\n\t\ttx_id: { type: \"text\" },\n\t\ttx_index: { type: \"int\" },\n\t},\n\tpox5_events: {\n\t\tamount_sats: { type: \"text\", nullable: true },\n\t\tamount_ustx: { type: \"text\", nullable: true },\n\t\tblock_height: { type: \"int\" },\n\t\tblock_time: { type: \"timestamp\" },\n\t\tbond_index: { type: \"int\", nullable: true },\n\t\tcanonical: { type: \"boolean\" },\n\t\tcursor: { type: \"text\" },\n\t\tdata: { type: \"jsonb\", nullable: true },\n\t\tevent_index: { type: \"int\" },\n\t\tfirst_reward_cycle: { type: \"int\", nullable: true },\n\t\tis_l1_lock: { type: \"boolean\", nullable: true },\n\t\treward_cycle: { type: \"int\", nullable: true },\n\t\tsigner: { type: \"text\", nullable: true },\n\t\tsigner_key: { type: \"text\", nullable: true },\n\t\tsigner_manager: { type: \"text\", nullable: true },\n\t\tstaker: { type: \"text\", nullable: true },\n\t\ttopic: { type: \"text\" },\n\t\ttx_id: { type: \"text\" },\n\t\ttx_index: { type: \"int\" },\n\t\tunlock_burn_height: { type: \"int\", nullable: true },\n\t\tunlock_cycle: { type: \"int\", nullable: true },\n\t},\n\tsbtc_events: {\n\t\tamount: { type: \"text\", nullable: true },\n\t\tbitcoin_txid: { type: \"text\", nullable: true },\n\t\tblock_height: { type: \"int\" },\n\t\tblock_time: { type: \"timestamp\" },\n\t\tburn_hash: { type: \"text\", nullable: true },\n\t\tburn_height: { type: \"int\", nullable: true },\n\t\tcanonical: { type: \"boolean\" },\n\t\tcursor: { type: \"text\" },\n\t\tevent_index: { type: \"int\" },\n\t\tfee: { type: \"text\", nullable: true },\n\t\tgovernance_contract_type: { type: \"int\", nullable: true },\n\t\tgovernance_new_contract: { type: \"text\", nullable: true },\n\t\tmax_fee: { type: \"text\", nullable: true },\n\t\toutput_index: { type: \"int\", nullable: true },\n\t\trecipient_btc_hashbytes: { type: \"text\", nullable: true },\n\t\trecipient_btc_version: { type: \"int\", nullable: true },\n\t\trequest_id: { type: \"int\", nullable: true },\n\t\tsender: { type: \"text\", nullable: true },\n\t\tsigner_address: { type: \"text\", nullable: true },\n\t\tsigner_aggregate_pubkey: { type: \"text\", nullable: true },\n\t\tsigner_bitmap: { type: \"text\", nullable: true },\n\t\tsigner_keys_count: { type: \"int\", nullable: true },\n\t\tsigner_threshold: { type: \"int\", nullable: true },\n\t\tsweep_txid: { type: \"text\", nullable: true },\n\t\ttopic: { type: \"text\" },\n\t\ttx_id: { type: \"text\" },\n\t\ttx_index: { type: \"int\" },\n\t},\n\tsbtc_token_events: {\n\t\tamount: { type: \"text\" },\n\t\tblock_height: { type: \"int\" },\n\t\tblock_time: { type: \"timestamp\" },\n\t\tcanonical: { type: \"boolean\" },\n\t\tcursor: { type: \"text\" },\n\t\tevent_index: { type: \"int\" },\n\t\tevent_type: { type: \"text\" },\n\t\tmemo: { type: \"text\", nullable: true },\n\t\trecipient: { type: \"text\", nullable: true },\n\t\tsender: { type: \"text\", nullable: true },\n\t\ttx_id: { type: \"text\" },\n\t\ttx_index: { type: \"int\" },\n\t},\n\tbns_name_events: {\n\t\tblock_height: { type: \"int\" },\n\t\tblock_time: { type: \"timestamp\" },\n\t\tbns_id: { type: \"text\" },\n\t\tcanonical: { type: \"boolean\" },\n\t\tcursor: { type: \"text\" },\n\t\tevent_index: { type: \"int\" },\n\t\tfqn: { type: \"text\" },\n\t\thashed_salted_fqn_preorder: { type: \"text\", nullable: true },\n\t\timported_at: { type: \"int\", nullable: true },\n\t\tname: { type: \"text\" },\n\t\tnamespace: { type: \"text\" },\n\t\towner: { type: \"text\", nullable: true },\n\t\tpreordered_by: { type: \"text\", nullable: true },\n\t\tregistered_at: { type: \"int\", nullable: true },\n\t\trenewal_height: { type: \"int\", nullable: true },\n\t\tstx_burn: { type: \"text\", nullable: true },\n\t\ttopic: { type: \"text\" },\n\t\ttx_id: { type: \"text\" },\n\t\ttx_index: { type: \"int\" },\n\t},\n\tbns_namespace_events: {\n\t\tblock_height: { type: \"int\" },\n\t\tblock_time: { type: \"timestamp\" },\n\t\tcanonical: { type: \"boolean\" },\n\t\tcursor: { type: \"text\" },\n\t\tevent_index: { type: \"int\" },\n\t\tlaunched_at: { type: \"int\", nullable: true },\n\t\tlifetime: { type: \"int\", nullable: true },\n\t\tmanager: { type: \"text\", nullable: true },\n\t\tmanager_frozen: { type: \"boolean\", nullable: true },\n\t\tmanager_transfers_disabled: { type: \"boolean\", nullable: true },\n\t\tnamespace: { type: \"text\" },\n\t\tprice_frozen: { type: \"boolean\", nullable: true },\n\t\tprice_function: { type: \"text\", nullable: true },\n\t\trevealed_at: { type: \"int\", nullable: true },\n\t\tstatus: { type: \"text\" },\n\t\ttx_id: { type: \"text\" },\n\t\ttx_index: { type: \"int\" },\n\t},\n\tbns_marketplace_events: {\n\t\taction: { type: \"text\" },\n\t\tblock_height: { type: \"int\" },\n\t\tblock_time: { type: \"timestamp\" },\n\t\tbns_id: { type: \"text\" },\n\t\tcanonical: { type: \"boolean\" },\n\t\tcommission: { type: \"text\", nullable: true },\n\t\tcursor: { type: \"text\" },\n\t\tevent_index: { type: \"int\" },\n\t\tprice_ustx: { type: \"text\", nullable: true },\n\t\ttx_id: { type: \"text\" },\n\t\ttx_index: { type: \"int\" },\n\t},\n\tbns_names: {\n\t\tbns_id: { type: \"text\" },\n\t\tfqn: { type: \"text\" },\n\t\tlast_event_at: { type: \"timestamp\" },\n\t\tlast_event_cursor: { type: \"text\" },\n\t\tname: { type: \"text\" },\n\t\tnamespace: { type: \"text\" },\n\t\towner: { type: \"text\" },\n\t\tregistered_at: { type: \"int\", nullable: true },\n\t\trenewal_height: { type: \"int\", nullable: true },\n\t},\n\tbns_namespaces: {\n\t\tlast_event_at: { type: \"timestamp\" },\n\t\tlast_event_cursor: { type: \"text\" },\n\t\tlaunched_at: { type: \"int\", nullable: true },\n\t\tlifetime: { type: \"int\", nullable: true },\n\t\tmanager: { type: \"text\", nullable: true },\n\t\tmanager_frozen: { type: \"boolean\" },\n\t\tname_count: { type: \"int\" },\n\t\tnamespace: { type: \"text\" },\n\t\tprice_frozen: { type: \"boolean\" },\n\t},\n\tburn_block_rewards: {\n\t\tamount_sats: { type: \"text\" },\n\t\tburn_amount: { type: \"text\" },\n\t\tburn_block_hash: { type: \"text\" },\n\t\tburn_block_height: { type: \"int\" },\n\t\tcursor: { type: \"text\" },\n\t\trecipient_btc: { type: \"text\" },\n\t\treward_index: { type: \"int\" },\n\t},\n\tburn_block_reward_slots: {\n\t\tburn_block_hash: { type: \"text\" },\n\t\tburn_block_height: { type: \"int\" },\n\t\tcursor: { type: \"text\" },\n\t\tholder_btc: { type: \"text\" },\n\t\tslot_index: { type: \"int\" },\n\t},\n\tevents: {\n\t\tblock_height: { type: \"int\" },\n\t\tdata: { type: \"jsonb\" },\n\t\tevent_index: { type: \"int\" },\n\t\ttx_id: { type: \"text\" },\n\t\ttype: { type: \"text\" },\n\t},\n\tchain_reorgs: {\n\t\tdetected_at: { type: \"timestamp\" },\n\t},\n} as const satisfies Record<\n\tkeyof typeof SOURCE_READ_COLUMNS,\n\tRecord<string, IndexColumn>\n>;\n\nexport type IndexPrimaryKey = readonly string[] | null;\n\n// Primary key per read table, for codegen targets that require a model identity\n// (Prisma). Every listed column must be a read column (drift-tested). `null` = no\n// usable read-column key — the physical PK is a synthetic id excluded from the\n// read contract (chain_reorgs) — so Prisma omits the table. `events` uses its\n// natural unique (tx_id, event_index); its physical PK is a synthetic uuid not\n// surfaced in the read set.\nexport const SOURCE_READ_PKS = {\n\tblocks: [\"height\"],\n\tdecoded_events: [\"cursor\"],\n\ttransactions: [\"tx_id\"],\n\tmempool_transactions: [\"seq\"],\n\tpox4_calls: [\"cursor\"],\n\tpox5_events: [\"cursor\"],\n\tsbtc_events: [\"cursor\"],\n\tsbtc_token_events: [\"cursor\"],\n\tbns_name_events: [\"cursor\"],\n\tbns_namespace_events: [\"cursor\"],\n\tbns_marketplace_events: [\"cursor\"],\n\tbns_names: [\"fqn\"],\n\tbns_namespaces: [\"namespace\"],\n\tburn_block_rewards: [\"cursor\"],\n\tburn_block_reward_slots: [\"cursor\"],\n\tevents: [\"tx_id\", \"event_index\"],\n\tchain_reorgs: null,\n} as const satisfies Record<keyof typeof SOURCE_READ_COLUMNS, IndexPrimaryKey>;\n",
|
|
10
|
+
"import type { Database } from \"./types.ts\";\n\n// The physical source-table columns the public read API (packages/api) actually\n// reads via getSourceDb(). This is the read contract between the indexer (which\n// owns the write schema) and the API (which reads it raw). A column rename or\n// removal on the producer side that isn't reflected here is caught by the\n// snapshot drift test; the `satisfies` clause guarantees every entry is a real\n// table + column today.\n//\n// Computed/aliased read expressions are intentionally excluded — they are not\n// part of the producer contract: `cursor`/`block_time` where derived in SQL\n// rather than stored, window ranks (stream_event_index), and JSONB extractions\n// from `events.data`. Generated bookkeeping columns (created_at/updated_at) are\n// excluded unless a read path selects them.\nexport const SOURCE_READ_COLUMNS = {\n\tblocks: [\n\t\t\"burn_block_hash\",\n\t\t\"burn_block_height\",\n\t\t\"canonical\",\n\t\t\"hash\",\n\t\t\"height\",\n\t\t\"parent_hash\",\n\t\t\"timestamp\",\n\t],\n\tdecoded_events: [\n\t\t\"amount\",\n\t\t\"asset_identifier\",\n\t\t\"block_height\",\n\t\t\"canonical\",\n\t\t\"contract_id\",\n\t\t\"cursor\",\n\t\t\"event_index\",\n\t\t\"event_type\",\n\t\t\"memo\",\n\t\t\"payload\",\n\t\t\"recipient\",\n\t\t\"sender\",\n\t\t\"tx_id\",\n\t\t\"tx_index\",\n\t\t\"value\",\n\t],\n\ttransactions: [\n\t\t\"block_height\",\n\t\t\"contract_id\",\n\t\t\"function_args\",\n\t\t\"function_name\",\n\t\t\"raw_result\",\n\t\t\"raw_tx\",\n\t\t\"sender\",\n\t\t\"status\",\n\t\t\"tx_id\",\n\t\t\"tx_index\",\n\t\t\"type\",\n\t],\n\tmempool_transactions: [\n\t\t\"contract_id\",\n\t\t\"function_args\",\n\t\t\"function_name\",\n\t\t\"raw_tx\",\n\t\t\"received_at\",\n\t\t\"sender\",\n\t\t\"seq\",\n\t\t\"tx_id\",\n\t\t\"type\",\n\t],\n\tpox4_calls: [\n\t\t\"aggregated_amount_ustx\",\n\t\t\"aggregated_signer_index\",\n\t\t\"amount_ustx\",\n\t\t\"auth_allowed\",\n\t\t\"auth_id\",\n\t\t\"auth_period\",\n\t\t\"auth_topic\",\n\t\t\"block_height\",\n\t\t\"block_time\",\n\t\t\"burn_block_height\",\n\t\t\"caller\",\n\t\t\"canonical\",\n\t\t\"cursor\",\n\t\t\"delegate_to\",\n\t\t\"end_cycle\",\n\t\t\"function_name\",\n\t\t\"lock_period\",\n\t\t\"max_amount\",\n\t\t\"pox_addr_btc\",\n\t\t\"pox_addr_hashbytes\",\n\t\t\"pox_addr_version\",\n\t\t\"result_ok\",\n\t\t\"reward_cycle\",\n\t\t\"signer_key\",\n\t\t\"signer_signature\",\n\t\t\"source_cursor\",\n\t\t\"stacker\",\n\t\t\"start_cycle\",\n\t\t\"tx_id\",\n\t\t\"tx_index\",\n\t],\n\tpox5_events: [\n\t\t\"amount_sats\",\n\t\t\"amount_ustx\",\n\t\t\"block_height\",\n\t\t\"block_time\",\n\t\t\"bond_index\",\n\t\t\"canonical\",\n\t\t\"cursor\",\n\t\t\"data\",\n\t\t\"event_index\",\n\t\t\"first_reward_cycle\",\n\t\t\"is_l1_lock\",\n\t\t\"reward_cycle\",\n\t\t\"signer\",\n\t\t\"signer_key\",\n\t\t\"signer_manager\",\n\t\t\"staker\",\n\t\t\"topic\",\n\t\t\"tx_id\",\n\t\t\"tx_index\",\n\t\t\"unlock_burn_height\",\n\t\t\"unlock_cycle\",\n\t],\n\tsbtc_events: [\n\t\t\"amount\",\n\t\t\"bitcoin_txid\",\n\t\t\"block_height\",\n\t\t\"block_time\",\n\t\t\"burn_hash\",\n\t\t\"burn_height\",\n\t\t\"canonical\",\n\t\t\"cursor\",\n\t\t\"event_index\",\n\t\t\"fee\",\n\t\t\"governance_contract_type\",\n\t\t\"governance_new_contract\",\n\t\t\"max_fee\",\n\t\t\"output_index\",\n\t\t\"recipient_btc_hashbytes\",\n\t\t\"recipient_btc_version\",\n\t\t\"request_id\",\n\t\t\"sender\",\n\t\t\"signer_address\",\n\t\t\"signer_aggregate_pubkey\",\n\t\t\"signer_bitmap\",\n\t\t\"signer_keys_count\",\n\t\t\"signer_threshold\",\n\t\t\"sweep_txid\",\n\t\t\"topic\",\n\t\t\"tx_id\",\n\t\t\"tx_index\",\n\t],\n\tsbtc_token_events: [\n\t\t\"amount\",\n\t\t\"block_height\",\n\t\t\"block_time\",\n\t\t\"canonical\",\n\t\t\"cursor\",\n\t\t\"event_index\",\n\t\t\"event_type\",\n\t\t\"memo\",\n\t\t\"recipient\",\n\t\t\"sender\",\n\t\t\"tx_id\",\n\t\t\"tx_index\",\n\t],\n\tbns_name_events: [\n\t\t\"block_height\",\n\t\t\"block_time\",\n\t\t\"bns_id\",\n\t\t\"canonical\",\n\t\t\"cursor\",\n\t\t\"event_index\",\n\t\t\"fqn\",\n\t\t\"hashed_salted_fqn_preorder\",\n\t\t\"imported_at\",\n\t\t\"name\",\n\t\t\"namespace\",\n\t\t\"owner\",\n\t\t\"preordered_by\",\n\t\t\"registered_at\",\n\t\t\"renewal_height\",\n\t\t\"stx_burn\",\n\t\t\"topic\",\n\t\t\"tx_id\",\n\t\t\"tx_index\",\n\t],\n\tbns_namespace_events: [\n\t\t\"block_height\",\n\t\t\"block_time\",\n\t\t\"canonical\",\n\t\t\"cursor\",\n\t\t\"event_index\",\n\t\t\"launched_at\",\n\t\t\"lifetime\",\n\t\t\"manager\",\n\t\t\"manager_frozen\",\n\t\t\"manager_transfers_disabled\",\n\t\t\"namespace\",\n\t\t\"price_frozen\",\n\t\t\"price_function\",\n\t\t\"revealed_at\",\n\t\t\"status\",\n\t\t\"tx_id\",\n\t\t\"tx_index\",\n\t],\n\tbns_marketplace_events: [\n\t\t\"action\",\n\t\t\"block_height\",\n\t\t\"block_time\",\n\t\t\"bns_id\",\n\t\t\"canonical\",\n\t\t\"commission\",\n\t\t\"cursor\",\n\t\t\"event_index\",\n\t\t\"price_ustx\",\n\t\t\"tx_id\",\n\t\t\"tx_index\",\n\t],\n\tbns_names: [\n\t\t\"bns_id\",\n\t\t\"fqn\",\n\t\t\"last_event_at\",\n\t\t\"last_event_cursor\",\n\t\t\"name\",\n\t\t\"namespace\",\n\t\t\"owner\",\n\t\t\"registered_at\",\n\t\t\"renewal_height\",\n\t],\n\tbns_namespaces: [\n\t\t\"last_event_at\",\n\t\t\"last_event_cursor\",\n\t\t\"launched_at\",\n\t\t\"lifetime\",\n\t\t\"manager\",\n\t\t\"manager_frozen\",\n\t\t\"name_count\",\n\t\t\"namespace\",\n\t\t\"price_frozen\",\n\t],\n\tburn_block_rewards: [\n\t\t\"amount_sats\",\n\t\t\"burn_amount\",\n\t\t\"burn_block_hash\",\n\t\t\"burn_block_height\",\n\t\t\"cursor\",\n\t\t\"recipient_btc\",\n\t\t\"reward_index\",\n\t],\n\tburn_block_reward_slots: [\n\t\t\"burn_block_hash\",\n\t\t\"burn_block_height\",\n\t\t\"cursor\",\n\t\t\"holder_btc\",\n\t\t\"slot_index\",\n\t],\n\tevents: [\"block_height\", \"data\", \"event_index\", \"tx_id\", \"type\"],\n\tchain_reorgs: [\"detected_at\"],\n} as const satisfies {\n\treadonly [T in keyof Database]?: readonly (keyof Database[T])[];\n};\n\n/** Portable column type for the Index read domain (a lean, ORM-agnostic vocab). */\nexport type IndexColumnType =\n\t| \"text\"\n\t| \"int\"\n\t| \"boolean\"\n\t| \"timestamp\"\n\t| \"jsonb\";\n\nexport interface IndexColumn {\n\ttype: IndexColumnType;\n\t/** SELECT type includes `| null`. Omitted = NOT NULL. */\n\tnullable?: boolean;\n}\n\n// The portable TS/SQL type for every column in SOURCE_READ_COLUMNS, single-sourced\n// from the `Database` interface (the producer's typed schema). This is what lets\n// `sl codegen index` emit a typed ORM/JSON-Schema for the public Index domain so a\n// BYO database mirror is fully typed and can't drift from the API. The drift test\n// (source-read-columns.test.ts) asserts this covers exactly SOURCE_READ_COLUMNS;\n// the `satisfies` clause guarantees a type entry for every read table.\n//\n// `string` amount/value columns (e.g. amount_ustx, fee, price_ustx) are NUMERIC in\n// Postgres surfaced as strings — `text` here is lossless for a read mirror. `number`\n// columns map to `int`. Enum string-unions (topic, status, …) map to `text`.\nexport const SOURCE_READ_TYPES = {\n\tblocks: {\n\t\tburn_block_hash: { type: \"text\", nullable: true },\n\t\tburn_block_height: { type: \"int\" },\n\t\tcanonical: { type: \"boolean\" },\n\t\thash: { type: \"text\" },\n\t\theight: { type: \"int\" },\n\t\tparent_hash: { type: \"text\" },\n\t\ttimestamp: { type: \"int\" },\n\t},\n\tdecoded_events: {\n\t\tamount: { type: \"text\", nullable: true },\n\t\tasset_identifier: { type: \"text\", nullable: true },\n\t\tblock_height: { type: \"int\" },\n\t\tcanonical: { type: \"boolean\" },\n\t\tcontract_id: { type: \"text\", nullable: true },\n\t\tcursor: { type: \"text\" },\n\t\tevent_index: { type: \"int\" },\n\t\tevent_type: { type: \"text\" },\n\t\tmemo: { type: \"text\", nullable: true },\n\t\tpayload: { type: \"jsonb\", nullable: true },\n\t\trecipient: { type: \"text\", nullable: true },\n\t\tsender: { type: \"text\", nullable: true },\n\t\ttx_id: { type: \"text\" },\n\t\ttx_index: { type: \"int\" },\n\t\tvalue: { type: \"text\", nullable: true },\n\t},\n\ttransactions: {\n\t\tblock_height: { type: \"int\" },\n\t\tcontract_id: { type: \"text\", nullable: true },\n\t\tfunction_args: { type: \"jsonb\", nullable: true },\n\t\tfunction_name: { type: \"text\", nullable: true },\n\t\traw_result: { type: \"text\", nullable: true },\n\t\traw_tx: { type: \"text\" },\n\t\tsender: { type: \"text\" },\n\t\tstatus: { type: \"text\" },\n\t\ttx_id: { type: \"text\" },\n\t\ttx_index: { type: \"int\" },\n\t\ttype: { type: \"text\" },\n\t},\n\tmempool_transactions: {\n\t\tcontract_id: { type: \"text\", nullable: true },\n\t\tfunction_args: { type: \"jsonb\", nullable: true },\n\t\tfunction_name: { type: \"text\", nullable: true },\n\t\traw_tx: { type: \"text\" },\n\t\treceived_at: { type: \"timestamp\" },\n\t\tsender: { type: \"text\" },\n\t\tseq: { type: \"text\" },\n\t\ttx_id: { type: \"text\" },\n\t\ttype: { type: \"text\" },\n\t},\n\tpox4_calls: {\n\t\taggregated_amount_ustx: { type: \"text\", nullable: true },\n\t\taggregated_signer_index: { type: \"int\", nullable: true },\n\t\tamount_ustx: { type: \"text\", nullable: true },\n\t\tauth_allowed: { type: \"boolean\", nullable: true },\n\t\tauth_id: { type: \"text\", nullable: true },\n\t\tauth_period: { type: \"int\", nullable: true },\n\t\tauth_topic: { type: \"text\", nullable: true },\n\t\tblock_height: { type: \"int\" },\n\t\tblock_time: { type: \"timestamp\" },\n\t\tburn_block_height: { type: \"int\" },\n\t\tcaller: { type: \"text\" },\n\t\tcanonical: { type: \"boolean\" },\n\t\tcursor: { type: \"text\" },\n\t\tdelegate_to: { type: \"text\", nullable: true },\n\t\tend_cycle: { type: \"int\", nullable: true },\n\t\tfunction_name: { type: \"text\" },\n\t\tlock_period: { type: \"int\", nullable: true },\n\t\tmax_amount: { type: \"text\", nullable: true },\n\t\tpox_addr_btc: { type: \"text\", nullable: true },\n\t\tpox_addr_hashbytes: { type: \"text\", nullable: true },\n\t\tpox_addr_version: { type: \"int\", nullable: true },\n\t\tresult_ok: { type: \"boolean\" },\n\t\treward_cycle: { type: \"int\", nullable: true },\n\t\tsigner_key: { type: \"text\", nullable: true },\n\t\tsigner_signature: { type: \"text\", nullable: true },\n\t\tsource_cursor: { type: \"text\" },\n\t\tstacker: { type: \"text\", nullable: true },\n\t\tstart_cycle: { type: \"int\", nullable: true },\n\t\ttx_id: { type: \"text\" },\n\t\ttx_index: { type: \"int\" },\n\t},\n\tpox5_events: {\n\t\tamount_sats: { type: \"text\", nullable: true },\n\t\tamount_ustx: { type: \"text\", nullable: true },\n\t\tblock_height: { type: \"int\" },\n\t\tblock_time: { type: \"timestamp\" },\n\t\tbond_index: { type: \"int\", nullable: true },\n\t\tcanonical: { type: \"boolean\" },\n\t\tcursor: { type: \"text\" },\n\t\tdata: { type: \"jsonb\", nullable: true },\n\t\tevent_index: { type: \"int\" },\n\t\tfirst_reward_cycle: { type: \"int\", nullable: true },\n\t\tis_l1_lock: { type: \"boolean\", nullable: true },\n\t\treward_cycle: { type: \"int\", nullable: true },\n\t\tsigner: { type: \"text\", nullable: true },\n\t\tsigner_key: { type: \"text\", nullable: true },\n\t\tsigner_manager: { type: \"text\", nullable: true },\n\t\tstaker: { type: \"text\", nullable: true },\n\t\ttopic: { type: \"text\" },\n\t\ttx_id: { type: \"text\" },\n\t\ttx_index: { type: \"int\" },\n\t\tunlock_burn_height: { type: \"int\", nullable: true },\n\t\tunlock_cycle: { type: \"int\", nullable: true },\n\t},\n\tsbtc_events: {\n\t\tamount: { type: \"text\", nullable: true },\n\t\tbitcoin_txid: { type: \"text\", nullable: true },\n\t\tblock_height: { type: \"int\" },\n\t\tblock_time: { type: \"timestamp\" },\n\t\tburn_hash: { type: \"text\", nullable: true },\n\t\tburn_height: { type: \"int\", nullable: true },\n\t\tcanonical: { type: \"boolean\" },\n\t\tcursor: { type: \"text\" },\n\t\tevent_index: { type: \"int\" },\n\t\tfee: { type: \"text\", nullable: true },\n\t\tgovernance_contract_type: { type: \"int\", nullable: true },\n\t\tgovernance_new_contract: { type: \"text\", nullable: true },\n\t\tmax_fee: { type: \"text\", nullable: true },\n\t\toutput_index: { type: \"int\", nullable: true },\n\t\trecipient_btc_hashbytes: { type: \"text\", nullable: true },\n\t\trecipient_btc_version: { type: \"int\", nullable: true },\n\t\trequest_id: { type: \"int\", nullable: true },\n\t\tsender: { type: \"text\", nullable: true },\n\t\tsigner_address: { type: \"text\", nullable: true },\n\t\tsigner_aggregate_pubkey: { type: \"text\", nullable: true },\n\t\tsigner_bitmap: { type: \"text\", nullable: true },\n\t\tsigner_keys_count: { type: \"int\", nullable: true },\n\t\tsigner_threshold: { type: \"int\", nullable: true },\n\t\tsweep_txid: { type: \"text\", nullable: true },\n\t\ttopic: { type: \"text\" },\n\t\ttx_id: { type: \"text\" },\n\t\ttx_index: { type: \"int\" },\n\t},\n\tsbtc_token_events: {\n\t\tamount: { type: \"text\" },\n\t\tblock_height: { type: \"int\" },\n\t\tblock_time: { type: \"timestamp\" },\n\t\tcanonical: { type: \"boolean\" },\n\t\tcursor: { type: \"text\" },\n\t\tevent_index: { type: \"int\" },\n\t\tevent_type: { type: \"text\" },\n\t\tmemo: { type: \"text\", nullable: true },\n\t\trecipient: { type: \"text\", nullable: true },\n\t\tsender: { type: \"text\", nullable: true },\n\t\ttx_id: { type: \"text\" },\n\t\ttx_index: { type: \"int\" },\n\t},\n\tbns_name_events: {\n\t\tblock_height: { type: \"int\" },\n\t\tblock_time: { type: \"timestamp\" },\n\t\tbns_id: { type: \"text\" },\n\t\tcanonical: { type: \"boolean\" },\n\t\tcursor: { type: \"text\" },\n\t\tevent_index: { type: \"int\" },\n\t\tfqn: { type: \"text\" },\n\t\thashed_salted_fqn_preorder: { type: \"text\", nullable: true },\n\t\timported_at: { type: \"int\", nullable: true },\n\t\tname: { type: \"text\" },\n\t\tnamespace: { type: \"text\" },\n\t\towner: { type: \"text\", nullable: true },\n\t\tpreordered_by: { type: \"text\", nullable: true },\n\t\tregistered_at: { type: \"int\", nullable: true },\n\t\trenewal_height: { type: \"int\", nullable: true },\n\t\tstx_burn: { type: \"text\", nullable: true },\n\t\ttopic: { type: \"text\" },\n\t\ttx_id: { type: \"text\" },\n\t\ttx_index: { type: \"int\" },\n\t},\n\tbns_namespace_events: {\n\t\tblock_height: { type: \"int\" },\n\t\tblock_time: { type: \"timestamp\" },\n\t\tcanonical: { type: \"boolean\" },\n\t\tcursor: { type: \"text\" },\n\t\tevent_index: { type: \"int\" },\n\t\tlaunched_at: { type: \"int\", nullable: true },\n\t\tlifetime: { type: \"int\", nullable: true },\n\t\tmanager: { type: \"text\", nullable: true },\n\t\tmanager_frozen: { type: \"boolean\", nullable: true },\n\t\tmanager_transfers_disabled: { type: \"boolean\", nullable: true },\n\t\tnamespace: { type: \"text\" },\n\t\tprice_frozen: { type: \"boolean\", nullable: true },\n\t\tprice_function: { type: \"text\", nullable: true },\n\t\trevealed_at: { type: \"int\", nullable: true },\n\t\tstatus: { type: \"text\" },\n\t\ttx_id: { type: \"text\" },\n\t\ttx_index: { type: \"int\" },\n\t},\n\tbns_marketplace_events: {\n\t\taction: { type: \"text\" },\n\t\tblock_height: { type: \"int\" },\n\t\tblock_time: { type: \"timestamp\" },\n\t\tbns_id: { type: \"text\" },\n\t\tcanonical: { type: \"boolean\" },\n\t\tcommission: { type: \"text\", nullable: true },\n\t\tcursor: { type: \"text\" },\n\t\tevent_index: { type: \"int\" },\n\t\tprice_ustx: { type: \"text\", nullable: true },\n\t\ttx_id: { type: \"text\" },\n\t\ttx_index: { type: \"int\" },\n\t},\n\tbns_names: {\n\t\tbns_id: { type: \"text\" },\n\t\tfqn: { type: \"text\" },\n\t\tlast_event_at: { type: \"timestamp\" },\n\t\tlast_event_cursor: { type: \"text\" },\n\t\tname: { type: \"text\" },\n\t\tnamespace: { type: \"text\" },\n\t\towner: { type: \"text\" },\n\t\tregistered_at: { type: \"int\", nullable: true },\n\t\trenewal_height: { type: \"int\", nullable: true },\n\t},\n\tbns_namespaces: {\n\t\tlast_event_at: { type: \"timestamp\" },\n\t\tlast_event_cursor: { type: \"text\" },\n\t\tlaunched_at: { type: \"int\", nullable: true },\n\t\tlifetime: { type: \"int\", nullable: true },\n\t\tmanager: { type: \"text\", nullable: true },\n\t\tmanager_frozen: { type: \"boolean\" },\n\t\tname_count: { type: \"int\" },\n\t\tnamespace: { type: \"text\" },\n\t\tprice_frozen: { type: \"boolean\" },\n\t},\n\tburn_block_rewards: {\n\t\tamount_sats: { type: \"text\" },\n\t\tburn_amount: { type: \"text\" },\n\t\tburn_block_hash: { type: \"text\" },\n\t\tburn_block_height: { type: \"int\" },\n\t\tcursor: { type: \"text\" },\n\t\trecipient_btc: { type: \"text\" },\n\t\treward_index: { type: \"int\" },\n\t},\n\tburn_block_reward_slots: {\n\t\tburn_block_hash: { type: \"text\" },\n\t\tburn_block_height: { type: \"int\" },\n\t\tcursor: { type: \"text\" },\n\t\tholder_btc: { type: \"text\" },\n\t\tslot_index: { type: \"int\" },\n\t},\n\tevents: {\n\t\tblock_height: { type: \"int\" },\n\t\tdata: { type: \"jsonb\" },\n\t\tevent_index: { type: \"int\" },\n\t\ttx_id: { type: \"text\" },\n\t\ttype: { type: \"text\" },\n\t},\n\tchain_reorgs: {\n\t\tdetected_at: { type: \"timestamp\" },\n\t},\n} as const satisfies Record<\n\tkeyof typeof SOURCE_READ_COLUMNS,\n\tRecord<string, IndexColumn>\n>;\n\nexport type IndexPrimaryKey = readonly string[] | null;\n\n// Primary key per read table, for codegen targets that require a model identity\n// (Prisma). Every listed column must be a read column (drift-tested). `null` = no\n// usable read-column key — the physical PK is a synthetic id excluded from the\n// read contract (chain_reorgs) — so Prisma omits the table. `events` uses its\n// natural unique (tx_id, event_index); its physical PK is a synthetic uuid not\n// surfaced in the read set.\nexport const SOURCE_READ_PKS = {\n\tblocks: [\"height\"],\n\tdecoded_events: [\"cursor\"],\n\ttransactions: [\"tx_id\"],\n\tmempool_transactions: [\"seq\"],\n\tpox4_calls: [\"cursor\"],\n\tpox5_events: [\"cursor\"],\n\tsbtc_events: [\"cursor\"],\n\tsbtc_token_events: [\"cursor\"],\n\tbns_name_events: [\"cursor\"],\n\tbns_namespace_events: [\"cursor\"],\n\tbns_marketplace_events: [\"cursor\"],\n\tbns_names: [\"fqn\"],\n\tbns_namespaces: [\"namespace\"],\n\tburn_block_rewards: [\"cursor\"],\n\tburn_block_reward_slots: [\"cursor\"],\n\tevents: [\"tx_id\", \"event_index\"],\n\tchain_reorgs: null,\n} as const satisfies Record<keyof typeof SOURCE_READ_COLUMNS, IndexPrimaryKey>;\n",
|
|
11
11
|
"/**\n * Per-migration DB-plane gating for the source/target split.\n *\n * Under the split, SOURCE (chain DB) holds only chain+decoded tables — the\n * control-plane tables were dropped to reclaim space — while TARGET (platform\n * DB) holds the control plane. `migrate.ts` still runs EVERY migration on EVERY\n * database (kysely integrity: the provider set must never be filtered per-DB, or\n * kysely throws \"previously executed migration is missing\" because each DB's\n * `kysely_migration` already records all of them). Instead, `migrate.ts` sets a\n * role before each pass and a migration gates its DDL with the helpers below: a\n * control migration's `up()` is a no-op on SOURCE but is still recorded applied,\n * so there is no missing-migration error and no re-run.\n *\n * `'both'` is single-DB / collapsed-split mode (dev / OSS / CI): every helper\n * runs, identical to pre-split behavior.\n *\n * Authoring a new migration under the split:\n * export async function up(db: Kysely<unknown>): Promise<void> {\n * await onControlPlane(() => sql`ALTER TABLE accounts ADD COLUMN …`.execute(db));\n * await onChainPlane(() => sql`ALTER TABLE blocks ADD COLUMN …`.execute(db));\n * // schema-wide / mixed statements: leave unwrapped (run on both).\n * }\n */\n\nexport type MigrationRole = \"source\" | \"target\" | \"both\";\n\n// Module-level singleton: migrate runs sequentially in one process, one target\n// at a time, so there is no concurrency to guard.\nlet currentRole: MigrationRole = \"both\";\n\nexport function setMigrationRole(role: MigrationRole): void {\n\tcurrentRole = role;\n}\n\nexport function getMigrationRole(): MigrationRole {\n\treturn currentRole;\n}\n\n/** Run control-plane (TARGET) DDL only when this pass targets the control plane. */\nexport async function onControlPlane(fn: () => Promise<void>): Promise<void> {\n\tif (currentRole === \"target\" || currentRole === \"both\") await fn();\n}\n\n/** Run chain/decoded (SOURCE) DDL only when this pass targets the chain plane. */\nexport async function onChainPlane(fn: () => Promise<void>): Promise<void> {\n\tif (currentRole === \"source\" || currentRole === \"both\") await fn();\n}\n",
|
|
12
12
|
"import type { Database } from \"./types.ts\";\n\nexport type DbPlane = \"source\" | \"target\" | \"both\";\n\n/**\n * Canonical table → DB-plane mapping for the source/target split.\n *\n * SOURCE = chain + decoded (the `postgres` instance); TARGET = control plane\n * (the `postgres-platform` instance). `both` = present/used on both planes\n * (`service_heartbeats`: the indexer writes its row on SOURCE, the\n * subgraph-processor writes its row on TARGET).\n *\n * `satisfies Record<keyof Database, DbPlane>` makes adding a table to `Database`\n * without classifying it here a COMPILE error. This is the single source of\n * truth for the split — `docker/SCHEMA_SPLIT.md` and the cutover script's\n * `CONTROL_TABLES` mirror the `target` set (guarded by `table-plane.test.ts`).\n * Use it to decide which migration helper a table's DDL belongs in\n * (`onControlPlane` for `target`, `onChainPlane` for `source`).\n *\n * Note: `kysely_migration` / `kysely_migration_lock` are kysely-managed (not in\n * `Database`) and exist on both — they are intentionally not listed here.\n */\nexport const TABLE_TO_DB = {\n\t// ── SOURCE: raw chain ──\n\tblocks: \"source\",\n\ttransactions: \"source\",\n\tevents: \"source\",\n\ttransactions_archive: \"source\",\n\tevents_archive: \"source\",\n\tdead_letter_events: \"source\",\n\tmempool_transactions: \"source\",\n\tindex_progress: \"source\",\n\tcontracts: \"source\",\n\tchain_reorgs: \"source\",\n\t// Chain-derived read memo — keyed on index_block_hash, lives with the\n\t// blocks it pins to.\n\tchain_read_cache: \"source\",\n\t// Staged fork contenders — raw chain data, resolved during ingest.\n\tpending_fork_blocks: \"source\",\n\t// Exact observer callback receipts — the replay authority sits with chain data.\n\tobserver_journal: \"source\",\n\t// Declared scope — belongs with the chain whose start it declares.\n\tsync_scopes: \"source\",\n\t// Coverage kernel — attests the chain it sits next to.\n\tstage_registry: \"source\",\n\tstage_runs: \"source\",\n\tstage_block_receipts: \"source\",\n\tcoverage_segments: \"source\",\n\tstage_failures: \"source\",\n\t// ── SOURCE: decoded ──\n\tdecoded_events: \"source\",\n\tdecoder_checkpoints: \"source\",\n\tpox4_calls: \"source\",\n\tpox4_cycles_daily: \"source\",\n\tpox4_signers_daily: \"source\",\n\tpox5_events: \"source\",\n\tburn_block_rewards: \"source\",\n\tburn_block_reward_slots: \"source\",\n\tsbtc_events: \"source\",\n\tsbtc_token_events: \"source\",\n\tsbtc_settlements: \"source\",\n\tsbtc_supply_snapshots: \"source\",\n\tbns_name_events: \"source\",\n\tbns_namespace_events: \"source\",\n\tbns_marketplace_events: \"source\",\n\tbns_names: \"source\",\n\tbns_namespaces: \"source\",\n\t// ── TARGET: accounts / auth / billing ──\n\tinstances: \"target\",\n\taccounts: \"target\",\n\tapi_keys: \"target\",\n\tsessions: \"target\",\n\tmagic_links: \"target\",\n\taccount_spend_caps: \"target\",\n\tprocessed_stripe_events: \"target\",\n\t// ── TARGET: subscriptions ──\n\tsubscriptions: \"target\",\n\tsubscription_outbox: \"target\",\n\tsubscription_deliveries: \"target\",\n\ttrigger_evaluator_state: \"target\",\n\t// ── TARGET: subgraphs + metadata ──\n\tsubgraphs: \"target\",\n\tsubgraph_operations: \"target\",\n\tsubgraph_health_snapshots: \"target\",\n\tsubgraph_gaps: \"target\",\n\tsubgraph_processing_stats: \"target\",\n\tsubgraph_table_snapshots: \"target\",\n\t// ── TARGET: x402 payment rail ──\n\tx402_payments: \"target\",\n\tx402_balances: \"target\",\n\taccount_credits: \"target\",\n\t// ── TARGET: archive fetch gate (design-f089) ──\n\tarchive_fetches: \"target\",\n\t// ── BOTH ──\n\tservice_heartbeats: \"both\",\n} satisfies Record<keyof Database, DbPlane>;\n",
|
|
13
13
|
"import type { Kysely, Transaction } from \"kysely\";\nimport { getSourceDb, sql } from \"../index.ts\";\nimport type { Database } from \"../types.ts\";\n\nexport type ChainReorgCursor = {\n\tblock_height: number;\n\tevent_index: number;\n};\n\n/**\n * Wall-clock resume cursor for `readChainReorgsSince`. `detected_at` is kept as\n * a raw string all the way to SQL (cast to timestamptz there) because Postgres\n * stores microseconds and a JS Date round-trip truncates to milliseconds —\n * truncation makes `detected_at > cursor` re-match the row the cursor came\n * from, re-delivering the same reorg forever. `id` breaks ties between rows\n * sharing a detected_at.\n */\nexport type ChainReorgTimeCursor = {\n\tdetected_at: string;\n\tid: string | null;\n};\n\nexport type ChainReorgRecord = {\n\tid: string;\n\tdetected_at: string;\n\tfork_point_height: number;\n\told_index_block_hash: string | null;\n\tnew_index_block_hash: string | null;\n\torphaned_range: { from: string; to: string };\n\tnew_canonical_tip: string;\n};\n\nexport type InsertChainReorgParams = {\n\tforkPointHeight: number;\n\toldIndexBlockHash?: string | null;\n\tnewIndexBlockHash?: string | null;\n\torphanedFrom: ChainReorgCursor;\n\torphanedTo: ChainReorgCursor;\n\tnewCanonicalTip: ChainReorgCursor;\n\tdb?: Kysely<Database> | Transaction<Database>;\n};\n\nexport type ReadChainReorgsSinceParams = {\n\tsince: Date | ChainReorgTimeCursor | ChainReorgCursor;\n\tlimit: number;\n\tdb?: Kysely<Database>;\n};\n\nexport type ReadChainReorgsForRangeParams = {\n\tfrom: ChainReorgCursor;\n\tto: ChainReorgCursor;\n\tdb?: Kysely<Database>;\n};\n\nexport type ReadChainReorgsForHeightRangeParams = {\n\tfromHeight: number;\n\ttoHeight: number;\n\tdb?: Kysely<Database>;\n};\n\ntype ChainReorgRow = {\n\tid: string;\n\tdetected_at: Date | string;\n\t/** Microsecond-precision ISO text, selected alongside `detected_at` where\n\t * the value feeds a resume cursor (pg's Date parsing drops microseconds). */\n\tdetected_at_us?: string;\n\tfork_point_height: string | number;\n\told_index_block_hash: string | null;\n\tnew_index_block_hash: string | null;\n\torphaned_from_height: string | number;\n\torphaned_from_event_index: string | number;\n\torphaned_to_height: string | number;\n\torphaned_to_event_index: string | number;\n\tnew_canonical_height: string | number;\n\tnew_canonical_event_index: string | number;\n};\n\nexport function encodeChainReorgCursor(cursor: ChainReorgCursor): string {\n\treturn `${cursor.block_height}:${cursor.event_index}`;\n}\n\nfunction normalizeRow(row: ChainReorgRow): ChainReorgRecord {\n\tconst detectedAt =\n\t\trow.detected_at_us ??\n\t\t(row.detected_at instanceof Date\n\t\t\t? row.detected_at.toISOString()\n\t\t\t: new Date(row.detected_at).toISOString());\n\n\treturn {\n\t\tid: row.id,\n\t\tdetected_at: detectedAt,\n\t\tfork_point_height: Number(row.fork_point_height),\n\t\told_index_block_hash: row.old_index_block_hash,\n\t\tnew_index_block_hash: row.new_index_block_hash,\n\t\torphaned_range: {\n\t\t\tfrom: encodeChainReorgCursor({\n\t\t\t\tblock_height: Number(row.orphaned_from_height),\n\t\t\t\tevent_index: Number(row.orphaned_from_event_index),\n\t\t\t}),\n\t\t\tto: encodeChainReorgCursor({\n\t\t\t\tblock_height: Number(row.orphaned_to_height),\n\t\t\t\tevent_index: Number(row.orphaned_to_event_index),\n\t\t\t}),\n\t\t},\n\t\tnew_canonical_tip: encodeChainReorgCursor({\n\t\t\tblock_height: Number(row.new_canonical_height),\n\t\t\tevent_index: Number(row.new_canonical_event_index),\n\t\t}),\n\t};\n}\n\nexport async function insertChainReorg(\n\tparams: InsertChainReorgParams,\n): Promise<ChainReorgRecord> {\n\tconst db = params.db ?? getSourceDb();\n\tconst row = await db\n\t\t.insertInto(\"chain_reorgs\")\n\t\t.values({\n\t\t\tfork_point_height: params.forkPointHeight,\n\t\t\told_index_block_hash: params.oldIndexBlockHash ?? null,\n\t\t\tnew_index_block_hash: params.newIndexBlockHash ?? null,\n\t\t\torphaned_from_height: params.orphanedFrom.block_height,\n\t\t\torphaned_from_event_index: params.orphanedFrom.event_index,\n\t\t\torphaned_to_height: params.orphanedTo.block_height,\n\t\t\torphaned_to_event_index: params.orphanedTo.event_index,\n\t\t\tnew_canonical_height: params.newCanonicalTip.block_height,\n\t\t\tnew_canonical_event_index: params.newCanonicalTip.event_index,\n\t\t})\n\t\t.returningAll()\n\t\t.executeTakeFirstOrThrow();\n\n\treturn normalizeRow(row);\n}\n\n/** Microsecond-precision ISO text for `detected_at`. Selected wherever the\n * value can feed a resume cursor: pg parses timestamptz into a JS Date, which\n * drops microseconds, and a truncated cursor re-matches the row it came from. */\nconst detectedAtUs = sql<string>`to_char(detected_at AT TIME ZONE 'UTC', 'YYYY-MM-DD\"T\"HH24:MI:SS.US\"Z\"')`;\n\nexport async function readChainReorgsSince(\n\tparams: ReadChainReorgsSinceParams,\n): Promise<ChainReorgRecord[]> {\n\tconst db = params.db ?? getSourceDb();\n\tconst limit = Math.min(1000, Math.max(1, params.limit));\n\tconst { since } = params;\n\n\tif (since instanceof Date || \"detected_at\" in since) {\n\t\tconst cursor: ChainReorgTimeCursor =\n\t\t\tsince instanceof Date\n\t\t\t\t? { detected_at: since.toISOString(), id: null }\n\t\t\t\t: since;\n\t\t// The (…::text)::timestamptz double cast is load-bearing: a bare\n\t\t// ::timestamptz makes the driver infer the param as a timestamp and\n\t\t// convert the string client-side at millisecond precision, silently\n\t\t// discarding microseconds — the exact truncation this cursor exists to\n\t\t// avoid. ::text forces the raw string through so Postgres does the cast.\n\t\tconst result = cursor.id\n\t\t\t? await sql<ChainReorgRow>`\n\t\t\t\t\tSELECT *, ${detectedAtUs} AS detected_at_us\n\t\t\t\t\tFROM chain_reorgs\n\t\t\t\t\tWHERE (detected_at, id) > ((${cursor.detected_at}::text)::timestamptz, ${cursor.id}::uuid)\n\t\t\t\t\tORDER BY detected_at ASC, id ASC\n\t\t\t\t\tLIMIT ${limit}\n\t\t\t\t`.execute(db)\n\t\t\t: await sql<ChainReorgRow>`\n\t\t\t\t\tSELECT *, ${detectedAtUs} AS detected_at_us\n\t\t\t\t\tFROM chain_reorgs\n\t\t\t\t\tWHERE detected_at > (${cursor.detected_at}::text)::timestamptz\n\t\t\t\t\tORDER BY detected_at ASC, id ASC\n\t\t\t\t\tLIMIT ${limit}\n\t\t\t\t`.execute(db);\n\t\treturn result.rows.map(normalizeRow);\n\t}\n\n\tconst result = await sql<ChainReorgRow>`\n\t\tSELECT *, ${detectedAtUs} AS detected_at_us\n\t\tFROM chain_reorgs\n\t\tWHERE\n\t\t\torphaned_to_height > ${since.block_height}\n\t\t\tOR (\n\t\t\t\torphaned_to_height = ${since.block_height}\n\t\t\t\tAND orphaned_to_event_index >= ${since.event_index}\n\t\t\t)\n\t\tORDER BY detected_at ASC, id ASC\n\t\tLIMIT ${limit}\n\t`.execute(db);\n\n\treturn result.rows.map(normalizeRow);\n}\n\n/**\n * Reorgs overlapping the block-height window [fromHeight, toHeight], ignoring\n * event_index. For cursor keyspaces that are NOT event-indexed (the\n * transactions / contract-calls endpoints key on block_height:tx_index): a\n * height-granular overlap. Over-inclusive — a partially-orphaned height in range\n * surfaces even if the page's specific rows survived — but never under-reports.\n */\nexport async function readChainReorgsForHeightRange(\n\tparams: ReadChainReorgsForHeightRangeParams,\n): Promise<ChainReorgRecord[]> {\n\treturn readChainReorgsForRange({\n\t\tfrom: { block_height: params.fromHeight, event_index: 0 },\n\t\t// int4 max — event_index is a Postgres `integer`, so a larger sentinel\n\t\t// (e.g. Number.MAX_SAFE_INTEGER) overflows when bound for comparison.\n\t\tto: { block_height: params.toHeight, event_index: 2_147_483_647 },\n\t\tdb: params.db,\n\t});\n}\n\nexport async function readChainReorgsForRange(\n\tparams: ReadChainReorgsForRangeParams,\n): Promise<ChainReorgRecord[]> {\n\tconst db = params.db ?? getSourceDb();\n\tconst { from, to } = params;\n\tconst { rows } = await sql<ChainReorgRow>`\n\t\tSELECT *, ${detectedAtUs} AS detected_at_us\n\t\tFROM chain_reorgs\n\t\tWHERE\n\t\t\t(\n\t\t\t\torphaned_from_height < ${to.block_height}\n\t\t\t\tOR (\n\t\t\t\t\torphaned_from_height = ${to.block_height}\n\t\t\t\t\tAND orphaned_from_event_index <= ${to.event_index}\n\t\t\t\t)\n\t\t\t)\n\t\t\tAND (\n\t\t\t\torphaned_to_height > ${from.block_height}\n\t\t\t\tOR (\n\t\t\t\t\torphaned_to_height = ${from.block_height}\n\t\t\t\t\tAND orphaned_to_event_index >= ${from.event_index}\n\t\t\t\t)\n\t\t\t)\n\t\tORDER BY detected_at ASC, id ASC\n\t`.execute(db);\n\n\treturn rows.map(normalizeRow);\n}\n"
|
|
@@ -991,13 +991,6 @@ declare function registerSubgraph(db: Kysely<Database>, data: {
|
|
|
991
991
|
}): Promise<Subgraph>;
|
|
992
992
|
declare function getSubgraph(db: Kysely<Database>, name: string, accountId?: string): Promise<Subgraph | null>;
|
|
993
993
|
declare function listSubgraphs(db: Kysely<Database>, accountId?: string): Promise<Subgraph[]>;
|
|
994
|
-
/**
|
|
995
|
-
* Resolve a public subgraph by name. Public names are a single global
|
|
996
|
-
* namespace (partial unique index `subgraphs_public_name_uidx`), so at most
|
|
997
|
-
* one row matches regardless of account.
|
|
998
|
-
*/
|
|
999
|
-
declare function findPublicSubgraphByName(db: Kysely<Database>, name: string): Promise<Subgraph | null>;
|
|
1000
|
-
declare function updateSubgraphVisibility(db: Kysely<Database>, name: string, accountId: string, visibility: "public" | "private"): Promise<void>;
|
|
1001
994
|
declare function updateSubgraphStatus(db: Kysely<Database>, name: string, status: string, lastProcessedBlock?: number): Promise<void>;
|
|
1002
995
|
/** Live-walk ERROR write (f069): a block whose handlers ALL failed
|
|
1003
996
|
* (`processed === 0 && errors > 0`, `block-processor.ts`'s `applyProgress`)
|
|
@@ -1074,4 +1067,4 @@ declare function recordLiveProgress(db: Kysely<Database>, name: string, lastProc
|
|
|
1074
1067
|
* forward write between the rewind and the fork block's reprocess. Do not
|
|
1075
1068
|
* call this from the live catch-up path. */
|
|
1076
1069
|
declare function rewindLiveProgress(db: Kysely<Database>, name: string, lastProcessedBlock: number): Promise<void>;
|
|
1077
|
-
export {
|
|
1070
|
+
export { updateSubgraphStatus, updateSubgraphHandlerPath, updateSubgraphExpiry, rewindLiveProgress, registerSubgraph, recordSubgraphProcessed, recordLiveProgress, recordLiveError, pgSchemaNameFor, pgSchemaName, localSubgraphAccountId, listSubgraphs, getSubgraph, deleteSubgraph };
|
|
@@ -134,12 +134,6 @@ async function listSubgraphs(db, accountId) {
|
|
|
134
134
|
}
|
|
135
135
|
return query.execute();
|
|
136
136
|
}
|
|
137
|
-
async function findPublicSubgraphByName(db, name) {
|
|
138
|
-
return await db.selectFrom("subgraphs").selectAll().where("name", "=", name).where("visibility", "=", "public").executeTakeFirst() ?? null;
|
|
139
|
-
}
|
|
140
|
-
async function updateSubgraphVisibility(db, name, accountId, visibility) {
|
|
141
|
-
await db.updateTable("subgraphs").set({ visibility, updated_at: new Date }).where("name", "=", name).where("account_id", "=", accountId).execute();
|
|
142
|
-
}
|
|
143
137
|
async function updateSubgraphStatus(db, name, status, lastProcessedBlock) {
|
|
144
138
|
await db.updateTable("subgraphs").set({
|
|
145
139
|
status,
|
|
@@ -205,7 +199,6 @@ async function rewindLiveProgress(db, name, lastProcessedBlock) {
|
|
|
205
199
|
}).where("name", "=", name).execute();
|
|
206
200
|
}
|
|
207
201
|
export {
|
|
208
|
-
updateSubgraphVisibility,
|
|
209
202
|
updateSubgraphStatus,
|
|
210
203
|
updateSubgraphHandlerPath,
|
|
211
204
|
updateSubgraphExpiry,
|
|
@@ -219,9 +212,8 @@ export {
|
|
|
219
212
|
localSubgraphAccountId,
|
|
220
213
|
listSubgraphs,
|
|
221
214
|
getSubgraph,
|
|
222
|
-
findPublicSubgraphByName,
|
|
223
215
|
deleteSubgraph
|
|
224
216
|
};
|
|
225
217
|
|
|
226
|
-
//# debugId=
|
|
218
|
+
//# debugId=B3ECE96E3D7D4AE664756E2164756E21
|
|
227
219
|
//# sourceMappingURL=subgraphs.js.map
|
|
@@ -4,9 +4,9 @@
|
|
|
4
4
|
"sourcesContent": [
|
|
5
5
|
"/**\n * Instance modes for the Secondlayer platform.\n *\n * - `oss`: self-hosted, single-tenant. No auth middleware, no platform routes\n * (projects, admin). Everything runs against a single `DATABASE_URL`.\n * Intended for `docker compose up`.\n *\n * - `platform`: the metered archive deployment. Magic-link auth, API keys,\n * accounts, credits, and metered `/v1` reads (Index + Streams + subgraphs)\n * against a single shared DB. No paid tier ladder — one metered `free`\n * tier plus first-party internal service credentials.\n */\n\nexport type InstanceMode = \"oss\" | \"platform\";\n\n/** What the operator wrote in the env — `archive` is the honest name for the\n * metered public-archive deployment and aliases `platform` behaviorally. */\nexport type DeclaredInstanceMode = InstanceMode | \"archive\";\n\nconst VALID_MODES: readonly DeclaredInstanceMode[] = [\n\t\"oss\",\n\t\"platform\",\n\t\"archive\",\n];\n\nfunction declaredMode(): DeclaredInstanceMode {\n\tconst raw = process.env.INSTANCE_MODE?.trim().toLowerCase();\n\tif (raw && (VALID_MODES as readonly string[]).includes(raw)) {\n\t\treturn raw as DeclaredInstanceMode;\n\t}\n\treturn \"oss\";\n}\n\n/**\n * Resolve the active instance mode from `process.env.INSTANCE_MODE`.\n * Defaults to `\"oss\"` — the safest default for self-hosters who deploy\n * without setting the variable. `archive` normalizes to `platform` here so\n * every behavioral branch stays a two-way choice; use\n * `getDeclaredInstanceMode()` for display surfaces.\n */\nexport function getInstanceMode(): InstanceMode {\n\tconst declared = declaredMode();\n\treturn declared === \"archive\" ? \"platform\" : declared;\n}\n\n/** The mode as declared in the env, for status/identity surfaces. */\nexport function getDeclaredInstanceMode(): DeclaredInstanceMode {\n\treturn declaredMode();\n}\n\n/**\n * True when public reads are credit-metered (the free window, retention, and\n * credits gates arm). Platform/archive deployments meter by default;\n * self-host never does unless the operator explicitly sets\n * `METERED_READS=true`. `METERED_READS=false` disarms a metered deployment.\n */\nexport function isMeteredReads(): boolean {\n\tconst override = process.env.METERED_READS?.trim().toLowerCase();\n\tif (override === \"true\") return true;\n\tif (override === \"false\") return false;\n\treturn getInstanceMode() === \"platform\";\n}\n\n/** True when the active mode is `\"platform\"` (shared multi-tenant). */\nexport function isPlatformMode(): boolean {\n\treturn getInstanceMode() === \"platform\";\n}\n\n/** True when the active mode is `\"oss\"` (self-hosted). */\nexport function isOssMode(): boolean {\n\treturn getInstanceMode() === \"oss\";\n}\n",
|
|
6
6
|
"import { type RawBuilder, sql } from \"kysely\";\n\n/**\n * Safely encode a JS value as a JSONB literal for Kysely inserts/updates.\n * Kysely + postgres.js double-encodes JSON when using parameterized queries\n * with ::jsonb casts. This uses sql.raw to inline a properly escaped literal.\n *\n * Generic parameter lets callers set the RawBuilder's output type so they\n * don't need to cast at the insert site. Default is `unknown` — widen at\n * the call site when the column type is narrower, e.g. `jsonb<MyShape>(...)`.\n */\nexport function jsonb<T = unknown>(value: T): RawBuilder<T> {\n\tconst escaped = JSON.stringify(value, (_k, v) =>\n\t\ttypeof v === \"bigint\" ? v.toString() : v,\n\t).replace(/'/g, \"''\");\n\treturn sql`${sql.raw(`'${escaped}'::jsonb`)}`;\n}\n\n/**\n * Safely parse a JSONB value from the database.\n * Handles double-encoded strings where postgres.js returns a JSON string\n * instead of a parsed object.\n */\nexport function parseJsonb<T = unknown>(value: unknown): T {\n\tif (typeof value === \"string\") {\n\t\ttry {\n\t\t\treturn JSON.parse(value) as T;\n\t\t} catch {\n\t\t\treturn value as T;\n\t\t}\n\t}\n\treturn (value ?? {}) as T;\n}\n",
|
|
7
|
-
"import { type Kysely, sql } from \"kysely\";\nimport { isPlatformMode } from \"../../mode.ts\";\nimport { jsonb } from \"../jsonb.ts\";\nimport type { Database, Subgraph } from \"../types.ts\";\n\n/**\n * Convert a subgraph name to its PostgreSQL schema name (legacy form).\n * Pre shared-rip every tenant DB had its own schema namespace so disambiguation\n * was implicit. Kept for oss mode (single-tenant) and legacy-row fallback.\n * Platform-mode deploys use `pgSchemaNameFor(accountId, name)`.\n */\nexport function pgSchemaName(subgraphName: string): string {\n\tconst safeName = subgraphName.replace(/-/g, \"_\");\n\treturn `subgraph_${safeName}`;\n}\n\n/** OSS writes empty account_id — name is the local unique key. */\nexport function localSubgraphAccountId(accountId?: string): string {\n\tif (!isPlatformMode()) return \"\";\n\treturn accountId ?? \"\";\n}\n\n/**\n * Account-scoped schema name. Matches migration 0028's rename pattern:\n * subgraph_{first8charsOfAccountId, dashes-as-underscores}_{name}\n * Empty accountId falls back to legacy form (oss mode).\n */\nexport function pgSchemaNameFor(\n\taccountId: string,\n\tsubgraphName: string,\n): string {\n\tif (!accountId) return pgSchemaName(subgraphName);\n\tconst accountPrefix = accountId.slice(0, 8).replace(/-/g, \"_\");\n\tconst safeName = subgraphName.replace(/-/g, \"_\");\n\treturn `subgraph_${accountPrefix}_${safeName}`;\n}\n\nexport async function registerSubgraph(\n\tdb: Kysely<Database>,\n\tdata: {\n\t\tname: string;\n\t\tversion: string;\n\t\tdefinition: Record<string, unknown>;\n\t\tschemaHash: string;\n\t\thandlerPath: string;\n\t\tapiKeyId?: string;\n\t\taccountId?: string;\n\t\tschemaName?: string;\n\t\tstartBlock?: number;\n\t\thandlerCode?: string;\n\t\tsourceCode?: string;\n\t},\n): Promise<Subgraph> {\n\tconst accountId = localSubgraphAccountId(data.accountId);\n\tconst values = {\n\t\tname: data.name,\n\t\tversion: data.version,\n\t\tdefinition: jsonb<Record<string, unknown>>(data.definition),\n\t\tschema_hash: data.schemaHash,\n\t\thandler_path: data.handlerPath,\n\t\taccount_id: accountId,\n\t\thandler_code: data.handlerCode ?? null,\n\t\tsource_code: data.sourceCode ?? null,\n\t\tschema_name: data.schemaName ?? null,\n\t\tstart_block: data.startBlock ?? 0,\n\t\tdatabase_url_enc: null,\n\t};\n\tconst updateSet = {\n\t\tversion: data.version,\n\t\tdefinition: jsonb<Record<string, unknown>>(data.definition),\n\t\tschema_hash: data.schemaHash,\n\t\thandler_path: data.handlerPath,\n\t\thandler_code: data.handlerCode ?? null,\n\t\tsource_code: data.sourceCode ?? null,\n\t\tschema_name: data.schemaName ?? null,\n\t\tstart_block: data.startBlock ?? 0,\n\t\tdatabase_url_enc: null,\n\t\tupdated_at: new Date(),\n\t};\n\n\t// OSS: name is unique. Upsert by name so leftover rows with a non-empty\n\t// account_id (pre-local-namespace deploys) don't insert a second row.\n\tif (!isPlatformMode()) {\n\t\tconst existing = await db\n\t\t\t.selectFrom(\"subgraphs\")\n\t\t\t.select(\"id\")\n\t\t\t.where(\"name\", \"=\", data.name)\n\t\t\t.executeTakeFirst();\n\t\tif (existing) {\n\t\t\treturn await db\n\t\t\t\t.updateTable(\"subgraphs\")\n\t\t\t\t.set({ ...updateSet, account_id: \"\" })\n\t\t\t\t.where(\"id\", \"=\", existing.id)\n\t\t\t\t.returningAll()\n\t\t\t\t.executeTakeFirstOrThrow();\n\t\t}\n\t\treturn await db\n\t\t\t.insertInto(\"subgraphs\")\n\t\t\t.values(values)\n\t\t\t.returningAll()\n\t\t\t.executeTakeFirstOrThrow();\n\t}\n\n\treturn await db\n\t\t.insertInto(\"subgraphs\")\n\t\t.values(values)\n\t\t.onConflict((oc) =>\n\t\t\toc.columns([\"name\", \"account_id\"]).doUpdateSet(updateSet),\n\t\t)\n\t\t.returningAll()\n\t\t.executeTakeFirstOrThrow();\n}\n\nexport async function getSubgraph(\n\tdb: Kysely<Database>,\n\tname: string,\n\taccountId?: string,\n): Promise<Subgraph | null> {\n\tlet query = db.selectFrom(\"subgraphs\").selectAll().where(\"name\", \"=\", name);\n\n\tif (isPlatformMode() && accountId !== undefined) {\n\t\tquery = query.where(\"account_id\", \"=\", accountId);\n\t}\n\n\treturn (await query.executeTakeFirst()) ?? null;\n}\n\nexport async function listSubgraphs(\n\tdb: Kysely<Database>,\n\taccountId?: string,\n): Promise<Subgraph[]> {\n\tlet query = db.selectFrom(\"subgraphs\").selectAll();\n\tif (isPlatformMode() && accountId !== undefined) {\n\t\tquery = query.where(\"account_id\", \"=\", accountId);\n\t}\n\treturn query.execute();\n}\n\n/**\n * Resolve a public subgraph by name. Public names are a single global\n * namespace (partial unique index `subgraphs_public_name_uidx`), so at most\n * one row matches regardless of account.\n */\nexport async function findPublicSubgraphByName(\n\tdb: Kysely<Database>,\n\tname: string,\n): Promise<Subgraph | null> {\n\treturn (\n\t\t(await db\n\t\t\t.selectFrom(\"subgraphs\")\n\t\t\t.selectAll()\n\t\t\t.where(\"name\", \"=\", name)\n\t\t\t.where(\"visibility\", \"=\", \"public\")\n\t\t\t.executeTakeFirst()) ?? null\n\t);\n}\n\nexport async function updateSubgraphVisibility(\n\tdb: Kysely<Database>,\n\tname: string,\n\taccountId: string,\n\tvisibility: \"public\" | \"private\",\n): Promise<void> {\n\tawait db\n\t\t.updateTable(\"subgraphs\")\n\t\t.set({ visibility, updated_at: new Date() })\n\t\t.where(\"name\", \"=\", name)\n\t\t.where(\"account_id\", \"=\", accountId)\n\t\t.execute();\n}\n\nexport async function updateSubgraphStatus(\n\tdb: Kysely<Database>,\n\tname: string,\n\tstatus: string,\n\tlastProcessedBlock?: number,\n): Promise<void> {\n\tawait db\n\t\t.updateTable(\"subgraphs\")\n\t\t.set({\n\t\t\tstatus,\n\t\t\t...(lastProcessedBlock !== undefined\n\t\t\t\t? { last_processed_block: lastProcessedBlock }\n\t\t\t\t: {}),\n\t\t\tupdated_at: new Date(),\n\t\t})\n\t\t.where(\"name\", \"=\", name)\n\t\t.execute();\n}\n\n/** Live-walk ERROR write (f069): a block whose handlers ALL failed\n * (`processed === 0 && errors > 0`, `block-processor.ts`'s `applyProgress`)\n * is stamped 'error' and its height is recorded as processed, same as\n * `updateSubgraphStatus(db, name, \"error\", blockHeight)` — except the\n * cursor half of that write now goes through the same conditional `<`\n * guard as {@link recordLiveProgress}. Marking a subgraph 'error' is not\n * itself replay-sensitive, but it shares the `last_processed_block` column\n * with the replay guard: an unconditional write here could regress the\n * cursor a racing (successful) writer already advanced past, which is\n * exactly the regress-then-re-walk loop this guard exists to close. In\n * practice `ctx.rollbackTo` (`runner.ts`) means an all-failed block never\n * flushes writes, so a lost race here is always a silent no-op, never a\n * case that needs `CursorRaceLostError`-style tx abort. Returns whether\n * the write applied. Live path only — reindex/backfill's error stamping\n * goes through {@link updateSubgraphStatus} unconditionally, as before. */\nexport async function recordLiveError(\n\tdb: Kysely<Database>,\n\tname: string,\n\tblockHeight: number,\n): Promise<boolean> {\n\tconst result = await db\n\t\t.updateTable(\"subgraphs\")\n\t\t.set({\n\t\t\tstatus: \"error\",\n\t\t\tlast_processed_block: blockHeight,\n\t\t\tupdated_at: new Date(),\n\t\t})\n\t\t.where(\"name\", \"=\", name)\n\t\t.where(\"last_processed_block\", \"<\", blockHeight)\n\t\t.executeTakeFirst();\n\treturn Number(result.numUpdatedRows ?? 0n) > 0;\n}\n\nexport async function recordSubgraphProcessed(\n\tdb: Kysely<Database>,\n\tname: string,\n\tprocessed: number,\n\terrors: number,\n\tlastError?: string,\n): Promise<void> {\n\tawait db\n\t\t.updateTable(\"subgraphs\")\n\t\t.set({\n\t\t\ttotal_processed: sql`total_processed + ${processed}`,\n\t\t\ttotal_errors: sql`total_errors + ${errors}`,\n\t\t\t...(lastError\n\t\t\t\t? { last_error: lastError, last_error_at: new Date() }\n\t\t\t\t: {}),\n\t\t\tupdated_at: new Date(),\n\t\t})\n\t\t.where(\"name\", \"=\", name)\n\t\t.execute();\n}\n\nexport async function updateSubgraphHandlerPath(\n\tdb: Kysely<Database>,\n\tname: string,\n\thandlerPath: string,\n\topts?: { handlerCode?: string; sourceCode?: string },\n): Promise<void> {\n\tawait db\n\t\t.updateTable(\"subgraphs\")\n\t\t.set({\n\t\t\thandler_path: handlerPath,\n\t\t\t...(opts?.handlerCode != null ? { handler_code: opts.handlerCode } : {}),\n\t\t\t...(opts?.sourceCode != null ? { source_code: opts.sourceCode } : {}),\n\t\t\tupdated_at: new Date(),\n\t\t})\n\t\t.where(\"name\", \"=\", name)\n\t\t.execute();\n}\n\nexport async function deleteSubgraph(\n\tdb: Kysely<Database>,\n\tname: string,\n\taccountId?: string,\n): Promise<Subgraph | null> {\n\tconst subgraph = await getSubgraph(db, name, accountId);\n\tif (!subgraph) return null;\n\n\t// Use stored schema_name if available, otherwise compute\n\tconst schemaName = subgraph.schema_name ?? pgSchemaName(name);\n\n\t// Cascade to subscriptions: a subscription pointing at a deleted\n\t// subgraph + table will throw `relation does not exist` on every\n\t// subsequent emission. Pause active subs and purge any pending outbox\n\t// rows so receivers don't get phantom replays. We don't delete the\n\t// subscriptions themselves — operators may want to repoint them at a\n\t// resurrected subgraph; we just stop them firing.\n\tawait db\n\t\t.updateTable(\"subscriptions\")\n\t\t.set({\n\t\t\tstatus: \"paused\",\n\t\t\tlast_error: `Subgraph \"${name}\" deleted; subscription auto-paused.`,\n\t\t\tupdated_at: new Date(),\n\t\t})\n\t\t.where(\"subgraph_name\", \"=\", name)\n\t\t.execute();\n\tawait db\n\t\t.deleteFrom(\"subscription_outbox\")\n\t\t.where(\"status\", \"=\", \"pending\")\n\t\t.where(\"subscription_id\", \"in\", (qb) =>\n\t\t\tqb\n\t\t\t\t.selectFrom(\"subscriptions\")\n\t\t\t\t.select(\"id\")\n\t\t\t\t.where(\"subgraph_name\", \"=\", name),\n\t\t)\n\t\t.execute();\n\n\t// Drop the subgraph's schema (CASCADE drops all tables within).\n\tawait sql`DROP SCHEMA IF EXISTS ${sql.raw(`\"${schemaName}\"`)} CASCADE`.execute(\n\t\tdb,\n\t);\n\n\t// Remove from registry\n\tawait db.deleteFrom(\"subgraphs\").where(\"id\", \"=\", subgraph.id).execute();\n\n\treturn subgraph;\n}\n\n/** Set or clear a paid subgraph's expiry (NULL = no expiry, e.g. on claim). */\nexport async function updateSubgraphExpiry(\n\tdb: Kysely<Database>,\n\tname: string,\n\taccountId: string,\n\texpiresAt: Date | null,\n): Promise<void> {\n\tawait db\n\t\t.updateTable(\"subgraphs\")\n\t\t.set({ expires_at: expiresAt })\n\t\t.where(\"name\", \"=\", name)\n\t\t.where(\"account_id\", \"=\", accountId)\n\t\t.execute();\n}\n\n/** Live-walk progress write: advances the cursor and PROMOTES toward\n * 'active' (deploying/error → active keeps reorg eligibility, reorg.ts gates\n * on it) but never overwrites an explicit 'reindexing' park — unconditional\n * status stamping let catch-up flap a parked subgraph back into its own path\n * per block, fighting the queued reindex op.\n *\n * `last_processed_block` is now written CONDITIONALLY — `WHERE\n * last_processed_block < lastProcessedBlock`, mirroring\n * `advanceOperationCursor`'s (`subgraph-operations.ts`) comparison shape —\n * and returns whether THIS call actually advanced it. This used to be\n * unconditional by design, on the premise that the only two writers able to\n * advance/rewind a given subgraph's cursor concurrently (the catch-up walk\n * and the reorg handler) serialize on an in-process per-subgraph lock +\n * reorg epoch guard (`packages/subgraphs/src/runtime/catchup.ts`,\n * `subgraphBlockLockHeld` / `reorgEpoch`; f057). That guard is in-process\n * only. The f068 investigation\n * (`docs/internal/audits/asset-holdings-unreproducible-balances-f068.md`)\n * found a cross-process sequence it does not cover: a catch-up leader that\n * loses its advisory-lock connection mid-walk keeps writing (leadership is\n * checked only at walk entry, not per-iteration) while a new leader takes\n * over and walks the same range — both commit increments for overlapping\n * heights, and every regressed cursor write from the laggard re-triggers a\n * full re-walk, growing per-height application without bound. The\n * conditional advance here (paired with the fast-path replay guard armed on\n * the live path in `block-processor.ts`) is now the cross-process\n * guarantee: a block at/below the current cursor can never re-advance it,\n * so a laggard's commits can no longer regress-then-retrigger.\n *\n * This conditionality is why the reorg rewind can no longer go through this\n * function — a rewind's whole point is to move the cursor *backward*, which\n * the `<` guard would silently refuse. Reorg mode calls\n * {@link rewindLiveProgress} instead, which keeps the old unconditional\n * semantics and is safe specifically because the reorg path takes a\n * transaction-scoped advisory lock (`reorg.ts`) other writers can't cross. */\nexport async function recordLiveProgress(\n\tdb: Kysely<Database>,\n\tname: string,\n\tlastProcessedBlock: number,\n): Promise<boolean> {\n\tconst result = await db\n\t\t.updateTable(\"subgraphs\")\n\t\t.set({\n\t\t\tlast_processed_block: lastProcessedBlock,\n\t\t\tstatus: sql`CASE WHEN status = 'reindexing' THEN status ELSE 'active' END`,\n\t\t\tupdated_at: new Date(),\n\t\t})\n\t\t.where(\"name\", \"=\", name)\n\t\t.where(\"last_processed_block\", \"<\", lastProcessedBlock)\n\t\t.executeTakeFirst();\n\treturn Number(result.numUpdatedRows ?? 0n) > 0;\n}\n\n/** Reorg-rewind progress write: the OLD unconditional `recordLiveProgress`\n * semantics, verbatim — moves `last_processed_block` to the fork height\n * regardless of its current value, because that is by definition a\n * backward move the conditional `<` guard in `recordLiveProgress` would\n * refuse. Same status-promotion CASE (never unparks an explicit\n * 'reindexing').\n *\n * Callers: exclusively the subgraph-reorg path\n * (`packages/subgraphs/src/runtime/reorg.ts`, via `block-processor.ts`'s\n * `reorgRewind` mode), which is safe from the cross-process race this\n * function reintroduces because it holds a transaction-scoped Postgres\n * advisory lock (`pg_advisory_xact_lock`) for the whole\n * delete+reprocess+rewind sequence — no other writer can interleave a\n * forward write between the rewind and the fork block's reprocess. Do not\n * call this from the live catch-up path. */\nexport async function rewindLiveProgress(\n\tdb: Kysely<Database>,\n\tname: string,\n\tlastProcessedBlock: number,\n): Promise<void> {\n\tawait db\n\t\t.updateTable(\"subgraphs\")\n\t\t.set({\n\t\t\tlast_processed_block: lastProcessedBlock,\n\t\t\tstatus: sql`CASE WHEN status = 'reindexing' THEN status ELSE 'active' END`,\n\t\t\tupdated_at: new Date(),\n\t\t})\n\t\t.where(\"name\", \"=\", name)\n\t\t.execute();\n}\n"
|
|
7
|
+
"import { type Kysely, sql } from \"kysely\";\nimport { isPlatformMode } from \"../../mode.ts\";\nimport { jsonb } from \"../jsonb.ts\";\nimport type { Database, Subgraph } from \"../types.ts\";\n\n/**\n * Convert a subgraph name to its PostgreSQL schema name (legacy form).\n * Pre shared-rip every tenant DB had its own schema namespace so disambiguation\n * was implicit. Kept for oss mode (single-tenant) and legacy-row fallback.\n * Platform-mode deploys use `pgSchemaNameFor(accountId, name)`.\n */\nexport function pgSchemaName(subgraphName: string): string {\n\tconst safeName = subgraphName.replace(/-/g, \"_\");\n\treturn `subgraph_${safeName}`;\n}\n\n/** OSS writes empty account_id — name is the local unique key. */\nexport function localSubgraphAccountId(accountId?: string): string {\n\tif (!isPlatformMode()) return \"\";\n\treturn accountId ?? \"\";\n}\n\n/**\n * Account-scoped schema name. Matches migration 0028's rename pattern:\n * subgraph_{first8charsOfAccountId, dashes-as-underscores}_{name}\n * Empty accountId falls back to legacy form (oss mode).\n */\nexport function pgSchemaNameFor(\n\taccountId: string,\n\tsubgraphName: string,\n): string {\n\tif (!accountId) return pgSchemaName(subgraphName);\n\tconst accountPrefix = accountId.slice(0, 8).replace(/-/g, \"_\");\n\tconst safeName = subgraphName.replace(/-/g, \"_\");\n\treturn `subgraph_${accountPrefix}_${safeName}`;\n}\n\nexport async function registerSubgraph(\n\tdb: Kysely<Database>,\n\tdata: {\n\t\tname: string;\n\t\tversion: string;\n\t\tdefinition: Record<string, unknown>;\n\t\tschemaHash: string;\n\t\thandlerPath: string;\n\t\tapiKeyId?: string;\n\t\taccountId?: string;\n\t\tschemaName?: string;\n\t\tstartBlock?: number;\n\t\thandlerCode?: string;\n\t\tsourceCode?: string;\n\t},\n): Promise<Subgraph> {\n\tconst accountId = localSubgraphAccountId(data.accountId);\n\tconst values = {\n\t\tname: data.name,\n\t\tversion: data.version,\n\t\tdefinition: jsonb<Record<string, unknown>>(data.definition),\n\t\tschema_hash: data.schemaHash,\n\t\thandler_path: data.handlerPath,\n\t\taccount_id: accountId,\n\t\thandler_code: data.handlerCode ?? null,\n\t\tsource_code: data.sourceCode ?? null,\n\t\tschema_name: data.schemaName ?? null,\n\t\tstart_block: data.startBlock ?? 0,\n\t\tdatabase_url_enc: null,\n\t};\n\tconst updateSet = {\n\t\tversion: data.version,\n\t\tdefinition: jsonb<Record<string, unknown>>(data.definition),\n\t\tschema_hash: data.schemaHash,\n\t\thandler_path: data.handlerPath,\n\t\thandler_code: data.handlerCode ?? null,\n\t\tsource_code: data.sourceCode ?? null,\n\t\tschema_name: data.schemaName ?? null,\n\t\tstart_block: data.startBlock ?? 0,\n\t\tdatabase_url_enc: null,\n\t\tupdated_at: new Date(),\n\t};\n\n\t// OSS: name is unique. Upsert by name so leftover rows with a non-empty\n\t// account_id (pre-local-namespace deploys) don't insert a second row.\n\tif (!isPlatformMode()) {\n\t\tconst existing = await db\n\t\t\t.selectFrom(\"subgraphs\")\n\t\t\t.select(\"id\")\n\t\t\t.where(\"name\", \"=\", data.name)\n\t\t\t.executeTakeFirst();\n\t\tif (existing) {\n\t\t\treturn await db\n\t\t\t\t.updateTable(\"subgraphs\")\n\t\t\t\t.set({ ...updateSet, account_id: \"\" })\n\t\t\t\t.where(\"id\", \"=\", existing.id)\n\t\t\t\t.returningAll()\n\t\t\t\t.executeTakeFirstOrThrow();\n\t\t}\n\t\treturn await db\n\t\t\t.insertInto(\"subgraphs\")\n\t\t\t.values(values)\n\t\t\t.returningAll()\n\t\t\t.executeTakeFirstOrThrow();\n\t}\n\n\treturn await db\n\t\t.insertInto(\"subgraphs\")\n\t\t.values(values)\n\t\t.onConflict((oc) =>\n\t\t\toc.columns([\"name\", \"account_id\"]).doUpdateSet(updateSet),\n\t\t)\n\t\t.returningAll()\n\t\t.executeTakeFirstOrThrow();\n}\n\nexport async function getSubgraph(\n\tdb: Kysely<Database>,\n\tname: string,\n\taccountId?: string,\n): Promise<Subgraph | null> {\n\tlet query = db.selectFrom(\"subgraphs\").selectAll().where(\"name\", \"=\", name);\n\n\tif (isPlatformMode() && accountId !== undefined) {\n\t\tquery = query.where(\"account_id\", \"=\", accountId);\n\t}\n\n\treturn (await query.executeTakeFirst()) ?? null;\n}\n\nexport async function listSubgraphs(\n\tdb: Kysely<Database>,\n\taccountId?: string,\n): Promise<Subgraph[]> {\n\tlet query = db.selectFrom(\"subgraphs\").selectAll();\n\tif (isPlatformMode() && accountId !== undefined) {\n\t\tquery = query.where(\"account_id\", \"=\", accountId);\n\t}\n\treturn query.execute();\n}\n\nexport async function updateSubgraphStatus(\n\tdb: Kysely<Database>,\n\tname: string,\n\tstatus: string,\n\tlastProcessedBlock?: number,\n): Promise<void> {\n\tawait db\n\t\t.updateTable(\"subgraphs\")\n\t\t.set({\n\t\t\tstatus,\n\t\t\t...(lastProcessedBlock !== undefined\n\t\t\t\t? { last_processed_block: lastProcessedBlock }\n\t\t\t\t: {}),\n\t\t\tupdated_at: new Date(),\n\t\t})\n\t\t.where(\"name\", \"=\", name)\n\t\t.execute();\n}\n\n/** Live-walk ERROR write (f069): a block whose handlers ALL failed\n * (`processed === 0 && errors > 0`, `block-processor.ts`'s `applyProgress`)\n * is stamped 'error' and its height is recorded as processed, same as\n * `updateSubgraphStatus(db, name, \"error\", blockHeight)` — except the\n * cursor half of that write now goes through the same conditional `<`\n * guard as {@link recordLiveProgress}. Marking a subgraph 'error' is not\n * itself replay-sensitive, but it shares the `last_processed_block` column\n * with the replay guard: an unconditional write here could regress the\n * cursor a racing (successful) writer already advanced past, which is\n * exactly the regress-then-re-walk loop this guard exists to close. In\n * practice `ctx.rollbackTo` (`runner.ts`) means an all-failed block never\n * flushes writes, so a lost race here is always a silent no-op, never a\n * case that needs `CursorRaceLostError`-style tx abort. Returns whether\n * the write applied. Live path only — reindex/backfill's error stamping\n * goes through {@link updateSubgraphStatus} unconditionally, as before. */\nexport async function recordLiveError(\n\tdb: Kysely<Database>,\n\tname: string,\n\tblockHeight: number,\n): Promise<boolean> {\n\tconst result = await db\n\t\t.updateTable(\"subgraphs\")\n\t\t.set({\n\t\t\tstatus: \"error\",\n\t\t\tlast_processed_block: blockHeight,\n\t\t\tupdated_at: new Date(),\n\t\t})\n\t\t.where(\"name\", \"=\", name)\n\t\t.where(\"last_processed_block\", \"<\", blockHeight)\n\t\t.executeTakeFirst();\n\treturn Number(result.numUpdatedRows ?? 0n) > 0;\n}\n\nexport async function recordSubgraphProcessed(\n\tdb: Kysely<Database>,\n\tname: string,\n\tprocessed: number,\n\terrors: number,\n\tlastError?: string,\n): Promise<void> {\n\tawait db\n\t\t.updateTable(\"subgraphs\")\n\t\t.set({\n\t\t\ttotal_processed: sql`total_processed + ${processed}`,\n\t\t\ttotal_errors: sql`total_errors + ${errors}`,\n\t\t\t...(lastError\n\t\t\t\t? { last_error: lastError, last_error_at: new Date() }\n\t\t\t\t: {}),\n\t\t\tupdated_at: new Date(),\n\t\t})\n\t\t.where(\"name\", \"=\", name)\n\t\t.execute();\n}\n\nexport async function updateSubgraphHandlerPath(\n\tdb: Kysely<Database>,\n\tname: string,\n\thandlerPath: string,\n\topts?: { handlerCode?: string; sourceCode?: string },\n): Promise<void> {\n\tawait db\n\t\t.updateTable(\"subgraphs\")\n\t\t.set({\n\t\t\thandler_path: handlerPath,\n\t\t\t...(opts?.handlerCode != null ? { handler_code: opts.handlerCode } : {}),\n\t\t\t...(opts?.sourceCode != null ? { source_code: opts.sourceCode } : {}),\n\t\t\tupdated_at: new Date(),\n\t\t})\n\t\t.where(\"name\", \"=\", name)\n\t\t.execute();\n}\n\nexport async function deleteSubgraph(\n\tdb: Kysely<Database>,\n\tname: string,\n\taccountId?: string,\n): Promise<Subgraph | null> {\n\tconst subgraph = await getSubgraph(db, name, accountId);\n\tif (!subgraph) return null;\n\n\t// Use stored schema_name if available, otherwise compute\n\tconst schemaName = subgraph.schema_name ?? pgSchemaName(name);\n\n\t// Cascade to subscriptions: a subscription pointing at a deleted\n\t// subgraph + table will throw `relation does not exist` on every\n\t// subsequent emission. Pause active subs and purge any pending outbox\n\t// rows so receivers don't get phantom replays. We don't delete the\n\t// subscriptions themselves — operators may want to repoint them at a\n\t// resurrected subgraph; we just stop them firing.\n\tawait db\n\t\t.updateTable(\"subscriptions\")\n\t\t.set({\n\t\t\tstatus: \"paused\",\n\t\t\tlast_error: `Subgraph \"${name}\" deleted; subscription auto-paused.`,\n\t\t\tupdated_at: new Date(),\n\t\t})\n\t\t.where(\"subgraph_name\", \"=\", name)\n\t\t.execute();\n\tawait db\n\t\t.deleteFrom(\"subscription_outbox\")\n\t\t.where(\"status\", \"=\", \"pending\")\n\t\t.where(\"subscription_id\", \"in\", (qb) =>\n\t\t\tqb\n\t\t\t\t.selectFrom(\"subscriptions\")\n\t\t\t\t.select(\"id\")\n\t\t\t\t.where(\"subgraph_name\", \"=\", name),\n\t\t)\n\t\t.execute();\n\n\t// Drop the subgraph's schema (CASCADE drops all tables within).\n\tawait sql`DROP SCHEMA IF EXISTS ${sql.raw(`\"${schemaName}\"`)} CASCADE`.execute(\n\t\tdb,\n\t);\n\n\t// Remove from registry\n\tawait db.deleteFrom(\"subgraphs\").where(\"id\", \"=\", subgraph.id).execute();\n\n\treturn subgraph;\n}\n\n/** Set or clear a paid subgraph's expiry (NULL = no expiry, e.g. on claim). */\nexport async function updateSubgraphExpiry(\n\tdb: Kysely<Database>,\n\tname: string,\n\taccountId: string,\n\texpiresAt: Date | null,\n): Promise<void> {\n\tawait db\n\t\t.updateTable(\"subgraphs\")\n\t\t.set({ expires_at: expiresAt })\n\t\t.where(\"name\", \"=\", name)\n\t\t.where(\"account_id\", \"=\", accountId)\n\t\t.execute();\n}\n\n/** Live-walk progress write: advances the cursor and PROMOTES toward\n * 'active' (deploying/error → active keeps reorg eligibility, reorg.ts gates\n * on it) but never overwrites an explicit 'reindexing' park — unconditional\n * status stamping let catch-up flap a parked subgraph back into its own path\n * per block, fighting the queued reindex op.\n *\n * `last_processed_block` is now written CONDITIONALLY — `WHERE\n * last_processed_block < lastProcessedBlock`, mirroring\n * `advanceOperationCursor`'s (`subgraph-operations.ts`) comparison shape —\n * and returns whether THIS call actually advanced it. This used to be\n * unconditional by design, on the premise that the only two writers able to\n * advance/rewind a given subgraph's cursor concurrently (the catch-up walk\n * and the reorg handler) serialize on an in-process per-subgraph lock +\n * reorg epoch guard (`packages/subgraphs/src/runtime/catchup.ts`,\n * `subgraphBlockLockHeld` / `reorgEpoch`; f057). That guard is in-process\n * only. The f068 investigation\n * (`docs/internal/audits/asset-holdings-unreproducible-balances-f068.md`)\n * found a cross-process sequence it does not cover: a catch-up leader that\n * loses its advisory-lock connection mid-walk keeps writing (leadership is\n * checked only at walk entry, not per-iteration) while a new leader takes\n * over and walks the same range — both commit increments for overlapping\n * heights, and every regressed cursor write from the laggard re-triggers a\n * full re-walk, growing per-height application without bound. The\n * conditional advance here (paired with the fast-path replay guard armed on\n * the live path in `block-processor.ts`) is now the cross-process\n * guarantee: a block at/below the current cursor can never re-advance it,\n * so a laggard's commits can no longer regress-then-retrigger.\n *\n * This conditionality is why the reorg rewind can no longer go through this\n * function — a rewind's whole point is to move the cursor *backward*, which\n * the `<` guard would silently refuse. Reorg mode calls\n * {@link rewindLiveProgress} instead, which keeps the old unconditional\n * semantics and is safe specifically because the reorg path takes a\n * transaction-scoped advisory lock (`reorg.ts`) other writers can't cross. */\nexport async function recordLiveProgress(\n\tdb: Kysely<Database>,\n\tname: string,\n\tlastProcessedBlock: number,\n): Promise<boolean> {\n\tconst result = await db\n\t\t.updateTable(\"subgraphs\")\n\t\t.set({\n\t\t\tlast_processed_block: lastProcessedBlock,\n\t\t\tstatus: sql`CASE WHEN status = 'reindexing' THEN status ELSE 'active' END`,\n\t\t\tupdated_at: new Date(),\n\t\t})\n\t\t.where(\"name\", \"=\", name)\n\t\t.where(\"last_processed_block\", \"<\", lastProcessedBlock)\n\t\t.executeTakeFirst();\n\treturn Number(result.numUpdatedRows ?? 0n) > 0;\n}\n\n/** Reorg-rewind progress write: the OLD unconditional `recordLiveProgress`\n * semantics, verbatim — moves `last_processed_block` to the fork height\n * regardless of its current value, because that is by definition a\n * backward move the conditional `<` guard in `recordLiveProgress` would\n * refuse. Same status-promotion CASE (never unparks an explicit\n * 'reindexing').\n *\n * Callers: exclusively the subgraph-reorg path\n * (`packages/subgraphs/src/runtime/reorg.ts`, via `block-processor.ts`'s\n * `reorgRewind` mode), which is safe from the cross-process race this\n * function reintroduces because it holds a transaction-scoped Postgres\n * advisory lock (`pg_advisory_xact_lock`) for the whole\n * delete+reprocess+rewind sequence — no other writer can interleave a\n * forward write between the rewind and the fork block's reprocess. Do not\n * call this from the live catch-up path. */\nexport async function rewindLiveProgress(\n\tdb: Kysely<Database>,\n\tname: string,\n\tlastProcessedBlock: number,\n): Promise<void> {\n\tawait db\n\t\t.updateTable(\"subgraphs\")\n\t\t.set({\n\t\t\tlast_processed_block: lastProcessedBlock,\n\t\t\tstatus: sql`CASE WHEN status = 'reindexing' THEN status ELSE 'active' END`,\n\t\t\tupdated_at: new Date(),\n\t\t})\n\t\t.where(\"name\", \"=\", name)\n\t\t.execute();\n}\n"
|
|
8
8
|
],
|
|
9
|
-
"mappings": "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
|
|
10
|
-
"debugId": "
|
|
9
|
+
"mappings": "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",
|
|
10
|
+
"debugId": "B3ECE96E3D7D4AE664756E2164756E21",
|
|
11
11
|
"names": []
|
|
12
12
|
}
|
package/dist/src/errors.d.ts
CHANGED
|
@@ -40,7 +40,10 @@ declare class DatabaseError extends SecondLayerError {
|
|
|
40
40
|
constructor(message: string, cause?: unknown);
|
|
41
41
|
}
|
|
42
42
|
declare class AuthenticationError extends SecondLayerError {
|
|
43
|
-
|
|
43
|
+
/** `details` is merged into the HTTP body — use it to carry the recovery
|
|
44
|
+
* hint (which credential, where it comes from) instead of a bare "invalid
|
|
45
|
+
* key" the caller cannot act on. */
|
|
46
|
+
constructor(message: string, details?: Record<string, unknown>);
|
|
44
47
|
}
|
|
45
48
|
declare class AuthorizationError extends SecondLayerError {
|
|
46
49
|
constructor(message: string, details?: Record<string, unknown>);
|
|
@@ -72,6 +75,6 @@ declare class TenantSuspendedError extends SecondLayerError {
|
|
|
72
75
|
}
|
|
73
76
|
/** Error code → HTTP status. Used by API middleware for code-based matching
|
|
74
77
|
* (avoids cross-bundle instanceof failures from bunup class duplication). */
|
|
75
|
-
declare const CODE_TO_STATUS: Record<string, 400 | 401 | 402 | 403 | 404 | 409 | 422 | 423 | 429 | 503>;
|
|
78
|
+
declare const CODE_TO_STATUS: Record<string, 400 | 401 | 402 | 403 | 404 | 409 | 415 | 422 | 423 | 429 | 503>;
|
|
76
79
|
declare function getErrorMessage(err: unknown): string;
|
|
77
80
|
export { getErrorMessage, VersionConflictError, ValidationError, TenantSuspendedError, SecondLayerError, RateLimitError, PaymentRequiredError, NotFoundError, KeyRotatedError, ForbiddenError, ErrorCodes, ErrorCode, DatabaseError, CODE_TO_STATUS, AuthorizationError, AuthenticationError };
|
package/dist/src/errors.js
CHANGED
|
@@ -74,8 +74,8 @@ class DatabaseError extends SecondLayerError {
|
|
|
74
74
|
}
|
|
75
75
|
|
|
76
76
|
class AuthenticationError extends SecondLayerError {
|
|
77
|
-
constructor(message) {
|
|
78
|
-
super("AUTHENTICATION_ERROR", message);
|
|
77
|
+
constructor(message, details) {
|
|
78
|
+
super("AUTHENTICATION_ERROR", message, undefined, details);
|
|
79
79
|
}
|
|
80
80
|
}
|
|
81
81
|
|
|
@@ -138,8 +138,8 @@ var CODE_TO_STATUS = {
|
|
|
138
138
|
NO_TENANT_FOR_PROJECT: 404,
|
|
139
139
|
INSTANCE_EXISTS: 409,
|
|
140
140
|
SUBGRAPH_NOT_FOUND: 404,
|
|
141
|
-
|
|
142
|
-
|
|
141
|
+
GHOST_KEY_READ_ONLY: 403,
|
|
142
|
+
UNSUPPORTED_MEDIA_TYPE: 415
|
|
143
143
|
};
|
|
144
144
|
function getErrorMessage(err) {
|
|
145
145
|
return err instanceof Error ? err.message : String(err);
|
|
@@ -162,5 +162,5 @@ export {
|
|
|
162
162
|
AuthenticationError
|
|
163
163
|
};
|
|
164
164
|
|
|
165
|
-
//# debugId=
|
|
165
|
+
//# debugId=D8A474DF7664F17764756E2164756E21
|
|
166
166
|
//# sourceMappingURL=errors.js.map
|