envio 3.14.0 → 3.14.1-resolvers.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/evm.schema.json +8 -1
- package/fuel.schema.json +7 -0
- package/index.d.ts +258 -5
- package/index.js +10 -0
- package/package.json +6 -6
- package/src/Bin.res +48 -0
- package/src/Bin.res.mjs +34 -1
- package/src/Config.res +12 -2
- package/src/Config.res.mjs +7 -0
- package/src/Env.res +141 -1
- package/src/Env.res.mjs +133 -1
- package/src/HandlerLoader.res +7 -13
- package/src/HandlerLoader.res.mjs +7 -14
- package/src/HandlerTypeCheck.res +207 -0
- package/src/HandlerTypeCheck.res.mjs +262 -0
- package/src/bindings/NodeJs.res +31 -0
- package/src/bindings/NodeJs.res.mjs +3 -0
- package/src/resolvers/ResolverProcess.res +805 -0
- package/src/resolvers/ResolverProcess.res.mjs +662 -0
- package/src/resolvers/ResolverQuery.res +313 -0
- package/src/resolvers/ResolverQuery.res.mjs +220 -0
- package/src/resolvers/collisions.js +108 -0
- package/src/resolvers/db.js +363 -0
- package/src/resolvers/dispatch.js +259 -0
- package/src/resolvers/errors.js +30 -0
- package/src/resolvers/graphqlSelection.js +292 -0
- package/src/resolvers/hasuraAction.js +84 -0
- package/src/resolvers/hasuraApply.js +333 -0
- package/src/resolvers/hasuraMetadata.js +137 -0
- package/src/resolvers/index.js +123 -0
- package/src/resolvers/manifest.js +376 -0
- package/src/resolvers/server.js +304 -0
- package/src/sources/EvmRpcClient.res +2 -0
- package/src/sources/RpcSource.res +17 -4
- package/src/sources/RpcSource.res.mjs +23 -5
- package/src/sources/SourceManager.res +63 -40
- package/src/sources/SourceManager.res.mjs +32 -20
- package/src/tui/Tui.res +1 -1
- package/src/tui/Tui.res.mjs +1 -1
- package/svm.schema.json +7 -0
- package/src/HandlerTypeCheck.mjs +0 -14
- package/src/HandlerTypeCheckWorker.mjs +0 -68
|
@@ -0,0 +1,313 @@
|
|
|
1
|
+
// Written against the entity table definitions rather than raw rows, so the
|
|
2
|
+
// resolver path reads entities the way the indexer writes them: column names
|
|
3
|
+
// come from the table's own field mapping, filter values are serialized with
|
|
4
|
+
// each field's schema, and rows are decoded with `Table.pgRowsSchema`. A
|
|
5
|
+
// `db.find` therefore hands back what a handler's `context.<Entity>.get` does,
|
|
6
|
+
// BigInt and enum values included.
|
|
7
|
+
//
|
|
8
|
+
// The operator vocabulary is `getWhere`'s, so one filter syntax spans handlers
|
|
9
|
+
// and resolvers. The translation is deliberately not shared with
|
|
10
|
+
// `EntityFilter.parseGetWhereOrThrow`: that expands `_in` and `_gte` into a
|
|
11
|
+
// disjunction for the load layer to memoize per value, which here would turn
|
|
12
|
+
// one indexed query into several.
|
|
13
|
+
|
|
14
|
+
type orderBy = {field: string, direction: string}
|
|
15
|
+
|
|
16
|
+
type findOptions = {
|
|
17
|
+
entityConfig: Internal.entityConfig,
|
|
18
|
+
pgSchema: string,
|
|
19
|
+
where?: dict<unknown>,
|
|
20
|
+
orderBy?: array<orderBy>,
|
|
21
|
+
limit?: int,
|
|
22
|
+
offset?: int,
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
type query = {text: string, params: array<JSON.t>}
|
|
26
|
+
|
|
27
|
+
let quoted = (~pgSchema, ~tableName) => `"${pgSchema}"."${tableName}"`
|
|
28
|
+
|
|
29
|
+
let getQueryFieldOrThrow = (~table: Table.table, ~entityName, ~apiFieldName, ~where) =>
|
|
30
|
+
switch table->Table.queryFields->Dict.get(apiFieldName) {
|
|
31
|
+
| Some(queryField) => queryField
|
|
32
|
+
| None =>
|
|
33
|
+
JsError.throwWithMessage(
|
|
34
|
+
`Invalid field "${apiFieldName}" in db.find("${entityName}", { ${where}: ... }). The entity has no such field.`,
|
|
35
|
+
)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
let bindOrThrow = (
|
|
39
|
+
~queryField: Table.queryField,
|
|
40
|
+
~entityName,
|
|
41
|
+
~apiFieldName,
|
|
42
|
+
~operator,
|
|
43
|
+
~value: unknown,
|
|
44
|
+
~isArray,
|
|
45
|
+
~params: array<JSON.t>,
|
|
46
|
+
) => {
|
|
47
|
+
let json = try value->S.reverseConvertToJsonOrThrow(
|
|
48
|
+
isArray ? queryField.arrayFieldSchema : queryField.fieldSchema,
|
|
49
|
+
) catch {
|
|
50
|
+
| exn =>
|
|
51
|
+
let reason = switch exn {
|
|
52
|
+
| JsExn(e) => e->JsExn.message->Option.getOr("it isn't the field's type")
|
|
53
|
+
| _ => "it isn't the field's type"
|
|
54
|
+
}
|
|
55
|
+
JsError.throwWithMessage(
|
|
56
|
+
`Invalid value for db.find("${entityName}", { where: { ${apiFieldName}: { ${operator}: ... } } }): ${reason}`,
|
|
57
|
+
)
|
|
58
|
+
}
|
|
59
|
+
params->Array.push(json)->ignore
|
|
60
|
+
`$${params->Array.length->Int.toString}`
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
let makeWhereCondition = (~table, ~entityName, ~where: dict<unknown>, ~params) => {
|
|
64
|
+
let apiFieldNames = where->Dict.keysToArray
|
|
65
|
+
if apiFieldNames->Array.length === 0 {
|
|
66
|
+
JsError.throwWithMessage(
|
|
67
|
+
`Empty where passed to db.find("${entityName}"). Drop it, or filter like { fieldName: { _eq: value } }.`,
|
|
68
|
+
)
|
|
69
|
+
}
|
|
70
|
+
apiFieldNames
|
|
71
|
+
->Array.flatMap(apiFieldName => {
|
|
72
|
+
let queryField = getQueryFieldOrThrow(~table, ~entityName, ~apiFieldName, ~where="where")
|
|
73
|
+
let operators = where->Dict.getUnsafe(apiFieldName)
|
|
74
|
+
if operators->typeof !== #object || operators->Array.isArray || operators === %raw(`null`) {
|
|
75
|
+
JsError.throwWithMessage(
|
|
76
|
+
`Invalid value in db.find("${entityName}", { where: { ${apiFieldName}: ... } }). Provide an operator like { _eq: value }.`,
|
|
77
|
+
)
|
|
78
|
+
}
|
|
79
|
+
let operators = operators->(Utils.magic: unknown => dict<unknown>)
|
|
80
|
+
let operatorKeys = operators->Dict.keysToArray
|
|
81
|
+
if operatorKeys->Array.length === 0 {
|
|
82
|
+
JsError.throwWithMessage(
|
|
83
|
+
`Empty operator in db.find("${entityName}", { where: { ${apiFieldName}: {} } }). Valid operators are _eq, _gt, _lt, _gte, _lte, _in.`,
|
|
84
|
+
)
|
|
85
|
+
}
|
|
86
|
+
operatorKeys->Array.map(operator => {
|
|
87
|
+
let value = operators->Dict.getUnsafe(operator)
|
|
88
|
+
let bind = (~isArray=false) =>
|
|
89
|
+
bindOrThrow(~queryField, ~entityName, ~apiFieldName, ~operator, ~value, ~isArray, ~params)
|
|
90
|
+
let column = `"${queryField.pgDbFieldName}"`
|
|
91
|
+
switch operator {
|
|
92
|
+
| "_eq" => `${column} = ${bind()}`
|
|
93
|
+
| "_gt" => `${column} > ${bind()}`
|
|
94
|
+
| "_lt" => `${column} < ${bind()}`
|
|
95
|
+
| "_gte" => `${column} >= ${bind()}`
|
|
96
|
+
| "_lte" => `${column} <= ${bind()}`
|
|
97
|
+
| "_in" =>
|
|
98
|
+
if !(value->Array.isArray) {
|
|
99
|
+
JsError.throwWithMessage(
|
|
100
|
+
`Invalid value in db.find("${entityName}", { where: { ${apiFieldName}: { _in: ... } } }). _in takes an array.`,
|
|
101
|
+
)
|
|
102
|
+
}
|
|
103
|
+
`${column} = ANY(${bind(~isArray=true)})`
|
|
104
|
+
| _ =>
|
|
105
|
+
JsError.throwWithMessage(
|
|
106
|
+
`Invalid operator "${operator}" in db.find("${entityName}", { where: { ${apiFieldName}: { ${operator}: ... } } }). Valid operators are _eq, _gt, _lt, _gte, _lte, _in.`,
|
|
107
|
+
)
|
|
108
|
+
}
|
|
109
|
+
})
|
|
110
|
+
})
|
|
111
|
+
->Array.join(" AND ")
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
let makeOrderByClause = (~table, ~entityName, ~orderBy: array<orderBy>) =>
|
|
115
|
+
switch orderBy {
|
|
116
|
+
| [] => ""
|
|
117
|
+
| orderBy =>
|
|
118
|
+
" ORDER BY " ++
|
|
119
|
+
orderBy
|
|
120
|
+
->Array.map(({field, direction}) => {
|
|
121
|
+
let queryField = getQueryFieldOrThrow(
|
|
122
|
+
~table,
|
|
123
|
+
~entityName,
|
|
124
|
+
~apiFieldName=field,
|
|
125
|
+
~where="orderBy",
|
|
126
|
+
)
|
|
127
|
+
let direction = switch direction->String.toLowerCase {
|
|
128
|
+
| "asc" => "ASC"
|
|
129
|
+
| "desc" => "DESC"
|
|
130
|
+
| other =>
|
|
131
|
+
JsError.throwWithMessage(
|
|
132
|
+
`Invalid direction "${other}" in db.find("${entityName}", { orderBy: [{ field: "${field}" }] }). Use "asc" or "desc".`,
|
|
133
|
+
)
|
|
134
|
+
}
|
|
135
|
+
`"${queryField.pgDbFieldName}" ${direction}`
|
|
136
|
+
})
|
|
137
|
+
->Array.join(", ")
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Bound rather than spliced: a count reaching SQL as a parameter can't widen
|
|
141
|
+
// the statement no matter where the resolver got it from.
|
|
142
|
+
let makeBoundCount = (~keyword, ~entityName, ~count, ~params: array<JSON.t>) => {
|
|
143
|
+
if count < 0 || count->Int.toFloat->Math.trunc !== count->Int.toFloat {
|
|
144
|
+
JsError.throwWithMessage(
|
|
145
|
+
`Invalid ${keyword->String.toLowerCase} ${count->Int.toString} in db.find("${entityName}"). It must be a non-negative whole number.`,
|
|
146
|
+
)
|
|
147
|
+
}
|
|
148
|
+
params->Array.push(JSON.Encode.int(count))->ignore
|
|
149
|
+
` ${keyword} $${params->Array.length->Int.toString}`
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
let makeFindQuery = (options: findOptions): query => {
|
|
153
|
+
let {entityConfig, pgSchema} = options
|
|
154
|
+
let table = entityConfig.table
|
|
155
|
+
let entityName = entityConfig.name
|
|
156
|
+
let params = []
|
|
157
|
+
|
|
158
|
+
let whereClause = switch options.where {
|
|
159
|
+
| None => ""
|
|
160
|
+
| Some(where) => " WHERE " ++ makeWhereCondition(~table, ~entityName, ~where, ~params)
|
|
161
|
+
}
|
|
162
|
+
let orderByClause = switch options.orderBy {
|
|
163
|
+
| None => ""
|
|
164
|
+
| Some(orderBy) => makeOrderByClause(~table, ~entityName, ~orderBy)
|
|
165
|
+
}
|
|
166
|
+
let limitClause = switch options.limit {
|
|
167
|
+
| None => ""
|
|
168
|
+
| Some(count) => makeBoundCount(~keyword="LIMIT", ~entityName, ~count, ~params)
|
|
169
|
+
}
|
|
170
|
+
let offsetClause = switch options.offset {
|
|
171
|
+
| None => ""
|
|
172
|
+
| Some(count) => makeBoundCount(~keyword="OFFSET", ~entityName, ~count, ~params)
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
{
|
|
176
|
+
text: `SELECT * FROM ${quoted(
|
|
177
|
+
~pgSchema,
|
|
178
|
+
~tableName=table.tableName,
|
|
179
|
+
)}${whereClause}${orderByClause}${limitClause}${offsetClause};`,
|
|
180
|
+
params,
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// `id` alone identifies a row only on a cross-chain entity: a per-chain table
|
|
185
|
+
// is keyed by (id, chain id), so a bare `get` there would answer with whichever
|
|
186
|
+
// chain's row Postgres reached first.
|
|
187
|
+
let makeGetQuery = (entityConfig: Internal.entityConfig, pgSchema: string, id: unknown): query => {
|
|
188
|
+
let table = entityConfig.table
|
|
189
|
+
let entityName = entityConfig.name
|
|
190
|
+
switch table->Table.getChainIdField {
|
|
191
|
+
| Some(_) =>
|
|
192
|
+
JsError.throwWithMessage(
|
|
193
|
+
`db.get("${entityName}", id) is ambiguous: the entity is per-chain, so the same id can exist on every chain. Use db.find("${entityName}", { where: { id: { _eq: id }, ${Config.chainIdFieldName}: { _eq: chainId } } }).`,
|
|
194
|
+
)
|
|
195
|
+
| None => ()
|
|
196
|
+
}
|
|
197
|
+
// Bound through the id field's own schema, like every other value: the
|
|
198
|
+
// loaders must not be the one place a value reaches SQL unconverted.
|
|
199
|
+
let params = []
|
|
200
|
+
let queryField = getQueryFieldOrThrow(
|
|
201
|
+
~table,
|
|
202
|
+
~entityName,
|
|
203
|
+
~apiFieldName=Table.idFieldName,
|
|
204
|
+
~where="where",
|
|
205
|
+
)
|
|
206
|
+
let placeholder = bindOrThrow(
|
|
207
|
+
~queryField,
|
|
208
|
+
~entityName,
|
|
209
|
+
~apiFieldName=Table.idFieldName,
|
|
210
|
+
~operator="_eq",
|
|
211
|
+
~value=id,
|
|
212
|
+
~isArray=false,
|
|
213
|
+
~params,
|
|
214
|
+
)
|
|
215
|
+
{
|
|
216
|
+
text: `SELECT * FROM ${quoted(
|
|
217
|
+
~pgSchema,
|
|
218
|
+
~tableName=table.tableName,
|
|
219
|
+
)} WHERE "${queryField.pgDbFieldName}" = ${placeholder} LIMIT 1;`,
|
|
220
|
+
params,
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
let decodeRows = (entityConfig: Internal.entityConfig, rows: array<unknown>): array<unknown> =>
|
|
225
|
+
rows->S.parseOrThrow(entityConfig.table->Table.pgRowsSchema)
|
|
226
|
+
|
|
227
|
+
type chainHeight = {
|
|
228
|
+
chainId: ChainId.t,
|
|
229
|
+
ecosystem: string,
|
|
230
|
+
startBlock: int,
|
|
231
|
+
endBlock: Null.t<int>,
|
|
232
|
+
sourceBlock: int,
|
|
233
|
+
bufferBlock: int,
|
|
234
|
+
progressBlock: int,
|
|
235
|
+
readyAt: Null.t<Date.t>,
|
|
236
|
+
isReady: bool,
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// `envio_chains` is read by column rather than with `Table.pgRowsSchema`: the
|
|
240
|
+
// table's BIGINT columns come back from the driver as strings, which is why
|
|
241
|
+
// every other raw read of it casts on the way out. Only the watermark columns
|
|
242
|
+
// are selected, so none of those are in the result at all.
|
|
243
|
+
let chainColumns: array<InternalTable.Chains.field> = [
|
|
244
|
+
#id,
|
|
245
|
+
#ecosystem,
|
|
246
|
+
#start_block,
|
|
247
|
+
#end_block,
|
|
248
|
+
#source_block,
|
|
249
|
+
#buffer_block,
|
|
250
|
+
#progress_block,
|
|
251
|
+
#ready_at,
|
|
252
|
+
]
|
|
253
|
+
|
|
254
|
+
type chainRow = {
|
|
255
|
+
id: unknown,
|
|
256
|
+
ecosystem: string,
|
|
257
|
+
startBlock: int,
|
|
258
|
+
endBlock: option<int>,
|
|
259
|
+
sourceBlock: int,
|
|
260
|
+
bufferBlock: int,
|
|
261
|
+
progressBlock: int,
|
|
262
|
+
readyAt: option<Date.t>,
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
let chainRowsSchema = S.array(
|
|
266
|
+
S.object((s): chainRow => {
|
|
267
|
+
id: s.field((#id: InternalTable.Chains.field :> string), S.unknown),
|
|
268
|
+
ecosystem: s.field((#ecosystem: InternalTable.Chains.field :> string), S.string),
|
|
269
|
+
startBlock: s.field((#start_block: InternalTable.Chains.field :> string), S.int),
|
|
270
|
+
endBlock: s.field((#end_block: InternalTable.Chains.field :> string), S.null(S.int)),
|
|
271
|
+
sourceBlock: s.field((#source_block: InternalTable.Chains.field :> string), S.int),
|
|
272
|
+
bufferBlock: s.field((#buffer_block: InternalTable.Chains.field :> string), S.int),
|
|
273
|
+
progressBlock: s.field((#progress_block: InternalTable.Chains.field :> string), S.int),
|
|
274
|
+
readyAt: s.field(
|
|
275
|
+
(#ready_at: InternalTable.Chains.field :> string),
|
|
276
|
+
S.null(Utils.Schema.dbDate),
|
|
277
|
+
),
|
|
278
|
+
}),
|
|
279
|
+
)
|
|
280
|
+
|
|
281
|
+
let makeChainHeightsQuery = (pgSchema: string): query => {
|
|
282
|
+
text: `SELECT ${chainColumns
|
|
283
|
+
->Array.map(column => `"${(column :> string)}"`)
|
|
284
|
+
->Array.join(", ")} FROM ${quoted(
|
|
285
|
+
~pgSchema,
|
|
286
|
+
~tableName=InternalTable.Chains.table.tableName,
|
|
287
|
+
)};`,
|
|
288
|
+
params: [],
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
let decodeChainHeights = (rows: array<unknown>): dict<chainHeight> => {
|
|
292
|
+
let heights = Dict.make()
|
|
293
|
+
rows
|
|
294
|
+
->S.parseOrThrow(chainRowsSchema)
|
|
295
|
+
->Array.forEach(row => {
|
|
296
|
+
let chainId = row.id->ChainId.normalizeOrThrow
|
|
297
|
+
heights->Dict.set(
|
|
298
|
+
chainId->ChainId.toString,
|
|
299
|
+
{
|
|
300
|
+
chainId,
|
|
301
|
+
ecosystem: row.ecosystem,
|
|
302
|
+
startBlock: row.startBlock,
|
|
303
|
+
endBlock: row.endBlock->Null.fromOption,
|
|
304
|
+
sourceBlock: row.sourceBlock,
|
|
305
|
+
bufferBlock: row.bufferBlock,
|
|
306
|
+
progressBlock: row.progressBlock,
|
|
307
|
+
readyAt: row.readyAt->Null.fromOption,
|
|
308
|
+
isReady: row.readyAt->Option.isSome,
|
|
309
|
+
},
|
|
310
|
+
)
|
|
311
|
+
})
|
|
312
|
+
heights
|
|
313
|
+
}
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
// Generated by ReScript, PLEASE EDIT WITH CARE
|
|
2
|
+
|
|
3
|
+
import * as Table from "../db/Table.res.mjs";
|
|
4
|
+
import * as Utils from "../Utils.res.mjs";
|
|
5
|
+
import * as Config from "../Config.res.mjs";
|
|
6
|
+
import * as ChainId from "../ChainId.res.mjs";
|
|
7
|
+
import * as Stdlib_Null from "@rescript/runtime/lib/es6/Stdlib_Null.js";
|
|
8
|
+
import * as Stdlib_JsExn from "@rescript/runtime/lib/es6/Stdlib_JsExn.js";
|
|
9
|
+
import * as InternalTable from "../db/InternalTable.res.mjs";
|
|
10
|
+
import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
|
|
11
|
+
import * as Stdlib_JsError from "@rescript/runtime/lib/es6/Stdlib_JsError.js";
|
|
12
|
+
import * as S$RescriptSchema from "rescript-schema/src/S.res.mjs";
|
|
13
|
+
import * as Primitive_exceptions from "@rescript/runtime/lib/es6/Primitive_exceptions.js";
|
|
14
|
+
|
|
15
|
+
function quoted(pgSchema, tableName) {
|
|
16
|
+
return `"` + pgSchema + `"."` + tableName + `"`;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function getQueryFieldOrThrow(table, entityName, apiFieldName, where) {
|
|
20
|
+
let queryField = Table.queryFields(table)[apiFieldName];
|
|
21
|
+
if (queryField !== undefined) {
|
|
22
|
+
return queryField;
|
|
23
|
+
} else {
|
|
24
|
+
return Stdlib_JsError.throwWithMessage(`Invalid field "` + apiFieldName + `" in db.find("` + entityName + `", { ` + where + `: ... }). The entity has no such field.`);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function bindOrThrow(queryField, entityName, apiFieldName, operator, value, isArray, params) {
|
|
29
|
+
let json;
|
|
30
|
+
try {
|
|
31
|
+
json = S$RescriptSchema.reverseConvertToJsonOrThrow(value, isArray ? queryField.arrayFieldSchema : queryField.fieldSchema);
|
|
32
|
+
} catch (raw_exn) {
|
|
33
|
+
let exn = Primitive_exceptions.internalToException(raw_exn);
|
|
34
|
+
let reason = exn.RE_EXN_ID === "JsExn" ? Stdlib_Option.getOr(Stdlib_JsExn.message(exn._1), "it isn't the field's type") : "it isn't the field's type";
|
|
35
|
+
json = Stdlib_JsError.throwWithMessage(`Invalid value for db.find("` + entityName + `", { where: { ` + apiFieldName + `: { ` + operator + `: ... } } }): ` + reason);
|
|
36
|
+
}
|
|
37
|
+
params.push(json);
|
|
38
|
+
return `$` + params.length.toString();
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function makeWhereCondition(table, entityName, where, params) {
|
|
42
|
+
let apiFieldNames = Object.keys(where);
|
|
43
|
+
if (apiFieldNames.length === 0) {
|
|
44
|
+
Stdlib_JsError.throwWithMessage(`Empty where passed to db.find("` + entityName + `"). Drop it, or filter like { fieldName: { _eq: value } }.`);
|
|
45
|
+
}
|
|
46
|
+
return apiFieldNames.flatMap(apiFieldName => {
|
|
47
|
+
let queryField = getQueryFieldOrThrow(table, entityName, apiFieldName, "where");
|
|
48
|
+
let operators = where[apiFieldName];
|
|
49
|
+
if (typeof operators !== "object" || Array.isArray(operators) || operators === null) {
|
|
50
|
+
Stdlib_JsError.throwWithMessage(`Invalid value in db.find("` + entityName + `", { where: { ` + apiFieldName + `: ... } }). Provide an operator like { _eq: value }.`);
|
|
51
|
+
}
|
|
52
|
+
let operatorKeys = Object.keys(operators);
|
|
53
|
+
if (operatorKeys.length === 0) {
|
|
54
|
+
Stdlib_JsError.throwWithMessage(`Empty operator in db.find("` + entityName + `", { where: { ` + apiFieldName + `: {} } }). Valid operators are _eq, _gt, _lt, _gte, _lte, _in.`);
|
|
55
|
+
}
|
|
56
|
+
return operatorKeys.map(operator => {
|
|
57
|
+
let value = operators[operator];
|
|
58
|
+
let bind = isArrayOpt => {
|
|
59
|
+
let isArray = isArrayOpt !== undefined ? isArrayOpt : false;
|
|
60
|
+
return bindOrThrow(queryField, entityName, apiFieldName, operator, value, isArray, params);
|
|
61
|
+
};
|
|
62
|
+
let column = `"` + queryField.pgDbFieldName + `"`;
|
|
63
|
+
switch (operator) {
|
|
64
|
+
case "_eq" :
|
|
65
|
+
return column + ` = ` + bind(undefined);
|
|
66
|
+
case "_gt" :
|
|
67
|
+
return column + ` > ` + bind(undefined);
|
|
68
|
+
case "_gte" :
|
|
69
|
+
return column + ` >= ` + bind(undefined);
|
|
70
|
+
case "_in" :
|
|
71
|
+
if (!Array.isArray(value)) {
|
|
72
|
+
Stdlib_JsError.throwWithMessage(`Invalid value in db.find("` + entityName + `", { where: { ` + apiFieldName + `: { _in: ... } } }). _in takes an array.`);
|
|
73
|
+
}
|
|
74
|
+
return column + ` = ANY(` + bind(true) + `)`;
|
|
75
|
+
case "_lt" :
|
|
76
|
+
return column + ` < ` + bind(undefined);
|
|
77
|
+
case "_lte" :
|
|
78
|
+
return column + ` <= ` + bind(undefined);
|
|
79
|
+
default:
|
|
80
|
+
return Stdlib_JsError.throwWithMessage(`Invalid operator "` + operator + `" in db.find("` + entityName + `", { where: { ` + apiFieldName + `: { ` + operator + `: ... } } }). Valid operators are _eq, _gt, _lt, _gte, _lte, _in.`);
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
}).join(" AND ");
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function makeOrderByClause(table, entityName, orderBy) {
|
|
87
|
+
if (orderBy.length !== 0) {
|
|
88
|
+
return " ORDER BY " + orderBy.map(param => {
|
|
89
|
+
let field = param.field;
|
|
90
|
+
let queryField = getQueryFieldOrThrow(table, entityName, field, "orderBy");
|
|
91
|
+
let other = param.direction.toLowerCase();
|
|
92
|
+
let direction;
|
|
93
|
+
switch (other) {
|
|
94
|
+
case "asc" :
|
|
95
|
+
direction = "ASC";
|
|
96
|
+
break;
|
|
97
|
+
case "desc" :
|
|
98
|
+
direction = "DESC";
|
|
99
|
+
break;
|
|
100
|
+
default:
|
|
101
|
+
direction = Stdlib_JsError.throwWithMessage(`Invalid direction "` + other + `" in db.find("` + entityName + `", { orderBy: [{ field: "` + field + `" }] }). Use "asc" or "desc".`);
|
|
102
|
+
}
|
|
103
|
+
return `"` + queryField.pgDbFieldName + `" ` + direction;
|
|
104
|
+
}).join(", ");
|
|
105
|
+
} else {
|
|
106
|
+
return "";
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function makeBoundCount(keyword, entityName, count, params) {
|
|
111
|
+
if (count < 0 || Math.trunc(count) !== count) {
|
|
112
|
+
Stdlib_JsError.throwWithMessage(`Invalid ` + keyword.toLowerCase() + ` ` + count.toString() + ` in db.find("` + entityName + `"). It must be a non-negative whole number.`);
|
|
113
|
+
}
|
|
114
|
+
params.push(count);
|
|
115
|
+
return ` ` + keyword + ` $` + params.length.toString();
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function makeFindQuery(options) {
|
|
119
|
+
let entityConfig = options.entityConfig;
|
|
120
|
+
let table = entityConfig.table;
|
|
121
|
+
let entityName = entityConfig.name;
|
|
122
|
+
let params = [];
|
|
123
|
+
let where = options.where;
|
|
124
|
+
let whereClause = where !== undefined ? " WHERE " + makeWhereCondition(table, entityName, where, params) : "";
|
|
125
|
+
let orderBy = options.orderBy;
|
|
126
|
+
let orderByClause = orderBy !== undefined ? makeOrderByClause(table, entityName, orderBy) : "";
|
|
127
|
+
let count = options.limit;
|
|
128
|
+
let limitClause = count !== undefined ? makeBoundCount("LIMIT", entityName, count, params) : "";
|
|
129
|
+
let count$1 = options.offset;
|
|
130
|
+
let offsetClause = count$1 !== undefined ? makeBoundCount("OFFSET", entityName, count$1, params) : "";
|
|
131
|
+
return {
|
|
132
|
+
text: `SELECT * FROM ` + quoted(options.pgSchema, table.tableName) + whereClause + orderByClause + limitClause + offsetClause + `;`,
|
|
133
|
+
params: params
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function makeGetQuery(entityConfig, pgSchema, id) {
|
|
138
|
+
let table = entityConfig.table;
|
|
139
|
+
let entityName = entityConfig.name;
|
|
140
|
+
let match = Table.getChainIdField(table);
|
|
141
|
+
if (match !== undefined) {
|
|
142
|
+
Stdlib_JsError.throwWithMessage(`db.get("` + entityName + `", id) is ambiguous: the entity is per-chain, so the same id can exist on every chain. Use db.find("` + entityName + `", { where: { id: { _eq: id }, ` + Config.chainIdFieldName + `: { _eq: chainId } } }).`);
|
|
143
|
+
}
|
|
144
|
+
let params = [];
|
|
145
|
+
let queryField = getQueryFieldOrThrow(table, entityName, Table.idFieldName, "where");
|
|
146
|
+
let placeholder = bindOrThrow(queryField, entityName, Table.idFieldName, "_eq", id, false, params);
|
|
147
|
+
return {
|
|
148
|
+
text: `SELECT * FROM ` + quoted(pgSchema, table.tableName) + ` WHERE "` + queryField.pgDbFieldName + `" = ` + placeholder + ` LIMIT 1;`,
|
|
149
|
+
params: params
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function decodeRows(entityConfig, rows) {
|
|
154
|
+
return S$RescriptSchema.parseOrThrow(rows, Table.pgRowsSchema(entityConfig.table));
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
let chainColumns = [
|
|
158
|
+
"id",
|
|
159
|
+
"ecosystem",
|
|
160
|
+
"start_block",
|
|
161
|
+
"end_block",
|
|
162
|
+
"source_block",
|
|
163
|
+
"buffer_block",
|
|
164
|
+
"progress_block",
|
|
165
|
+
"ready_at"
|
|
166
|
+
];
|
|
167
|
+
|
|
168
|
+
let chainRowsSchema = S$RescriptSchema.array(S$RescriptSchema.object(s => ({
|
|
169
|
+
id: s.f("id", S$RescriptSchema.unknown),
|
|
170
|
+
ecosystem: s.f("ecosystem", S$RescriptSchema.string),
|
|
171
|
+
startBlock: s.f("start_block", S$RescriptSchema.int),
|
|
172
|
+
endBlock: s.f("end_block", S$RescriptSchema.$$null(S$RescriptSchema.int)),
|
|
173
|
+
sourceBlock: s.f("source_block", S$RescriptSchema.int),
|
|
174
|
+
bufferBlock: s.f("buffer_block", S$RescriptSchema.int),
|
|
175
|
+
progressBlock: s.f("progress_block", S$RescriptSchema.int),
|
|
176
|
+
readyAt: s.f("ready_at", S$RescriptSchema.$$null(Utils.Schema.dbDate))
|
|
177
|
+
})));
|
|
178
|
+
|
|
179
|
+
function makeChainHeightsQuery(pgSchema) {
|
|
180
|
+
return {
|
|
181
|
+
text: `SELECT ` + chainColumns.map(column => `"` + column + `"`).join(", ") + ` FROM ` + quoted(pgSchema, InternalTable.Chains.table.tableName) + `;`,
|
|
182
|
+
params: []
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function decodeChainHeights(rows) {
|
|
187
|
+
let heights = {};
|
|
188
|
+
S$RescriptSchema.parseOrThrow(rows, chainRowsSchema).forEach(row => {
|
|
189
|
+
let chainId = ChainId.normalizeOrThrow(row.id);
|
|
190
|
+
heights[ChainId.toString(chainId)] = {
|
|
191
|
+
chainId: chainId,
|
|
192
|
+
ecosystem: row.ecosystem,
|
|
193
|
+
startBlock: row.startBlock,
|
|
194
|
+
endBlock: Stdlib_Null.fromOption(row.endBlock),
|
|
195
|
+
sourceBlock: row.sourceBlock,
|
|
196
|
+
bufferBlock: row.bufferBlock,
|
|
197
|
+
progressBlock: row.progressBlock,
|
|
198
|
+
readyAt: Stdlib_Null.fromOption(row.readyAt),
|
|
199
|
+
isReady: Stdlib_Option.isSome(row.readyAt)
|
|
200
|
+
};
|
|
201
|
+
});
|
|
202
|
+
return heights;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
export {
|
|
206
|
+
quoted,
|
|
207
|
+
getQueryFieldOrThrow,
|
|
208
|
+
bindOrThrow,
|
|
209
|
+
makeWhereCondition,
|
|
210
|
+
makeOrderByClause,
|
|
211
|
+
makeBoundCount,
|
|
212
|
+
makeFindQuery,
|
|
213
|
+
makeGetQuery,
|
|
214
|
+
decodeRows,
|
|
215
|
+
chainColumns,
|
|
216
|
+
chainRowsSchema,
|
|
217
|
+
makeChainHeightsQuery,
|
|
218
|
+
decodeChainHeights,
|
|
219
|
+
}
|
|
220
|
+
/* chainRowsSchema Not a pure module */
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
// Names envio-serve derives from schema.graphql, which a custom resolver must
|
|
2
|
+
// not squat.
|
|
3
|
+
//
|
|
4
|
+
// This mirrors the builder in envio-serve's `gql/schema_build.rs`: the two are
|
|
5
|
+
// one contract, and drift shows up as a schema serve refuses to start on.
|
|
6
|
+
// Serve does re-check at startup, deliberately -- but by then the user is
|
|
7
|
+
// reading a deployment log, and the build is where they can act. Hence both,
|
|
8
|
+
// as §6.1 asks.
|
|
9
|
+
//
|
|
10
|
+
// Not covered here: the `<scalar>_comparison_exp` types serve derives from
|
|
11
|
+
// column types. Mapping an entity field's type to serve's scalar name is not
|
|
12
|
+
// something this side can do without guessing, and serve's own check is the
|
|
13
|
+
// backstop for it.
|
|
14
|
+
|
|
15
|
+
const ROOT_AND_BUILTIN_TYPES = [
|
|
16
|
+
"query_root",
|
|
17
|
+
"subscription_root",
|
|
18
|
+
"order_by",
|
|
19
|
+
"cursor_ordering",
|
|
20
|
+
"Boolean",
|
|
21
|
+
"Float",
|
|
22
|
+
"ID",
|
|
23
|
+
"Int",
|
|
24
|
+
"String",
|
|
25
|
+
];
|
|
26
|
+
|
|
27
|
+
const rootFieldsOf = (entity) => [
|
|
28
|
+
[entity, `entity '${entity}'`],
|
|
29
|
+
[`${entity}_by_pk`, `the by-primary-key field of entity '${entity}'`],
|
|
30
|
+
[`${entity}_aggregate`, `the aggregate field of entity '${entity}'`],
|
|
31
|
+
[`${entity}_stream`, `the subscription stream of entity '${entity}'`],
|
|
32
|
+
];
|
|
33
|
+
|
|
34
|
+
const typesOf = (entity) =>
|
|
35
|
+
[
|
|
36
|
+
"",
|
|
37
|
+
"_aggregate",
|
|
38
|
+
"_aggregate_bool_exp",
|
|
39
|
+
"_aggregate_bool_exp_count",
|
|
40
|
+
"_aggregate_fields",
|
|
41
|
+
"_aggregate_order_by",
|
|
42
|
+
"_bool_exp",
|
|
43
|
+
"_order_by",
|
|
44
|
+
"_select_column",
|
|
45
|
+
"_stream_cursor_input",
|
|
46
|
+
"_stream_cursor_value_input",
|
|
47
|
+
].map((suffix) => [
|
|
48
|
+
`${entity}${suffix}`,
|
|
49
|
+
suffix === ""
|
|
50
|
+
? `the row type of entity '${entity}'`
|
|
51
|
+
: `a type generated for entity '${entity}'`,
|
|
52
|
+
]);
|
|
53
|
+
|
|
54
|
+
/** Every generated name, mapped to how it would read in an error. */
|
|
55
|
+
export function reservedNames({ entities = [], enums = [] }) {
|
|
56
|
+
const rootFields = new Map();
|
|
57
|
+
const types = new Map();
|
|
58
|
+
|
|
59
|
+
for (const [name, describe] of ROOT_AND_BUILTIN_TYPES.map((name) => [
|
|
60
|
+
name,
|
|
61
|
+
"a built-in GraphQL type name",
|
|
62
|
+
])) {
|
|
63
|
+
types.set(name, describe);
|
|
64
|
+
}
|
|
65
|
+
for (const name of enums) {
|
|
66
|
+
types.set(name, "an enum declared in schema.graphql");
|
|
67
|
+
}
|
|
68
|
+
for (const entity of entities) {
|
|
69
|
+
for (const [name, describe] of rootFieldsOf(entity)) {
|
|
70
|
+
rootFields.set(name, describe);
|
|
71
|
+
}
|
|
72
|
+
for (const [name, describe] of typesOf(entity)) {
|
|
73
|
+
types.set(name, describe);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
return { rootFields, types };
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Throws when a manifest declares a root field or type name envio-serve
|
|
81
|
+
* generates. Reports all of them at once: renaming one at a time through a
|
|
82
|
+
* build each is the slow way to find out there were three.
|
|
83
|
+
*/
|
|
84
|
+
export function checkCollisions(manifest, { entities, enums }) {
|
|
85
|
+
const reserved = reservedNames({ entities, enums });
|
|
86
|
+
const found = [];
|
|
87
|
+
|
|
88
|
+
for (const resolver of manifest.resolvers) {
|
|
89
|
+
const describe = reserved.rootFields.get(resolver.name);
|
|
90
|
+
if (describe !== undefined) {
|
|
91
|
+
found.push(` - resolver '${resolver.name}' is ${describe}`);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
for (const type of manifest.types) {
|
|
95
|
+
const describe = reserved.types.get(type.name);
|
|
96
|
+
if (describe !== undefined) {
|
|
97
|
+
found.push(` - type '${type.name}' is ${describe}`);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (found.length > 0) {
|
|
102
|
+
throw new Error(
|
|
103
|
+
`Custom resolvers collide with names envio-serve generates from schema.graphql:\n${found.join(
|
|
104
|
+
"\n"
|
|
105
|
+
)}\nRename them: envio-serve owns those names and refuses to start on an ambiguous schema.`
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
}
|