envio 3.5.0 → 3.5.1-svm-alpha.2

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 (59) hide show
  1. package/evm.schema.json +7 -0
  2. package/fuel.schema.json +7 -0
  3. package/index.d.ts +33 -10
  4. package/package.json +6 -6
  5. package/src/ChainFetching.res +11 -21
  6. package/src/ChainFetching.res.mjs +12 -20
  7. package/src/ChainState.res +41 -4
  8. package/src/ChainState.res.mjs +39 -5
  9. package/src/ChainState.resi +10 -1
  10. package/src/Config.res +62 -3
  11. package/src/Config.res.mjs +49 -13
  12. package/src/EffectState.res +2 -2
  13. package/src/EffectState.res.mjs +2 -2
  14. package/src/EntityTables.res +32 -0
  15. package/src/EntityTables.res.mjs +44 -0
  16. package/src/Envio.res +7 -7
  17. package/src/Envio.res.mjs +5 -6
  18. package/src/FetchState.res +38 -75
  19. package/src/FetchState.res.mjs +20 -62
  20. package/src/Hasura.res +37 -3
  21. package/src/Hasura.res.mjs +23 -5
  22. package/src/InMemoryStore.res +95 -43
  23. package/src/InMemoryStore.res.mjs +69 -38
  24. package/src/IndexerState.res +30 -24
  25. package/src/IndexerState.res.mjs +20 -35
  26. package/src/IndexerState.resi +8 -6
  27. package/src/Internal.res +29 -18
  28. package/src/Internal.res.mjs +27 -10
  29. package/src/LoadLayer.res +39 -8
  30. package/src/LoadLayer.res.mjs +36 -9
  31. package/src/LoadLayer.resi +2 -0
  32. package/src/Persistence.res +12 -1
  33. package/src/PgStorage.res +183 -37
  34. package/src/PgStorage.res.mjs +149 -34
  35. package/src/PruneStaleHistory.res +1 -0
  36. package/src/PruneStaleHistory.res.mjs +2 -1
  37. package/src/Sink.res +3 -3
  38. package/src/Sink.res.mjs +2 -2
  39. package/src/TestIndexer.res +128 -17
  40. package/src/TestIndexer.res.mjs +75 -9
  41. package/src/UserContext.res +19 -4
  42. package/src/UserContext.res.mjs +15 -9
  43. package/src/Writing.res +8 -16
  44. package/src/Writing.res.mjs +10 -10
  45. package/src/bindings/ClickHouse.res +40 -4
  46. package/src/bindings/ClickHouse.res.mjs +37 -5
  47. package/src/bindings/Vitest.res +0 -3
  48. package/src/bindings/Vitest.res.mjs +2 -12
  49. package/src/db/EntityHistory.res +64 -21
  50. package/src/db/EntityHistory.res.mjs +43 -22
  51. package/src/db/InternalTable.res.mjs +31 -31
  52. package/src/db/Table.res +24 -1
  53. package/src/db/Table.res.mjs +26 -4
  54. package/src/sources/AddressStore.res +77 -26
  55. package/src/sources/AddressStore.res.mjs +21 -10
  56. package/src/sources/SvmHyperSyncClient.res +8 -7
  57. package/src/sources/SvmHyperSyncSource.res +38 -1
  58. package/src/sources/SvmHyperSyncSource.res.mjs +24 -0
  59. package/svm.schema.json +17 -4
@@ -690,13 +690,19 @@ module OptimizedPartitions = {
690
690
  // Update density for every response, independently from whether this range
691
691
  // is valid evidence of source capacity. A cap hit is still useful density
692
692
  // evidence because it reports items returned across the scanned range.
693
- let observedEventDensity = itemsCount->Int.toFloat /. blockRange->Int.toFloat
694
- // Seed from the first observation, then smooth every later observation
695
- // with a 1:1 moving average. Keeping initialization explicit is important:
696
- // zero is valid density evidence and must participate in the next blend.
697
- let updatedEventDensity = switch p.eventDensity {
698
- | None => Some(observedEventDensity)
699
- | Some(eventDensity) => Some((eventDensity +. observedEventDensity) /. 2.)
693
+ // A response that didn't advance covers no range at all — skip it, or the
694
+ // 0/0 division would latch the moving average at NaN permanently.
695
+ let updatedEventDensity = if blockRange <= 0 {
696
+ p.eventDensity
697
+ } else {
698
+ let observedEventDensity = itemsCount->Int.toFloat /. blockRange->Int.toFloat
699
+ // Seed from the first observation, then smooth every later observation
700
+ // with a 1:1 moving average. Keeping initialization explicit is important:
701
+ // zero is valid density evidence and must participate in the next blend.
702
+ switch p.eventDensity {
703
+ | None => Some(observedEventDensity)
704
+ | Some(eventDensity) => Some((eventDensity +. observedEventDensity) /. 2.)
705
+ }
700
706
  }
701
707
 
702
708
  // Skip updating source capacity if a later response already updated it.
@@ -1473,7 +1479,7 @@ let warnRejectedRegistration = (
1473
1479
  ~params={"contractName": contractName},
1474
1480
  `Skipping contract registration: Not a valid address for this chain's ecosystem.`,
1475
1481
  )
1476
- | Added(_) | NoEvents(_) => ()
1482
+ | Added(_) => ()
1477
1483
  }
1478
1484
 
1479
1485
  /**
@@ -1678,9 +1684,9 @@ let registerDynamicContracts = (
1678
1684
  // height in here. Defaults to what the fetch state knows, which is all
1679
1685
  // there is to go on when no response is being applied.
1680
1686
  ~claimCeiling=fetchState.knownHeight,
1681
- // These are raw items which might have dynamic contracts received from contractRegister call.
1682
- // Might contain duplicates which we should filter out
1683
- items: array<Internal.item>,
1687
+ // Registrations collected from contractRegister calls, in the order the
1688
+ // handlers made them. May contain duplicates, which the store filters out.
1689
+ registrations: array<AddressStore.registration>,
1684
1690
  ) => {
1685
1691
  if fetchState.normalSelection.onEventRegistrations->Utils.Array.isEmpty {
1686
1692
  // Can the normalSelection be empty?
@@ -1689,91 +1695,42 @@ let registerDynamicContracts = (
1689
1695
  )
1690
1696
  }
1691
1697
 
1692
- // Flatten every item's dcs into one batch, remembering where each came from
1693
- // so the rejected ones can be spliced back out of their item (which is what
1694
- // keeps them from being persisted twice).
1695
- let registrations: array<AddressStore.registration> = []
1696
- let sourceDcs: array<Internal.dcs> = []
1697
- let sourceIndexes: array<int> = []
1698
- for itemIdx in 0 to items->Array.length - 1 {
1699
- switch items->Array.getUnsafe(itemIdx)->Internal.getItemDcs {
1700
- | None => ()
1701
- | Some(dcs) =>
1702
- for dcIdx in 0 to dcs->Array.length - 1 {
1703
- let dc = dcs->Array.getUnsafe(dcIdx)
1704
- registrations
1705
- ->Array.push({
1706
- address: dc.address,
1707
- contractName: dc.contractName,
1708
- registrationBlock: dc.registrationBlock,
1709
- })
1710
- ->ignore
1711
- sourceDcs->Array.push(dcs)->ignore
1712
- sourceIndexes->Array.push(dcIdx)->ignore
1713
- }
1714
- }
1715
- }
1716
-
1717
1698
  // Ids are handed out in registration order, so a cursor taken now selects
1718
1699
  // exactly what this batch adds.
1719
1700
  let idCursor = addressStore->AddressStore.nextId
1720
1701
  // The store resolves each address against both what it already holds and the
1721
1702
  // batch's own earlier entries, so two contracts claiming one address inside a
1722
- // single batch conflict the same way as across batches.
1703
+ // single batch conflict the same way as across batches. It also decides which
1704
+ // additions this chain fetches for, since it's what holds the contract list.
1723
1705
  let verdicts = addressStore->AddressStore.registerBatch(registrations)
1724
1706
 
1725
1707
  let registeringContractNames = []
1726
- let hasNoEventsUpdatesRef = ref(false)
1727
1708
  for idx in 0 to verdicts->Array.length - 1 {
1728
1709
  let registration = registrations->Array.getUnsafe(idx)
1729
1710
  let verdict = verdicts->Array.getUnsafe(idx)
1730
- let keep = switch verdict {
1731
- | Added(_) =>
1711
+ switch verdict {
1712
+ | Added({fetchable: true}) =>
1732
1713
  if !(registeringContractNames->Array.includes(registration.contractName)) {
1733
1714
  registeringContractNames->Array.push(registration.contractName)->ignore
1734
1715
  }
1735
- true
1736
- | NoEvents(_) =>
1737
- hasNoEventsUpdatesRef := true
1738
- // Persist the address to the db so a future config change that adds
1739
- // events for this contract can pick it up on restart, but skip partition
1740
- // registration since there's nothing to fetch.
1741
- warnAddressRegistration(
1742
- ~chainId=fetchState.chainId,
1743
- ~contractAddress=registration.address,
1744
- ~params={"contractName": registration.contractName},
1745
- `Persisting contract registration without fetching: Contract doesn't have any events to fetch. It'll be picked up on restart if you add events for the contract.`,
1746
- )
1747
- true
1716
+ // Nothing on this chain is fetched by address for the contract, so there's
1717
+ // no partition to build. The address is still stored and persisted, so a
1718
+ // config that later adds address-dependent events picks it up on restart.
1719
+ | Added({fetchable: false}) => ()
1748
1720
  | Conflict(_) | Duplicate(_) | Invalid =>
1749
1721
  verdict->warnRejectedRegistration(
1750
1722
  ~chainId=fetchState.chainId,
1751
1723
  ~contractAddress=registration.address,
1752
1724
  ~contractName=registration.contractName,
1753
1725
  )
1754
- false
1755
- }
1756
- if !keep {
1757
- // Mark for removal; the splice happens below, back to front, so earlier
1758
- // indexes stay valid.
1759
- sourceIndexes->Array.setUnsafe(idx, -1 - sourceIndexes->Array.getUnsafe(idx))
1760
- }
1761
- }
1762
- for idx in verdicts->Array.length - 1 downto 0 {
1763
- let marked = sourceIndexes->Array.getUnsafe(idx)
1764
- if marked < 0 {
1765
- let _ =
1766
- sourceDcs->Array.getUnsafe(idx)->Array.splice(~start=-1 - marked, ~remove=1, ~insert=[])
1767
1726
  }
1768
1727
  }
1769
1728
 
1770
- switch (registeringContractNames, hasNoEventsUpdatesRef.contents) {
1771
- // Dont update anything when everything was filtered out
1772
- | ([], false) => fetchState
1773
- // Only dcs for contracts without events. The store already tracks them so
1774
- // subsequent registrations see them, but there are no partitions to touch.
1775
- | ([], true) => fetchState
1776
- | (_, _) => {
1729
+ switch registeringContractNames {
1730
+ // Nothing to fetch: everything was rejected, or registered for contracts
1731
+ // this chain has no address-dependent events for.
1732
+ | [] => fetchState
1733
+ | _ => {
1777
1734
  let newPartitions = []
1778
1735
  let dynamicContractsRef = ref(fetchState.optimizedPartitions.dynamicContracts)
1779
1736
  let mutExistingPartitions = fetchState.optimizedPartitions.entities->Dict.valuesToArray
@@ -1835,7 +1792,11 @@ let registerDynamicContracts = (
1835
1792
  dynamicContract: Some(contractName),
1836
1793
  addresses: splitAddresses,
1837
1794
  mergeBlock: None,
1838
- mutPendingQueries: p.mutPendingQueries,
1795
+ // Must be a fresh array: sharing p's (empty) queue by
1796
+ // reference lets both split halves push and consume in
1797
+ // one array, so one half can consume the other's
1798
+ // fetched range and skip it for its own addresses.
1799
+ mutPendingQueries: [],
1839
1800
  sourceRangeCapacity: p.sourceRangeCapacity,
1840
1801
  prevSourceRangeCapacity: p.prevSourceRangeCapacity,
1841
1802
  eventDensity: p.eventDensity,
@@ -2709,7 +2670,9 @@ let make = (
2709
2670
  // contract has no address-dependent events, so a later registration of the
2710
2671
  // same address still conflicts and the address is still persisted.
2711
2672
  addressStore
2712
- ->AddressStore.registerBatch(
2673
+ // These come from the config or from a resume, so they're already stored and
2674
+ // must never be drained back into a write.
2675
+ ->AddressStore.seedBatch(
2713
2676
  addresses->Array.map((contract): AddressStore.registration => {
2714
2677
  address: contract.address,
2715
2678
  contractName: contract.contractName,
@@ -532,9 +532,14 @@ function handleQueryResponseForPartition(optimizedPartitions, p, query, knownHei
532
532
  let pendingQuery = getPendingQueryOrThrow(p, query.fromBlock);
533
533
  pendingQuery.fetchedBlock = latestFetchedBlock;
534
534
  let blockRange = (latestFetchedBlock.blockNumber - query.fromBlock | 0) + 1 | 0;
535
- let observedEventDensity = itemsCount / blockRange;
536
- let eventDensity = p.eventDensity;
537
- let updatedEventDensity = eventDensity !== undefined ? (eventDensity + observedEventDensity) / 2 : observedEventDensity;
535
+ let updatedEventDensity;
536
+ if (blockRange <= 0) {
537
+ updatedEventDensity = p.eventDensity;
538
+ } else {
539
+ let observedEventDensity = itemsCount / blockRange;
540
+ let eventDensity = p.eventDensity;
541
+ updatedEventDensity = eventDensity !== undefined ? (eventDensity + observedEventDensity) / 2 : observedEventDensity;
542
+ }
538
543
  let shouldUpdateSourceRangeCapacity = false;
539
544
  if (latestFetchedBlock.blockNumber > p.latestSourceRangeCapacityUpdateBlock) {
540
545
  let queryToBlock = query.toBlock;
@@ -1086,7 +1091,6 @@ function warnRejectedRegistration(verdict, chainId, contractAddress, contractNam
1086
1091
  }
1087
1092
  switch (verdict.TAG) {
1088
1093
  case "Added" :
1089
- case "NoEvents" :
1090
1094
  return;
1091
1095
  case "Duplicate" :
1092
1096
  let existingEffectiveStartBlock = verdict.existingEffectiveStartBlock;
@@ -1225,71 +1229,25 @@ function createPartitions(registeringSetsByContract, addressStore, dynamicContra
1225
1229
  return make(allPartitions, maxAddrInPartition, nextPartitionIndexRef.contents, dynamicContracts, clientFilteredContracts);
1226
1230
  }
1227
1231
 
1228
- function registerDynamicContracts(fetchState, addressStore, claimCeilingOpt, items) {
1232
+ function registerDynamicContracts(fetchState, addressStore, claimCeilingOpt, registrations) {
1229
1233
  let claimCeiling = claimCeilingOpt !== undefined ? claimCeilingOpt : fetchState.knownHeight;
1230
1234
  if (Utils.$$Array.isEmpty(fetchState.normalSelection.onEventRegistrations)) {
1231
1235
  Stdlib_JsError.throwWithMessage("Invalid configuration. No events to fetch for the dynamic contract registration.");
1232
1236
  }
1233
- let registrations = [];
1234
- let sourceDcs = [];
1235
- let sourceIndexes = [];
1236
- for (let itemIdx = 0, itemIdx_finish = items.length; itemIdx < itemIdx_finish; ++itemIdx) {
1237
- let dcs = items[itemIdx].dcs;
1238
- if (dcs !== undefined) {
1239
- for (let dcIdx = 0, dcIdx_finish = dcs.length; dcIdx < dcIdx_finish; ++dcIdx) {
1240
- let dc = dcs[dcIdx];
1241
- registrations.push({
1242
- address: dc.address,
1243
- contractName: dc.contractName,
1244
- registrationBlock: dc.registrationBlock
1245
- });
1246
- sourceDcs.push(dcs);
1247
- sourceIndexes.push(dcIdx);
1248
- }
1249
- }
1250
- }
1251
1237
  let idCursor = addressStore.nextId();
1252
1238
  let verdicts = AddressStore.registerBatch(addressStore, registrations);
1253
1239
  let registeringContractNames = [];
1254
- let hasNoEventsUpdatesRef = false;
1255
1240
  for (let idx = 0, idx_finish = verdicts.length; idx < idx_finish; ++idx) {
1256
1241
  let registration = registrations[idx];
1257
1242
  let verdict = verdicts[idx];
1258
- let keep;
1259
1243
  let exit = 0;
1260
- if (typeof verdict !== "object") {
1244
+ if (typeof verdict !== "object" || verdict.TAG !== "Added") {
1261
1245
  exit = 1;
1262
- } else {
1263
- switch (verdict.TAG) {
1264
- case "Added" :
1265
- if (!registeringContractNames.includes(registration.contractName)) {
1266
- registeringContractNames.push(registration.contractName);
1267
- }
1268
- keep = true;
1269
- break;
1270
- case "NoEvents" :
1271
- hasNoEventsUpdatesRef = true;
1272
- warnAddressRegistration(fetchState.chainId, registration.address, {
1273
- contractName: registration.contractName
1274
- }, `Persisting contract registration without fetching: Contract doesn't have any events to fetch. It'll be picked up on restart if you add events for the contract.`);
1275
- keep = true;
1276
- break;
1277
- default:
1278
- exit = 1;
1279
- }
1246
+ } else if (verdict.fetchable && !registeringContractNames.includes(registration.contractName)) {
1247
+ registeringContractNames.push(registration.contractName);
1280
1248
  }
1281
1249
  if (exit === 1) {
1282
1250
  warnRejectedRegistration(verdict, fetchState.chainId, registration.address, registration.contractName);
1283
- keep = false;
1284
- }
1285
- if (!keep) {
1286
- sourceIndexes[idx] = -1 - sourceIndexes[idx] | 0;
1287
- }
1288
- }
1289
- for (let idx$1 = verdicts.length - 1 | 0; idx$1 >= 0; --idx$1) {
1290
- let marked = sourceIndexes[idx$1];
1291
- if (marked < 0) {
1292
- sourceDcs[idx$1].splice(-1 - marked | 0, 1);
1293
1251
  }
1294
1252
  }
1295
1253
  if (registeringContractNames.length === 0) {
@@ -1298,12 +1256,12 @@ function registerDynamicContracts(fetchState, addressStore, claimCeilingOpt, ite
1298
1256
  let newPartitions = [];
1299
1257
  let dynamicContractsRef = fetchState.optimizedPartitions.dynamicContracts;
1300
1258
  let mutExistingPartitions = Object.values(fetchState.optimizedPartitions.entities);
1301
- for (let idx$2 = 0, idx_finish$1 = registeringContractNames.length; idx$2 < idx_finish$1; ++idx$2) {
1302
- let contractName = registeringContractNames[idx$2];
1259
+ for (let idx$1 = 0, idx_finish$1 = registeringContractNames.length; idx$1 < idx_finish$1; ++idx$1) {
1260
+ let contractName = registeringContractNames[idx$1];
1303
1261
  if (!dynamicContractsRef.has(contractName)) {
1304
1262
  dynamicContractsRef = Utils.$$Set.immutableAdd(dynamicContractsRef, contractName);
1305
- for (let idx$3 = 0, idx_finish$2 = mutExistingPartitions.length; idx$3 < idx_finish$2; ++idx$3) {
1306
- let p = mutExistingPartitions[idx$3];
1263
+ for (let idx$2 = 0, idx_finish$2 = mutExistingPartitions.length; idx$2 < idx_finish$2; ++idx$2) {
1264
+ let p = mutExistingPartitions[idx$2];
1307
1265
  let match = p.addresses.countFor(contractName);
1308
1266
  if (match !== 0 && p.selection.dependsOnAddresses && p.mergeBlock === undefined) {
1309
1267
  let allPartitionContractNames = p.addresses.contractNames();
@@ -1312,7 +1270,7 @@ function registerDynamicContracts(fetchState, addressStore, claimCeilingOpt, ite
1312
1270
  if (!isFetching) {
1313
1271
  let newPartitionId = (fetchState.optimizedPartitions.nextPartitionIndex + newPartitions.length | 0).toString();
1314
1272
  let restNames = allPartitionContractNames.filter(name => name !== contractName);
1315
- mutExistingPartitions[idx$3] = withAddresses(p, p.addresses.filterByContracts(restNames));
1273
+ mutExistingPartitions[idx$2] = withAddresses(p, p.addresses.filterByContracts(restNames));
1316
1274
  let splitAddresses = p.addresses.filterByContracts([contractName]);
1317
1275
  newPartitions.push({
1318
1276
  id: newPartitionId,
@@ -1321,7 +1279,7 @@ function registerDynamicContracts(fetchState, addressStore, claimCeilingOpt, ite
1321
1279
  addresses: splitAddresses,
1322
1280
  mergeBlock: undefined,
1323
1281
  dynamicContract: contractName,
1324
- mutPendingQueries: p.mutPendingQueries,
1282
+ mutPendingQueries: [],
1325
1283
  sourceRangeCapacity: p.sourceRangeCapacity,
1326
1284
  prevSourceRangeCapacity: p.prevSourceRangeCapacity,
1327
1285
  eventDensity: p.eventDensity,
@@ -1329,7 +1287,7 @@ function registerDynamicContracts(fetchState, addressStore, claimCeilingOpt, ite
1329
1287
  });
1330
1288
  }
1331
1289
  } else {
1332
- mutExistingPartitions[idx$3] = {
1290
+ mutExistingPartitions[idx$2] = {
1333
1291
  id: p.id,
1334
1292
  latestFetchedBlock: p.latestFetchedBlock,
1335
1293
  selection: p.selection,
@@ -1786,7 +1744,7 @@ function make$1(startBlock, endBlock, onEventRegistrations, addressStore, addres
1786
1744
  });
1787
1745
  }
1788
1746
  let normalSelection = makeSelection(normalRegistrations, true, undefined);
1789
- AddressStore.registerBatch(addressStore, addresses.map(contract => ({
1747
+ AddressStore.seedBatch(addressStore, addresses.map(contract => ({
1790
1748
  address: contract.address,
1791
1749
  contractName: contract.contractName,
1792
1750
  registrationBlock: contract.registrationBlock
package/src/Hasura.res CHANGED
@@ -281,6 +281,21 @@ let createSelectPermission = async (
281
281
  )
282
282
  }
283
283
 
284
+ // The column_mapping references columns by their db names. A reference between
285
+ // two per-chain entities is only meaningful within one chain, so the chain-id
286
+ // column joins alongside the id — without it the relationship would resolve to
287
+ // another chain's row with the same id.
288
+ let makeColumnMapping = (~relationalKey, ~isDerivedFrom, ~chainIdColumn) => {
289
+ let pairs = [
290
+ isDerivedFrom ? `"id": "${relationalKey}"` : `"${relationalKey}": "id"`,
291
+ ]
292
+ switch chainIdColumn {
293
+ | Some(column) => pairs->Array.push(`"${column}": "${column}"`)->ignore
294
+ | None => ()
295
+ }
296
+ `{${pairs->Array.joinUnsafe(", ")}}`
297
+ }
298
+
284
299
  let createEntityRelationship = async (
285
300
  ~endpoint,
286
301
  ~auth,
@@ -291,10 +306,9 @@ let createEntityRelationship = async (
291
306
  ~objectName: string,
292
307
  ~mappedEntity: string,
293
308
  ~isDerivedFrom: bool,
309
+ ~chainIdColumn: option<string>,
294
310
  ~comment: option<string>=?,
295
311
  ) => {
296
- // The column_mapping references columns by their db names
297
- let derivedFromTo = isDerivedFrom ? `"id": "${relationalKey}"` : `"${relationalKey}" : "id"`
298
312
 
299
313
  let tableJson = {
300
314
  "schema": pgSchema,
@@ -306,7 +320,9 @@ let createEntityRelationship = async (
306
320
  "schema": pgSchema,
307
321
  "name": mappedEntity,
308
322
  },
309
- "column_mapping": JSON.parseOrThrow(`{${derivedFromTo}}`),
323
+ "column_mapping": JSON.parseOrThrow(
324
+ makeColumnMapping(~relationalKey, ~isDerivedFrom, ~chainIdColumn),
325
+ ),
310
326
  },
311
327
  }->(Utils.magic: {..} => JSON.t)
312
328
 
@@ -388,9 +404,25 @@ let trackDatabase = async (
388
404
  )
389
405
  }
390
406
 
407
+ // Both sides of a relationship must be per-chain for the chain to be part of
408
+ // the join; a per-chain entity referencing a cross-chain one resolves by id
409
+ // alone. The reverse (cross-chain referencing per-chain) is rejected at
410
+ // codegen, so it can't reach here.
411
+ let chainIdColumnOf = (entityName: string) =>
412
+ userEntities
413
+ ->Array.find((e: Internal.entityConfig) => e.name === entityName)
414
+ ->Option.flatMap(e => e.table->Table.getChainIdField)
415
+ ->Option.map(Table.getPgDbFieldName)
416
+
391
417
  for i in 0 to userEntities->Array.length - 1 {
392
418
  let entityConfig = userEntities->Array.getUnsafe(i)
393
419
  let {tableName} = entityConfig.table
420
+ let ownChainIdColumn = entityConfig.table->Table.getChainIdField->Option.map(Table.getPgDbFieldName)
421
+ let sharedChainIdColumn = mappedEntity =>
422
+ switch (ownChainIdColumn, chainIdColumnOf(mappedEntity)) {
423
+ | (Some(column), Some(_)) => Some(column)
424
+ | _ => None
425
+ }
394
426
 
395
427
  //Set array relationships
396
428
  let derivedFromFields = entityConfig.table->Table.getDerivedFromFields
@@ -410,6 +442,7 @@ let trackDatabase = async (
410
442
  ~objectName=derivedFromField.fieldName,
411
443
  ~relationalKey=relationalFieldName,
412
444
  ~mappedEntity=derivedFromField.derivedFromEntity,
445
+ ~chainIdColumn=sharedChainIdColumn(derivedFromField.derivedFromEntity),
413
446
  ~comment=?derivedFromField.description,
414
447
  )
415
448
  }
@@ -428,6 +461,7 @@ let trackDatabase = async (
428
461
  ~objectName=field.fieldName,
429
462
  ~relationalKey=field->Table.getPgDbFieldName,
430
463
  ~mappedEntity=linkedEntityName,
464
+ ~chainIdColumn=sharedChainIdColumn(linkedEntityName),
431
465
  ~comment=?field.description,
432
466
  )
433
467
  }
@@ -7,6 +7,7 @@ import * as Utils from "./Utils.res.mjs";
7
7
  import * as Schema from "./db/Schema.res.mjs";
8
8
  import * as Logging from "./Logging.res.mjs";
9
9
  import * as InternalTable from "./db/InternalTable.res.mjs";
10
+ import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
10
11
  import * as S$RescriptSchema from "rescript-schema/src/S.res.mjs";
11
12
  import * as Primitive_exceptions from "@rescript/runtime/lib/es6/Primitive_exceptions.js";
12
13
 
@@ -247,8 +248,15 @@ async function createSelectPermission(endpoint, auth, tableName, pgSchema, respo
247
248
  });
248
249
  }
249
250
 
250
- async function createEntityRelationship(endpoint, auth, pgSchema, tableName, relationshipType, relationalKey, objectName, mappedEntity, isDerivedFrom, comment) {
251
- let derivedFromTo = isDerivedFrom ? `"id": "` + relationalKey + `"` : `"` + relationalKey + `" : "id"`;
251
+ function makeColumnMapping(relationalKey, isDerivedFrom, chainIdColumn) {
252
+ let pairs = [isDerivedFrom ? `"id": "` + relationalKey + `"` : `"` + relationalKey + `": "id"`];
253
+ if (chainIdColumn !== undefined) {
254
+ pairs.push(`"` + chainIdColumn + `": "` + chainIdColumn + `"`);
255
+ }
256
+ return `{` + pairs.join(", ") + `}`;
257
+ }
258
+
259
+ async function createEntityRelationship(endpoint, auth, pgSchema, tableName, relationshipType, relationalKey, objectName, mappedEntity, isDerivedFrom, chainIdColumn, comment) {
252
260
  let tableJson = {
253
261
  schema: pgSchema,
254
262
  name: tableName
@@ -259,7 +267,7 @@ async function createEntityRelationship(endpoint, auth, pgSchema, tableName, rel
259
267
  schema: pgSchema,
260
268
  name: mappedEntity
261
269
  },
262
- column_mapping: JSON.parse(`{` + derivedFromTo + `}`)
270
+ column_mapping: JSON.parse(makeColumnMapping(relationalKey, isDerivedFrom, chainIdColumn))
263
271
  }
264
272
  };
265
273
  let args = {
@@ -313,21 +321,30 @@ async function trackDatabase(endpoint, auth, pgSchema, userEntities, aggregateEn
313
321
  let tableName = tableNames[i];
314
322
  await createSelectPermission(endpoint, auth, tableName, pgSchema, responseLimit, aggregateEntities);
315
323
  }
324
+ let chainIdColumnOf = entityName => Stdlib_Option.map(Stdlib_Option.flatMap(userEntities.find(e => e.name === entityName), e => Table.getChainIdField(e.table)), Table.getPgDbFieldName);
316
325
  for (let i$1 = 0, i_finish$1 = userEntities.length; i$1 < i_finish$1; ++i$1) {
317
326
  let entityConfig = userEntities[i$1];
318
327
  let match = entityConfig.table;
319
328
  let tableName$1 = match.tableName;
329
+ let ownChainIdColumn = Stdlib_Option.map(Table.getChainIdField(entityConfig.table), Table.getPgDbFieldName);
330
+ let sharedChainIdColumn = mappedEntity => {
331
+ let match = chainIdColumnOf(mappedEntity);
332
+ if (ownChainIdColumn !== undefined && match !== undefined) {
333
+ return ownChainIdColumn;
334
+ }
335
+ };
320
336
  let derivedFromFields = Table.getDerivedFromFields(entityConfig.table);
321
337
  for (let j = 0, j_finish = derivedFromFields.length; j < j_finish; ++j) {
322
338
  let derivedFromField = derivedFromFields[j];
323
339
  let relationalFieldName = Utils.unwrapResultExn(Schema.getDerivedFromPgFieldName(schema, derivedFromField));
324
- await createEntityRelationship(endpoint, auth, pgSchema, tableName$1, "array", relationalFieldName, derivedFromField.fieldName, derivedFromField.derivedFromEntity, true, derivedFromField.description);
340
+ await createEntityRelationship(endpoint, auth, pgSchema, tableName$1, "array", relationalFieldName, derivedFromField.fieldName, derivedFromField.derivedFromEntity, true, sharedChainIdColumn(derivedFromField.derivedFromEntity), derivedFromField.description);
325
341
  }
326
342
  let linkedEntityFields = Table.getLinkedEntityFields(entityConfig.table);
327
343
  for (let j$1 = 0, j_finish$1 = linkedEntityFields.length; j$1 < j_finish$1; ++j$1) {
328
344
  let match$1 = linkedEntityFields[j$1];
345
+ let linkedEntityName = match$1[1];
329
346
  let field = match$1[0];
330
- await createEntityRelationship(endpoint, auth, pgSchema, tableName$1, "object", Table.getPgDbFieldName(field), field.fieldName, match$1[1], false, field.description);
347
+ await createEntityRelationship(endpoint, auth, pgSchema, tableName$1, "object", Table.getPgDbFieldName(field), field.fieldName, linkedEntityName, false, sharedChainIdColumn(linkedEntityName), field.description);
331
348
  }
332
349
  }
333
350
  return Logging.info("Hasura configuration completed");
@@ -346,6 +363,7 @@ export {
346
363
  makeColumnConfigs,
347
364
  trackTables,
348
365
  createSelectPermission,
366
+ makeColumnMapping,
349
367
  createEntityRelationship,
350
368
  trackDatabase,
351
369
  }