envio 2.27.6 → 2.28.0-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.
package/src/PgStorage.res CHANGED
@@ -57,11 +57,19 @@ let makeCreateTableQuery = (table: Table.table, ~pgSchema) => {
57
57
  let makeInitializeTransaction = (
58
58
  ~pgSchema,
59
59
  ~pgUser,
60
- ~generalTables=[],
60
+ ~chainConfigs=[],
61
61
  ~entities=[],
62
62
  ~enums=[],
63
63
  ~isEmptyPgSchema=false,
64
64
  ) => {
65
+ let generalTables = [
66
+ InternalTable.EventSyncState.table,
67
+ InternalTable.Chains.table,
68
+ InternalTable.PersistedState.table,
69
+ InternalTable.EndOfBlockRangeScannedData.table,
70
+ InternalTable.RawEvents.table,
71
+ ]
72
+
65
73
  let allTables = generalTables->Array.copy
66
74
  let allEntityTables = []
67
75
  entities->Js.Array2.forEach((entity: Internal.entityConfig) => {
@@ -113,7 +121,8 @@ GRANT ALL ON SCHEMA "${pgSchema}" TO public;`,
113
121
 
114
122
  // Add derived indices
115
123
  entities->Js.Array2.forEach((entity: Internal.entityConfig) => {
116
- functionsQuery := functionsQuery.contents ++ "\n" ++ entity.entityHistory.createInsertFnQuery
124
+ functionsQuery :=
125
+ functionsQuery.contents ++ "\n" ++ entity.entityHistory.makeInsertFnQuery(~pgSchema)
117
126
 
118
127
  entity.table
119
128
  ->Table.getDerivedFromFields
@@ -131,6 +140,16 @@ GRANT ALL ON SCHEMA "${pgSchema}" TO public;`,
131
140
  })
132
141
  })
133
142
 
143
+ // Create views for Hasura integration
144
+ query := query.contents ++ "\n" ++ InternalTable.Views.makeMetaViewQuery(~pgSchema)
145
+ query := query.contents ++ "\n" ++ InternalTable.Views.makeChainMetadataViewQuery(~pgSchema)
146
+
147
+ // Populate initial chain data
148
+ switch InternalTable.Chains.makeInitialValuesQuery(~pgSchema, ~chainConfigs) {
149
+ | Some(initialChainsValuesQuery) => query := query.contents ++ "\n" ++ initialChainsValuesQuery
150
+ | None => ()
151
+ }
152
+
134
153
  // Add cache row count function
135
154
  functionsQuery :=
136
155
  functionsQuery.contents ++
@@ -162,6 +181,10 @@ let makeLoadByIdsQuery = (~pgSchema, ~tableName) => {
162
181
  `SELECT * FROM "${pgSchema}"."${tableName}" WHERE id = ANY($1::text[]);`
163
182
  }
164
183
 
184
+ let makeLoadAllQuery = (~pgSchema, ~tableName) => {
185
+ `SELECT * FROM "${pgSchema}"."${tableName}";`
186
+ }
187
+
165
188
  let makeInsertUnnestSetQuery = (~pgSchema, ~table: Table.table, ~itemSchema, ~isRawEvents) => {
166
189
  let {quotedFieldNames, quotedNonPrimaryFieldNames, arrayFieldTypes} =
167
190
  table->Table.toSqlParams(~schema=itemSchema, ~pgSchema)
@@ -234,21 +257,19 @@ VALUES${placeholders.contents}` ++
234
257
  } ++ ";"
235
258
  }
236
259
 
237
- // Should move this to a better place
238
- // We need it for the isRawEvents check in makeTableBatchSet
239
- // to always apply the unnest optimization.
240
- // This is needed, because even though it has JSON fields,
241
- // they are always guaranteed to be an object.
242
- // FIXME what about Fuel params?
243
- let rawEventsTableName = "raw_events"
244
- let eventSyncStateTableName = "event_sync_state"
245
-
246
260
  // Constants for chunking
247
261
  let maxItemsPerQuery = 500
248
262
 
249
263
  let makeTableBatchSetQuery = (~pgSchema, ~table: Table.table, ~itemSchema: S.t<'item>) => {
250
264
  let {dbSchema, hasArrayField} = table->Table.toSqlParams(~schema=itemSchema, ~pgSchema)
251
- let isRawEvents = table.tableName === rawEventsTableName
265
+
266
+ // Should move this to a better place
267
+ // We need it for the isRawEvents check in makeTableBatchSet
268
+ // to always apply the unnest optimization.
269
+ // This is needed, because even though it has JSON fields,
270
+ // they are always guaranteed to be an object.
271
+ // FIXME what about Fuel params?
272
+ let isRawEvents = table.tableName === InternalTable.RawEvents.table.tableName
252
273
 
253
274
  // Should experiment how much it'll affect performance
254
275
  // Although, it should be fine not to perform the validation check,
@@ -401,8 +422,7 @@ let setEntityHistoryOrThrow = (
401
422
  ~shouldCopyCurrentEntity=?,
402
423
  ~shouldRemoveInvalidUtf8=false,
403
424
  ) => {
404
- rows
405
- ->Belt.Array.map(historyRow => {
425
+ rows->Belt.Array.map(historyRow => {
406
426
  let row = historyRow->S.reverseConvertToJsonOrThrow(entityHistory.schema)
407
427
  if shouldRemoveInvalidUtf8 {
408
428
  [row]->removeInvalidUtf8InPlace
@@ -418,10 +438,19 @@ let setEntityHistoryOrThrow = (
418
438
  !containsRollbackDiffChange
419
439
  }
420
440
  },
421
- )
441
+ )->Promise.catch(exn => {
442
+ let reason = exn->Utils.prettifyExn
443
+ let detail = %raw(`reason?.detail || ""`)
444
+ raise(
445
+ Persistence.StorageError({
446
+ message: `Failed to insert history item into table "${entityHistory.table.tableName}".${detail !== ""
447
+ ? ` Details: ${detail}`
448
+ : ""}`,
449
+ reason,
450
+ }),
451
+ )
452
+ })
422
453
  })
423
- ->Promise.all
424
- ->(Utils.magic: promise<array<unit>> => promise<unit>)
425
454
  }
426
455
 
427
456
  type schemaTableName = {
@@ -539,12 +568,95 @@ let make = (
539
568
  let isInitialized = async () => {
540
569
  let envioTables =
541
570
  await sql->Postgres.unsafe(
542
- `SELECT table_schema FROM information_schema.tables WHERE table_schema = '${pgSchema}' AND table_name = '${eventSyncStateTableName}';`,
571
+ `SELECT table_schema FROM information_schema.tables WHERE table_schema = '${pgSchema}' AND table_name = '${InternalTable.EventSyncState.table.tableName}' OR table_name = '${InternalTable.Chains.table.tableName}';`,
543
572
  )
544
573
  envioTables->Utils.Array.notEmpty
545
574
  }
546
575
 
547
- let initialize = async (~entities=[], ~generalTables=[], ~enums=[]) => {
576
+ let restoreEffectCache = async (~withUpload) => {
577
+ if withUpload {
578
+ // Try to restore cache tables from binary files
579
+ let nothingToUploadErrorMessage = "Nothing to upload."
580
+
581
+ switch await Promise.all2((
582
+ NodeJs.Fs.Promises.readdir(cacheDirPath)
583
+ ->Promise.thenResolve(e => Ok(e))
584
+ ->Promise.catch(_ => Promise.resolve(Error(nothingToUploadErrorMessage))),
585
+ getConnectedPsqlExec(~pgUser, ~pgHost, ~pgDatabase, ~pgPort),
586
+ )) {
587
+ | (Ok(entries), Ok(psqlExec)) => {
588
+ let cacheFiles = entries->Js.Array2.filter(entry => {
589
+ entry->Js.String2.endsWith(".tsv")
590
+ })
591
+
592
+ let _ =
593
+ await cacheFiles
594
+ ->Js.Array2.map(entry => {
595
+ let effectName = entry->Js.String2.slice(~from=0, ~to_=-4) // Remove .tsv extension
596
+ let table = Internal.makeCacheTable(~effectName)
597
+
598
+ sql
599
+ ->Postgres.unsafe(makeCreateTableQuery(table, ~pgSchema))
600
+ ->Promise.then(() => {
601
+ let inputFile = NodeJs.Path.join(cacheDirPath, entry)->NodeJs.Path.toString
602
+
603
+ let command = `${psqlExec} -c 'COPY "${pgSchema}"."${table.tableName}" FROM STDIN WITH (FORMAT text, HEADER);' < ${inputFile}`
604
+
605
+ Promise.make(
606
+ (resolve, reject) => {
607
+ NodeJs.ChildProcess.execWithOptions(
608
+ command,
609
+ psqlExecOptions,
610
+ (~error, ~stdout, ~stderr as _) => {
611
+ switch error {
612
+ | Value(error) => reject(error)
613
+ | Null => resolve(stdout)
614
+ }
615
+ },
616
+ )
617
+ },
618
+ )
619
+ })
620
+ })
621
+ ->Promise.all
622
+
623
+ Logging.info("Successfully uploaded cache.")
624
+ }
625
+ | (Error(message), _)
626
+ | (_, Error(message)) =>
627
+ if message === nothingToUploadErrorMessage {
628
+ Logging.info("No cache found to upload.")
629
+ } else {
630
+ Logging.error(`Failed to upload cache, continuing without it. ${message}`)
631
+ }
632
+ }
633
+ }
634
+
635
+ let cacheTableInfo: array<schemaCacheTableInfo> =
636
+ await sql->Postgres.unsafe(makeSchemaCacheTableInfoQuery(~pgSchema))
637
+
638
+ if withUpload && cacheTableInfo->Utils.Array.notEmpty {
639
+ // Integration with other tools like Hasura
640
+ switch onNewTables {
641
+ | Some(onNewTables) =>
642
+ await onNewTables(
643
+ ~tableNames=cacheTableInfo->Js.Array2.map(info => {
644
+ info.tableName
645
+ }),
646
+ )
647
+ | None => ()
648
+ }
649
+ }
650
+
651
+ let cache = Js.Dict.empty()
652
+ cacheTableInfo->Js.Array2.forEach(({tableName, count}) => {
653
+ let effectName = tableName->Js.String2.sliceToEnd(~from=cacheTablePrefixLength)
654
+ cache->Js.Dict.set(effectName, ({effectName, count}: Persistence.effectCacheRecord))
655
+ })
656
+ cache
657
+ }
658
+
659
+ let initialize = async (~chainConfigs=[], ~entities=[], ~enums=[]): Persistence.initialState => {
548
660
  let schemaTableNames: array<schemaTableName> =
549
661
  await sql->Postgres.unsafe(makeSchemaTableNamesQuery(~pgSchema))
550
662
 
@@ -557,7 +669,11 @@ let make = (
557
669
  schemaTableNames->Utils.Array.notEmpty &&
558
670
  // Otherwise should throw if there's a table, but no envio specific one
559
671
  // This means that the schema is used for something else than envio.
560
- !(schemaTableNames->Js.Array2.some(table => table.tableName === eventSyncStateTableName))
672
+ !(
673
+ schemaTableNames->Js.Array2.some(table =>
674
+ table.tableName === InternalTable.EventSyncState.table.tableName
675
+ )
676
+ )
561
677
  ) {
562
678
  Js.Exn.raiseError(
563
679
  `Cannot run Envio migrations on PostgreSQL schema "${pgSchema}" because it contains non-Envio tables. Running migrations would delete all data in this schema.\n\nTo resolve this:\n1. If you want to use this schema, first backup any important data, then drop it with: "pnpm envio local db-migrate down"\n2. Or specify a different schema name by setting the "ENVIO_PG_PUBLIC_SCHEMA" environment variable\n3. Or manually drop the schema in your database if you're certain the data is not needed.`,
@@ -567,21 +683,31 @@ let make = (
567
683
  let queries = makeInitializeTransaction(
568
684
  ~pgSchema,
569
685
  ~pgUser,
570
- ~generalTables,
571
686
  ~entities,
572
687
  ~enums,
688
+ ~chainConfigs,
573
689
  ~isEmptyPgSchema=schemaTableNames->Utils.Array.isEmpty,
574
690
  )
575
691
  // Execute all queries within a single transaction for integrity
576
692
  let _ = await sql->Postgres.beginSql(sql => {
577
- queries->Js.Array2.map(query => sql->Postgres.unsafe(query))
693
+ // Promise.all might be not safe to use here,
694
+ // but it's just how it worked before.
695
+ Promise.all(queries->Js.Array2.map(query => sql->Postgres.unsafe(query)))
578
696
  })
579
697
 
698
+ let cache = await restoreEffectCache(~withUpload=true)
699
+
580
700
  // Integration with other tools like Hasura
581
701
  switch onInitialize {
582
702
  | Some(onInitialize) => await onInitialize()
583
703
  | None => ()
584
704
  }
705
+
706
+ {
707
+ cleanRun: true,
708
+ cache,
709
+ chains: chainConfigs->Js.Array2.map(InternalTable.Chains.initialFromConfig),
710
+ }
585
711
  }
586
712
 
587
713
  let loadByIdsOrThrow = async (~ids, ~table: Table.table, ~rowsSchema) => {
@@ -767,97 +893,31 @@ let make = (
767
893
  }
768
894
  }
769
895
 
770
- let restoreEffectCache = async (~withUpload) => {
771
- if withUpload {
772
- // Try to restore cache tables from binary files
773
- let nothingToUploadErrorMessage = "Nothing to upload."
774
-
775
- switch await Promise.all2((
776
- NodeJs.Fs.Promises.readdir(cacheDirPath)
777
- ->Promise.thenResolve(e => Ok(e))
778
- ->Promise.catch(_ => Promise.resolve(Error(nothingToUploadErrorMessage))),
779
- getConnectedPsqlExec(~pgUser, ~pgHost, ~pgDatabase, ~pgPort),
780
- )) {
781
- | (Ok(entries), Ok(psqlExec)) => {
782
- let cacheFiles = entries->Js.Array2.filter(entry => {
783
- entry->Js.String2.endsWith(".tsv")
784
- })
785
-
786
- let _ =
787
- await cacheFiles
788
- ->Js.Array2.map(entry => {
789
- let effectName = entry->Js.String2.slice(~from=0, ~to_=-4) // Remove .tsv extension
790
- let table = Internal.makeCacheTable(~effectName)
791
-
792
- sql
793
- ->Postgres.unsafe(makeCreateTableQuery(table, ~pgSchema))
794
- ->Promise.then(() => {
795
- let inputFile = NodeJs.Path.join(cacheDirPath, entry)->NodeJs.Path.toString
796
-
797
- let command = `${psqlExec} -c 'COPY "${pgSchema}"."${table.tableName}" FROM STDIN WITH (FORMAT text, HEADER);' < ${inputFile}`
798
-
799
- Promise.make(
800
- (resolve, reject) => {
801
- NodeJs.ChildProcess.execWithOptions(
802
- command,
803
- psqlExecOptions,
804
- (~error, ~stdout, ~stderr as _) => {
805
- switch error {
806
- | Value(error) => reject(error)
807
- | Null => resolve(stdout)
808
- }
809
- },
810
- )
811
- },
812
- )
813
- })
814
- })
815
- ->Promise.all
816
-
817
- Logging.info("Successfully uploaded cache.")
818
- }
819
- | (Error(message), _)
820
- | (_, Error(message)) =>
821
- if message === nothingToUploadErrorMessage {
822
- Logging.info("No cache found to upload.")
823
- } else {
824
- Logging.error(`Failed to upload cache, continuing without it. ${message}`)
825
- }
826
- }
827
- }
828
-
829
- let cacheTableInfo: array<schemaCacheTableInfo> =
830
- await sql->Postgres.unsafe(makeSchemaCacheTableInfoQuery(~pgSchema))
896
+ let loadInitialState = async (): Persistence.initialState => {
897
+ let (cache, chains) = await Promise.all2((
898
+ restoreEffectCache(~withUpload=false),
899
+ sql
900
+ ->Postgres.unsafe(
901
+ makeLoadAllQuery(~pgSchema, ~tableName=InternalTable.Chains.table.tableName),
902
+ )
903
+ ->(Utils.magic: promise<array<unknown>> => promise<array<InternalTable.Chains.t>>),
904
+ ))
831
905
 
832
- if withUpload && cacheTableInfo->Utils.Array.notEmpty {
833
- // Integration with other tools like Hasura
834
- switch onNewTables {
835
- | Some(onNewTables) =>
836
- await onNewTables(
837
- ~tableNames=cacheTableInfo->Js.Array2.map(info => {
838
- info.tableName
839
- }),
840
- )
841
- | None => ()
842
- }
906
+ {
907
+ cleanRun: false,
908
+ cache,
909
+ chains,
843
910
  }
844
-
845
- cacheTableInfo->Js.Array2.map((info): Persistence.effectCacheRecord => {
846
- {
847
- effectName: info.tableName->Js.String2.sliceToEnd(~from=cacheTablePrefixLength),
848
- count: info.count,
849
- }
850
- })
851
911
  }
852
912
 
853
913
  {
854
914
  isInitialized,
855
915
  initialize,
916
+ loadInitialState,
856
917
  loadByFieldOrThrow,
857
918
  loadByIdsOrThrow,
858
919
  setOrThrow,
859
920
  setEffectCacheOrThrow,
860
921
  dumpEffectCache,
861
- restoreEffectCache,
862
922
  }
863
923
  }