@effect-app/infra 2.73.3 → 2.74.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 +17 -0
- package/dist/QueueMaker/errors.d.ts +1 -1
- package/dist/QueueMaker/errors.d.ts.map +1 -1
- package/dist/api/routing/DynamicMiddleware.d.ts +22 -9
- package/dist/api/routing/DynamicMiddleware.d.ts.map +1 -1
- package/dist/api/routing/DynamicMiddleware.js +83 -17
- package/dist/api/routing/dynamic-middleware.d.ts +24 -0
- package/dist/api/routing/dynamic-middleware.d.ts.map +1 -0
- package/dist/api/routing/dynamic-middleware.js +39 -0
- package/dist/api/routing/tsort.d.ts +8 -0
- package/dist/api/routing/tsort.d.ts.map +1 -0
- package/dist/api/routing/tsort.js +51 -0
- package/dist/api/routing.d.ts +19 -19
- package/dist/api/routing.d.ts.map +1 -1
- package/dist/api/routing.js +6 -55
- package/package.json +10 -2
- package/src/api/routing/DynamicMiddleware.ts +148 -36
- package/src/api/routing/dynamic-middleware.ts +154 -0
- package/src/api/routing/tsort.ts +56 -0
- package/src/api/routing.ts +12 -64
- package/test/controller.test.ts +115 -49
- package/test/dist/controller.legacy2.test.d.ts.map +1 -0
- package/test/dist/controller.legacy3.test.d.ts.map +1 -0
- package/test/dist/controller.test copy.d.ts +169 -0
- package/test/dist/controller.test copy.d.ts.map +1 -0
- package/test/dist/controller.test copy.js +152 -0
- package/test/dist/controller.test.d.ts.map +1 -1
- package/test/dist/controller6.test.d.ts.map +1 -0
- package/test/dist/controller7.test.d.ts.map +1 -0
- package/test/dist/filterApi.test.d.ts.map +1 -0
|
@@ -1,11 +1,16 @@
|
|
|
1
1
|
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
|
|
2
2
|
/* eslint-disable @typescript-eslint/no-unsafe-return */
|
|
3
3
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
4
|
-
import {
|
|
4
|
+
import { Array, Cause, Context, Effect, Layer, type NonEmptyArray, ParseResult, pipe, type Request, type S, type Scope } from "effect-app"
|
|
5
5
|
import type { GetEffectContext, RPCContextMap } from "effect-app/client/req"
|
|
6
|
-
import { type HttpRouter } from "effect-app/http"
|
|
6
|
+
import { HttpHeaders, type HttpRouter, HttpServerRequest } from "effect-app/http"
|
|
7
|
+
|
|
8
|
+
import { pretty } from "effect-app/utils"
|
|
7
9
|
import type * as EffectRequest from "effect/Request"
|
|
10
|
+
import { logError, reportError } from "../../errorReporter.js"
|
|
11
|
+
import { InfraLogger } from "../../logger.js"
|
|
8
12
|
import { type LayersUtils } from "../routing.js"
|
|
13
|
+
import { type AnyContextWithLayer, implementMiddleware, mergeContexts } from "./dynamic-middleware.js"
|
|
9
14
|
|
|
10
15
|
// utils:
|
|
11
16
|
//
|
|
@@ -30,9 +35,10 @@ export type MakeRPCHandlerFactory<
|
|
|
30
35
|
) => Effect.Effect<
|
|
31
36
|
EffectRequest.Request.Success<Req>,
|
|
32
37
|
EffectRequest.Request.Error<Req>,
|
|
33
|
-
HandlerR
|
|
38
|
+
// dynamic middlewares removes the dynamic context from HandlerR
|
|
39
|
+
Exclude<HandlerR, GetEffectContext<RequestContextMap, (T & S.Schema<Req, any, never>)["config"]>>
|
|
34
40
|
>,
|
|
35
|
-
moduleName
|
|
41
|
+
moduleName: string
|
|
36
42
|
) => (
|
|
37
43
|
req: Req,
|
|
38
44
|
headers: any
|
|
@@ -64,7 +70,7 @@ export type RPCHandlerFactory<
|
|
|
64
70
|
EffectRequest.Request.Error<Req>,
|
|
65
71
|
HandlerR
|
|
66
72
|
>,
|
|
67
|
-
moduleName
|
|
73
|
+
moduleName: string
|
|
68
74
|
) => (
|
|
69
75
|
req: Req,
|
|
70
76
|
headers: any
|
|
@@ -92,6 +98,14 @@ export interface ContextProviderId {
|
|
|
92
98
|
_tag: "ContextProvider"
|
|
93
99
|
}
|
|
94
100
|
|
|
101
|
+
type RequestContextMapProvider<RequestContextMap extends Record<string, RPCContextMap.Any>> = {
|
|
102
|
+
[K in keyof RequestContextMap]: AnyContextWithLayer<
|
|
103
|
+
{ [K in keyof RequestContextMap]?: RequestContextMap[K]["contextActivation"] },
|
|
104
|
+
RequestContextMap[K]["service"],
|
|
105
|
+
S.Schema.Type<RequestContextMap[K]["error"]>
|
|
106
|
+
>
|
|
107
|
+
}
|
|
108
|
+
|
|
95
109
|
export interface MiddlewareMake<
|
|
96
110
|
RequestContextMap extends Record<string, RPCContextMap.Any>, // what services will the middleware provide dynamically to the handler, or raise errors.
|
|
97
111
|
MakeMiddlewareE, // what the middleware construction can fail with
|
|
@@ -102,9 +116,11 @@ export interface MiddlewareMake<
|
|
|
102
116
|
ContextProviderA, // what the context provider provides
|
|
103
117
|
ContextProviderR extends HttpRouter.HttpRouter.Provided, // what the context provider requires
|
|
104
118
|
MakeContextProviderE, // what the context provider construction can fail with
|
|
105
|
-
MakeContextProviderR // what the context provider construction requires
|
|
119
|
+
MakeContextProviderR, // what the context provider construction requires
|
|
120
|
+
TI extends RequestContextMapProvider<RequestContextMap> // how to resolve the dynamic middleware
|
|
106
121
|
> {
|
|
107
122
|
dependencies?: MiddlewareDependencies
|
|
123
|
+
dynamicMiddlewares: TI
|
|
108
124
|
contextProvider:
|
|
109
125
|
& Context.Tag<
|
|
110
126
|
ContextProviderId,
|
|
@@ -123,7 +139,7 @@ export interface MiddlewareMake<
|
|
|
123
139
|
) => Effect<
|
|
124
140
|
MakeRPCHandlerFactory<RequestContextMap, ContextProviderA | HttpRouter.HttpRouter.Provided>,
|
|
125
141
|
MakeMiddlewareE,
|
|
126
|
-
MakeMiddlewareR // ...that's why MakeMiddlewareR is here
|
|
142
|
+
MakeMiddlewareR | Scope // ...that's why MakeMiddlewareR is here
|
|
127
143
|
>
|
|
128
144
|
}
|
|
129
145
|
|
|
@@ -161,17 +177,19 @@ export const mergeContextProviders = <
|
|
|
161
177
|
Effect.Context<InstanceType<TDeps[number]>>
|
|
162
178
|
>,
|
|
163
179
|
LayersUtils.GetLayersError<{ [K in keyof TDeps]: TDeps[K]["Default"] }>,
|
|
164
|
-
|
|
180
|
+
LayersUtils.GetLayersSuccess<{ [K in keyof TDeps]: TDeps[K]["Default"] }>
|
|
165
181
|
>
|
|
166
182
|
} => ({
|
|
167
183
|
dependencies: deps.map((_) => _.Default) as any,
|
|
168
184
|
effect: Effect.gen(function*() {
|
|
169
|
-
const
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
185
|
+
const makers = yield* Effect.all(deps)
|
|
186
|
+
return Effect
|
|
187
|
+
.gen(function*() {
|
|
188
|
+
const services = (makers as any[]).map((handle, i) => ({ maker: deps[i], handle }))
|
|
189
|
+
// services are effects which return some Context.Context<...>
|
|
190
|
+
const context = yield* mergeContexts(services as any)
|
|
191
|
+
return context
|
|
192
|
+
})
|
|
175
193
|
}) as any
|
|
176
194
|
})
|
|
177
195
|
|
|
@@ -187,7 +205,11 @@ export const ContextProvider = <
|
|
|
187
205
|
Dependencies extends NonEmptyArray<Layer.Layer.Any>
|
|
188
206
|
>(
|
|
189
207
|
input: {
|
|
190
|
-
effect: Effect<
|
|
208
|
+
effect: Effect<
|
|
209
|
+
Effect<ContextProviderA, never, ContextProviderR>,
|
|
210
|
+
MakeContextProviderE,
|
|
211
|
+
MakeContextProviderR | Scope
|
|
212
|
+
>
|
|
191
213
|
dependencies?: Dependencies
|
|
192
214
|
}
|
|
193
215
|
) => {
|
|
@@ -197,7 +219,7 @@ export const ContextProvider = <
|
|
|
197
219
|
>(
|
|
198
220
|
"ContextProvider"
|
|
199
221
|
)
|
|
200
|
-
const l = Layer.
|
|
222
|
+
const l = Layer.scoped(ctx, input.effect)
|
|
201
223
|
return Object.assign(ctx, {
|
|
202
224
|
Default: l.pipe(
|
|
203
225
|
input.dependencies ? Layer.provide(input.dependencies) as any : (_) => _
|
|
@@ -238,8 +260,8 @@ export const MergedContextProvider = <
|
|
|
238
260
|
}
|
|
239
261
|
) =>
|
|
240
262
|
pipe(
|
|
241
|
-
deps as Parameters<typeof mergeContextProviders>[0],
|
|
242
|
-
mergeContextProviders,
|
|
263
|
+
deps as [Parameters<typeof mergeContextProviders>[0]],
|
|
264
|
+
(_) => mergeContextProviders(..._),
|
|
243
265
|
(_) => ContextProvider(_ as any)
|
|
244
266
|
) as unknown as
|
|
245
267
|
& Context.Tag<
|
|
@@ -288,6 +310,76 @@ export type RequestContextMapErrors<RequestContextMap extends Record<string, RPC
|
|
|
288
310
|
RequestContextMap[keyof RequestContextMap]["error"]
|
|
289
311
|
>
|
|
290
312
|
|
|
313
|
+
const logRequestError = logError("Request")
|
|
314
|
+
const reportRequestError = reportError("Request")
|
|
315
|
+
|
|
316
|
+
export class DevMode extends Context.Reference<DevMode>()("DevMode", { defaultValue: () => false }) {}
|
|
317
|
+
|
|
318
|
+
// TODO: pull out to a generic middleware system..
|
|
319
|
+
export const requestMiddleware = <A, E, R>(
|
|
320
|
+
handle: (input: any, headers: HttpHeaders.Headers) => Effect.Effect<A, E, R>,
|
|
321
|
+
moduleName: string
|
|
322
|
+
) =>
|
|
323
|
+
Effect.fnUntraced(function*(input: any, rpcHeaders: HttpHeaders.Headers) {
|
|
324
|
+
const devMode = yield* DevMode
|
|
325
|
+
// merge in the request headers
|
|
326
|
+
// we should consider if we should merge them into rpc headers on the Protocol layer instead.
|
|
327
|
+
const httpReq = yield* HttpServerRequest.HttpServerRequest
|
|
328
|
+
const headers = HttpHeaders.merge(httpReq.headers, rpcHeaders)
|
|
329
|
+
|
|
330
|
+
return yield* Effect
|
|
331
|
+
.annotateCurrentSpan(
|
|
332
|
+
"requestInput",
|
|
333
|
+
Object.entries(input).reduce((prev, [key, value]: [string, unknown]) => {
|
|
334
|
+
prev[key] = key === "password"
|
|
335
|
+
? "<redacted>"
|
|
336
|
+
: typeof value === "string" || typeof value === "number" || typeof value === "boolean"
|
|
337
|
+
? typeof value === "string" && value.length > 256
|
|
338
|
+
? (value.substring(0, 253) + "...")
|
|
339
|
+
: value
|
|
340
|
+
: Array.isArray(value)
|
|
341
|
+
? `Array[${value.length}]`
|
|
342
|
+
: value === null || value === undefined
|
|
343
|
+
? `${value}`
|
|
344
|
+
: typeof value === "object" && value
|
|
345
|
+
? `Object[${Object.keys(value).length}]`
|
|
346
|
+
: typeof value
|
|
347
|
+
return prev
|
|
348
|
+
}, {} as Record<string, string | number | boolean>)
|
|
349
|
+
)
|
|
350
|
+
.pipe(
|
|
351
|
+
// can't use andThen due to some being a function and effect
|
|
352
|
+
Effect.zipRight(handle(input, headers)),
|
|
353
|
+
// TODO: support ParseResult if the error channel of the request allows it.. but who would want that?
|
|
354
|
+
Effect.catchAll((_) => ParseResult.isParseError(_) ? Effect.die(_) : Effect.fail(_)),
|
|
355
|
+
Effect.tapErrorCause((cause) => Cause.isFailure(cause) ? logRequestError(cause) : Effect.void),
|
|
356
|
+
Effect.tapDefect((cause) =>
|
|
357
|
+
Effect
|
|
358
|
+
.all([
|
|
359
|
+
reportRequestError(cause, {
|
|
360
|
+
action: `${moduleName}.${input._tag}`
|
|
361
|
+
}),
|
|
362
|
+
InfraLogger
|
|
363
|
+
.logError("Finished request", cause)
|
|
364
|
+
.pipe(Effect.annotateLogs({
|
|
365
|
+
action: `${moduleName}.${input._tag}`,
|
|
366
|
+
req: pretty(input),
|
|
367
|
+
headers: pretty(headers)
|
|
368
|
+
// resHeaders: pretty(
|
|
369
|
+
// Object
|
|
370
|
+
// .entries(headers)
|
|
371
|
+
// .reduce((prev, [key, value]) => {
|
|
372
|
+
// prev[key] = value && typeof value === "string" ? snipString(value) : value
|
|
373
|
+
// return prev
|
|
374
|
+
// }, {} as Record<string, any>)
|
|
375
|
+
// )
|
|
376
|
+
}))
|
|
377
|
+
])
|
|
378
|
+
),
|
|
379
|
+
devMode ? (_) => _ : Effect.catchAllDefect(() => Effect.die("Internal Server Error"))
|
|
380
|
+
)
|
|
381
|
+
})
|
|
382
|
+
|
|
291
383
|
// factory for middlewares
|
|
292
384
|
export const makeMiddleware =
|
|
293
385
|
// by setting RequestContextMap beforehand, execute contextual typing does not fuck up itself to anys
|
|
@@ -303,7 +395,8 @@ export const makeMiddleware =
|
|
|
303
395
|
ContextProviderA, // what the context provider provides
|
|
304
396
|
ContextProviderR extends HttpRouter.HttpRouter.Provided, // what the context provider requires
|
|
305
397
|
MakeContextProviderE, // what the context provider construction can fail with
|
|
306
|
-
MakeContextProviderR // what the context provider construction requires
|
|
398
|
+
MakeContextProviderR, // what the context provider construction requires
|
|
399
|
+
TI extends RequestContextMapProvider<RequestContextMap> // how to resolve the dynamic middleware
|
|
307
400
|
>(
|
|
308
401
|
make: MiddlewareMake<
|
|
309
402
|
RequestContextMap,
|
|
@@ -313,7 +406,8 @@ export const makeMiddleware =
|
|
|
313
406
|
ContextProviderA,
|
|
314
407
|
ContextProviderR,
|
|
315
408
|
MakeContextProviderE,
|
|
316
|
-
MakeContextProviderR
|
|
409
|
+
MakeContextProviderR,
|
|
410
|
+
TI
|
|
317
411
|
>
|
|
318
412
|
) => {
|
|
319
413
|
// type Id = MiddlewareMakerId &
|
|
@@ -327,33 +421,44 @@ export const makeMiddleware =
|
|
|
327
421
|
"MiddlewareMaker"
|
|
328
422
|
)
|
|
329
423
|
|
|
330
|
-
const
|
|
424
|
+
const dynamicMiddlewares = implementMiddleware<RequestContextMap>()(make.dynamicMiddlewares)
|
|
425
|
+
|
|
426
|
+
const l = Layer.scoped(
|
|
331
427
|
MiddlewareMaker,
|
|
332
428
|
Effect
|
|
333
429
|
.all({
|
|
430
|
+
dynamicMiddlewares: dynamicMiddlewares.effect,
|
|
334
431
|
middleware: make.execute((
|
|
335
432
|
cb: MakeRPCHandlerFactory<RequestContextMap, HttpRouter.HttpRouter.Provided | ContextProviderA>
|
|
336
433
|
) => cb),
|
|
337
434
|
contextProvider: make.contextProvider // uses the middleware.contextProvider tag to get the context provider service
|
|
338
435
|
})
|
|
339
436
|
.pipe(
|
|
340
|
-
Effect.map(({ contextProvider, middleware }) => ({
|
|
437
|
+
Effect.map(({ contextProvider, dynamicMiddlewares, middleware }) => ({
|
|
341
438
|
_tag: "MiddlewareMaker" as const,
|
|
342
439
|
effect: makeRpcEffect<RequestContextMap, ContextProviderA>()(
|
|
343
440
|
(schema, handler, moduleName) => {
|
|
344
|
-
const h = middleware(schema, handler, moduleName)
|
|
345
|
-
return (
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
441
|
+
const h = middleware(schema, handler as any, moduleName)
|
|
442
|
+
return requestMiddleware(
|
|
443
|
+
Effect.fnUntraced(function*(req, headers) {
|
|
444
|
+
yield* Effect.annotateCurrentSpan(
|
|
445
|
+
"request.name",
|
|
446
|
+
moduleName ? `${moduleName}.${req._tag}` : req._tag
|
|
447
|
+
)
|
|
448
|
+
|
|
449
|
+
// the contextProvider is an Effect that builds the context for the request
|
|
450
|
+
return yield* contextProvider.pipe(
|
|
451
|
+
Effect.flatMap((contextProviderContext) =>
|
|
452
|
+
// the dynamicMiddlewares is an Effect that builds the dynamiuc context for the request
|
|
453
|
+
dynamicMiddlewares(schema.config ?? {}, headers).pipe(
|
|
454
|
+
Effect.flatMap((dynamicContext) => h(req, headers).pipe(Effect.provide(dynamicContext))),
|
|
455
|
+
Effect.provide(contextProviderContext)
|
|
354
456
|
)
|
|
457
|
+
)
|
|
355
458
|
)
|
|
356
|
-
)
|
|
459
|
+
}) as any,
|
|
460
|
+
moduleName
|
|
461
|
+
)
|
|
357
462
|
}
|
|
358
463
|
)
|
|
359
464
|
}))
|
|
@@ -361,13 +466,20 @@ export const makeMiddleware =
|
|
|
361
466
|
)
|
|
362
467
|
const middlewareLayer = l
|
|
363
468
|
.pipe(
|
|
364
|
-
|
|
365
|
-
|
|
469
|
+
Layer.provide(
|
|
470
|
+
Layer.mergeAll(
|
|
471
|
+
make.dependencies ? make.dependencies as any : Layer.empty,
|
|
472
|
+
...(dynamicMiddlewares.dependencies as any),
|
|
473
|
+
make.contextProvider.Default
|
|
474
|
+
)
|
|
475
|
+
)
|
|
366
476
|
) as Layer.Layer<
|
|
367
477
|
MiddlewareMakerId,
|
|
368
478
|
| MakeMiddlewareE // what the middleware construction can fail with
|
|
479
|
+
| LayersUtils.GetLayersContext<typeof dynamicMiddlewares.dependencies> // what could go wrong when building the dynamic middleware provider
|
|
369
480
|
| Layer.Error<typeof make.contextProvider.Default>, // what could go wrong when building the context provider
|
|
370
481
|
| LayersUtils.GetLayersContext<MiddlewareDependencies> // what's needed to build layers
|
|
482
|
+
| LayersUtils.GetLayersContext<typeof dynamicMiddlewares.dependencies> // what's needed to build dynamic middleware layers
|
|
371
483
|
| Exclude<MakeMiddlewareR, LayersUtils.GetLayersSuccess<MiddlewareDependencies>> // what layers provides
|
|
372
484
|
| Layer.Context<typeof make.contextProvider.Default> // what's needed to build the contextProvider
|
|
373
485
|
>
|
|
@@ -397,7 +509,7 @@ function makeRpcEffect<
|
|
|
397
509
|
EffectRequest.Request.Error<Req>,
|
|
398
510
|
HandlerR
|
|
399
511
|
>,
|
|
400
|
-
moduleName
|
|
512
|
+
moduleName: string
|
|
401
513
|
) => (
|
|
402
514
|
req: Req,
|
|
403
515
|
headers: any
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
2
|
+
import { Array, Context, Effect, type Layer, Option, type S } from "effect-app"
|
|
3
|
+
import { type GetEffectContext, type RPCContextMap } from "effect-app/client"
|
|
4
|
+
import { type Tag } from "effect-app/Context"
|
|
5
|
+
import { typedValuesOf } from "effect-app/utils"
|
|
6
|
+
import { InfraLogger } from "../../logger.js"
|
|
7
|
+
import { sort } from "./tsort.js"
|
|
8
|
+
|
|
9
|
+
export type ContextWithLayer<
|
|
10
|
+
Config,
|
|
11
|
+
Id,
|
|
12
|
+
Service,
|
|
13
|
+
E,
|
|
14
|
+
R,
|
|
15
|
+
MakeE,
|
|
16
|
+
MakeR,
|
|
17
|
+
Tag extends string,
|
|
18
|
+
Args extends [config: Config, headers: Record<string, string>],
|
|
19
|
+
Dependencies extends any[]
|
|
20
|
+
> =
|
|
21
|
+
& Context.Tag<
|
|
22
|
+
Id,
|
|
23
|
+
{ handle: (...args: Args) => Effect<Option<Context<Service>>, E, R>; _tag: Tag }
|
|
24
|
+
>
|
|
25
|
+
& {
|
|
26
|
+
Default: Layer.Layer<Id, MakeE, MakeR>
|
|
27
|
+
dependsOn?: Dependencies
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export type AnyContextWithLayer<Config, Service, Error> =
|
|
31
|
+
| ContextWithLayer<
|
|
32
|
+
Config,
|
|
33
|
+
any,
|
|
34
|
+
Service,
|
|
35
|
+
Error,
|
|
36
|
+
any,
|
|
37
|
+
any,
|
|
38
|
+
any,
|
|
39
|
+
string,
|
|
40
|
+
any,
|
|
41
|
+
any
|
|
42
|
+
>
|
|
43
|
+
| ContextWithLayer<
|
|
44
|
+
Config,
|
|
45
|
+
any,
|
|
46
|
+
Service,
|
|
47
|
+
Error,
|
|
48
|
+
never,
|
|
49
|
+
any,
|
|
50
|
+
never,
|
|
51
|
+
any,
|
|
52
|
+
any,
|
|
53
|
+
any
|
|
54
|
+
>
|
|
55
|
+
| ContextWithLayer<
|
|
56
|
+
Config,
|
|
57
|
+
any,
|
|
58
|
+
Service,
|
|
59
|
+
Error,
|
|
60
|
+
any,
|
|
61
|
+
any,
|
|
62
|
+
never,
|
|
63
|
+
any,
|
|
64
|
+
any,
|
|
65
|
+
any
|
|
66
|
+
>
|
|
67
|
+
| ContextWithLayer<
|
|
68
|
+
Config,
|
|
69
|
+
any,
|
|
70
|
+
Service,
|
|
71
|
+
Error,
|
|
72
|
+
never,
|
|
73
|
+
any,
|
|
74
|
+
any,
|
|
75
|
+
any,
|
|
76
|
+
any,
|
|
77
|
+
any
|
|
78
|
+
>
|
|
79
|
+
|
|
80
|
+
export const mergeContexts = Effect.fnUntraced(
|
|
81
|
+
function*<T extends readonly { maker: any; handle: Effect<Context<any>> }[]>(makers: T) {
|
|
82
|
+
let context = Context.empty()
|
|
83
|
+
for (const mw of makers) {
|
|
84
|
+
yield* InfraLogger.logDebug("Building context for middleware", mw.maker.key ?? mw.maker)
|
|
85
|
+
const moreContext = yield* mw.handle.pipe(Effect.provide(context))
|
|
86
|
+
yield* InfraLogger.logDebug(
|
|
87
|
+
"Built context for middleware",
|
|
88
|
+
mw.maker.key ?? mw.maker,
|
|
89
|
+
(moreContext as any).toJSON().services
|
|
90
|
+
)
|
|
91
|
+
context = Context.merge(context, moreContext)
|
|
92
|
+
}
|
|
93
|
+
return context as Context.Context<Effect.Success<T[number]["handle"]>>
|
|
94
|
+
}
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
export const mergeOptionContexts = Effect.fnUntraced(
|
|
98
|
+
function*<T extends readonly { maker: any; handle: Effect<Option<Context<any>>> }[]>(makers: T) {
|
|
99
|
+
let context = Context.empty()
|
|
100
|
+
for (const mw of makers) {
|
|
101
|
+
yield* InfraLogger.logDebug("Building context for middleware", mw.maker.key ?? mw.maker)
|
|
102
|
+
const moreContext = yield* mw.handle.pipe(Effect.provide(context))
|
|
103
|
+
yield* InfraLogger.logDebug(
|
|
104
|
+
"Built context for middleware",
|
|
105
|
+
mw.maker.key ?? mw.maker,
|
|
106
|
+
Option.map(moreContext, (c) => (c as any).toJSON().services)
|
|
107
|
+
)
|
|
108
|
+
if (moreContext.value) {
|
|
109
|
+
context = Context.merge(context, moreContext.value)
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
return context
|
|
113
|
+
}
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
export const implementMiddleware = <T extends Record<string, RPCContextMap.Any>>() =>
|
|
117
|
+
<
|
|
118
|
+
TI extends {
|
|
119
|
+
[K in keyof T]: AnyContextWithLayer<
|
|
120
|
+
{ [K in keyof T]?: T[K]["contextActivation"] },
|
|
121
|
+
T[K]["service"],
|
|
122
|
+
S.Schema.Type<T[K]["error"]>
|
|
123
|
+
>
|
|
124
|
+
}
|
|
125
|
+
>(implementations: TI) => ({
|
|
126
|
+
dependencies: typedValuesOf(implementations).map((_) => _.Default) as {
|
|
127
|
+
[K in keyof TI]: TI[K]["Default"]
|
|
128
|
+
}[keyof TI][],
|
|
129
|
+
effect: Effect.gen(function*() {
|
|
130
|
+
const sorted = sort(typedValuesOf(implementations))
|
|
131
|
+
|
|
132
|
+
const makers = yield* Effect.all(sorted)
|
|
133
|
+
return Effect.fnUntraced(
|
|
134
|
+
function*(config: { [K in keyof T]?: T[K]["contextActivation"] }, headers: Record<string, string>) {
|
|
135
|
+
const ctx = yield* mergeOptionContexts(
|
|
136
|
+
Array.map(
|
|
137
|
+
makers,
|
|
138
|
+
(_, i) => ({ maker: sorted[i], handle: (_ as any).handle(config, headers) as any }) as any
|
|
139
|
+
)
|
|
140
|
+
)
|
|
141
|
+
return ctx as Context.Context<
|
|
142
|
+
GetEffectContext<T, typeof config>
|
|
143
|
+
>
|
|
144
|
+
}
|
|
145
|
+
) as (
|
|
146
|
+
config: { [K in keyof T]?: T[K]["contextActivation"] },
|
|
147
|
+
headers: Record<string, string>
|
|
148
|
+
) => Effect.Effect<
|
|
149
|
+
Context.Context<GetEffectContext<T, typeof config>>,
|
|
150
|
+
Effect.Error<ReturnType<Tag.Service<TI[keyof TI]>["handle"]>>,
|
|
151
|
+
Effect.Context<ReturnType<Tag.Service<TI[keyof TI]>["handle"]>>
|
|
152
|
+
>
|
|
153
|
+
})
|
|
154
|
+
})
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
2
|
+
// @ts-nocheck
|
|
3
|
+
export function tsort(edges) {
|
|
4
|
+
const nodes = new Map(), sorted = [], visited = new Map()
|
|
5
|
+
|
|
6
|
+
const Node = function(id) {
|
|
7
|
+
this.id = id
|
|
8
|
+
this.afters = []
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
edges.forEach((v) => {
|
|
12
|
+
const from = v[0], to = v[1]
|
|
13
|
+
if (!nodes.get(from)) nodes.set(from, new Node(from))
|
|
14
|
+
if (!nodes.get(to)) nodes.set(to, new Node(to))
|
|
15
|
+
nodes.get(from).afters.push(to)
|
|
16
|
+
})
|
|
17
|
+
;[...nodes.keys()].forEach(function visit(idstr, ancestors) {
|
|
18
|
+
const node = nodes.get(idstr), id = node.id
|
|
19
|
+
|
|
20
|
+
if (visited.get(idstr)) return
|
|
21
|
+
if (!Array.isArray(ancestors)) ancestors = []
|
|
22
|
+
|
|
23
|
+
ancestors.push(id)
|
|
24
|
+
visited.set(idstr, true)
|
|
25
|
+
node.afters.forEach(function(afterID) {
|
|
26
|
+
if (ancestors.indexOf(afterID) >= 0) {
|
|
27
|
+
throw new Error("closed chain : " + afterID + " is in " + id)
|
|
28
|
+
}
|
|
29
|
+
visit(
|
|
30
|
+
afterID,
|
|
31
|
+
ancestors.map(function(v) {
|
|
32
|
+
return v
|
|
33
|
+
})
|
|
34
|
+
)
|
|
35
|
+
})
|
|
36
|
+
sorted.unshift(id)
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
return sorted
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export const createEdges = <T extends { dependsOn?: any[] }>(dep: readonly T[]) => {
|
|
43
|
+
const result = []
|
|
44
|
+
dep.forEach((key) => {
|
|
45
|
+
key.dependsOn?.forEach((n) => {
|
|
46
|
+
result.push([n, key])
|
|
47
|
+
})
|
|
48
|
+
})
|
|
49
|
+
return result
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export const sort = <T>(dep: readonly (T & { dependsOn?: any[] })[]): readonly T[] => {
|
|
53
|
+
const edges = createEdges(dep)
|
|
54
|
+
const result = tsort(edges)
|
|
55
|
+
return result.concat(dep.filter((v) => !result.includes(v)))
|
|
56
|
+
}
|
package/src/api/routing.ts
CHANGED
|
@@ -3,16 +3,14 @@
|
|
|
3
3
|
/* eslint-disable @typescript-eslint/no-empty-object-type */
|
|
4
4
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
5
5
|
import { determineMethod, isCommand } from "@effect-app/infra/api/routing/utils"
|
|
6
|
-
import { logError, reportError } from "@effect-app/infra/errorReporter"
|
|
7
|
-
import { InfraLogger } from "@effect-app/infra/logger"
|
|
8
6
|
import { Rpc, RpcGroup, RpcServer } from "@effect/rpc"
|
|
9
|
-
import { Array,
|
|
7
|
+
import { type Array, Duration, Effect, Layer, type NonEmptyArray, type NonEmptyReadonlyArray, Predicate, Request, S, Schedule, Schema } from "effect-app"
|
|
10
8
|
import type { GetEffectContext, GetEffectError, RPCContextMap } from "effect-app/client/req"
|
|
11
9
|
import { type HttpHeaders, HttpRouter } from "effect-app/http"
|
|
12
|
-
import {
|
|
10
|
+
import { typedKeysOf, typedValuesOf } from "effect-app/utils"
|
|
13
11
|
import type { Contravariant } from "effect/Types"
|
|
14
12
|
import { type YieldWrap } from "effect/Utils"
|
|
15
|
-
import { type Middleware } from "./routing/DynamicMiddleware.js"
|
|
13
|
+
import { DevMode, type Middleware } from "./routing/DynamicMiddleware.js"
|
|
16
14
|
|
|
17
15
|
export * from "./routing/DynamicMiddleware.js"
|
|
18
16
|
|
|
@@ -36,9 +34,6 @@ export namespace LayersUtils {
|
|
|
36
34
|
: never
|
|
37
35
|
}
|
|
38
36
|
|
|
39
|
-
const logRequestError = logError("Request")
|
|
40
|
-
const reportRequestError = reportError("Request")
|
|
41
|
-
|
|
42
37
|
// retry just once on optimistic concurrency exceptions
|
|
43
38
|
const optimisticConcurrencySchedule = Schedule.once.pipe(
|
|
44
39
|
Schedule.intersect(Schedule.recurWhile<any>((a) => a?._tag === "OptimisticConcurrencyException"))
|
|
@@ -423,64 +418,17 @@ export const makeRouter = <
|
|
|
423
418
|
}
|
|
424
419
|
} as any
|
|
425
420
|
: resource,
|
|
426
|
-
rpc.effect(
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
.
|
|
430
|
-
"requestInput",
|
|
431
|
-
Object.entries(input).reduce((prev, [key, value]: [string, unknown]) => {
|
|
432
|
-
prev[key] = key === "password"
|
|
433
|
-
? "<redacted>"
|
|
434
|
-
: typeof value === "string" || typeof value === "number" || typeof value === "boolean"
|
|
435
|
-
? typeof value === "string" && value.length > 256
|
|
436
|
-
? (value.substring(0, 253) + "...")
|
|
437
|
-
: value
|
|
438
|
-
: Array.isArray(value)
|
|
439
|
-
? `Array[${value.length}]`
|
|
440
|
-
: value === null || value === undefined
|
|
441
|
-
? `${value}`
|
|
442
|
-
: typeof value === "object" && value
|
|
443
|
-
? `Object[${Object.keys(value).length}]`
|
|
444
|
-
: typeof value
|
|
445
|
-
return prev
|
|
446
|
-
}, {} as Record<string, string | number | boolean>)
|
|
447
|
-
)
|
|
448
|
-
.pipe(
|
|
449
|
-
// can't use andThen due to some being a function and effect
|
|
450
|
-
Effect.zipRight(handle(input, headers)),
|
|
451
|
-
// TODO: support ParseResult if the error channel of the request allows it.. but who would want that?
|
|
452
|
-
Effect.catchAll((_) => ParseResult.isParseError(_) ? Effect.die(_) : Effect.fail(_)),
|
|
453
|
-
Effect.tapErrorCause((cause) => Cause.isFailure(cause) ? logRequestError(cause) : Effect.void),
|
|
454
|
-
Effect.tapDefect((cause) =>
|
|
455
|
-
Effect
|
|
456
|
-
.all([
|
|
457
|
-
reportRequestError(cause, {
|
|
458
|
-
action: `${meta.moduleName}.${resource._tag}`
|
|
459
|
-
}),
|
|
460
|
-
InfraLogger
|
|
461
|
-
.logError("Finished request", cause)
|
|
462
|
-
.pipe(Effect.annotateLogs({
|
|
463
|
-
action: `${meta.moduleName}.${resource._tag}`,
|
|
464
|
-
req: pretty(resource),
|
|
465
|
-
headers: pretty(headers)
|
|
466
|
-
// resHeaders: pretty(
|
|
467
|
-
// Object
|
|
468
|
-
// .entries(headers)
|
|
469
|
-
// .reduce((prev, [key, value]) => {
|
|
470
|
-
// prev[key] = value && typeof value === "string" ? snipString(value) : value
|
|
471
|
-
// return prev
|
|
472
|
-
// }, {} as Record<string, any>)
|
|
473
|
-
// )
|
|
474
|
-
}))
|
|
475
|
-
])
|
|
476
|
-
),
|
|
477
|
-
// NOTE: this does not catch errors from the middlewares..
|
|
478
|
-
// we should re-evalute this in any case..
|
|
479
|
-
devMode ? (_) => _ : Effect.catchAllDefect(() => Effect.die("Internal Server Error")),
|
|
421
|
+
rpc.effect(
|
|
422
|
+
resource,
|
|
423
|
+
(req, headers) =>
|
|
424
|
+
handle(req, headers).pipe(
|
|
480
425
|
Effect.withSpan("Request." + meta.moduleName + "." + resource._tag, {
|
|
481
426
|
captureStackTrace: () => handler.stack
|
|
482
|
-
})
|
|
483
|
-
|
|
427
|
+
}),
|
|
428
|
+
Effect.provideService(DevMode, devMode)
|
|
429
|
+
),
|
|
430
|
+
meta.moduleName
|
|
431
|
+
),
|
|
484
432
|
meta.moduleName
|
|
485
433
|
] as const
|
|
486
434
|
return acc
|