@effect-app/infra 2.1.1 → 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,510 +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
- RPCRouteR<
315
- { [K in keyof Filter<Rsc>]: Rpc.Rpc<Rsc[K], _R<ReturnType<THandlers[K]["handler"]>>> }[keyof Filter<Rsc>]
316
- >,
317
- { [k in keyof TLayers]: Layer.Layer.Success<TLayers[k]> }[number]
318
- >
319
- > = (class Router extends HttpRouter.Tag(`${meta.moduleName}Router`)<Router>() {}) as any
320
-
321
- const layer = r.use((router) =>
322
- Effect.gen(function*() {
323
- const controllers = yield* make(items)
324
- // return make.pipe(Effect.map((c) => controllers(c, layers)))
325
- const mapped = typedKeysOf(filtered).reduce((acc, cur) => {
326
- const handler = controllers[cur as keyof typeof controllers]
327
- const req = rsc[cur]
328
-
329
- acc[cur] = rpc.effect(
330
- handler._tag === "raw"
331
- ? class extends (req as any) {
332
- static success = S.encodedSchema(req.success)
333
- get [Schema.symbolSerializable]() {
334
- return this.constructor
335
- }
336
- get [Schema.symbolWithResult]() {
337
- return {
338
- failure: req.failure,
339
- success: S.encodedSchema(req.success)
340
- }
341
- }
342
- } as any
343
- : req,
344
- (req) =>
345
- Effect
346
- .annotateCurrentSpan(
347
- "requestInput",
348
- Object.entries(req).reduce((prev, [key, value]: [string, unknown]) => {
349
- prev[key] = key === "password"
350
- ? "<redacted>"
351
- : typeof value === "string" || typeof value === "number" || typeof value === "boolean"
352
- ? typeof value === "string" && value.length > 256
353
- ? (value.substring(0, 253) + "...")
354
- : value
355
- : Array.isArray(value)
356
- ? `Array[${value.length}]`
357
- : value === null || value === undefined
358
- ? `${value}`
359
- : typeof value === "object" && value
360
- ? `Object[${Object.keys(value).length}]`
361
- : typeof value
362
- return prev
363
- }, {} as Record<string, string | number | boolean>)
364
- )
365
- .pipe(
366
- // can't use andThen due to some being a function and effect
367
- Effect.zipRight(handler.handler(req as any) as any),
368
- Effect.tapErrorCause((cause) => Cause.isFailure(cause) ? logRequestError(cause) : Effect.void),
369
- Effect.tapDefect((cause) =>
370
- Effect
371
- .all([
372
- reportRequestError(cause, {
373
- action: `${meta.moduleName}.${req._tag}`
374
- }),
375
- Rpc.currentHeaders.pipe(Effect.andThen((headers) => {
376
- return InfraLogger
377
- .logError("Finished request", cause)
378
- .pipe(Effect.annotateLogs({
379
- action: `${meta.moduleName}.${req._tag}`,
380
- req: pretty(req),
381
- headers: pretty(headers)
382
- // resHeaders: pretty(
383
- // Object
384
- // .entries(headers)
385
- // .reduce((prev, [key, value]) => {
386
- // prev[key] = value && typeof value === "string" ? snipString(value) : value
387
- // return prev
388
- // }, {} as Record<string, any>)
389
- // )
390
- }))
391
- }))
392
- ])
393
- ),
394
- devMode ? (_) => _ : Effect.catchAllDefect(() => Effect.die("Internal Server Error")),
395
- Effect.withSpan("Request." + meta.moduleName + "." + req._tag, {
396
- captureStackTrace: () => handler.stack
397
- })
398
- ),
399
- meta.moduleName
400
- ) // TODO
401
- return acc
402
- }, {} as any) as {
403
- [K in Keys]: Rpc.Rpc<
404
- Rsc[K],
405
- _R<ReturnType<THandlers[K]["handler"]>>
406
- >
407
- }
408
-
409
- const rpcRouter = RpcRouter.make(...Object.values(mapped) as any) as RpcRouter.RpcRouter<
410
- RPCRouteReq<typeof mapped[keyof typeof mapped]>,
411
- RPCRouteR<typeof mapped[keyof typeof mapped]>
412
- >
413
- const httpApp = toHttpApp(rpcRouter, {
414
- spanPrefix: rsc
415
- .meta
416
- .moduleName + "."
417
- })
418
- const services = (yield* Effect.context<never>()).pipe(
419
- Context.omit(Scope.Scope as never),
420
- Context.omit(Tracer.ParentSpan as never)
421
- )
422
- yield* router
423
- .all(
424
- "/",
425
- (httpApp
426
- .pipe(HttpMiddleware.make(Effect.provide(services)))) as any,
427
- // TODO: not queries.
428
- { uninterruptible: true }
429
- )
430
- })
431
- )
432
-
433
- // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion
434
- const routes = layer.pipe(
435
- Layer.provideMerge(r.Live),
436
- layers ? Layer.provide(layers) as any : (_) => _
437
- ) as Layer.Layer<
438
- Router,
439
- { [k in keyof TLayers]: Layer.Layer.Error<TLayers[k]> }[number] | E,
440
- | { [k in keyof TLayers]: Layer.Layer.Context<TLayers[k]> }[number]
441
- | Exclude<R, { [k in keyof TLayers]: Layer.Layer.Success<TLayers[k]> }[number]>
442
- >
443
-
444
- // Effect.Effect<HttpRouter.HttpRouter<unknown, HttpRouter.HttpRouter.DefaultServices>, never, UserRouter>
445
-
446
- return {
447
- moduleName: meta.moduleName,
448
- Router: r,
449
- routes
450
- }
451
- }
452
-
453
- return effect
454
- }
455
-
456
- type HR<T> = T extends HttpRouter.HttpRouter<any, infer R> ? R : never
457
- type HE<T> = T extends HttpRouter.HttpRouter<infer E, any> ? E : never
458
-
459
- type RequestHandlersTest = {
460
- [key: string]: {
461
- Router: { router: Effect<HttpRouter.HttpRouter<any, any>, any, any> }
462
- routes: Layer.Layer<any, any, any>
463
- moduleName: string
464
- }
465
- }
466
- function matchAll<T extends RequestHandlersTest, A, E, R>(
467
- handlers: T,
468
- requestLayer: Layer.Layer<A, E, R>
469
- ) {
470
- const routers = typedValuesOf(handlers)
471
-
472
- const rootRouter = class extends HttpRouter.Tag("RootRouter")<
473
- "RootRouter",
474
- HR<Effect.Success<typeof handlers[keyof typeof handlers]["Router"]["router"]>>,
475
- HE<Effect.Success<typeof handlers[keyof typeof handlers]["Router"]["router"]>>
476
- >() {}
477
-
478
- const r = rootRouter
479
- .use((router) =>
480
- Effect.gen(function*() {
481
- for (const route of routers) {
482
- yield* router.mount(
483
- ("/rpc/" + route.moduleName) as any,
484
- yield* route
485
- .Router
486
- .router
487
- .pipe(Effect.map(HttpRouter.use(flow(Effect.provide(requestLayer))))) as any
488
- )
489
- }
490
- })
491
- )
492
- .pipe(Layer.provide(routers.map((r) => r.routes).flat() as unknown as NonEmptyArray<Layer.Layer.Any>))
493
-
494
- return {
495
- layer: r as Layer.Layer<
496
- never,
497
- Layer.Layer.Error<typeof handlers[keyof typeof handlers]["routes"]>,
498
- Layer.Layer.Context<typeof handlers[keyof typeof handlers]["routes"]>
499
- >,
500
- Router: rootRouter as any as HttpRouter.HttpRouter.TagClass<
501
- "RootRouter",
502
- "RootRouter",
503
- HE<Effect.Success<typeof handlers[keyof typeof handlers]["Router"]["router"]>>,
504
- R | Exclude<HR<Effect.Success<typeof handlers[keyof typeof handlers]["Router"]["router"]>>, A>
505
- >
506
- }
507
- }
508
-
509
- return { matchAll, matchFor }
510
- }