@secondlayer/shared 6.28.0 → 6.29.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.d.ts +40 -2
- package/dist/src/db/index.js +3 -1
- package/dist/src/db/index.js.map +3 -3
- package/dist/src/db/queries/chain-reorgs.d.ts +35 -1
- package/dist/src/db/queries/chain-reorgs.js +3 -1
- package/dist/src/db/queries/chain-reorgs.js.map +3 -3
- package/dist/src/db/queries/contracts.d.ts +35 -1
- package/dist/src/db/queries/integrity.d.ts +35 -1
- package/dist/src/db/queries/subgraph-gaps.d.ts +35 -1
- package/dist/src/db/queries/subgraph-operations.d.ts +35 -1
- package/dist/src/db/queries/subgraphs.d.ts +43 -2
- package/dist/src/db/queries/subgraphs.js +11 -1
- package/dist/src/db/queries/subgraphs.js.map +4 -4
- package/dist/src/db/queries/subscriptions.d.ts +35 -1
- package/dist/src/db/schema.d.ts +38 -2
- package/dist/src/errors.d.ts +11 -2
- package/dist/src/errors.js +13 -2
- package/dist/src/errors.js.map +3 -3
- package/dist/src/index.d.ts +62 -3
- package/dist/src/index.js +99 -38
- package/dist/src/index.js.map +6 -6
- package/dist/src/node/hiro-client.d.ts +15 -1
- package/dist/src/node/hiro-client.js +10 -1
- package/dist/src/node/hiro-client.js.map +3 -3
- package/dist/src/node/local-client.d.ts +35 -1
- package/dist/src/schemas/index.d.ts +10 -0
- package/dist/src/schemas/index.js +3 -2
- package/dist/src/schemas/index.js.map +3 -3
- package/dist/src/schemas/subgraphs.d.ts +10 -0
- package/dist/src/schemas/subgraphs.js +3 -2
- package/dist/src/schemas/subgraphs.js.map +3 -3
- package/dist/src/subgraphs/spec.d.ts +3 -0
- package/dist/src/subgraphs/spec.js +83 -36
- package/dist/src/subgraphs/spec.js.map +3 -3
- package/dist/src/x402.d.ts +38 -0
- package/dist/src/x402.js +74 -0
- package/dist/src/x402.js.map +10 -0
- package/migrations/0090_events_streams_filter_idx.ts +72 -0
- package/migrations/0091_x402_payments.ts +41 -0
- package/migrations/0092_subgraph_visibility.ts +33 -0
- package/migrations/0093_ghost_accounts.ts +47 -0
- package/package.json +6 -2
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../src/schemas/subgraphs.ts"],
|
|
4
4
|
"sourcesContent": [
|
|
5
|
-
"import { z } from \"zod/v4\";\n\n// ── Deploy Subgraph Request ─────────────────────────────────────────────────\n\nexport interface DeploySubgraphRequest {\n\tname: string;\n\tversion?: string;\n\tdescription?: string;\n\tsources: Record<string, Record<string, unknown>>;\n\tschema: Record<string, unknown>;\n\thandlerCode: string;\n\t/** Override the definition's startBlock for this deploy only. */\n\tstartBlock?: number;\n\t/** Original TypeScript source, persisted so chat can read/diff/edit later. */\n\tsourceCode?: string;\n\t/**\n\t * BYO data plane: a user-owned Postgres connection string. When set, the\n\t * subgraph's schema, handler writes, and serving reads live in this DB instead\n\t * of the managed one. Stored encrypted at rest, never returned.\n\t */\n\tdatabaseUrl?: string;\n\t/** Validate the connection + print the DDL/grant plan without deploying. */\n\tdryRun?: boolean;\n}\n\nexport const DeploySubgraphRequestSchema: z.ZodType<DeploySubgraphRequest> =\n\tz.object({\n\t\tname: z\n\t\t\t.string()\n\t\t\t.regex(/^[a-z0-9-]+$/, \"lowercase alphanumeric + hyphens only\")\n\t\t\t.max(63),\n\t\tversion: z.string().optional(),\n\t\tdescription: z.string().optional(),\n\t\tsources: z\n\t\t\t.record(z.string(), z.record(z.string(), z.unknown()))\n\t\t\t.refine(\n\t\t\t\t(s) => Object.keys(s).length > 0,\n\t\t\t\t\"Must have at least one source\",\n\t\t\t),\n\t\tschema: z.record(z.string(), z.unknown()),\n\t\thandlerCode: z.string().max(1_048_576, \"handler code exceeds 1MB limit\"),\n\t\tstartBlock: z.number().int().nonnegative().optional(),\n\t\tsourceCode: z\n\t\t\t.string()\n\t\t\t.max(1_048_576, \"source code exceeds 1MB limit\")\n\t\t\t.optional(),\n\t\tdatabaseUrl: z\n\t\t\t.string()\n\t\t\t.url()\n\t\t\t.refine(\n\t\t\t\t(u) => u.startsWith(\"postgres://\") || u.startsWith(\"postgresql://\"),\n\t\t\t\t\"must be a postgres:// connection string\",\n\t\t\t)\n\t\t\t.optional(),\n\t\tdryRun: z.boolean().optional(),\n\t});\n\nexport interface DeploySubgraphResponse {\n\taction: \"created\" | \"unchanged\" | \"handler_updated\" | \"updated\" | \"reindexed\";\n\tsubgraphId: string;\n\tversion: string;\n\tmessage: string;\n\toperationId?: string;\n\treindexStarted?: boolean;\n\tdiff?: {\n\t\taddedTables: string[];\n\t\tremovedTables: string[];\n\t\taddedColumns: Record<string, string[]>;\n\t\tbreakingChanges: string[];\n\t};\n}\n\n// Subgraph API response types\n\nexport interface SubgraphSummary {\n\tname: string;\n\tversion: string;\n\tstatus: string;\n\tlastProcessedBlock: number;\n\ttotalProcessed: number;\n\ttotalErrors: number;\n\ttables: string[];\n\tchainTip: number;\n\tsourceChainTip?: number;\n\ttargetBlock?: number;\n\tprogress: number;\n\tblocksRemaining?: number;\n\tsyncMode?: \"sync\" | \"reindex\";\n\tresourceWarning?: SubgraphResourceWarning;\n\tgapCount: number;\n\tintegrity: \"complete\" | \"gaps_detected\";\n\tcreatedAt: string;\n}\n\nexport interface SubgraphGapRange {\n\tstart: number;\n\tend: number;\n\tsize: number;\n\treason: string;\n}\n\nexport interface SubgraphSyncInfo {\n\tstatus: \"synced\" | \"catching_up\" | \"reindexing\" | \"error\";\n\tmode?: \"sync\" | \"reindex\";\n\tstartBlock: number;\n\tlastProcessedBlock: number;\n\t/**\n\t * Backward-compatible denominator for progress displays. During reindexing,\n\t * this is the reindex target block rather than the live source chain tip.\n\t */\n\tchainTip: number;\n\tsourceChainTip?: number;\n\ttargetBlock?: number;\n\tblocksRemaining: number;\n\tprocessedBlocks?: number;\n\ttotalBlocks?: number;\n\tprogress: number;\n\tresourceWarning?: SubgraphResourceWarning;\n\tgaps: {\n\t\tcount: number;\n\t\ttotalMissingBlocks: number;\n\t\tranges: SubgraphGapRange[];\n\t};\n\tintegrity: \"complete\" | \"gaps_detected\";\n}\n\nexport interface SubgraphResourceWarning {\n\tcode: string;\n\tmessage: string;\n\tplan?: string;\n\tblockRange: number;\n\tprocessorMemoryMb: number;\n\trecommendedPlan: \"launch\";\n}\n\nexport interface SubgraphDetail {\n\tname: string;\n\tversion: string;\n\tschemaHash?: string;\n\tstatus: string;\n\tlastProcessedBlock: number;\n\tdescription?: string;\n\tsources?: Record<string, unknown>;\n\tdefinition?: Record<string, unknown>;\n\thealth: {\n\t\ttotalProcessed: number;\n\t\ttotalErrors: number;\n\t\terrorRate: number;\n\t\tlastError: string | null;\n\t\tlastErrorAt: string | null;\n\t};\n\tsync: SubgraphSyncInfo;\n\ttables: Record<\n\t\tstring,\n\t\t{\n\t\t\tendpoint: string;\n\t\t\tcolumns: Record<\n\t\t\t\tstring,\n\t\t\t\t{\n\t\t\t\t\ttype: string;\n\t\t\t\t\tnullable?: boolean;\n\t\t\t\t\tindexed?: boolean;\n\t\t\t\t\tsearchable?: boolean;\n\t\t\t\t\tdefault?: string | number | boolean;\n\t\t\t\t}\n\t\t\t>;\n\t\t\trowCount: number;\n\t\t\texample: string;\n\t\t\tindexes?: string[][];\n\t\t\tuniqueKeys?: string[][];\n\t\t}\n\t>;\n\tcreatedAt: string;\n\tupdatedAt: string;\n}\n\nexport interface SubgraphGapEntry {\n\tstart: number;\n\tend: number;\n\tsize: number;\n\treason: string;\n\tdetectedAt: string;\n\tresolvedAt: string | null;\n}\n\nexport interface SubgraphGapsResponse {\n\tdata: SubgraphGapEntry[];\n\tmeta: {\n\t\ttotal: number;\n\t\ttotalMissingBlocks: number;\n\t\tlimit: number;\n\t\toffset: number;\n\t};\n}\n\nexport interface ReindexResponse {\n\tmessage: string;\n\tfromBlock: number;\n\ttoBlock: number | string;\n\toperationId?: string;\n\tstatus?: \"queued\" | \"running\" | \"cancel_requested\";\n}\n\nexport interface SubgraphQueryParams {\n\tsort?: string;\n\torder?: string;\n\tlimit?: number;\n\toffset?: number;\n\tfields?: string;\n\tfilters?: Record<string, string>;\n}\n\n/**\n * Request shape for `GET /api/subgraphs/:subgraphName/:tableName/aggregate`.\n * `filters` reuses the list/count where-surface; the rest name the columns to\n * aggregate. SUM/MIN/MAX columns must be numeric (uint/int, plus `_block_height`).\n */\nexport interface SubgraphAggregateParams {\n\tfilters?: Record<string, string>;\n\tcount?: boolean;\n\tcountDistinct?: string[];\n\tsum?: string[];\n\tmin?: string[];\n\tmax?: string[];\n}\n\n/**\n * Aggregate response. Keys are present only for requested aggregates.\n * `count`/`countDistinct` are JSON numbers (counts << 2^53); `sum`/`min`/`max`\n * are lossless strings (NUMERIC `::text`). `sum` of an empty set is `\"0\"`;\n * `min`/`max` are `null` when the filtered set is empty or all-null.\n */\nexport interface SubgraphAggregateResponse {\n\tcount?: number;\n\tcountDistinct?: Record<string, number>;\n\tsum?: Record<string, string>;\n\tmin?: Record<string, string | null>;\n\tmax?: Record<string, string | null>;\n}\n"
|
|
5
|
+
"import { z } from \"zod/v4\";\n\n// ── Deploy Subgraph Request ─────────────────────────────────────────────────\n\nexport interface DeploySubgraphRequest {\n\tname: string;\n\tversion?: string;\n\tdescription?: string;\n\tsources: Record<string, Record<string, unknown>>;\n\tschema: Record<string, unknown>;\n\thandlerCode: string;\n\t/** Override the definition's startBlock for this deploy only. */\n\tstartBlock?: number;\n\t/** Original TypeScript source, persisted so chat can read/diff/edit later. */\n\tsourceCode?: string;\n\t/**\n\t * BYO data plane: a user-owned Postgres connection string. When set, the\n\t * subgraph's schema, handler writes, and serving reads live in this DB instead\n\t * of the managed one. Stored encrypted at rest, never returned.\n\t */\n\tdatabaseUrl?: string;\n\t/** Validate the connection + print the DDL/grant plan without deploying. */\n\tdryRun?: boolean;\n\t/**\n\t * Read visibility. Server-side defaults: managed deploys → public (anon\n\t * reads on /v1/subgraphs, name claimed in the global public namespace);\n\t * BYO-database deploys → private (public reads hit the user's own\n\t * Postgres, so going public is an explicit choice).\n\t */\n\tvisibility?: \"public\" | \"private\";\n}\n\nexport const DeploySubgraphRequestSchema: z.ZodType<DeploySubgraphRequest> =\n\tz.object({\n\t\tname: z\n\t\t\t.string()\n\t\t\t.regex(/^[a-z0-9-]+$/, \"lowercase alphanumeric + hyphens only\")\n\t\t\t.max(63),\n\t\tversion: z.string().optional(),\n\t\tdescription: z.string().optional(),\n\t\tsources: z\n\t\t\t.record(z.string(), z.record(z.string(), z.unknown()))\n\t\t\t.refine(\n\t\t\t\t(s) => Object.keys(s).length > 0,\n\t\t\t\t\"Must have at least one source\",\n\t\t\t),\n\t\tschema: z.record(z.string(), z.unknown()),\n\t\thandlerCode: z.string().max(1_048_576, \"handler code exceeds 1MB limit\"),\n\t\tstartBlock: z.number().int().nonnegative().optional(),\n\t\tsourceCode: z\n\t\t\t.string()\n\t\t\t.max(1_048_576, \"source code exceeds 1MB limit\")\n\t\t\t.optional(),\n\t\tdatabaseUrl: z\n\t\t\t.string()\n\t\t\t.url()\n\t\t\t.refine(\n\t\t\t\t(u) => u.startsWith(\"postgres://\") || u.startsWith(\"postgresql://\"),\n\t\t\t\t\"must be a postgres:// connection string\",\n\t\t\t)\n\t\t\t.optional(),\n\t\tdryRun: z.boolean().optional(),\n\t\tvisibility: z.enum([\"public\", \"private\"]).optional(),\n\t});\n\nexport interface DeploySubgraphResponse {\n\taction: \"created\" | \"unchanged\" | \"handler_updated\" | \"updated\" | \"reindexed\";\n\tsubgraphId: string;\n\tversion: string;\n\tvisibility?: \"public\" | \"private\";\n\tmessage: string;\n\toperationId?: string;\n\treindexStarted?: boolean;\n\tdiff?: {\n\t\taddedTables: string[];\n\t\tremovedTables: string[];\n\t\taddedColumns: Record<string, string[]>;\n\t\tbreakingChanges: string[];\n\t};\n}\n\n// Subgraph API response types\n\nexport interface SubgraphSummary {\n\tname: string;\n\tversion: string;\n\tstatus: string;\n\tlastProcessedBlock: number;\n\ttotalProcessed: number;\n\ttotalErrors: number;\n\ttables: string[];\n\tchainTip: number;\n\tsourceChainTip?: number;\n\ttargetBlock?: number;\n\tprogress: number;\n\tblocksRemaining?: number;\n\tsyncMode?: \"sync\" | \"reindex\";\n\tresourceWarning?: SubgraphResourceWarning;\n\tgapCount: number;\n\tintegrity: \"complete\" | \"gaps_detected\";\n\tvisibility?: \"public\" | \"private\";\n\tcreatedAt: string;\n}\n\nexport interface SubgraphGapRange {\n\tstart: number;\n\tend: number;\n\tsize: number;\n\treason: string;\n}\n\nexport interface SubgraphSyncInfo {\n\tstatus: \"synced\" | \"catching_up\" | \"reindexing\" | \"error\";\n\tmode?: \"sync\" | \"reindex\";\n\tstartBlock: number;\n\tlastProcessedBlock: number;\n\t/**\n\t * Backward-compatible denominator for progress displays. During reindexing,\n\t * this is the reindex target block rather than the live source chain tip.\n\t */\n\tchainTip: number;\n\tsourceChainTip?: number;\n\ttargetBlock?: number;\n\tblocksRemaining: number;\n\tprocessedBlocks?: number;\n\ttotalBlocks?: number;\n\tprogress: number;\n\tresourceWarning?: SubgraphResourceWarning;\n\tgaps: {\n\t\tcount: number;\n\t\ttotalMissingBlocks: number;\n\t\tranges: SubgraphGapRange[];\n\t};\n\tintegrity: \"complete\" | \"gaps_detected\";\n}\n\nexport interface SubgraphResourceWarning {\n\tcode: string;\n\tmessage: string;\n\tplan?: string;\n\tblockRange: number;\n\tprocessorMemoryMb: number;\n\trecommendedPlan: \"launch\";\n}\n\nexport interface SubgraphDetail {\n\tname: string;\n\tversion: string;\n\tschemaHash?: string;\n\tstatus: string;\n\tvisibility?: \"public\" | \"private\";\n\tlastProcessedBlock: number;\n\tdescription?: string;\n\tsources?: Record<string, unknown>;\n\tdefinition?: Record<string, unknown>;\n\thealth: {\n\t\ttotalProcessed: number;\n\t\ttotalErrors: number;\n\t\terrorRate: number;\n\t\tlastError: string | null;\n\t\tlastErrorAt: string | null;\n\t};\n\tsync: SubgraphSyncInfo;\n\ttables: Record<\n\t\tstring,\n\t\t{\n\t\t\tendpoint: string;\n\t\t\tcolumns: Record<\n\t\t\t\tstring,\n\t\t\t\t{\n\t\t\t\t\ttype: string;\n\t\t\t\t\tnullable?: boolean;\n\t\t\t\t\tindexed?: boolean;\n\t\t\t\t\tsearchable?: boolean;\n\t\t\t\t\tdefault?: string | number | boolean;\n\t\t\t\t}\n\t\t\t>;\n\t\t\trowCount: number;\n\t\t\texample: string;\n\t\t\tindexes?: string[][];\n\t\t\tuniqueKeys?: string[][];\n\t\t}\n\t>;\n\tcreatedAt: string;\n\tupdatedAt: string;\n}\n\nexport interface SubgraphGapEntry {\n\tstart: number;\n\tend: number;\n\tsize: number;\n\treason: string;\n\tdetectedAt: string;\n\tresolvedAt: string | null;\n}\n\nexport interface SubgraphGapsResponse {\n\tdata: SubgraphGapEntry[];\n\tmeta: {\n\t\ttotal: number;\n\t\ttotalMissingBlocks: number;\n\t\tlimit: number;\n\t\toffset: number;\n\t};\n}\n\nexport interface ReindexResponse {\n\tmessage: string;\n\tfromBlock: number;\n\ttoBlock: number | string;\n\toperationId?: string;\n\tstatus?: \"queued\" | \"running\" | \"cancel_requested\";\n}\n\nexport interface SubgraphQueryParams {\n\tsort?: string;\n\torder?: string;\n\tlimit?: number;\n\toffset?: number;\n\tfields?: string;\n\tfilters?: Record<string, string>;\n}\n\n/**\n * Request shape for `GET /api/subgraphs/:subgraphName/:tableName/aggregate`.\n * `filters` reuses the list/count where-surface; the rest name the columns to\n * aggregate. SUM/MIN/MAX columns must be numeric (uint/int, plus `_block_height`).\n */\nexport interface SubgraphAggregateParams {\n\tfilters?: Record<string, string>;\n\tcount?: boolean;\n\tcountDistinct?: string[];\n\tsum?: string[];\n\tmin?: string[];\n\tmax?: string[];\n}\n\n/**\n * Aggregate response. Keys are present only for requested aggregates.\n * `count`/`countDistinct` are JSON numbers (counts << 2^53); `sum`/`min`/`max`\n * are lossless strings (NUMERIC `::text`). `sum` of an empty set is `\"0\"`;\n * `min`/`max` are `null` when the filtered set is empty or all-null.\n */\nexport interface SubgraphAggregateResponse {\n\tcount?: number;\n\tcountDistinct?: Record<string, number>;\n\tsum?: Record<string, string>;\n\tmin?: Record<string, string | null>;\n\tmax?: Record<string, string | null>;\n}\n"
|
|
6
6
|
],
|
|
7
|
-
"mappings": ";;;;;;;;;;;;;;;;;AAAA;
|
|
8
|
-
"debugId": "
|
|
7
|
+
"mappings": ";;;;;;;;;;;;;;;;;AAAA;AAgCO,IAAM,8BACZ,EAAE,OAAO;AAAA,EACR,MAAM,EACJ,OAAO,EACP,MAAM,gBAAgB,uCAAuC,EAC7D,IAAI,EAAE;AAAA,EACR,SAAS,EAAE,OAAO,EAAE,SAAS;AAAA,EAC7B,aAAa,EAAE,OAAO,EAAE,SAAS;AAAA,EACjC,SAAS,EACP,OAAO,EAAE,OAAO,GAAG,EAAE,OAAO,EAAE,OAAO,GAAG,EAAE,QAAQ,CAAC,CAAC,EACpD,OACA,CAAC,MAAM,OAAO,KAAK,CAAC,EAAE,SAAS,GAC/B,+BACD;AAAA,EACD,QAAQ,EAAE,OAAO,EAAE,OAAO,GAAG,EAAE,QAAQ,CAAC;AAAA,EACxC,aAAa,EAAE,OAAO,EAAE,IAAI,SAAW,gCAAgC;AAAA,EACvE,YAAY,EAAE,OAAO,EAAE,IAAI,EAAE,YAAY,EAAE,SAAS;AAAA,EACpD,YAAY,EACV,OAAO,EACP,IAAI,SAAW,+BAA+B,EAC9C,SAAS;AAAA,EACX,aAAa,EACX,OAAO,EACP,IAAI,EACJ,OACA,CAAC,MAAM,EAAE,WAAW,aAAa,KAAK,EAAE,WAAW,eAAe,GAClE,yCACD,EACC,SAAS;AAAA,EACX,QAAQ,EAAE,QAAQ,EAAE,SAAS;AAAA,EAC7B,YAAY,EAAE,KAAK,CAAC,UAAU,SAAS,CAAC,EAAE,SAAS;AACpD,CAAC;",
|
|
8
|
+
"debugId": "53B5E18D3DA61A3D64756E2164756E21",
|
|
9
9
|
"names": []
|
|
10
10
|
}
|
|
@@ -41,6 +41,7 @@ interface SubgraphDetail {
|
|
|
41
41
|
version: string;
|
|
42
42
|
schemaHash?: string;
|
|
43
43
|
status: string;
|
|
44
|
+
visibility?: "public" | "private";
|
|
44
45
|
lastProcessedBlock: number;
|
|
45
46
|
description?: string;
|
|
46
47
|
sources?: Record<string, unknown>;
|
|
@@ -86,6 +87,8 @@ interface SubgraphAgentSchema {
|
|
|
86
87
|
tables: Record<string, {
|
|
87
88
|
endpoint: string
|
|
88
89
|
countEndpoint: string
|
|
90
|
+
aggregateEndpoint?: string
|
|
91
|
+
streamEndpoint?: string
|
|
89
92
|
rowCount: number
|
|
90
93
|
columns: SubgraphDetail["tables"][string]["columns"]
|
|
91
94
|
indexes?: string[][]
|
|
@@ -17,18 +17,23 @@ var __export = (target, all) => {
|
|
|
17
17
|
// src/subgraphs/spec.ts
|
|
18
18
|
var SYSTEM_COLUMNS = ["_id", "_block_height", "_tx_id", "_created_at"];
|
|
19
19
|
var BASE_QUERY_PARAMS = ["_limit", "_offset", "_sort", "_order", "_fields"];
|
|
20
|
+
var PUBLIC_BASE_QUERY_PARAMS = ["_limit", "cursor", "_order", "_fields"];
|
|
20
21
|
var COMPARISON_OPS = ["neq", "gt", "gte", "lt", "lte"];
|
|
22
|
+
function isPublicRead(detail) {
|
|
23
|
+
return detail.visibility === "public";
|
|
24
|
+
}
|
|
21
25
|
function generatedAt(options) {
|
|
22
26
|
return options.generatedAt ?? new Date().toISOString();
|
|
23
27
|
}
|
|
24
28
|
function normalizeServerUrl(serverUrl) {
|
|
25
29
|
return (serverUrl ?? "https://api.secondlayer.tools").replace(/\/+$/, "");
|
|
26
30
|
}
|
|
27
|
-
function tablePath(subgraphName, tableName) {
|
|
28
|
-
|
|
31
|
+
function tablePath(subgraphName, tableName, publicRead) {
|
|
32
|
+
const base = publicRead ? "/v1/subgraphs" : "/api/subgraphs";
|
|
33
|
+
return `${base}/${subgraphName}/${tableName}`;
|
|
29
34
|
}
|
|
30
|
-
function countPath(subgraphName, tableName) {
|
|
31
|
-
return `${tablePath(subgraphName, tableName)}/count`;
|
|
35
|
+
function countPath(subgraphName, tableName, publicRead) {
|
|
36
|
+
return `${tablePath(subgraphName, tableName, publicRead)}/count`;
|
|
32
37
|
}
|
|
33
38
|
function isTextLike(type) {
|
|
34
39
|
return type === "text" || type === "principal" || type === "timestamp";
|
|
@@ -115,8 +120,8 @@ function filterNames(table) {
|
|
|
115
120
|
}
|
|
116
121
|
return result;
|
|
117
122
|
}
|
|
118
|
-
function queryParameters(table) {
|
|
119
|
-
const params = [...BASE_QUERY_PARAMS];
|
|
123
|
+
function queryParameters(table, publicRead) {
|
|
124
|
+
const params = publicRead ? [...PUBLIC_BASE_QUERY_PARAMS] : [...BASE_QUERY_PARAMS];
|
|
120
125
|
if (searchableColumns(table).length > 0)
|
|
121
126
|
params.push("_search");
|
|
122
127
|
return params;
|
|
@@ -137,8 +142,24 @@ function openApiParameter(name, description, schema = { type: "string" }) {
|
|
|
137
142
|
schema
|
|
138
143
|
};
|
|
139
144
|
}
|
|
140
|
-
function tableParameters(table) {
|
|
141
|
-
const parameters = [
|
|
145
|
+
function tableParameters(table, publicRead) {
|
|
146
|
+
const parameters = publicRead ? [
|
|
147
|
+
openApiParameter("_limit", "Maximum rows to return.", {
|
|
148
|
+
type: "integer",
|
|
149
|
+
default: 50,
|
|
150
|
+
minimum: 1,
|
|
151
|
+
maximum: 1000
|
|
152
|
+
}),
|
|
153
|
+
openApiParameter("cursor", "Resume token from next_cursor (keyset pagination on _id).", { type: "string" }),
|
|
154
|
+
openApiParameter("_order", "Sort direction (_id keyset).", {
|
|
155
|
+
type: "string",
|
|
156
|
+
enum: ["asc", "desc"],
|
|
157
|
+
default: "asc"
|
|
158
|
+
}),
|
|
159
|
+
openApiParameter("_fields", "Comma-separated columns to include.", {
|
|
160
|
+
type: "string"
|
|
161
|
+
})
|
|
162
|
+
] : [
|
|
142
163
|
openApiParameter("_limit", "Maximum rows to return.", {
|
|
143
164
|
type: "integer",
|
|
144
165
|
default: 50,
|
|
@@ -192,19 +213,24 @@ function tableParameters(table) {
|
|
|
192
213
|
}
|
|
193
214
|
function generateSubgraphAgentSchema(detail, options = {}) {
|
|
194
215
|
const serverUrl = normalizeServerUrl(options.serverUrl);
|
|
216
|
+
const publicRead = isPublicRead(detail);
|
|
195
217
|
const tables = {};
|
|
196
218
|
for (const [tableName, table] of Object.entries(detail.tables)) {
|
|
197
|
-
const path = tablePath(detail.name, tableName);
|
|
219
|
+
const path = tablePath(detail.name, tableName, publicRead);
|
|
198
220
|
tables[tableName] = {
|
|
199
221
|
endpoint: `${serverUrl}${path}`,
|
|
200
|
-
countEndpoint: `${serverUrl}${countPath(detail.name, tableName)}`,
|
|
222
|
+
countEndpoint: `${serverUrl}${countPath(detail.name, tableName, publicRead)}`,
|
|
223
|
+
...publicRead ? {
|
|
224
|
+
aggregateEndpoint: `${serverUrl}${path}/aggregate`,
|
|
225
|
+
streamEndpoint: `${serverUrl}${path}/stream`
|
|
226
|
+
} : {},
|
|
201
227
|
rowCount: table.rowCount,
|
|
202
228
|
columns: table.columns,
|
|
203
229
|
...table.indexes ? { indexes: table.indexes } : {},
|
|
204
230
|
...table.uniqueKeys ? { uniqueKeys: table.uniqueKeys } : {},
|
|
205
231
|
query: {
|
|
206
|
-
parameters: queryParameters(table),
|
|
207
|
-
sortable: selectableColumns(table),
|
|
232
|
+
parameters: queryParameters(table, publicRead),
|
|
233
|
+
sortable: publicRead ? [] : selectableColumns(table),
|
|
208
234
|
selectable: selectableColumns(table),
|
|
209
235
|
searchable: searchableColumns(table),
|
|
210
236
|
filters: filterNames(table)
|
|
@@ -212,7 +238,7 @@ function generateSubgraphAgentSchema(detail, options = {}) {
|
|
|
212
238
|
examples: {
|
|
213
239
|
list: rowExample(table),
|
|
214
240
|
count: { count: table.rowCount },
|
|
215
|
-
curl: `curl '${serverUrl}${path}?_limit=10&_sort=_block_height&_order=desc'`
|
|
241
|
+
curl: publicRead ? `curl '${serverUrl}${path}?_limit=10&_order=desc'` : `curl '${serverUrl}${path}?_limit=10&_sort=_block_height&_order=desc'`
|
|
216
242
|
}
|
|
217
243
|
};
|
|
218
244
|
}
|
|
@@ -229,6 +255,7 @@ function generateSubgraphAgentSchema(detail, options = {}) {
|
|
|
229
255
|
}
|
|
230
256
|
function generateSubgraphOpenApi(detail, options = {}) {
|
|
231
257
|
const serverUrl = normalizeServerUrl(options.serverUrl);
|
|
258
|
+
const publicRead = isPublicRead(detail);
|
|
232
259
|
const paths = {};
|
|
233
260
|
const schemas = {};
|
|
234
261
|
for (const [tableName, table] of Object.entries(detail.tables)) {
|
|
@@ -246,44 +273,62 @@ function generateSubgraphOpenApi(detail, options = {}) {
|
|
|
246
273
|
required,
|
|
247
274
|
example: rowExample(table)
|
|
248
275
|
};
|
|
249
|
-
|
|
276
|
+
const responseSchema = publicRead ? {
|
|
277
|
+
type: "object",
|
|
278
|
+
properties: {
|
|
279
|
+
rows: {
|
|
280
|
+
type: "array",
|
|
281
|
+
items: { $ref: `#/components/schemas/${schemaName}` }
|
|
282
|
+
},
|
|
283
|
+
next_cursor: { type: ["string", "null"] },
|
|
284
|
+
tip: {
|
|
285
|
+
type: "object",
|
|
286
|
+
properties: {
|
|
287
|
+
block_height: { type: "integer" },
|
|
288
|
+
subgraph_height: { type: "integer" },
|
|
289
|
+
blocks_behind: { type: "integer" }
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
} : {
|
|
294
|
+
type: "object",
|
|
295
|
+
properties: {
|
|
296
|
+
data: {
|
|
297
|
+
type: "array",
|
|
298
|
+
items: { $ref: `#/components/schemas/${schemaName}` }
|
|
299
|
+
},
|
|
300
|
+
meta: {
|
|
301
|
+
type: "object",
|
|
302
|
+
properties: {
|
|
303
|
+
total: { type: "integer" },
|
|
304
|
+
limit: { type: "integer" },
|
|
305
|
+
offset: { type: "integer" }
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
};
|
|
310
|
+
paths[tablePath(detail.name, tableName, publicRead)] = {
|
|
250
311
|
get: {
|
|
251
312
|
summary: `Query ${detail.name}.${tableName}`,
|
|
252
313
|
operationId: `query_${detail.name.replace(/-/g, "_")}_${tableName}`,
|
|
253
|
-
parameters: tableParameters(table),
|
|
314
|
+
parameters: tableParameters(table, publicRead),
|
|
254
315
|
responses: {
|
|
255
316
|
"200": {
|
|
256
317
|
description: "Rows returned from the subgraph table.",
|
|
257
318
|
content: {
|
|
258
319
|
"application/json": {
|
|
259
|
-
schema:
|
|
260
|
-
type: "object",
|
|
261
|
-
properties: {
|
|
262
|
-
data: {
|
|
263
|
-
type: "array",
|
|
264
|
-
items: { $ref: `#/components/schemas/${schemaName}` }
|
|
265
|
-
},
|
|
266
|
-
meta: {
|
|
267
|
-
type: "object",
|
|
268
|
-
properties: {
|
|
269
|
-
total: { type: "integer" },
|
|
270
|
-
limit: { type: "integer" },
|
|
271
|
-
offset: { type: "integer" }
|
|
272
|
-
}
|
|
273
|
-
}
|
|
274
|
-
}
|
|
275
|
-
}
|
|
320
|
+
schema: responseSchema
|
|
276
321
|
}
|
|
277
322
|
}
|
|
278
323
|
}
|
|
279
324
|
}
|
|
280
325
|
}
|
|
281
326
|
};
|
|
282
|
-
paths[countPath(detail.name, tableName)] = {
|
|
327
|
+
paths[countPath(detail.name, tableName, publicRead)] = {
|
|
283
328
|
get: {
|
|
284
329
|
summary: `Count ${detail.name}.${tableName}`,
|
|
285
330
|
operationId: `count_${detail.name.replace(/-/g, "_")}_${tableName}`,
|
|
286
|
-
parameters: tableParameters(table),
|
|
331
|
+
parameters: tableParameters(table, publicRead),
|
|
287
332
|
responses: {
|
|
288
333
|
"200": {
|
|
289
334
|
description: "Row count for the filtered table query.",
|
|
@@ -322,17 +367,19 @@ function generateSubgraphOpenApi(detail, options = {}) {
|
|
|
322
367
|
}
|
|
323
368
|
function generateSubgraphMarkdown(detail, options = {}) {
|
|
324
369
|
const agent = generateSubgraphAgentSchema(detail, options);
|
|
370
|
+
const publicRead = isPublicRead(detail);
|
|
325
371
|
const lines = [
|
|
326
372
|
`# ${detail.name} Subgraph API`,
|
|
327
373
|
"",
|
|
328
374
|
`Version: ${detail.version}`,
|
|
329
375
|
detail.schemaHash ? `Schema hash: ${detail.schemaHash}` : undefined,
|
|
330
376
|
`Server: ${agent.serverUrl}`,
|
|
377
|
+
publicRead ? "Visibility: public — anon reads, no API key. Responses use the `{ rows, next_cursor, tip }` envelope; paginate with `?cursor=<next_cursor>` and `_order=asc|desc` (`_offset`/`_sort` are rejected)." : undefined,
|
|
331
378
|
"",
|
|
332
379
|
detail.description
|
|
333
380
|
].filter((line) => line !== undefined && line !== "");
|
|
334
381
|
for (const [tableName, table] of Object.entries(agent.tables)) {
|
|
335
|
-
lines.push("", `## ${tableName}`, "", `GET ${table.endpoint}`, `GET ${table.countEndpoint}`, "", `Rows: ${table.rowCount}`, "", "### Columns", "", "| Column | Type | Attributes |", "| --- | --- | --- |");
|
|
382
|
+
lines.push("", `## ${tableName}`, "", `GET ${table.endpoint}`, `GET ${table.countEndpoint}`, ...table.aggregateEndpoint ? [`GET ${table.aggregateEndpoint}`] : [], ...table.streamEndpoint ? [`GET ${table.streamEndpoint} (SSE)`] : [], "", `Rows: ${table.rowCount}`, "", "### Columns", "", "| Column | Type | Attributes |", "| --- | --- | --- |");
|
|
336
383
|
for (const [columnName, col] of Object.entries(table.columns)) {
|
|
337
384
|
const attrs = [
|
|
338
385
|
SYSTEM_COLUMNS.includes(columnName) ? "system" : undefined,
|
|
@@ -362,5 +409,5 @@ export {
|
|
|
362
409
|
generateSubgraphAgentSchema
|
|
363
410
|
};
|
|
364
411
|
|
|
365
|
-
//# debugId=
|
|
412
|
+
//# debugId=948E6C1DD2B5F6FC64756E2164756E21
|
|
366
413
|
//# sourceMappingURL=spec.js.map
|
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../src/subgraphs/spec.ts"],
|
|
4
4
|
"sourcesContent": [
|
|
5
|
-
"import type { SubgraphDetail } from \"../schemas/subgraphs.ts\";\n\nexport type SubgraphSpecFormat = \"openapi\" | \"agent\" | \"markdown\";\n\nexport interface SubgraphSpecOptions {\n\tserverUrl?: string;\n\tgeneratedAt?: string;\n}\n\nexport interface SubgraphAgentSchema {\n\tname: string;\n\tversion: string;\n\tdescription?: string;\n\tschemaHash?: string;\n\tgeneratedAt: string;\n\tserverUrl: string;\n\tsources?: Record<string, unknown>;\n\ttables: Record<\n\t\tstring,\n\t\t{\n\t\t\tendpoint: string;\n\t\t\tcountEndpoint: string;\n\t\t\trowCount: number;\n\t\t\tcolumns: SubgraphDetail[\"tables\"][string][\"columns\"];\n\t\t\tindexes?: string[][];\n\t\t\tuniqueKeys?: string[][];\n\t\t\tquery: {\n\t\t\t\tparameters: string[];\n\t\t\t\tsortable: string[];\n\t\t\t\tselectable: string[];\n\t\t\t\tsearchable: string[];\n\t\t\t\tfilters: string[];\n\t\t\t};\n\t\t\texamples: {\n\t\t\t\tlist: Record<string, unknown>;\n\t\t\t\tcount: { count: number };\n\t\t\t\tcurl: string;\n\t\t\t};\n\t\t}\n\t>;\n}\n\ntype ColumnMeta = SubgraphDetail[\"tables\"][string][\"columns\"][string];\n\nconst SYSTEM_COLUMNS = [\"_id\", \"_block_height\", \"_tx_id\", \"_created_at\"];\nconst BASE_QUERY_PARAMS = [\"_limit\", \"_offset\", \"_sort\", \"_order\", \"_fields\"];\nconst COMPARISON_OPS = [\"neq\", \"gt\", \"gte\", \"lt\", \"lte\"];\n\nfunction generatedAt(options: SubgraphSpecOptions): string {\n\treturn options.generatedAt ?? new Date().toISOString();\n}\n\nfunction normalizeServerUrl(serverUrl?: string): string {\n\treturn (serverUrl ?? \"https://api.secondlayer.tools\").replace(/\\/+$/, \"\");\n}\n\nfunction tablePath(subgraphName: string, tableName: string): string {\n\treturn `/api/subgraphs/${subgraphName}/${tableName}`;\n}\n\nfunction countPath(subgraphName: string, tableName: string): string {\n\treturn `${tablePath(subgraphName, tableName)}/count`;\n}\n\nfunction isTextLike(type: string): boolean {\n\treturn type === \"text\" || type === \"principal\" || type === \"timestamp\";\n}\n\nfunction isComparable(type: string): boolean {\n\treturn (\n\t\ttype === \"uint\" ||\n\t\ttype === \"int\" ||\n\t\ttype === \"bigint\" ||\n\t\ttype === \"serial\" ||\n\t\ttype === \"timestamp\"\n\t);\n}\n\nfunction exampleForColumn(type: string): unknown {\n\tswitch (type) {\n\t\tcase \"uint\":\n\t\tcase \"int\":\n\t\tcase \"bigint\":\n\t\t\treturn \"1000\";\n\t\tcase \"serial\":\n\t\t\treturn 1;\n\t\tcase \"principal\":\n\t\t\treturn \"SP000000000000000000002Q6VF78\";\n\t\tcase \"timestamp\":\n\t\t\treturn \"2026-01-01T00:00:00.000Z\";\n\t\tcase \"boolean\":\n\t\t\treturn true;\n\t\tcase \"jsonb\":\n\t\t\treturn { example: true };\n\t\tdefault:\n\t\t\treturn \"example\";\n\t}\n}\n\nfunction openApiSchemaForColumn(col: ColumnMeta): Record<string, unknown> {\n\tlet schema: Record<string, unknown>;\n\tswitch (col.type) {\n\t\tcase \"uint\":\n\t\tcase \"int\":\n\t\tcase \"bigint\":\n\t\t\tschema = { type: \"string\", pattern: \"^-?\\\\d+(\\\\.\\\\d+)?$\" };\n\t\t\tbreak;\n\t\tcase \"serial\":\n\t\t\tschema = { type: \"integer\" };\n\t\t\tbreak;\n\t\tcase \"principal\":\n\t\tcase \"text\":\n\t\t\tschema = { type: \"string\" };\n\t\t\tbreak;\n\t\tcase \"timestamp\":\n\t\t\tschema = { type: \"string\", format: \"date-time\" };\n\t\t\tbreak;\n\t\tcase \"boolean\":\n\t\t\tschema = { type: \"boolean\" };\n\t\t\tbreak;\n\t\tcase \"jsonb\":\n\t\t\tschema = { type: \"object\", additionalProperties: true };\n\t\t\tbreak;\n\t\tdefault:\n\t\t\tschema = {};\n\t\t\tbreak;\n\t}\n\tif (col.nullable) {\n\t\tconst type = schema.type;\n\t\tif (typeof type === \"string\") schema.type = [type, \"null\"];\n\t}\n\treturn schema;\n}\n\nfunction columnEntries(table: SubgraphDetail[\"tables\"][string]) {\n\treturn Object.entries(table.columns);\n}\n\nfunction selectableColumns(table: SubgraphDetail[\"tables\"][string]): string[] {\n\treturn columnEntries(table).map(([name]) => name);\n}\n\nfunction searchableColumns(table: SubgraphDetail[\"tables\"][string]): string[] {\n\treturn columnEntries(table)\n\t\t.filter(([, col]) => col.searchable)\n\t\t.map(([name]) => name);\n}\n\nfunction filterNames(table: SubgraphDetail[\"tables\"][string]): string[] {\n\tconst result: string[] = [];\n\tfor (const [name, col] of columnEntries(table)) {\n\t\tresult.push(name);\n\t\tresult.push(`${name}.neq`);\n\t\tif (isComparable(col.type)) {\n\t\t\tfor (const op of COMPARISON_OPS.filter((op) => op !== \"neq\")) {\n\t\t\t\tresult.push(`${name}.${op}`);\n\t\t\t}\n\t\t}\n\t\tif (isTextLike(col.type)) result.push(`${name}.like`);\n\t}\n\treturn result;\n}\n\nfunction queryParameters(table: SubgraphDetail[\"tables\"][string]): string[] {\n\tconst params = [...BASE_QUERY_PARAMS];\n\tif (searchableColumns(table).length > 0) params.push(\"_search\");\n\treturn params;\n}\n\nfunction rowExample(table: SubgraphDetail[\"tables\"][string]) {\n\tconst row: Record<string, unknown> = {};\n\tfor (const [name, col] of columnEntries(table)) {\n\t\trow[name] = exampleForColumn(col.type);\n\t}\n\treturn row;\n}\n\nfunction openApiParameter(\n\tname: string,\n\tdescription: string,\n\tschema: Record<string, unknown> = { type: \"string\" },\n) {\n\treturn {\n\t\tname,\n\t\tin: \"query\",\n\t\trequired: false,\n\t\tdescription,\n\t\tschema,\n\t};\n}\n\nfunction tableParameters(table: SubgraphDetail[\"tables\"][string]) {\n\tconst parameters = [\n\t\topenApiParameter(\"_limit\", \"Maximum rows to return.\", {\n\t\t\ttype: \"integer\",\n\t\t\tdefault: 50,\n\t\t\tminimum: 1,\n\t\t\tmaximum: 1000,\n\t\t}),\n\t\topenApiParameter(\"_offset\", \"Rows to skip for pagination.\", {\n\t\t\ttype: \"integer\",\n\t\t\tdefault: 0,\n\t\t\tminimum: 0,\n\t\t}),\n\t\topenApiParameter(\"_sort\", \"Column to sort by.\", {\n\t\t\ttype: \"string\",\n\t\t\tenum: selectableColumns(table),\n\t\t}),\n\t\topenApiParameter(\"_order\", \"Sort direction.\", {\n\t\t\ttype: \"string\",\n\t\t\tenum: [\"asc\", \"desc\"],\n\t\t\tdefault: \"asc\",\n\t\t}),\n\t\topenApiParameter(\"_fields\", \"Comma-separated columns to include.\", {\n\t\t\ttype: \"string\",\n\t\t}),\n\t];\n\tif (searchableColumns(table).length > 0) {\n\t\tparameters.push(\n\t\t\topenApiParameter(\"_search\", \"Search across searchable columns.\", {\n\t\t\t\ttype: \"string\",\n\t\t\t}),\n\t\t);\n\t}\n\tfor (const [name, col] of columnEntries(table)) {\n\t\tparameters.push(\n\t\t\topenApiParameter(name, `Filter ${name} by equality.`, {\n\t\t\t\ttype: \"string\",\n\t\t\t}),\n\t\t);\n\t\tparameters.push(\n\t\t\topenApiParameter(`${name}.neq`, `Filter ${name} by inequality.`, {\n\t\t\t\ttype: \"string\",\n\t\t\t}),\n\t\t);\n\t\tif (isComparable(col.type)) {\n\t\t\tfor (const op of [\"gt\", \"gte\", \"lt\", \"lte\"]) {\n\t\t\t\tparameters.push(\n\t\t\t\t\topenApiParameter(`${name}.${op}`, `Filter ${name} with ${op}.`, {\n\t\t\t\t\t\ttype: \"string\",\n\t\t\t\t\t}),\n\t\t\t\t);\n\t\t\t}\n\t\t}\n\t\tif (isTextLike(col.type)) {\n\t\t\tparameters.push(\n\t\t\t\topenApiParameter(\n\t\t\t\t\t`${name}.like`,\n\t\t\t\t\t`Case-insensitive contains filter for ${name}.`,\n\t\t\t\t\t{\n\t\t\t\t\t\ttype: \"string\",\n\t\t\t\t\t},\n\t\t\t\t),\n\t\t\t);\n\t\t}\n\t}\n\treturn parameters;\n}\n\nexport function generateSubgraphAgentSchema(\n\tdetail: SubgraphDetail,\n\toptions: SubgraphSpecOptions = {},\n): SubgraphAgentSchema {\n\tconst serverUrl = normalizeServerUrl(options.serverUrl);\n\tconst tables: SubgraphAgentSchema[\"tables\"] = {};\n\tfor (const [tableName, table] of Object.entries(detail.tables)) {\n\t\tconst path = tablePath(detail.name, tableName);\n\t\ttables[tableName] = {\n\t\t\tendpoint: `${serverUrl}${path}`,\n\t\t\tcountEndpoint: `${serverUrl}${countPath(detail.name, tableName)}`,\n\t\t\trowCount: table.rowCount,\n\t\t\tcolumns: table.columns,\n\t\t\t...(table.indexes ? { indexes: table.indexes } : {}),\n\t\t\t...(table.uniqueKeys ? { uniqueKeys: table.uniqueKeys } : {}),\n\t\t\tquery: {\n\t\t\t\tparameters: queryParameters(table),\n\t\t\t\tsortable: selectableColumns(table),\n\t\t\t\tselectable: selectableColumns(table),\n\t\t\t\tsearchable: searchableColumns(table),\n\t\t\t\tfilters: filterNames(table),\n\t\t\t},\n\t\t\texamples: {\n\t\t\t\tlist: rowExample(table),\n\t\t\t\tcount: { count: table.rowCount },\n\t\t\t\tcurl: `curl '${serverUrl}${path}?_limit=10&_sort=_block_height&_order=desc'`,\n\t\t\t},\n\t\t};\n\t}\n\treturn {\n\t\tname: detail.name,\n\t\tversion: detail.version,\n\t\t...(detail.description ? { description: detail.description } : {}),\n\t\t...(detail.schemaHash ? { schemaHash: detail.schemaHash } : {}),\n\t\tgeneratedAt: generatedAt(options),\n\t\tserverUrl,\n\t\t...(detail.sources ? { sources: detail.sources } : {}),\n\t\ttables,\n\t};\n}\n\nexport function generateSubgraphOpenApi(\n\tdetail: SubgraphDetail,\n\toptions: SubgraphSpecOptions = {},\n): Record<string, unknown> {\n\tconst serverUrl = normalizeServerUrl(options.serverUrl);\n\tconst paths: Record<string, unknown> = {};\n\tconst schemas: Record<string, unknown> = {};\n\n\tfor (const [tableName, table] of Object.entries(detail.tables)) {\n\t\tconst schemaName = `${tableName}Row`;\n\t\tconst properties: Record<string, unknown> = {};\n\t\tconst required: string[] = [];\n\t\tfor (const [columnName, column] of columnEntries(table)) {\n\t\t\tproperties[columnName] = openApiSchemaForColumn(column);\n\t\t\tif (!column.nullable) required.push(columnName);\n\t\t}\n\t\tschemas[schemaName] = {\n\t\t\ttype: \"object\",\n\t\t\tproperties,\n\t\t\trequired,\n\t\t\texample: rowExample(table),\n\t\t};\n\n\t\tpaths[tablePath(detail.name, tableName)] = {\n\t\t\tget: {\n\t\t\t\tsummary: `Query ${detail.name}.${tableName}`,\n\t\t\t\toperationId: `query_${detail.name.replace(/-/g, \"_\")}_${tableName}`,\n\t\t\t\tparameters: tableParameters(table),\n\t\t\t\tresponses: {\n\t\t\t\t\t\"200\": {\n\t\t\t\t\t\tdescription: \"Rows returned from the subgraph table.\",\n\t\t\t\t\t\tcontent: {\n\t\t\t\t\t\t\t\"application/json\": {\n\t\t\t\t\t\t\t\tschema: {\n\t\t\t\t\t\t\t\t\ttype: \"object\",\n\t\t\t\t\t\t\t\t\tproperties: {\n\t\t\t\t\t\t\t\t\t\tdata: {\n\t\t\t\t\t\t\t\t\t\t\ttype: \"array\",\n\t\t\t\t\t\t\t\t\t\t\titems: { $ref: `#/components/schemas/${schemaName}` },\n\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\tmeta: {\n\t\t\t\t\t\t\t\t\t\t\ttype: \"object\",\n\t\t\t\t\t\t\t\t\t\t\tproperties: {\n\t\t\t\t\t\t\t\t\t\t\t\ttotal: { type: \"integer\" },\n\t\t\t\t\t\t\t\t\t\t\t\tlimit: { type: \"integer\" },\n\t\t\t\t\t\t\t\t\t\t\t\toffset: { type: \"integer\" },\n\t\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t};\n\n\t\tpaths[countPath(detail.name, tableName)] = {\n\t\t\tget: {\n\t\t\t\tsummary: `Count ${detail.name}.${tableName}`,\n\t\t\t\toperationId: `count_${detail.name.replace(/-/g, \"_\")}_${tableName}`,\n\t\t\t\tparameters: tableParameters(table),\n\t\t\t\tresponses: {\n\t\t\t\t\t\"200\": {\n\t\t\t\t\t\tdescription: \"Row count for the filtered table query.\",\n\t\t\t\t\t\tcontent: {\n\t\t\t\t\t\t\t\"application/json\": {\n\t\t\t\t\t\t\t\tschema: {\n\t\t\t\t\t\t\t\t\ttype: \"object\",\n\t\t\t\t\t\t\t\t\tproperties: { count: { type: \"integer\" } },\n\t\t\t\t\t\t\t\t\trequired: [\"count\"],\n\t\t\t\t\t\t\t\t\texample: { count: table.rowCount },\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t};\n\t}\n\n\treturn {\n\t\topenapi: \"3.1.0\",\n\t\tinfo: {\n\t\t\ttitle: `${detail.name} Subgraph API`,\n\t\t\tversion: detail.version,\n\t\t\t...(detail.description ? { description: detail.description } : {}),\n\t\t},\n\t\tservers: [{ url: serverUrl }],\n\t\tpaths,\n\t\tcomponents: { schemas },\n\t\t\"x-secondlayer-subgraph\": detail.name,\n\t\t\"x-secondlayer-version\": detail.version,\n\t\t\"x-secondlayer-schema-hash\": detail.schemaHash,\n\t\t\"x-secondlayer-generated-at\": generatedAt(options),\n\t\t\"x-secondlayer-sources\": detail.sources ?? {},\n\t\t\"x-secondlayer-tables\": Object.keys(detail.tables),\n\t};\n}\n\nexport function generateSubgraphMarkdown(\n\tdetail: SubgraphDetail,\n\toptions: SubgraphSpecOptions = {},\n): string {\n\tconst agent = generateSubgraphAgentSchema(detail, options);\n\tconst lines = [\n\t\t`# ${detail.name} Subgraph API`,\n\t\t\"\",\n\t\t`Version: ${detail.version}`,\n\t\tdetail.schemaHash ? `Schema hash: ${detail.schemaHash}` : undefined,\n\t\t`Server: ${agent.serverUrl}`,\n\t\t\"\",\n\t\tdetail.description,\n\t].filter((line): line is string => line !== undefined && line !== \"\");\n\n\tfor (const [tableName, table] of Object.entries(agent.tables)) {\n\t\tlines.push(\n\t\t\t\"\",\n\t\t\t`## ${tableName}`,\n\t\t\t\"\",\n\t\t\t`GET ${table.endpoint}`,\n\t\t\t`GET ${table.countEndpoint}`,\n\t\t\t\"\",\n\t\t\t`Rows: ${table.rowCount}`,\n\t\t\t\"\",\n\t\t\t\"### Columns\",\n\t\t\t\"\",\n\t\t\t\"| Column | Type | Attributes |\",\n\t\t\t\"| --- | --- | --- |\",\n\t\t);\n\t\tfor (const [columnName, col] of Object.entries(table.columns)) {\n\t\t\tconst attrs = [\n\t\t\t\tSYSTEM_COLUMNS.includes(columnName) ? \"system\" : undefined,\n\t\t\t\tcol.nullable ? \"nullable\" : undefined,\n\t\t\t\tcol.indexed ? \"indexed\" : undefined,\n\t\t\t\tcol.searchable ? \"searchable\" : undefined,\n\t\t\t]\n\t\t\t\t.filter(Boolean)\n\t\t\t\t.join(\", \");\n\t\t\tlines.push(`| \\`${columnName}\\` | \\`${col.type}\\` | ${attrs || \"-\"} |`);\n\t\t}\n\t\tlines.push(\n\t\t\t\"\",\n\t\t\t\"### Query\",\n\t\t\t\"\",\n\t\t\t`Parameters: ${table.query.parameters.map((p) => `\\`${p}\\``).join(\", \")}`,\n\t\t\t`Filters: ${table.query.filters.map((p) => `\\`${p}\\``).join(\", \")}`,\n\t\t\t\"\",\n\t\t\t\"### Example\",\n\t\t\t\"\",\n\t\t\t\"```bash\",\n\t\t\ttable.examples.curl,\n\t\t\t\"```\",\n\t\t);\n\t}\n\treturn `${lines.join(\"\\n\")}\\n`;\n}\n\nexport function generateSubgraphSpec(\n\tdetail: SubgraphDetail,\n\tformat: SubgraphSpecFormat,\n\toptions: SubgraphSpecOptions = {},\n): Record<string, unknown> | SubgraphAgentSchema | string {\n\tif (format === \"openapi\") return generateSubgraphOpenApi(detail, options);\n\tif (format === \"agent\") return generateSubgraphAgentSchema(detail, options);\n\treturn generateSubgraphMarkdown(detail, options);\n}\n"
|
|
5
|
+
"import type { SubgraphDetail } from \"../schemas/subgraphs.ts\";\n\nexport type SubgraphSpecFormat = \"openapi\" | \"agent\" | \"markdown\";\n\nexport interface SubgraphSpecOptions {\n\tserverUrl?: string;\n\tgeneratedAt?: string;\n}\n\nexport interface SubgraphAgentSchema {\n\tname: string;\n\tversion: string;\n\tdescription?: string;\n\tschemaHash?: string;\n\tgeneratedAt: string;\n\tserverUrl: string;\n\tsources?: Record<string, unknown>;\n\ttables: Record<\n\t\tstring,\n\t\t{\n\t\t\tendpoint: string;\n\t\t\tcountEndpoint: string;\n\t\t\taggregateEndpoint?: string;\n\t\t\tstreamEndpoint?: string;\n\t\t\trowCount: number;\n\t\t\tcolumns: SubgraphDetail[\"tables\"][string][\"columns\"];\n\t\t\tindexes?: string[][];\n\t\t\tuniqueKeys?: string[][];\n\t\t\tquery: {\n\t\t\t\tparameters: string[];\n\t\t\t\tsortable: string[];\n\t\t\t\tselectable: string[];\n\t\t\t\tsearchable: string[];\n\t\t\t\tfilters: string[];\n\t\t\t};\n\t\t\texamples: {\n\t\t\t\tlist: Record<string, unknown>;\n\t\t\t\tcount: { count: number };\n\t\t\t\tcurl: string;\n\t\t\t};\n\t\t}\n\t>;\n}\n\ntype ColumnMeta = SubgraphDetail[\"tables\"][string][\"columns\"][string];\n\nconst SYSTEM_COLUMNS = [\"_id\", \"_block_height\", \"_tx_id\", \"_created_at\"];\nconst BASE_QUERY_PARAMS = [\"_limit\", \"_offset\", \"_sort\", \"_order\", \"_fields\"];\n// /v1 rejects _offset/_sort: keyset pagination via cursor + _order only.\nconst PUBLIC_BASE_QUERY_PARAMS = [\"_limit\", \"cursor\", \"_order\", \"_fields\"];\nconst COMPARISON_OPS = [\"neq\", \"gt\", \"gte\", \"lt\", \"lte\"];\n\nfunction isPublicRead(detail: SubgraphDetail): boolean {\n\treturn detail.visibility === \"public\";\n}\n\nfunction generatedAt(options: SubgraphSpecOptions): string {\n\treturn options.generatedAt ?? new Date().toISOString();\n}\n\nfunction normalizeServerUrl(serverUrl?: string): string {\n\treturn (serverUrl ?? \"https://api.secondlayer.tools\").replace(/\\/+$/, \"\");\n}\n\nfunction tablePath(\n\tsubgraphName: string,\n\ttableName: string,\n\tpublicRead: boolean,\n): string {\n\tconst base = publicRead ? \"/v1/subgraphs\" : \"/api/subgraphs\";\n\treturn `${base}/${subgraphName}/${tableName}`;\n}\n\nfunction countPath(\n\tsubgraphName: string,\n\ttableName: string,\n\tpublicRead: boolean,\n): string {\n\treturn `${tablePath(subgraphName, tableName, publicRead)}/count`;\n}\n\nfunction isTextLike(type: string): boolean {\n\treturn type === \"text\" || type === \"principal\" || type === \"timestamp\";\n}\n\nfunction isComparable(type: string): boolean {\n\treturn (\n\t\ttype === \"uint\" ||\n\t\ttype === \"int\" ||\n\t\ttype === \"bigint\" ||\n\t\ttype === \"serial\" ||\n\t\ttype === \"timestamp\"\n\t);\n}\n\nfunction exampleForColumn(type: string): unknown {\n\tswitch (type) {\n\t\tcase \"uint\":\n\t\tcase \"int\":\n\t\tcase \"bigint\":\n\t\t\treturn \"1000\";\n\t\tcase \"serial\":\n\t\t\treturn 1;\n\t\tcase \"principal\":\n\t\t\treturn \"SP000000000000000000002Q6VF78\";\n\t\tcase \"timestamp\":\n\t\t\treturn \"2026-01-01T00:00:00.000Z\";\n\t\tcase \"boolean\":\n\t\t\treturn true;\n\t\tcase \"jsonb\":\n\t\t\treturn { example: true };\n\t\tdefault:\n\t\t\treturn \"example\";\n\t}\n}\n\nfunction openApiSchemaForColumn(col: ColumnMeta): Record<string, unknown> {\n\tlet schema: Record<string, unknown>;\n\tswitch (col.type) {\n\t\tcase \"uint\":\n\t\tcase \"int\":\n\t\tcase \"bigint\":\n\t\t\tschema = { type: \"string\", pattern: \"^-?\\\\d+(\\\\.\\\\d+)?$\" };\n\t\t\tbreak;\n\t\tcase \"serial\":\n\t\t\tschema = { type: \"integer\" };\n\t\t\tbreak;\n\t\tcase \"principal\":\n\t\tcase \"text\":\n\t\t\tschema = { type: \"string\" };\n\t\t\tbreak;\n\t\tcase \"timestamp\":\n\t\t\tschema = { type: \"string\", format: \"date-time\" };\n\t\t\tbreak;\n\t\tcase \"boolean\":\n\t\t\tschema = { type: \"boolean\" };\n\t\t\tbreak;\n\t\tcase \"jsonb\":\n\t\t\tschema = { type: \"object\", additionalProperties: true };\n\t\t\tbreak;\n\t\tdefault:\n\t\t\tschema = {};\n\t\t\tbreak;\n\t}\n\tif (col.nullable) {\n\t\tconst type = schema.type;\n\t\tif (typeof type === \"string\") schema.type = [type, \"null\"];\n\t}\n\treturn schema;\n}\n\nfunction columnEntries(table: SubgraphDetail[\"tables\"][string]) {\n\treturn Object.entries(table.columns);\n}\n\nfunction selectableColumns(table: SubgraphDetail[\"tables\"][string]): string[] {\n\treturn columnEntries(table).map(([name]) => name);\n}\n\nfunction searchableColumns(table: SubgraphDetail[\"tables\"][string]): string[] {\n\treturn columnEntries(table)\n\t\t.filter(([, col]) => col.searchable)\n\t\t.map(([name]) => name);\n}\n\nfunction filterNames(table: SubgraphDetail[\"tables\"][string]): string[] {\n\tconst result: string[] = [];\n\tfor (const [name, col] of columnEntries(table)) {\n\t\tresult.push(name);\n\t\tresult.push(`${name}.neq`);\n\t\tif (isComparable(col.type)) {\n\t\t\tfor (const op of COMPARISON_OPS.filter((op) => op !== \"neq\")) {\n\t\t\t\tresult.push(`${name}.${op}`);\n\t\t\t}\n\t\t}\n\t\tif (isTextLike(col.type)) result.push(`${name}.like`);\n\t}\n\treturn result;\n}\n\nfunction queryParameters(\n\ttable: SubgraphDetail[\"tables\"][string],\n\tpublicRead: boolean,\n): string[] {\n\tconst params = publicRead\n\t\t? [...PUBLIC_BASE_QUERY_PARAMS]\n\t\t: [...BASE_QUERY_PARAMS];\n\tif (searchableColumns(table).length > 0) params.push(\"_search\");\n\treturn params;\n}\n\nfunction rowExample(table: SubgraphDetail[\"tables\"][string]) {\n\tconst row: Record<string, unknown> = {};\n\tfor (const [name, col] of columnEntries(table)) {\n\t\trow[name] = exampleForColumn(col.type);\n\t}\n\treturn row;\n}\n\nfunction openApiParameter(\n\tname: string,\n\tdescription: string,\n\tschema: Record<string, unknown> = { type: \"string\" },\n) {\n\treturn {\n\t\tname,\n\t\tin: \"query\",\n\t\trequired: false,\n\t\tdescription,\n\t\tschema,\n\t};\n}\n\nfunction tableParameters(\n\ttable: SubgraphDetail[\"tables\"][string],\n\tpublicRead: boolean,\n) {\n\tconst parameters = publicRead\n\t\t? [\n\t\t\t\topenApiParameter(\"_limit\", \"Maximum rows to return.\", {\n\t\t\t\t\ttype: \"integer\",\n\t\t\t\t\tdefault: 50,\n\t\t\t\t\tminimum: 1,\n\t\t\t\t\tmaximum: 1000,\n\t\t\t\t}),\n\t\t\t\topenApiParameter(\n\t\t\t\t\t\"cursor\",\n\t\t\t\t\t\"Resume token from next_cursor (keyset pagination on _id).\",\n\t\t\t\t\t{ type: \"string\" },\n\t\t\t\t),\n\t\t\t\topenApiParameter(\"_order\", \"Sort direction (_id keyset).\", {\n\t\t\t\t\ttype: \"string\",\n\t\t\t\t\tenum: [\"asc\", \"desc\"],\n\t\t\t\t\tdefault: \"asc\",\n\t\t\t\t}),\n\t\t\t\topenApiParameter(\"_fields\", \"Comma-separated columns to include.\", {\n\t\t\t\t\ttype: \"string\",\n\t\t\t\t}),\n\t\t\t]\n\t\t: [\n\t\t\t\topenApiParameter(\"_limit\", \"Maximum rows to return.\", {\n\t\t\t\t\ttype: \"integer\",\n\t\t\t\t\tdefault: 50,\n\t\t\t\t\tminimum: 1,\n\t\t\t\t\tmaximum: 1000,\n\t\t\t\t}),\n\t\t\t\topenApiParameter(\"_offset\", \"Rows to skip for pagination.\", {\n\t\t\t\t\ttype: \"integer\",\n\t\t\t\t\tdefault: 0,\n\t\t\t\t\tminimum: 0,\n\t\t\t\t}),\n\t\t\t\topenApiParameter(\"_sort\", \"Column to sort by.\", {\n\t\t\t\t\ttype: \"string\",\n\t\t\t\t\tenum: selectableColumns(table),\n\t\t\t\t}),\n\t\t\t\topenApiParameter(\"_order\", \"Sort direction.\", {\n\t\t\t\t\ttype: \"string\",\n\t\t\t\t\tenum: [\"asc\", \"desc\"],\n\t\t\t\t\tdefault: \"asc\",\n\t\t\t\t}),\n\t\t\t\topenApiParameter(\"_fields\", \"Comma-separated columns to include.\", {\n\t\t\t\t\ttype: \"string\",\n\t\t\t\t}),\n\t\t\t];\n\tif (searchableColumns(table).length > 0) {\n\t\tparameters.push(\n\t\t\topenApiParameter(\"_search\", \"Search across searchable columns.\", {\n\t\t\t\ttype: \"string\",\n\t\t\t}),\n\t\t);\n\t}\n\tfor (const [name, col] of columnEntries(table)) {\n\t\tparameters.push(\n\t\t\topenApiParameter(name, `Filter ${name} by equality.`, {\n\t\t\t\ttype: \"string\",\n\t\t\t}),\n\t\t);\n\t\tparameters.push(\n\t\t\topenApiParameter(`${name}.neq`, `Filter ${name} by inequality.`, {\n\t\t\t\ttype: \"string\",\n\t\t\t}),\n\t\t);\n\t\tif (isComparable(col.type)) {\n\t\t\tfor (const op of [\"gt\", \"gte\", \"lt\", \"lte\"]) {\n\t\t\t\tparameters.push(\n\t\t\t\t\topenApiParameter(`${name}.${op}`, `Filter ${name} with ${op}.`, {\n\t\t\t\t\t\ttype: \"string\",\n\t\t\t\t\t}),\n\t\t\t\t);\n\t\t\t}\n\t\t}\n\t\tif (isTextLike(col.type)) {\n\t\t\tparameters.push(\n\t\t\t\topenApiParameter(\n\t\t\t\t\t`${name}.like`,\n\t\t\t\t\t`Case-insensitive contains filter for ${name}.`,\n\t\t\t\t\t{\n\t\t\t\t\t\ttype: \"string\",\n\t\t\t\t\t},\n\t\t\t\t),\n\t\t\t);\n\t\t}\n\t}\n\treturn parameters;\n}\n\nexport function generateSubgraphAgentSchema(\n\tdetail: SubgraphDetail,\n\toptions: SubgraphSpecOptions = {},\n): SubgraphAgentSchema {\n\tconst serverUrl = normalizeServerUrl(options.serverUrl);\n\tconst publicRead = isPublicRead(detail);\n\tconst tables: SubgraphAgentSchema[\"tables\"] = {};\n\tfor (const [tableName, table] of Object.entries(detail.tables)) {\n\t\tconst path = tablePath(detail.name, tableName, publicRead);\n\t\ttables[tableName] = {\n\t\t\tendpoint: `${serverUrl}${path}`,\n\t\t\tcountEndpoint: `${serverUrl}${countPath(detail.name, tableName, publicRead)}`,\n\t\t\t...(publicRead\n\t\t\t\t? {\n\t\t\t\t\t\taggregateEndpoint: `${serverUrl}${path}/aggregate`,\n\t\t\t\t\t\tstreamEndpoint: `${serverUrl}${path}/stream`,\n\t\t\t\t\t}\n\t\t\t\t: {}),\n\t\t\trowCount: table.rowCount,\n\t\t\tcolumns: table.columns,\n\t\t\t...(table.indexes ? { indexes: table.indexes } : {}),\n\t\t\t...(table.uniqueKeys ? { uniqueKeys: table.uniqueKeys } : {}),\n\t\t\tquery: {\n\t\t\t\tparameters: queryParameters(table, publicRead),\n\t\t\t\t// /v1 is _id keyset only — no _sort.\n\t\t\t\tsortable: publicRead ? [] : selectableColumns(table),\n\t\t\t\tselectable: selectableColumns(table),\n\t\t\t\tsearchable: searchableColumns(table),\n\t\t\t\tfilters: filterNames(table),\n\t\t\t},\n\t\t\texamples: {\n\t\t\t\tlist: rowExample(table),\n\t\t\t\tcount: { count: table.rowCount },\n\t\t\t\tcurl: publicRead\n\t\t\t\t\t? `curl '${serverUrl}${path}?_limit=10&_order=desc'`\n\t\t\t\t\t: `curl '${serverUrl}${path}?_limit=10&_sort=_block_height&_order=desc'`,\n\t\t\t},\n\t\t};\n\t}\n\treturn {\n\t\tname: detail.name,\n\t\tversion: detail.version,\n\t\t...(detail.description ? { description: detail.description } : {}),\n\t\t...(detail.schemaHash ? { schemaHash: detail.schemaHash } : {}),\n\t\tgeneratedAt: generatedAt(options),\n\t\tserverUrl,\n\t\t...(detail.sources ? { sources: detail.sources } : {}),\n\t\ttables,\n\t};\n}\n\nexport function generateSubgraphOpenApi(\n\tdetail: SubgraphDetail,\n\toptions: SubgraphSpecOptions = {},\n): Record<string, unknown> {\n\tconst serverUrl = normalizeServerUrl(options.serverUrl);\n\tconst publicRead = isPublicRead(detail);\n\tconst paths: Record<string, unknown> = {};\n\tconst schemas: Record<string, unknown> = {};\n\n\tfor (const [tableName, table] of Object.entries(detail.tables)) {\n\t\tconst schemaName = `${tableName}Row`;\n\t\tconst properties: Record<string, unknown> = {};\n\t\tconst required: string[] = [];\n\t\tfor (const [columnName, column] of columnEntries(table)) {\n\t\t\tproperties[columnName] = openApiSchemaForColumn(column);\n\t\t\tif (!column.nullable) required.push(columnName);\n\t\t}\n\t\tschemas[schemaName] = {\n\t\t\ttype: \"object\",\n\t\t\tproperties,\n\t\t\trequired,\n\t\t\texample: rowExample(table),\n\t\t};\n\n\t\tconst responseSchema = publicRead\n\t\t\t? {\n\t\t\t\t\ttype: \"object\",\n\t\t\t\t\tproperties: {\n\t\t\t\t\t\trows: {\n\t\t\t\t\t\t\ttype: \"array\",\n\t\t\t\t\t\t\titems: { $ref: `#/components/schemas/${schemaName}` },\n\t\t\t\t\t\t},\n\t\t\t\t\t\tnext_cursor: { type: [\"string\", \"null\"] },\n\t\t\t\t\t\ttip: {\n\t\t\t\t\t\t\ttype: \"object\",\n\t\t\t\t\t\t\tproperties: {\n\t\t\t\t\t\t\t\tblock_height: { type: \"integer\" },\n\t\t\t\t\t\t\t\tsubgraph_height: { type: \"integer\" },\n\t\t\t\t\t\t\t\tblocks_behind: { type: \"integer\" },\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t}\n\t\t\t: {\n\t\t\t\t\ttype: \"object\",\n\t\t\t\t\tproperties: {\n\t\t\t\t\t\tdata: {\n\t\t\t\t\t\t\ttype: \"array\",\n\t\t\t\t\t\t\titems: { $ref: `#/components/schemas/${schemaName}` },\n\t\t\t\t\t\t},\n\t\t\t\t\t\tmeta: {\n\t\t\t\t\t\t\ttype: \"object\",\n\t\t\t\t\t\t\tproperties: {\n\t\t\t\t\t\t\t\ttotal: { type: \"integer\" },\n\t\t\t\t\t\t\t\tlimit: { type: \"integer\" },\n\t\t\t\t\t\t\t\toffset: { type: \"integer\" },\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t};\n\n\t\tpaths[tablePath(detail.name, tableName, publicRead)] = {\n\t\t\tget: {\n\t\t\t\tsummary: `Query ${detail.name}.${tableName}`,\n\t\t\t\toperationId: `query_${detail.name.replace(/-/g, \"_\")}_${tableName}`,\n\t\t\t\tparameters: tableParameters(table, publicRead),\n\t\t\t\tresponses: {\n\t\t\t\t\t\"200\": {\n\t\t\t\t\t\tdescription: \"Rows returned from the subgraph table.\",\n\t\t\t\t\t\tcontent: {\n\t\t\t\t\t\t\t\"application/json\": {\n\t\t\t\t\t\t\t\tschema: responseSchema,\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t};\n\n\t\tpaths[countPath(detail.name, tableName, publicRead)] = {\n\t\t\tget: {\n\t\t\t\tsummary: `Count ${detail.name}.${tableName}`,\n\t\t\t\toperationId: `count_${detail.name.replace(/-/g, \"_\")}_${tableName}`,\n\t\t\t\tparameters: tableParameters(table, publicRead),\n\t\t\t\tresponses: {\n\t\t\t\t\t\"200\": {\n\t\t\t\t\t\tdescription: \"Row count for the filtered table query.\",\n\t\t\t\t\t\tcontent: {\n\t\t\t\t\t\t\t\"application/json\": {\n\t\t\t\t\t\t\t\tschema: {\n\t\t\t\t\t\t\t\t\ttype: \"object\",\n\t\t\t\t\t\t\t\t\tproperties: { count: { type: \"integer\" } },\n\t\t\t\t\t\t\t\t\trequired: [\"count\"],\n\t\t\t\t\t\t\t\t\texample: { count: table.rowCount },\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t};\n\t}\n\n\treturn {\n\t\topenapi: \"3.1.0\",\n\t\tinfo: {\n\t\t\ttitle: `${detail.name} Subgraph API`,\n\t\t\tversion: detail.version,\n\t\t\t...(detail.description ? { description: detail.description } : {}),\n\t\t},\n\t\tservers: [{ url: serverUrl }],\n\t\tpaths,\n\t\tcomponents: { schemas },\n\t\t\"x-secondlayer-subgraph\": detail.name,\n\t\t\"x-secondlayer-version\": detail.version,\n\t\t\"x-secondlayer-schema-hash\": detail.schemaHash,\n\t\t\"x-secondlayer-generated-at\": generatedAt(options),\n\t\t\"x-secondlayer-sources\": detail.sources ?? {},\n\t\t\"x-secondlayer-tables\": Object.keys(detail.tables),\n\t};\n}\n\nexport function generateSubgraphMarkdown(\n\tdetail: SubgraphDetail,\n\toptions: SubgraphSpecOptions = {},\n): string {\n\tconst agent = generateSubgraphAgentSchema(detail, options);\n\tconst publicRead = isPublicRead(detail);\n\tconst lines = [\n\t\t`# ${detail.name} Subgraph API`,\n\t\t\"\",\n\t\t`Version: ${detail.version}`,\n\t\tdetail.schemaHash ? `Schema hash: ${detail.schemaHash}` : undefined,\n\t\t`Server: ${agent.serverUrl}`,\n\t\tpublicRead\n\t\t\t? \"Visibility: public — anon reads, no API key. Responses use the `{ rows, next_cursor, tip }` envelope; paginate with `?cursor=<next_cursor>` and `_order=asc|desc` (`_offset`/`_sort` are rejected).\"\n\t\t\t: undefined,\n\t\t\"\",\n\t\tdetail.description,\n\t].filter((line): line is string => line !== undefined && line !== \"\");\n\n\tfor (const [tableName, table] of Object.entries(agent.tables)) {\n\t\tlines.push(\n\t\t\t\"\",\n\t\t\t`## ${tableName}`,\n\t\t\t\"\",\n\t\t\t`GET ${table.endpoint}`,\n\t\t\t`GET ${table.countEndpoint}`,\n\t\t\t...(table.aggregateEndpoint ? [`GET ${table.aggregateEndpoint}`] : []),\n\t\t\t...(table.streamEndpoint ? [`GET ${table.streamEndpoint} (SSE)`] : []),\n\t\t\t\"\",\n\t\t\t`Rows: ${table.rowCount}`,\n\t\t\t\"\",\n\t\t\t\"### Columns\",\n\t\t\t\"\",\n\t\t\t\"| Column | Type | Attributes |\",\n\t\t\t\"| --- | --- | --- |\",\n\t\t);\n\t\tfor (const [columnName, col] of Object.entries(table.columns)) {\n\t\t\tconst attrs = [\n\t\t\t\tSYSTEM_COLUMNS.includes(columnName) ? \"system\" : undefined,\n\t\t\t\tcol.nullable ? \"nullable\" : undefined,\n\t\t\t\tcol.indexed ? \"indexed\" : undefined,\n\t\t\t\tcol.searchable ? \"searchable\" : undefined,\n\t\t\t]\n\t\t\t\t.filter(Boolean)\n\t\t\t\t.join(\", \");\n\t\t\tlines.push(`| \\`${columnName}\\` | \\`${col.type}\\` | ${attrs || \"-\"} |`);\n\t\t}\n\t\tlines.push(\n\t\t\t\"\",\n\t\t\t\"### Query\",\n\t\t\t\"\",\n\t\t\t`Parameters: ${table.query.parameters.map((p) => `\\`${p}\\``).join(\", \")}`,\n\t\t\t`Filters: ${table.query.filters.map((p) => `\\`${p}\\``).join(\", \")}`,\n\t\t\t\"\",\n\t\t\t\"### Example\",\n\t\t\t\"\",\n\t\t\t\"```bash\",\n\t\t\ttable.examples.curl,\n\t\t\t\"```\",\n\t\t);\n\t}\n\treturn `${lines.join(\"\\n\")}\\n`;\n}\n\nexport function generateSubgraphSpec(\n\tdetail: SubgraphDetail,\n\tformat: SubgraphSpecFormat,\n\toptions: SubgraphSpecOptions = {},\n): Record<string, unknown> | SubgraphAgentSchema | string {\n\tif (format === \"openapi\") return generateSubgraphOpenApi(detail, options);\n\tif (format === \"agent\") return generateSubgraphAgentSchema(detail, options);\n\treturn generateSubgraphMarkdown(detail, options);\n}\n"
|
|
6
6
|
],
|
|
7
|
-
"mappings": ";;;;;;;;;;;;;;;;;
|
|
8
|
-
"debugId": "
|
|
7
|
+
"mappings": "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",
|
|
8
|
+
"debugId": "948E6C1DD2B5F6FC64756E2164756E21",
|
|
9
9
|
"names": []
|
|
10
10
|
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* x402 payment-rail shared constants: the v1 token set + CAIP-2 network ids.
|
|
3
|
+
*
|
|
4
|
+
* Single-sourced here (not in the API package) so the SDK/MCP client and the
|
|
5
|
+
* API facilitator agree on asset ids, decimals, and the `accepts[]` asset-string
|
|
6
|
+
* format. All ids are mainnet and confirmed on-chain (2026-06). Per-call prices
|
|
7
|
+
* are NOT here — they are API-local (see `packages/api/src/x402/catalog.ts`) and
|
|
8
|
+
* exposed to clients at runtime via `GET /x402/supported`.
|
|
9
|
+
*/
|
|
10
|
+
/** CAIP-2 network ids used in x402 v2 `accepts[]` entries. */
|
|
11
|
+
declare const X402_NETWORK: {
|
|
12
|
+
readonly mainnet: "stacks:1"
|
|
13
|
+
readonly testnet: "stacks:2147483648"
|
|
14
|
+
};
|
|
15
|
+
type X402Network = (typeof X402_NETWORK)[keyof typeof X402_NETWORK];
|
|
16
|
+
type X402TokenSymbol = "STX" | "sBTC" | "USDCx";
|
|
17
|
+
type X402Token = {
|
|
18
|
+
symbol: X402TokenSymbol
|
|
19
|
+
/** x402 `asset` string: `"STX"` for native, else the `<addr>.<contract>` id. */
|
|
20
|
+
asset: string
|
|
21
|
+
/** SIP-010 contract id (`<addr>.<contract>`); `null` for native STX. */
|
|
22
|
+
contractId: string | null
|
|
23
|
+
/** SIP-010 fungible-token asset name (the `::<name>` suffix); `null` for STX. */
|
|
24
|
+
assetName: string | null
|
|
25
|
+
/** Fully-qualified SIP-010 asset identifier (`<id>::<name>`); `null` for STX. */
|
|
26
|
+
assetIdentifier: string | null
|
|
27
|
+
decimals: number
|
|
28
|
+
};
|
|
29
|
+
/** v1 token set. sBTC + USDCx ids verified against the deployed mainnet contracts. */
|
|
30
|
+
declare const X402_TOKENS: Record<X402TokenSymbol, X402Token>;
|
|
31
|
+
declare const X402_TOKEN_SYMBOLS: X402TokenSymbol[];
|
|
32
|
+
declare function getX402Token(symbol: X402TokenSymbol): X402Token;
|
|
33
|
+
/**\\n* Optimistic-serve reputation: the Redis key + TTL for a payer principal's\\n* "strike" counter (reverted/dropped payments). Single-sourced so the API gate\\n* (reader) and the worker reconciler (writer, on revert) agree without a\\n* cross-package import.\\n*/
|
|
34
|
+
declare const X402_STRIKE_TTL_SECONDS: number;
|
|
35
|
+
declare function x402StrikeKey(principal: string): string;
|
|
36
|
+
/** Resolve a token by its x402 `asset` string (the value carried in `accepts[].asset`). */
|
|
37
|
+
declare function findX402TokenByAsset(asset: string): X402Token | undefined;
|
|
38
|
+
export { x402StrikeKey, getX402Token, findX402TokenByAsset, X402_TOKEN_SYMBOLS, X402_TOKENS, X402_STRIKE_TTL_SECONDS, X402_NETWORK, X402TokenSymbol, X402Token, X402Network };
|
package/dist/src/x402.js
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { createRequire } from "node:module";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __returnValue = (v) => v;
|
|
4
|
+
function __exportSetter(name, newValue) {
|
|
5
|
+
this[name] = __returnValue.bind(null, newValue);
|
|
6
|
+
}
|
|
7
|
+
var __export = (target, all) => {
|
|
8
|
+
for (var name in all)
|
|
9
|
+
__defProp(target, name, {
|
|
10
|
+
get: all[name],
|
|
11
|
+
enumerable: true,
|
|
12
|
+
configurable: true,
|
|
13
|
+
set: __exportSetter.bind(all, name)
|
|
14
|
+
});
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
// src/x402.ts
|
|
18
|
+
var X402_NETWORK = {
|
|
19
|
+
mainnet: "stacks:1",
|
|
20
|
+
testnet: "stacks:2147483648"
|
|
21
|
+
};
|
|
22
|
+
var X402_TOKENS = {
|
|
23
|
+
STX: {
|
|
24
|
+
symbol: "STX",
|
|
25
|
+
asset: "STX",
|
|
26
|
+
contractId: null,
|
|
27
|
+
assetName: null,
|
|
28
|
+
assetIdentifier: null,
|
|
29
|
+
decimals: 6
|
|
30
|
+
},
|
|
31
|
+
sBTC: {
|
|
32
|
+
symbol: "sBTC",
|
|
33
|
+
asset: "SM3VDXK3WZZSA84XXFKAFAF15NNZX32CTSG82JFQ4.sbtc-token",
|
|
34
|
+
contractId: "SM3VDXK3WZZSA84XXFKAFAF15NNZX32CTSG82JFQ4.sbtc-token",
|
|
35
|
+
assetName: "sbtc-token",
|
|
36
|
+
assetIdentifier: "SM3VDXK3WZZSA84XXFKAFAF15NNZX32CTSG82JFQ4.sbtc-token::sbtc-token",
|
|
37
|
+
decimals: 8
|
|
38
|
+
},
|
|
39
|
+
USDCx: {
|
|
40
|
+
symbol: "USDCx",
|
|
41
|
+
asset: "SP120SBRBQJ00MCWS7TM5R8WJNTTKD5K0HFRC2CNE.usdcx",
|
|
42
|
+
contractId: "SP120SBRBQJ00MCWS7TM5R8WJNTTKD5K0HFRC2CNE.usdcx",
|
|
43
|
+
assetName: "usdcx-token",
|
|
44
|
+
assetIdentifier: "SP120SBRBQJ00MCWS7TM5R8WJNTTKD5K0HFRC2CNE.usdcx::usdcx-token",
|
|
45
|
+
decimals: 6
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
var X402_TOKEN_SYMBOLS = Object.keys(X402_TOKENS);
|
|
49
|
+
function getX402Token(symbol) {
|
|
50
|
+
return X402_TOKENS[symbol];
|
|
51
|
+
}
|
|
52
|
+
var X402_STRIKE_TTL_SECONDS = 24 * 60 * 60;
|
|
53
|
+
function x402StrikeKey(principal) {
|
|
54
|
+
return `x402:strikes:${principal}`;
|
|
55
|
+
}
|
|
56
|
+
function findX402TokenByAsset(asset) {
|
|
57
|
+
for (const symbol of X402_TOKEN_SYMBOLS) {
|
|
58
|
+
if (X402_TOKENS[symbol].asset === asset)
|
|
59
|
+
return X402_TOKENS[symbol];
|
|
60
|
+
}
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
export {
|
|
64
|
+
x402StrikeKey,
|
|
65
|
+
getX402Token,
|
|
66
|
+
findX402TokenByAsset,
|
|
67
|
+
X402_TOKEN_SYMBOLS,
|
|
68
|
+
X402_TOKENS,
|
|
69
|
+
X402_STRIKE_TTL_SECONDS,
|
|
70
|
+
X402_NETWORK
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
//# debugId=7AA9D65F1DB692C864756E2164756E21
|
|
74
|
+
//# sourceMappingURL=x402.js.map
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../src/x402.ts"],
|
|
4
|
+
"sourcesContent": [
|
|
5
|
+
"/**\n * x402 payment-rail shared constants: the v1 token set + CAIP-2 network ids.\n *\n * Single-sourced here (not in the API package) so the SDK/MCP client and the\n * API facilitator agree on asset ids, decimals, and the `accepts[]` asset-string\n * format. All ids are mainnet and confirmed on-chain (2026-06). Per-call prices\n * are NOT here — they are API-local (see `packages/api/src/x402/catalog.ts`) and\n * exposed to clients at runtime via `GET /x402/supported`.\n */\n\n/** CAIP-2 network ids used in x402 v2 `accepts[]` entries. */\nexport const X402_NETWORK = {\n\tmainnet: \"stacks:1\",\n\ttestnet: \"stacks:2147483648\",\n} as const;\n\nexport type X402Network = (typeof X402_NETWORK)[keyof typeof X402_NETWORK];\n\nexport type X402TokenSymbol = \"STX\" | \"sBTC\" | \"USDCx\";\n\nexport type X402Token = {\n\tsymbol: X402TokenSymbol;\n\t/** x402 `asset` string: `\"STX\"` for native, else the `<addr>.<contract>` id. */\n\tasset: string;\n\t/** SIP-010 contract id (`<addr>.<contract>`); `null` for native STX. */\n\tcontractId: string | null;\n\t/** SIP-010 fungible-token asset name (the `::<name>` suffix); `null` for STX. */\n\tassetName: string | null;\n\t/** Fully-qualified SIP-010 asset identifier (`<id>::<name>`); `null` for STX. */\n\tassetIdentifier: string | null;\n\tdecimals: number;\n};\n\n/** v1 token set. sBTC + USDCx ids verified against the deployed mainnet contracts. */\nexport const X402_TOKENS: Record<X402TokenSymbol, X402Token> = {\n\tSTX: {\n\t\tsymbol: \"STX\",\n\t\tasset: \"STX\",\n\t\tcontractId: null,\n\t\tassetName: null,\n\t\tassetIdentifier: null,\n\t\tdecimals: 6,\n\t},\n\tsBTC: {\n\t\tsymbol: \"sBTC\",\n\t\tasset: \"SM3VDXK3WZZSA84XXFKAFAF15NNZX32CTSG82JFQ4.sbtc-token\",\n\t\tcontractId: \"SM3VDXK3WZZSA84XXFKAFAF15NNZX32CTSG82JFQ4.sbtc-token\",\n\t\tassetName: \"sbtc-token\",\n\t\tassetIdentifier:\n\t\t\t\"SM3VDXK3WZZSA84XXFKAFAF15NNZX32CTSG82JFQ4.sbtc-token::sbtc-token\",\n\t\tdecimals: 8,\n\t},\n\tUSDCx: {\n\t\tsymbol: \"USDCx\",\n\t\tasset: \"SP120SBRBQJ00MCWS7TM5R8WJNTTKD5K0HFRC2CNE.usdcx\",\n\t\tcontractId: \"SP120SBRBQJ00MCWS7TM5R8WJNTTKD5K0HFRC2CNE.usdcx\",\n\t\tassetName: \"usdcx-token\",\n\t\tassetIdentifier:\n\t\t\t\"SP120SBRBQJ00MCWS7TM5R8WJNTTKD5K0HFRC2CNE.usdcx::usdcx-token\",\n\t\tdecimals: 6,\n\t},\n} as const;\n\nexport const X402_TOKEN_SYMBOLS = Object.keys(X402_TOKENS) as X402TokenSymbol[];\n\nexport function getX402Token(symbol: X402TokenSymbol): X402Token {\n\treturn X402_TOKENS[symbol];\n}\n\n/**\n * Optimistic-serve reputation: the Redis key + TTL for a payer principal's\n * \"strike\" counter (reverted/dropped payments). Single-sourced so the API gate\n * (reader) and the worker reconciler (writer, on revert) agree without a\n * cross-package import.\n */\nexport const X402_STRIKE_TTL_SECONDS: number = 24 * 60 * 60;\n\nexport function x402StrikeKey(principal: string): string {\n\treturn `x402:strikes:${principal}`;\n}\n\n/** Resolve a token by its x402 `asset` string (the value carried in `accepts[].asset`). */\nexport function findX402TokenByAsset(asset: string): X402Token | undefined {\n\tfor (const symbol of X402_TOKEN_SYMBOLS) {\n\t\tif (X402_TOKENS[symbol].asset === asset) return X402_TOKENS[symbol];\n\t}\n\treturn undefined;\n}\n"
|
|
6
|
+
],
|
|
7
|
+
"mappings": ";;;;;;;;;;;;;;;;;AAWO,IAAM,eAAe;AAAA,EAC3B,SAAS;AAAA,EACT,SAAS;AACV;AAoBO,IAAM,cAAkD;AAAA,EAC9D,KAAK;AAAA,IACJ,QAAQ;AAAA,IACR,OAAO;AAAA,IACP,YAAY;AAAA,IACZ,WAAW;AAAA,IACX,iBAAiB;AAAA,IACjB,UAAU;AAAA,EACX;AAAA,EACA,MAAM;AAAA,IACL,QAAQ;AAAA,IACR,OAAO;AAAA,IACP,YAAY;AAAA,IACZ,WAAW;AAAA,IACX,iBACC;AAAA,IACD,UAAU;AAAA,EACX;AAAA,EACA,OAAO;AAAA,IACN,QAAQ;AAAA,IACR,OAAO;AAAA,IACP,YAAY;AAAA,IACZ,WAAW;AAAA,IACX,iBACC;AAAA,IACD,UAAU;AAAA,EACX;AACD;AAEO,IAAM,qBAAqB,OAAO,KAAK,WAAW;AAElD,SAAS,YAAY,CAAC,QAAoC;AAAA,EAChE,OAAO,YAAY;AAAA;AASb,IAAM,0BAAkC,KAAK,KAAK;AAElD,SAAS,aAAa,CAAC,WAA2B;AAAA,EACxD,OAAO,gBAAgB;AAAA;AAIjB,SAAS,oBAAoB,CAAC,OAAsC;AAAA,EAC1E,WAAW,UAAU,oBAAoB;AAAA,IACxC,IAAI,YAAY,QAAQ,UAAU;AAAA,MAAO,OAAO,YAAY;AAAA,EAC7D;AAAA,EACA;AAAA;",
|
|
8
|
+
"debugId": "7AA9D65F1DB692C864756E2164756E21",
|
|
9
|
+
"names": []
|
|
10
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { type Kysely, sql } from "kysely";
|
|
2
|
+
import { onChainPlane } from "../src/db/migration-role.ts";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Streams read-path indexes on `events` (chain plane / SOURCE).
|
|
6
|
+
*
|
|
7
|
+
* The Streams firehose filters raw `events` by `data->>'sender'`,
|
|
8
|
+
* `data->>'recipient'`, and `data->>'asset_identifier'` (see
|
|
9
|
+
* `streamsFilterPredicate` in packages/indexer/src/streams-events.ts). Without
|
|
10
|
+
* these the filtered scan is a `Seq Scan on events` over the cursor range —
|
|
11
|
+
* millions of rows on mainnet — and the response window times out. The
|
|
12
|
+
* `(block_height, type)` composite backs both the candidate-row scan and the
|
|
13
|
+
* per-block all-types ordinal CTE that replaces the old per-row correlated
|
|
14
|
+
* COUNT(*).
|
|
15
|
+
*
|
|
16
|
+
* Partial predicate is `(data->>'<field>') IS NOT NULL`, NOT `type IN (...)`:
|
|
17
|
+
* the query filters on equality (`data->>'sender' = $1`), which Postgres can
|
|
18
|
+
* prove implies `IS NOT NULL`, so the index is usable even when the caller
|
|
19
|
+
* filters by sender/recipient WITHOUT a `types=` (a `type IN (transfer types)`
|
|
20
|
+
* predicate would not be provably implied by the unfiltered `type IN (all)`
|
|
21
|
+
* candidate scan, and the planner would skip the index). Keeps the index
|
|
22
|
+
* compact (only rows that carry the field are indexed) while staying usable.
|
|
23
|
+
*
|
|
24
|
+
* `CREATE INDEX CONCURRENTLY` cannot run inside the migrate runner's tx; in
|
|
25
|
+
* prod build these manually with CONCURRENTLY first (matching the index name
|
|
26
|
+
* EXACTLY), then this migration is a no-op there via `IF NOT EXISTS`. On
|
|
27
|
+
* dev/staging the events table is small, so the brief lock is acceptable.
|
|
28
|
+
* `events` is a chain-plane table → DDL no-ops on the control DB under the
|
|
29
|
+
* source/target split.
|
|
30
|
+
*/
|
|
31
|
+
export async function up(db: Kysely<unknown>): Promise<void> {
|
|
32
|
+
await onChainPlane(async () => {
|
|
33
|
+
// Safety net: a blocking CREATE INDEX on the large prod `events` table
|
|
34
|
+
// exceeds the default statement_timeout (~60s) and aborts the deploy
|
|
35
|
+
// (error 57014). Lift it for THIS migration tx so the build completes.
|
|
36
|
+
// On prod the indexes should still be pre-created CONCURRENTLY (see the
|
|
37
|
+
// header) so this is a no-op via IF NOT EXISTS with no write-lock held;
|
|
38
|
+
// this only saves a fresh deploy where pre-creation was skipped — there
|
|
39
|
+
// the build runs to completion instead of hard-failing the deploy.
|
|
40
|
+
await sql`SET LOCAL statement_timeout = 0`.execute(db);
|
|
41
|
+
await sql`
|
|
42
|
+
CREATE INDEX IF NOT EXISTS events_height_type_idx
|
|
43
|
+
ON events (block_height, type)
|
|
44
|
+
`.execute(db);
|
|
45
|
+
await sql`
|
|
46
|
+
CREATE INDEX IF NOT EXISTS events_sender_height_idx
|
|
47
|
+
ON events ((data->>'sender'), block_height)
|
|
48
|
+
WHERE (data->>'sender') IS NOT NULL
|
|
49
|
+
`.execute(db);
|
|
50
|
+
await sql`
|
|
51
|
+
CREATE INDEX IF NOT EXISTS events_recipient_height_idx
|
|
52
|
+
ON events ((data->>'recipient'), block_height)
|
|
53
|
+
WHERE (data->>'recipient') IS NOT NULL
|
|
54
|
+
`.execute(db);
|
|
55
|
+
await sql`
|
|
56
|
+
CREATE INDEX IF NOT EXISTS events_asset_identifier_height_idx
|
|
57
|
+
ON events ((data->>'asset_identifier'), block_height)
|
|
58
|
+
WHERE (data->>'asset_identifier') IS NOT NULL
|
|
59
|
+
`.execute(db);
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export async function down(db: Kysely<unknown>): Promise<void> {
|
|
64
|
+
await onChainPlane(async () => {
|
|
65
|
+
await sql`DROP INDEX IF EXISTS events_asset_identifier_height_idx`.execute(
|
|
66
|
+
db,
|
|
67
|
+
);
|
|
68
|
+
await sql`DROP INDEX IF EXISTS events_recipient_height_idx`.execute(db);
|
|
69
|
+
await sql`DROP INDEX IF EXISTS events_sender_height_idx`.execute(db);
|
|
70
|
+
await sql`DROP INDEX IF EXISTS events_height_type_idx`.execute(db);
|
|
71
|
+
});
|
|
72
|
+
}
|