@effect-app/infra 2.1.2 → 2.2.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,511 +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 interface ExtendedMiddleware<Context, CTXMap extends Record<string, RPCContextMap.Any>>
147
- extends Middleware<Context, CTXMap>
148
- {
149
- // TODO
150
- makeContext: Effect<
151
- { [K in keyof CTXMap as CTXMap[K][1] extends never ? never : CTXMap[K][0]]: CTXMap[K][1] },
152
- never,
153
- never
154
- >
155
- }
156
-
157
- export const RouterSymbol = Symbol()
158
- export interface RouterShape<Rsc> {
159
- [RouterSymbol]: Rsc
160
- }
161
-
162
- type RPCRouteR<T extends Rpc.Rpc<any, any>> = [T] extends [
163
- Rpc.Rpc<any, infer R>
164
- ] ? R
165
- : never
166
-
167
- type RPCRouteReq<T extends Rpc.Rpc<any, any>> = [T] extends [
168
- Rpc.Rpc<infer Req, any>
169
- ] ? Req
170
- : never
171
-
172
- type Match<
173
- Rsc extends Record<string, any>,
174
- CTXMap extends Record<string, any>,
175
- RT extends "raw" | "d",
176
- Key extends keyof Rsc
177
- > = {
178
- // TODO: deal with HandleVoid and ability to extends from GetSuccessShape...
179
- // aka we want to make sure that the return type is void if the success is void,
180
- // and make sure A is the actual expected type
181
-
182
- // note: the defaults of = never prevent the whole router to error
183
- <A extends GetSuccessShape<Rsc[Key], RT>, R2 = never, E = never>(
184
- f: Effect<A, E, R2>
185
- ): HandleVoid<
186
- GetSuccessShape<Rsc[Key], RT>,
187
- A,
188
- Handler<
189
- Rsc[Key],
190
- RT,
191
- Exclude<R2, GetEffectContext<CTXMap, Rsc[Key]["config"]>>
192
- >
193
- >
194
-
195
- <A extends GetSuccessShape<Rsc[Key], RT>, R2 = never, E = never>(
196
- f: (req: S.Schema.Type<Rsc[Key]>) => Effect<A, E, R2>
197
- ): HandleVoid<
198
- GetSuccessShape<Rsc[Key], RT>,
199
- A,
200
- Handler<
201
- Rsc[Key],
202
- RT,
203
- Exclude<R2, GetEffectContext<CTXMap, Rsc[Key]["config"]>>
204
- >
205
- >
206
- }
207
-
208
- export type RouteMatcher<
209
- CTXMap extends Record<string, any>,
210
- Rsc extends Record<string, any>
211
- > = {
212
- // use Rsc as Key over using Keys, so that the Go To on X.Action remain in tact in Controllers files
213
- /**
214
- * Requires the Type shape
215
- */
216
- [Key in keyof Filter<Rsc>]: Match<Rsc, CTXMap, "d", Key> & {
217
- success: Rsc[Key]["success"]
218
- successRaw: S.SchemaClass<S.Schema.Encoded<Rsc[Key]["success"]>>
219
- failure: Rsc[Key]["failure"]
220
- /**
221
- * Requires the Encoded shape (e.g directly undecoded from DB, so that we don't do multiple Decode/Encode)
222
- */
223
- raw: Match<Rsc, CTXMap, "raw", Key>
224
- }
225
- }
226
- // export interface RouteMatcher<
227
- // Filtered extends Record<string, any>,
228
- // CTXMap extends Record<string, any>,
229
- // Rsc extends Filtered
230
- // > extends RouteMatcherInt<Filtered, CTXMap, Rsc> {}
231
-
232
- export const makeRouter = <Context, CTXMap extends Record<string, RPCContextMap.Any>>(
233
- middleware: ExtendedMiddleware<Context, CTXMap>,
234
- devMode: boolean
235
- ) => {
236
- const rpc = makeRpc(middleware)
237
- function matchFor<
238
- const ModuleName extends string,
239
- const Rsc extends Record<string, any>
240
- >(
241
- rsc: Rsc & { meta: { moduleName: ModuleName } }
242
- ) {
243
- const meta = rsc.meta
244
- type Filtered = Filter<Rsc>
245
- const filtered = typedKeysOf(rsc).reduce((acc, cur) => {
246
- if (Predicate.isObject(rsc[cur]) && rsc[cur]["success"]) {
247
- acc[cur as keyof Filtered] = rsc[cur]
248
- }
249
- return acc
250
- }, {} as Filtered)
251
-
252
- const items = typedKeysOf(filtered).reduce(
253
- (prev, cur) => {
254
- ;(prev as any)[cur] = Object.assign((fnOrEffect: any) => {
255
- const stack = new Error().stack?.split("\n").slice(2).join("\n")
256
- return Effect.isEffect(fnOrEffect)
257
- ? class {
258
- static stack = stack
259
- static _tag = "d"
260
- static handler = () => fnOrEffect
261
- }
262
- : class {
263
- static stack = stack
264
- static _tag = "d"
265
- static handler = fnOrEffect
266
- }
267
- }, {
268
- success: rsc[cur].success,
269
- successRaw: S.encodedSchema(rsc[cur].success),
270
- failure: rsc[cur].failure,
271
- raw: // "Raw" variations are for when you don't want to decode just to encode it again on the response
272
- // e.g for direct projection from DB
273
- // but more importantly, to skip Effectful decoders, like to resolve relationships from the database or remote client.
274
- (fnOrEffect: any) => {
275
- const stack = new Error().stack?.split("\n").slice(2).join("\n")
276
- return Effect.isEffect(fnOrEffect)
277
- ? class {
278
- static stack = stack
279
- static _tag = "raw"
280
- static handler = () => fnOrEffect
281
- }
282
- : class {
283
- static stack = stack
284
- static _tag = "raw"
285
- static handler = (req: any, ctx: any) => fnOrEffect(req, { ...ctx, Response: rsc[cur].success })
286
- }
287
- }
288
- })
289
- return prev
290
- },
291
- {} as RouteMatcher<CTXMap, Rsc>
292
- )
293
-
294
- type Keys = keyof Filtered
295
-
296
- const effect = <
297
- E,
298
- R,
299
- THandlers extends {
300
- // import to keep them separate via | for type checking!!
301
- [K in Keys]: AHandler<Rsc[K]>
302
- },
303
- TLayers extends NonEmptyArray<Layer.Layer.Any>
304
- >(
305
- layers: TLayers,
306
- make: (requests: typeof items) => Effect<THandlers, E, R>
307
- ) => {
308
- type Router = RouterShape<Rsc>
309
- const r: HttpRouter.HttpRouter.TagClass<
310
- Router,
311
- `${typeof meta.moduleName}Router`,
312
- never,
313
- Exclude<
314
- | Context
315
- | RPCRouteR<
316
- { [K in keyof Filter<Rsc>]: Rpc.Rpc<Rsc[K], _R<ReturnType<THandlers[K]["handler"]>>> }[keyof Filter<Rsc>]
317
- >,
318
- { [k in keyof TLayers]: Layer.Layer.Success<TLayers[k]> }[number]
319
- >
320
- > = (class Router extends HttpRouter.Tag(`${meta.moduleName}Router`)<Router>() {}) as any
321
-
322
- const layer = r.use((router) =>
323
- Effect.gen(function*() {
324
- const controllers = yield* make(items)
325
- // return make.pipe(Effect.map((c) => controllers(c, layers)))
326
- const mapped = typedKeysOf(filtered).reduce((acc, cur) => {
327
- const handler = controllers[cur as keyof typeof controllers]
328
- const req = rsc[cur]
329
-
330
- acc[cur] = rpc.effect(
331
- handler._tag === "raw"
332
- ? class extends (req as any) {
333
- static success = S.encodedSchema(req.success)
334
- get [Schema.symbolSerializable]() {
335
- return this.constructor
336
- }
337
- get [Schema.symbolWithResult]() {
338
- return {
339
- failure: req.failure,
340
- success: S.encodedSchema(req.success)
341
- }
342
- }
343
- } as any
344
- : req,
345
- (req) =>
346
- Effect
347
- .annotateCurrentSpan(
348
- "requestInput",
349
- Object.entries(req).reduce((prev, [key, value]: [string, unknown]) => {
350
- prev[key] = key === "password"
351
- ? "<redacted>"
352
- : typeof value === "string" || typeof value === "number" || typeof value === "boolean"
353
- ? typeof value === "string" && value.length > 256
354
- ? (value.substring(0, 253) + "...")
355
- : value
356
- : Array.isArray(value)
357
- ? `Array[${value.length}]`
358
- : value === null || value === undefined
359
- ? `${value}`
360
- : typeof value === "object" && value
361
- ? `Object[${Object.keys(value).length}]`
362
- : typeof value
363
- return prev
364
- }, {} as Record<string, string | number | boolean>)
365
- )
366
- .pipe(
367
- // can't use andThen due to some being a function and effect
368
- Effect.zipRight(handler.handler(req as any) as any),
369
- Effect.tapErrorCause((cause) => Cause.isFailure(cause) ? logRequestError(cause) : Effect.void),
370
- Effect.tapDefect((cause) =>
371
- Effect
372
- .all([
373
- reportRequestError(cause, {
374
- action: `${meta.moduleName}.${req._tag}`
375
- }),
376
- Rpc.currentHeaders.pipe(Effect.andThen((headers) => {
377
- return InfraLogger
378
- .logError("Finished request", cause)
379
- .pipe(Effect.annotateLogs({
380
- action: `${meta.moduleName}.${req._tag}`,
381
- req: pretty(req),
382
- headers: pretty(headers)
383
- // resHeaders: pretty(
384
- // Object
385
- // .entries(headers)
386
- // .reduce((prev, [key, value]) => {
387
- // prev[key] = value && typeof value === "string" ? snipString(value) : value
388
- // return prev
389
- // }, {} as Record<string, any>)
390
- // )
391
- }))
392
- }))
393
- ])
394
- ),
395
- devMode ? (_) => _ : Effect.catchAllDefect(() => Effect.die("Internal Server Error")),
396
- Effect.withSpan("Request." + meta.moduleName + "." + req._tag, {
397
- captureStackTrace: () => handler.stack
398
- })
399
- ),
400
- meta.moduleName
401
- ) // TODO
402
- return acc
403
- }, {} as any) as {
404
- [K in Keys]: Rpc.Rpc<
405
- Rsc[K],
406
- Context | _R<ReturnType<THandlers[K]["handler"]>>
407
- >
408
- }
409
-
410
- const rpcRouter = RpcRouter.make(...Object.values(mapped) as any) as RpcRouter.RpcRouter<
411
- RPCRouteReq<typeof mapped[keyof typeof mapped]>,
412
- RPCRouteR<typeof mapped[keyof typeof mapped]>
413
- >
414
- const httpApp = toHttpApp(rpcRouter, {
415
- spanPrefix: rsc
416
- .meta
417
- .moduleName + "."
418
- })
419
- const services = (yield* Effect.context<never>()).pipe(
420
- Context.omit(Scope.Scope as never),
421
- Context.omit(Tracer.ParentSpan as never)
422
- )
423
- yield* router
424
- .all(
425
- "/",
426
- (httpApp
427
- .pipe(HttpMiddleware.make(Effect.provide(services)))) as any,
428
- // TODO: not queries.
429
- { uninterruptible: true }
430
- )
431
- })
432
- )
433
-
434
- // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion
435
- const routes = layer.pipe(
436
- Layer.provideMerge(r.Live),
437
- layers ? Layer.provide(layers) as any : (_) => _
438
- ) as Layer.Layer<
439
- Router,
440
- { [k in keyof TLayers]: Layer.Layer.Error<TLayers[k]> }[number] | E,
441
- | { [k in keyof TLayers]: Layer.Layer.Context<TLayers[k]> }[number]
442
- | Exclude<R, { [k in keyof TLayers]: Layer.Layer.Success<TLayers[k]> }[number]>
443
- >
444
-
445
- // Effect.Effect<HttpRouter.HttpRouter<unknown, HttpRouter.HttpRouter.DefaultServices>, never, UserRouter>
446
-
447
- return {
448
- moduleName: meta.moduleName,
449
- Router: r,
450
- routes
451
- }
452
- }
453
-
454
- return effect
455
- }
456
-
457
- type HR<T> = T extends HttpRouter.HttpRouter<any, infer R> ? R : never
458
- type HE<T> = T extends HttpRouter.HttpRouter<infer E, any> ? E : never
459
-
460
- type RequestHandlersTest = {
461
- [key: string]: {
462
- Router: { router: Effect<HttpRouter.HttpRouter<any, any>, any, any> }
463
- routes: Layer.Layer<any, any, any>
464
- moduleName: string
465
- }
466
- }
467
- function matchAll<T extends RequestHandlersTest, A, E, R>(
468
- handlers: T,
469
- requestLayer: Layer.Layer<A, E, R>
470
- ) {
471
- const routers = typedValuesOf(handlers)
472
-
473
- const rootRouter = class extends HttpRouter.Tag("RootRouter")<
474
- "RootRouter",
475
- HR<Effect.Success<typeof handlers[keyof typeof handlers]["Router"]["router"]>>,
476
- HE<Effect.Success<typeof handlers[keyof typeof handlers]["Router"]["router"]>>
477
- >() {}
478
-
479
- const r = rootRouter
480
- .use((router) =>
481
- Effect.gen(function*() {
482
- for (const route of routers) {
483
- yield* router.mount(
484
- ("/rpc/" + route.moduleName) as any,
485
- yield* route
486
- .Router
487
- .router
488
- .pipe(Effect.map(HttpRouter.use(flow(Effect.provide(requestLayer))))) as any
489
- )
490
- }
491
- })
492
- )
493
- .pipe(Layer.provide(routers.map((r) => r.routes).flat() as unknown as NonEmptyArray<Layer.Layer.Any>))
494
-
495
- return {
496
- layer: r as Layer.Layer<
497
- never,
498
- Layer.Layer.Error<typeof handlers[keyof typeof handlers]["routes"]>,
499
- Layer.Layer.Context<typeof handlers[keyof typeof handlers]["routes"]>
500
- >,
501
- Router: rootRouter as any as HttpRouter.HttpRouter.TagClass<
502
- "RootRouter",
503
- "RootRouter",
504
- HE<Effect.Success<typeof handlers[keyof typeof handlers]["Router"]["router"]>>,
505
- R | Exclude<HR<Effect.Success<typeof handlers[keyof typeof handlers]["Router"]["router"]>>, A>
506
- >
507
- }
508
- }
509
-
510
- return { matchAll, matchFor }
511
- }