@effect-app/infra 2.35.3 → 2.36.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.
@@ -1,530 +0,0 @@
1
- /* eslint-disable @typescript-eslint/no-unsafe-argument */
2
- /* eslint-disable @typescript-eslint/no-empty-object-type */
3
- /* eslint-disable @typescript-eslint/no-explicit-any */
4
- /*
5
- TODO: Effect.retry(r2, optimisticConcurrencySchedule) / was for PATCH only
6
- TODO: uninteruptible commands! was for All except GET.
7
- */
8
- import type * as HttpApp from "@effect/platform/HttpApp"
9
- import { Rpc, RpcRouter } from "@effect/rpc"
10
- import type { NonEmptyArray } from "effect-app"
11
- import {
12
- Cause,
13
- Chunk,
14
- Context,
15
- Effect,
16
- FiberRef,
17
- flow,
18
- Layer,
19
- Predicate,
20
- S,
21
- Schema,
22
- Scope,
23
- Stream,
24
- Tracer
25
- } from "effect-app"
26
- import type { GetEffectContext, RPCContextMap } from "effect-app/client/req"
27
- import type { HttpServerError } from "effect-app/http"
28
- import { HttpMiddleware, HttpRouter, HttpServerRequest, HttpServerResponse } from "effect-app/http"
29
- import { pretty, typedKeysOf, typedValuesOf } from "effect-app/utils"
30
- import { logError, reportError } from "../errorReporter.js"
31
- import { InfraLogger } from "../logger.js"
32
- import type { Middleware } from "./routing/DynamicMiddleware.js"
33
- import { makeRpc } from "./routing/DynamicMiddleware.js"
34
-
35
- const logRequestError = logError("Request")
36
- const reportRequestError = reportError("Request")
37
-
38
- export type _R<T extends Effect<any, any, any>> = [T] extends [
39
- Effect<any, any, infer R>
40
- ] ? R
41
- : never
42
-
43
- export type _E<T extends Effect<any, any, any>> = [T] extends [
44
- Effect<any, infer E, any>
45
- ] ? E
46
- : never
47
-
48
- export type EffectDeps<A> = {
49
- [K in keyof A as A[K] extends Effect<any, any, any> ? K : never]: A[K] extends Effect<any, any, any> ? A[K] : never
50
- }
51
- /**
52
- * Plain jane JSON version
53
- * @deprecated use HttpRpcRouterNoStream.toHttpApp once support options
54
- */
55
- export const toHttpApp = <R extends RpcRouter.RpcRouter<any, any>>(self: R, options?: {
56
- readonly spanPrefix?: string
57
- }): HttpApp.Default<
58
- HttpServerError.RequestError,
59
- RpcRouter.RpcRouter.Context<R>
60
- > => {
61
- const handler = RpcRouter.toHandler(self, options)
62
- return Effect.withFiberRuntime((fiber) => {
63
- const context = fiber.getFiberRef(FiberRef.currentContext)
64
- const request = Context.unsafeGet(context, HttpServerRequest.HttpServerRequest)
65
- return Effect.flatMap(
66
- request.json,
67
- (_) =>
68
- handler(_).pipe(
69
- Stream.provideContext(context),
70
- Stream.runCollect,
71
- Effect.map((_) => Chunk.toReadonlyArray(_)),
72
- Effect.andThen((_) => {
73
- let status = 200
74
- for (const r of _.flat()) {
75
- if (typeof r === "number") continue
76
- const results = Array.isArray(r) ? r : [r]
77
- if (results.some((_: S.ExitEncoded<any, any, any>) => _._tag === "Failure" && _.cause._tag === "Die")) {
78
- status = 500
79
- break
80
- }
81
- if (results.some((_: S.ExitEncoded<any, any, any>) => _._tag === "Failure" && _.cause._tag === "Fail")) {
82
- status = 422 // 418
83
- break
84
- }
85
- }
86
- return HttpServerResponse.json(_, { status })
87
- }),
88
- Effect.orDie,
89
- Effect.tapDefect(reportError("RPCHttpApp"))
90
- )
91
- )
92
- })
93
- }
94
-
95
- export interface Hint<Err extends string> {
96
- Err: Err
97
- }
98
-
99
- type HandleVoid<Expected, Actual, Result> = [Expected] extends [void]
100
- ? [Actual] extends [void] ? Result : Hint<"You're returning non void for a void Response, please fix">
101
- : Result
102
-
103
- type AnyRequestModule = S.Schema.Any & { success?: S.Schema.Any; failure?: S.Schema.Any }
104
-
105
- type GetSuccess<T> = T extends { success: S.Schema.Any } ? T["success"] : typeof S.Void
106
-
107
- type GetSuccessShape<Action extends { success?: S.Schema.Any }, RT extends "d" | "raw"> = RT extends "raw"
108
- ? S.Schema.Encoded<GetSuccess<Action>>
109
- : S.Schema.Type<GetSuccess<Action>>
110
- type GetFailure<T extends { failure?: S.Schema.Any }> = T["failure"] extends never ? typeof S.Never : T["failure"]
111
-
112
- type HandlerFull<Action extends AnyRequestModule, RT extends "raw" | "d", A, E, R> = {
113
- new(): {}
114
- _tag: RT
115
- stack: string
116
- handler: (
117
- req: S.Schema.Type<Action>
118
- ) => Effect<
119
- A,
120
- E,
121
- R
122
- >
123
- }
124
-
125
- export interface Handler<Action extends AnyRequestModule, RT extends "raw" | "d", R> extends
126
- HandlerFull<
127
- Action,
128
- RT,
129
- GetSuccessShape<Action, RT>,
130
- S.Schema.Type<GetFailure<Action>> | S.ParseResult.ParseError,
131
- R
132
- >
133
- {
134
- }
135
-
136
- type AHandler<Action extends AnyRequestModule> = Handler<
137
- Action,
138
- any,
139
- any
140
- >
141
-
142
- type Filter<T> = {
143
- [K in keyof T as T[K] extends S.Schema.All & { success: S.Schema.Any; failure: S.Schema.Any } ? K : never]: T[K]
144
- }
145
-
146
- export const RouterSymbol = Symbol()
147
- export interface RouterShape<Rsc> {
148
- [RouterSymbol]: Rsc
149
- }
150
-
151
- type RPCRouteR<T extends Rpc.Rpc<any, any>> = [T] extends [
152
- Rpc.Rpc<any, infer R>
153
- ] ? R
154
- : never
155
-
156
- type RPCRouteReq<T extends Rpc.Rpc<any, any>> = [T] extends [
157
- Rpc.Rpc<infer Req, any>
158
- ] ? Req
159
- : never
160
-
161
- type Match<
162
- Rsc extends Record<string, any>,
163
- CTXMap extends Record<string, any>,
164
- RT extends "raw" | "d",
165
- Key extends keyof Rsc,
166
- Context
167
- > = {
168
- // TODO: deal with HandleVoid and ability to extends from GetSuccessShape...
169
- // aka we want to make sure that the return type is void if the success is void,
170
- // and make sure A is the actual expected type
171
-
172
- // note: the defaults of = never prevent the whole router to error
173
- <A extends GetSuccessShape<Rsc[Key], RT>, R2 = never, E = never>(
174
- f: Effect<A, E, R2>
175
- ): HandleVoid<
176
- GetSuccessShape<Rsc[Key], RT>,
177
- A,
178
- Handler<
179
- Rsc[Key],
180
- RT,
181
- Exclude<
182
- Context | Exclude<R2, GetEffectContext<CTXMap, Rsc[Key]["config"]>>,
183
- HttpRouter.HttpRouter.Provided
184
- >
185
- >
186
- >
187
-
188
- <A extends GetSuccessShape<Rsc[Key], RT>, R2 = never, E = never>(
189
- f: (req: S.Schema.Type<Rsc[Key]>) => Effect<A, E, R2>
190
- ): HandleVoid<
191
- GetSuccessShape<Rsc[Key], RT>,
192
- A,
193
- Handler<
194
- Rsc[Key],
195
- RT,
196
- Exclude<
197
- Context | Exclude<R2, GetEffectContext<CTXMap, Rsc[Key]["config"]>>,
198
- HttpRouter.HttpRouter.Provided
199
- >
200
- >
201
- >
202
- }
203
-
204
- export type RouteMatcher<
205
- CTXMap extends Record<string, any>,
206
- Rsc extends Record<string, any>,
207
- Context
208
- > = {
209
- // use Rsc as Key over using Keys, so that the Go To on X.Action remain in tact in Controllers files
210
- /**
211
- * Requires the Type shape
212
- */
213
- [Key in keyof Filter<Rsc>]: Match<Rsc, CTXMap, "d", Key, Context> & {
214
- success: Rsc[Key]["success"]
215
- successRaw: S.SchemaClass<S.Schema.Encoded<Rsc[Key]["success"]>>
216
- failure: Rsc[Key]["failure"]
217
- /**
218
- * Requires the Encoded shape (e.g directly undecoded from DB, so that we don't do multiple Decode/Encode)
219
- */
220
- raw: Match<Rsc, CTXMap, "raw", Key, Context>
221
- }
222
- }
223
- // export interface RouteMatcher<
224
- // Filtered extends Record<string, any>,
225
- // CTXMap extends Record<string, any>,
226
- // Rsc extends Filtered
227
- // > extends RouteMatcherInt<Filtered, CTXMap, Rsc> {}
228
-
229
- export const makeMiddleware = <
230
- Context,
231
- CTXMap extends Record<string, RPCContextMap.Any>,
232
- RMW,
233
- Layers extends Array<Layer.Layer.Any>
234
- >(content: Middleware<Context, CTXMap, RMW, Layers>): Middleware<Context, CTXMap, RMW, Layers> => content
235
-
236
- export const makeRouter = <
237
- Context,
238
- CTXMap extends Record<string, RPCContextMap.Any>,
239
- RMW,
240
- Layers extends Array<Layer.Layer.Any>
241
- >(
242
- middleware: Middleware<Context, CTXMap, RMW, Layers>,
243
- devMode: boolean
244
- ) => {
245
- function matchFor<
246
- const ModuleName extends string,
247
- const Rsc extends Record<string, any>
248
- >(
249
- rsc: Rsc & { meta: { moduleName: ModuleName } }
250
- ) {
251
- const meta = rsc.meta
252
- type Filtered = Filter<Rsc>
253
- const filtered = typedKeysOf(rsc).reduce((acc, cur) => {
254
- if (Predicate.isObject(rsc[cur]) && rsc[cur]["success"]) {
255
- acc[cur as keyof Filtered] = rsc[cur]
256
- }
257
- return acc
258
- }, {} as Filtered)
259
-
260
- const items = typedKeysOf(filtered).reduce(
261
- (prev, cur) => {
262
- ;(prev as any)[cur] = Object.assign((fnOrEffect: any) => {
263
- const stack = new Error().stack?.split("\n").slice(2).join("\n")
264
- return Effect.isEffect(fnOrEffect)
265
- ? class {
266
- static stack = stack
267
- static _tag = "d"
268
- static handler = () => fnOrEffect
269
- }
270
- : class {
271
- static stack = stack
272
- static _tag = "d"
273
- static handler = fnOrEffect
274
- }
275
- }, {
276
- success: rsc[cur].success,
277
- successRaw: S.encodedSchema(rsc[cur].success),
278
- failure: rsc[cur].failure,
279
- raw: // "Raw" variations are for when you don't want to decode just to encode it again on the response
280
- // e.g for direct projection from DB
281
- // but more importantly, to skip Effectful decoders, like to resolve relationships from the database or remote client.
282
- (fnOrEffect: any) => {
283
- const stack = new Error().stack?.split("\n").slice(2).join("\n")
284
- return Effect.isEffect(fnOrEffect)
285
- ? class {
286
- static stack = stack
287
- static _tag = "raw"
288
- static handler = () => fnOrEffect
289
- }
290
- : class {
291
- static stack = stack
292
- static _tag = "raw"
293
- static handler = (req: any, ctx: any) => fnOrEffect(req, { ...ctx, Response: rsc[cur].success })
294
- }
295
- }
296
- })
297
- return prev
298
- },
299
- {} as RouteMatcher<CTXMap, Rsc, Context>
300
- )
301
-
302
- type Keys = keyof Filtered
303
-
304
- const effect = <
305
- E,
306
- R,
307
- THandlers extends {
308
- // import to keep them separate via | for type checking!!
309
- [K in Keys]: AHandler<Rsc[K]>
310
- },
311
- TLayers extends NonEmptyArray<Layer.Layer.Any>
312
- >(
313
- layers: TLayers,
314
- make: (requests: typeof items) => Effect<THandlers, E, R>
315
- ) => {
316
- type ProvidedLayers =
317
- | { [k in keyof Layers]: Layer.Layer.Success<Layers[k]> }[number]
318
- | { [k in keyof TLayers]: Layer.Layer.Success<TLayers[k]> }[number]
319
- type Router = RouterShape<Rsc>
320
- const r: HttpRouter.HttpRouter.TagClass<
321
- Router,
322
- `${typeof meta.moduleName}Router`,
323
- never,
324
- Exclude<
325
- | Context
326
- | RPCRouteR<
327
- { [K in keyof Filter<Rsc>]: Rpc.Rpc<Rsc[K], _R<ReturnType<THandlers[K]["handler"]>>> }[keyof Filter<Rsc>]
328
- >,
329
- | ProvidedLayers
330
- | HttpRouter.HttpRouter.Provided
331
- >
332
- > = (class Router extends HttpRouter.Tag(`${meta.moduleName}Router`)<Router>() {}) as any
333
-
334
- const layer = r.use((router) =>
335
- Effect.gen(function*() {
336
- const controllers = yield* make(items)
337
- const rpc = yield* makeRpc(middleware)
338
-
339
- // return make.pipe(Effect.map((c) => controllers(c, layers)))
340
- const mapped = typedKeysOf(filtered).reduce((acc, cur) => {
341
- const handler = controllers[cur as keyof typeof controllers]
342
- const req = rsc[cur]
343
-
344
- acc[cur] = rpc.effect(
345
- handler._tag === "raw"
346
- ? class extends (req as any) {
347
- static success = S.encodedSchema(req.success)
348
- get [Schema.symbolSerializable]() {
349
- return this.constructor
350
- }
351
- get [Schema.symbolWithResult]() {
352
- return {
353
- failure: req.failure,
354
- success: S.encodedSchema(req.success)
355
- }
356
- }
357
- } as any
358
- : req,
359
- (req) =>
360
- // TODO: render more data... similar to console?
361
- Effect
362
- .annotateCurrentSpan(
363
- "requestInput",
364
- Object.entries(req).reduce((prev, [key, value]: [string, unknown]) => {
365
- prev[key] = key === "password"
366
- ? "<redacted>"
367
- : typeof value === "string" || typeof value === "number" || typeof value === "boolean"
368
- ? typeof value === "string" && value.length > 256
369
- ? (value.substring(0, 253) + "...")
370
- : value
371
- : Array.isArray(value)
372
- ? `Array[${value.length}]`
373
- : value === null || value === undefined
374
- ? `${value}`
375
- : typeof value === "object" && value
376
- ? `Object[${Object.keys(value).length}]`
377
- : typeof value
378
- return prev
379
- }, {} as Record<string, string | number | boolean>)
380
- )
381
- .pipe(
382
- // can't use andThen due to some being a function and effect
383
- Effect.zipRight(handler.handler(req as any) as any),
384
- Effect.tapErrorCause((cause) => Cause.isFailure(cause) ? logRequestError(cause) : Effect.void),
385
- Effect.tapDefect((cause) =>
386
- Effect
387
- .all([
388
- reportRequestError(cause, {
389
- action: `${meta.moduleName}.${req._tag}`
390
- }),
391
- Rpc.currentHeaders.pipe(Effect.andThen((headers) => {
392
- return InfraLogger
393
- .logError("Finished request", cause)
394
- .pipe(Effect.annotateLogs({
395
- action: `${meta.moduleName}.${req._tag}`,
396
- req: pretty(req),
397
- headers: pretty(headers)
398
- // resHeaders: pretty(
399
- // Object
400
- // .entries(headers)
401
- // .reduce((prev, [key, value]) => {
402
- // prev[key] = value && typeof value === "string" ? snipString(value) : value
403
- // return prev
404
- // }, {} as Record<string, any>)
405
- // )
406
- }))
407
- }))
408
- ])
409
- ),
410
- devMode ? (_) => _ : Effect.catchAllDefect(() => Effect.die("Internal Server Error")),
411
- Effect.withSpan("Request." + meta.moduleName + "." + req._tag, {
412
- captureStackTrace: () => handler.stack
413
- })
414
- ),
415
- meta.moduleName
416
- ) // TODO
417
- return acc
418
- }, {} as any) as {
419
- [K in Keys]: Rpc.Rpc<
420
- Rsc[K],
421
- Context | _R<ReturnType<THandlers[K]["handler"]>>
422
- >
423
- }
424
-
425
- const rpcRouter = RpcRouter.make(...Object.values(mapped) as any) as RpcRouter.RpcRouter<
426
- RPCRouteReq<typeof mapped[keyof typeof mapped]>,
427
- RPCRouteR<typeof mapped[keyof typeof mapped]>
428
- >
429
- const httpApp = toHttpApp(rpcRouter, {
430
- spanPrefix: rsc
431
- .meta
432
- .moduleName + "."
433
- })
434
- const services = (yield* Effect.context<never>()).pipe(
435
- Context.omit(Scope.Scope as never),
436
- Context.omit(Tracer.ParentSpan as never)
437
- )
438
- yield* router
439
- .post(
440
- "/",
441
- (httpApp
442
- .pipe(HttpMiddleware.make(Effect.provide(services)))) as any,
443
- // TODO: not queries.
444
- { uninterruptible: true }
445
- )
446
- })
447
- )
448
-
449
- // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion
450
- const routes = layer.pipe(
451
- Layer.provideMerge(r.Live),
452
- layers ? Layer.provide(layers) as any : (_) => _,
453
- middleware.dependencies ? Layer.provide(middleware.dependencies as any) : (_) => _
454
- ) as Layer.Layer<
455
- Router,
456
- { [k in keyof TLayers]: Layer.Layer.Error<TLayers[k]> }[number] | E,
457
- | { [k in keyof TLayers]: Layer.Layer.Context<TLayers[k]> }[number]
458
- | Exclude<
459
- RMW | R,
460
- ProvidedLayers
461
- >
462
- >
463
-
464
- // Effect.Effect<HttpRouter.HttpRouter<unknown, HttpRouter.HttpRouter.DefaultServices>, never, UserRouter>
465
-
466
- return {
467
- moduleName: meta.moduleName,
468
- Router: r,
469
- routes
470
- }
471
- }
472
-
473
- return effect
474
- }
475
-
476
- type HR<T> = T extends HttpRouter.HttpRouter<any, infer R> ? R : never
477
- type HE<T> = T extends HttpRouter.HttpRouter<infer E, any> ? E : never
478
-
479
- type RequestHandlersTest = {
480
- [key: string]: {
481
- Router: { router: Effect<HttpRouter.HttpRouter<any, any>, any, any> }
482
- routes: Layer.Layer<any, any, any>
483
- moduleName: string
484
- }
485
- }
486
- function matchAll<T extends RequestHandlersTest, A, E, R>(
487
- handlers: T,
488
- requestLayer: Layer.Layer<A, E, R>
489
- ) {
490
- const routers = typedValuesOf(handlers)
491
-
492
- const rootRouter = class extends HttpRouter.Tag("RootRouter")<
493
- "RootRouter",
494
- HR<Effect.Success<typeof handlers[keyof typeof handlers]["Router"]["router"]>>,
495
- HE<Effect.Success<typeof handlers[keyof typeof handlers]["Router"]["router"]>>
496
- >() {}
497
-
498
- const r = rootRouter
499
- .use((router) =>
500
- Effect.gen(function*() {
501
- for (const route of routers) {
502
- yield* router.mount(
503
- ("/rpc/" + route.moduleName) as any,
504
- yield* route
505
- .Router
506
- .router
507
- .pipe(Effect.map(HttpRouter.use(flow(Effect.provide(requestLayer))))) as any
508
- )
509
- }
510
- })
511
- )
512
- .pipe(Layer.provide(routers.map((r) => r.routes).flat() as unknown as NonEmptyArray<Layer.Layer.Any>))
513
-
514
- return {
515
- layer: r as Layer.Layer<
516
- never,
517
- Layer.Layer.Error<typeof handlers[keyof typeof handlers]["routes"]>,
518
- Layer.Layer.Context<typeof handlers[keyof typeof handlers]["routes"]>
519
- >,
520
- Router: rootRouter as any as HttpRouter.HttpRouter.TagClass<
521
- "RootRouter",
522
- "RootRouter",
523
- HE<Effect.Success<typeof handlers[keyof typeof handlers]["Router"]["router"]>>,
524
- R | Exclude<HR<Effect.Success<typeof handlers[keyof typeof handlers]["Router"]["router"]>>, A>
525
- >
526
- }
527
- }
528
-
529
- return { matchAll, matchFor }
530
- }