envio 3.1.1 → 3.2.0
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/evm.schema.json +83 -11
- package/fuel.schema.json +83 -11
- package/index.d.ts +184 -3
- package/package.json +6 -6
- package/src/Batch.res +2 -2
- package/src/ChainFetcher.res +27 -3
- package/src/ChainFetcher.res.mjs +17 -3
- package/src/ChainManager.res +163 -0
- package/src/ChainManager.res.mjs +136 -0
- package/src/Config.res +213 -30
- package/src/Config.res.mjs +102 -41
- package/src/Core.res +16 -10
- package/src/Ecosystem.res +0 -3
- package/src/Env.res +2 -2
- package/src/Env.res.mjs +2 -2
- package/src/Envio.res +101 -2
- package/src/Envio.res.mjs +2 -3
- package/src/EventConfigBuilder.res +52 -0
- package/src/EventConfigBuilder.res.mjs +32 -0
- package/src/EventUtils.res +2 -2
- package/src/FetchState.res +126 -71
- package/src/FetchState.res.mjs +73 -51
- package/src/GlobalState.res +219 -363
- package/src/GlobalState.res.mjs +314 -491
- package/src/GlobalStateManager.res +49 -59
- package/src/GlobalStateManager.res.mjs +5 -4
- package/src/GlobalStateManager.resi +1 -1
- package/src/HandlerLoader.res +12 -1
- package/src/HandlerLoader.res.mjs +6 -1
- package/src/HandlerRegister.res +9 -9
- package/src/HandlerRegister.res.mjs +9 -9
- package/src/Hasura.res +102 -32
- package/src/Hasura.res.mjs +88 -34
- package/src/InMemoryStore.res +10 -1
- package/src/InMemoryStore.res.mjs +4 -1
- package/src/InMemoryTable.res +83 -136
- package/src/InMemoryTable.res.mjs +57 -86
- package/src/Internal.res +54 -5
- package/src/Internal.res.mjs +2 -8
- package/src/LazyLoader.res +2 -2
- package/src/LazyLoader.res.mjs +3 -3
- package/src/LoadLayer.res +47 -60
- package/src/LoadLayer.res.mjs +28 -50
- package/src/LoadLayer.resi +2 -5
- package/src/LogSelection.res +4 -4
- package/src/LogSelection.res.mjs +5 -7
- package/src/Logging.res +1 -1
- package/src/Main.res +61 -2
- package/src/Main.res.mjs +37 -1
- package/src/Persistence.res +3 -16
- package/src/PgStorage.res +125 -114
- package/src/PgStorage.res.mjs +112 -95
- package/src/Ports.res +5 -0
- package/src/Ports.res.mjs +9 -0
- package/src/Prometheus.res +3 -3
- package/src/Prometheus.res.mjs +4 -4
- package/src/ReorgDetection.res +4 -4
- package/src/ReorgDetection.res.mjs +4 -5
- package/src/SafeCheckpointTracking.res +16 -16
- package/src/SafeCheckpointTracking.res.mjs +2 -2
- package/src/SimulateItems.res +10 -14
- package/src/SimulateItems.res.mjs +5 -2
- package/src/Sink.res +1 -1
- package/src/Sink.res.mjs +1 -2
- package/src/SvmTypes.res +9 -0
- package/src/SvmTypes.res.mjs +14 -0
- package/src/TestIndexer.res +17 -57
- package/src/TestIndexer.res.mjs +14 -48
- package/src/TestIndexerProxyStorage.res +23 -23
- package/src/TestIndexerProxyStorage.res.mjs +12 -15
- package/src/Throttler.res +2 -2
- package/src/Time.res +2 -2
- package/src/Time.res.mjs +2 -2
- package/src/UserContext.res +19 -118
- package/src/UserContext.res.mjs +10 -66
- package/src/Utils.res +15 -15
- package/src/Utils.res.mjs +7 -8
- package/src/adapters/MarkBatchProcessedAdapter.res +5 -0
- package/src/adapters/MarkBatchProcessedAdapter.res.mjs +14 -0
- package/src/bindings/BigDecimal.res +1 -1
- package/src/bindings/BigDecimal.res.mjs +2 -2
- package/src/bindings/ClickHouse.res +8 -6
- package/src/bindings/ClickHouse.res.mjs +5 -5
- package/src/bindings/Hrtime.res +1 -1
- package/src/bindings/Pino.res +2 -2
- package/src/bindings/Pino.res.mjs +3 -4
- package/src/db/EntityFilter.res +410 -0
- package/src/db/EntityFilter.res.mjs +424 -0
- package/src/db/EntityHistory.res +1 -1
- package/src/db/EntityHistory.res.mjs +1 -1
- package/src/db/InternalTable.res +10 -10
- package/src/db/InternalTable.res.mjs +41 -45
- package/src/db/Schema.res +2 -2
- package/src/db/Schema.res.mjs +3 -3
- package/src/db/Table.res +106 -22
- package/src/db/Table.res.mjs +84 -35
- package/src/sources/EventRouter.res +67 -2
- package/src/sources/EventRouter.res.mjs +45 -3
- package/src/sources/Evm.res +0 -7
- package/src/sources/Evm.res.mjs +0 -15
- package/src/sources/EvmChain.res +1 -1
- package/src/sources/EvmChain.res.mjs +1 -2
- package/src/sources/EvmRpcClient.res +42 -0
- package/src/sources/EvmRpcClient.res.mjs +64 -0
- package/src/sources/Fuel.res +0 -7
- package/src/sources/Fuel.res.mjs +0 -15
- package/src/sources/HyperFuelSource.res +5 -4
- package/src/sources/HyperFuelSource.res.mjs +2 -2
- package/src/sources/HyperSyncClient.res +9 -5
- package/src/sources/HyperSyncClient.res.mjs +2 -2
- package/src/sources/HyperSyncHeightStream.res +2 -2
- package/src/sources/HyperSyncHeightStream.res.mjs +2 -2
- package/src/sources/HyperSyncSource.res +10 -9
- package/src/sources/HyperSyncSource.res.mjs +4 -4
- package/src/sources/Rpc.res +1 -5
- package/src/sources/Rpc.res.mjs +1 -9
- package/src/sources/RpcSource.res +57 -21
- package/src/sources/RpcSource.res.mjs +47 -20
- package/src/sources/RpcWebSocketHeightStream.res +1 -1
- package/src/sources/SourceManager.res +3 -2
- package/src/sources/SourceManager.res.mjs +1 -1
- package/src/sources/Svm.res +3 -10
- package/src/sources/Svm.res.mjs +4 -18
- package/src/sources/SvmHyperSyncClient.res +265 -0
- package/src/sources/SvmHyperSyncClient.res.mjs +28 -0
- package/src/sources/SvmHyperSyncSource.res +638 -0
- package/src/sources/SvmHyperSyncSource.res.mjs +557 -0
- package/src/tui/Tui.res +9 -2
- package/src/tui/Tui.res.mjs +18 -3
- package/src/tui/components/BufferedProgressBar.res +2 -2
- package/src/tui/components/TuiData.res +3 -0
- package/svm.schema.json +523 -14
- package/src/TableIndices.res +0 -115
- package/src/TableIndices.res.mjs +0 -144
|
@@ -2,9 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
import * as Pino from "pino";
|
|
4
4
|
import * as Utils from "../Utils.res.mjs";
|
|
5
|
-
import * as Belt_Array from "@rescript/runtime/lib/es6/Belt_Array.js";
|
|
6
|
-
import * as Belt_Option from "@rescript/runtime/lib/es6/Belt_Option.js";
|
|
7
5
|
import * as PinoPretty from "pino-pretty";
|
|
6
|
+
import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
|
|
8
7
|
import EcsPinoFormat from "@elastic/ecs-pino-format";
|
|
9
8
|
|
|
10
9
|
function createPinoMessage(message) {
|
|
@@ -61,7 +60,7 @@ function makeStreams(userLogLevel, formatter, logFile, defaultFileLogLevel) {
|
|
|
61
60
|
stream: stream_stream,
|
|
62
61
|
level: userLogLevel
|
|
63
62
|
};
|
|
64
|
-
let maybeFileStream =
|
|
63
|
+
let maybeFileStream = Stdlib_Option.mapOr(logFile, [], dest => [{
|
|
65
64
|
stream: Pino.destination({
|
|
66
65
|
dest: dest,
|
|
67
66
|
sync: false,
|
|
@@ -69,7 +68,7 @@ function makeStreams(userLogLevel, formatter, logFile, defaultFileLogLevel) {
|
|
|
69
68
|
}),
|
|
70
69
|
level: defaultFileLogLevel
|
|
71
70
|
}]);
|
|
72
|
-
return
|
|
71
|
+
return [stream].concat(maybeFileStream);
|
|
73
72
|
}
|
|
74
73
|
|
|
75
74
|
function make$1(userLogLevel, customLevels, logFile, options, defaultFileLogLevel) {
|
|
@@ -0,0 +1,410 @@
|
|
|
1
|
+
module FieldValue = {
|
|
2
|
+
@unboxed
|
|
3
|
+
type rec tNonOptional =
|
|
4
|
+
| String(string)
|
|
5
|
+
| BigInt(bigint)
|
|
6
|
+
| Int(int)
|
|
7
|
+
| BigDecimal(BigDecimal.t)
|
|
8
|
+
| Bool(bool)
|
|
9
|
+
| Array(array<tNonOptional>)
|
|
10
|
+
|
|
11
|
+
let rec toString = tNonOptional =>
|
|
12
|
+
switch tNonOptional {
|
|
13
|
+
| String(v) => v
|
|
14
|
+
| BigInt(v) => v->BigInt.toString
|
|
15
|
+
| Int(v) => v->Int.toString
|
|
16
|
+
| BigDecimal(v) => v->BigDecimal.toString
|
|
17
|
+
| Bool(v) => v ? "true" : "false"
|
|
18
|
+
| Array(v) => `[${v->Array.map(toString)->Array.join(",")}]`
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
//This needs to be a castable type from any type that we
|
|
22
|
+
//support in entities so that we can create evaluations
|
|
23
|
+
//and serialize the types without parsing/wrapping them
|
|
24
|
+
type t = option<tNonOptional>
|
|
25
|
+
|
|
26
|
+
let toString = (value: t) =>
|
|
27
|
+
switch value {
|
|
28
|
+
| Some(v) => v->toString
|
|
29
|
+
| None => "undefined"
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
external castFrom: 'a => t = "%identity"
|
|
33
|
+
|
|
34
|
+
let eq = (a, b) =>
|
|
35
|
+
switch (a, b) {
|
|
36
|
+
//For big decimal use custom equals operator otherwise let Caml_obj.equal do its magic
|
|
37
|
+
| (Some(BigDecimal(bdA)), Some(BigDecimal(bdB))) => BigDecimal.equals(bdA, bdB)
|
|
38
|
+
| (a, b) => a == b
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
let gt = (a, b) =>
|
|
42
|
+
switch (a, b) {
|
|
43
|
+
//For big decimal use custom equals operator otherwise let Caml_obj.equal do its magic
|
|
44
|
+
| (Some(BigDecimal(bdA)), Some(BigDecimal(bdB))) => BigDecimal.gt(bdA, bdB)
|
|
45
|
+
| (a, b) => a > b
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
let lt = (a, b) =>
|
|
49
|
+
switch (a, b) {
|
|
50
|
+
//For big decimal use custom equals operator otherwise let Caml_obj.equal do its magic
|
|
51
|
+
| (Some(BigDecimal(bdA)), Some(BigDecimal(bdB))) => BigDecimal.lt(bdA, bdB)
|
|
52
|
+
| (a, b) => a < b
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// The And case requires at least one nested filter (storage throws otherwise),
|
|
57
|
+
// while In with an empty array matches nothing.
|
|
58
|
+
@tag("operator")
|
|
59
|
+
type rec t =
|
|
60
|
+
| @as("=") Eq({fieldName: string, fieldValue: unknown})
|
|
61
|
+
| @as(">") Gt({fieldName: string, fieldValue: unknown})
|
|
62
|
+
| @as("<") Lt({fieldName: string, fieldValue: unknown})
|
|
63
|
+
| @as("in") In({fieldName: string, fieldValue: array<unknown>})
|
|
64
|
+
| @as("and") And({filters: array<t>})
|
|
65
|
+
|
|
66
|
+
// Used as a stable in-memory cache key, so it must be unambiguous
|
|
67
|
+
// for any two different filters.
|
|
68
|
+
let rec toString = (filter: t) =>
|
|
69
|
+
switch filter {
|
|
70
|
+
| Eq({fieldName, fieldValue}) =>
|
|
71
|
+
`${fieldName}:Eq:${fieldValue->FieldValue.castFrom->FieldValue.toString}`
|
|
72
|
+
| Gt({fieldName, fieldValue}) =>
|
|
73
|
+
`${fieldName}:Gt:${fieldValue->FieldValue.castFrom->FieldValue.toString}`
|
|
74
|
+
| Lt({fieldName, fieldValue}) =>
|
|
75
|
+
`${fieldName}:Lt:${fieldValue->FieldValue.castFrom->FieldValue.toString}`
|
|
76
|
+
| In({fieldName, fieldValue}) =>
|
|
77
|
+
`${fieldName}:In:[${fieldValue
|
|
78
|
+
->Array.map(v => v->FieldValue.castFrom->FieldValue.toString)
|
|
79
|
+
->Array.join(",")}]`
|
|
80
|
+
| And({filters}) => `And(${filters->Array.map(toString)->Array.join(",")})`
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
let rec valuesCount = (filter: t) =>
|
|
84
|
+
switch filter {
|
|
85
|
+
| Eq(_) | Gt(_) | Lt(_) => 1
|
|
86
|
+
| In({fieldValue}) => fieldValue->Array.length
|
|
87
|
+
| And({filters}) => filters->Array.reduce(0, (acc, filter) => acc + filter->valuesCount)
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
let codegenHelpMessage = `Rerun 'pnpm dev' to update generated code after schema.graphql changes.`
|
|
91
|
+
|
|
92
|
+
let getUndefinedOrNullName = (value: 'a) =>
|
|
93
|
+
if value === %raw(`undefined`) {
|
|
94
|
+
Some("undefined")
|
|
95
|
+
} else if value === %raw(`null`) {
|
|
96
|
+
Some("null")
|
|
97
|
+
} else {
|
|
98
|
+
None
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Nullish values would otherwise turn into a "= NULL" query
|
|
102
|
+
// silently matching nothing.
|
|
103
|
+
let throwUnsupportedGetWhereValue = (~valueName, ~entityName, ~filterDisplay, ~hint="") =>
|
|
104
|
+
JsError.throwWithMessage(
|
|
105
|
+
`Invalid ${valueName} value passed to context.${entityName}.getWhere(${filterDisplay}). Filtering by null or undefined values is not supported in getWhere.${hint}`,
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
// Each returned filter should be loaded separately and the results flattened:
|
|
109
|
+
// _in maps to one Eq per value so loads memoize on the per-value level,
|
|
110
|
+
// and _gte/_lte are composed from Eq + Gt/Lt. Each field+operator pair
|
|
111
|
+
// expands into a group of such alternatives, and multiple pairs combine
|
|
112
|
+
// as a cross product of And filters — the groups stay disjoint, so the
|
|
113
|
+
// flattened results contain no duplicates.
|
|
114
|
+
let parseGetWhereOrThrow = (filter: dict<dict<unknown>>, ~entityName, ~table: Table.table): array<
|
|
115
|
+
t,
|
|
116
|
+
> => {
|
|
117
|
+
let filterKeys = filter->Dict.keysToArray
|
|
118
|
+
|
|
119
|
+
if filterKeys->Array.length === 0 {
|
|
120
|
+
JsError.throwWithMessage(
|
|
121
|
+
`Empty filter passed to context.${entityName}.getWhere(). Please provide a filter like { fieldName: { _eq: value } }.`,
|
|
122
|
+
)
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
let filterGroups = filterKeys->Array.flatMap(apiFieldName => {
|
|
126
|
+
let operatorObj = filter->Dict.getUnsafe(apiFieldName)
|
|
127
|
+
|
|
128
|
+
switch operatorObj->getUndefinedOrNullName {
|
|
129
|
+
| Some(valueName) =>
|
|
130
|
+
throwUnsupportedGetWhereValue(
|
|
131
|
+
~valueName,
|
|
132
|
+
~entityName,
|
|
133
|
+
~filterDisplay=`{ ${apiFieldName}: ${valueName} }`,
|
|
134
|
+
~hint=` Please provide an operator like { _eq: value }.`,
|
|
135
|
+
)
|
|
136
|
+
| None => ()
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// A primitive operator value wouldn't throw on Dict.keysToArray, but report
|
|
140
|
+
// string indices or no keys as operators, so catch it with a real hint instead
|
|
141
|
+
if operatorObj->typeof !== #object || operatorObj->Array.isArray {
|
|
142
|
+
JsError.throwWithMessage(
|
|
143
|
+
`Invalid value passed to context.${entityName}.getWhere({ ${apiFieldName}: ... }). Please provide an operator like { _eq: value }.`,
|
|
144
|
+
)
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
let operatorKeys = operatorObj->Dict.keysToArray
|
|
148
|
+
|
|
149
|
+
if operatorKeys->Array.length === 0 {
|
|
150
|
+
JsError.throwWithMessage(
|
|
151
|
+
`Empty operator passed to context.${entityName}.getWhere({ ${apiFieldName}: {} }). Please provide an operator like { _eq: value }, { _gt: value }, { _lt: value }, { _gte: value }, { _lte: value }, or { _in: [values] }.`,
|
|
152
|
+
)
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
let throwInvalidOperator = operatorKey =>
|
|
156
|
+
JsError.throwWithMessage(
|
|
157
|
+
`Invalid operator "${operatorKey}" in context.${entityName}.getWhere({ ${apiFieldName}: { ${operatorKey}: ... } }). Valid operators are _eq, _gt, _lt, _gte, _lte, _in.`,
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
// Validate the operators and the field before the values, so a typoed
|
|
161
|
+
// operator or field gets the more specific error even when the value
|
|
162
|
+
// is also nullish
|
|
163
|
+
operatorKeys->Array.forEach(operatorKey =>
|
|
164
|
+
switch operatorKey {
|
|
165
|
+
| "_eq" | "_gt" | "_lt" | "_gte" | "_lte" | "_in" => ()
|
|
166
|
+
| _ => throwInvalidOperator(operatorKey)
|
|
167
|
+
}
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
switch table->Table.getFieldByApiName(apiFieldName) {
|
|
171
|
+
| None =>
|
|
172
|
+
JsError.throwWithMessage(
|
|
173
|
+
`Invalid field "${apiFieldName}" in context.${entityName}.getWhere(). The field doesn't exist. ${codegenHelpMessage}`,
|
|
174
|
+
)
|
|
175
|
+
| Some(DerivedFrom(_)) =>
|
|
176
|
+
JsError.throwWithMessage(
|
|
177
|
+
`The field "${apiFieldName}" on entity "${entityName}" is a derived field and cannot be used in getWhere(). Use the source entity's indexed field instead.`,
|
|
178
|
+
)
|
|
179
|
+
| Some(Field({isPrimaryKey: false, isIndex: false, linkedEntity: None})) =>
|
|
180
|
+
JsError.throwWithMessage(
|
|
181
|
+
`The field "${apiFieldName}" on entity "${entityName}" does not have an index. To use it in getWhere(), add the @index directive in your schema.graphql:\n\n ${apiFieldName}: ... @index\n\nThen run 'pnpm envio codegen' to regenerate.`,
|
|
182
|
+
)
|
|
183
|
+
| Some(Field(_)) => ()
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
operatorKeys->Array.map(operatorKey => {
|
|
187
|
+
let fieldValue = operatorObj->Dict.getUnsafe(operatorKey)
|
|
188
|
+
switch fieldValue->getUndefinedOrNullName {
|
|
189
|
+
| Some(valueName) =>
|
|
190
|
+
throwUnsupportedGetWhereValue(
|
|
191
|
+
~valueName,
|
|
192
|
+
~entityName,
|
|
193
|
+
~filterDisplay=`{ ${apiFieldName}: { ${operatorKey}: ${valueName} } }`,
|
|
194
|
+
)
|
|
195
|
+
| None => ()
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
switch operatorKey {
|
|
199
|
+
| "_in" => {
|
|
200
|
+
if !(fieldValue->Array.isArray) {
|
|
201
|
+
JsError.throwWithMessage(
|
|
202
|
+
`Invalid value passed to context.${entityName}.getWhere({ ${apiFieldName}: { _in: ... } }). The _in operator expects an array of values.`,
|
|
203
|
+
)
|
|
204
|
+
}
|
|
205
|
+
let fieldValues = fieldValue->(Utils.magic: unknown => array<unknown>)
|
|
206
|
+
|
|
207
|
+
fieldValues->Array.mapWithIndex(
|
|
208
|
+
(fieldValue, index) => {
|
|
209
|
+
switch fieldValue->getUndefinedOrNullName {
|
|
210
|
+
| Some(valueName) =>
|
|
211
|
+
throwUnsupportedGetWhereValue(
|
|
212
|
+
~valueName,
|
|
213
|
+
~entityName,
|
|
214
|
+
~filterDisplay=`{ ${apiFieldName}: { _in: [...] } }`,
|
|
215
|
+
~hint=` The ${valueName} value is at index ${index->Int.toString} of the _in array.`,
|
|
216
|
+
)
|
|
217
|
+
| None => ()
|
|
218
|
+
}
|
|
219
|
+
Eq({fieldName: apiFieldName, fieldValue})
|
|
220
|
+
},
|
|
221
|
+
)
|
|
222
|
+
}
|
|
223
|
+
| "_gte" => [
|
|
224
|
+
Eq({fieldName: apiFieldName, fieldValue}),
|
|
225
|
+
Gt({fieldName: apiFieldName, fieldValue}),
|
|
226
|
+
]
|
|
227
|
+
| "_lte" => [
|
|
228
|
+
Eq({fieldName: apiFieldName, fieldValue}),
|
|
229
|
+
Lt({fieldName: apiFieldName, fieldValue}),
|
|
230
|
+
]
|
|
231
|
+
| "_eq" => [Eq({fieldName: apiFieldName, fieldValue})]
|
|
232
|
+
| "_gt" => [Gt({fieldName: apiFieldName, fieldValue})]
|
|
233
|
+
| "_lt" => [Lt({fieldName: apiFieldName, fieldValue})]
|
|
234
|
+
| _ => throwInvalidOperator(operatorKey)
|
|
235
|
+
}
|
|
236
|
+
})
|
|
237
|
+
})
|
|
238
|
+
|
|
239
|
+
filterGroups
|
|
240
|
+
->Array.reduce([[]], (combinations, group) =>
|
|
241
|
+
combinations->Array.flatMap(combination =>
|
|
242
|
+
group->Array.map(filter => combination->Array.concat([filter]))
|
|
243
|
+
)
|
|
244
|
+
)
|
|
245
|
+
->Array.map(filters =>
|
|
246
|
+
switch filters {
|
|
247
|
+
| [filter] => filter
|
|
248
|
+
| _ => And({filters: filters})
|
|
249
|
+
}
|
|
250
|
+
)
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
let rec printOperationFilter = (filter: t, ~paramsCount: ref<int>) =>
|
|
254
|
+
switch filter {
|
|
255
|
+
| Eq({fieldName}) => {
|
|
256
|
+
paramsCount := paramsCount.contents + 1
|
|
257
|
+
`${fieldName}: $${paramsCount.contents->Int.toString}`
|
|
258
|
+
}
|
|
259
|
+
| Gt({fieldName}) => {
|
|
260
|
+
paramsCount := paramsCount.contents + 1
|
|
261
|
+
`${fieldName}: {_gt: $${paramsCount.contents->Int.toString}}`
|
|
262
|
+
}
|
|
263
|
+
| Lt({fieldName}) => {
|
|
264
|
+
paramsCount := paramsCount.contents + 1
|
|
265
|
+
`${fieldName}: {_lt: $${paramsCount.contents->Int.toString}}`
|
|
266
|
+
}
|
|
267
|
+
| In({fieldName}) => {
|
|
268
|
+
paramsCount := paramsCount.contents + 1
|
|
269
|
+
`${fieldName}: {_in: $${paramsCount.contents->Int.toString}}`
|
|
270
|
+
}
|
|
271
|
+
| And({filters}) => {
|
|
272
|
+
let acc = ref("")
|
|
273
|
+
for idx in 0 to filters->Array.length - 1 {
|
|
274
|
+
let part = filters->Array.getUnsafe(idx)->printOperationFilter(~paramsCount)
|
|
275
|
+
acc := (acc.contents === "" ? part : `${acc.contents}, ${part}`)
|
|
276
|
+
}
|
|
277
|
+
acc.contents
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// Filters that may be batched into a single storage query must produce
|
|
282
|
+
// the same key, so concrete values are replaced with $N placeholders.
|
|
283
|
+
// The flat cases duplicate printOperationFilter to keep this hot path
|
|
284
|
+
// allocation-free.
|
|
285
|
+
let toOperationKey = (filter: t, ~entityName) =>
|
|
286
|
+
switch filter {
|
|
287
|
+
| Eq({fieldName}) => `${entityName}.getWhere({${fieldName}: $1})`
|
|
288
|
+
| Gt({fieldName}) => `${entityName}.getWhere({${fieldName}: {_gt: $1}})`
|
|
289
|
+
| Lt({fieldName}) => `${entityName}.getWhere({${fieldName}: {_lt: $1}})`
|
|
290
|
+
| In({fieldName}) => `${entityName}.getWhere({${fieldName}: {_in: $1}})`
|
|
291
|
+
| And(_) => `${entityName}.getWhere({${filter->printOperationFilter(~paramsCount=ref(0))}})`
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// Values bound to the operation key's $N placeholders, in placeholder
|
|
295
|
+
// order. A top-level In is reported flat, since a merged query holds one
|
|
296
|
+
// value per batched call there, while an In nested in And binds its whole
|
|
297
|
+
// array to a single placeholder, mirroring the one paramsCount increment
|
|
298
|
+
// per flat filter in printOperationFilter.
|
|
299
|
+
let getParams = (filter: t) =>
|
|
300
|
+
switch filter {
|
|
301
|
+
| Eq({fieldValue}) => [fieldValue]
|
|
302
|
+
| Gt({fieldValue}) => [fieldValue]
|
|
303
|
+
| Lt({fieldValue}) => [fieldValue]
|
|
304
|
+
| In({fieldValue}) => fieldValue
|
|
305
|
+
| And(_) => {
|
|
306
|
+
let acc = []
|
|
307
|
+
let rec collect = (filter: t) =>
|
|
308
|
+
switch filter {
|
|
309
|
+
| Eq({fieldValue}) => acc->Array.push(fieldValue)->ignore
|
|
310
|
+
| Gt({fieldValue}) => acc->Array.push(fieldValue)->ignore
|
|
311
|
+
| Lt({fieldValue}) => acc->Array.push(fieldValue)->ignore
|
|
312
|
+
| In({fieldValue}) =>
|
|
313
|
+
acc->Array.push(fieldValue->(Utils.magic: array<unknown> => unknown))->ignore
|
|
314
|
+
| And({filters}) => filters->Array.forEach(collect)
|
|
315
|
+
}
|
|
316
|
+
collect(filter)
|
|
317
|
+
acc
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
// Collapses filters sharing an operation key into fewer storage queries:
|
|
322
|
+
// Eq and In batches merge into a single In on the field. Gt/Lt/And have
|
|
323
|
+
// no lossless single-query form without an Or operator, so they stay as is.
|
|
324
|
+
// Expects a homogeneous batch — filters with the same operation key.
|
|
325
|
+
// A mismatched filter throws: dropping it would leave its already
|
|
326
|
+
// registered index without the matching db rows, silently losing data.
|
|
327
|
+
let throwUnmergeable = (filter: t) =>
|
|
328
|
+
JsError.throwWithMessage(
|
|
329
|
+
`Unexpected filter ${filter->toString} in a merged batch. Filters batched into a single query must use the same operator and field.`,
|
|
330
|
+
)
|
|
331
|
+
|
|
332
|
+
let merge = (filters: array<t>) =>
|
|
333
|
+
switch filters {
|
|
334
|
+
| [] | [_] => filters
|
|
335
|
+
| _ =>
|
|
336
|
+
switch filters->Array.getUnsafe(0) {
|
|
337
|
+
| Eq({fieldName}) => [
|
|
338
|
+
In({
|
|
339
|
+
fieldName,
|
|
340
|
+
fieldValue: filters->Array.map(filter =>
|
|
341
|
+
switch filter {
|
|
342
|
+
| Eq({fieldValue}) => fieldValue
|
|
343
|
+
| _ => throwUnmergeable(filter)
|
|
344
|
+
}
|
|
345
|
+
),
|
|
346
|
+
}),
|
|
347
|
+
]
|
|
348
|
+
| In({fieldName}) => [
|
|
349
|
+
In({
|
|
350
|
+
fieldName,
|
|
351
|
+
fieldValue: filters
|
|
352
|
+
->Array.map(filter =>
|
|
353
|
+
switch filter {
|
|
354
|
+
| In({fieldValue}) => fieldValue
|
|
355
|
+
| _ => throwUnmergeable(filter)
|
|
356
|
+
}
|
|
357
|
+
)
|
|
358
|
+
->Array.flat,
|
|
359
|
+
}),
|
|
360
|
+
]
|
|
361
|
+
| Gt(_) | Lt(_) | And(_) => filters
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
// A field missing on the entity reads as `undefined`, which matches the `None`
|
|
366
|
+
// arm of `FieldValue.t` (`option<...>`), so nullable columns omitted on the
|
|
367
|
+
// entity object are compared as null rather than crashing.
|
|
368
|
+
let rec matches = (filter: t, ~entity: dict<FieldValue.t>) =>
|
|
369
|
+
switch filter {
|
|
370
|
+
| Eq({fieldName, fieldValue}) =>
|
|
371
|
+
entity->Dict.getUnsafe(fieldName)->FieldValue.eq(fieldValue->FieldValue.castFrom)
|
|
372
|
+
| Gt({fieldName, fieldValue}) =>
|
|
373
|
+
entity->Dict.getUnsafe(fieldName)->FieldValue.gt(fieldValue->FieldValue.castFrom)
|
|
374
|
+
| Lt({fieldName, fieldValue}) =>
|
|
375
|
+
entity->Dict.getUnsafe(fieldName)->FieldValue.lt(fieldValue->FieldValue.castFrom)
|
|
376
|
+
| In({fieldName, fieldValue}) => {
|
|
377
|
+
let entityFieldValue = entity->Dict.getUnsafe(fieldName)
|
|
378
|
+
fieldValue->Array.some(fieldValue =>
|
|
379
|
+
entityFieldValue->FieldValue.eq(fieldValue->FieldValue.castFrom)
|
|
380
|
+
)
|
|
381
|
+
}
|
|
382
|
+
| And({filters: []}) =>
|
|
383
|
+
JsError.throwWithMessage(`The "and" filter must contain at least one nested filter.`)
|
|
384
|
+
| And({filters}) => filters->Array.every(filter => filter->matches(~entity))
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
// In values are mapped as one array (isArray=true), so they can be
|
|
388
|
+
// converted with the table's cached array schema in a single pass.
|
|
389
|
+
let rec mapValues = (
|
|
390
|
+
filter: t,
|
|
391
|
+
~mapValue: (~fieldName: string, ~fieldValue: unknown, ~isArray: bool) => unknown,
|
|
392
|
+
) =>
|
|
393
|
+
switch filter {
|
|
394
|
+
| Eq({fieldName, fieldValue}) =>
|
|
395
|
+
Eq({fieldName, fieldValue: mapValue(~fieldName, ~fieldValue, ~isArray=false)})
|
|
396
|
+
| Gt({fieldName, fieldValue}) =>
|
|
397
|
+
Gt({fieldName, fieldValue: mapValue(~fieldName, ~fieldValue, ~isArray=false)})
|
|
398
|
+
| Lt({fieldName, fieldValue}) =>
|
|
399
|
+
Lt({fieldName, fieldValue: mapValue(~fieldName, ~fieldValue, ~isArray=false)})
|
|
400
|
+
| In({fieldName, fieldValue}) =>
|
|
401
|
+
In({
|
|
402
|
+
fieldName,
|
|
403
|
+
fieldValue: mapValue(
|
|
404
|
+
~fieldName,
|
|
405
|
+
~fieldValue=fieldValue->(Utils.magic: array<unknown> => unknown),
|
|
406
|
+
~isArray=true,
|
|
407
|
+
)->(Utils.magic: unknown => array<unknown>),
|
|
408
|
+
})
|
|
409
|
+
| And({filters}) => And({filters: filters->Array.map(filter => filter->mapValues(~mapValue))})
|
|
410
|
+
}
|