envio 3.12.0 → 3.13.0-alpha.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/package.json +6 -6
- package/src/BatchProcessing.res +9 -9
- package/src/BatchProcessing.res.mjs +5 -5
- package/src/Bin.res +24 -17
- package/src/Bin.res.mjs +5 -0
- package/src/ChainFetching.res +31 -13
- package/src/ChainFetching.res.mjs +9 -4
- package/src/ChainState.res +55 -3
- package/src/ChainState.res.mjs +46 -3
- package/src/ChainState.resi +3 -1
- package/src/Config.res +35 -0
- package/src/Config.res.mjs +39 -0
- package/src/Core.res +4 -0
- package/src/Core.res.mjs +4 -0
- package/src/CrossChainState.res +61 -4
- package/src/CrossChainState.res.mjs +39 -5
- package/src/CrossChainState.resi +10 -1
- package/src/Env.res +4 -0
- package/src/FetchState.res +51 -46
- package/src/FetchState.res.mjs +54 -36
- package/src/InMemoryTable.res +155 -67
- package/src/InMemoryTable.res.mjs +151 -60
- package/src/IndexerLoop.res +2 -0
- package/src/IndexerLoop.res.mjs +1 -0
- package/src/IndexerState.res +41 -1
- package/src/IndexerState.res.mjs +43 -4
- package/src/IndexerState.resi +10 -0
- package/src/LoadLayer.res +57 -34
- package/src/LoadLayer.res.mjs +45 -62
- package/src/LoadLayer.resi +1 -1
- package/src/Logging.res +38 -6
- package/src/Logging.res.mjs +32 -5
- package/src/Main.res +131 -268
- package/src/Main.res.mjs +28 -152
- package/src/Metrics.res +263 -102
- package/src/Metrics.res.mjs +227 -48
- package/src/Persistence.res +27 -2
- package/src/Persistence.res.mjs +9 -2
- package/src/PgStorage.res +109 -71
- package/src/PgStorage.res.mjs +88 -58
- package/src/Server.res +181 -0
- package/src/Server.res.mjs +143 -0
- package/src/Supervisor.res +415 -0
- package/src/Supervisor.res.mjs +325 -0
- package/src/TestIndexer.res +9 -25
- package/src/TestIndexer.res.mjs +5 -4
- package/src/UserContext.res +13 -32
- package/src/UserContext.res.mjs +1 -7
- package/src/Utils.res +1 -4
- package/src/Utils.res.mjs +7 -16
- package/src/Worker.res +95 -0
- package/src/Worker.res.mjs +80 -0
- package/src/bindings/NodeJs.res +41 -0
- package/src/db/EntityFilter.res +487 -275
- package/src/db/EntityFilter.res.mjs +557 -309
- package/src/db/InternalTable.res +8 -1
- package/src/db/InternalTable.res.mjs +5 -1
- package/src/db/Table.res +21 -6
- package/src/db/Table.res.mjs +13 -4
- package/src/sources/BlockStore.res +7 -2
- package/src/sources/EvmHyperSyncSource.res +2 -0
- package/src/sources/EvmHyperSyncSource.res.mjs +2 -2
- package/src/sources/FuelHyperSyncSource.res +1 -0
- package/src/sources/FuelHyperSyncSource.res.mjs +1 -1
- package/src/sources/HyperSync.res +4 -0
- package/src/sources/HyperSync.res.mjs +4 -2
- package/src/sources/HyperSync.resi +1 -0
- package/src/sources/HyperSyncClient.res +3 -0
- package/src/sources/HyperSyncSSE.res +1 -1
- package/src/sources/HyperSyncSSE.res.mjs +4 -10
- package/src/sources/RpcSource.res +1 -0
- package/src/sources/RpcSource.res.mjs +1 -1
- package/src/sources/SimulateSource.res +1 -0
- package/src/sources/SimulateSource.res.mjs +1 -1
- package/src/sources/Source.res +7 -0
- package/src/sources/SourceManager.res +4 -3
- package/src/sources/SourceManager.res.mjs +2 -2
- package/src/sources/SvmHyperSyncClient.res +5 -0
- package/src/sources/SvmHyperSyncSource.res +3 -0
- package/src/sources/SvmHyperSyncSource.res.mjs +4 -2
- package/src/tui/Tui.res +24 -0
- package/src/tui/Tui.res.mjs +18 -0
- package/src/tui/components/SyncETA.res +12 -6
- package/src/tui/components/SyncETA.res.mjs +12 -8
package/src/db/EntityFilter.res
CHANGED
|
@@ -1,111 +1,61 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
)
|
|
55
|
-
|
|
56
|
-
let gt = (a, b) =>
|
|
57
|
-
compareWith(
|
|
58
|
-
a,
|
|
59
|
-
b,
|
|
60
|
-
~bigDecimal=BigDecimal.gt,
|
|
61
|
-
~bytes=order => order > 0.,
|
|
62
|
-
~fallback=(a, b) => a > b,
|
|
63
|
-
)
|
|
64
|
-
|
|
65
|
-
let lt = (a, b) =>
|
|
66
|
-
compareWith(
|
|
67
|
-
a,
|
|
68
|
-
b,
|
|
69
|
-
~bigDecimal=BigDecimal.lt,
|
|
70
|
-
~bytes=order => order < 0.,
|
|
71
|
-
~fallback=(a, b) => a < b,
|
|
1
|
+
@scope("JSON") @val external jsonStringify: unknown => string = "stringify"
|
|
2
|
+
|
|
3
|
+
let nullish: unknown => bool = %raw(`v => v === undefined || v === null`)
|
|
4
|
+
|
|
5
|
+
let asArray = (v: unknown) => v->(Utils.magic: unknown => array<unknown>)
|
|
6
|
+
|
|
7
|
+
// Renders one already-projected value into a cache key: a type tag, then the
|
|
8
|
+
// length, then the value. The tag keeps "5" apart from 5, and the length means
|
|
9
|
+
// a delimiter inside a value can't imitate the delimiter, so no escaping pass
|
|
10
|
+
// is needed. Both together make the encoding injective, which is the only
|
|
11
|
+
// property a key needs.
|
|
12
|
+
//
|
|
13
|
+
// Every branch but the last is reached because the column's projection already
|
|
14
|
+
// collapsed its values to a primitive — a Date to its epoch millis, a Bytea to
|
|
15
|
+
// hex — so nothing here has to guess a value's type at runtime. JSON is the
|
|
16
|
+
// last resort, for a column whose values are objects in the first place.
|
|
17
|
+
let encodeValueKey: unknown => string = %raw(`v => {
|
|
18
|
+
if (v === null) return "z"
|
|
19
|
+
switch (typeof v) {
|
|
20
|
+
case "string": return "s" + v.length + ":" + v
|
|
21
|
+
case "number": { const s = "" + v; return "n" + s.length + ":" + s }
|
|
22
|
+
case "bigint": { const s = "" + v; return "g" + s.length + ":" + s }
|
|
23
|
+
case "boolean": return v ? "t" : "f"
|
|
24
|
+
case "undefined": return "u"
|
|
25
|
+
default: { const s = JSON.stringify(v); return "o" + s.length + ":" + s }
|
|
26
|
+
}
|
|
27
|
+
}`)
|
|
28
|
+
|
|
29
|
+
// JSON is the one step above that can fail, and only on an object: one holding
|
|
30
|
+
// a bigint, or one that refers to itself. An array is walked rather than
|
|
31
|
+
// stringified, because its key is built from its elements' keys.
|
|
32
|
+
let isKeyable: unknown => bool = %raw(`function isKeyable(v) {
|
|
33
|
+
if (typeof v !== "object" || v === null) return true
|
|
34
|
+
if (Array.isArray(v)) return v.every(isKeyable)
|
|
35
|
+
try { JSON.stringify(v); return true } catch { return false }
|
|
36
|
+
}`)
|
|
37
|
+
|
|
38
|
+
// The filter as the handler wrote it: field -> operator -> value. Private, so
|
|
39
|
+
// a value of this type is one parseOrThrow accepted or the indexer built
|
|
40
|
+
// itself, and nothing downstream has to check the filter again.
|
|
41
|
+
type t = private dict<dict<unknown>>
|
|
42
|
+
|
|
43
|
+
external unsafeFromDict: dict<dict<unknown>> => t = "%identity"
|
|
44
|
+
|
|
45
|
+
let entries = (filter: t) => (filter :> dict<dict<unknown>>)
|
|
46
|
+
|
|
47
|
+
let valuesCount = (filter: t) => {
|
|
48
|
+
let count = ref(0)
|
|
49
|
+
filter
|
|
50
|
+
->entries
|
|
51
|
+
->Utils.Dict.forEachWithKey((operators, _) =>
|
|
52
|
+
operators->Utils.Dict.forEachWithKey((fieldValue, operator) =>
|
|
53
|
+
count := count.contents + (operator === "_in" ? fieldValue->asArray->Array.length : 1)
|
|
72
54
|
)
|
|
55
|
+
)
|
|
56
|
+
count.contents
|
|
73
57
|
}
|
|
74
58
|
|
|
75
|
-
// The And case requires at least one nested filter (storage throws otherwise),
|
|
76
|
-
// while In with an empty array matches nothing.
|
|
77
|
-
@tag("operator")
|
|
78
|
-
type rec t =
|
|
79
|
-
| @as("=") Eq({fieldName: string, fieldValue: unknown})
|
|
80
|
-
| @as(">") Gt({fieldName: string, fieldValue: unknown})
|
|
81
|
-
| @as("<") Lt({fieldName: string, fieldValue: unknown})
|
|
82
|
-
| @as("in") In({fieldName: string, fieldValue: array<unknown>})
|
|
83
|
-
| @as("and") And({filters: array<t>})
|
|
84
|
-
|
|
85
|
-
// Used as a stable in-memory cache key, so it must be unambiguous
|
|
86
|
-
// for any two different filters.
|
|
87
|
-
let rec toString = (filter: t) =>
|
|
88
|
-
switch filter {
|
|
89
|
-
| Eq({fieldName, fieldValue}) =>
|
|
90
|
-
`${fieldName}:Eq:${fieldValue->FieldValue.castFrom->FieldValue.toString}`
|
|
91
|
-
| Gt({fieldName, fieldValue}) =>
|
|
92
|
-
`${fieldName}:Gt:${fieldValue->FieldValue.castFrom->FieldValue.toString}`
|
|
93
|
-
| Lt({fieldName, fieldValue}) =>
|
|
94
|
-
`${fieldName}:Lt:${fieldValue->FieldValue.castFrom->FieldValue.toString}`
|
|
95
|
-
| In({fieldName, fieldValue}) =>
|
|
96
|
-
`${fieldName}:In:[${fieldValue
|
|
97
|
-
->Array.map(v => v->FieldValue.castFrom->FieldValue.toString)
|
|
98
|
-
->Array.join(",")}]`
|
|
99
|
-
| And({filters}) => `And(${filters->Array.map(toString)->Array.join(",")})`
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
let rec valuesCount = (filter: t) =>
|
|
103
|
-
switch filter {
|
|
104
|
-
| Eq(_) | Gt(_) | Lt(_) => 1
|
|
105
|
-
| In({fieldValue}) => fieldValue->Array.length
|
|
106
|
-
| And({filters}) => filters->Array.reduce(0, (acc, filter) => acc + filter->valuesCount)
|
|
107
|
-
}
|
|
108
|
-
|
|
109
59
|
let codegenHelpMessage = `Rerun 'pnpm dev' to update generated code after schema.graphql changes.`
|
|
110
60
|
|
|
111
61
|
let getUndefinedOrNullName = (value: 'a) =>
|
|
@@ -124,15 +74,42 @@ let throwUnsupportedGetWhereValue = (~valueName, ~entityName, ~filterDisplay, ~h
|
|
|
124
74
|
`Invalid ${valueName} value passed to context.${entityName}.getWhere(${filterDisplay}). Filtering by null or undefined values is not supported in getWhere.${hint}`,
|
|
125
75
|
)
|
|
126
76
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
//
|
|
130
|
-
//
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
77
|
+
let isDate: unknown => bool = %raw(`v => v instanceof Date`)
|
|
78
|
+
|
|
79
|
+
// Columns whose comparison needs a specific object shape. The rest compare
|
|
80
|
+
// natively, so whatever they're handed is already safe.
|
|
81
|
+
let expectedValueType = (field: Table.field) =>
|
|
82
|
+
switch field.fieldType {
|
|
83
|
+
| Date => Some(field.isArray ? "an array of Date" : "a Date")
|
|
84
|
+
| Bytea => Some(field.isArray ? "an array of Uint8Array" : "a Uint8Array")
|
|
85
|
+
| _ => None
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Whether this column's values can reach JSON when a key is built. Date, Bytea
|
|
89
|
+
// and BigDecimal collapse to a primitive first, and an array's elements follow
|
|
90
|
+
// the same rule as the scalar they hold.
|
|
91
|
+
let keyUsesSerializer = (field: Table.field) =>
|
|
92
|
+
switch field.fieldType {
|
|
93
|
+
| Date | Bytea | BigDecimal(_) => false
|
|
94
|
+
| _ => true
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
let matchesFieldType = (value: unknown, ~field: Table.field) => {
|
|
98
|
+
let matchesScalar = value =>
|
|
99
|
+
switch field.fieldType {
|
|
100
|
+
| Date => value->isDate
|
|
101
|
+
| Bytea => value->Utils.Bytes.asUint8Array->Option.isSome
|
|
102
|
+
| _ => true
|
|
103
|
+
}
|
|
104
|
+
field.isArray
|
|
105
|
+
? value->Array.isArray && value->asArray->Array.every(matchesScalar)
|
|
106
|
+
: value->matchesScalar
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// The one way in for a handler's filter. A bad field, operator or value is
|
|
110
|
+
// reported against the call the handler made rather than surfacing later as a
|
|
111
|
+
// comparator or SQL failure.
|
|
112
|
+
let parseOrThrow = (filter: dict<dict<unknown>>, ~entityName, ~table: Table.table): t => {
|
|
136
113
|
let filterKeys = filter->Dict.keysToArray
|
|
137
114
|
|
|
138
115
|
if filterKeys->Array.length === 0 {
|
|
@@ -141,7 +118,7 @@ let parseGetWhereOrThrow = (filter: dict<dict<unknown>>, ~entityName, ~table: Ta
|
|
|
141
118
|
)
|
|
142
119
|
}
|
|
143
120
|
|
|
144
|
-
|
|
121
|
+
filterKeys->Array.forEach(apiFieldName => {
|
|
145
122
|
let operatorObj = filter->Dict.getUnsafe(apiFieldName)
|
|
146
123
|
|
|
147
124
|
switch operatorObj->getUndefinedOrNullName {
|
|
@@ -186,7 +163,7 @@ let parseGetWhereOrThrow = (filter: dict<dict<unknown>>, ~entityName, ~table: Ta
|
|
|
186
163
|
}
|
|
187
164
|
)
|
|
188
165
|
|
|
189
|
-
switch table->Table.getFieldByApiName(apiFieldName) {
|
|
166
|
+
let field = switch table->Table.getFieldByApiName(apiFieldName) {
|
|
190
167
|
| None =>
|
|
191
168
|
JsError.throwWithMessage(
|
|
192
169
|
`Invalid field "${apiFieldName}" in context.${entityName}.getWhere(). The field doesn't exist. ${codegenHelpMessage}`,
|
|
@@ -195,10 +172,16 @@ let parseGetWhereOrThrow = (filter: dict<dict<unknown>>, ~entityName, ~table: Ta
|
|
|
195
172
|
JsError.throwWithMessage(
|
|
196
173
|
`The field "${apiFieldName}" on entity "${entityName}" is a derived field and cannot be used in getWhere(). Use the source entity's indexed field instead.`,
|
|
197
174
|
)
|
|
198
|
-
| Some(Field(
|
|
175
|
+
| Some(Field(field)) => field
|
|
199
176
|
}
|
|
200
177
|
|
|
201
|
-
|
|
178
|
+
// Constant per field, and None for every column that accepts any runtime
|
|
179
|
+
// shape — so the per-value check below is a single comparison on the path
|
|
180
|
+
// that matters, an _in of many values that are all about to pass.
|
|
181
|
+
let expectedType = field->expectedValueType
|
|
182
|
+
let checkKeyable = field->keyUsesSerializer
|
|
183
|
+
|
|
184
|
+
operatorKeys->Array.forEach(operatorKey => {
|
|
202
185
|
let fieldValue = operatorObj->Dict.getUnsafe(operatorKey)
|
|
203
186
|
switch fieldValue->getUndefinedOrNullName {
|
|
204
187
|
| Some(valueName) =>
|
|
@@ -209,6 +192,14 @@ let parseGetWhereOrThrow = (filter: dict<dict<unknown>>, ~entityName, ~table: Ta
|
|
|
209
192
|
)
|
|
210
193
|
| None => ()
|
|
211
194
|
}
|
|
195
|
+
let throwUnexpectedType = (~typeName, ~hint) =>
|
|
196
|
+
JsError.throwWithMessage(
|
|
197
|
+
`Invalid value passed to context.${entityName}.getWhere({ ${apiFieldName}: { ${operatorKey}: ... } }). The field "${apiFieldName}" expects ${typeName}.${hint}`,
|
|
198
|
+
)
|
|
199
|
+
let throwUnkeyable = (~hint) =>
|
|
200
|
+
JsError.throwWithMessage(
|
|
201
|
+
`Invalid value passed to context.${entityName}.getWhere({ ${apiFieldName}: { ${operatorKey}: ... } }). The value can't be serialized, so it can't be used as a filter. An object holding a bigint, or a circular reference, is not supported.${hint}`,
|
|
202
|
+
)
|
|
212
203
|
|
|
213
204
|
switch operatorKey {
|
|
214
205
|
| "_in" => {
|
|
@@ -217,9 +208,9 @@ let parseGetWhereOrThrow = (filter: dict<dict<unknown>>, ~entityName, ~table: Ta
|
|
|
217
208
|
`Invalid value passed to context.${entityName}.getWhere({ ${apiFieldName}: { _in: ... } }). The _in operator expects an array of values.`,
|
|
218
209
|
)
|
|
219
210
|
}
|
|
220
|
-
let fieldValues = fieldValue->
|
|
211
|
+
let fieldValues = fieldValue->asArray
|
|
221
212
|
|
|
222
|
-
fieldValues->Array.
|
|
213
|
+
fieldValues->Array.forEachWithIndex(
|
|
223
214
|
(fieldValue, index) => {
|
|
224
215
|
switch fieldValue->getUndefinedOrNullName {
|
|
225
216
|
| Some(valueName) =>
|
|
@@ -231,195 +222,416 @@ let parseGetWhereOrThrow = (filter: dict<dict<unknown>>, ~entityName, ~table: Ta
|
|
|
231
222
|
)
|
|
232
223
|
| None => ()
|
|
233
224
|
}
|
|
234
|
-
|
|
225
|
+
switch expectedType {
|
|
226
|
+
| Some(typeName) if !(fieldValue->matchesFieldType(~field)) =>
|
|
227
|
+
throwUnexpectedType(
|
|
228
|
+
~typeName,
|
|
229
|
+
~hint=` The value is at index ${index->Int.toString} of the _in array.`,
|
|
230
|
+
)
|
|
231
|
+
| _ => ()
|
|
232
|
+
}
|
|
233
|
+
if checkKeyable && !(fieldValue->isKeyable) {
|
|
234
|
+
throwUnkeyable(
|
|
235
|
+
~hint=` The value is at index ${index->Int.toString} of the _in array.`,
|
|
236
|
+
)
|
|
237
|
+
}
|
|
235
238
|
},
|
|
236
239
|
)
|
|
237
240
|
}
|
|
238
|
-
|
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
| "_gt" => [Gt({fieldName: apiFieldName, fieldValue})]
|
|
248
|
-
| "_lt" => [Lt({fieldName: apiFieldName, fieldValue})]
|
|
249
|
-
| _ => throwInvalidOperator(operatorKey)
|
|
241
|
+
| _ =>
|
|
242
|
+
switch expectedType {
|
|
243
|
+
| Some(typeName) if !(fieldValue->matchesFieldType(~field)) =>
|
|
244
|
+
throwUnexpectedType(~typeName, ~hint="")
|
|
245
|
+
| _ => ()
|
|
246
|
+
}
|
|
247
|
+
if checkKeyable && !(fieldValue->isKeyable) {
|
|
248
|
+
throwUnkeyable(~hint="")
|
|
249
|
+
}
|
|
250
250
|
}
|
|
251
251
|
})
|
|
252
252
|
})
|
|
253
253
|
|
|
254
|
-
|
|
255
|
-
->Array.reduce([[]], (combinations, group) =>
|
|
256
|
-
combinations->Array.flatMap(combination =>
|
|
257
|
-
group->Array.map(filter => combination->Array.concat([filter]))
|
|
258
|
-
)
|
|
259
|
-
)
|
|
260
|
-
->Array.map(filters =>
|
|
261
|
-
switch filters {
|
|
262
|
-
| [filter] => filter
|
|
263
|
-
| _ => And({filters: filters})
|
|
264
|
-
}
|
|
265
|
-
)
|
|
254
|
+
filter->unsafeFromDict
|
|
266
255
|
}
|
|
267
256
|
|
|
268
|
-
let
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
`${fieldName}: {_in: $${paramsCount.contents->Int.toString}}`
|
|
285
|
-
}
|
|
286
|
-
| And({filters}) => {
|
|
287
|
-
let acc = ref("")
|
|
288
|
-
for idx in 0 to filters->Array.length - 1 {
|
|
289
|
-
let part = filters->Array.getUnsafe(idx)->printOperationFilter(~paramsCount)
|
|
290
|
-
acc := (acc.contents === "" ? part : `${acc.contents}, ${part}`)
|
|
291
|
-
}
|
|
292
|
-
acc.contents
|
|
293
|
-
}
|
|
257
|
+
let byIds = (ids: array<string>): t =>
|
|
258
|
+
Dict.fromArray([
|
|
259
|
+
(Table.idFieldName, dict{"_in": ids->(Utils.magic: array<string> => unknown)}),
|
|
260
|
+
])->unsafeFromDict
|
|
261
|
+
|
|
262
|
+
// Narrows a filter to the scope's chain. Cross-chain entities have no chain-id
|
|
263
|
+
// column, so their filter is left untouched.
|
|
264
|
+
let scoped = (filter: t, ~table: Table.table, ~scope: Internal.chainScope): t =>
|
|
265
|
+
switch (scope, table->Table.getChainIdField) {
|
|
266
|
+
| (Chain(chainId), Some(field)) =>
|
|
267
|
+
// Copied, because the filter the handler passed in must not gain a column
|
|
268
|
+
// it never asked for.
|
|
269
|
+
let scoped = filter->entries->Utils.Dict.shallowCopy
|
|
270
|
+
scoped->Dict.set(field.fieldName, dict{"_eq": chainId->(Utils.magic: ChainId.t => unknown)})
|
|
271
|
+
scoped->unsafeFromDict
|
|
272
|
+
| _ => filter
|
|
294
273
|
}
|
|
295
274
|
|
|
296
|
-
//
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
| And(_) => `${entityName}.getWhere({${filter->printOperationFilter(~paramsCount=ref(0))}})`
|
|
307
|
-
}
|
|
275
|
+
// Values bound to the query's $N placeholders, in the order it binds them.
|
|
276
|
+
let getParams = (filter: t) => {
|
|
277
|
+
let params = []
|
|
278
|
+
filter
|
|
279
|
+
->entries
|
|
280
|
+
->Utils.Dict.forEachWithKey((operators, _) =>
|
|
281
|
+
operators->Utils.Dict.forEachWithKey((fieldValue, _) => params->Array.push(fieldValue)->ignore)
|
|
282
|
+
)
|
|
283
|
+
params
|
|
284
|
+
}
|
|
308
285
|
|
|
309
|
-
//
|
|
310
|
-
//
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
let
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
| Lt({fieldValue}) => [fieldValue]
|
|
319
|
-
| In({fieldValue}) => fieldValue
|
|
320
|
-
| And(_) => {
|
|
321
|
-
let acc = []
|
|
322
|
-
let rec collect = (filter: t) =>
|
|
323
|
-
switch filter {
|
|
324
|
-
| Eq({fieldValue}) => acc->Array.push(fieldValue)->ignore
|
|
325
|
-
| Gt({fieldValue}) => acc->Array.push(fieldValue)->ignore
|
|
326
|
-
| Lt({fieldValue}) => acc->Array.push(fieldValue)->ignore
|
|
327
|
-
| In({fieldValue}) =>
|
|
328
|
-
acc->Array.push(fieldValue->(Utils.magic: array<unknown> => unknown))->ignore
|
|
329
|
-
| And({filters}) => filters->Array.forEach(collect)
|
|
330
|
-
}
|
|
331
|
-
collect(filter)
|
|
332
|
-
acc
|
|
286
|
+
// The one shape a value can key an index or a merged query by: a single field
|
|
287
|
+
// under a single operator.
|
|
288
|
+
let asSingleOperator = (filter: t) =>
|
|
289
|
+
switch filter->entries->Dict.keysToArray {
|
|
290
|
+
| [fieldName] =>
|
|
291
|
+
let operators = filter->entries->Dict.getUnsafe(fieldName)
|
|
292
|
+
switch operators->Dict.keysToArray {
|
|
293
|
+
| [operator] => Some((fieldName, operator, operators->Dict.getUnsafe(operator)))
|
|
294
|
+
| _ => None
|
|
333
295
|
}
|
|
296
|
+
| _ => None
|
|
334
297
|
}
|
|
335
298
|
|
|
336
299
|
// Collapses filters sharing an operation key into fewer storage queries:
|
|
337
|
-
//
|
|
338
|
-
//
|
|
300
|
+
// _eq and _in batches merge into a single _in on the field. The rest have no
|
|
301
|
+
// lossless single-query form without an Or operator, so they stay as is.
|
|
339
302
|
// Expects a homogeneous batch — filters with the same operation key.
|
|
340
303
|
// A mismatched filter throws: dropping it would leave its already
|
|
341
304
|
// registered index without the matching db rows, silently losing data.
|
|
342
305
|
let throwUnmergeable = (filter: t) =>
|
|
343
306
|
JsError.throwWithMessage(
|
|
344
|
-
`Unexpected
|
|
307
|
+
`Unexpected ${switch filter->asSingleOperator {
|
|
308
|
+
| Some((_, operator, _)) => operator
|
|
309
|
+
| None => "composite"
|
|
310
|
+
}} filter in a merged batch. Filters batched into a single query must use the same operator and field.`,
|
|
345
311
|
)
|
|
346
312
|
|
|
347
313
|
let merge = (filters: array<t>) =>
|
|
348
314
|
switch filters {
|
|
349
315
|
| [] | [_] => filters
|
|
350
316
|
| _ =>
|
|
351
|
-
switch filters->Array.getUnsafe(0) {
|
|
352
|
-
|
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
| _ => throwUnmergeable(filter)
|
|
371
|
-
}
|
|
372
|
-
)
|
|
373
|
-
->Array.flat,
|
|
374
|
-
}),
|
|
317
|
+
switch filters->Array.getUnsafe(0)->asSingleOperator {
|
|
318
|
+
| Some((fieldName, ("_eq" | "_in") as operator, _)) =>
|
|
319
|
+
let values = []
|
|
320
|
+
filters->Array.forEach(filter =>
|
|
321
|
+
switch filter->asSingleOperator {
|
|
322
|
+
| Some((candidateField, candidateOperator, fieldValue))
|
|
323
|
+
if candidateField === fieldName && candidateOperator === operator =>
|
|
324
|
+
if operator === "_in" {
|
|
325
|
+
fieldValue->asArray->Array.forEach(value => values->Array.push(value)->ignore)
|
|
326
|
+
} else {
|
|
327
|
+
values->Array.push(fieldValue)->ignore
|
|
328
|
+
}
|
|
329
|
+
| _ => throwUnmergeable(filter)
|
|
330
|
+
}
|
|
331
|
+
)
|
|
332
|
+
[
|
|
333
|
+
Dict.fromArray([
|
|
334
|
+
(fieldName, dict{"_in": values->(Utils.magic: array<unknown> => unknown)}),
|
|
335
|
+
])->unsafeFromDict,
|
|
375
336
|
]
|
|
376
|
-
|
|
|
337
|
+
| _ => filters
|
|
377
338
|
}
|
|
378
339
|
}
|
|
379
340
|
|
|
380
|
-
// A
|
|
381
|
-
//
|
|
382
|
-
//
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
341
|
+
// A predicate specialized to a single filter. The field's comparison is
|
|
342
|
+
// resolved once from the table config, so per-entity matching avoids both the
|
|
343
|
+
// operator dispatch and the polymorphic compare.
|
|
344
|
+
type matcher = Internal.entity => bool
|
|
345
|
+
|
|
346
|
+
// Reads a field off an entity by its API name. Indexed/queryable fields hold
|
|
347
|
+
// raw runtime values, so the result is compared directly.
|
|
348
|
+
@get_index external getField: (Internal.entity, string) => unknown = ""
|
|
349
|
+
|
|
350
|
+
// Compares (entityValue, filterValue) raw runtime values for one field. A
|
|
351
|
+
// nullish entity value (a missing or null column) matches nothing, mirroring
|
|
352
|
+
// SQL NULL semantics and the Postgres-side filter.
|
|
353
|
+
type valueCompare = {
|
|
354
|
+
eq: (unknown, unknown) => bool,
|
|
355
|
+
gt: (unknown, unknown) => bool,
|
|
356
|
+
lt: (unknown, unknown) => bool,
|
|
357
|
+
// Projects a value onto something a Map can key by. Primitives key by
|
|
358
|
+
// themselves; the object-shaped types have to collapse to a primitive or
|
|
359
|
+
// equal values would miss each other on identity.
|
|
360
|
+
key: unknown => unknown,
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
// `>`/`<` on `unknown` would compile to the polymorphic Primitive_object path; the raw
|
|
364
|
+
// operators give native JS comparison for primitive (string/number/bigint)
|
|
365
|
+
// fields. `===` is already physical equality and false for a nullish column,
|
|
366
|
+
// but `<` and `>` coerce a `null` column to 0 (a JS handler can write one for
|
|
367
|
+
// a nullable field), so the ordering operators check for it first.
|
|
368
|
+
let nativeEq = (a: unknown, b: unknown) => a === b
|
|
369
|
+
let nativeGt: (unknown, unknown) => bool = %raw(`(a, b) => a != null && a > b`)
|
|
370
|
+
let nativeLt: (unknown, unknown) => bool = %raw(`(a, b) => a != null && a < b`)
|
|
371
|
+
let identityKey = (v: unknown) => v
|
|
372
|
+
let native = {eq: nativeEq, gt: nativeGt, lt: nativeLt, key: identityKey}
|
|
373
|
+
|
|
374
|
+
// A nullable column holds null where a row has no value, and a comparison that
|
|
375
|
+
// calls a method on it would throw. Matching nothing is what SQL does, and
|
|
376
|
+
// saying so once here keeps every comparison below a plain two-value compare.
|
|
377
|
+
// The filter's own value needs no such guard — parseOrThrow rejects a nullish one.
|
|
378
|
+
let nullSafe = (compare: valueCompare): valueCompare => {
|
|
379
|
+
eq: (a, b) => !(a->nullish) && compare.eq(a, b),
|
|
380
|
+
gt: (a, b) => !(a->nullish) && compare.gt(a, b),
|
|
381
|
+
lt: (a, b) => !(a->nullish) && compare.lt(a, b),
|
|
382
|
+
key: compare.key,
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
let asBigDecimal = (v: unknown) => v->(Utils.magic: unknown => BigDecimal.t)
|
|
386
|
+
let bigDecimal = nullSafe({
|
|
387
|
+
eq: (a, b) => BigDecimal.equals(a->asBigDecimal, b->asBigDecimal),
|
|
388
|
+
gt: (a, b) => BigDecimal.gt(a->asBigDecimal, b->asBigDecimal),
|
|
389
|
+
lt: (a, b) => BigDecimal.lt(a->asBigDecimal, b->asBigDecimal),
|
|
390
|
+
key: v => v->asBigDecimal->BigDecimal.toString->(Utils.magic: string => unknown),
|
|
391
|
+
})
|
|
392
|
+
|
|
393
|
+
let getTime = (v: unknown) => v->(Utils.magic: unknown => Date.t)->Date.getTime
|
|
394
|
+
let date = nullSafe({
|
|
395
|
+
eq: (a, b) => getTime(a) === getTime(b),
|
|
396
|
+
gt: (a, b) => getTime(a) > getTime(b),
|
|
397
|
+
lt: (a, b) => getTime(a) < getTime(b),
|
|
398
|
+
key: v => v->getTime->(Utils.magic: float => unknown),
|
|
399
|
+
})
|
|
400
|
+
|
|
401
|
+
// Json has no meaningful ordering, so reuse the structural compare for every
|
|
402
|
+
// operator. Polymorphic `==` is intentional here.
|
|
403
|
+
let json = nullSafe({
|
|
404
|
+
eq: (a: unknown, b: unknown) => a == b,
|
|
405
|
+
gt: (a, b) => a > b,
|
|
406
|
+
lt: (a, b) => a < b,
|
|
407
|
+
key: v => v->jsonStringify->(Utils.magic: string => unknown),
|
|
408
|
+
})
|
|
409
|
+
|
|
410
|
+
let asBytes = (v: unknown) => v->(Utils.magic: unknown => Uint8Array.t)
|
|
411
|
+
let bytes = nullSafe({
|
|
412
|
+
eq: (a, b) => Utils.Bytes.compare(a->asBytes, b->asBytes) === 0.,
|
|
413
|
+
gt: (a, b) => Utils.Bytes.compare(a->asBytes, b->asBytes) > 0.,
|
|
414
|
+
lt: (a, b) => Utils.Bytes.compare(a->asBytes, b->asBytes) < 0.,
|
|
415
|
+
key: v => v->asBytes->Utils.Bytes.toHex->(Utils.magic: string => unknown),
|
|
416
|
+
})
|
|
417
|
+
|
|
418
|
+
// Postgres orders an enum by the position its members were declared in, not
|
|
419
|
+
// by their names. Equality stays native so an _in keeps the Set fast path.
|
|
420
|
+
let enum_ = (config: Table.enumConfig<Table.enum>): valueCompare => {
|
|
421
|
+
let ranks = Dict.make()
|
|
422
|
+
config.variants->Array.forEachWithIndex((variant, rank) =>
|
|
423
|
+
ranks->Dict.set(variant->(Utils.magic: Table.enum => string), rank)
|
|
424
|
+
)
|
|
425
|
+
let rank = (v: unknown) => ranks->Dict.getUnsafe(v->(Utils.magic: unknown => string))
|
|
426
|
+
{
|
|
427
|
+
eq: nativeEq,
|
|
428
|
+
gt: (a, b) => !(a->nullish) && rank(a) > rank(b),
|
|
429
|
+
lt: (a, b) => !(a->nullish) && rank(a) < rank(b),
|
|
430
|
+
key: identityKey,
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
let scalarCompare = (fieldType: Table.fieldType): valueCompare =>
|
|
435
|
+
switch fieldType {
|
|
436
|
+
| BigDecimal(_) => bigDecimal
|
|
437
|
+
| Date => date
|
|
438
|
+
| Json => json
|
|
439
|
+
| Bytea => bytes
|
|
440
|
+
| Enum({config}) => enum_(config)
|
|
441
|
+
| String
|
|
442
|
+
| Boolean
|
|
443
|
+
| Uint32
|
|
444
|
+
| UInt52
|
|
445
|
+
| SmallInt
|
|
446
|
+
| UInt64
|
|
447
|
+
| Int32
|
|
448
|
+
| ChainId
|
|
449
|
+
| Number
|
|
450
|
+
| BigInt(_)
|
|
451
|
+
| Serial
|
|
452
|
+
| BigSerial => native
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
// Array-valued fields compare element-wise with the element type's comparator:
|
|
456
|
+
// equality is length + pairwise eq, ordering is lexicographic where the first
|
|
457
|
+
// differing element decides and a proper prefix is the smaller array. The key
|
|
458
|
+
// follows the same rule, concatenating each element's own key, so a Date[]
|
|
459
|
+
// keys by epoch millis and a Bytea[] by hex without inspecting a value.
|
|
460
|
+
let arrayCompare = (element: valueCompare): valueCompare => {
|
|
461
|
+
let eq = (a, b) => {
|
|
462
|
+
let a = a->asArray
|
|
463
|
+
let b = b->asArray
|
|
464
|
+
let len = a->Array.length
|
|
465
|
+
len === b->Array.length && {
|
|
466
|
+
let rec go = i =>
|
|
467
|
+
i >= len || (element.eq(a->Array.getUnsafe(i), b->Array.getUnsafe(i)) && go(i + 1))
|
|
468
|
+
go(0)
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
let order = (~gt) =>
|
|
472
|
+
(a, b) => {
|
|
473
|
+
let a = a->asArray
|
|
474
|
+
let b = b->asArray
|
|
475
|
+
let la = a->Array.length
|
|
476
|
+
let lb = b->Array.length
|
|
477
|
+
let len = la < lb ? la : lb
|
|
478
|
+
let rec go = i =>
|
|
479
|
+
if i >= len {
|
|
480
|
+
gt ? la > lb : la < lb
|
|
481
|
+
} else {
|
|
482
|
+
let x = a->Array.getUnsafe(i)
|
|
483
|
+
let y = b->Array.getUnsafe(i)
|
|
484
|
+
if element.eq(x, y) {
|
|
485
|
+
go(i + 1)
|
|
486
|
+
} else if gt {
|
|
487
|
+
element.gt(x, y)
|
|
488
|
+
} else {
|
|
489
|
+
element.lt(x, y)
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
go(0)
|
|
493
|
+
}
|
|
494
|
+
nullSafe({
|
|
495
|
+
eq,
|
|
496
|
+
gt: order(~gt=true),
|
|
497
|
+
lt: order(~gt=false),
|
|
498
|
+
key: v => {
|
|
499
|
+
let key = ref("")
|
|
500
|
+
v->asArray->Array.forEach(item => key := key.contents ++ encodeValueKey(element.key(item)))
|
|
501
|
+
key.contents->(Utils.magic: string => unknown)
|
|
502
|
+
},
|
|
503
|
+
})
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
// Built once per table: an array field's comparator closes over its element's,
|
|
507
|
+
// so resolving it per lookup would allocate that chain on every getWhere.
|
|
508
|
+
let comparesByField: Table.table => dict<
|
|
509
|
+
valueCompare,
|
|
510
|
+
> = Utils.WeakMap.memoize((table: Table.table) => {
|
|
511
|
+
let compares = Dict.make()
|
|
512
|
+
table.fields->Array.forEach(field =>
|
|
513
|
+
switch field {
|
|
514
|
+
| Field(field) =>
|
|
515
|
+
let element = scalarCompare(field.fieldType)
|
|
516
|
+
compares->Dict.set(
|
|
517
|
+
field->Table.getApiFieldName,
|
|
518
|
+
field.isArray ? arrayCompare(element) : element,
|
|
395
519
|
)
|
|
520
|
+
| DerivedFrom(_) => ()
|
|
396
521
|
}
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
522
|
+
)
|
|
523
|
+
compares
|
|
524
|
+
})
|
|
525
|
+
|
|
526
|
+
let fieldCompare = (~table: Table.table, fieldName) =>
|
|
527
|
+
switch table->comparesByField->Utils.Dict.dangerouslyGetNonOption(fieldName) {
|
|
528
|
+
| Some(compare) => compare
|
|
529
|
+
| None =>
|
|
530
|
+
JsError.throwWithMessage(
|
|
531
|
+
`Internal error: no comparator for the field "${fieldName}" on the table "${table.tableName}". A filter is parsed against its table before it reaches the in-memory matcher.`,
|
|
532
|
+
)
|
|
400
533
|
}
|
|
401
534
|
|
|
402
|
-
//
|
|
403
|
-
//
|
|
404
|
-
let
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
535
|
+
// Projects a field's values onto Map keys, so an index can be found by value
|
|
536
|
+
// instead of by a serialized filter.
|
|
537
|
+
let makeValueKey = (~table: Table.table, ~fieldName) => (fieldName->fieldCompare(~table)).key
|
|
538
|
+
|
|
539
|
+
// The cache key stands in for structural equality between filters. Values go
|
|
540
|
+
// through the same per-field projection the equality buckets key by, so the
|
|
541
|
+
// two agree on which values are distinct.
|
|
542
|
+
//
|
|
543
|
+
// Each field/operator entry ends with ";", which no field name or value key
|
|
544
|
+
// starts with, so an _in's value list can't run into the next field name: a
|
|
545
|
+
// one-letter key like "t" for true followed by field "b" would otherwise read
|
|
546
|
+
// the same as an empty list followed by field "tb".
|
|
547
|
+
let toString = (filter: t, ~table: Table.table) => {
|
|
548
|
+
let key = ref("")
|
|
549
|
+
filter
|
|
550
|
+
->entries
|
|
551
|
+
->Utils.Dict.forEachWithKey((operators, fieldName) => {
|
|
552
|
+
let keyOf = makeValueKey(~table, ~fieldName)
|
|
553
|
+
operators->Utils.Dict.forEachWithKey((fieldValue, operator) => {
|
|
554
|
+
key := key.contents ++ fieldName ++ operator
|
|
555
|
+
if operator === "_in" {
|
|
556
|
+
fieldValue
|
|
557
|
+
->asArray
|
|
558
|
+
->Array.forEach(value => key := key.contents ++ encodeValueKey(keyOf(value)))
|
|
559
|
+
} else {
|
|
560
|
+
key := key.contents ++ encodeValueKey(keyOf(fieldValue))
|
|
561
|
+
}
|
|
562
|
+
key := key.contents ++ ";"
|
|
563
|
+
})
|
|
564
|
+
})
|
|
565
|
+
key.contents
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
// Values are replaced by placeholders so calls that differ only in what they
|
|
569
|
+
// filter for batch together.
|
|
570
|
+
let toOperationKey = (filter: t, ~entityName) => {
|
|
571
|
+
let params = ref(0)
|
|
572
|
+
let printed = ref("")
|
|
573
|
+
filter
|
|
574
|
+
->entries
|
|
575
|
+
->Utils.Dict.forEachWithKey((operators, fieldName) => {
|
|
576
|
+
let ops = ref("")
|
|
577
|
+
// An _eq on its own is written without the operator, the way the user does.
|
|
578
|
+
let loneEq = ref(false)
|
|
579
|
+
operators->Utils.Dict.forEachWithKey((_, operator) => {
|
|
580
|
+
params := params.contents + 1
|
|
581
|
+
let part = `${operator}: $${params.contents->Int.toString}`
|
|
582
|
+
loneEq := ops.contents === "" && operator === "_eq"
|
|
583
|
+
ops := (ops.contents === "" ? part : ops.contents ++ ", " ++ part)
|
|
423
584
|
})
|
|
424
|
-
|
|
585
|
+
let part = loneEq.contents
|
|
586
|
+
? `${fieldName}: $${params.contents->Int.toString}`
|
|
587
|
+
: `${fieldName}: {${ops.contents}}`
|
|
588
|
+
printed := (printed.contents === "" ? part : printed.contents ++ ", " ++ part)
|
|
589
|
+
})
|
|
590
|
+
`${entityName}.getWhere({${printed.contents}})`
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
let makeMatcher = (filter: t, ~table: Table.table): matcher => {
|
|
594
|
+
let checks = []
|
|
595
|
+
filter
|
|
596
|
+
->entries
|
|
597
|
+
->Utils.Dict.forEachWithKey((operators, fieldName) => {
|
|
598
|
+
let compare = fieldName->fieldCompare(~table)
|
|
599
|
+
operators->Utils.Dict.forEachWithKey((fieldValue, operator) => {
|
|
600
|
+
let check = switch operator {
|
|
601
|
+
| "_eq" => entity => compare.eq(entity->getField(fieldName), fieldValue)
|
|
602
|
+
| "_gt" => entity => compare.gt(entity->getField(fieldName), fieldValue)
|
|
603
|
+
| "_lt" => entity => compare.lt(entity->getField(fieldName), fieldValue)
|
|
604
|
+
| "_gte" =>
|
|
605
|
+
entity => {
|
|
606
|
+
let entityFieldValue = entity->getField(fieldName)
|
|
607
|
+
compare.eq(entityFieldValue, fieldValue) || compare.gt(entityFieldValue, fieldValue)
|
|
608
|
+
}
|
|
609
|
+
| "_lte" =>
|
|
610
|
+
entity => {
|
|
611
|
+
let entityFieldValue = entity->getField(fieldName)
|
|
612
|
+
compare.eq(entityFieldValue, fieldValue) || compare.lt(entityFieldValue, fieldValue)
|
|
613
|
+
}
|
|
614
|
+
| "_in" =>
|
|
615
|
+
let fieldValues = fieldValue->asArray
|
|
616
|
+
if compare.eq === nativeEq {
|
|
617
|
+
let set = fieldValues->Utils.Set.fromArray
|
|
618
|
+
entity => set->Utils.Set.has(entity->getField(fieldName))
|
|
619
|
+
} else {
|
|
620
|
+
entity => {
|
|
621
|
+
let entityFieldValue = entity->getField(fieldName)
|
|
622
|
+
fieldValues->Array.some(candidate => compare.eq(entityFieldValue, candidate))
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
| _ =>
|
|
626
|
+
JsError.throwWithMessage(
|
|
627
|
+
`Invalid operator "${operator}" in a getWhere filter. Valid operators are _eq, _gt, _lt, _gte, _lte, _in.`,
|
|
628
|
+
)
|
|
629
|
+
}
|
|
630
|
+
checks->Array.push(check)->ignore
|
|
631
|
+
})
|
|
632
|
+
})
|
|
633
|
+
switch checks {
|
|
634
|
+
| [check] => check
|
|
635
|
+
| _ => entity => checks->Array.every(check => check(entity))
|
|
425
636
|
}
|
|
637
|
+
}
|