envio 2.27.5 → 2.28.0-alpha.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.
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,12 @@ GRANT ALL ON SCHEMA "${pgSchema}" TO public;`,
131
140
  })
132
141
  })
133
142
 
143
+ // Populate initial chain data
144
+ switch InternalTable.Chains.makeInitialValuesQuery(~pgSchema, ~chainConfigs) {
145
+ | Some(initialChainsValuesQuery) => query := query.contents ++ "\n" ++ initialChainsValuesQuery
146
+ | None => ()
147
+ }
148
+
134
149
  // Add cache row count function
135
150
  functionsQuery :=
136
151
  functionsQuery.contents ++
@@ -162,6 +177,10 @@ let makeLoadByIdsQuery = (~pgSchema, ~tableName) => {
162
177
  `SELECT * FROM "${pgSchema}"."${tableName}" WHERE id = ANY($1::text[]);`
163
178
  }
164
179
 
180
+ let makeLoadAllQuery = (~pgSchema, ~tableName) => {
181
+ `SELECT * FROM "${pgSchema}"."${tableName}";`
182
+ }
183
+
165
184
  let makeInsertUnnestSetQuery = (~pgSchema, ~table: Table.table, ~itemSchema, ~isRawEvents) => {
166
185
  let {quotedFieldNames, quotedNonPrimaryFieldNames, arrayFieldTypes} =
167
186
  table->Table.toSqlParams(~schema=itemSchema, ~pgSchema)
@@ -234,21 +253,19 @@ VALUES${placeholders.contents}` ++
234
253
  } ++ ";"
235
254
  }
236
255
 
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
256
  // Constants for chunking
247
257
  let maxItemsPerQuery = 500
248
258
 
249
259
  let makeTableBatchSetQuery = (~pgSchema, ~table: Table.table, ~itemSchema: S.t<'item>) => {
250
260
  let {dbSchema, hasArrayField} = table->Table.toSqlParams(~schema=itemSchema, ~pgSchema)
251
- let isRawEvents = table.tableName === rawEventsTableName
261
+
262
+ // Should move this to a better place
263
+ // We need it for the isRawEvents check in makeTableBatchSet
264
+ // to always apply the unnest optimization.
265
+ // This is needed, because even though it has JSON fields,
266
+ // they are always guaranteed to be an object.
267
+ // FIXME what about Fuel params?
268
+ let isRawEvents = table.tableName === InternalTable.RawEvents.table.tableName
252
269
 
253
270
  // Should experiment how much it'll affect performance
254
271
  // Although, it should be fine not to perform the validation check,
@@ -325,9 +342,7 @@ let removeInvalidUtf8InPlace = entities =>
325
342
  })
326
343
  })
327
344
 
328
- let pgErrorMessageSchema = S.object(s =>
329
- s.field("message", S.string)
330
- )
345
+ let pgErrorMessageSchema = S.object(s => s.field("message", S.string))
331
346
 
332
347
  exception PgEncodingError({table: Table.table})
333
348
 
@@ -389,7 +404,7 @@ let setOrThrow = async (sql, ~items, ~table: Table.table, ~itemSchema, ~pgSchema
389
404
  raise(
390
405
  Persistence.StorageError({
391
406
  message: `Failed to insert items into table "${table.tableName}"`,
392
- reason: exn->Internal.prettifyExn,
407
+ reason: exn->Utils.prettifyExn,
393
408
  }),
394
409
  )
395
410
  }
@@ -403,8 +418,7 @@ let setEntityHistoryOrThrow = (
403
418
  ~shouldCopyCurrentEntity=?,
404
419
  ~shouldRemoveInvalidUtf8=false,
405
420
  ) => {
406
- rows
407
- ->Belt.Array.map(historyRow => {
421
+ rows->Belt.Array.map(historyRow => {
408
422
  let row = historyRow->S.reverseConvertToJsonOrThrow(entityHistory.schema)
409
423
  if shouldRemoveInvalidUtf8 {
410
424
  [row]->removeInvalidUtf8InPlace
@@ -420,10 +434,19 @@ let setEntityHistoryOrThrow = (
420
434
  !containsRollbackDiffChange
421
435
  }
422
436
  },
423
- )
437
+ )->Promise.catch(exn => {
438
+ let reason = exn->Utils.prettifyExn
439
+ let detail = %raw(`reason?.detail || ""`)
440
+ raise(
441
+ Persistence.StorageError({
442
+ message: `Failed to insert history item into table "${entityHistory.table.tableName}".${detail !== ""
443
+ ? ` Details: ${detail}`
444
+ : ""}`,
445
+ reason,
446
+ }),
447
+ )
448
+ })
424
449
  })
425
- ->Promise.all
426
- ->(Utils.magic: promise<array<unit>> => promise<unit>)
427
450
  }
428
451
 
429
452
  type schemaTableName = {
@@ -541,12 +564,95 @@ let make = (
541
564
  let isInitialized = async () => {
542
565
  let envioTables =
543
566
  await sql->Postgres.unsafe(
544
- `SELECT table_schema FROM information_schema.tables WHERE table_schema = '${pgSchema}' AND table_name = '${eventSyncStateTableName}';`,
567
+ `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}';`,
545
568
  )
546
569
  envioTables->Utils.Array.notEmpty
547
570
  }
548
571
 
549
- let initialize = async (~entities=[], ~generalTables=[], ~enums=[]) => {
572
+ let restoreEffectCache = async (~withUpload) => {
573
+ if withUpload {
574
+ // Try to restore cache tables from binary files
575
+ let nothingToUploadErrorMessage = "Nothing to upload."
576
+
577
+ switch await Promise.all2((
578
+ NodeJs.Fs.Promises.readdir(cacheDirPath)
579
+ ->Promise.thenResolve(e => Ok(e))
580
+ ->Promise.catch(_ => Promise.resolve(Error(nothingToUploadErrorMessage))),
581
+ getConnectedPsqlExec(~pgUser, ~pgHost, ~pgDatabase, ~pgPort),
582
+ )) {
583
+ | (Ok(entries), Ok(psqlExec)) => {
584
+ let cacheFiles = entries->Js.Array2.filter(entry => {
585
+ entry->Js.String2.endsWith(".tsv")
586
+ })
587
+
588
+ let _ =
589
+ await cacheFiles
590
+ ->Js.Array2.map(entry => {
591
+ let effectName = entry->Js.String2.slice(~from=0, ~to_=-4) // Remove .tsv extension
592
+ let table = Internal.makeCacheTable(~effectName)
593
+
594
+ sql
595
+ ->Postgres.unsafe(makeCreateTableQuery(table, ~pgSchema))
596
+ ->Promise.then(() => {
597
+ let inputFile = NodeJs.Path.join(cacheDirPath, entry)->NodeJs.Path.toString
598
+
599
+ let command = `${psqlExec} -c 'COPY "${pgSchema}"."${table.tableName}" FROM STDIN WITH (FORMAT text, HEADER);' < ${inputFile}`
600
+
601
+ Promise.make(
602
+ (resolve, reject) => {
603
+ NodeJs.ChildProcess.execWithOptions(
604
+ command,
605
+ psqlExecOptions,
606
+ (~error, ~stdout, ~stderr as _) => {
607
+ switch error {
608
+ | Value(error) => reject(error)
609
+ | Null => resolve(stdout)
610
+ }
611
+ },
612
+ )
613
+ },
614
+ )
615
+ })
616
+ })
617
+ ->Promise.all
618
+
619
+ Logging.info("Successfully uploaded cache.")
620
+ }
621
+ | (Error(message), _)
622
+ | (_, Error(message)) =>
623
+ if message === nothingToUploadErrorMessage {
624
+ Logging.info("No cache found to upload.")
625
+ } else {
626
+ Logging.error(`Failed to upload cache, continuing without it. ${message}`)
627
+ }
628
+ }
629
+ }
630
+
631
+ let cacheTableInfo: array<schemaCacheTableInfo> =
632
+ await sql->Postgres.unsafe(makeSchemaCacheTableInfoQuery(~pgSchema))
633
+
634
+ if withUpload && cacheTableInfo->Utils.Array.notEmpty {
635
+ // Integration with other tools like Hasura
636
+ switch onNewTables {
637
+ | Some(onNewTables) =>
638
+ await onNewTables(
639
+ ~tableNames=cacheTableInfo->Js.Array2.map(info => {
640
+ info.tableName
641
+ }),
642
+ )
643
+ | None => ()
644
+ }
645
+ }
646
+
647
+ let cache = Js.Dict.empty()
648
+ cacheTableInfo->Js.Array2.forEach(({tableName, count}) => {
649
+ let effectName = tableName->Js.String2.sliceToEnd(~from=cacheTablePrefixLength)
650
+ cache->Js.Dict.set(effectName, ({effectName, count}: Persistence.effectCacheRecord))
651
+ })
652
+ cache
653
+ }
654
+
655
+ let initialize = async (~chainConfigs=[], ~entities=[], ~enums=[]): Persistence.initialState => {
550
656
  let schemaTableNames: array<schemaTableName> =
551
657
  await sql->Postgres.unsafe(makeSchemaTableNamesQuery(~pgSchema))
552
658
 
@@ -559,7 +665,11 @@ let make = (
559
665
  schemaTableNames->Utils.Array.notEmpty &&
560
666
  // Otherwise should throw if there's a table, but no envio specific one
561
667
  // This means that the schema is used for something else than envio.
562
- !(schemaTableNames->Js.Array2.some(table => table.tableName === eventSyncStateTableName))
668
+ !(
669
+ schemaTableNames->Js.Array2.some(table =>
670
+ table.tableName === InternalTable.EventSyncState.table.tableName
671
+ )
672
+ )
563
673
  ) {
564
674
  Js.Exn.raiseError(
565
675
  `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.`,
@@ -569,9 +679,9 @@ let make = (
569
679
  let queries = makeInitializeTransaction(
570
680
  ~pgSchema,
571
681
  ~pgUser,
572
- ~generalTables,
573
682
  ~entities,
574
683
  ~enums,
684
+ ~chainConfigs,
575
685
  ~isEmptyPgSchema=schemaTableNames->Utils.Array.isEmpty,
576
686
  )
577
687
  // Execute all queries within a single transaction for integrity
@@ -579,11 +689,19 @@ let make = (
579
689
  queries->Js.Array2.map(query => sql->Postgres.unsafe(query))
580
690
  })
581
691
 
692
+ let cache = await restoreEffectCache(~withUpload=true)
693
+
582
694
  // Integration with other tools like Hasura
583
695
  switch onInitialize {
584
696
  | Some(onInitialize) => await onInitialize()
585
697
  | None => ()
586
698
  }
699
+
700
+ {
701
+ cleanRun: true,
702
+ cache,
703
+ chains: chainConfigs->Js.Array2.map(InternalTable.Chains.initialFromConfig),
704
+ }
587
705
  }
588
706
 
589
707
  let loadByIdsOrThrow = async (~ids, ~table: Table.table, ~rowsSchema) => {
@@ -765,101 +883,35 @@ let make = (
765
883
  }
766
884
  }
767
885
  } catch {
768
- | exn => Logging.errorWithExn(exn->Internal.prettifyExn, `Failed to dump cache.`)
886
+ | exn => Logging.errorWithExn(exn->Utils.prettifyExn, `Failed to dump cache.`)
769
887
  }
770
888
  }
771
889
 
772
- let restoreEffectCache = async (~withUpload) => {
773
- if withUpload {
774
- // Try to restore cache tables from binary files
775
- let nothingToUploadErrorMessage = "Nothing to upload."
776
-
777
- switch await Promise.all2((
778
- NodeJs.Fs.Promises.readdir(cacheDirPath)
779
- ->Promise.thenResolve(e => Ok(e))
780
- ->Promise.catch(_ => Promise.resolve(Error(nothingToUploadErrorMessage))),
781
- getConnectedPsqlExec(~pgUser, ~pgHost, ~pgDatabase, ~pgPort),
782
- )) {
783
- | (Ok(entries), Ok(psqlExec)) => {
784
- let cacheFiles = entries->Js.Array2.filter(entry => {
785
- entry->Js.String2.endsWith(".tsv")
786
- })
787
-
788
- let _ =
789
- await cacheFiles
790
- ->Js.Array2.map(entry => {
791
- let effectName = entry->Js.String2.slice(~from=0, ~to_=-4) // Remove .tsv extension
792
- let table = Internal.makeCacheTable(~effectName)
793
-
794
- sql
795
- ->Postgres.unsafe(makeCreateTableQuery(table, ~pgSchema))
796
- ->Promise.then(() => {
797
- let inputFile = NodeJs.Path.join(cacheDirPath, entry)->NodeJs.Path.toString
798
-
799
- let command = `${psqlExec} -c 'COPY "${pgSchema}"."${table.tableName}" FROM STDIN WITH (FORMAT text, HEADER);' < ${inputFile}`
800
-
801
- Promise.make(
802
- (resolve, reject) => {
803
- NodeJs.ChildProcess.execWithOptions(
804
- command,
805
- psqlExecOptions,
806
- (~error, ~stdout, ~stderr as _) => {
807
- switch error {
808
- | Value(error) => reject(error)
809
- | Null => resolve(stdout)
810
- }
811
- },
812
- )
813
- },
814
- )
815
- })
816
- })
817
- ->Promise.all
818
-
819
- Logging.info("Successfully uploaded cache.")
820
- }
821
- | (Error(message), _)
822
- | (_, Error(message)) =>
823
- if message === nothingToUploadErrorMessage {
824
- Logging.info("No cache found to upload.")
825
- } else {
826
- Logging.error(`Failed to upload cache, continuing without it. ${message}`)
827
- }
828
- }
829
- }
830
-
831
- let cacheTableInfo: array<schemaCacheTableInfo> =
832
- await sql->Postgres.unsafe(makeSchemaCacheTableInfoQuery(~pgSchema))
890
+ let loadInitialState = async (): Persistence.initialState => {
891
+ let (cache, chains) = await Promise.all2((
892
+ restoreEffectCache(~withUpload=false),
893
+ sql
894
+ ->Postgres.unsafe(
895
+ makeLoadAllQuery(~pgSchema, ~tableName=InternalTable.Chains.table.tableName),
896
+ )
897
+ ->(Utils.magic: promise<array<unknown>> => promise<array<InternalTable.Chains.t>>),
898
+ ))
833
899
 
834
- if withUpload && cacheTableInfo->Utils.Array.notEmpty {
835
- // Integration with other tools like Hasura
836
- switch onNewTables {
837
- | Some(onNewTables) =>
838
- await onNewTables(
839
- ~tableNames=cacheTableInfo->Js.Array2.map(info => {
840
- info.tableName
841
- }),
842
- )
843
- | None => ()
844
- }
900
+ {
901
+ cleanRun: false,
902
+ cache,
903
+ chains,
845
904
  }
846
-
847
- cacheTableInfo->Js.Array2.map((info): Persistence.effectCacheRecord => {
848
- {
849
- effectName: info.tableName->Js.String2.sliceToEnd(~from=cacheTablePrefixLength),
850
- count: info.count,
851
- }
852
- })
853
905
  }
854
906
 
855
907
  {
856
908
  isInitialized,
857
909
  initialize,
910
+ loadInitialState,
858
911
  loadByFieldOrThrow,
859
912
  loadByIdsOrThrow,
860
913
  setOrThrow,
861
914
  setEffectCacheOrThrow,
862
915
  dumpEffectCache,
863
- restoreEffectCache,
864
916
  }
865
917
  }