@effect-app/infra 2.73.4 → 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 +11 -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 +82 -16
- 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 +146 -34
- 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
|
@@ -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
|
package/test/controller.test.ts
CHANGED
|
@@ -1,18 +1,20 @@
|
|
|
1
1
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
2
2
|
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
|
|
3
|
-
import { type MakeContext, type MakeErrors, makeRouter } from "@effect-app/infra/api/routing"
|
|
3
|
+
import { type MakeContext, type MakeErrors, makeRouter, RequestCacheLayers } from "@effect-app/infra/api/routing"
|
|
4
4
|
import type { RequestContext } from "@effect-app/infra/RequestContext"
|
|
5
|
-
import { expectTypeOf } from "@effect/vitest"
|
|
6
|
-
import { Context, Effect, Layer, S } from "effect-app"
|
|
7
|
-
import {
|
|
5
|
+
import { expect, expectTypeOf, it } from "@effect/vitest"
|
|
6
|
+
import { type Array, Context, Effect, Layer, Option, S } from "effect-app"
|
|
7
|
+
import { InvalidStateError, makeRpcClient, type RPCContextMap, UnauthorizedError } from "effect-app/client"
|
|
8
8
|
import { HttpServerRequest } from "effect-app/http"
|
|
9
9
|
import { Class, TaggedError } from "effect-app/Schema"
|
|
10
10
|
import { ContextProvider, makeMiddleware, mergeContextProviders, MergedContextProvider } from "../src/api/routing/DynamicMiddleware.js"
|
|
11
|
+
import { sort } from "../src/api/routing/tsort.js"
|
|
11
12
|
import { SomeService } from "./query.test.js"
|
|
12
13
|
|
|
13
14
|
class UserProfile extends Context.assignTag<UserProfile, UserProfile>("UserProfile")(
|
|
14
15
|
Class<UserProfile>("UserProfile")({
|
|
15
|
-
id: S.String
|
|
16
|
+
id: S.String,
|
|
17
|
+
roles: S.Array(S.String)
|
|
16
18
|
})
|
|
17
19
|
) {
|
|
18
20
|
}
|
|
@@ -32,7 +34,7 @@ export class Some extends Context.TagMakeId("Some", Effect.succeed({ a: 1 }))<So
|
|
|
32
34
|
export class SomeElse extends Context.TagMakeId("SomeElse", Effect.succeed({ b: 2 }))<SomeElse>() {}
|
|
33
35
|
|
|
34
36
|
// @effect-diagnostics-next-line missingEffectServiceDependency:off
|
|
35
|
-
const
|
|
37
|
+
export const someContextProvider = ContextProvider({
|
|
36
38
|
effect: Effect.gen(function*() {
|
|
37
39
|
yield* SomeService
|
|
38
40
|
if (Math.random() > 0.5) return yield* new CustomError1()
|
|
@@ -59,7 +61,7 @@ class MyContextProvider extends Effect.Service<MyContextProvider>()("MyContextPr
|
|
|
59
61
|
if (Math.random() > 0.5) return yield* new CustomError1()
|
|
60
62
|
|
|
61
63
|
return Effect.gen(function*() {
|
|
62
|
-
// the only
|
|
64
|
+
// the only requiremeno you can have are the one provided by HttpRouter.HttpRouter.Provided
|
|
63
65
|
yield* HttpServerRequest.HttpServerRequest
|
|
64
66
|
|
|
65
67
|
// this is allowed here but mergeContextProviders/MergedContextProvider will trigger an error
|
|
@@ -74,62 +76,118 @@ class MyContextProvider extends Effect.Service<MyContextProvider>()("MyContextPr
|
|
|
74
76
|
})
|
|
75
77
|
}) {}
|
|
76
78
|
|
|
79
|
+
class RequestCacheContext extends Effect.Service<RequestCacheContext>()("RequestCacheContext", {
|
|
80
|
+
effect: Effect.gen(function*() {
|
|
81
|
+
return Effect.gen(function*() {
|
|
82
|
+
const ctx = yield* Layer.build(RequestCacheLayers)
|
|
83
|
+
return ctx as Context.Context<any> // todo: ugh.
|
|
84
|
+
})
|
|
85
|
+
})
|
|
86
|
+
}) {}
|
|
87
|
+
|
|
77
88
|
const merged = mergeContextProviders(MyContextProvider)
|
|
78
89
|
export const contextProvider2 = ContextProvider(merged)
|
|
79
90
|
export const contextProvider3 = MergedContextProvider(MyContextProvider)
|
|
80
|
-
|
|
81
|
-
expectTypeOf(contextProvider2).toEqualTypeOf<typeof contextProvider>()
|
|
91
|
+
expectTypeOf(contextProvider2).toEqualTypeOf<typeof someContextProvider>()
|
|
82
92
|
expectTypeOf(contextProvider3).toEqualTypeOf<typeof contextProvider2>()
|
|
93
|
+
const merged2 = mergeContextProviders(MyContextProvider, RequestCacheContext)
|
|
94
|
+
export const contextProvider22 = ContextProvider(merged2)
|
|
95
|
+
export const contextProvider23 = MergedContextProvider(MyContextProvider, RequestCacheContext)
|
|
96
|
+
expectTypeOf(contextProvider23).toEqualTypeOf<typeof contextProvider22>()
|
|
83
97
|
|
|
84
98
|
export type RequestContextMap = {
|
|
85
|
-
allowAnonymous: RPCContextMap.Inverted<
|
|
86
|
-
|
|
87
|
-
|
|
99
|
+
allowAnonymous: RPCContextMap.Inverted<UserProfile, typeof NotLoggedInError>
|
|
100
|
+
requireRoles: RPCContextMap.Custom<never, typeof UnauthorizedError, Array<string>>
|
|
101
|
+
test: RPCContextMap<never, typeof S.Never>
|
|
88
102
|
}
|
|
89
103
|
|
|
90
104
|
const Str = Context.GenericTag<"str", "str">("str")
|
|
91
105
|
const Str2 = Context.GenericTag<"str2", "str">("str2")
|
|
92
106
|
|
|
107
|
+
class AllowAnonymous extends Effect.Service<AllowAnonymous>()("AllowAnonymous", {
|
|
108
|
+
effect: Effect.gen(function*() {
|
|
109
|
+
return {
|
|
110
|
+
handle: Effect.fn(function*(opts: { allowAnonymous?: false }, headers: Record<string, string>) {
|
|
111
|
+
const isLoggedIn = !!headers["x-user"]
|
|
112
|
+
if (!isLoggedIn) {
|
|
113
|
+
if (!opts.allowAnonymous) {
|
|
114
|
+
return yield* new NotLoggedInError({ message: "Not logged in" })
|
|
115
|
+
}
|
|
116
|
+
return Option.none()
|
|
117
|
+
}
|
|
118
|
+
return Option.some(Context.make(
|
|
119
|
+
UserProfile,
|
|
120
|
+
{ id: "whatever", roles: ["user", "manager"] }
|
|
121
|
+
))
|
|
122
|
+
})
|
|
123
|
+
}
|
|
124
|
+
})
|
|
125
|
+
}) {}
|
|
126
|
+
|
|
127
|
+
class RequireRoles extends Effect.Service<RequireRoles>()("RequireRoles", {
|
|
128
|
+
effect: Effect.gen(function*() {
|
|
129
|
+
return {
|
|
130
|
+
handle: Effect.fn(
|
|
131
|
+
function*(cfg: { requireRoles?: readonly string[] }) {
|
|
132
|
+
// we don't know if the service will be provided or not, so we use option..
|
|
133
|
+
const userProfile = yield* Effect.serviceOption(UserProfile)
|
|
134
|
+
const { requireRoles } = cfg
|
|
135
|
+
if (requireRoles && !userProfile.value?.roles?.some((role) => requireRoles.includes(role))) {
|
|
136
|
+
return yield* new UnauthorizedError({ message: "don't have the right roles" })
|
|
137
|
+
}
|
|
138
|
+
return Option.none<Context<never>>()
|
|
139
|
+
}
|
|
140
|
+
)
|
|
141
|
+
}
|
|
142
|
+
})
|
|
143
|
+
}) {
|
|
144
|
+
static dependsOn = [AllowAnonymous]
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
class Test extends Effect.Service<Test>()("Test", {
|
|
148
|
+
effect: Effect.gen(function*() {
|
|
149
|
+
return {
|
|
150
|
+
handle: Effect.fn(function*() {
|
|
151
|
+
return Option.none<Context<never>>()
|
|
152
|
+
})
|
|
153
|
+
}
|
|
154
|
+
})
|
|
155
|
+
}) {}
|
|
156
|
+
|
|
157
|
+
// TODO: eventually it might be nice if we have total control over order somehow..
|
|
158
|
+
// [ AddRequestNameToSpanContext, RequestCacheContext, UninterruptibleMiddleware, Dynamic(or individual, AllowAnonymous, RequireRoles, Test - or whichever order) ]
|
|
93
159
|
const middleware = makeMiddleware<RequestContextMap>()({
|
|
94
160
|
dependencies: [Layer.effect(Str2, Str)],
|
|
95
|
-
|
|
161
|
+
// TODO: I guess it makes sense to support just passing array of context providers too, like dynamicMiddlewares?
|
|
162
|
+
contextProvider: MergedContextProvider(RequestCacheContext, MyContextProvider),
|
|
163
|
+
// or is the better api to use constructors outside, like how contextProvider is used now?
|
|
164
|
+
dynamicMiddlewares: {
|
|
165
|
+
requireRoles: RequireRoles,
|
|
166
|
+
allowAnonymous: AllowAnonymous,
|
|
167
|
+
test: Test
|
|
168
|
+
},
|
|
169
|
+
// TODO: 0..n of these generic middlewares?
|
|
96
170
|
execute: (maker) =>
|
|
97
171
|
Effect.gen(function*() {
|
|
98
|
-
return maker(
|
|
99
|
-
|
|
100
|
-
.
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
)
|
|
118
|
-
})
|
|
119
|
-
.pipe(
|
|
120
|
-
Effect.provide(
|
|
121
|
-
Effect
|
|
122
|
-
.gen(function*() {
|
|
123
|
-
yield* Effect.annotateCurrentSpan("request.name", moduleName ? `${moduleName}.${req._tag}` : req._tag)
|
|
124
|
-
|
|
125
|
-
// const httpReq = yield* HttpServerRequest.HttpServerRequest
|
|
126
|
-
|
|
127
|
-
//
|
|
128
|
-
})
|
|
129
|
-
.pipe(Layer.effectDiscard)
|
|
130
|
-
)
|
|
131
|
-
)
|
|
132
|
-
})
|
|
172
|
+
return maker(
|
|
173
|
+
(_schema, handler) => (req, headers) =>
|
|
174
|
+
// contextProvider and dynamicMiddlewares are already provided here.
|
|
175
|
+
// aka this runs "last"
|
|
176
|
+
Effect
|
|
177
|
+
.gen(function*() {
|
|
178
|
+
// you can use only HttpRouter.HttpRouter.Provided here as additional context
|
|
179
|
+
// and what ContextMaker provides too
|
|
180
|
+
// const someElse = yield* SomeElse
|
|
181
|
+
yield* Some // provided by ContextMaker
|
|
182
|
+
yield* HttpServerRequest.HttpServerRequest // provided by HttpRouter.HttpRouter.Provided
|
|
183
|
+
|
|
184
|
+
return yield* handler(req, headers)
|
|
185
|
+
.pipe(
|
|
186
|
+
// TODO: make this depend on query/command, and consider if middleware also should be affected. right now it's not.
|
|
187
|
+
Effect.uninterruptible
|
|
188
|
+
)
|
|
189
|
+
})
|
|
190
|
+
)
|
|
133
191
|
})
|
|
134
192
|
})
|
|
135
193
|
|
|
@@ -141,7 +199,8 @@ export type RequestConfig = {
|
|
|
141
199
|
}
|
|
142
200
|
export const { TaggedRequest: Req } = makeRpcClient<RequestConfig, RequestContextMap>({
|
|
143
201
|
allowAnonymous: NotLoggedInError,
|
|
144
|
-
requireRoles: UnauthorizedError
|
|
202
|
+
requireRoles: UnauthorizedError,
|
|
203
|
+
test: S.Never
|
|
145
204
|
})
|
|
146
205
|
|
|
147
206
|
export class Eff extends Req<Eff>()("Eff", {}, { success: S.Void }) {}
|
|
@@ -269,6 +328,13 @@ const router = Router(Something)({
|
|
|
269
328
|
}
|
|
270
329
|
})
|
|
271
330
|
|
|
331
|
+
it("sorts based on requirements", () => {
|
|
332
|
+
const input = [RequireRoles, AllowAnonymous, Test]
|
|
333
|
+
const sorted = sort(input)
|
|
334
|
+
console.dir({ input, sorted }, { depth: 10 })
|
|
335
|
+
expect(sorted).toEqual([AllowAnonymous, RequireRoles, Test])
|
|
336
|
+
})
|
|
337
|
+
|
|
272
338
|
// eslint-disable-next-line unused-imports/no-unused-vars
|
|
273
339
|
const matched = matchAll({ router })
|
|
274
340
|
expectTypeOf({} as Layer.Context<typeof matched>).toEqualTypeOf<SomeService | "str">()
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"controller.legacy2.test.d.ts","sourceRoot":"","sources":["../controller.legacy2.test.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"controller.legacy3.test.d.ts","sourceRoot":"","sources":["../controller.legacy3.test.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,kCAAkC,CAAA;AACtE,OAAO,EAAE,GAAG,EAAE,MAAM,aAAa,CAAA;AAEjC,OAAO,EAAW,MAAM,EAAY,KAAK,EAAE,CAAC,EAAY,MAAM,YAAY,CAAA;AAC1E,OAAO,EAAwC,KAAK,aAAa,EAAE,iBAAiB,EAAE,MAAM,mBAAmB,CAAA;AAC/G,OAAO,EAAe,iBAAiB,EAAE,MAAM,iBAAiB,CAAA;AAOhE,MAAM,WAAW,GAAG;IAClB,OAAO,EAAE,cAAc,CAAA;CACxB;AAED,MAAM,MAAM,MAAM,GAAG;IAGnB,YAAY,EAAE,aAAa,CAAC,MAAM,CAAC,EAAE,EAAE,KAAK,EAAE,OAAO,iBAAiB,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC,CAAA;CACvF,CAAA;AAmGD,eAAO,MAAQ,QAAQ;;;;;gBA0EgwD,MAAO,KAAK;;;;;;GA1E1wD,QAAQ;;;;;;iDA0Eq0F,MAAM,KAAK,CAAC,GAAG;;;;;;;;;;;;iDAAg1D,MAAM,KAAK,CAAC,GAAG;;;;;;;;;;;;iDAAg1D,MAAM,KAAK,CAAC,GAAG;;;;;;;;;;;;iDAAg1D,MAAM,KAAK,CAAC,GAAG;;;;;;;;;;;;iDAA2yD,MAAM,KAAK,CAAC,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;CA1E1oU,CAAA;AAElE,MAAM,MAAM,aAAa,GAAG;IAC1B,yCAAyC;IACzC,cAAc,CAAC,EAAE,IAAI,CAAA;IACrB,iEAAiE;IACjE,UAAU,CAAC,EAAE,SAAS,MAAM,EAAE,CAAA;CAC/B,CAAA;AACD,eAAO,MAAuB,GAAG;yCAxEL,EAAG,MAAM,CAAC,MAAM;iBAgClC,EA/BK,MACb,CA8BQ,GA9BJ,GA8BI,EA9BC,MAEV,CA4BS,MA5BF;iBAAuB,EAAE,MAC9B,CA2BO,GA3BH,GA2BG,EA3BG,MAAK,CAAC,MAAM;;;;;;2CASkB,EAAG,MAAM,CAAC,MAAM;iBAkBjD,EAAA,MAjBA,CAAC,GAAG,GAAE,EAAG,MAAM,CAAC,MAAM;;;;;;2CAiBtB,EAPO,MAAC,CAAC,MAAM;iBACA,EAAG,MAAM,CAAC,GAAG,GAAE,EAAG,MAAM,CAAC,MAAM;;;;;;2CAc1C,EAAA,MAAL,CAAK,MAAE;;;;;2CAMqC,EAAG,MAAM,CAAC,MAAM;;;;;CA6BnE,CAAA;;;;;;;;;;AAEF,qBAAa,YAAa,SAAQ,iBAEX;CAAG;;;;;;;;;;AAE1B,qBAAa,gBAAiB,SAAQ,qBAEb;CAAG;;;;;AAI5B,qBAAa,gBAAiB,SAAQ,qBAKpC;CAAG;;;;;AASL,qBAAa,aAAc,SAAQ,kBAOjC;CAAG;;;;;AAEL,qBAAa,iBAAkB,SAAQ,sBAKrC;CAAG"}
|