envio 3.9.0 → 3.10.0-resolvers.1

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 (133) hide show
  1. package/evm.schema.json +33 -0
  2. package/fuel.schema.json +33 -0
  3. package/index.d.ts +300 -14
  4. package/index.js +10 -0
  5. package/package.json +6 -7
  6. package/src/Batch.res +9 -0
  7. package/src/Bin.res +47 -0
  8. package/src/Bin.res.mjs +34 -1
  9. package/src/ChainState.res +35 -39
  10. package/src/ChainState.res.mjs +14 -11
  11. package/src/ChainState.resi +15 -7
  12. package/src/Config.res +82 -49
  13. package/src/Config.res.mjs +82 -18
  14. package/src/Core.res +4 -0
  15. package/src/CrossChainState.res +15 -23
  16. package/src/CrossChainState.res.mjs +10 -18
  17. package/src/CrossChainState.resi +4 -1
  18. package/src/Ecosystem.res +7 -2
  19. package/src/Env.res +141 -1
  20. package/src/Env.res.mjs +133 -1
  21. package/src/Envio.res +6 -4
  22. package/src/EventConfigBuilder.res +174 -27
  23. package/src/EventConfigBuilder.res.mjs +126 -15
  24. package/src/HandlerRegister.res +24 -4
  25. package/src/HandlerRegister.res.mjs +24 -5
  26. package/src/InMemoryStore.res +9 -7
  27. package/src/InMemoryStore.res.mjs +3 -3
  28. package/src/IndexerState.res +43 -6
  29. package/src/IndexerState.res.mjs +36 -7
  30. package/src/IndexerState.resi +4 -4
  31. package/src/Internal.res +36 -21
  32. package/src/Internal.res.mjs +9 -0
  33. package/src/Metrics.res +72 -6
  34. package/src/Metrics.res.mjs +30 -2
  35. package/src/Persistence.res +36 -30
  36. package/src/Persistence.res.mjs +1 -20
  37. package/src/PgStorage.res +104 -56
  38. package/src/PgStorage.res.mjs +57 -35
  39. package/src/PruneStaleHistory.res +85 -52
  40. package/src/PruneStaleHistory.res.mjs +63 -37
  41. package/src/Rollback.res +68 -45
  42. package/src/Rollback.res.mjs +38 -19
  43. package/src/RollbackCommit.res +7 -9
  44. package/src/RollbackCommit.res.mjs +5 -6
  45. package/src/SafeCheckpointTracking.res +11 -11
  46. package/src/SafeCheckpointTracking.res.mjs +7 -6
  47. package/src/SimulateItems.res +44 -22
  48. package/src/SimulateItems.res.mjs +32 -27
  49. package/src/Sink.res +38 -26
  50. package/src/Sink.res.mjs +21 -16
  51. package/src/TestIndexer.res +5 -5
  52. package/src/TestIndexer.res.mjs +1 -1
  53. package/src/Utils.res +170 -0
  54. package/src/Utils.res.mjs +153 -0
  55. package/src/Writing.res +2 -2
  56. package/src/Writing.res.mjs +1 -1
  57. package/src/bindings/ClickHouse.res +366 -698
  58. package/src/bindings/ClickHouse.res.mjs +371 -421
  59. package/src/bindings/ClickHouseSink.res +337 -0
  60. package/src/bindings/ClickHouseSink.res.mjs +246 -0
  61. package/src/bindings/EventSource.res +8 -2
  62. package/src/bindings/NodeJs.res +6 -0
  63. package/src/bindings/Vitest.res +23 -0
  64. package/src/bindings/Vitest.res.mjs +3 -0
  65. package/src/bindings/WebSocket.res +9 -10
  66. package/src/db/CheckpointBounds.res +53 -0
  67. package/src/db/CheckpointBounds.res.mjs +44 -0
  68. package/src/db/EntityFilter.res +36 -17
  69. package/src/db/EntityFilter.res.mjs +25 -14
  70. package/src/db/EntityHistory.res +47 -22
  71. package/src/db/EntityHistory.res.mjs +19 -12
  72. package/src/db/InternalTable.res +84 -35
  73. package/src/db/InternalTable.res.mjs +56 -23
  74. package/src/db/RollbackFloors.res +55 -0
  75. package/src/db/RollbackFloors.res.mjs +92 -0
  76. package/src/db/Table.res +22 -4
  77. package/src/db/Table.res.mjs +30 -9
  78. package/src/resolvers/ResolverProcess.res +811 -0
  79. package/src/resolvers/ResolverProcess.res.mjs +668 -0
  80. package/src/resolvers/ResolverQuery.res +317 -0
  81. package/src/resolvers/ResolverQuery.res.mjs +220 -0
  82. package/src/resolvers/collisions.js +108 -0
  83. package/src/resolvers/db.js +363 -0
  84. package/src/resolvers/dispatch.js +259 -0
  85. package/src/resolvers/errors.js +30 -0
  86. package/src/resolvers/graphqlSelection.js +292 -0
  87. package/src/resolvers/hasuraAction.js +84 -0
  88. package/src/resolvers/hasuraApply.js +333 -0
  89. package/src/resolvers/hasuraMetadata.js +137 -0
  90. package/src/resolvers/index.js +123 -0
  91. package/src/resolvers/manifest.js +376 -0
  92. package/src/resolvers/server.js +304 -0
  93. package/src/sources/Evm.res +2 -0
  94. package/src/sources/Evm.res.mjs +2 -0
  95. package/src/sources/EvmHyperSyncSource.res +4 -10
  96. package/src/sources/EvmHyperSyncSource.res.mjs +5 -9
  97. package/src/sources/EvmRpcWs.res +124 -0
  98. package/src/sources/EvmRpcWs.res.mjs +117 -0
  99. package/src/sources/Fuel.res +2 -0
  100. package/src/sources/Fuel.res.mjs +2 -0
  101. package/src/sources/FuelHyperSyncSource.res +2 -8
  102. package/src/sources/FuelHyperSyncSource.res.mjs +4 -6
  103. package/src/sources/HeightFeed.res +568 -0
  104. package/src/sources/HeightFeed.res.mjs +452 -0
  105. package/src/sources/HeightStream.res +257 -0
  106. package/src/sources/HeightStream.res.mjs +194 -0
  107. package/src/sources/HyperSync.res +14 -0
  108. package/src/sources/HyperSync.res.mjs +16 -0
  109. package/src/sources/HyperSync.resi +9 -0
  110. package/src/sources/HyperSyncSSE.res +49 -0
  111. package/src/sources/HyperSyncSSE.res.mjs +79 -0
  112. package/src/sources/RpcSource.res +1 -1
  113. package/src/sources/RpcSource.res.mjs +2 -2
  114. package/src/sources/Source.res +54 -1
  115. package/src/sources/Source.res.mjs +25 -0
  116. package/src/sources/SourceManager.res +220 -227
  117. package/src/sources/SourceManager.res.mjs +139 -164
  118. package/src/sources/SourceManager.resi +8 -0
  119. package/src/sources/Svm.res +2 -0
  120. package/src/sources/Svm.res.mjs +2 -0
  121. package/src/sources/SvmHyperSyncClient.res +70 -127
  122. package/src/sources/SvmHyperSyncClient.res.mjs +51 -25
  123. package/src/sources/SvmHyperSyncSource.res +32 -18
  124. package/src/sources/SvmHyperSyncSource.res.mjs +39 -16
  125. package/src/tui/Tui.res +1 -1
  126. package/src/tui/Tui.res.mjs +1 -1
  127. package/svm.schema.json +29 -101
  128. package/src/MemoryStorage.res +0 -729
  129. package/src/MemoryStorage.res.mjs +0 -586
  130. package/src/sources/HyperSyncHeightStream.res +0 -129
  131. package/src/sources/HyperSyncHeightStream.res.mjs +0 -110
  132. package/src/sources/RpcWebSocketHeightStream.res +0 -175
  133. package/src/sources/RpcWebSocketHeightStream.res.mjs +0 -180
package/evm.schema.json CHANGED
@@ -29,6 +29,13 @@
29
29
  "null"
30
30
  ]
31
31
  },
32
+ "resolvers": {
33
+ "description": "Optional relative path to the custom GraphQL resolvers, either a single module (e.g. 'src/Resolvers.ts') or a directory (e.g. 'src/resolvers'), in which case every .js/.mjs/.ts file beneath it is loaded, tests excluded. The resolvers they export are served as additional root fields on the indexer's GraphQL endpoint.",
34
+ "type": [
35
+ "string",
36
+ "null"
37
+ ]
38
+ },
32
39
  "full_batch_size": {
33
40
  "description": "Target number of events to be processed per batch. Set it to smaller number if you have many Effect API calls which are slow to resolve and can't be batched. (Default: 5000)",
34
41
  "type": [
@@ -126,6 +133,17 @@
126
133
  "type": "null"
127
134
  }
128
135
  ]
136
+ },
137
+ "bytes_type": {
138
+ "description": "How the `Bytes` scalar in schema.graphql is represented. `hex` keeps 0x-prefixed hex strings stored as text, `uint8array` exposes `Uint8Array` values in handlers and stores raw bytes (BYTEA in Postgres, String in ClickHouse). (default: hex)",
139
+ "anyOf": [
140
+ {
141
+ "$ref": "#/$defs/BytesType"
142
+ },
143
+ {
144
+ "type": "null"
145
+ }
146
+ ]
129
147
  }
130
148
  },
131
149
  "additionalProperties": false,
@@ -758,6 +776,21 @@
758
776
  "checksum",
759
777
  "lowercase"
760
778
  ]
779
+ },
780
+ "BytesType": {
781
+ "description": "How the schema.graphql `Bytes` scalar reaches handlers and storage.",
782
+ "oneOf": [
783
+ {
784
+ "description": "`0x`-prefixed hex strings, stored as text.",
785
+ "type": "string",
786
+ "const": "hex"
787
+ },
788
+ {
789
+ "description": "`Uint8Array` values, stored as raw bytes (`BYTEA` in Postgres, `String` in ClickHouse).",
790
+ "type": "string",
791
+ "const": "uint8array"
792
+ }
793
+ ]
761
794
  }
762
795
  }
763
796
  }
package/fuel.schema.json CHANGED
@@ -29,6 +29,13 @@
29
29
  "null"
30
30
  ]
31
31
  },
32
+ "resolvers": {
33
+ "description": "Optional relative path to the custom GraphQL resolvers, either a single module (e.g. 'src/Resolvers.ts') or a directory (e.g. 'src/resolvers'), in which case every .js/.mjs/.ts file beneath it is loaded, tests excluded. The resolvers they export are served as additional root fields on the indexer's GraphQL endpoint.",
34
+ "type": [
35
+ "string",
36
+ "null"
37
+ ]
38
+ },
32
39
  "full_batch_size": {
33
40
  "description": "Target number of events to be processed per batch. Set it to smaller number if you have many Effect API calls which are slow to resolve and can't be batched. (Default: 5000)",
34
41
  "type": [
@@ -83,6 +90,17 @@
83
90
  "boolean",
84
91
  "null"
85
92
  ]
93
+ },
94
+ "bytes_type": {
95
+ "description": "How the `Bytes` scalar in schema.graphql is represented. `hex` keeps 0x-prefixed hex strings stored as text, `uint8array` exposes `Uint8Array` values in handlers and stores raw bytes (BYTEA in Postgres, String in ClickHouse). (default: hex)",
96
+ "anyOf": [
97
+ {
98
+ "$ref": "#/$defs/BytesType"
99
+ },
100
+ {
101
+ "type": "null"
102
+ }
103
+ ]
86
104
  }
87
105
  },
88
106
  "additionalProperties": false,
@@ -464,6 +482,21 @@
464
482
  }
465
483
  }
466
484
  ]
485
+ },
486
+ "BytesType": {
487
+ "description": "How the schema.graphql `Bytes` scalar reaches handlers and storage.",
488
+ "oneOf": [
489
+ {
490
+ "description": "`0x`-prefixed hex strings, stored as text.",
491
+ "type": "string",
492
+ "const": "hex"
493
+ },
494
+ {
495
+ "description": "`Uint8Array` values, stored as raw bytes (`BYTEA` in Postgres, `String` in ClickHouse).",
496
+ "type": "string",
497
+ "const": "uint8array"
498
+ }
499
+ ]
467
500
  }
468
501
  }
469
502
  }
package/index.d.ts CHANGED
@@ -1377,7 +1377,7 @@ type SvmNamedAccounts<
1377
1377
  Acc extends Readonly<Record<string, unknown>>,
1378
1378
  Fields extends SvmFieldsSelection,
1379
1379
  > = {
1380
- readonly [K in keyof Acc & string]: SvmInstructionAccount<Fields, K>;
1380
+ readonly [K in keyof Acc]: SvmInstructionAccount<Fields, K & string>;
1381
1381
  };
1382
1382
 
1383
1383
  /** The parent transaction of a {@link SvmInstruction}, narrowed to the
@@ -1423,7 +1423,7 @@ type SvmSelectedInstruction<Fields extends SvmFieldsSelection, ProgInstr> = {
1423
1423
  readonly instructionName: string;
1424
1424
  readonly discriminator: string;
1425
1425
  readonly programId: SvmInstrField<Fields, "programId", string>;
1426
- readonly data: SvmInstrField<Fields, "data", string>;
1426
+ readonly data: SvmInstrField<Fields, "data", Uint8Array>;
1427
1427
  readonly path: SvmInstrField<Fields, "path", readonly number[]>;
1428
1428
  readonly isInner: SvmInstrField<Fields, "isInner", boolean>;
1429
1429
  readonly args: SvmInstrField<
@@ -1484,6 +1484,37 @@ export type SvmOnInstructionHandlerArgs<
1484
1484
  Instruction extends keyof SvmProgramsT[Program] & string = never,
1485
1485
  > = SvmOnInstructionArgsFor<SvmInstruction<Fields, Program, Instruction>, GlobalConfig>;
1486
1486
 
1487
+ /** The named accounts of a configured instruction. Falls back to an open
1488
+ * record when the instruction carries no resolved schema, so its filter keys
1489
+ * are unconstrained rather than unusable. */
1490
+ type SvmAccountsOf<Program extends string, Instruction extends string> =
1491
+ SvmConfiguredInstruction<Program, Instruction>["accounts"];
1492
+
1493
+ /** One AND-group of account narrowings: every named account must match one of
1494
+ * its listed pubkeys. Names are the instruction's own account names. */
1495
+ export type SvmAccountsFilter<Accounts> = {
1496
+ readonly [K in keyof Accounts]?: string | readonly string[];
1497
+ };
1498
+
1499
+ /** The `where` option value of `indexer.onInstruction`.
1500
+ *
1501
+ * `accounts` accepts either a single AND-group of account narrowings, or an
1502
+ * array of them (OR semantics). `block.slot._gte` promotes to the
1503
+ * registration's startBlock and overrides the chain-level `start_block` — use
1504
+ * it to restrict per-instruction processing without touching `config.yaml`.
1505
+ * Only `_gte` is supported here; use `indexer.onSlot` for `_lte` / `_every`. */
1506
+ export type SvmOnInstructionWhere<Accounts> = {
1507
+ /** Match only inner (CPI-invoked) or only outer instructions. Absent
1508
+ * matches both. */
1509
+ readonly isInner?: boolean;
1510
+ readonly accounts?: SvmAccountsFilter<Accounts> | readonly SvmAccountsFilter<Accounts>[];
1511
+ readonly block?: {
1512
+ readonly slot?: {
1513
+ readonly _gte?: number;
1514
+ };
1515
+ };
1516
+ };
1517
+
1487
1518
  /** Options for an SVM `indexer.onInstruction` registration. */
1488
1519
  export type SvmOnInstructionOptions<
1489
1520
  P extends string = string,
@@ -1493,10 +1524,11 @@ export type SvmOnInstructionOptions<
1493
1524
  /** Program name as declared under `chains[].programs[].name` in
1494
1525
  * `config.yaml`. */
1495
1526
  readonly program: P;
1496
- /** Instruction name as declared under
1497
- * `chains[].programs[].instructions[].name` in `config.yaml`. */
1527
+ /** Instruction name from the program's IDL, or from
1528
+ * `chains[].programs[].instructions[].name` when the layout is inline. */
1498
1529
  readonly instruction: I;
1499
1530
  readonly fields?: Fields & SvmFieldsLiteralCheck<Fields>;
1531
+ readonly where?: SvmOnInstructionWhere<SvmAccountsOf<P, I>>;
1500
1532
  };
1501
1533
 
1502
1534
  /** Handler function for an SVM `indexer.onInstruction` registration. Takes
@@ -1736,9 +1768,10 @@ type SvmEcosystem<Config extends IndexerConfigTypes = GlobalConfig> =
1736
1768
  }
1737
1769
  ? {
1738
1770
  /**
1739
- * Register an instruction handler. Dispatch matches on
1740
- * `(programId, discriminator)` from the YAML config.
1741
- * Handler `fields` is the only source of payload selection.
1771
+ * Register an instruction handler. `program` and `instruction`
1772
+ * name an entry from the IDL or YAML. Dispatch uses that
1773
+ * instruction's discriminator. Handler `fields` is the only
1774
+ * source of payload selection.
1742
1775
  */
1743
1776
  readonly onInstruction: <
1744
1777
  P extends keyof Programs & string,
@@ -1905,8 +1938,8 @@ type SvmSimulateItem<Config extends IndexerConfigTypes = GlobalConfig> =
1905
1938
  path?: readonly number[];
1906
1939
  /** Override the program id. Defaults to the configured `program_id`. */
1907
1940
  programId?: string;
1908
- /** Raw instruction data, `0x`-prefixed hex. */
1909
- data?: string;
1941
+ /** Raw instruction data. Defaults to the configured discriminator bytes. */
1942
+ data?: Uint8Array;
1910
1943
  /** Whether this is a CPI-invoked inner instruction. */
1911
1944
  isInner?: boolean;
1912
1945
  /** Decoded args. Keys match the instruction's arg names. */
@@ -1980,10 +2013,10 @@ type ConfigEntities<Config extends IndexerConfigTypes = GlobalConfig> =
1980
2013
  type PerChainEntityNames<Config extends IndexerConfigTypes = GlobalConfig> =
1981
2014
  Config extends { perChainEntities: infer Names extends string } ? Names : never;
1982
2015
 
1983
- /** The row shape the chain-agnostic test-indexer operations exchange. A
1984
- * per-chain entity's row is only identified together with its chain, so the
1985
- * chain id travels alongside the entity fields. */
1986
- type TestIndexerEntityRow<
2016
+ /** The row shape chain-agnostic APIs exchange. A per-chain entity's row is
2017
+ * only identified together with its chain, so the chain id travels alongside
2018
+ * the entity fields. */
2019
+ type ChainScopedEntityRow<
1987
2020
  Config extends IndexerConfigTypes,
1988
2021
  Name,
1989
2022
  Entity
@@ -2177,7 +2210,7 @@ export type TestIndexerFromConfig<Config extends IndexerConfigTypes = GlobalConf
2177
2210
  } & SingleEcosystemTestChains<Config> & {
2178
2211
  /** Entity operations for direct manipulation outside of handlers. */
2179
2212
  readonly [K in keyof ConfigEntities<Config>]: TestIndexerEntityOperations<
2180
- TestIndexerEntityRow<Config, K, ConfigEntities<Config>[K]>
2213
+ ChainScopedEntityRow<Config, K, ConfigEntities<Config>[K]>
2181
2214
  >;
2182
2215
  };
2183
2216
 
@@ -2251,6 +2284,259 @@ export type EnumName = keyof EnumsT & string;
2251
2284
  /** Lookup an enum value type by name (e.g. `Enum<"AccountType">`). */
2252
2285
  export type Enum<TName extends EnumName> = EnumsT[TName];
2253
2286
 
2287
+ // ============== Custom Resolver Types ==============
2288
+
2289
+ /** Handle for a custom GraphQL resolver created via {@link createResolver}. */
2290
+ export declare abstract class Resolver<Args, Output> {
2291
+ protected opaque: Args | Output;
2292
+ }
2293
+
2294
+ /** The selection the caller asked for: field names and their nesting, with
2295
+ * aliases and fragments already resolved away. A resolver may use it to skip
2296
+ * work it wasn't asked for — the response is projected against the declared
2297
+ * schema either way, so correctness never depends on honouring it. */
2298
+ export type ResolverSelection = { readonly [field: string]: ResolverSelection };
2299
+
2300
+ /** Request metadata, forwarded by envio-serve. */
2301
+ export type ResolverContext = {
2302
+ /** The role the operation was executed under. `admin: true` resolvers are
2303
+ * only reachable as `"admin"`. */
2304
+ readonly role: "public" | "admin";
2305
+ /** Correlates this field's work with the operation it belongs to. */
2306
+ readonly requestId: string;
2307
+ /** W3C trace context of the incoming request, when the caller sent one. */
2308
+ readonly traceparent?: string;
2309
+ };
2310
+
2311
+ /** How far a chain has been indexed. Read it to refuse to answer from a stale
2312
+ * index — `sourceBlock - progressBlock` is how far behind the head the
2313
+ * indexer is. */
2314
+ export type ChainHeight = {
2315
+ readonly chainId: number;
2316
+ /** `"evm"`, `"fuel"` or `"svm"`. Chain ids are only unique within one. */
2317
+ readonly ecosystem: string;
2318
+ readonly startBlock: number;
2319
+ readonly endBlock: number | null;
2320
+ /** The latest block seen on the chain. */
2321
+ readonly sourceBlock: number;
2322
+ /** The latest block fetched from the source. */
2323
+ readonly bufferBlock: number;
2324
+ /** The latest block whose events have been processed and committed. */
2325
+ readonly progressBlock: number;
2326
+ /** When the chain first caught up to the head, or null while it hasn't. */
2327
+ readonly readyAt: Date | null;
2328
+ readonly isReady: boolean;
2329
+ };
2330
+
2331
+ /** Raw SQL, run under the resolver's `statement_timeout` and against its
2332
+ * bounded pool. */
2333
+ export type ResolverSql = {
2334
+ /** ``db.sql`select 1` `` */
2335
+ <Row = Record<string, unknown>>(
2336
+ strings: TemplateStringsArray,
2337
+ ...values: readonly unknown[]
2338
+ ): Promise<Row[]>;
2339
+ /** `db.sql.unsafe(text, params)` — the direct lift for a query built as a
2340
+ * string with positional parameters. */
2341
+ unsafe<Row = Record<string, unknown>>(
2342
+ text: string,
2343
+ params?: readonly unknown[],
2344
+ options?: { readonly prepare?: boolean }
2345
+ ): Promise<Row[]>;
2346
+ };
2347
+
2348
+ /** Options accepted by `db.find`. */
2349
+ export type ResolverFindOptions<Entity> = {
2350
+ /** Filter by field, with the same operators as `context.<Entity>.getWhere`.
2351
+ * Unlike `getWhere`, any field can be filtered on here — it is a query
2352
+ * against the table, so an unindexed field is a slow one, not an error. */
2353
+ readonly where?: GetWhereFilter<Entity>;
2354
+ readonly orderBy?: readonly {
2355
+ readonly field: keyof Entity & string;
2356
+ readonly direction: "asc" | "desc";
2357
+ }[];
2358
+ readonly limit?: number;
2359
+ readonly offset?: number;
2360
+ };
2361
+
2362
+ /** The database a resolver reads through. Every query it runs carries the
2363
+ * resolver's `timeoutMs` as a `statement_timeout` and takes one of the
2364
+ * process's bounded connections. */
2365
+ export type ResolverDb<Config extends IndexerConfigTypes = GlobalConfig> = {
2366
+ /** Entities matching `where`, decoded exactly as a handler sees them. */
2367
+ readonly find: <Name extends keyof ConfigEntities<Config> & string>(
2368
+ entityName: Name,
2369
+ options?: ResolverFindOptions<
2370
+ ChainScopedEntityRow<Config, Name, ConfigEntities<Config>[Name]>
2371
+ >
2372
+ ) => Promise<
2373
+ ChainScopedEntityRow<Config, Name, ConfigEntities<Config>[Name]>[]
2374
+ >;
2375
+ /** One entity by id, or null. Not available for per-chain entities, whose
2376
+ * key is the id together with a chain — use {@link ResolverDb.find}. */
2377
+ readonly get: <
2378
+ Name extends Exclude<
2379
+ keyof ConfigEntities<Config> & string,
2380
+ PerChainEntityNames<Config>
2381
+ >
2382
+ >(
2383
+ entityName: Name,
2384
+ id: EntityId<ConfigEntities<Config>[Name]>
2385
+ ) => Promise<ConfigEntities<Config>[Name] | null>;
2386
+ /** Raw SQL, for what the loaders don't express. */
2387
+ readonly sql: ResolverSql;
2388
+ /** The Postgres schema the indexer's tables live in. Raw SQL has to qualify
2389
+ * table names with it — it is not `"public"` on a hosted deployment. */
2390
+ readonly pgSchema: string;
2391
+ /** Several queries on one connection, under one timeout. */
2392
+ readonly transaction: <T>(work: (sql: ResolverSql) => Promise<T>) => Promise<T>;
2393
+ /** How far each chain has been indexed, keyed by chain id. */
2394
+ readonly chainHeights: () => Promise<Record<string, ChainHeight>>;
2395
+ };
2396
+
2397
+ /** Arguments passed to a {@link Resolver}'s handler. */
2398
+ export type ResolverHandlerArgs<
2399
+ Args,
2400
+ Config extends IndexerConfigTypes = GlobalConfig
2401
+ > = {
2402
+ /** Coerced and validated against the declared arg schemas before the
2403
+ * resolver is reached. */
2404
+ readonly args: Args;
2405
+ readonly db: ResolverDb<Config>;
2406
+ readonly selection: ResolverSelection;
2407
+ readonly ctx: ResolverContext;
2408
+ };
2409
+
2410
+ /**
2411
+ * Declares a custom GraphQL resolver: a root query field served alongside the
2412
+ * generated entity fields, backed by TypeScript in this project.
2413
+ *
2414
+ * Arguments and result are declared with the same `S` schemas effects use, and
2415
+ * the GraphQL types are derived from them. A field is non-null unless its
2416
+ * schema is optional. Object types must be named with {@link defineType} —
2417
+ * GraphQL has no anonymous types.
2418
+ *
2419
+ * ```ts
2420
+ * export const marketsAprByPeriod = createResolver({
2421
+ * name: "marketsAprByPeriod",
2422
+ * args: { periodStart: S.int32, periodEnd: S.int32 },
2423
+ * output: S.array(defineType("MarketApr", { marketAddress: S.string })),
2424
+ * timeoutMs: 30_000,
2425
+ * handler: async ({ args, db }) => db.find("MarketInfo"),
2426
+ * });
2427
+ * ```
2428
+ */
2429
+ export function createResolver<
2430
+ // Both are schemas at runtime: `createResolver` reads `output.t` and walks
2431
+ // every argument with `toGraphQLType` while the module is being imported, so
2432
+ // anything else fails at indexer startup rather than in the editor.
2433
+ AS extends Record<string, Sury.Schema<unknown>>,
2434
+ OS extends Sury.Schema<unknown>,
2435
+ A = UnknownToOutput<AS>,
2436
+ O = UnknownToOutput<OS>,
2437
+ // A hack to enforce that the inferred return type matches the output schema.
2438
+ R extends O = O
2439
+ >(options: {
2440
+ /** The GraphQL field name. Must not collide with a generated one. */
2441
+ readonly name: string;
2442
+ /** Shown in the schema, for the people querying it. */
2443
+ readonly description?: string;
2444
+ /** Argument schemas, keyed by argument name. */
2445
+ readonly args?: AS;
2446
+ /** The result schema. Determines the field's GraphQL type. */
2447
+ readonly output: OS;
2448
+ /** Refuse the field to callers who cannot present a key from
2449
+ * `ENVIO_RESOLVERS_PRIVATE_KEYS` in the `x-envio-private-key` header. With
2450
+ * no keys configured it refuses everyone.
2451
+ *
2452
+ * This hides the answers, not the field: Hasura will not route to an action
2453
+ * it does not publish, so a private resolver is on the public schema and its
2454
+ * name, arguments and result type stay visible to anonymous introspection.
2455
+ * A name that would itself disclose something does not belong here. */
2456
+ readonly private?: boolean;
2457
+ /** @deprecated The former spelling of `private`, and identical to it. */
2458
+ readonly admin?: boolean;
2459
+ /** Refuse to answer with a 503 when the index is further behind head than
2460
+ * this. A number applies to every chain; an object of chainId to blocks
2461
+ * applies per chain and ignores the chains it does not name — which is
2462
+ * usually what you want, since a few hundred blocks is seconds on one chain
2463
+ * and hours on another. Omitted, the resolver answers whatever the index
2464
+ * currently holds. */
2465
+ readonly maxBlocksBehind?: number | Readonly<Record<number, number>>;
2466
+ /** The `statement_timeout` every query this resolver runs is bounded by.
2467
+ * Required: it is the only thing bounding a runaway query. */
2468
+ readonly timeoutMs: number;
2469
+ readonly handler: (args: ResolverHandlerArgs<A>) => Promise<R>;
2470
+ }): Resolver<A, O>;
2471
+
2472
+ /**
2473
+ * Thrown by a resolver to put a specific error on the client's response.
2474
+ *
2475
+ * Anything else a handler throws is reported as an internal failure with its
2476
+ * message withheld — a driver error can carry a connection string, and the
2477
+ * caller is the public internet — so this is how a resolver says more than
2478
+ * that. envio-serve carries the extensions through unchanged.
2479
+ *
2480
+ * ```ts
2481
+ * throw new ResolverError("Service is syncing", {
2482
+ * code: "SERVICE_UNAVAILABLE",
2483
+ * httpStatus: 503,
2484
+ * });
2485
+ * ```
2486
+ */
2487
+ export declare class ResolverError extends Error {
2488
+ constructor(
2489
+ message: string,
2490
+ options?: {
2491
+ /** Becomes `extensions.code`. Defaults to `"INTERNAL_SERVER_ERROR"`. */
2492
+ readonly code?: string;
2493
+ /** Becomes `extensions.http.status`. */
2494
+ readonly httpStatus?: number;
2495
+ /** Merged into `extensions` alongside `code`. */
2496
+ readonly extensions?: Record<string, unknown>;
2497
+ }
2498
+ );
2499
+ readonly code: string;
2500
+ readonly httpStatus?: number;
2501
+ readonly extensions?: Record<string, unknown>;
2502
+ }
2503
+
2504
+ /** Names a schema so it can appear in the GraphQL schema as an object type. */
2505
+ export function defineType<F extends Record<string, unknown>>(
2506
+ name: string,
2507
+ fields: F
2508
+ ): Sury.Schema<UnknownToOutput<F>>;
2509
+
2510
+ /**
2511
+ * Names a schema that appears in an argument.
2512
+ *
2513
+ * GraphQL keeps input and output types in a separate namespace from object
2514
+ * types, so a `where` argument is declared with this rather than with
2515
+ * {@link defineType}.
2516
+ */
2517
+ export function defineInput<F extends Record<string, unknown>>(
2518
+ name: string,
2519
+ fields: F
2520
+ ): Sury.Schema<UnknownToOutput<F>>;
2521
+
2522
+ /** Every resolver declared so far, in declaration order. */
2523
+ export function getRegisteredResolvers(): readonly Resolver<unknown, unknown>[];
2524
+
2525
+ /** The manifest and SDL for everything declared so far. */
2526
+ export function buildRegisteredManifest(): { manifest: unknown; sdl: string };
2527
+
2528
+ /** Names a schema representing a GraphQL enum. */
2529
+ export function defineEnum<const V extends readonly string[]>(
2530
+ name: string,
2531
+ values: V
2532
+ ): Sury.Schema<V[number]>;
2533
+
2534
+ /** Names a schema representing a custom GraphQL scalar, such as `BigInt`. */
2535
+ export function defineScalar<T>(
2536
+ name: string,
2537
+ schema: Sury.Schema<T>
2538
+ ): Sury.Schema<T>;
2539
+
2254
2540
  // ============== Runtime values ==============
2255
2541
 
2256
2542
  /** The indexer instance. Register handlers with `indexer.onEvent`,
package/index.js CHANGED
@@ -9,6 +9,16 @@ import { schema as bigDecimalSchema } from "./src/bindings/BigDecimal.res.mjs";
9
9
 
10
10
  export * from "./src/Envio.res.mjs";
11
11
  export { indexer, createTestIndexer } from "./src/Api.res.mjs";
12
+ export {
13
+ createResolver,
14
+ defineType,
15
+ defineInput,
16
+ defineEnum,
17
+ defineScalar,
18
+ getRegisteredResolvers,
19
+ buildRegisteredManifest,
20
+ ResolverError,
21
+ } from "./src/resolvers/index.js";
12
22
  export { default as BigDecimal } from "bignumber.js";
13
23
 
14
24
  // Important! Should match the index.d.ts file
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "envio",
3
- "version": "3.9.0",
3
+ "version": "3.10.0-resolvers.1",
4
4
  "type": "module",
5
5
  "description": "A latency and sync speed optimized, developer friendly blockchain data indexer.",
6
6
  "bin": "./bin.mjs",
@@ -41,7 +41,6 @@
41
41
  "licenses"
42
42
  ],
43
43
  "dependencies": {
44
- "@clickhouse/client": "1.17.0",
45
44
  "@elastic/ecs-pino-format": "1.4.0",
46
45
  "@fuel-ts/crypto": "0.96.1",
47
46
  "@fuel-ts/errors": "0.96.1",
@@ -69,10 +68,10 @@
69
68
  "tsx": "4.21.0"
70
69
  },
71
70
  "optionalDependencies": {
72
- "envio-linux-x64": "3.9.0",
73
- "envio-linux-x64-musl": "3.9.0",
74
- "envio-linux-arm64": "3.9.0",
75
- "envio-darwin-x64": "3.9.0",
76
- "envio-darwin-arm64": "3.9.0"
71
+ "envio-linux-x64": "3.10.0-resolvers.1",
72
+ "envio-linux-x64-musl": "3.10.0-resolvers.1",
73
+ "envio-linux-arm64": "3.10.0-resolvers.1",
74
+ "envio-darwin-x64": "3.10.0-resolvers.1",
75
+ "envio-darwin-arm64": "3.10.0-resolvers.1"
77
76
  }
78
77
  }
package/src/Batch.res CHANGED
@@ -189,6 +189,15 @@ let addReorgCheckpoints = (
189
189
  }
190
190
  }
191
191
 
192
+ // Checkpoint ids are handed out in ascending order as each chain's items are
193
+ // walked in block order, so within a chain a higher id always means a later
194
+ // block. Rollback preserves that: it deletes the chain's ids above its target
195
+ // before any higher one is allocated, so the ids the re-indexed blocks get are
196
+ // above everything the chain still holds. An isolated rollback leans on the
197
+ // invariant — it deletes by `chain_id = c AND id > target`, which is only the
198
+ // chain's stale suffix if ids and blocks agree on order within the chain. Ids
199
+ // interleave freely *across* chains, which is exactly why that predicate can't
200
+ // be the id bound alone.
192
201
  let prepareBatch = (
193
202
  ~checkpointIdBeforeBatch,
194
203
  ~chainsBeforeBatch: dict<chainBeforeBatch>,
package/src/Bin.res CHANGED
@@ -18,11 +18,13 @@ type startCmd = {
18
18
  }
19
19
  type migrateCmd = {reset: bool, config: JSON.t}
20
20
  type dropSchemaCmd = {config: JSON.t}
21
+ type resolversCmd = {mode: string, cwd: string, env: dict<JSON.t>, config: JSON.t}
21
22
 
22
23
  type command =
23
24
  | Start(startCmd)
24
25
  | Migrate(migrateCmd)
25
26
  | DropSchema(dropSchemaCmd)
27
+ | Resolvers(resolversCmd)
26
28
 
27
29
  let decodeCommand = (json: JSON.t): command => {
28
30
  let obj = switch json->JSON.Decode.object {
@@ -37,6 +39,7 @@ let decodeCommand = (json: JSON.t): command => {
37
39
  | "start" => Start(json->(Utils.magic: JSON.t => startCmd))
38
40
  | "migrate" => Migrate(json->(Utils.magic: JSON.t => migrateCmd))
39
41
  | "drop-schema" => DropSchema(json->(Utils.magic: JSON.t => dropSchemaCmd))
42
+ | "resolvers" => Resolvers(json->(Utils.magic: JSON.t => resolversCmd))
40
43
  | other => JsError.throwWithMessage(`Unknown command kind: ${other}`)
41
44
  }
42
45
  }
@@ -61,6 +64,12 @@ let run = async args => {
61
64
  Config.prime(config)
62
65
  processChdir(cwd)
63
66
  applyEnv(env)
67
+ // `envio dev` only — see ResolverProcess.startForDev. Deployed, the
68
+ // resolvers are their own service and are never started from here.
69
+ let loaded = Config.load()
70
+ if loaded.isDev {
71
+ let _ = await ResolverProcess.startForDev(~config=loaded, ~projectRoot=cwd)
72
+ }
64
73
  await Main.start(~reset)
65
74
  | Migrate({reset, config}) =>
66
75
  Config.prime(config)
@@ -68,6 +77,44 @@ let run = async args => {
68
77
  | DropSchema({config}) =>
69
78
  Config.prime(config)
70
79
  await Main.dropSchema()
80
+ | Resolvers({mode, cwd, env, config}) =>
81
+ Config.prime(config)
82
+ processChdir(cwd)
83
+ applyEnv(env)
84
+ switch mode {
85
+ | "manifest" => await ResolverProcess.writeManifest(~config=Config.load(), ~projectRoot=cwd)
86
+ | "metadata" =>
87
+ let handlerUrl = switch Env.Resolvers.publicUrl() {
88
+ | Some(url) => url
89
+ | None =>
90
+ JsError.throwWithMessage(
91
+ "ENVIO_RESOLVERS_PUBLIC_URL is not set. It is the URL Hasura posts to and is baked into every action, so the metadata cannot be printed without it.",
92
+ )
93
+ }
94
+ let metadata = await ResolverProcess.metadataJson(
95
+ ~config=Config.load(),
96
+ ~projectRoot=cwd,
97
+ ~handlerUrl,
98
+ )
99
+ // stdout, so it pipes into `hasura metadata apply` or `jq` rather
100
+ // than being buried in the log stream.
101
+ Console.log(JSON.stringify(metadata, ~space=2))
102
+ | "migrate" =>
103
+ let {applied, reasons} = await ResolverProcess.migrate(
104
+ ~config=Config.load(),
105
+ ~projectRoot=cwd,
106
+ )
107
+ Logging.info(
108
+ applied
109
+ ? `Updated Hasura to match these resolvers: ${reasons->Array.join("; ")}`
110
+ : "Hasura already matches these resolvers; nothing to update",
111
+ )
112
+ // Serving keeps the event loop alive on its own, so returning here
113
+ // leaves the process running rather than exiting.
114
+ | _ =>
115
+ let running = await ResolverProcess.serve(~config=Config.load(), ~projectRoot=cwd)
116
+ running->ResolverProcess.handleSignals
117
+ }
71
118
  }
72
119
  }
73
120
  } catch {