@secondlayer/shared 0.3.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (73) hide show
  1. package/README.md +23 -0
  2. package/dist/src/crypto/hmac.js +6 -2
  3. package/dist/src/crypto/hmac.js.map +2 -2
  4. package/dist/src/db/index.d.ts +83 -6
  5. package/dist/src/db/index.js +6 -2
  6. package/dist/src/db/index.js.map +2 -2
  7. package/dist/src/db/jsonb.js +6 -2
  8. package/dist/src/db/jsonb.js.map +2 -2
  9. package/dist/src/db/queries/accounts.d.ts +75 -3
  10. package/dist/src/db/queries/accounts.js +17 -2
  11. package/dist/src/db/queries/accounts.js.map +3 -3
  12. package/dist/src/db/queries/integrity.d.ts +73 -2
  13. package/dist/src/db/queries/integrity.js +6 -2
  14. package/dist/src/db/queries/integrity.js.map +2 -2
  15. package/dist/src/db/queries/metrics.d.ts +73 -2
  16. package/dist/src/db/queries/metrics.js +6 -2
  17. package/dist/src/db/queries/metrics.js.map +2 -2
  18. package/dist/src/db/queries/{views.d.ts → subgraphs.d.ts} +87 -16
  19. package/dist/src/db/queries/{views.js → subgraphs.js} +37 -33
  20. package/dist/src/db/queries/subgraphs.js.map +11 -0
  21. package/dist/src/db/queries/usage.d.ts +76 -5
  22. package/dist/src/db/queries/usage.js +13 -9
  23. package/dist/src/db/queries/usage.js.map +4 -4
  24. package/dist/src/db/schema.d.ts +83 -6
  25. package/dist/src/env.js +6 -2
  26. package/dist/src/env.js.map +2 -2
  27. package/dist/src/errors.d.ts +4 -1
  28. package/dist/src/errors.js +13 -2
  29. package/dist/src/errors.js.map +3 -3
  30. package/dist/src/index.d.ts +93 -13
  31. package/dist/src/index.js +16 -5
  32. package/dist/src/index.js.map +5 -5
  33. package/dist/src/lib/plans.d.ts +1 -1
  34. package/dist/src/lib/plans.js +7 -3
  35. package/dist/src/lib/plans.js.map +3 -3
  36. package/dist/src/logger.js +6 -2
  37. package/dist/src/logger.js.map +2 -2
  38. package/dist/src/node/client.d.ts +1 -0
  39. package/dist/src/node/client.js +18 -2
  40. package/dist/src/node/client.js.map +3 -3
  41. package/dist/src/node/hiro-client.js +8 -4
  42. package/dist/src/node/hiro-client.js.map +3 -3
  43. package/dist/src/node/local-client.d.ts +73 -2
  44. package/dist/src/node/local-client.js +6 -2
  45. package/dist/src/node/local-client.js.map +2 -2
  46. package/dist/src/queue/index.js +6 -2
  47. package/dist/src/queue/index.js.map +2 -2
  48. package/dist/src/queue/listener.js +6 -2
  49. package/dist/src/queue/listener.js.map +2 -2
  50. package/dist/src/queue/recovery.js +6 -2
  51. package/dist/src/queue/recovery.js.map +2 -2
  52. package/dist/src/schemas/filters.js +6 -2
  53. package/dist/src/schemas/filters.js.map +2 -2
  54. package/dist/src/schemas/index.d.ts +8 -8
  55. package/dist/src/schemas/index.js +9 -5
  56. package/dist/src/schemas/index.js.map +4 -4
  57. package/dist/src/schemas/{views.d.ts → subgraphs.d.ts} +8 -8
  58. package/dist/src/schemas/{views.js → subgraphs.js} +10 -6
  59. package/dist/src/schemas/subgraphs.js.map +10 -0
  60. package/dist/src/types.js +1 -12
  61. package/dist/src/types.js.map +1 -1
  62. package/migrations/0007_contracts.ts +58 -0
  63. package/migrations/0008_drop_contracts.ts +28 -0
  64. package/migrations/0009_waitlist.ts +19 -0
  65. package/migrations/0010_waitlist_status.ts +12 -0
  66. package/migrations/0011_account_insights.ts +63 -0
  67. package/migrations/0012_view_health_snapshots.ts +29 -0
  68. package/migrations/0013_view_processing_stats.ts +42 -0
  69. package/migrations/0014_view_table_snapshots.ts +33 -0
  70. package/migrations/0015_rename_views_to_subgraphs.ts +136 -0
  71. package/package.json +12 -8
  72. package/dist/src/db/queries/views.js.map +0 -11
  73. package/dist/src/schemas/views.js.map +0 -10
@@ -0,0 +1,10 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../src/schemas/subgraphs.ts"],
4
+ "sourcesContent": [
5
+ "import { z } from \"zod\";\n\n// ── Deploy Subgraph Request ─────────────────────────────────────────────────\n\nexport interface DeploySubgraphRequest {\n name: string;\n version?: string;\n description?: string;\n sources: string[];\n schema: Record<string, unknown>;\n handlerCode: string;\n reindex?: boolean;\n}\n\nexport const DeploySubgraphRequestSchema: z.ZodType<DeploySubgraphRequest> = z.object({\n name: z.string().regex(/^[a-z0-9-]+$/, \"lowercase alphanumeric + hyphens only\").max(63),\n version: z.string().optional(),\n description: z.string().optional(),\n sources: z.array(z.string()).min(1),\n schema: z.record(z.unknown()),\n handlerCode: z.string().max(1_048_576, \"handler code exceeds 1MB limit\"),\n reindex: z.boolean().optional(),\n});\n\nexport interface DeploySubgraphResponse {\n action: \"created\" | \"unchanged\" | \"updated\" | \"reindexed\";\n subgraphId: string;\n message: string;\n}\n\n// Subgraph API response types\n\nexport interface SubgraphSummary {\n name: string;\n version: string;\n status: string;\n lastProcessedBlock: number;\n tables: string[];\n createdAt: string;\n}\n\nexport interface SubgraphDetail {\n name: string;\n version: string;\n status: string;\n lastProcessedBlock: number;\n health: {\n totalProcessed: number;\n totalErrors: number;\n errorRate: number;\n lastError: string | null;\n lastErrorAt: string | null;\n };\n tables: Record<string, {\n endpoint: string;\n columns: Record<string, { type: string; nullable?: boolean }>;\n rowCount: number;\n example: string;\n }>;\n createdAt: string;\n updatedAt: string;\n}\n\nexport interface ReindexResponse {\n message: string;\n fromBlock: number;\n toBlock: number | string;\n}\n\nexport interface SubgraphQueryParams {\n sort?: string;\n order?: string;\n limit?: number;\n offset?: number;\n fields?: string;\n filters?: Record<string, string>;\n}\n"
6
+ ],
7
+ "mappings": ";;;;;;;;;;;;;;;;;AAAA;AAcO,IAAM,8BAAgE,EAAE,OAAO;AAAA,EACpF,MAAM,EAAE,OAAO,EAAE,MAAM,gBAAgB,uCAAuC,EAAE,IAAI,EAAE;AAAA,EACtF,SAAS,EAAE,OAAO,EAAE,SAAS;AAAA,EAC7B,aAAa,EAAE,OAAO,EAAE,SAAS;AAAA,EACjC,SAAS,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,IAAI,CAAC;AAAA,EAClC,QAAQ,EAAE,OAAO,EAAE,QAAQ,CAAC;AAAA,EAC5B,aAAa,EAAE,OAAO,EAAE,IAAI,SAAW,gCAAgC;AAAA,EACvE,SAAS,EAAE,QAAQ,EAAE,SAAS;AAChC,CAAC;",
8
+ "debugId": "8C96304B8A3F90B664756E2164756E21",
9
+ "names": []
10
+ }
package/dist/src/types.js CHANGED
@@ -1,14 +1,3 @@
1
- import { createRequire } from "node:module";
2
- var __defProp = Object.defineProperty;
3
- var __export = (target, all) => {
4
- for (var name in all)
5
- __defProp(target, name, {
6
- get: all[name],
7
- enumerable: true,
8
- configurable: true,
9
- set: (newValue) => all[name] = () => newValue
10
- });
11
- };
12
1
 
13
- //# debugId=6E26BDBC1FE19BB464756E2164756E21
2
+ //# debugId=F2B76011AEAE140064756E2164756E21
14
3
  //# sourceMappingURL=types.js.map
@@ -4,6 +4,6 @@
4
4
  "sourcesContent": [
5
5
  ],
6
6
  "mappings": "",
7
- "debugId": "6E26BDBC1FE19BB464756E2164756E21",
7
+ "debugId": "F2B76011AEAE140064756E2164756E21",
8
8
  "names": []
9
9
  }
@@ -0,0 +1,58 @@
1
+ import { type Kysely, sql } from "kysely";
2
+
3
+ export async function up(db: Kysely<any>): Promise<void> {
4
+ // Enable pg_trgm for fast ILIKE search
5
+ await sql`CREATE EXTENSION IF NOT EXISTS pg_trgm`.execute(db);
6
+
7
+ await db.schema
8
+ .createTable("contracts")
9
+ .addColumn("contract_id", "text", (c) => c.primaryKey())
10
+ .addColumn("name", "text", (c) => c.notNull())
11
+ .addColumn("deployer", "text", (c) => c.notNull())
12
+ .addColumn("deploy_block", "integer", (c) => c.notNull())
13
+ .addColumn("deploy_tx_id", "text", (c) => c.notNull())
14
+ .addColumn("call_count", "integer", (c) => c.notNull().defaultTo(0))
15
+ .addColumn("last_called_at", "timestamptz")
16
+ .addColumn("abi", "jsonb")
17
+ .addColumn("abi_fetched_at", "timestamptz")
18
+ .addColumn("created_at", "timestamptz", (c) => c.notNull().defaultTo(sql`NOW()`))
19
+ .addColumn("updated_at", "timestamptz", (c) => c.notNull().defaultTo(sql`NOW()`))
20
+ .execute();
21
+
22
+ await db.schema.createIndex("contracts_name_idx").on("contracts").column("name").execute();
23
+ await db.schema.createIndex("contracts_deployer_idx").on("contracts").column("deployer").execute();
24
+ await sql`CREATE INDEX contracts_name_trgm_idx ON contracts USING gin(name gin_trgm_ops)`.execute(db);
25
+
26
+ // Backfill step 1: insert deployed contracts from transactions
27
+ await sql`
28
+ INSERT INTO contracts (contract_id, name, deployer, deploy_block, deploy_tx_id, created_at)
29
+ SELECT DISTINCT ON (contract_id)
30
+ contract_id,
31
+ split_part(contract_id, '.', 2),
32
+ sender,
33
+ block_height,
34
+ tx_id,
35
+ created_at
36
+ FROM transactions
37
+ WHERE type = 'smart_contract' AND contract_id IS NOT NULL
38
+ ORDER BY contract_id, block_height ASC
39
+ ON CONFLICT (contract_id) DO NOTHING
40
+ `.execute(db);
41
+
42
+ // Backfill step 2: update call counts from contract_call transactions
43
+ await sql`
44
+ UPDATE contracts c
45
+ SET call_count = sub.cnt, last_called_at = sub.last_call
46
+ FROM (
47
+ SELECT contract_id, COUNT(*)::int AS cnt, MAX(created_at) AS last_call
48
+ FROM transactions
49
+ WHERE type = 'contract_call' AND contract_id IS NOT NULL
50
+ GROUP BY contract_id
51
+ ) sub
52
+ WHERE c.contract_id = sub.contract_id
53
+ `.execute(db);
54
+ }
55
+
56
+ export async function down(db: Kysely<any>): Promise<void> {
57
+ await db.schema.dropTable("contracts").ifExists().cascade().execute();
58
+ }
@@ -0,0 +1,28 @@
1
+ import { type Kysely, sql } from "kysely";
2
+
3
+ export async function up(db: Kysely<any>): Promise<void> {
4
+ await db.schema.dropTable("contracts").ifExists().cascade().execute();
5
+ }
6
+
7
+ export async function down(db: Kysely<any>): Promise<void> {
8
+ await sql`CREATE EXTENSION IF NOT EXISTS pg_trgm`.execute(db);
9
+
10
+ await db.schema
11
+ .createTable("contracts")
12
+ .addColumn("contract_id", "text", (c) => c.primaryKey())
13
+ .addColumn("name", "text", (c) => c.notNull())
14
+ .addColumn("deployer", "text", (c) => c.notNull())
15
+ .addColumn("deploy_block", "integer", (c) => c.notNull())
16
+ .addColumn("deploy_tx_id", "text", (c) => c.notNull())
17
+ .addColumn("call_count", "integer", (c) => c.notNull().defaultTo(0))
18
+ .addColumn("last_called_at", "timestamptz")
19
+ .addColumn("abi", "jsonb")
20
+ .addColumn("abi_fetched_at", "timestamptz")
21
+ .addColumn("created_at", "timestamptz", (c) => c.notNull().defaultTo(sql`NOW()`))
22
+ .addColumn("updated_at", "timestamptz", (c) => c.notNull().defaultTo(sql`NOW()`))
23
+ .execute();
24
+
25
+ await db.schema.createIndex("contracts_name_idx").on("contracts").column("name").execute();
26
+ await db.schema.createIndex("contracts_deployer_idx").on("contracts").column("deployer").execute();
27
+ await sql`CREATE INDEX contracts_name_trgm_idx ON contracts USING gin(name gin_trgm_ops)`.execute(db);
28
+ }
@@ -0,0 +1,19 @@
1
+ import type { Kysely } from "kysely";
2
+
3
+ export async function up(db: Kysely<any>): Promise<void> {
4
+ await db.schema
5
+ .createTable("waitlist")
6
+ .addColumn("id", "uuid", (col) =>
7
+ col.primaryKey().defaultTo(db.fn("gen_random_uuid")),
8
+ )
9
+ .addColumn("email", "text", (col) => col.notNull().unique())
10
+ .addColumn("source", "text", (col) => col.defaultTo("website"))
11
+ .addColumn("created_at", "timestamptz", (col) =>
12
+ col.notNull().defaultTo(db.fn("now")),
13
+ )
14
+ .execute();
15
+ }
16
+
17
+ export async function down(db: Kysely<any>): Promise<void> {
18
+ await db.schema.dropTable("waitlist").execute();
19
+ }
@@ -0,0 +1,12 @@
1
+ import type { Kysely } from "kysely";
2
+
3
+ export async function up(db: Kysely<any>): Promise<void> {
4
+ await db.schema
5
+ .alterTable("waitlist")
6
+ .addColumn("status", "text", (col) => col.notNull().defaultTo("pending"))
7
+ .execute();
8
+ }
9
+
10
+ export async function down(db: Kysely<any>): Promise<void> {
11
+ await db.schema.alterTable("waitlist").dropColumn("status").execute();
12
+ }
@@ -0,0 +1,63 @@
1
+ import type { Kysely } from "kysely";
2
+ import { sql } from "kysely";
3
+
4
+ export async function up(db: Kysely<any>): Promise<void> {
5
+ await db.schema
6
+ .createTable("account_insights")
7
+ .addColumn("id", "uuid", (col) =>
8
+ col.primaryKey().defaultTo(db.fn("gen_random_uuid")),
9
+ )
10
+ .addColumn("account_id", "uuid", (col) =>
11
+ col.notNull().references("accounts.id"),
12
+ )
13
+ .addColumn("category", "text", (col) => col.notNull())
14
+ .addColumn("insight_type", "text", (col) => col.notNull())
15
+ .addColumn("resource_id", "text")
16
+ .addColumn("severity", "text", (col) => col.notNull())
17
+ .addColumn("title", "text", (col) => col.notNull())
18
+ .addColumn("body", "text", (col) => col.notNull())
19
+ .addColumn("data", "jsonb")
20
+ .addColumn("dismissed_at", "timestamptz")
21
+ .addColumn("expires_at", "timestamptz")
22
+ .addColumn("created_at", "timestamptz", (col) =>
23
+ col.notNull().defaultTo(db.fn("now")),
24
+ )
25
+ .execute();
26
+
27
+ await db.schema
28
+ .createIndex("idx_account_insights_account")
29
+ .on("account_insights")
30
+ .column("account_id")
31
+ .execute();
32
+
33
+ await db.schema
34
+ .createTable("account_agent_runs")
35
+ .addColumn("id", "uuid", (col) =>
36
+ col.primaryKey().defaultTo(db.fn("gen_random_uuid")),
37
+ )
38
+ .addColumn("account_id", "uuid", (col) =>
39
+ col.notNull().references("accounts.id"),
40
+ )
41
+ .addColumn("started_at", "timestamptz", (col) =>
42
+ col.notNull().defaultTo(db.fn("now")),
43
+ )
44
+ .addColumn("completed_at", "timestamptz")
45
+ .addColumn("status", "text", (col) => col.notNull().defaultTo("running"))
46
+ .addColumn("input_tokens", "integer", (col) => col.defaultTo(0))
47
+ .addColumn("output_tokens", "integer", (col) => col.defaultTo(0))
48
+ .addColumn("cost_usd", sql`numeric(10,6)`, (col) => col.defaultTo(0))
49
+ .addColumn("insights_created", "integer", (col) => col.defaultTo(0))
50
+ .addColumn("error", "text")
51
+ .execute();
52
+
53
+ await db.schema
54
+ .createIndex("idx_account_agent_runs_account")
55
+ .on("account_agent_runs")
56
+ .column("account_id")
57
+ .execute();
58
+ }
59
+
60
+ export async function down(db: Kysely<any>): Promise<void> {
61
+ await db.schema.dropTable("account_agent_runs").execute();
62
+ await db.schema.dropTable("account_insights").execute();
63
+ }
@@ -0,0 +1,29 @@
1
+ import type { Kysely } from "kysely";
2
+
3
+ export async function up(db: Kysely<any>): Promise<void> {
4
+ await db.schema
5
+ .createTable("view_health_snapshots")
6
+ .addColumn("id", "uuid", (col) =>
7
+ col.primaryKey().defaultTo(db.fn("gen_random_uuid")),
8
+ )
9
+ .addColumn("view_id", "uuid", (col) =>
10
+ col.notNull().references("views.id").onDelete("cascade"),
11
+ )
12
+ .addColumn("total_processed", "bigint", (col) => col.notNull())
13
+ .addColumn("total_errors", "bigint", (col) => col.notNull())
14
+ .addColumn("last_processed_block", "integer")
15
+ .addColumn("captured_at", "timestamptz", (col) =>
16
+ col.notNull().defaultTo(db.fn("now")),
17
+ )
18
+ .execute();
19
+
20
+ await db.schema
21
+ .createIndex("idx_view_health_snapshots_view_captured")
22
+ .on("view_health_snapshots")
23
+ .columns(["view_id", "captured_at"])
24
+ .execute();
25
+ }
26
+
27
+ export async function down(db: Kysely<any>): Promise<void> {
28
+ await db.schema.dropTable("view_health_snapshots").execute();
29
+ }
@@ -0,0 +1,42 @@
1
+ import type { Kysely } from "kysely";
2
+ import { sql } from "kysely";
3
+
4
+ export async function up(db: Kysely<any>): Promise<void> {
5
+ await db.schema
6
+ .createTable("view_processing_stats")
7
+ .addColumn("id", "uuid", (col) =>
8
+ col.primaryKey().defaultTo(db.fn("gen_random_uuid")),
9
+ )
10
+ .addColumn("view_name", "text", (col) => col.notNull())
11
+ .addColumn("api_key_id", "text")
12
+ .addColumn("bucket_start", "timestamptz")
13
+ .addColumn("bucket_end", "timestamptz")
14
+ .addColumn("blocks_processed", "integer")
15
+ .addColumn("total_time_ms", "integer")
16
+ .addColumn("handler_time_ms", "integer")
17
+ .addColumn("flush_time_ms", "integer")
18
+ .addColumn("max_block_time_ms", "integer")
19
+ .addColumn("max_handler_time_ms", "integer")
20
+ .addColumn("avg_ops_per_block", sql`real`)
21
+ .addColumn("is_catchup", "boolean", (col) => col.defaultTo(false))
22
+ .addColumn("created_at", "timestamptz", (col) =>
23
+ col.notNull().defaultTo(db.fn("now")),
24
+ )
25
+ .execute();
26
+
27
+ await db.schema
28
+ .createIndex("idx_view_processing_stats_view_bucket")
29
+ .on("view_processing_stats")
30
+ .columns(["view_name", "bucket_start"])
31
+ .execute();
32
+
33
+ await db.schema
34
+ .createIndex("idx_view_processing_stats_api_key")
35
+ .on("view_processing_stats")
36
+ .column("api_key_id")
37
+ .execute();
38
+ }
39
+
40
+ export async function down(db: Kysely<any>): Promise<void> {
41
+ await db.schema.dropTable("view_processing_stats").execute();
42
+ }
@@ -0,0 +1,33 @@
1
+ import type { Kysely } from "kysely";
2
+
3
+ export async function up(db: Kysely<any>): Promise<void> {
4
+ await db.schema
5
+ .createTable("view_table_snapshots")
6
+ .addColumn("id", "uuid", (col) =>
7
+ col.primaryKey().defaultTo(db.fn("gen_random_uuid")),
8
+ )
9
+ .addColumn("view_name", "text", (col) => col.notNull())
10
+ .addColumn("api_key_id", "text")
11
+ .addColumn("table_name", "text", (col) => col.notNull())
12
+ .addColumn("row_count", "bigint")
13
+ .addColumn("created_at", "timestamptz", (col) =>
14
+ col.notNull().defaultTo(db.fn("now")),
15
+ )
16
+ .execute();
17
+
18
+ await db.schema
19
+ .createIndex("idx_view_table_snapshots_view_table_created")
20
+ .on("view_table_snapshots")
21
+ .columns(["view_name", "table_name", "created_at"])
22
+ .execute();
23
+
24
+ await db.schema
25
+ .createIndex("idx_view_table_snapshots_api_key")
26
+ .on("view_table_snapshots")
27
+ .column("api_key_id")
28
+ .execute();
29
+ }
30
+
31
+ export async function down(db: Kysely<any>): Promise<void> {
32
+ await db.schema.dropTable("view_table_snapshots").execute();
33
+ }
@@ -0,0 +1,136 @@
1
+ import type { Kysely } from "kysely";
2
+ import { sql } from "kysely";
3
+
4
+ export async function up(db: Kysely<any>): Promise<void> {
5
+ // Drop old trigger + function
6
+ await sql`DROP TRIGGER IF EXISTS views_notify_trigger ON "views"`.execute(db);
7
+ await sql`DROP FUNCTION IF EXISTS notify_view_changes()`.execute(db);
8
+
9
+ // Rename tables
10
+ await sql`ALTER TABLE "views" RENAME TO "subgraphs"`.execute(db);
11
+ await sql`ALTER TABLE "view_health_snapshots" RENAME TO "subgraph_health_snapshots"`.execute(db);
12
+ await sql`ALTER TABLE "view_processing_stats" RENAME TO "subgraph_processing_stats"`.execute(db);
13
+ await sql`ALTER TABLE "view_table_snapshots" RENAME TO "subgraph_table_snapshots"`.execute(db);
14
+
15
+ // Rename indexes on subgraphs (formerly views)
16
+ await sql`ALTER INDEX "views_name_idx" RENAME TO "subgraphs_name_idx"`.execute(db);
17
+ await sql`ALTER INDEX "views_status_idx" RENAME TO "subgraphs_status_idx"`.execute(db);
18
+
19
+ // Rename indexes on subgraph_health_snapshots
20
+ await sql`ALTER INDEX "idx_view_health_snapshots_view_captured" RENAME TO "idx_subgraph_health_snapshots_subgraph_captured"`.execute(db);
21
+
22
+ // Rename indexes on subgraph_processing_stats
23
+ await sql`ALTER INDEX "idx_view_processing_stats_view_bucket" RENAME TO "idx_subgraph_processing_stats_subgraph_bucket"`.execute(db);
24
+ await sql`ALTER INDEX "idx_view_processing_stats_api_key" RENAME TO "idx_subgraph_processing_stats_api_key"`.execute(db);
25
+
26
+ // Rename indexes on subgraph_table_snapshots
27
+ await sql`ALTER INDEX "idx_view_table_snapshots_view_table_created" RENAME TO "idx_subgraph_table_snapshots_subgraph_table_created"`.execute(db);
28
+ await sql`ALTER INDEX "idx_view_table_snapshots_api_key" RENAME TO "idx_subgraph_table_snapshots_api_key"`.execute(db);
29
+
30
+ // Rename column view_id → subgraph_id in health snapshots
31
+ await sql`ALTER TABLE "subgraph_health_snapshots" RENAME COLUMN "view_id" TO "subgraph_id"`.execute(db);
32
+
33
+ // Rename column view_name → subgraph_name in processing stats and table snapshots
34
+ await sql`ALTER TABLE "subgraph_processing_stats" RENAME COLUMN "view_name" TO "subgraph_name"`.execute(db);
35
+ await sql`ALTER TABLE "subgraph_table_snapshots" RENAME COLUMN "view_name" TO "subgraph_name"`.execute(db);
36
+
37
+ // Rename FK constraint (Postgres auto-names it based on original table/column)
38
+ await sql`ALTER TABLE "subgraph_health_snapshots" RENAME CONSTRAINT "view_health_snapshots_view_id_fkey" TO "subgraph_health_snapshots_subgraph_id_fkey"`.execute(db);
39
+
40
+ // Recreate notify trigger with new names
41
+ await sql`
42
+ CREATE OR REPLACE FUNCTION notify_subgraph_changes() RETURNS trigger AS $$
43
+ BEGIN
44
+ PERFORM pg_notify('subgraph_changes', json_build_object(
45
+ 'operation', TG_OP,
46
+ 'name', COALESCE(NEW.name, OLD.name)
47
+ )::text);
48
+ RETURN COALESCE(NEW, OLD);
49
+ END;
50
+ $$ LANGUAGE plpgsql
51
+ `.execute(db);
52
+
53
+ await sql`
54
+ CREATE TRIGGER subgraphs_notify_trigger
55
+ AFTER INSERT OR UPDATE OR DELETE ON "subgraphs"
56
+ FOR EACH ROW EXECUTE FUNCTION notify_subgraph_changes()
57
+ `.execute(db);
58
+
59
+ // Rename any existing tenant schemas from view_* to subgraph_*
60
+ await sql`
61
+ DO $$
62
+ DECLARE
63
+ s record;
64
+ BEGIN
65
+ FOR s IN SELECT schema_name FROM information_schema.schemata WHERE schema_name LIKE 'view_%'
66
+ LOOP
67
+ EXECUTE format('ALTER SCHEMA %I RENAME TO %I', s.schema_name, 'subgraph_' || substring(s.schema_name from 6));
68
+ END LOOP;
69
+ END $$
70
+ `.execute(db);
71
+
72
+ // Update schema_name column in subgraphs table to match new prefix
73
+ await sql`UPDATE "subgraphs" SET schema_name = 'subgraph_' || substring(schema_name from 6) WHERE schema_name LIKE 'view_%'`.execute(db);
74
+ }
75
+
76
+ export async function down(db: Kysely<any>): Promise<void> {
77
+ // Drop new trigger + function
78
+ await sql`DROP TRIGGER IF EXISTS subgraphs_notify_trigger ON "subgraphs"`.execute(db);
79
+ await sql`DROP FUNCTION IF EXISTS notify_subgraph_changes()`.execute(db);
80
+
81
+ // Revert schema_name column
82
+ await sql`UPDATE "subgraphs" SET schema_name = 'view_' || substring(schema_name from 10) WHERE schema_name LIKE 'subgraph_%'`.execute(db);
83
+
84
+ // Revert tenant schemas
85
+ await sql`
86
+ DO $$
87
+ DECLARE
88
+ s record;
89
+ BEGIN
90
+ FOR s IN SELECT schema_name FROM information_schema.schemata WHERE schema_name LIKE 'subgraph_%'
91
+ LOOP
92
+ EXECUTE format('ALTER SCHEMA %I RENAME TO %I', s.schema_name, 'view_' || substring(s.schema_name from 10));
93
+ END LOOP;
94
+ END $$
95
+ `.execute(db);
96
+
97
+ // Rename columns back
98
+ await sql`ALTER TABLE "subgraph_health_snapshots" RENAME CONSTRAINT "subgraph_health_snapshots_subgraph_id_fkey" TO "view_health_snapshots_view_id_fkey"`.execute(db);
99
+ await sql`ALTER TABLE "subgraph_table_snapshots" RENAME COLUMN "subgraph_name" TO "view_name"`.execute(db);
100
+ await sql`ALTER TABLE "subgraph_processing_stats" RENAME COLUMN "subgraph_name" TO "view_name"`.execute(db);
101
+ await sql`ALTER TABLE "subgraph_health_snapshots" RENAME COLUMN "subgraph_id" TO "view_id"`.execute(db);
102
+
103
+ // Rename indexes back
104
+ await sql`ALTER INDEX "idx_subgraph_table_snapshots_api_key" RENAME TO "idx_view_table_snapshots_api_key"`.execute(db);
105
+ await sql`ALTER INDEX "idx_subgraph_table_snapshots_subgraph_table_created" RENAME TO "idx_view_table_snapshots_view_table_created"`.execute(db);
106
+ await sql`ALTER INDEX "idx_subgraph_processing_stats_api_key" RENAME TO "idx_view_processing_stats_api_key"`.execute(db);
107
+ await sql`ALTER INDEX "idx_subgraph_processing_stats_subgraph_bucket" RENAME TO "idx_view_processing_stats_view_bucket"`.execute(db);
108
+ await sql`ALTER INDEX "idx_subgraph_health_snapshots_subgraph_captured" RENAME TO "idx_view_health_snapshots_view_captured"`.execute(db);
109
+ await sql`ALTER INDEX "subgraphs_status_idx" RENAME TO "views_status_idx"`.execute(db);
110
+ await sql`ALTER INDEX "subgraphs_name_idx" RENAME TO "views_name_idx"`.execute(db);
111
+
112
+ // Rename tables back
113
+ await sql`ALTER TABLE "subgraph_table_snapshots" RENAME TO "view_table_snapshots"`.execute(db);
114
+ await sql`ALTER TABLE "subgraph_processing_stats" RENAME TO "view_processing_stats"`.execute(db);
115
+ await sql`ALTER TABLE "subgraph_health_snapshots" RENAME TO "view_health_snapshots"`.execute(db);
116
+ await sql`ALTER TABLE "subgraphs" RENAME TO "views"`.execute(db);
117
+
118
+ // Recreate old trigger
119
+ await sql`
120
+ CREATE OR REPLACE FUNCTION notify_view_changes() RETURNS trigger AS $$
121
+ BEGIN
122
+ PERFORM pg_notify('view_changes', json_build_object(
123
+ 'operation', TG_OP,
124
+ 'name', COALESCE(NEW.name, OLD.name)
125
+ )::text);
126
+ RETURN COALESCE(NEW, OLD);
127
+ END;
128
+ $$ LANGUAGE plpgsql
129
+ `.execute(db);
130
+
131
+ await sql`
132
+ CREATE TRIGGER views_notify_trigger
133
+ AFTER INSERT OR UPDATE OR DELETE ON "views"
134
+ FOR EACH ROW EXECUTE FUNCTION notify_view_changes()
135
+ `.execute(db);
136
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@secondlayer/shared",
3
- "version": "0.3.0",
3
+ "version": "0.5.0",
4
4
  "type": "module",
5
5
  "main": "./dist/src/index.js",
6
6
  "types": "./dist/src/index.d.ts",
@@ -29,9 +29,13 @@
29
29
  "types": "./dist/src/db/queries/usage.d.ts",
30
30
  "import": "./dist/src/db/queries/usage.js"
31
31
  },
32
- "./db/queries/views": {
33
- "types": "./dist/src/db/queries/views.d.ts",
34
- "import": "./dist/src/db/queries/views.js"
32
+ "./db/queries/subgraphs": {
33
+ "types": "./dist/src/db/queries/subgraphs.d.ts",
34
+ "import": "./dist/src/db/queries/subgraphs.js"
35
+ },
36
+ "./db/queries/contracts": {
37
+ "types": "./dist/src/db/queries/contracts.d.ts",
38
+ "import": "./dist/src/db/queries/contracts.js"
35
39
  },
36
40
  "./db/jsonb": {
37
41
  "types": "./dist/src/db/jsonb.d.ts",
@@ -65,9 +69,9 @@
65
69
  "types": "./dist/src/schemas/filters.d.ts",
66
70
  "import": "./dist/src/schemas/filters.js"
67
71
  },
68
- "./schemas/views": {
69
- "types": "./dist/src/schemas/views.d.ts",
70
- "import": "./dist/src/schemas/views.js"
72
+ "./schemas/subgraphs": {
73
+ "types": "./dist/src/schemas/subgraphs.d.ts",
74
+ "import": "./dist/src/schemas/subgraphs.js"
71
75
  },
72
76
  "./types": {
73
77
  "types": "./dist/src/types.d.ts",
@@ -120,7 +124,7 @@
120
124
  "prepublishOnly": "bun run build"
121
125
  },
122
126
  "dependencies": {
123
- "@secondlayer/stacks": "^0.1.0",
127
+ "@secondlayer/stacks": "^0.2.0",
124
128
  "kysely": "0.28.10",
125
129
  "kysely-postgres-js": "3.0.0",
126
130
  "postgres": "^3.4.6",
@@ -1,11 +0,0 @@
1
- {
2
- "version": 3,
3
- "sources": ["../src/db/jsonb.ts", "../src/db/queries/views.ts"],
4
- "sourcesContent": [
5
- "import { sql, type RawBuilder } 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 */\nexport function jsonb(value: unknown): RawBuilder<unknown> {\n const escaped = JSON.stringify(value).replace(/'/g, \"''\");\n return 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 if (typeof value === \"string\") {\n try {\n return JSON.parse(value) as T;\n } catch {\n return value as T;\n }\n }\n return (value ?? {}) as T;\n}\n",
6
- "import { sql, type Kysely } from \"kysely\";\nimport { jsonb } from \"../jsonb.ts\";\nimport type { Database, View } from \"../types.ts\";\n\n/**\n * Convert a view name to its PostgreSQL schema name.\n * With keyPrefix: \"view_{prefix}_{name}\" (tenant-isolated)\n * Without keyPrefix: \"view_{name}\" (backward compat)\n */\nexport function pgSchemaName(viewName: string, keyPrefix?: string): string {\n const safeName = viewName.replace(/-/g, \"_\");\n if (!keyPrefix) {\n return `view_${safeName}`;\n }\n const safePrefix = keyPrefix.replace(/^sk-sl_/, \"\").replace(/-/g, \"_\");\n return `view_${safePrefix}_${safeName}`;\n}\n\nexport async function registerView(\n db: Kysely<Database>,\n data: {\n name: string;\n version: string;\n definition: Record<string, unknown>;\n schemaHash: string;\n handlerPath: string;\n apiKeyId?: string;\n schemaName?: string;\n },\n): Promise<View> {\n return await db\n .insertInto(\"views\")\n .values({\n name: data.name,\n version: data.version,\n definition: jsonb(data.definition) as any,\n schema_hash: data.schemaHash,\n handler_path: data.handlerPath,\n api_key_id: data.apiKeyId ?? null,\n schema_name: data.schemaName ?? null,\n })\n .onConflict((oc) =>\n oc.columns([\"name\", \"api_key_id\"]).doUpdateSet({\n version: data.version,\n definition: jsonb(data.definition) as any,\n schema_hash: data.schemaHash,\n handler_path: data.handlerPath,\n schema_name: data.schemaName ?? null,\n updated_at: new Date(),\n }),\n )\n .returningAll()\n .executeTakeFirstOrThrow();\n}\n\nexport async function getView(db: Kysely<Database>, name: string, apiKeyId?: string): Promise<View | null> {\n let query = db\n .selectFrom(\"views\")\n .selectAll()\n .where(\"name\", \"=\", name);\n\n if (apiKeyId) {\n query = query.where(\"api_key_id\", \"=\", apiKeyId);\n }\n\n return (await query.executeTakeFirst()) ?? null;\n}\n\nexport async function listViews(db: Kysely<Database>, apiKeyId?: string): Promise<View[]> {\n let query = db.selectFrom(\"views\").selectAll();\n if (apiKeyId) {\n query = query.where(\"api_key_id\", \"=\", apiKeyId);\n }\n return query.execute();\n}\n\nexport async function updateViewStatus(\n db: Kysely<Database>,\n name: string,\n status: string,\n lastProcessedBlock?: number,\n): Promise<void> {\n await db\n .updateTable(\"views\")\n .set({\n status,\n ...(lastProcessedBlock !== undefined ? { last_processed_block: lastProcessedBlock } : {}),\n updated_at: new Date(),\n })\n .where(\"name\", \"=\", name)\n .execute();\n}\n\nexport async function recordViewProcessed(\n db: Kysely<Database>,\n name: string,\n processed: number,\n errors: number,\n lastError?: string,\n): Promise<void> {\n await db\n .updateTable(\"views\")\n .set({\n total_processed: sql`total_processed + ${processed}`,\n total_errors: sql`total_errors + ${errors}`,\n ...(lastError\n ? { last_error: lastError, last_error_at: new Date() }\n : {}),\n updated_at: new Date(),\n })\n .where(\"name\", \"=\", name)\n .execute();\n}\n\nexport async function updateViewHandlerPath(\n db: Kysely<Database>,\n name: string,\n handlerPath: string,\n): Promise<void> {\n await db\n .updateTable(\"views\")\n .set({ handler_path: handlerPath, updated_at: new Date() })\n .where(\"name\", \"=\", name)\n .execute();\n}\n\nexport async function deleteView(db: Kysely<Database>, name: string, apiKeyId?: string): Promise<View | null> {\n const view = await getView(db, name, apiKeyId);\n if (!view) return null;\n\n // Use stored schema_name if available, otherwise compute\n const schemaName = view.schema_name ?? pgSchemaName(name);\n\n // Drop the view's schema (CASCADE drops all tables within)\n await sql`DROP SCHEMA IF EXISTS ${sql.raw(`\"${schemaName}\"`)} CASCADE`.execute(db);\n\n // Remove from registry\n await db.deleteFrom(\"views\").where(\"id\", \"=\", view.id).execute();\n\n return view;\n}\n"
7
- ],
8
- "mappings": "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",
9
- "debugId": "C19949368802BB4964756E2164756E21",
10
- "names": []
11
- }
@@ -1,10 +0,0 @@
1
- {
2
- "version": 3,
3
- "sources": ["../src/schemas/views.ts"],
4
- "sourcesContent": [
5
- "import { z } from \"zod\";\n\n// ── Deploy View Request ─────────────────────────────────────────────────\n\nexport interface DeployViewRequest {\n name: string;\n version?: string;\n description?: string;\n sources: string[];\n schema: Record<string, unknown>;\n handlerCode: string;\n reindex?: boolean;\n}\n\nexport const DeployViewRequestSchema: z.ZodType<DeployViewRequest> = z.object({\n name: z.string().regex(/^[a-z0-9-]+$/, \"lowercase alphanumeric + hyphens only\").max(63),\n version: z.string().optional(),\n description: z.string().optional(),\n sources: z.array(z.string()).min(1),\n schema: z.record(z.unknown()),\n handlerCode: z.string().max(1_048_576, \"handler code exceeds 1MB limit\"),\n reindex: z.boolean().optional(),\n});\n\nexport interface DeployViewResponse {\n action: \"created\" | \"unchanged\" | \"updated\" | \"reindexed\";\n viewId: string;\n message: string;\n}\n\n// View API response types\n\nexport interface ViewSummary {\n name: string;\n version: string;\n status: string;\n lastProcessedBlock: number;\n tables: string[];\n createdAt: string;\n}\n\nexport interface ViewDetail {\n name: string;\n version: string;\n status: string;\n lastProcessedBlock: number;\n health: {\n totalProcessed: number;\n totalErrors: number;\n errorRate: number;\n lastError: string | null;\n lastErrorAt: string | null;\n };\n tables: Record<string, {\n endpoint: string;\n columns: Record<string, { type: string; nullable?: boolean }>;\n rowCount: number;\n example: string;\n }>;\n createdAt: string;\n updatedAt: string;\n}\n\nexport interface ReindexResponse {\n message: string;\n fromBlock: number;\n toBlock: number | string;\n}\n\nexport interface ViewQueryParams {\n sort?: string;\n order?: string;\n limit?: number;\n offset?: number;\n fields?: string;\n filters?: Record<string, string>;\n}\n"
6
- ],
7
- "mappings": ";;;;;;;;;;;;;AAAA;AAcO,IAAM,0BAAwD,EAAE,OAAO;AAAA,EAC5E,MAAM,EAAE,OAAO,EAAE,MAAM,gBAAgB,uCAAuC,EAAE,IAAI,EAAE;AAAA,EACtF,SAAS,EAAE,OAAO,EAAE,SAAS;AAAA,EAC7B,aAAa,EAAE,OAAO,EAAE,SAAS;AAAA,EACjC,SAAS,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,IAAI,CAAC;AAAA,EAClC,QAAQ,EAAE,OAAO,EAAE,QAAQ,CAAC;AAAA,EAC5B,aAAa,EAAE,OAAO,EAAE,IAAI,SAAW,gCAAgC;AAAA,EACvE,SAAS,EAAE,QAAQ,EAAE,SAAS;AAChC,CAAC;",
8
- "debugId": "F7D47497A7667C0064756E2164756E21",
9
- "names": []
10
- }