effect-app 1.20.0 → 1.21.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +11 -0
- package/_cjs/client/clientFor.cjs +147 -96
- package/_cjs/client/clientFor.cjs.map +1 -1
- package/_cjs/client.cjs +0 -11
- package/_cjs/client.cjs.map +1 -1
- package/dist/client/clientFor.d.ts +38 -28
- package/dist/client/clientFor.d.ts.map +1 -1
- package/dist/client/clientFor.js +115 -97
- package/dist/client.d.ts +0 -1
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +1 -2
- package/package.json +2 -32
- package/src/client/clientFor.ts +241 -158
- package/src/client.ts +0 -1
- package/vitest.config.ts.timestamp-1670862388823.mjs +23 -0
- package/_cjs/client/clientFor2.cjs +0 -197
- package/_cjs/client/clientFor2.cjs.map +0 -1
- package/_cjs/client/fetch.cjs +0 -176
- package/_cjs/client/fetch.cjs.map +0 -1
- package/_cjs/client/router.cjs +0 -31
- package/_cjs/client/router.cjs.map +0 -1
- package/dist/client/clientFor2.d.ts +0 -49
- package/dist/client/clientFor2.d.ts.map +0 -1
- package/dist/client/clientFor2.js +0 -165
- package/dist/client/fetch.d.ts +0 -56
- package/dist/client/fetch.d.ts.map +0 -1
- package/dist/client/fetch.js +0 -158
- package/dist/client/router.d.ts +0 -32
- package/dist/client/router.d.ts.map +0 -1
- package/dist/client/router.js +0 -17
- package/src/client/clientFor2.ts +0 -339
- package/src/client/fetch.ts +0 -299
- package/src/client/router.ts +0 -87
- package/vitest.config.ts.timestamp-1709838418683-4e8d39caec6be.mjs +0 -33
- package/vitest.config.ts.timestamp-1711656440837-b61cd88636759.mjs +0 -37
- package/vitest.config.ts.timestamp-1711724061890-f88ec51585e5c.mjs +0 -37
- package/vitest.config.ts.timestamp-1711743471020-0a3f182bca84f.mjs +0 -0
- package/vitest.config.ts.timestamp-1711743489536-f03d3db08b368.mjs +0 -37
- package/vitest.config.ts.timestamp-1711743593445-f537ce2bb2c6a.mjs +0 -0
package/src/client/clientFor2.ts
DELETED
|
@@ -1,339 +0,0 @@
|
|
|
1
|
-
/* eslint-disable @typescript-eslint/no-unsafe-argument */
|
|
2
|
-
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
3
|
-
|
|
4
|
-
import { Effect, flow, HashMap, Layer, Option, Predicate, Struct } from "@effect-app/core"
|
|
5
|
-
import { RpcResolver } from "@effect/rpc"
|
|
6
|
-
import { HttpRpcResolver } from "@effect/rpc-http"
|
|
7
|
-
import type { RpcRouter } from "@effect/rpc/RpcRouter"
|
|
8
|
-
import type * as Serializable from "@effect/schema/Serializable"
|
|
9
|
-
import { S } from "effect-app"
|
|
10
|
-
import type { FetchResponse } from "effect-app/client"
|
|
11
|
-
import { ApiConfig, makePathWithBody, makePathWithQuery } from "effect-app/client"
|
|
12
|
-
import { HttpClient, HttpClientRequest } from "effect-app/http"
|
|
13
|
-
import type { Schema } from "effect-app/schema"
|
|
14
|
-
import { typedKeysOf } from "effect-app/utils"
|
|
15
|
-
import type * as Request from "effect/Request"
|
|
16
|
-
import { Path } from "path-parser"
|
|
17
|
-
|
|
18
|
-
type Requests = Record<string, any>
|
|
19
|
-
|
|
20
|
-
const apiClient = Effect.gen(function*() {
|
|
21
|
-
const client = yield* HttpClient.HttpClient
|
|
22
|
-
const config = yield* ApiConfig.Tag
|
|
23
|
-
return client.pipe(
|
|
24
|
-
HttpClient.mapRequest(HttpClientRequest.prependUrl(config.apiUrl + "/rpc")),
|
|
25
|
-
HttpClient.mapRequest(
|
|
26
|
-
HttpClientRequest.setHeaders(config.headers.pipe(Option.getOrElse(() => HashMap.empty())))
|
|
27
|
-
)
|
|
28
|
-
)
|
|
29
|
-
})
|
|
30
|
-
|
|
31
|
-
export type Client<M extends Requests> =
|
|
32
|
-
& RequestHandlers<
|
|
33
|
-
ApiConfig | HttpClient.HttpClient.Service,
|
|
34
|
-
never, // SupportedErrors | FetchError | ResError,
|
|
35
|
-
M
|
|
36
|
-
>
|
|
37
|
-
& RequestHandlersE<
|
|
38
|
-
ApiConfig | HttpClient.HttpClient.Service,
|
|
39
|
-
never, // SupportedErrors | FetchError | ResError,
|
|
40
|
-
M
|
|
41
|
-
>
|
|
42
|
-
|
|
43
|
-
export function clientFor2(layers: Layer.Layer<never, never, never>) {
|
|
44
|
-
const cache = new Map<any, Client<any>>()
|
|
45
|
-
|
|
46
|
-
return <M extends Requests>(
|
|
47
|
-
models: M
|
|
48
|
-
): Client<Omit<M, "meta">> => {
|
|
49
|
-
const found = cache.get(models)
|
|
50
|
-
if (found) {
|
|
51
|
-
return found
|
|
52
|
-
}
|
|
53
|
-
const m = clientFor_(models, layers)
|
|
54
|
-
cache.set(models, m)
|
|
55
|
-
return m
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
type Req = S.Schema.All & {
|
|
60
|
-
new(...args: any[]): any
|
|
61
|
-
_tag: string
|
|
62
|
-
fields: S.Struct.Fields
|
|
63
|
-
success: S.Schema.Any
|
|
64
|
-
failure: S.Schema.Any
|
|
65
|
-
config?: Record<string, any>
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
function clientFor_<M extends Requests>(models: M, layers = Layer.empty) {
|
|
69
|
-
type Filtered = {
|
|
70
|
-
[K in keyof Requests as Requests[K] extends Req ? K : never]: Requests[K] extends Req ? Requests[K] : never
|
|
71
|
-
}
|
|
72
|
-
const filtered = typedKeysOf(models).reduce((acc, cur) => {
|
|
73
|
-
if (
|
|
74
|
-
Predicate.isObject(models[cur])
|
|
75
|
-
&& (models[cur].success)
|
|
76
|
-
) {
|
|
77
|
-
acc[cur as keyof Filtered] = models[cur]
|
|
78
|
-
}
|
|
79
|
-
return acc
|
|
80
|
-
}, {} as Record<keyof Filtered, Req>)
|
|
81
|
-
|
|
82
|
-
const meta = (models as any).meta as { moduleName: string }
|
|
83
|
-
if (!meta) throw new Error("No meta defined in Resource!")
|
|
84
|
-
|
|
85
|
-
const resolver = flow(
|
|
86
|
-
HttpRpcResolver.make<RpcRouter<any, any>>,
|
|
87
|
-
(_) => RpcResolver.toClient(_ as any)
|
|
88
|
-
)
|
|
89
|
-
|
|
90
|
-
const baseClient = apiClient.pipe(
|
|
91
|
-
Effect.andThen(HttpClient.mapRequest(HttpClientRequest.appendUrl("/" + meta.moduleName)))
|
|
92
|
-
)
|
|
93
|
-
|
|
94
|
-
return (typedKeysOf(filtered)
|
|
95
|
-
.reduce((prev, cur) => {
|
|
96
|
-
const h = filtered[cur]!
|
|
97
|
-
|
|
98
|
-
const Request = h
|
|
99
|
-
const Response = h.success
|
|
100
|
-
|
|
101
|
-
const encodeRequest = S.encodeSync(
|
|
102
|
-
Request as unknown as S.Schema<any, any>
|
|
103
|
-
)
|
|
104
|
-
|
|
105
|
-
const requestName = `${meta.moduleName}.${cur as string}`
|
|
106
|
-
.replaceAll(".js", "")
|
|
107
|
-
|
|
108
|
-
const requestMeta = {
|
|
109
|
-
method: "POST", // TODO
|
|
110
|
-
Request,
|
|
111
|
-
Response,
|
|
112
|
-
mapPath: requestName,
|
|
113
|
-
name: requestName
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
const client = baseClient.pipe(
|
|
117
|
-
Effect.andThen(HttpClient.mapRequest(HttpClientRequest.appendUrlParam("action", cur as string))),
|
|
118
|
-
Effect.andThen(resolver)
|
|
119
|
-
)
|
|
120
|
-
|
|
121
|
-
const fields = Struct.omit(Request.fields, "_tag")
|
|
122
|
-
const p = requestName
|
|
123
|
-
const path = new Path(p) // TODO
|
|
124
|
-
// @ts-expect-error doc
|
|
125
|
-
prev[cur] = requestMeta.method === "GET"
|
|
126
|
-
? Object.keys(fields).length === 0
|
|
127
|
-
? {
|
|
128
|
-
handler: client
|
|
129
|
-
.pipe(
|
|
130
|
-
Effect.andThen((cl) => cl(new Request())),
|
|
131
|
-
Effect.map((_) => ({ body: _, status: 200, headers: {} })), // TODO
|
|
132
|
-
Effect
|
|
133
|
-
.withSpan("client.request " + requestName, {
|
|
134
|
-
captureStackTrace: false,
|
|
135
|
-
attributes: { "request.name": requestName }
|
|
136
|
-
}),
|
|
137
|
-
Effect.provide(layers)
|
|
138
|
-
),
|
|
139
|
-
...requestMeta
|
|
140
|
-
}
|
|
141
|
-
: {
|
|
142
|
-
handler: (req: any) =>
|
|
143
|
-
client
|
|
144
|
-
.pipe(
|
|
145
|
-
Effect.andThen((cl) => cl(new Request(req))),
|
|
146
|
-
Effect.map((_) => ({ body: _, status: 200, headers: {} })), // TODO
|
|
147
|
-
Effect
|
|
148
|
-
.withSpan("client.request " + requestName, {
|
|
149
|
-
captureStackTrace: false,
|
|
150
|
-
attributes: { "request.name": requestName }
|
|
151
|
-
}),
|
|
152
|
-
Effect.provide(layers)
|
|
153
|
-
),
|
|
154
|
-
...requestMeta,
|
|
155
|
-
mapPath: (req: any) => req ? makePathWithQuery(path, encodeRequest(req)) : p
|
|
156
|
-
}
|
|
157
|
-
: Object.keys(fields).length === 0
|
|
158
|
-
? {
|
|
159
|
-
handler: client
|
|
160
|
-
.pipe(
|
|
161
|
-
Effect.andThen((cl) => cl(new Request())),
|
|
162
|
-
Effect.map((_) => ({ body: _, status: 200, headers: {} })), // TODO
|
|
163
|
-
Effect.withSpan("client.request " + requestName, {
|
|
164
|
-
captureStackTrace: false,
|
|
165
|
-
attributes: { "request.name": requestName }
|
|
166
|
-
}),
|
|
167
|
-
Effect.provide(layers)
|
|
168
|
-
),
|
|
169
|
-
...requestMeta
|
|
170
|
-
}
|
|
171
|
-
: {
|
|
172
|
-
handler: (req: any) =>
|
|
173
|
-
client
|
|
174
|
-
.pipe(
|
|
175
|
-
Effect.andThen((cl) => cl(new Request(req))),
|
|
176
|
-
Effect.map((_) => ({ body: _, status: 200, headers: {} })), // TODO
|
|
177
|
-
Effect.withSpan("client.request " + requestName, {
|
|
178
|
-
captureStackTrace: false,
|
|
179
|
-
attributes: { "request.name": requestName }
|
|
180
|
-
}),
|
|
181
|
-
Effect.provide(layers)
|
|
182
|
-
),
|
|
183
|
-
|
|
184
|
-
...requestMeta,
|
|
185
|
-
mapPath: (req: any) =>
|
|
186
|
-
req
|
|
187
|
-
? requestMeta.method === "DELETE"
|
|
188
|
-
? makePathWithQuery(path, encodeRequest(req))
|
|
189
|
-
: makePathWithBody(path, encodeRequest(req))
|
|
190
|
-
: p
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
// generate handler
|
|
194
|
-
|
|
195
|
-
// @ts-expect-error doc
|
|
196
|
-
prev[`${cur}E`] = requestMeta.method === "GET"
|
|
197
|
-
? Object.keys(fields).length === 0
|
|
198
|
-
? {
|
|
199
|
-
handler: client
|
|
200
|
-
.pipe(
|
|
201
|
-
Effect.andThen((cl) => cl(new Request())),
|
|
202
|
-
Effect.flatMap((res) => S.encode(Response)(res)), // TODO
|
|
203
|
-
Effect.map((_) => ({ body: _, status: 200, headers: {} })), // TODO,
|
|
204
|
-
Effect
|
|
205
|
-
.withSpan("client.request " + requestName, {
|
|
206
|
-
captureStackTrace: false,
|
|
207
|
-
attributes: { "request.name": requestName }
|
|
208
|
-
}),
|
|
209
|
-
Effect.provide(layers)
|
|
210
|
-
),
|
|
211
|
-
...requestMeta
|
|
212
|
-
}
|
|
213
|
-
: {
|
|
214
|
-
handler: (req: any) =>
|
|
215
|
-
client
|
|
216
|
-
.pipe(
|
|
217
|
-
Effect.andThen((cl) => cl(new Request(req))),
|
|
218
|
-
Effect.flatMap((res) => S.encode(Response)(res)), // TODO
|
|
219
|
-
Effect.map((_) => ({ body: _, status: 200, headers: {} })), // TODO,
|
|
220
|
-
Effect
|
|
221
|
-
.withSpan("client.request " + requestName, {
|
|
222
|
-
captureStackTrace: false,
|
|
223
|
-
attributes: { "request.name": requestName }
|
|
224
|
-
}),
|
|
225
|
-
Effect.provide(layers)
|
|
226
|
-
),
|
|
227
|
-
|
|
228
|
-
...requestMeta,
|
|
229
|
-
mapPath: (req: any) => req ? makePathWithQuery(path, encodeRequest(req)) : p
|
|
230
|
-
}
|
|
231
|
-
: Object.keys(fields).length === 0
|
|
232
|
-
? {
|
|
233
|
-
handler: client
|
|
234
|
-
.pipe(
|
|
235
|
-
Effect.andThen((cl) => cl(new Request())),
|
|
236
|
-
Effect.flatMap((res) => S.encode(Response)(res)), // TODO
|
|
237
|
-
Effect.map((_) => ({ body: _, status: 200, headers: {} })), // TODO,
|
|
238
|
-
Effect.withSpan("client.request " + requestName, {
|
|
239
|
-
captureStackTrace: false,
|
|
240
|
-
attributes: { "request.name": requestName }
|
|
241
|
-
}),
|
|
242
|
-
Effect.provide(layers)
|
|
243
|
-
),
|
|
244
|
-
...requestMeta
|
|
245
|
-
}
|
|
246
|
-
: {
|
|
247
|
-
handler: (req: any) =>
|
|
248
|
-
client
|
|
249
|
-
.pipe(
|
|
250
|
-
Effect.andThen((cl) => cl(new Request(req))),
|
|
251
|
-
Effect.flatMap((res) => S.encode(Response)(res)), // TODO
|
|
252
|
-
Effect.map((_) => ({ body: _, status: 200, headers: {} })), // TODO,
|
|
253
|
-
Effect.withSpan("client.request " + requestName, {
|
|
254
|
-
captureStackTrace: false,
|
|
255
|
-
attributes: { "request.name": requestName }
|
|
256
|
-
}),
|
|
257
|
-
Effect.provide(layers)
|
|
258
|
-
),
|
|
259
|
-
|
|
260
|
-
...requestMeta,
|
|
261
|
-
mapPath: (req: any) =>
|
|
262
|
-
req
|
|
263
|
-
? requestMeta.method === "DELETE"
|
|
264
|
-
? makePathWithQuery(path, encodeRequest(req))
|
|
265
|
-
: makePathWithBody(path, encodeRequest(req))
|
|
266
|
-
: p
|
|
267
|
-
}
|
|
268
|
-
// generate handler
|
|
269
|
-
|
|
270
|
-
return prev
|
|
271
|
-
}, {} as Client<M>))
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
export type ExtractResponse<T> = T extends Schema<any, any, any> ? Schema.Type<T>
|
|
275
|
-
: T extends unknown ? void
|
|
276
|
-
: never
|
|
277
|
-
|
|
278
|
-
export type ExtractEResponse<T> = T extends Schema<any, any, any> ? Schema.Encoded<T>
|
|
279
|
-
: T extends unknown ? void
|
|
280
|
-
: never
|
|
281
|
-
|
|
282
|
-
type IsEmpty<T> = keyof T extends never ? true
|
|
283
|
-
: false
|
|
284
|
-
|
|
285
|
-
type Cruft = "_tag" | Request.RequestTypeId | typeof Serializable.symbol | typeof Serializable.symbolResult
|
|
286
|
-
|
|
287
|
-
// TODO: refactor to new Request pattern, then filter out non-requests similar to the runtime changes in clientFor, and matchFor (boilerplate)
|
|
288
|
-
type RequestHandlers<R, E, M extends Requests> = {
|
|
289
|
-
[K in keyof M]: IsEmpty<Omit<S.Schema.Type<M[K]>, Cruft>> extends true ? {
|
|
290
|
-
handler: Effect<FetchResponse<Schema.Type<M[K]["success"]>>, Schema.Type<M[K]["failure"]> | E, R>
|
|
291
|
-
Request: M[K]
|
|
292
|
-
Reponse: Schema.Type<M[K]["success"]>
|
|
293
|
-
mapPath: string
|
|
294
|
-
name: string
|
|
295
|
-
}
|
|
296
|
-
: {
|
|
297
|
-
handler: (
|
|
298
|
-
req: Omit<S.Schema.Type<M[K]>, Cruft>
|
|
299
|
-
) => Effect<
|
|
300
|
-
FetchResponse<Schema.Type<M[K]["success"]>>,
|
|
301
|
-
Schema.Type<M[K]["failure"]> | E,
|
|
302
|
-
R
|
|
303
|
-
>
|
|
304
|
-
Request: M[K]
|
|
305
|
-
Reponse: Schema.Type<M[K]["success"]>
|
|
306
|
-
mapPath: (req: Omit<S.Schema.Type<M[K]>, Cruft>) => string
|
|
307
|
-
name: string
|
|
308
|
-
}
|
|
309
|
-
}
|
|
310
|
-
|
|
311
|
-
type RequestHandlersE<R, E, M extends Requests> = {
|
|
312
|
-
[K in keyof M & string as `${K}E`]: IsEmpty<Omit<S.Schema.Type<M[K]>, Cruft>> extends true ? {
|
|
313
|
-
handler: Effect<
|
|
314
|
-
FetchResponse<Schema.Encoded<M[K]["success"]>>,
|
|
315
|
-
Schema.Type<M[K]["failure"]> | E,
|
|
316
|
-
R
|
|
317
|
-
>
|
|
318
|
-
Request: M[K]
|
|
319
|
-
Reponse: Schema.Type<M[K]["success"]>
|
|
320
|
-
mapPath: string
|
|
321
|
-
name: string
|
|
322
|
-
}
|
|
323
|
-
: {
|
|
324
|
-
handler: (
|
|
325
|
-
req: Omit<
|
|
326
|
-
S.Schema.Type<M[K]>,
|
|
327
|
-
Cruft
|
|
328
|
-
>
|
|
329
|
-
) => Effect<
|
|
330
|
-
FetchResponse<Schema.Encoded<M[K]["success"]>>,
|
|
331
|
-
Schema.Type<M[K]["failure"]> | E,
|
|
332
|
-
R
|
|
333
|
-
>
|
|
334
|
-
Request: M[K]
|
|
335
|
-
Reponse: Schema.Type<M[K]["success"]>
|
|
336
|
-
mapPath: (req: Omit<S.Schema.Type<M[K]>, Cruft>) => string
|
|
337
|
-
name: string
|
|
338
|
-
}
|
|
339
|
-
}
|
package/src/client/fetch.ts
DELETED
|
@@ -1,299 +0,0 @@
|
|
|
1
|
-
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
2
|
-
import { Effect, HashMap, Option } from "@effect-app/core"
|
|
3
|
-
import { constant } from "@effect-app/core/Function"
|
|
4
|
-
import type { Headers, HttpError, HttpRequestError, HttpResponseError, Method } from "@effect-app/core/http/http-client"
|
|
5
|
-
import type { ResponseError } from "@effect/platform/HttpClientError"
|
|
6
|
-
import { Record } from "effect"
|
|
7
|
-
import type { REST, Schema } from "effect-app/schema"
|
|
8
|
-
import { StringId } from "effect-app/schema"
|
|
9
|
-
import { Path } from "path-parser"
|
|
10
|
-
import qs from "query-string"
|
|
11
|
-
import { HttpClient, HttpClientRequest } from "../http.js"
|
|
12
|
-
import { S } from "../lib.js"
|
|
13
|
-
import { PreludeLogger } from "../logger.js"
|
|
14
|
-
import { ApiConfig } from "./config.js"
|
|
15
|
-
import type { SupportedErrors } from "./errors.js"
|
|
16
|
-
import {
|
|
17
|
-
InvalidStateError,
|
|
18
|
-
NotFoundError,
|
|
19
|
-
NotLoggedInError,
|
|
20
|
-
OptimisticConcurrencyException,
|
|
21
|
-
ServiceUnavailableError,
|
|
22
|
-
UnauthorizedError,
|
|
23
|
-
ValidationError
|
|
24
|
-
} from "./errors.js"
|
|
25
|
-
|
|
26
|
-
export type FetchError = HttpError<string>
|
|
27
|
-
|
|
28
|
-
export class ResError {
|
|
29
|
-
public readonly _tag = "ResponseError"
|
|
30
|
-
constructor(public readonly error: unknown) {}
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
const getClient = Effect.flatMap(
|
|
34
|
-
HttpClient.HttpClient,
|
|
35
|
-
(defaultClient) =>
|
|
36
|
-
Effect.map(ApiConfig.Tag, ({ apiUrl, headers }) =>
|
|
37
|
-
defaultClient
|
|
38
|
-
.pipe(
|
|
39
|
-
HttpClient.filterStatusOk,
|
|
40
|
-
HttpClient
|
|
41
|
-
.mapRequest((_) =>
|
|
42
|
-
_.pipe(
|
|
43
|
-
HttpClientRequest.acceptJson,
|
|
44
|
-
HttpClientRequest.prependUrl(apiUrl),
|
|
45
|
-
HttpClientRequest
|
|
46
|
-
.setHeaders({
|
|
47
|
-
"request-id": Option.getOrUndefined(Option.flatMap(headers, (_) => HashMap.get(_, "request-id")))
|
|
48
|
-
?? StringId.make(),
|
|
49
|
-
...Option.getOrUndefined(Option.map(headers, (_) => Object.fromEntries(_)))
|
|
50
|
-
})
|
|
51
|
-
)
|
|
52
|
-
),
|
|
53
|
-
HttpClient
|
|
54
|
-
.tapRequest((r) =>
|
|
55
|
-
PreludeLogger
|
|
56
|
-
.logDebug(`[HTTP] ${r.method}`)
|
|
57
|
-
.pipe(Effect.annotateLogs({
|
|
58
|
-
"url": r.url,
|
|
59
|
-
"body": r.body._tag === "Uint8Array"
|
|
60
|
-
? new TextDecoder().decode(r.body.body)
|
|
61
|
-
: r.body._tag,
|
|
62
|
-
"headers": r.headers
|
|
63
|
-
}))
|
|
64
|
-
),
|
|
65
|
-
HttpClient
|
|
66
|
-
.mapEffect((_) =>
|
|
67
|
-
(_.status === 204
|
|
68
|
-
? Effect.sync(() => ({ status: _.status, body: void 0, headers: _.headers }))
|
|
69
|
-
: Effect.map(_.json, (body) => ({ status: _.status, body, headers: _.headers })))
|
|
70
|
-
.pipe(Effect.withSpan("client.response", { captureStackTrace: false }))
|
|
71
|
-
)
|
|
72
|
-
))
|
|
73
|
-
)
|
|
74
|
-
|
|
75
|
-
export function fetchApi(
|
|
76
|
-
method: Method,
|
|
77
|
-
path: string,
|
|
78
|
-
body?: unknown,
|
|
79
|
-
responseError?: S.Schema.AnyNoContext
|
|
80
|
-
) {
|
|
81
|
-
return Effect.flatMap(getClient, (client) =>
|
|
82
|
-
(method === "GET"
|
|
83
|
-
? client.execute(HttpClientRequest.make(method)(path))
|
|
84
|
-
: body === undefined
|
|
85
|
-
? client.execute(HttpClientRequest.make(method)(path))
|
|
86
|
-
: HttpClientRequest
|
|
87
|
-
.make(method)(path)
|
|
88
|
-
.pipe(HttpClientRequest.bodyJson(body), Effect.flatMap(client.execute)))
|
|
89
|
-
.pipe(
|
|
90
|
-
Effect
|
|
91
|
-
.catchTag(
|
|
92
|
-
"ResponseError",
|
|
93
|
-
(err): Effect<FetchResponse<unknown>, ResponseError | SupportedErrors> => {
|
|
94
|
-
const toError = <R, From, To>(s: Schema<To, From, R>) =>
|
|
95
|
-
Effect
|
|
96
|
-
.flatMap(
|
|
97
|
-
err
|
|
98
|
-
.response
|
|
99
|
-
.json,
|
|
100
|
-
(_) => S.decodeUnknown(s)(_).pipe(Effect.catchAll(() => Effect.fail(err)))
|
|
101
|
-
)
|
|
102
|
-
.pipe(Effect.flatMap(Effect.fail))
|
|
103
|
-
|
|
104
|
-
// opposite of api's `defaultErrorHandler`
|
|
105
|
-
if (err.response.status === 404) {
|
|
106
|
-
return toError(NotFoundError)
|
|
107
|
-
}
|
|
108
|
-
if (err.response.status === 400) {
|
|
109
|
-
return toError(ValidationError)
|
|
110
|
-
}
|
|
111
|
-
if (err.response.status === 401) {
|
|
112
|
-
return toError(NotLoggedInError)
|
|
113
|
-
}
|
|
114
|
-
// TODO: DomainError
|
|
115
|
-
if (err.response.status === 422) {
|
|
116
|
-
return responseError
|
|
117
|
-
? toError(S.Union(responseError, InvalidStateError) as any)
|
|
118
|
-
: toError(InvalidStateError)
|
|
119
|
-
}
|
|
120
|
-
if (err.response.status === 503) {
|
|
121
|
-
return toError(ServiceUnavailableError)
|
|
122
|
-
}
|
|
123
|
-
if (err.response.status === 403) {
|
|
124
|
-
return toError(UnauthorizedError)
|
|
125
|
-
}
|
|
126
|
-
if (err.response.status === 412) {
|
|
127
|
-
return toError(OptimisticConcurrencyException)
|
|
128
|
-
}
|
|
129
|
-
return Effect.fail(err)
|
|
130
|
-
}
|
|
131
|
-
),
|
|
132
|
-
Effect.catchTags({
|
|
133
|
-
"ResponseError": (err) =>
|
|
134
|
-
Effect
|
|
135
|
-
.orDie(
|
|
136
|
-
err
|
|
137
|
-
.response
|
|
138
|
-
.text
|
|
139
|
-
// TODO
|
|
140
|
-
)
|
|
141
|
-
.pipe(Effect
|
|
142
|
-
.flatMap((_) =>
|
|
143
|
-
Effect.fail({
|
|
144
|
-
_tag: "HttpErrorResponse" as const,
|
|
145
|
-
response: {
|
|
146
|
-
body: Option.fromNullable(_),
|
|
147
|
-
status: err.response.status,
|
|
148
|
-
headers: err.response.headers
|
|
149
|
-
}
|
|
150
|
-
} as HttpResponseError<unknown>)
|
|
151
|
-
)),
|
|
152
|
-
"RequestError": (err) => Effect.fail({ _tag: "HttpErrorRequest", error: err.cause } as HttpRequestError)
|
|
153
|
-
}),
|
|
154
|
-
Effect.scoped
|
|
155
|
-
))
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
export function fetchApi2S<
|
|
159
|
-
RequestR,
|
|
160
|
-
RequestFrom,
|
|
161
|
-
RequestTo,
|
|
162
|
-
ResponseR,
|
|
163
|
-
ResponseFrom,
|
|
164
|
-
ResponseTo,
|
|
165
|
-
ResponseErrorFrom = never,
|
|
166
|
-
ResponseErrorTo extends { _tag: string } = never
|
|
167
|
-
>(
|
|
168
|
-
request: Schema<RequestTo, RequestFrom, RequestR>,
|
|
169
|
-
response: Schema<ResponseTo, ResponseFrom, ResponseR>,
|
|
170
|
-
responseError?: Schema<ResponseErrorTo, ResponseErrorFrom, never>
|
|
171
|
-
) {
|
|
172
|
-
const encodeRequest = S.encode(request)
|
|
173
|
-
const decRes = S.decodeUnknown(response)
|
|
174
|
-
const decodeRes = (u: unknown) => Effect.mapError(decRes(u), (err) => new ResError(err))
|
|
175
|
-
const parse = mapResponseM(decodeRes)
|
|
176
|
-
return (method: Method, path: Path) => (req: RequestTo) => {
|
|
177
|
-
return Effect.andThen(encodeRequest(req), (encoded) =>
|
|
178
|
-
fetchApi(
|
|
179
|
-
method,
|
|
180
|
-
method === "DELETE"
|
|
181
|
-
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
|
|
182
|
-
? makePathWithQuery(path, encoded as any)
|
|
183
|
-
: makePathWithBody(path, encoded as any),
|
|
184
|
-
encoded,
|
|
185
|
-
responseError
|
|
186
|
-
)
|
|
187
|
-
.pipe(Effect.flatMap(parse)))
|
|
188
|
-
}
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
export function fetchApi3S<RequestA, RequestE, ResponseE = unknown, ResponseA = void>({
|
|
192
|
-
Request,
|
|
193
|
-
Response
|
|
194
|
-
}: {
|
|
195
|
-
// eslint-disable-next-line @typescript-eslint/ban-types
|
|
196
|
-
Request: REST.RequestSchemed<RequestA, RequestE>
|
|
197
|
-
// eslint-disable-next-line @typescript-eslint/ban-types
|
|
198
|
-
Response: REST.ReqRes<ResponseA, ResponseE, any>
|
|
199
|
-
}) {
|
|
200
|
-
return fetchApi2S(Request, Response, Request.errors)(
|
|
201
|
-
Request.method,
|
|
202
|
-
new Path(Request.path)
|
|
203
|
-
)
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
export function fetchApi3SE<RequestA, RequestE, ResponseE = unknown, ResponseA = void>({
|
|
207
|
-
Request,
|
|
208
|
-
Response
|
|
209
|
-
}: {
|
|
210
|
-
// eslint-disable-next-line @typescript-eslint/ban-types
|
|
211
|
-
Request: REST.RequestSchemed<RequestA, RequestE>
|
|
212
|
-
// eslint-disable-next-line @typescript-eslint/ban-types
|
|
213
|
-
Response: REST.ReqRes<ResponseA, ResponseE, any>
|
|
214
|
-
}) {
|
|
215
|
-
const a = fetchApi2S(Request, Response)(
|
|
216
|
-
Request.method,
|
|
217
|
-
new Path(Request.path)
|
|
218
|
-
)
|
|
219
|
-
const parse = mapResponseM(S.encode(Response))
|
|
220
|
-
return (req: RequestA) => Effect.flatMap(a(req), parse)
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
export function makePathWithQuery(
|
|
224
|
-
path: Path,
|
|
225
|
-
pars: Record<
|
|
226
|
-
string,
|
|
227
|
-
| string
|
|
228
|
-
| number
|
|
229
|
-
| boolean
|
|
230
|
-
| readonly string[]
|
|
231
|
-
| readonly number[]
|
|
232
|
-
| readonly boolean[]
|
|
233
|
-
| null
|
|
234
|
-
>
|
|
235
|
-
) {
|
|
236
|
-
const forQs = Record.filter(pars, (_, k) => !path.params.includes(k))
|
|
237
|
-
const q = forQs // { ...forQs, _: JSON.stringify(forQs) } // TODO: drop completely individual keys from query?, sticking to json only
|
|
238
|
-
return (
|
|
239
|
-
path.build(pars, { ignoreSearch: true, ignoreConstraints: true })
|
|
240
|
-
+ (Object.keys(q).length
|
|
241
|
-
? "?" + qs.stringify(q)
|
|
242
|
-
: "")
|
|
243
|
-
)
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
export function makePathWithBody(
|
|
247
|
-
path: Path,
|
|
248
|
-
pars: Record<
|
|
249
|
-
string,
|
|
250
|
-
| string
|
|
251
|
-
| number
|
|
252
|
-
| boolean
|
|
253
|
-
| readonly string[]
|
|
254
|
-
| readonly number[]
|
|
255
|
-
| readonly boolean[]
|
|
256
|
-
| null
|
|
257
|
-
>
|
|
258
|
-
) {
|
|
259
|
-
return path.build(pars, { ignoreSearch: true, ignoreConstraints: true })
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
export function mapResponse<T, A>(map: (t: T) => A) {
|
|
263
|
-
return (r: FetchResponse<T>): FetchResponse<A> => {
|
|
264
|
-
return { ...r, body: map(r.body) }
|
|
265
|
-
}
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
export function mapResponseM<T, R, E, A>(map: (t: T) => Effect<A, E, R>) {
|
|
269
|
-
return (r: FetchResponse<T>): Effect<FetchResponse<A>, E, R> => {
|
|
270
|
-
return Effect
|
|
271
|
-
.all({
|
|
272
|
-
body: map(r.body),
|
|
273
|
-
headers: Effect.sync(() => r.headers),
|
|
274
|
-
status: Effect.sync(() => r.status)
|
|
275
|
-
})
|
|
276
|
-
.pipe(Effect.withSpan("client.decode", { captureStackTrace: false }))
|
|
277
|
-
}
|
|
278
|
-
}
|
|
279
|
-
export type FetchResponse<T> = { body: T; headers: Headers; status: number }
|
|
280
|
-
|
|
281
|
-
export const EmptyResponse = Object.freeze({ body: null, headers: {}, status: 404 })
|
|
282
|
-
export const EmptyResponseM = Effect.sync(() => EmptyResponse)
|
|
283
|
-
const EmptyResponseMThunk_ = constant(EmptyResponseM)
|
|
284
|
-
export function EmptyResponseMThunk<T>(): Effect<
|
|
285
|
-
Readonly<{
|
|
286
|
-
body: null | T
|
|
287
|
-
// eslint-disable-next-line @typescript-eslint/ban-types
|
|
288
|
-
headers: {}
|
|
289
|
-
status: 404
|
|
290
|
-
}>,
|
|
291
|
-
never,
|
|
292
|
-
unknown
|
|
293
|
-
> {
|
|
294
|
-
return EmptyResponseMThunk_()
|
|
295
|
-
}
|
|
296
|
-
|
|
297
|
-
export function getBody<R, E, A>(eff: Effect<FetchResponse<A | null>, E, R>) {
|
|
298
|
-
return Effect.flatMap(eff, (r) => r.body === null ? Effect.die("Not found") : Effect.sync(() => r.body))
|
|
299
|
-
}
|