envio 2.13.0 → 2.14.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -18,6 +18,7 @@ Build a real-time API for your blockchain application in minutes.
18
18
  - Write JavaScript, TypeScript, or ReScript with automatically generated types
19
19
  - Detailed logging & Error messaging
20
20
  - [Hosted Service](https://docs.envio.dev/docs/HyperIndex/hosted-service) to take care of your infrastructure
21
+ - Seamlessly integrate new chains and enhance reliability with the RPC data source
21
22
 
22
23
  ## Getting Started
23
24
 
package/evm.schema.json CHANGED
@@ -272,13 +272,13 @@
272
272
  "type": "object",
273
273
  "properties": {
274
274
  "id": {
275
- "description": "Public chain/network id",
275
+ "description": "The public blockchain network ID.",
276
276
  "type": "integer",
277
277
  "format": "uint64",
278
278
  "minimum": 0
279
279
  },
280
280
  "rpc_config": {
281
- "description": "RPC Config that will be used to subscribe to blockchain data on this network (TIP: This is optional and in most cases does not need to be specified if the network is supported with HyperSync. We recommend using HyperSync instead of RPC for 100x speed-up)",
281
+ "description": "RPC configuration for utilizing as the network's data-source. Typically optional for chains with HyperSync support, which is highly recommended. HyperSync dramatically enhances performance, providing up to a 1000x speed boost over traditional RPC.",
282
282
  "anyOf": [
283
283
  {
284
284
  "$ref": "#/$defs/RpcConfig"
@@ -288,6 +288,17 @@
288
288
  }
289
289
  ]
290
290
  },
291
+ "rpc": {
292
+ "description": "RPC configuration for your indexer. If not specified otherwise, for networks supported by HyperSync, RPC serves as a fallback for added reliability. For others, it acts as the primary data-source. HyperSync offers significant performance improvements, up to a 1000x faster than traditional RPC.",
293
+ "anyOf": [
294
+ {
295
+ "$ref": "#/$defs/NetworkRpc"
296
+ },
297
+ {
298
+ "type": "null"
299
+ }
300
+ ]
301
+ },
291
302
  "hypersync_config": {
292
303
  "description": "Optional HyperSync Config for additional fine-tuning",
293
304
  "anyOf": [
@@ -422,6 +433,116 @@
422
433
  "url"
423
434
  ]
424
435
  },
436
+ "NetworkRpc": {
437
+ "anyOf": [
438
+ {
439
+ "type": "string"
440
+ },
441
+ {
442
+ "$ref": "#/$defs/Rpc"
443
+ },
444
+ {
445
+ "type": "array",
446
+ "items": {
447
+ "$ref": "#/$defs/Rpc"
448
+ }
449
+ }
450
+ ]
451
+ },
452
+ "Rpc": {
453
+ "type": "object",
454
+ "properties": {
455
+ "url": {
456
+ "description": "The RPC endpoint URL.",
457
+ "type": "string"
458
+ },
459
+ "for": {
460
+ "description": "Determines if this RPC is for historical sync, real-time chain indexing, or as a fallback.",
461
+ "$ref": "#/$defs/For"
462
+ },
463
+ "initial_block_interval": {
464
+ "description": "The starting interval in range of blocks per query",
465
+ "type": [
466
+ "integer",
467
+ "null"
468
+ ],
469
+ "format": "uint32",
470
+ "minimum": 0
471
+ },
472
+ "backoff_multiplicative": {
473
+ "description": "After an RPC error, how much to scale back the number of blocks requested at once",
474
+ "type": [
475
+ "number",
476
+ "null"
477
+ ],
478
+ "format": "double"
479
+ },
480
+ "acceleration_additive": {
481
+ "description": "Without RPC errors or timeouts, how much to increase the number of blocks requested by for the next batch",
482
+ "type": [
483
+ "integer",
484
+ "null"
485
+ ],
486
+ "format": "uint32",
487
+ "minimum": 0
488
+ },
489
+ "interval_ceiling": {
490
+ "description": "Do not further increase the block interval past this limit",
491
+ "type": [
492
+ "integer",
493
+ "null"
494
+ ],
495
+ "format": "uint32",
496
+ "minimum": 0
497
+ },
498
+ "backoff_millis": {
499
+ "description": "After an error, how long to wait before retrying",
500
+ "type": [
501
+ "integer",
502
+ "null"
503
+ ],
504
+ "format": "uint32",
505
+ "minimum": 0
506
+ },
507
+ "fallback_stall_timeout": {
508
+ "description": "If a fallback RPC is provided, the amount of time in ms to wait before kicking off the next provider",
509
+ "type": [
510
+ "integer",
511
+ "null"
512
+ ],
513
+ "format": "uint32",
514
+ "minimum": 0
515
+ },
516
+ "query_timeout_millis": {
517
+ "description": "How long to wait before cancelling an RPC request",
518
+ "type": [
519
+ "integer",
520
+ "null"
521
+ ],
522
+ "format": "uint32",
523
+ "minimum": 0
524
+ }
525
+ },
526
+ "additionalProperties": false,
527
+ "required": [
528
+ "url",
529
+ "for"
530
+ ]
531
+ },
532
+ "For": {
533
+ "oneOf": [
534
+ {
535
+ "description": "Use RPC as the main data-source for both historical sync and real-time chain indexing.",
536
+ "type": "string",
537
+ "const": "sync"
538
+ },
539
+ {
540
+ "description": "Use RPC as a backup for the main data-source. Currently, it acts as a fallback when real-time indexing stalls, with potential for more cases in the future.",
541
+ "type": "string",
542
+ "const": "fallback"
543
+ }
544
+ ]
545
+ },
425
546
  "HypersyncConfig": {
426
547
  "type": "object",
427
548
  "properties": {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "envio",
3
- "version": "v2.13.0",
3
+ "version": "v2.14.1",
4
4
  "description": "A latency and sync speed optimized, developer friendly blockchain data indexer.",
5
5
  "bin": "./bin.js",
6
6
  "repository": {
@@ -23,10 +23,10 @@
23
23
  },
24
24
  "homepage": "https://envio.dev",
25
25
  "optionalDependencies": {
26
- "envio-linux-x64": "v2.13.0",
27
- "envio-linux-arm64": "v2.13.0",
28
- "envio-darwin-x64": "v2.13.0",
29
- "envio-darwin-arm64": "v2.13.0"
26
+ "envio-linux-x64": "v2.14.1",
27
+ "envio-linux-arm64": "v2.14.1",
28
+ "envio-darwin-x64": "v2.14.1",
29
+ "envio-darwin-arm64": "v2.14.1"
30
30
  },
31
31
  "dependencies": {
32
32
  "@envio-dev/hypersync-client": "0.6.3",
package/src/Utils.res CHANGED
@@ -92,6 +92,7 @@ module Math = {
92
92
  | (None, None) => None
93
93
  }
94
94
  }
95
+
95
96
  module Array = {
96
97
  @val external jsArrayCreate: int => array<'a> = "Array"
97
98
 
@@ -306,15 +307,11 @@ module Schema = {
306
307
  let coerceToJsonPgType = schema => {
307
308
  schema->S.preprocess(s => {
308
309
  switch s.schema->S.classify {
309
- | Literal(Null(_))
310
- | // This is a workaround for Fuel Bytes type
311
- Unknown => {serializer: _ => %raw(`"null"`)}
312
- | Null(_)
310
+ // This is a workaround for Fuel Bytes type
311
+ | Unknown => {serializer: _ => %raw(`"null"`)}
313
312
  | Bool => {
314
313
  serializer: unknown => {
315
- if unknown === %raw(`null`) {
316
- %raw(`"null"`)
317
- } else if unknown === %raw(`false`) {
314
+ if unknown === %raw(`false`) {
318
315
  %raw(`"false"`)
319
316
  } else if unknown === %raw(`true`) {
320
317
  %raw(`"true"`)
@@ -339,7 +336,7 @@ module Set = {
339
336
  external make: unit => t<'value> = "Set"
340
337
 
341
338
  @ocaml.doc("Creates a new `Set` object.") @new
342
- external fromEntries: array<'value> => t<'value> = "Set"
339
+ external fromArray: array<'value> => t<'value> = "Set"
343
340
 
344
341
  /*
345
342
  * Instance properties
@@ -128,43 +128,17 @@ module JsonRpcProvider = {
128
128
  weight?: int,
129
129
  }
130
130
 
131
- type fallbackProviderOptions = {
132
- // How many providers must agree on a value before reporting
133
- // back the response
134
- // Note: Default the half of the providers weight, so we need to set it to accept result from the first rpc
135
- quorum?: int,
136
- }
137
-
138
131
  @module("ethers") @scope("ethers") @new
139
132
  external makeWithOptions: (~rpcUrl: string, ~network: Network.t, ~options: rpcOptions) => t =
140
133
  "JsonRpcProvider"
141
134
 
142
- @module("ethers") @scope("ethers") @new
143
- external makeFallbackProvider: (
144
- ~providers: array<t>,
145
- ~network: Network.t,
146
- ~options: fallbackProviderOptions,
147
- ) => t = "FallbackProvider"
148
-
149
135
  let makeStatic = (~rpcUrl: string, ~network: Network.t, ~priority=?, ~stallTimeout=?): t => {
150
136
  makeWithOptions(~rpcUrl, ~network, ~options={staticNetwork: network, ?priority, ?stallTimeout})
151
137
  }
152
138
 
153
- let make = (~rpcUrls: array<string>, ~chainId: int, ~fallbackStallTimeout): t => {
139
+ let make = (~rpcUrl: string, ~chainId: int): t => {
154
140
  let network = Network.fromChainId(~chainId)
155
- switch rpcUrls {
156
- | [rpcUrl] => makeStatic(~rpcUrl, ~network)
157
- | rpcUrls =>
158
- makeFallbackProvider(
159
- ~providers=rpcUrls->Js.Array2.mapi((rpcUrl, index) =>
160
- makeStatic(~rpcUrl, ~network, ~priority=index, ~stallTimeout=fallbackStallTimeout)
161
- ),
162
- ~network,
163
- ~options={
164
- quorum: 1,
165
- },
166
- )
167
- }
141
+ makeStatic(~rpcUrl, ~network)
168
142
  }
169
143
 
170
144
  @send
@@ -185,9 +159,6 @@ module JsonRpcProvider = {
185
159
  fields->Obj.magic
186
160
  }
187
161
 
188
- @send
189
- external getBlockNumber: t => promise<int> = "getBlockNumber"
190
-
191
162
  type block = {
192
163
  _difficulty: bigint,
193
164
  difficulty: int,
@@ -360,13 +360,13 @@ module ResponseTypes = {
360
360
  let queryRoute = Rest.route(() => {
361
361
  path: "/query",
362
362
  method: Post,
363
- variables: s => s.body(QueryTypes.postQueryBodySchema),
363
+ input: s => s.body(QueryTypes.postQueryBodySchema),
364
364
  responses: [s => s.data(ResponseTypes.queryResponseSchema)],
365
365
  })
366
366
 
367
367
  let heightRoute = Rest.route(() => {
368
368
  path: "/height",
369
369
  method: Get,
370
- variables: _ => (),
370
+ input: _ => (),
371
371
  responses: [s => s.field("height", S.int)],
372
372
  })
@@ -4,7 +4,7 @@ let makeRpcRoute = (method: string, paramsSchema, resultSchema) => {
4
4
  Rest.route(() => {
5
5
  method: Post,
6
6
  path: "",
7
- variables: s => {
7
+ input: s => {
8
8
  let _ = s.field("method", S.literal(method))
9
9
  let _ = s.field("id", idSchema)
10
10
  let _ = s.field("jsonrpc", versionSchema)
@@ -1,6 +1,6 @@
1
1
  /*
2
2
  Vendored from: https://github.com/DZakh/rescript-rest
3
- Version: 1.0.1
3
+ Version: 2.0.0-rc.6
4
4
 
5
5
  IF EDITING THIS FILE, PLEASE LIST THE CHANGES BELOW
6
6
 
@@ -170,49 +170,50 @@ module Response = {
170
170
  data: 'value. S.t<'value> => 'value,
171
171
  field: 'value. (string, S.t<'value>) => 'value,
172
172
  header: 'value. (string, S.t<'value>) => 'value,
173
+ redirect: 'value. S.t<'value> => 'value,
173
174
  }
174
175
 
175
- type t<'response> = {
176
+ type t<'output> = {
176
177
  // When it's empty, treat response as a default
177
178
  status: option<int>,
178
179
  description: option<string>,
179
180
  dataSchema: S.t<unknown>,
180
181
  emptyData: bool,
181
- schema: S.t<'response>,
182
+ schema: S.t<'output>,
182
183
  }
183
184
 
184
- type builder<'response> = {
185
+ type builder<'output> = {
185
186
  // When it's empty, treat response as a default
186
187
  mutable status?: int,
187
188
  mutable description?: string,
188
189
  mutable dataSchema?: S.t<unknown>,
189
190
  mutable emptyData: bool,
190
- mutable schema?: S.t<'response>,
191
+ mutable schema?: S.t<'output>,
191
192
  }
192
193
 
193
194
  let register = (
194
- map: dict<t<'response>>,
195
+ map: dict<t<'output>>,
195
196
  status: [< status | #default],
196
- builder: builder<'response>,
197
+ builder: builder<'output>,
197
198
  ) => {
198
199
  let key = status->(Obj.magic: [< status | #default] => string)
199
200
  if map->Dict.has(key) {
200
201
  panic(`Response for the "${key}" status registered multiple times`)
201
202
  } else {
202
- map->Js.Dict.set(key, builder->(Obj.magic: builder<'response> => t<'response>))
203
+ map->Js.Dict.set(key, builder->(Obj.magic: builder<'output> => t<'output>))
203
204
  }
204
205
  }
205
206
 
206
207
  @inline
207
- let find = (map: dict<t<'response>>, responseStatus: int): option<t<'response>> => {
208
+ let find = (map: dict<t<'output>>, responseStatus: int): option<t<'output>> => {
208
209
  (map
209
210
  ->Js.Dict.unsafeGet(responseStatus->(Obj.magic: int => string))
210
- ->(Obj.magic: t<'response> => bool) ||
211
+ ->(Obj.magic: t<'output> => bool) ||
211
212
  map
212
213
  ->Js.Dict.unsafeGet((responseStatus / 100)->(Obj.magic: int => string) ++ "XX")
213
- ->(Obj.magic: t<'response> => bool) ||
214
- map->Js.Dict.unsafeGet("default")->(Obj.magic: t<'response> => bool))
215
- ->(Obj.magic: bool => option<t<'response>>)
214
+ ->(Obj.magic: t<'output> => bool) ||
215
+ map->Js.Dict.unsafeGet("default")->(Obj.magic: t<'output> => bool))
216
+ ->(Obj.magic: bool => option<t<'output>>)
216
217
  }
217
218
  }
218
219
 
@@ -242,26 +243,48 @@ type method =
242
243
  | @as("OPTIONS") Options
243
244
  | @as("TRACE") Trace
244
245
 
245
- type definition<'variables, 'response> = {
246
+ type definition<'input, 'output> = {
246
247
  method: method,
247
248
  path: string,
248
- variables: s => 'variables,
249
- responses: array<Response.s => 'response>,
249
+ input: s => 'input,
250
+ responses: array<Response.s => 'output>,
250
251
  summary?: string,
251
252
  description?: string,
252
253
  deprecated?: bool,
254
+ operationId?: string,
255
+ tags?: array<string>,
256
+ // By default, all query parameters are encoded as strings, however, you can use the jsonQuery option to encode query parameters as typed JSON values.
257
+ jsonQuery?: bool,
258
+ }
259
+
260
+ type rpc<'input, 'output> = {
261
+ input: S.t<'input>,
262
+ output: S.t<'output>,
263
+ summary?: string,
264
+ description?: string,
265
+ deprecated?: bool,
266
+ operationId?: string,
267
+ tags?: array<string>,
253
268
  }
254
269
 
255
- type routeParams<'variables, 'response> = {
256
- definition: definition<'variables, 'response>,
270
+ type routeParams<'input, 'output> = {
271
+ method: method,
272
+ path: string,
257
273
  pathItems: array<pathItem>,
258
- variablesSchema: S.t<'variables>,
259
- responses: array<Response.t<'response>>,
260
- responsesMap: dict<Response.t<'response>>,
274
+ inputSchema: S.t<'input>,
275
+ outputSchema: S.t<'output>,
276
+ responses: array<Response.t<'output>>,
277
+ responsesMap: dict<Response.t<'output>>,
261
278
  isRawBody: bool,
279
+ summary?: string,
280
+ description?: string,
281
+ deprecated?: bool,
282
+ operationId?: string,
283
+ tags?: array<string>,
284
+ jsonQuery?: bool,
262
285
  }
263
286
 
264
- type route<'variables, 'response> = unit => definition<'variables, 'response>
287
+ type route<'input, 'output> = unit => definition<'input, 'output>
265
288
 
266
289
  let rec parsePath = (path: string, ~pathItems, ~pathParams) => {
267
290
  if path !== "" {
@@ -376,181 +399,230 @@ let basicAuthSchema = S.string->S.transform(s => {
376
399
  })
377
400
 
378
401
  let params = route => {
379
- switch (route->Obj.magic)["_rest"]->(
380
- Obj.magic: unknown => option<routeParams<'variables, 'response>>
381
- ) {
402
+ switch (route->Obj.magic)["_rest"]->(Obj.magic: unknown => option<routeParams<'input, 'output>>) {
382
403
  | Some(params) => params
383
404
  | None => {
384
- let routeDefinition = (
385
- route->(Obj.magic: route<'variables, 'response> => route<unknown, unknown>)
386
- )()
387
-
388
- let pathItems = []
389
- let pathParams = Js.Dict.empty()
390
- parsePath(routeDefinition.path, ~pathItems, ~pathParams)
391
-
392
- // Don't use ref, since it creates an unnecessary object
393
- let isRawBody = %raw(`false`)
394
-
395
- let variablesSchema = S.object(s => {
396
- routeDefinition.variables({
397
- field: (fieldName, schema) => {
398
- s.nested("body").field(fieldName, schema)
399
- },
400
- body: schema => {
401
- if schema->isNestedFlattenSupported {
402
- s.nested("body").flatten(schema)
403
- } else {
404
- s.field("body", schema)
405
- }
406
- },
407
- rawBody: schema => {
408
- let isNonStringBased = switch schema->S.classify {
409
- | Literal(String(_))
410
- | String => false
411
- | _ => true
412
- }
413
- if isNonStringBased {
414
- panic("Only string-based schemas are allowed in rawBody")
415
- }
416
- let _ = %raw(`isRawBody = true`)
417
- s.field("body", schema)
418
- },
419
- header: (fieldName, schema) => {
420
- s.nested("headers").field(fieldName->Js.String2.toLowerCase, coerceSchema(schema))
421
- },
422
- query: (fieldName, schema) => {
423
- s.nested("query").field(fieldName, coerceSchema(schema))
424
- },
425
- param: (fieldName, schema) => {
426
- if !Dict.has(pathParams, fieldName) {
427
- panic(`Path parameter "${fieldName}" is not defined in the path`)
428
- }
429
- s.nested("params").field(fieldName, coerceSchema(schema))
430
- },
431
- auth: auth => {
432
- s.nested("headers").field(
433
- "authorization",
434
- switch auth {
435
- | Bearer => bearerAuthSchema
436
- | Basic => basicAuthSchema
437
- },
438
- )
439
- },
440
- })
441
- })
442
-
443
- {
444
- // The variables input is guaranteed to be an object, so we reset the rescript-schema type filter here
445
- variablesSchema->stripInPlace
446
- variablesSchema->removeTypeValidationInPlace
447
- switch variablesSchema->getSchemaField("headers") {
448
- | Some({schema}) =>
449
- schema->stripInPlace
450
- schema->removeTypeValidationInPlace
451
- | None => ()
452
- }
453
- switch variablesSchema->getSchemaField("params") {
454
- | Some({schema}) => schema->removeTypeValidationInPlace
455
- | None => ()
405
+ let definition = (route->(Obj.magic: route<'input, 'output> => route<unknown, unknown>))()
406
+
407
+ let params = if (definition->Obj.magic)["output"] {
408
+ let definition =
409
+ definition->(Obj.magic: definition<unknown, unknown> => rpc<unknown, unknown>)
410
+ let path =
411
+ `/` ++
412
+ switch definition.operationId {
413
+ | Some(p) => p
414
+ | None => (route->Obj.magic)["name"]
415
+ }
416
+ let inputSchema = S.object(s => s.field("body", definition.input))
417
+ inputSchema->stripInPlace
418
+ inputSchema->removeTypeValidationInPlace
419
+ let outputSchema = S.object(s => s.field("data", definition.output))
420
+ outputSchema->stripInPlace
421
+ outputSchema->removeTypeValidationInPlace
422
+ let response: Response.t<unknown> = {
423
+ status: Some(200),
424
+ description: None,
425
+ dataSchema: definition.input,
426
+ emptyData: false,
427
+ schema: outputSchema,
456
428
  }
457
- switch variablesSchema->getSchemaField("query") {
458
- | Some({schema}) => schema->removeTypeValidationInPlace
459
- | None => ()
429
+ let responsesMap = Js.Dict.empty()
430
+ responsesMap->Js.Dict.set("200", response)
431
+ {
432
+ method: Post,
433
+ path,
434
+ inputSchema,
435
+ outputSchema,
436
+ responses: [response],
437
+ responsesMap,
438
+ pathItems: [Static(path)],
439
+ isRawBody: false,
440
+ summary: ?definition.summary,
441
+ description: ?definition.description,
442
+ deprecated: ?definition.deprecated,
443
+ operationId: ?definition.operationId,
444
+ tags: ?definition.tags,
460
445
  }
461
- }
446
+ } else {
447
+ let pathItems = []
448
+ let pathParams = Js.Dict.empty()
449
+ parsePath(definition.path, ~pathItems, ~pathParams)
462
450
 
463
- let responsesMap = Js.Dict.empty()
464
- let responses = []
465
- routeDefinition.responses->Js.Array2.forEach(r => {
466
- let builder: Response.builder<unknown> = {
467
- emptyData: true,
468
- }
469
- let schema = S.object(s => {
470
- let definition = r({
471
- status: status => {
472
- builder.status = Some(status)
473
- let status = status->(Obj.magic: int => Response.status)
474
- responsesMap->Response.register(status, builder)
475
- s.tag("status", status)
476
- },
477
- description: d => builder.description = Some(d),
451
+ // Don't use ref, since it creates an unnecessary object
452
+ let isRawBody = %raw(`false`)
453
+
454
+ let inputSchema = S.object(s => {
455
+ definition.input({
478
456
  field: (fieldName, schema) => {
479
- builder.emptyData = false
480
- s.nested("data").field(fieldName, schema)
457
+ s.nested("body").field(fieldName, schema)
481
458
  },
482
- data: schema => {
483
- builder.emptyData = false
459
+ body: schema => {
484
460
  if schema->isNestedFlattenSupported {
485
- s.nested("data").flatten(schema)
461
+ s.nested("body").flatten(schema)
486
462
  } else {
487
- s.field("data", schema)
463
+ s.field("body", schema)
464
+ }
465
+ },
466
+ rawBody: schema => {
467
+ let isNonStringBased = switch schema->S.classify {
468
+ | Literal(String(_))
469
+ | String => false
470
+ | _ => true
488
471
  }
472
+ if isNonStringBased {
473
+ panic("Only string-based schemas are allowed in rawBody")
474
+ }
475
+ let _ = %raw(`isRawBody = true`)
476
+ s.field("body", schema)
489
477
  },
490
478
  header: (fieldName, schema) => {
491
479
  s.nested("headers").field(fieldName->Js.String2.toLowerCase, coerceSchema(schema))
492
480
  },
481
+ query: (fieldName, schema) => {
482
+ s.nested("query").field(fieldName, coerceSchema(schema))
483
+ },
484
+ param: (fieldName, schema) => {
485
+ if !Dict.has(pathParams, fieldName) {
486
+ panic(`Path parameter "${fieldName}" is not defined in the path`)
487
+ }
488
+ s.nested("params").field(fieldName, coerceSchema(schema))
489
+ },
490
+ auth: auth => {
491
+ s.nested("headers").field(
492
+ "authorization",
493
+ switch auth {
494
+ | Bearer => bearerAuthSchema
495
+ | Basic => basicAuthSchema
496
+ },
497
+ )
498
+ },
493
499
  })
494
- if builder.emptyData {
495
- s.tag("data", %raw(`null`))
496
- }
497
- definition
498
500
  })
499
- if builder.status === None {
500
- responsesMap->Response.register(#default, builder)
501
- }
502
- schema->stripInPlace
503
- schema->removeTypeValidationInPlace
504
- let dataSchema = (schema->getSchemaField("data")->Option.unsafeUnwrap).schema
505
- builder.dataSchema = dataSchema->Option.unsafeSome
506
- switch dataSchema->S.classify {
507
- | Literal(_) => {
508
- let dataTypeValidation = dataSchema->unsafeGetTypeValidationInPlace
509
- schema->setTypeValidationInPlace((b, ~inputVar) =>
510
- dataTypeValidation(b, ~inputVar=`${inputVar}.data`)
511
- )
501
+
502
+ {
503
+ // The input input is guaranteed to be an object, so we reset the rescript-schema type filter here
504
+ inputSchema->stripInPlace
505
+ inputSchema->removeTypeValidationInPlace
506
+ switch inputSchema->getSchemaField("headers") {
507
+ | Some({schema}) =>
508
+ schema->stripInPlace
509
+ schema->removeTypeValidationInPlace
510
+ | None => ()
511
+ }
512
+ switch inputSchema->getSchemaField("params") {
513
+ | Some({schema}) => schema->removeTypeValidationInPlace
514
+ | None => ()
515
+ }
516
+ switch inputSchema->getSchemaField("query") {
517
+ | Some({schema}) => schema->removeTypeValidationInPlace
518
+ | None => ()
512
519
  }
513
- | _ => ()
514
520
  }
515
- switch schema->getSchemaField("headers") {
516
- | Some({schema}) =>
521
+
522
+ let responsesMap = Js.Dict.empty()
523
+ let responses = []
524
+ definition.responses->Js.Array2.forEach(r => {
525
+ let builder: Response.builder<unknown> = {
526
+ emptyData: true,
527
+ }
528
+ let schema = S.object(s => {
529
+ let status = status => {
530
+ builder.status = Some(status)
531
+ let status = status->(Obj.magic: int => Response.status)
532
+ responsesMap->Response.register(status, builder)
533
+ s.tag("status", status)
534
+ }
535
+ let header = (fieldName, schema) => {
536
+ s.nested("headers").field(fieldName->Js.String2.toLowerCase, coerceSchema(schema))
537
+ }
538
+ let definition = r({
539
+ status,
540
+ redirect: schema => {
541
+ status(307)
542
+ header("location", coerceSchema(schema))
543
+ },
544
+ description: d => builder.description = Some(d),
545
+ field: (fieldName, schema) => {
546
+ builder.emptyData = false
547
+ s.nested("data").field(fieldName, schema)
548
+ },
549
+ data: schema => {
550
+ builder.emptyData = false
551
+ if schema->isNestedFlattenSupported {
552
+ s.nested("data").flatten(schema)
553
+ } else {
554
+ s.field("data", schema)
555
+ }
556
+ },
557
+ header,
558
+ })
559
+ if builder.emptyData {
560
+ s.tag("data", %raw(`null`))
561
+ }
562
+ definition
563
+ })
564
+ if builder.status === None {
565
+ responsesMap->Response.register(#default, builder)
566
+ }
517
567
  schema->stripInPlace
518
568
  schema->removeTypeValidationInPlace
519
- | None => ()
569
+ let dataSchema = (schema->getSchemaField("data")->Option.unsafeUnwrap).schema
570
+ builder.dataSchema = dataSchema->Option.unsafeSome
571
+ switch dataSchema->S.classify {
572
+ | Literal(_) => {
573
+ let dataTypeValidation = dataSchema->unsafeGetTypeValidationInPlace
574
+ schema->setTypeValidationInPlace((b, ~inputVar) =>
575
+ dataTypeValidation(b, ~inputVar=`${inputVar}.data`)
576
+ )
577
+ }
578
+ | _ => ()
579
+ }
580
+ switch schema->getSchemaField("headers") {
581
+ | Some({schema}) =>
582
+ schema->stripInPlace
583
+ schema->removeTypeValidationInPlace
584
+ | None => ()
585
+ }
586
+ builder.schema = Option.unsafeSome(schema)
587
+ responses
588
+ ->Js.Array2.push(builder->(Obj.magic: Response.builder<unknown> => Response.t<unknown>))
589
+ ->ignore
590
+ })
591
+
592
+ if responses->Js.Array2.length === 0 {
593
+ panic("At least single response should be registered")
520
594
  }
521
- builder.schema = Option.unsafeSome(schema)
522
- responses
523
- ->Js.Array2.push(builder->(Obj.magic: Response.builder<unknown> => Response.t<unknown>))
524
- ->ignore
525
- })
526
595
 
527
- if responses->Js.Array2.length === 0 {
528
- panic("At least single response should be registered")
596
+ {
597
+ method: definition.method,
598
+ path: definition.path,
599
+ inputSchema,
600
+ outputSchema: S.union(responses->Js.Array2.map(r => r.schema)),
601
+ responses,
602
+ pathItems,
603
+ responsesMap,
604
+ isRawBody,
605
+ summary: ?definition.summary,
606
+ description: ?definition.description,
607
+ deprecated: ?definition.deprecated,
608
+ operationId: ?definition.operationId,
609
+ tags: ?definition.tags,
610
+ jsonQuery: ?definition.jsonQuery,
611
+ }
529
612
  }
530
613
 
531
- let params = {
532
- definition: routeDefinition,
533
- variablesSchema,
534
- responses,
535
- pathItems,
536
- responsesMap,
537
- isRawBody,
538
- }
539
614
  (route->Obj.magic)["_rest"] = params
540
- params->(Obj.magic: routeParams<unknown, unknown> => routeParams<'variables, 'response>)
615
+ params->(Obj.magic: routeParams<unknown, unknown> => routeParams<'input, 'output>)
541
616
  }
542
617
  }
543
618
  }
544
619
 
545
- external route: (unit => definition<'variables, 'response>) => route<'variables, 'response> =
546
- "%identity"
620
+ external route: (unit => definition<'input, 'output>) => route<'input, 'output> = "%identity"
621
+ external rpc: (unit => rpc<'input, 'output>) => route<'input, 'output> = "%identity"
547
622
 
548
623
  type client = {
549
- call: 'variables 'response. (route<'variables, 'response>, 'variables) => promise<'response>,
550
624
  baseUrl: string,
551
625
  fetcher: ApiFetcher.t,
552
- // By default, all query parameters are encoded as strings, however, you can use the jsonQuery option to encode query parameters as typed JSON values.
553
- jsonQuery: bool,
554
626
  }
555
627
 
556
628
  /**
@@ -582,7 +654,7 @@ let rec tokeniseValue = (key, value, ~append) => {
582
654
  }
583
655
 
584
656
  // Inspired by https://github.com/ts-rest/ts-rest/blob/7792ef7bdc352e84a4f5766c53f984a9d630c60e/libs/ts-rest/core/src/lib/client.ts#L347
585
- let getCompletePath = (~baseUrl, ~pathItems, ~maybeQuery, ~maybeParams, ~jsonQuery) => {
657
+ let getCompletePath = (~baseUrl, ~pathItems, ~maybeQuery, ~maybeParams, ~jsonQuery=false) => {
586
658
  let path = ref(baseUrl)
587
659
 
588
660
  for idx in 0 to pathItems->Js.Array2.length - 1 {
@@ -593,7 +665,7 @@ let getCompletePath = (~baseUrl, ~pathItems, ~maybeQuery, ~maybeParams, ~jsonQue
593
665
  switch (maybeParams->Obj.magic && maybeParams->Js.Dict.unsafeGet(name)->Obj.magic)
594
666
  ->(Obj.magic: bool => option<string>) {
595
667
  | Some(param) => path := path.contents ++ param
596
- | None => panic(`Path parameter "${name}" is not defined in variables`)
668
+ | None => panic(`Path parameter "${name}" is not defined in input`)
597
669
  }
598
670
  }
599
671
  }
@@ -648,20 +720,46 @@ let getCompletePath = (~baseUrl, ~pathItems, ~maybeQuery, ~maybeParams, ~jsonQue
648
720
  path.contents
649
721
  }
650
722
 
651
- let fetch = (
652
- type variables response,
653
- route: route<variables, response>,
654
- baseUrl,
655
- variables,
656
- ~fetcher=ApiFetcher.default,
657
- ~jsonQuery=false,
658
- ) => {
659
- let route = route->(Obj.magic: route<variables, response> => route<unknown, unknown>)
660
- let variables = variables->(Obj.magic: variables => unknown)
723
+ let url = (route, input, ~baseUrl="") => {
724
+ let {pathItems, inputSchema} = route->params
725
+ let data = input->S.reverseConvertOrThrow(inputSchema)->Obj.magic
726
+ getCompletePath(
727
+ ~baseUrl,
728
+ ~pathItems,
729
+ ~maybeQuery=data["query"],
730
+ ~maybeParams=data["params"],
731
+ ~jsonQuery=false,
732
+ )
733
+ }
661
734
 
662
- let {definition, variablesSchema, responsesMap, pathItems, isRawBody} = route->params
735
+ type global = {
736
+ @as("c")
737
+ mutable client: option<client>,
738
+ }
663
739
 
664
- let data = variables->S.reverseConvertOrThrow(variablesSchema)->Obj.magic
740
+ let global = {
741
+ client: None,
742
+ }
743
+
744
+ let fetch = (type input response, route: route<input, response>, input, ~client=?) => {
745
+ let route = route->(Obj.magic: route<input, response> => route<unknown, unknown>)
746
+ let input = input->(Obj.magic: input => unknown)
747
+
748
+ let {path, method, ?jsonQuery, inputSchema, responsesMap, pathItems, isRawBody} = route->params
749
+
750
+ let client = switch client {
751
+ | Some(client) => client
752
+ | None =>
753
+ switch global.client {
754
+ | Some(client) => client
755
+ | None =>
756
+ panic(
757
+ `Client is not set for the ${path} fetch request. Please, use Rest.setGlobalClient or pass a client explicitly to the Rest.fetch arguments`,
758
+ )
759
+ }
760
+ }
761
+
762
+ let data = input->S.reverseConvertOrThrow(inputSchema)->Obj.magic
665
763
 
666
764
  if data["body"] !== %raw(`void 0`) {
667
765
  if !isRawBody {
@@ -673,17 +771,17 @@ let fetch = (
673
771
  data["headers"]["content-type"] = "application/json"
674
772
  }
675
773
 
676
- fetcher({
774
+ client.fetcher({
677
775
  body: data["body"],
678
776
  headers: data["headers"],
679
777
  path: getCompletePath(
680
- ~baseUrl,
778
+ ~baseUrl=client.baseUrl,
681
779
  ~pathItems,
682
780
  ~maybeQuery=data["query"],
683
781
  ~maybeParams=data["params"],
684
- ~jsonQuery,
782
+ ~jsonQuery?,
685
783
  ),
686
- method: (definition.method :> string),
784
+ method: (method :> string),
687
785
  })->Promise.thenResolve(fetcherResponse => {
688
786
  switch responsesMap->Response.find(fetcherResponse.status) {
689
787
  | None =>
@@ -716,12 +814,17 @@ let fetch = (
716
814
  })
717
815
  }
718
816
 
719
- let client = (~baseUrl, ~fetcher=ApiFetcher.default, ~jsonQuery=false) => {
720
- let call = (route, variables) => route->fetch(baseUrl, variables, ~fetcher, ~jsonQuery)
817
+ let client = (baseUrl, ~fetcher=ApiFetcher.default) => {
721
818
  {
722
819
  baseUrl,
723
820
  fetcher,
724
- call,
725
- jsonQuery,
821
+ }
822
+ }
823
+
824
+ let setGlobalClient = (baseUrl, ~fetcher=?) => {
825
+ switch global.client {
826
+ | Some(_) =>
827
+ panic("There's already a global client defined. You can have only one global client at a time.")
828
+ | None => global.client = Some(client(baseUrl, ~fetcher?))
726
829
  }
727
830
  }
@@ -1,6 +1,6 @@
1
1
  /*
2
2
  Vendored from: https://github.com/DZakh/rescript-rest
3
- Version: 1.0.1
3
+ Version: 2.0.0-rc.6
4
4
 
5
5
  IF EDITING THIS FILE, PLEASE LIST THE CHANGES BELOW
6
6
 
@@ -10,22 +10,6 @@ here
10
10
 
11
11
  @@uncurried
12
12
 
13
- module ApiFetcher: {
14
- type args = {body: option<unknown>, headers: option<dict<unknown>>, method: string, path: string}
15
- type response = {data: unknown, status: int, headers: dict<unknown>}
16
- type t = args => promise<response>
17
-
18
- // Inspired by https://github.com/ts-rest/ts-rest/blob/7792ef7bdc352e84a4f5766c53f984a9d630c60e/libs/ts-rest/core/src/lib/client.ts#L102
19
- /**
20
- * Default fetch api implementation:
21
- *
22
- * Can be used as a reference for implementing your own fetcher,
23
- * or used in the "api" field of ClientArgs to allow you to hook
24
- * into the request to run custom logic
25
- */
26
- let default: t
27
- }
28
-
29
13
  module Response: {
30
14
  type numiricStatus = [
31
15
  | #100
@@ -94,13 +78,13 @@ module Response: {
94
78
  | numiricStatus
95
79
  ]
96
80
 
97
- type t<'response> = {
81
+ type t<'output> = {
98
82
  // When it's empty, treat response as a default
99
83
  status: option<int>,
100
84
  description: option<string>,
101
85
  dataSchema: S.t<unknown>,
102
86
  emptyData: bool,
103
- schema: S.t<'response>,
87
+ schema: S.t<'output>,
104
88
  }
105
89
 
106
90
  type s = {
@@ -109,6 +93,7 @@ module Response: {
109
93
  data: 'value. S.t<'value> => 'value,
110
94
  field: 'value. (string, S.t<'value>) => 'value,
111
95
  header: 'value. (string, S.t<'value>) => 'value,
96
+ redirect: 'value. S.t<'value> => 'value,
112
97
  }
113
98
  }
114
99
 
@@ -134,49 +119,83 @@ type method =
134
119
  | @as("OPTIONS") Options
135
120
  | @as("TRACE") Trace
136
121
 
137
- type definition<'variables, 'response> = {
122
+ type definition<'input, 'output> = {
138
123
  method: method,
139
124
  path: string,
140
- variables: s => 'variables,
141
- responses: array<Response.s => 'response>,
125
+ input: s => 'input,
126
+ responses: array<Response.s => 'output>,
142
127
  summary?: string,
143
128
  description?: string,
144
129
  deprecated?: bool,
130
+ operationId?: string,
131
+ tags?: array<string>,
132
+ // By default, all query parameters are encoded as strings, however, you can use the jsonQuery option to encode query parameters as typed JSON values.
133
+ jsonQuery?: bool,
145
134
  }
146
135
 
147
- type route<'variables, 'response>
136
+ type rpc<'input, 'output> = {
137
+ input: S.t<'input>,
138
+ output: S.t<'output>,
139
+ summary?: string,
140
+ description?: string,
141
+ deprecated?: bool,
142
+ operationId?: string,
143
+ tags?: array<string>,
144
+ }
145
+
146
+ type route<'input, 'output>
148
147
 
149
148
  type pathParam = {name: string}
150
149
  @unboxed
151
150
  type pathItem = Static(string) | Param(pathParam)
152
151
 
153
- type routeParams<'variables, 'response> = {
154
- definition: definition<'variables, 'response>,
152
+ type routeParams<'input, 'output> = {
153
+ method: method,
154
+ path: string,
155
155
  pathItems: array<pathItem>,
156
- variablesSchema: S.t<'variables>,
157
- responses: array<Response.t<'response>>,
158
- responsesMap: dict<Response.t<'response>>,
156
+ inputSchema: S.t<'input>,
157
+ outputSchema: S.t<'output>,
158
+ responses: array<Response.t<'output>>,
159
+ responsesMap: dict<Response.t<'output>>,
159
160
  isRawBody: bool,
161
+ summary?: string,
162
+ description?: string,
163
+ deprecated?: bool,
164
+ operationId?: string,
165
+ tags?: array<string>,
166
+ jsonQuery?: bool,
160
167
  }
161
168
 
162
- let params: route<'variables, 'response> => routeParams<'variables, 'response>
169
+ let params: route<'input, 'output> => routeParams<'input, 'output>
170
+
171
+ external route: (unit => definition<'input, 'output>) => route<'input, 'output> = "%identity"
172
+ external rpc: (unit => rpc<'input, 'output>) => route<'input, 'output> = "%identity"
163
173
 
164
- external route: (unit => definition<'variables, 'response>) => route<'variables, 'response> =
165
- "%identity"
174
+ module ApiFetcher: {
175
+ type args = {body: option<unknown>, headers: option<dict<unknown>>, method: string, path: string}
176
+ type response = {data: unknown, status: int, headers: dict<unknown>}
177
+ type t = args => promise<response>
178
+
179
+ // Inspired by https://github.com/ts-rest/ts-rest/blob/7792ef7bdc352e84a4f5766c53f984a9d630c60e/libs/ts-rest/core/src/lib/client.ts#L102
180
+ /**
181
+ * Default fetch api implementation:
182
+ *
183
+ * Can be used as a reference for implementing your own fetcher,
184
+ * or used in the "api" field of ClientArgs to allow you to hook
185
+ * into the request to run custom logic
186
+ */
187
+ let default: t
188
+ }
166
189
 
167
190
  type client = {
168
- call: 'variables 'response. (route<'variables, 'response>, 'variables) => promise<'response>,
169
191
  baseUrl: string,
170
192
  fetcher: ApiFetcher.t,
171
- jsonQuery: bool,
172
193
  }
173
194
 
174
- let client: (~baseUrl: string, ~fetcher: ApiFetcher.t=?, ~jsonQuery: bool=?) => client
195
+ let url: (route<'input, 'output>, 'input, ~baseUrl: string=?) => string
196
+
197
+ let client: (string, ~fetcher: ApiFetcher.t=?) => client
198
+
199
+ let setGlobalClient: (string, ~fetcher: ApiFetcher.t=?) => unit
175
200
 
176
- let fetch: (
177
- route<'variables, 'response>,
178
- string,
179
- 'variables,
180
- ~fetcher: ApiFetcher.t=?,
181
- ~jsonQuery: bool=?,
182
- ) => promise<'response>
201
+ let fetch: (route<'input, 'output>, 'input, ~client: client=?) => promise<'output>