@secondlayer/subgraphs 3.20.0 → 3.21.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.
@@ -165,18 +165,45 @@ interface ContractDeployFilter {
165
165
  deployer?: string;
166
166
  contractName?: string;
167
167
  }
168
+ /**
169
+ * One declared field of a print payload.
170
+ *
171
+ * A bare {@link ColumnType} is a required scalar (`amount: "uint"`). The
172
+ * composite forms exist because real print payloads are not flat: BNS-V2
173
+ * emits `name` as a NESTED tuple, and a `prints` vocabulary that could only
174
+ * say `"jsonb"` is why a handler reading flat `data.name` type-checked,
175
+ * deployed, and then decoded to null on every event.
176
+ *
177
+ * - `{ type: F, optional: true }` — the KEY becomes optional on `event.data`
178
+ * (mirrors `always_present: false` in the inferred schema).
179
+ * - `{ tuple: { … } }` — a nested tuple, typed all the way down.
180
+ * - `{ list: F }` — a list of `F`.
181
+ */
182
+ type PrintField = ColumnType | {
183
+ type: PrintField
184
+ optional: true
185
+ } | {
186
+ tuple: Record<string, PrintField>
187
+ } | {
188
+ list: PrintField
189
+ };
168
190
  interface PrintEventFilter extends TraitScope {
169
191
  type: "print_event";
170
192
  contractId?: string;
171
193
  topic?: string;
172
194
  /**
173
- * Optional per-topic field schema. When declared, the handler's `event` is
174
- * a discriminated union keyed by `topic` and `event.data` is typed per topic
195
+ * Per-topic field schema. When declared, the handler's `event` is a
196
+ * discriminated union keyed by `topic` and `event.data` is typed per topic
175
197
  * (e.g. `{ "completed-deposit": { amount: "uint", sender: "principal" } }`).
176
- * Uses the same `ColumnType` vocab as `schema`; nested fields use `"jsonb"`.
177
- * Type-level only — not validated at runtime.
198
+ * Nested tuples, lists, and optional fields are expressible — see
199
+ * {@link PrintField}.
200
+ *
201
+ * Declaring `prints` also opts the source into RUNTIME validation: an
202
+ * event whose decoded payload does not match is skipped and logged rather
203
+ * than written as nulls (and the deploy-time field lint is promoted from
204
+ * a warning to an error).
178
205
  */
179
- prints?: Record<string, Record<string, ColumnType>>;
206
+ prints?: Record<string, Record<string, PrintField>>;
180
207
  }
181
208
  /** All subgraph filter types — discriminated on `type` */
182
209
  type SubgraphFilter = StxTransferFilter | StxMintFilter | StxBurnFilter | StxLockFilter | FtTransferFilter | FtMintFilter | FtBurnFilter | NftTransferFilter | NftMintFilter | NftBurnFilter | ContractCallFilter | ContractDeployFilter | PrintEventFilter;
@@ -604,13 +631,34 @@ type ContractCallPayload<F> = F extends {
604
631
  * per topic; otherwise it falls back to the untyped {@link PrintEventPayload}.
605
632
  */
606
633
  type PrintEventFor<F> = F extends {
607
- prints: infer P extends Record<string, Record<string, ColumnType>>
634
+ prints: infer P extends Record<string, Record<string, PrintField>>
608
635
  } ? { [K in keyof P] : {
609
636
  contractId: string
610
637
  topic: K
611
- data: { [Field in keyof P[K]] : ColumnToTS<P[K][Field]> }
638
+ data: PrintDataOf<P[K]>
612
639
  tx: TxMeta
613
640
  } }[keyof P] : PrintEventPayload;
641
+ /** TS type of one declared print field (nested tuples and lists included). */
642
+ type PrintFieldToTS<F> = F extends ColumnType ? ColumnToTS<F> : F extends {
643
+ type: infer Inner
644
+ optional: true
645
+ } ? PrintFieldToTS<Inner> : F extends {
646
+ tuple: infer T extends Record<string, PrintField>
647
+ } ? PrintDataOf<T> : F extends {
648
+ list: infer Item
649
+ } ? PrintFieldToTS<Item>[] : unknown;
650
+ type OptionalFieldKeys<T> = { [K in keyof T] : T[K] extends {
651
+ optional: true
652
+ } ? K : never }[keyof T];
653
+ /** Collapse an intersection into one object literal, so hovering `event.data`
654
+ * shows the fields rather than `A & B`. */
655
+ type Flatten<T> = { [K in keyof T] : T[K] } & {};
656
+ /**
657
+ * `event.data` for one topic: declared fields typed, with
658
+ * `{ optional: true }` fields made optional KEYS (a field observed on only
659
+ * some events must not be typed as always present).
660
+ */
661
+ type PrintDataOf<T extends Record<string, PrintField>> = Flatten<{ [K in Exclude<keyof T, OptionalFieldKeys<T>>] : PrintFieldToTS<T[K]> } & { [K in OptionalFieldKeys<T>]? : PrintFieldToTS<T[K]> }>;
614
662
  /** The event payload a handler receives for a given source filter. */
615
663
  type EventForFilter<F extends SubgraphFilter> = F extends {
616
664
  type: "print_event"
@@ -689,6 +737,18 @@ declare function defineSubgraph<
689
737
  const Sources extends Record<string, SubgraphFilter>,
690
738
  const S extends SubgraphSchema
691
739
  >(def: TypedSubgraphDefinition<Sources, S>): TypedSubgraphDefinition<Sources, S>;
740
+ type PrintValidationResult = {
741
+ ok: true
742
+ } | {
743
+ ok: false
744
+ reason: string
745
+ };
746
+ /**
747
+ * Validate one decoded print event against a source's `prints` declaration.
748
+ * Returns `{ ok: true }` when no declaration covers this topic (undeclared
749
+ * topics are not the developer's stated contract, so they pass through).
750
+ */
751
+ declare function validatePrintPayload(prints: Record<string, Record<string, PrintField>> | undefined, topic: string, data: Record<string, unknown>): PrintValidationResult;
692
752
  /**
693
753
  * Empirical print-payload schema inference. Callers (the index print-schema
694
754
  * endpoint) extract topic + raw Clarity hex per row; this module deserializes
@@ -928,4 +988,4 @@ type SparseProbeTarget = {
928
988
  declare function canSparseScan(subgraph: SubgraphDefinition): boolean;
929
989
  /** Probe targets for a subgraph's filters: decoded type + contract scope when\\n* the filter pins one (assetIdentifier "SP….contract::asset" or contractId). */
930
990
  declare function sparseProbeTargets(subgraph: SubgraphDefinition): SparseProbeTarget[];
931
- export { validateSubgraphDefinition, sparseProbeTargets, resumeReindex, renderDeployPlan, reindexSubgraph, pgSchemaName, inferPrintTopics, hasBreakingChanges, generateSubgraphSQL, generatePrismaSchema, generateKyselySchema, generateIndexSchema, generateDrizzleSchema, diffSchema, deploySchema, defineSubgraph, canSparseScan, camelizeDataKey, backfillSubgraph, WriteRow, WhereInput, TypedSubgraphDefinition, TypedSubgraphContext, TypedHandlers, TxMeta, TableDiff, SystemRow, SubscribeOptions, SubgraphTableClient, SubgraphTable, SubgraphSchema, SubgraphHandler, SubgraphFilter, SubgraphDefinition, SubgraphContext, SubgraphColumn, StxTransferPayload, StxTransferFilter, StxMintPayload, StxMintFilter, StxLockPayload, StxLockFilter, StxBurnPayload, StxBurnFilter, SparseProbeTarget, RowValue, ReindexOptions, PrismaGenOptions, PrintSample, PrintEventPayload, PrintEventFor, PrintEventFilter, NftTransferPayload, NftTransferFilter, NftMintPayload, NftMintFilter, NftBurnPayload, NftBurnFilter, KyselyGenOptions, InferredTopicSchema, InferredPrintField, InferTableRow, InferSubgraphClient, InferColumnType, IndexCodegenTarget, IndexCodegenOptions, INDEX_CODEGEN_TABLES, GeneratedSQL, FtTransferPayload, FtTransferFilter, FtMintPayload, FtMintFilter, FtBurnPayload, FtBurnFilter, FindManyOptions, EventForFilter, DrizzleGenOptions, DeployPlan, DeployDiff, ContractDeployPayload, ContractDeployFilter, ContractCallPayload, ContractCallFilter, ContractCallEvent, ComputedValue, ComparisonFilter, ColumnType, ColumnToTS, ColumnDiff, ByoMigrationPlan, ByoBreakingChangeError, AnyEvent, AggregateSpec, AggregateResult };
991
+ export { validateSubgraphDefinition, validatePrintPayload, sparseProbeTargets, resumeReindex, renderDeployPlan, reindexSubgraph, pgSchemaName, inferPrintTopics, hasBreakingChanges, generateSubgraphSQL, generatePrismaSchema, generateKyselySchema, generateIndexSchema, generateDrizzleSchema, diffSchema, deploySchema, defineSubgraph, canSparseScan, camelizeDataKey, backfillSubgraph, WriteRow, WhereInput, TypedSubgraphDefinition, TypedSubgraphContext, TypedHandlers, TxMeta, TableDiff, SystemRow, SubscribeOptions, SubgraphTableClient, SubgraphTable, SubgraphSchema, SubgraphHandler, SubgraphFilter, SubgraphDefinition, SubgraphContext, SubgraphColumn, StxTransferPayload, StxTransferFilter, StxMintPayload, StxMintFilter, StxLockPayload, StxLockFilter, StxBurnPayload, StxBurnFilter, SparseProbeTarget, RowValue, ReindexOptions, PrismaGenOptions, PrintValidationResult, PrintSample, PrintFieldToTS, PrintField, PrintEventPayload, PrintEventFor, PrintEventFilter, PrintDataOf, NftTransferPayload, NftTransferFilter, NftMintPayload, NftMintFilter, NftBurnPayload, NftBurnFilter, KyselyGenOptions, InferredTopicSchema, InferredPrintField, InferTableRow, InferSubgraphClient, InferColumnType, IndexCodegenTarget, IndexCodegenOptions, INDEX_CODEGEN_TABLES, GeneratedSQL, FtTransferPayload, FtTransferFilter, FtMintPayload, FtMintFilter, FtBurnPayload, FtBurnFilter, FindManyOptions, EventForFilter, DrizzleGenOptions, DeployPlan, DeployDiff, ContractDeployPayload, ContractDeployFilter, ContractCallPayload, ContractCallFilter, ContractCallEvent, ComputedValue, ComparisonFilter, ColumnType, ColumnToTS, ColumnDiff, ByoMigrationPlan, ByoBreakingChangeError, AnyEvent, AggregateSpec, AggregateResult };
package/dist/src/index.js CHANGED
@@ -14,6 +14,44 @@ var ColumnTypeSchema = z.enum([
14
14
  "timestamp",
15
15
  "jsonb"
16
16
  ]);
17
+ var PrintFieldSchema = z.lazy(() => z.union([
18
+ ColumnTypeSchema,
19
+ z.object({ type: PrintFieldSchema, optional: z.literal(true) }),
20
+ z.object({ tuple: z.record(z.string(), PrintFieldSchema) }),
21
+ z.object({ list: PrintFieldSchema })
22
+ ]));
23
+ var AbiTypeSchema = z.lazy(() => z.union([
24
+ z.string(),
25
+ z.object({ buff: z.object({ length: z.number() }) }),
26
+ z.object({ "string-ascii": z.object({ length: z.number() }) }),
27
+ z.object({ "string-utf8": z.object({ length: z.number() }) }),
28
+ z.object({ optional: AbiTypeSchema }),
29
+ z.object({
30
+ list: z.object({ type: AbiTypeSchema, length: z.number() })
31
+ }),
32
+ z.object({
33
+ tuple: z.array(z.object({ name: z.string(), type: AbiTypeSchema }))
34
+ }),
35
+ z.object({
36
+ response: z.object({ ok: AbiTypeSchema, error: AbiTypeSchema })
37
+ })
38
+ ]));
39
+ var AbiContractSchema = z.object({
40
+ functions: z.array(z.object({
41
+ name: z.string(),
42
+ access: z.enum(["public", "read-only", "private"], {
43
+ message: "ABI access must be public | read-only | private — a raw Hiro/Clarinet ABI uses `read_only`. Normalize it first: normalizeAbi() from @secondlayer/stacks/clarity."
44
+ }),
45
+ args: z.array(z.object({ name: z.string(), type: AbiTypeSchema })),
46
+ outputs: AbiTypeSchema
47
+ })),
48
+ maps: z.array(z.unknown()).optional(),
49
+ variables: z.array(z.unknown()).optional(),
50
+ fungible_tokens: z.array(z.unknown()).optional(),
51
+ non_fungible_tokens: z.array(z.unknown()).optional(),
52
+ implemented_traits: z.array(z.unknown()).optional(),
53
+ defined_traits: z.array(z.unknown()).optional()
54
+ });
17
55
  var SubgraphColumnSchema = z.object({
18
56
  type: ColumnTypeSchema,
19
57
  nullable: z.boolean().optional(),
@@ -62,9 +100,9 @@ var SubgraphFilterSchema = z.object({
62
100
  contractName: z.string().optional(),
63
101
  topic: z.string().optional(),
64
102
  lockedAddress: z.string().optional(),
65
- abi: z.record(z.string(), z.any()).optional(),
103
+ abi: AbiContractSchema.optional(),
66
104
  trait: z.string().optional(),
67
- prints: z.record(z.string(), z.record(z.string(), ColumnTypeSchema)).optional()
105
+ prints: z.record(z.string(), z.record(z.string(), PrintFieldSchema)).optional()
68
106
  }).strict();
69
107
  var SubgraphDefinitionSchema = z.object({
70
108
  name: SubgraphNameSchema,
@@ -837,6 +875,87 @@ import {
837
875
  deserializeCV as deserializeCV2,
838
876
  toCamelCase
839
877
  } from "@secondlayer/stacks/clarity";
878
+
879
+ // src/runtime/print-validate.ts
880
+ var COLUMN_TYPES = new Set([
881
+ "text",
882
+ "uint",
883
+ "int",
884
+ "principal",
885
+ "boolean",
886
+ "timestamp",
887
+ "jsonb"
888
+ ]);
889
+ function isColumnType(field) {
890
+ return typeof field === "string" && COLUMN_TYPES.has(field);
891
+ }
892
+ function matchesField(field, value) {
893
+ if (isColumnType(field)) {
894
+ switch (field) {
895
+ case "uint":
896
+ case "int":
897
+ return typeof value === "bigint" || typeof value === "number";
898
+ case "boolean":
899
+ return typeof value === "boolean";
900
+ case "text":
901
+ case "principal":
902
+ case "timestamp":
903
+ return typeof value === "string" || typeof value === "number" || typeof value === "bigint";
904
+ case "jsonb":
905
+ return value !== undefined;
906
+ }
907
+ }
908
+ if (typeof field !== "object" || field === null)
909
+ return false;
910
+ if ("optional" in field) {
911
+ return value === undefined || value === null || matchesField(field.type, value);
912
+ }
913
+ if ("tuple" in field) {
914
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
915
+ return false;
916
+ }
917
+ return matchesTopic(field.tuple, value).ok;
918
+ }
919
+ if ("list" in field) {
920
+ return Array.isArray(value) && value.every((item) => matchesField(field.list, item));
921
+ }
922
+ return false;
923
+ }
924
+ function matchesTopic(declared, data) {
925
+ for (const [name, field] of Object.entries(declared)) {
926
+ const present = name in data;
927
+ const optional = typeof field === "object" && field !== null && "optional" in field;
928
+ if (!present) {
929
+ if (optional)
930
+ continue;
931
+ return { ok: false, reason: `missing declared field "${name}"` };
932
+ }
933
+ if (!matchesField(field, data[name])) {
934
+ return {
935
+ ok: false,
936
+ reason: `field "${name}" does not match its declared type (got ${describe(data[name])})`
937
+ };
938
+ }
939
+ }
940
+ return { ok: true };
941
+ }
942
+ function describe(value) {
943
+ if (value === null)
944
+ return "null";
945
+ if (Array.isArray(value))
946
+ return "array";
947
+ return typeof value;
948
+ }
949
+ function validatePrintPayload(prints, topic, data) {
950
+ if (!prints)
951
+ return { ok: true };
952
+ const declared = prints[topic];
953
+ if (!declared)
954
+ return { ok: true };
955
+ return matchesTopic(declared, data);
956
+ }
957
+
958
+ // src/runtime/runner.ts
840
959
  var DEFAULT_ERROR_THRESHOLD = 50;
841
960
  function camelCase(str) {
842
961
  return str.replace(/-([a-z0-9])/g, (_, c) => c.toUpperCase());
@@ -1081,6 +1200,7 @@ function buildEventPayload(filter, tx, event) {
1081
1200
  async function runHandlers(subgraph, matched, ctx, opts) {
1082
1201
  let processed = 0;
1083
1202
  let errors = 0;
1203
+ let skipped = 0;
1084
1204
  const threshold = opts?.errorThreshold ?? DEFAULT_ERROR_THRESHOLD;
1085
1205
  const filterLookup = new Map;
1086
1206
  if (!Array.isArray(subgraph.sources)) {
@@ -1105,7 +1225,7 @@ async function runHandlers(subgraph, matched, ctx, opts) {
1105
1225
  errors,
1106
1226
  threshold
1107
1227
  });
1108
- return { processed, errors };
1228
+ return { processed, errors, ...skipped > 0 ? { skipped } : {} };
1109
1229
  }
1110
1230
  const handler = subgraph.handlers[sourceName] ?? subgraph.handlers["*"] ?? null;
1111
1231
  if (!handler) {
@@ -1161,6 +1281,21 @@ async function runHandlers(subgraph, matched, ctx, opts) {
1161
1281
  if (event !== null && filter?.type === "print_event" && filter.topic && payload.topic !== filter.topic) {
1162
1282
  continue;
1163
1283
  }
1284
+ if (event !== null && filter?.type === "print_event" && filter.prints) {
1285
+ const printPayload = payload;
1286
+ const verdict = validatePrintPayload(filter.prints, printPayload.topic ?? "", printPayload.data ?? {});
1287
+ if (!verdict.ok) {
1288
+ skipped++;
1289
+ logger2.warn("Print payload does not match declared prints schema", {
1290
+ subgraph: subgraph.name,
1291
+ sourceName,
1292
+ topic: printPayload.topic,
1293
+ txId: tx.tx_id,
1294
+ reason: verdict.reason
1295
+ });
1296
+ continue;
1297
+ }
1298
+ }
1164
1299
  await handler(payload, ctx);
1165
1300
  processed++;
1166
1301
  } catch (err) {
@@ -1175,7 +1310,7 @@ async function runHandlers(subgraph, matched, ctx, opts) {
1175
1310
  });
1176
1311
  }
1177
1312
  }
1178
- return { processed, errors };
1313
+ return { processed, errors, ...skipped > 0 ? { skipped } : {} };
1179
1314
  }
1180
1315
 
1181
1316
  // src/runtime/source-matcher.ts
@@ -4197,6 +4332,7 @@ function getDefault(type) {
4197
4332
  }
4198
4333
  export {
4199
4334
  validateSubgraphDefinition,
4335
+ validatePrintPayload,
4200
4336
  sparseProbeTargets,
4201
4337
  resumeReindex,
4202
4338
  renderDeployPlan,
@@ -4219,5 +4355,5 @@ export {
4219
4355
  ByoBreakingChangeError
4220
4356
  };
4221
4357
 
4222
- //# debugId=E040C325764492FC64756E2164756E21
4358
+ //# debugId=1014943AF0D5A26B64756E2164756E21
4223
4359
  //# sourceMappingURL=index.js.map