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/package.json +5 -5
- package/rescript.json +3 -0
- package/src/ErrorHandling.res +4 -5
- package/src/ErrorHandling.res.js +7 -7
- package/src/Hasura.res +139 -16
- package/src/Hasura.res.js +99 -18
- package/src/Internal.res +7 -11
- package/src/Internal.res.js +0 -11
- package/src/InternalConfig.res +20 -0
- package/src/InternalConfig.res.js +2 -0
- package/src/Js.shim.ts +11 -0
- package/src/LoadManager.res +13 -7
- package/src/LoadManager.res.js +14 -8
- package/src/Logging.res +1 -1
- package/src/Logging.res.js +2 -2
- package/src/Persistence.res +25 -33
- package/src/Persistence.res.js +18 -20
- package/src/PgStorage.res +158 -106
- package/src/PgStorage.res.js +143 -102
- package/src/Prometheus.res +2 -2
- package/src/Prometheus.res.js +2 -3
- package/src/Time.res +1 -1
- package/src/Time.res.js +1 -2
- package/src/Utils.res +7 -0
- package/src/Utils.res.js +11 -0
- package/src/bindings/Pino.res +1 -1
- package/src/bindings/Pino.res.js +2 -1
- package/src/db/EntityHistory.res +115 -50
- package/src/db/EntityHistory.res.js +43 -26
- package/src/db/InternalTable.gen.ts +43 -0
- package/src/db/InternalTable.res +392 -0
- package/src/db/InternalTable.res.js +295 -0
- package/src/sources/SourceManager.res +3 -3
- package/src/sources/SourceManager.res.js +3 -4
- package/src/vendored/Rest.res +11 -2
- package/src/vendored/Rest.res.js +44 -35
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
|
-
~
|
|
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 :=
|
|
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
|
-
|
|
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->
|
|
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 = '${
|
|
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
|
|
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
|
-
!(
|
|
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->
|
|
886
|
+
| exn => Logging.errorWithExn(exn->Utils.prettifyExn, `Failed to dump cache.`)
|
|
769
887
|
}
|
|
770
888
|
}
|
|
771
889
|
|
|
772
|
-
let
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
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
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
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
|
}
|