envio 2.8.2 → 2.9.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +6 -6
- package/src/Enum.res +22 -0
- package/src/LogSelection.res +60 -0
- package/src/Utils.res +2 -0
- package/src/bindings/BigInt.res +57 -0
- package/src/bindings/Express.res +29 -0
- package/src/bindings/Postgres.res +98 -0
- package/src/bindings/Promise.res +52 -0
- package/src/db/EntityHistory.res +335 -0
- package/src/db/Schema.res +18 -0
- package/src/db/Table.res +251 -0
- package/src/sources/Rpc.res +181 -0
- package/src/vendored/Rest.res +661 -0
- package/src/vendored/Rest.resi +182 -0
|
@@ -0,0 +1,661 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Vendored from: https://github.com/DZakh/rescript-rest
|
|
3
|
+
Version: 1.0.1
|
|
4
|
+
|
|
5
|
+
IF EDITING THIS FILE, PLEASE LIST THE CHANGES BELOW
|
|
6
|
+
|
|
7
|
+
here
|
|
8
|
+
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
@@uncurried
|
|
12
|
+
|
|
13
|
+
module Exn = {
|
|
14
|
+
type error
|
|
15
|
+
|
|
16
|
+
@new
|
|
17
|
+
external makeError: string => error = "Error"
|
|
18
|
+
|
|
19
|
+
let raiseAny = (any: 'any): 'a => any->Obj.magic->raise
|
|
20
|
+
|
|
21
|
+
let raiseError: error => 'a = raiseAny
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
module Obj = {
|
|
25
|
+
external magic: 'a => 'b = "%identity"
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
module Promise = {
|
|
29
|
+
type t<+'a> = promise<'a>
|
|
30
|
+
|
|
31
|
+
@send
|
|
32
|
+
external thenResolve: (t<'a>, 'a => 'b) => t<'b> = "then"
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
module Option = {
|
|
36
|
+
let unsafeSome: 'a => option<'a> = Obj.magic
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
module Dict = {
|
|
40
|
+
@inline
|
|
41
|
+
let has = (dict, key) => {
|
|
42
|
+
dict->Js.Dict.unsafeGet(key)->(Obj.magic: 'a => bool)
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
@inline
|
|
47
|
+
let panic = message => Exn.raiseError(Exn.makeError(`[rescript-rest] ${message}`))
|
|
48
|
+
|
|
49
|
+
@val
|
|
50
|
+
external encodeURIComponent: string => string = "encodeURIComponent"
|
|
51
|
+
|
|
52
|
+
module ApiFetcher = {
|
|
53
|
+
type args = {body: option<unknown>, headers: option<dict<unknown>>, method: string, path: string}
|
|
54
|
+
type response = {data: unknown, status: int, headers: dict<unknown>}
|
|
55
|
+
type t = args => promise<response>
|
|
56
|
+
|
|
57
|
+
%%private(external fetch: (string, args) => promise<{..}> = "fetch")
|
|
58
|
+
|
|
59
|
+
// Inspired by https://github.com/ts-rest/ts-rest/blob/7792ef7bdc352e84a4f5766c53f984a9d630c60e/libs/ts-rest/core/src/lib/client.ts#L102
|
|
60
|
+
/**
|
|
61
|
+
* Default fetch api implementation:
|
|
62
|
+
*
|
|
63
|
+
* Can be used as a reference for implementing your own fetcher,
|
|
64
|
+
* or used in the "api" field of ClientArgs to allow you to hook
|
|
65
|
+
* into the request to run custom logic
|
|
66
|
+
*/
|
|
67
|
+
let default: t = async (args): response => {
|
|
68
|
+
let result = await fetch(args.path, args)
|
|
69
|
+
let contentType = result["headers"]["get"]("content-type")
|
|
70
|
+
|
|
71
|
+
// Note: contentType might be null
|
|
72
|
+
if (
|
|
73
|
+
contentType->Obj.magic &&
|
|
74
|
+
contentType->Js.String2.includes("application/") &&
|
|
75
|
+
contentType->Js.String2.includes("json")
|
|
76
|
+
) {
|
|
77
|
+
{
|
|
78
|
+
status: result["status"],
|
|
79
|
+
data: await result["json"](),
|
|
80
|
+
headers: result["headers"],
|
|
81
|
+
}
|
|
82
|
+
} else if contentType->Obj.magic && contentType->Js.String2.includes("text/") {
|
|
83
|
+
{
|
|
84
|
+
status: result["status"],
|
|
85
|
+
data: await result["text"](),
|
|
86
|
+
headers: result["headers"],
|
|
87
|
+
}
|
|
88
|
+
} else {
|
|
89
|
+
{
|
|
90
|
+
status: result["status"],
|
|
91
|
+
data: await result["blob"](),
|
|
92
|
+
headers: result["headers"],
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
module Response = {
|
|
99
|
+
type numiricStatus = [
|
|
100
|
+
| #100
|
|
101
|
+
| #101
|
|
102
|
+
| #102
|
|
103
|
+
| #200
|
|
104
|
+
| #201
|
|
105
|
+
| #202
|
|
106
|
+
| #203
|
|
107
|
+
| #204
|
|
108
|
+
| #205
|
|
109
|
+
| #206
|
|
110
|
+
| #207
|
|
111
|
+
| #300
|
|
112
|
+
| #301
|
|
113
|
+
| #302
|
|
114
|
+
| #303
|
|
115
|
+
| #304
|
|
116
|
+
| #305
|
|
117
|
+
| #307
|
|
118
|
+
| #308
|
|
119
|
+
| #400
|
|
120
|
+
| #401
|
|
121
|
+
| #402
|
|
122
|
+
| #403
|
|
123
|
+
| #404
|
|
124
|
+
| #405
|
|
125
|
+
| #406
|
|
126
|
+
| #407
|
|
127
|
+
| #408
|
|
128
|
+
| #409
|
|
129
|
+
| #410
|
|
130
|
+
| #411
|
|
131
|
+
| #412
|
|
132
|
+
| #413
|
|
133
|
+
| #414
|
|
134
|
+
| #415
|
|
135
|
+
| #416
|
|
136
|
+
| #417
|
|
137
|
+
| #418
|
|
138
|
+
| #419
|
|
139
|
+
| #420
|
|
140
|
+
| #421
|
|
141
|
+
| #422
|
|
142
|
+
| #423
|
|
143
|
+
| #424
|
|
144
|
+
| #428
|
|
145
|
+
| #429
|
|
146
|
+
| #431
|
|
147
|
+
| #451
|
|
148
|
+
| #500
|
|
149
|
+
| #501
|
|
150
|
+
| #502
|
|
151
|
+
| #503
|
|
152
|
+
| #504
|
|
153
|
+
| #505
|
|
154
|
+
| #507
|
|
155
|
+
| #511
|
|
156
|
+
]
|
|
157
|
+
type status = [
|
|
158
|
+
| #"1XX"
|
|
159
|
+
| #"2XX"
|
|
160
|
+
| #"3XX"
|
|
161
|
+
| #"4XX"
|
|
162
|
+
| #"5XX"
|
|
163
|
+
| numiricStatus
|
|
164
|
+
]
|
|
165
|
+
|
|
166
|
+
type s = {
|
|
167
|
+
status: int => unit,
|
|
168
|
+
description: string => unit,
|
|
169
|
+
data: 'value. S.t<'value> => 'value,
|
|
170
|
+
field: 'value. (string, S.t<'value>) => 'value,
|
|
171
|
+
header: 'value. (string, S.t<'value>) => 'value,
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
type t<'response> = {
|
|
175
|
+
// When it's empty, treat response as a default
|
|
176
|
+
status: option<int>,
|
|
177
|
+
description: option<string>,
|
|
178
|
+
dataSchema: S.t<unknown>,
|
|
179
|
+
emptyData: bool,
|
|
180
|
+
schema: S.t<'response>,
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
type builder<'response> = {
|
|
184
|
+
// When it's empty, treat response as a default
|
|
185
|
+
mutable status?: int,
|
|
186
|
+
mutable description?: string,
|
|
187
|
+
mutable dataSchema?: S.t<unknown>,
|
|
188
|
+
mutable emptyData: bool,
|
|
189
|
+
mutable schema?: S.t<'response>,
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
let register = (
|
|
193
|
+
map: dict<t<'response>>,
|
|
194
|
+
status: [< status | #default],
|
|
195
|
+
builder: builder<'response>,
|
|
196
|
+
) => {
|
|
197
|
+
let key = status->(Obj.magic: [< status | #default] => string)
|
|
198
|
+
if map->Dict.has(key) {
|
|
199
|
+
panic(`Response for the "${key}" status registered multiple times`)
|
|
200
|
+
} else {
|
|
201
|
+
map->Js.Dict.set(key, builder->(Obj.magic: builder<'response> => t<'response>))
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
@inline
|
|
206
|
+
let find = (map: dict<t<'response>>, responseStatus: int): option<t<'response>> => {
|
|
207
|
+
(map
|
|
208
|
+
->Js.Dict.unsafeGet(responseStatus->(Obj.magic: int => string))
|
|
209
|
+
->(Obj.magic: t<'response> => bool) ||
|
|
210
|
+
map
|
|
211
|
+
->Js.Dict.unsafeGet((responseStatus / 100)->(Obj.magic: int => string) ++ "XX")
|
|
212
|
+
->(Obj.magic: t<'response> => bool) ||
|
|
213
|
+
map->Js.Dict.unsafeGet("default")->(Obj.magic: t<'response> => bool))
|
|
214
|
+
->(Obj.magic: bool => option<t<'response>>)
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
type pathParam = {name: string}
|
|
219
|
+
@unboxed
|
|
220
|
+
type pathItem = Static(string) | Param(pathParam)
|
|
221
|
+
|
|
222
|
+
type auth = Bearer | Basic
|
|
223
|
+
|
|
224
|
+
type s = {
|
|
225
|
+
field: 'value. (string, S.t<'value>) => 'value,
|
|
226
|
+
body: 'value. S.t<'value> => 'value,
|
|
227
|
+
rawBody: 'value. S.t<'value> => 'value,
|
|
228
|
+
header: 'value. (string, S.t<'value>) => 'value,
|
|
229
|
+
query: 'value. (string, S.t<'value>) => 'value,
|
|
230
|
+
param: 'value. (string, S.t<'value>) => 'value,
|
|
231
|
+
auth: auth => string,
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
type method =
|
|
235
|
+
| @as("GET") Get
|
|
236
|
+
| @as("POST") Post
|
|
237
|
+
| @as("PUT") Put
|
|
238
|
+
| @as("PATCH") Patch
|
|
239
|
+
| @as("DELETE") Delete
|
|
240
|
+
| @as("HEAD") Head
|
|
241
|
+
| @as("OPTIONS") Options
|
|
242
|
+
| @as("TRACE") Trace
|
|
243
|
+
|
|
244
|
+
type definition<'variables, 'response> = {
|
|
245
|
+
method: method,
|
|
246
|
+
path: string,
|
|
247
|
+
variables: s => 'variables,
|
|
248
|
+
responses: array<Response.s => 'response>,
|
|
249
|
+
summary?: string,
|
|
250
|
+
description?: string,
|
|
251
|
+
deprecated?: bool,
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
type routeParams<'variables, 'response> = {
|
|
255
|
+
definition: definition<'variables, 'response>,
|
|
256
|
+
pathItems: array<pathItem>,
|
|
257
|
+
variablesSchema: S.t<'variables>,
|
|
258
|
+
responses: array<Response.t<'response>>,
|
|
259
|
+
responsesMap: dict<Response.t<'response>>,
|
|
260
|
+
isRawBody: bool,
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
type route<'variables, 'response> = unit => definition<'variables, 'response>
|
|
264
|
+
|
|
265
|
+
let rec parsePath = (path: string, ~pathItems, ~pathParams) => {
|
|
266
|
+
if path !== "" {
|
|
267
|
+
switch path->Js.String2.indexOf("{") {
|
|
268
|
+
| -1 => pathItems->Js.Array2.push(Static(path))->ignore
|
|
269
|
+
| paramStartIdx =>
|
|
270
|
+
switch path->Js.String2.indexOf("}") {
|
|
271
|
+
| -1 => panic("Path contains an unclosed parameter")
|
|
272
|
+
| paramEndIdx =>
|
|
273
|
+
if paramStartIdx > paramEndIdx {
|
|
274
|
+
panic("Path parameter is not enclosed in curly braces")
|
|
275
|
+
}
|
|
276
|
+
let paramName = Js.String2.slice(path, ~from=paramStartIdx + 1, ~to_=paramEndIdx)
|
|
277
|
+
if paramName === "" {
|
|
278
|
+
panic("Path parameter name cannot be empty")
|
|
279
|
+
}
|
|
280
|
+
let param = {name: paramName}
|
|
281
|
+
|
|
282
|
+
pathItems
|
|
283
|
+
->Js.Array2.push(Static(Js.String2.slice(path, ~from=0, ~to_=paramStartIdx)))
|
|
284
|
+
->ignore
|
|
285
|
+
pathItems->Js.Array2.push(Param(param))->ignore
|
|
286
|
+
pathParams->Js.Dict.set(paramName, param)->ignore
|
|
287
|
+
|
|
288
|
+
parsePath(Js.String2.sliceToEnd(path, ~from=paramEndIdx + 1), ~pathItems, ~pathParams)
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
let coerceSchema = schema => {
|
|
295
|
+
schema->S.preprocess(s => {
|
|
296
|
+
let tagged = switch s.schema->S.classify {
|
|
297
|
+
| Option(optionalSchema) => optionalSchema->S.classify
|
|
298
|
+
| tagged => tagged
|
|
299
|
+
}
|
|
300
|
+
switch tagged {
|
|
301
|
+
| Literal(Boolean(_))
|
|
302
|
+
| Bool => {
|
|
303
|
+
parser: unknown =>
|
|
304
|
+
switch unknown->Obj.magic {
|
|
305
|
+
| "true" => true
|
|
306
|
+
| "false" => false
|
|
307
|
+
| _ => unknown->Obj.magic
|
|
308
|
+
}->Obj.magic,
|
|
309
|
+
}
|
|
310
|
+
| Literal(Number(_))
|
|
311
|
+
| Int
|
|
312
|
+
| Float => {
|
|
313
|
+
parser: unknown => {
|
|
314
|
+
let float = %raw(`+unknown`)
|
|
315
|
+
if Js.Float.isNaN(float) {
|
|
316
|
+
unknown
|
|
317
|
+
} else {
|
|
318
|
+
float->Obj.magic
|
|
319
|
+
}
|
|
320
|
+
},
|
|
321
|
+
}
|
|
322
|
+
| String
|
|
323
|
+
| Literal(String(_))
|
|
324
|
+
| Union(_)
|
|
325
|
+
| Never => {}
|
|
326
|
+
| _ => {}
|
|
327
|
+
}
|
|
328
|
+
})
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
let bearerAuthSchema = S.string->S.transform(s => {
|
|
332
|
+
serializer: token => {
|
|
333
|
+
`Bearer ${token}`
|
|
334
|
+
},
|
|
335
|
+
parser: string => {
|
|
336
|
+
switch string->Js.String2.split(" ") {
|
|
337
|
+
| ["Bearer", token] => token
|
|
338
|
+
| _ => s.fail("Invalid Bearer token")
|
|
339
|
+
}
|
|
340
|
+
},
|
|
341
|
+
})
|
|
342
|
+
|
|
343
|
+
let basicAuthSchema = S.string->S.transform(s => {
|
|
344
|
+
serializer: token => {
|
|
345
|
+
`Basic ${token}`
|
|
346
|
+
},
|
|
347
|
+
parser: string => {
|
|
348
|
+
switch string->Js.String2.split(" ") {
|
|
349
|
+
| ["Basic", token] => token
|
|
350
|
+
| _ => s.fail("Invalid Basic token")
|
|
351
|
+
}
|
|
352
|
+
},
|
|
353
|
+
})
|
|
354
|
+
|
|
355
|
+
let params = route => {
|
|
356
|
+
switch (route->Obj.magic)["_rest"]->(
|
|
357
|
+
Obj.magic: unknown => option<routeParams<'variables, 'response>>
|
|
358
|
+
) {
|
|
359
|
+
| Some(params) => params
|
|
360
|
+
| None => {
|
|
361
|
+
let routeDefinition = (
|
|
362
|
+
route->(Obj.magic: route<'variables, 'response> => route<unknown, unknown>)
|
|
363
|
+
)()
|
|
364
|
+
|
|
365
|
+
let pathItems = []
|
|
366
|
+
let pathParams = Js.Dict.empty()
|
|
367
|
+
parsePath(routeDefinition.path, ~pathItems, ~pathParams)
|
|
368
|
+
|
|
369
|
+
// Don't use ref, since it creates an unnecessary object
|
|
370
|
+
let isRawBody = %raw(`false`)
|
|
371
|
+
|
|
372
|
+
let variablesSchema = S.object(s => {
|
|
373
|
+
routeDefinition.variables({
|
|
374
|
+
field: (fieldName, schema) => {
|
|
375
|
+
s.nestedField("body", fieldName, schema)
|
|
376
|
+
},
|
|
377
|
+
body: schema => {
|
|
378
|
+
s.field("body", schema)
|
|
379
|
+
},
|
|
380
|
+
rawBody: schema => {
|
|
381
|
+
let isNonStringBased = switch schema->S.classify {
|
|
382
|
+
| Literal(String(_))
|
|
383
|
+
| String => false
|
|
384
|
+
| _ => true
|
|
385
|
+
}
|
|
386
|
+
if isNonStringBased {
|
|
387
|
+
panic("Only string-based schemas are allowed in rawBody")
|
|
388
|
+
}
|
|
389
|
+
let _ = %raw(`isRawBody = true`)
|
|
390
|
+
s.field("body", schema)
|
|
391
|
+
},
|
|
392
|
+
header: (fieldName, schema) => {
|
|
393
|
+
s.nestedField("headers", fieldName->Js.String2.toLowerCase, coerceSchema(schema))
|
|
394
|
+
},
|
|
395
|
+
query: (fieldName, schema) => {
|
|
396
|
+
s.nestedField("query", fieldName, coerceSchema(schema))
|
|
397
|
+
},
|
|
398
|
+
param: (fieldName, schema) => {
|
|
399
|
+
if !Dict.has(pathParams, fieldName) {
|
|
400
|
+
panic(`Path parameter "${fieldName}" is not defined in the path`)
|
|
401
|
+
}
|
|
402
|
+
s.nestedField("params", fieldName, coerceSchema(schema))
|
|
403
|
+
},
|
|
404
|
+
auth: auth => {
|
|
405
|
+
s.nestedField(
|
|
406
|
+
"headers",
|
|
407
|
+
"authorization",
|
|
408
|
+
switch auth {
|
|
409
|
+
| Bearer => bearerAuthSchema
|
|
410
|
+
| Basic => basicAuthSchema
|
|
411
|
+
},
|
|
412
|
+
)
|
|
413
|
+
},
|
|
414
|
+
})
|
|
415
|
+
})
|
|
416
|
+
|
|
417
|
+
{
|
|
418
|
+
// The variables input is guaranteed to be an object, so we reset the rescript-schema type filter here
|
|
419
|
+
(variablesSchema->Obj.magic)["f"] = ()
|
|
420
|
+
(variablesSchema->S.classify->Obj.magic)["unknownKeys"] = S.Strip
|
|
421
|
+
let items: array<S.item> = (variablesSchema->S.classify->Obj.magic)["items"]
|
|
422
|
+
items->Js.Array2.forEach(item => {
|
|
423
|
+
let schema = item.schema
|
|
424
|
+
// Remove ${inputVar}.constructor!==Object check
|
|
425
|
+
(schema->Obj.magic)["f"] = (_b, ~inputVar) => `!${inputVar}`
|
|
426
|
+
})
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
let responsesMap = Js.Dict.empty()
|
|
430
|
+
let responses = []
|
|
431
|
+
routeDefinition.responses->Js.Array2.forEach(r => {
|
|
432
|
+
let builder: Response.builder<unknown> = {
|
|
433
|
+
emptyData: true,
|
|
434
|
+
}
|
|
435
|
+
let schema = S.object(s => {
|
|
436
|
+
let definition = r({
|
|
437
|
+
status: status => {
|
|
438
|
+
builder.status = Some(status)
|
|
439
|
+
let status = status->(Obj.magic: int => Response.status)
|
|
440
|
+
responsesMap->Response.register(status, builder)
|
|
441
|
+
s.tag("status", status)
|
|
442
|
+
},
|
|
443
|
+
description: d => builder.description = Some(d),
|
|
444
|
+
field: (fieldName, schema) => {
|
|
445
|
+
builder.emptyData = false
|
|
446
|
+
s.nestedField("data", fieldName, schema)
|
|
447
|
+
},
|
|
448
|
+
data: schema => {
|
|
449
|
+
builder.emptyData = false
|
|
450
|
+
s.field("data", schema)
|
|
451
|
+
},
|
|
452
|
+
header: (fieldName, schema) => {
|
|
453
|
+
s.nestedField("headers", fieldName->Js.String2.toLowerCase, coerceSchema(schema))
|
|
454
|
+
},
|
|
455
|
+
})
|
|
456
|
+
if builder.emptyData {
|
|
457
|
+
s.tag("data", %raw(`null`))
|
|
458
|
+
}
|
|
459
|
+
definition
|
|
460
|
+
})
|
|
461
|
+
if builder.status === None {
|
|
462
|
+
responsesMap->Response.register(#default, builder)
|
|
463
|
+
}
|
|
464
|
+
(schema->S.classify->Obj.magic)["unknownKeys"] = S.Strip
|
|
465
|
+
builder.dataSchema = (schema->S.classify->Obj.magic)["fields"]["data"]["t"]
|
|
466
|
+
builder.schema = Option.unsafeSome(schema)
|
|
467
|
+
responses
|
|
468
|
+
->Js.Array2.push(builder->(Obj.magic: Response.builder<unknown> => Response.t<unknown>))
|
|
469
|
+
->ignore
|
|
470
|
+
})
|
|
471
|
+
|
|
472
|
+
if responses->Js.Array2.length === 0 {
|
|
473
|
+
panic("At least single response should be registered")
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
let params = {
|
|
477
|
+
definition: routeDefinition,
|
|
478
|
+
variablesSchema,
|
|
479
|
+
responses,
|
|
480
|
+
pathItems,
|
|
481
|
+
responsesMap,
|
|
482
|
+
isRawBody,
|
|
483
|
+
}
|
|
484
|
+
(route->Obj.magic)["_rest"] = params
|
|
485
|
+
params->(Obj.magic: routeParams<unknown, unknown> => routeParams<'variables, 'response>)
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
external route: (unit => definition<'variables, 'response>) => route<'variables, 'response> =
|
|
491
|
+
"%identity"
|
|
492
|
+
|
|
493
|
+
type client = {
|
|
494
|
+
call: 'variables 'response. (route<'variables, 'response>, 'variables) => promise<'response>,
|
|
495
|
+
baseUrl: string,
|
|
496
|
+
fetcher: ApiFetcher.t,
|
|
497
|
+
// By default, all query parameters are encoded as strings, however, you can use the jsonQuery option to encode query parameters as typed JSON values.
|
|
498
|
+
jsonQuery: bool,
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
/**
|
|
502
|
+
* A recursive function to convert an object/string/number/whatever into an array of key=value pairs
|
|
503
|
+
*
|
|
504
|
+
* This should be fully compatible with the "qs" library, but more optimised and without the need to add a dependency
|
|
505
|
+
*/
|
|
506
|
+
let rec tokeniseValue = (key, value, ~append) => {
|
|
507
|
+
if Js.Array2.isArray(value) {
|
|
508
|
+
value
|
|
509
|
+
->(Obj.magic: unknown => array<unknown>)
|
|
510
|
+
->Js.Array2.forEachi((v, idx) => {
|
|
511
|
+
tokeniseValue(`${key}[${idx->Js.Int.toString}]`, v, ~append)
|
|
512
|
+
})
|
|
513
|
+
} else if value === %raw(`null`) {
|
|
514
|
+
append(key, "")
|
|
515
|
+
} else if value === %raw(`void 0`) {
|
|
516
|
+
()
|
|
517
|
+
} else if Js.typeof(value) === "object" {
|
|
518
|
+
let dict = value->(Obj.magic: unknown => dict<unknown>)
|
|
519
|
+
dict
|
|
520
|
+
->Js.Dict.keys
|
|
521
|
+
->Js.Array2.forEach(k => {
|
|
522
|
+
tokeniseValue(`${key}[${encodeURIComponent(k)}]`, dict->Js.Dict.unsafeGet(k), ~append)
|
|
523
|
+
})
|
|
524
|
+
} else {
|
|
525
|
+
append(key, value->(Obj.magic: unknown => string))
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
// Inspired by https://github.com/ts-rest/ts-rest/blob/7792ef7bdc352e84a4f5766c53f984a9d630c60e/libs/ts-rest/core/src/lib/client.ts#L347
|
|
530
|
+
let getCompletePath = (~baseUrl, ~pathItems, ~maybeQuery, ~maybeParams, ~jsonQuery) => {
|
|
531
|
+
let path = ref(baseUrl)
|
|
532
|
+
|
|
533
|
+
for idx in 0 to pathItems->Js.Array2.length - 1 {
|
|
534
|
+
let pathItem = pathItems->Js.Array2.unsafe_get(idx)
|
|
535
|
+
switch pathItem {
|
|
536
|
+
| Static(static) => path := path.contents ++ static
|
|
537
|
+
| Param({name}) =>
|
|
538
|
+
switch (maybeParams->Obj.magic && maybeParams->Js.Dict.unsafeGet(name)->Obj.magic)
|
|
539
|
+
->(Obj.magic: bool => option<string>) {
|
|
540
|
+
| Some(param) => path := path.contents ++ param
|
|
541
|
+
| None => panic(`Path parameter "${name}" is not defined in variables`)
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
switch maybeQuery {
|
|
547
|
+
| None => ()
|
|
548
|
+
| Some(query) => {
|
|
549
|
+
let queryItems = []
|
|
550
|
+
|
|
551
|
+
let append = (key, value) => {
|
|
552
|
+
let _ = queryItems->Js.Array2.push(key ++ "=" ++ encodeURIComponent(value))
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
let queryNames = query->Js.Dict.keys
|
|
556
|
+
for idx in 0 to queryNames->Js.Array2.length - 1 {
|
|
557
|
+
let queryName = queryNames->Js.Array2.unsafe_get(idx)
|
|
558
|
+
let value = query->Js.Dict.unsafeGet(queryName)
|
|
559
|
+
let key = encodeURIComponent(queryName)
|
|
560
|
+
if value !== %raw(`void 0`) {
|
|
561
|
+
switch jsonQuery {
|
|
562
|
+
// if value is a string and is not a reserved JSON value or a number, pass it without encoding
|
|
563
|
+
// this makes strings look nicer in the URL (e.g. ?name=John instead of ?name=%22John%22)
|
|
564
|
+
// this is also how OpenAPI will pass strings even if they are marked as application/json types
|
|
565
|
+
| true =>
|
|
566
|
+
append(
|
|
567
|
+
key,
|
|
568
|
+
if (
|
|
569
|
+
Js.typeof(value) === "string" && {
|
|
570
|
+
let value = value->(Obj.magic: unknown => string)
|
|
571
|
+
value !== "true" &&
|
|
572
|
+
value !== "false" &&
|
|
573
|
+
value !== "null" &&
|
|
574
|
+
Js.Float.isNaN(Js.Float.fromString(value))
|
|
575
|
+
}
|
|
576
|
+
) {
|
|
577
|
+
value->(Obj.magic: unknown => string)
|
|
578
|
+
} else {
|
|
579
|
+
value->(Obj.magic: unknown => Js.Json.t)->Js.Json.stringify
|
|
580
|
+
},
|
|
581
|
+
)
|
|
582
|
+
| false => tokeniseValue(key, value, ~append)
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
if queryItems->Js.Array2.length > 0 {
|
|
588
|
+
path := path.contents ++ "?" ++ queryItems->Js.Array2.joinWith("&")
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
path.contents
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
let fetch = (
|
|
597
|
+
type variables response,
|
|
598
|
+
route: route<variables, response>,
|
|
599
|
+
baseUrl,
|
|
600
|
+
variables,
|
|
601
|
+
~fetcher=ApiFetcher.default,
|
|
602
|
+
~jsonQuery=false,
|
|
603
|
+
) => {
|
|
604
|
+
let route = route->(Obj.magic: route<variables, response> => route<unknown, unknown>)
|
|
605
|
+
let variables = variables->(Obj.magic: variables => unknown)
|
|
606
|
+
|
|
607
|
+
let {definition, variablesSchema, responsesMap, pathItems, isRawBody} = route->params
|
|
608
|
+
|
|
609
|
+
let data = variables->S.serializeToUnknownOrRaiseWith(variablesSchema)->Obj.magic
|
|
610
|
+
|
|
611
|
+
if data["body"] !== %raw(`void 0`) {
|
|
612
|
+
if !isRawBody {
|
|
613
|
+
data["body"] = %raw(`JSON.stringify(data["body"])`)
|
|
614
|
+
}
|
|
615
|
+
if data["headers"] === %raw(`void 0`) {
|
|
616
|
+
data["headers"] = %raw(`{}`)
|
|
617
|
+
}
|
|
618
|
+
data["headers"]["content-type"] = "application/json"
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
fetcher({
|
|
622
|
+
body: data["body"],
|
|
623
|
+
headers: data["headers"],
|
|
624
|
+
path: getCompletePath(
|
|
625
|
+
~baseUrl,
|
|
626
|
+
~pathItems,
|
|
627
|
+
~maybeQuery=data["query"],
|
|
628
|
+
~maybeParams=data["params"],
|
|
629
|
+
~jsonQuery,
|
|
630
|
+
),
|
|
631
|
+
method: (definition.method :> string),
|
|
632
|
+
})->Promise.thenResolve(fetcherResponse => {
|
|
633
|
+
switch responsesMap->Response.find(fetcherResponse.status) {
|
|
634
|
+
| None =>
|
|
635
|
+
let error = ref(`Unexpected response status "${fetcherResponse.status->Js.Int.toString}"`)
|
|
636
|
+
if (
|
|
637
|
+
fetcherResponse.data->Obj.magic &&
|
|
638
|
+
Js.typeof((fetcherResponse.data->Obj.magic)["message"]) === "string"
|
|
639
|
+
) {
|
|
640
|
+
error :=
|
|
641
|
+
error.contents ++ ". Message: " ++ (fetcherResponse.data->Obj.magic)["message"]->Obj.magic
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
panic(error.contents)
|
|
645
|
+
| Some(response) =>
|
|
646
|
+
fetcherResponse
|
|
647
|
+
->S.parseAnyOrRaiseWith(response.schema)
|
|
648
|
+
->(Obj.magic: unknown => response)
|
|
649
|
+
}
|
|
650
|
+
})
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
let client = (~baseUrl, ~fetcher=ApiFetcher.default, ~jsonQuery=false) => {
|
|
654
|
+
let call = (route, variables) => route->fetch(baseUrl, variables, ~fetcher, ~jsonQuery)
|
|
655
|
+
{
|
|
656
|
+
baseUrl,
|
|
657
|
+
fetcher,
|
|
658
|
+
call,
|
|
659
|
+
jsonQuery,
|
|
660
|
+
}
|
|
661
|
+
}
|