From c7282e5e0ed80347e9e3f629ff4ab79c9726f4e1 Mon Sep 17 00:00:00 2001 From: Patrick Roza Date: Sun, 3 Aug 2025 13:10:57 +0200 Subject: [PATCH 01/17] play with requires --- .../routing/middleware/DynamicMiddleware.ts | 40 ++++++++++++++----- .../routing/middleware/dynamic-middleware.ts | 6 ++- .../routing/middleware/generic-middleware.ts | 7 ++++ packages/infra/test/controller.test.ts | 4 +- 4 files changed, 44 insertions(+), 13 deletions(-) diff --git a/packages/infra/src/api/routing/middleware/DynamicMiddleware.ts b/packages/infra/src/api/routing/middleware/DynamicMiddleware.ts index 134d909e0e..1cb77084b7 100644 --- a/packages/infra/src/api/routing/middleware/DynamicMiddleware.ts +++ b/packages/infra/src/api/routing/middleware/DynamicMiddleware.ts @@ -446,22 +446,22 @@ function makeRpcEffect< } // updated to support Scope.Scope -export interface RpcMiddleware { +export interface RpcMiddleware { (options: { readonly clientId: number readonly rpc: Rpc.AnyWithProps readonly payload: unknown readonly headers: HttpHeaders.Headers - }): Effect.Effect + }): Effect.Effect } -export interface RpcMiddlewareWrap { +export interface RpcMiddlewareWrap { (options: { readonly clientId: number readonly rpc: Rpc.AnyWithProps readonly payload: unknown readonly headers: HttpHeaders.Headers - readonly next: Effect.Effect - }): Effect.Effect + readonly next: Effect.Effect + }): Effect.Effect } type RpcOptionsOriginal = { @@ -484,14 +484,14 @@ type RpcOptionsDynamic = RpcOpt export type Dynamic = Options extends RpcOptionsDynamic ? true : false -export interface RpcMiddlewareDynamic { +export interface RpcMiddlewareDynamic { (options: { readonly config: Config // todo readonly clientId: number readonly rpc: Rpc.AnyWithProps readonly payload: unknown readonly headers: HttpHeaders.Headers - }): Effect.Effect>, E, Scope.Scope> + }): Effect.Effect>, E, Scope.Scope | R> } export interface TagClassDynamicAny> @@ -522,6 +522,20 @@ export declare namespace TagClass { } ? ContextRepr.Identifier : never + /** + * @since 1.0.0 + * @category models + */ + export type Requires = Options extends { + readonly requires: Context.Tag + readonly optional?: false + } ? Context.Tag.Identifier + : Options extends { + readonly requires: ContextRepr + readonly optional?: false + } ? ContextRepr.Identifier + : never + /** * @since 1.0.0 * @category models @@ -610,16 +624,19 @@ export interface TagClass< Options, TagClass.Wrap extends true ? RpcMiddlewareWrap< TagClass.Provides, + TagClass.Requires, TagClass.Failure > : Options extends RpcOptionsDynamic ? RpcMiddlewareDynamic< TagClass.Service, TagClass.FailureService, + TagClass.Requires, { [K in Options["dynamic"]["key"]]?: Options["dynamic"]["settings"]["contextActivation"] } > : RpcMiddleware< TagClass.Service, - TagClass.FailureService + TagClass.FailureService, + TagClass.Requires > > {} @@ -636,16 +653,19 @@ export const Tag = () => effect: Effect.Effect< TagClass.Wrap extends true ? RpcMiddlewareWrap< TagClass.Provides, - TagClass.Failure + TagClass.Failure, + TagClass.Requires > : Options extends RpcOptionsDynamic ? RpcMiddlewareDynamic< TagClass.Service, TagClass.FailureService, + TagClass.Requires, { [K in Options["dynamic"]["key"]]?: Options["dynamic"]["settings"]["contextActivation"] } > : RpcMiddleware< TagClass.Service, - TagClass.FailureService + TagClass.FailureService, + TagClass.Requires >, E, R diff --git a/packages/infra/src/api/routing/middleware/dynamic-middleware.ts b/packages/infra/src/api/routing/middleware/dynamic-middleware.ts index 480b2640fc..612dcb6ad0 100644 --- a/packages/infra/src/api/routing/middleware/dynamic-middleware.ts +++ b/packages/infra/src/api/routing/middleware/dynamic-middleware.ts @@ -20,13 +20,15 @@ export type ContextWithLayer< & ( | ContextTagWithDefault< Id, - RpcMiddlewareDynamic, + // todo + RpcMiddlewareDynamic, LayerE, LayerR > | ContextTagWithDefault< Id, - RpcMiddlewareDynamic, + // todo + RpcMiddlewareDynamic, LayerE, LayerR > diff --git a/packages/infra/src/api/routing/middleware/generic-middleware.ts b/packages/infra/src/api/routing/middleware/generic-middleware.ts index 7ba09a2385..40497fc9c2 100644 --- a/packages/infra/src/api/routing/middleware/generic-middleware.ts +++ b/packages/infra/src/api/routing/middleware/generic-middleware.ts @@ -18,6 +18,7 @@ export interface TagClassAny extends Context.Tag { readonly [TypeId]: TypeId readonly optional: boolean readonly provides?: Context.Tag | ContextRepr | undefined + readonly requires?: Context.Tag | ContextRepr | undefined readonly failure: Schema.Schema.All readonly requiredForClient: boolean readonly wrap: boolean @@ -43,6 +44,12 @@ export namespace GenericMiddlewareMaker { : T extends { provides: ContextRepr } ? ContextRepr.Identifier : never : never + + export type Required = T extends TagClassAny + ? T extends { requires: Context.Tag } ? Context.Tag.Identifier + : T extends { requires: ContextRepr } ? ContextRepr.Identifier + : never + : never } export const genericMiddleware = (i: GenericMiddlewareMaker) => i diff --git a/packages/infra/test/controller.test.ts b/packages/infra/test/controller.test.ts index 9386c43a3c..558e90745f 100644 --- a/packages/infra/test/controller.test.ts +++ b/packages/infra/test/controller.test.ts @@ -34,13 +34,15 @@ export class SomeElse extends Context.TagMakeId("SomeElse", Effect.succeed({ b: // @effect-diagnostics-next-line missingEffectServiceDependency:off class MyContextProvider extends Middleware.Tag()("MyContextProvider", { - provides: [Some] + provides: [Some], + requires: [SomeElse] })({ effect: Effect.gen(function*() { yield* SomeService if (Math.random() > 0.5) return yield* new CustomError1() return Effect.fnUntraced(function*() { + yield* SomeElse // the only requirements you can have are the one provided by HttpRouter.HttpRouter.Provided yield* Scope.Scope From f6612124b36eb61f5ef1e53067f7473c642b26f7 Mon Sep 17 00:00:00 2001 From: Patrick Roza Date: Sun, 3 Aug 2025 18:34:33 +0200 Subject: [PATCH 02/17] progress --- packages/infra/src/api/layerUtils.ts | 8 ++-- .../routing/middleware/DynamicMiddleware.ts | 10 ++--- .../routing/middleware/generic-middleware.ts | 1 + .../api/routing/middleware/middleware-api.ts | 39 ++++++++++++++++--- packages/infra/test/controller.test.ts | 7 ++-- 5 files changed, 47 insertions(+), 18 deletions(-) diff --git a/packages/infra/src/api/layerUtils.ts b/packages/infra/src/api/layerUtils.ts index 7973e38f6f..9f41a4dfee 100644 --- a/packages/infra/src/api/layerUtils.ts +++ b/packages/infra/src/api/layerUtils.ts @@ -1,22 +1,22 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ -import { Context, Effect, type Layer, type NonEmptyReadonlyArray, Option } from "effect-app" +import { Context, Effect, type Layer, Option } from "effect-app" import { InfraLogger } from "../logger.js" export namespace LayerUtils { export type GetLayersSuccess> = Layers extends - NonEmptyReadonlyArray ? { + ReadonlyArray ? { [k in keyof Layers]: Layer.Layer.Success }[number] : never export type GetLayersContext> = Layers extends - NonEmptyReadonlyArray ? { + ReadonlyArray ? { [k in keyof Layers]: Layer.Layer.Context }[number] : never export type GetLayersError> = Layers extends - NonEmptyReadonlyArray ? { + ReadonlyArray ? { [k in keyof Layers]: Layer.Layer.Error }[number] : never diff --git a/packages/infra/src/api/routing/middleware/DynamicMiddleware.ts b/packages/infra/src/api/routing/middleware/DynamicMiddleware.ts index 1cb77084b7..d69ac3bbac 100644 --- a/packages/infra/src/api/routing/middleware/DynamicMiddleware.ts +++ b/packages/infra/src/api/routing/middleware/DynamicMiddleware.ts @@ -94,10 +94,10 @@ export type RequestContextMapProvider, // what services will the middleware provide dynamically to the next, or raise errors. DynamicMiddlewareProviders extends RequestContextMapProvider, // how to resolve the dynamic middleware - GenericMiddlewareProviders extends NonEmptyReadonlyArray, + GenericMiddlewareProviders extends ReadonlyArray, MakeMiddlewareE, // what the middleware construction can fail with MakeMiddlewareR, // what the middleware requires to be constructed - MiddlewareDependencies extends NonEmptyReadonlyArray // layers provided for the middleware to be constructed + MiddlewareDependencies extends ReadonlyArray // layers provided for the middleware to be constructed > { /* dynamic middlewares to be applied based on Request Configuration */ dynamicMiddlewares: DynamicMiddlewareProviders @@ -170,8 +170,8 @@ export const makeMiddleware = >() => < RequestContextProviders extends RequestContextMapProvider, // how to resolve the dynamic middleware - GenericMiddlewareProviders extends NonEmptyReadonlyArray, - MiddlewareDependencies extends NonEmptyReadonlyArray, // layers provided for the middlware to be constructed + GenericMiddlewareProviders extends ReadonlyArray, + MiddlewareDependencies extends ReadonlyArray, // layers provided for the middlware to be constructed MakeMiddlewareE = never, // what the middleware construction can fail with MakeMiddlewareR = never // what the middlware requires to be constructed >( @@ -295,7 +295,7 @@ export const makeMiddlewareBasic = < RequestContextMap extends Record, RequestContextProviders extends RequestContextMapProvider, // how to resolve the dynamic middleware - GenericMiddlewareProviders extends NonEmptyReadonlyArray + GenericMiddlewareProviders extends ReadonlyArray >( make: MiddlewareMake< RequestContextMap, diff --git a/packages/infra/src/api/routing/middleware/generic-middleware.ts b/packages/infra/src/api/routing/middleware/generic-middleware.ts index 40497fc9c2..52801c6bdd 100644 --- a/packages/infra/src/api/routing/middleware/generic-middleware.ts +++ b/packages/infra/src/api/routing/middleware/generic-middleware.ts @@ -39,6 +39,7 @@ export type DynamicMiddlewareMaker = Exclude> | Required export type Provided = T extends TagClassAny ? T extends { provides: Context.Tag } ? Context.Tag.Identifier : T extends { provides: ContextRepr } ? ContextRepr.Identifier diff --git a/packages/infra/src/api/routing/middleware/middleware-api.ts b/packages/infra/src/api/routing/middleware/middleware-api.ts index 676ea0d050..c43ce513d4 100644 --- a/packages/infra/src/api/routing/middleware/middleware-api.ts +++ b/packages/infra/src/api/routing/middleware/middleware-api.ts @@ -13,10 +13,32 @@ type DynamicMiddlewareMakerrsss< RequestContext extends Record, Provided extends keyof RequestContext, Middlewares extends NonEmptyReadonlyArray, - DynamicMiddlewareProviders extends RequestContextMapProvider -> = keyof Omit extends never - ? ReturnType> + DynamicMiddlewareProviders extends RequestContextMapProvider, + // out MiddlewareR = never + MiddlewareR = never +> = keyof Omit extends never ? + & ReturnType> + & { + middleware: ( + mw: MW + ) => DynamicMiddlewareMakerrsss< + RequestContext, + Provided, + [...Middlewares, MW], + DynamicMiddlewareProviders, + GenericMiddlewareMaker.ApplyServices + > + } : { + middleware: ( + mw: MW + ) => DynamicMiddlewareMakerrsss< + RequestContext, + Provided, + Middlewares, + DynamicMiddlewareProviders, + GenericMiddlewareMaker.ApplyServices + > addDynamicMiddleware: >>( ...middlewares: MW ) => DynamicMiddlewareMakerrsss< @@ -26,8 +48,9 @@ type DynamicMiddlewareMakerrsss< & DynamicMiddlewareProviders & { [K in keyof MW as MW[K] extends DynamicMiddlewareMaker ? MW[K]["dynamic"]["key"] : never]: MW[K] - } - > + }, + MiddlewareR + > // GenericMiddlewareMaker.ApplyServices } export const makeNewMiddleware: < @@ -38,12 +61,16 @@ export const makeNewMiddleware: < const dynamicMiddlewares: Record = {} as any const make = makeMiddleware() const it = { + middleware: (mw: MW) => { + genericMiddlewares = [...genericMiddlewares, mw] as any + return Object.assign(make({ genericMiddlewares, dynamicMiddlewares }), it) + }, addDynamicMiddleware: (...middlewares: any[]) => { for (const a of middlewares) { console.log("Adding dynamic middleware", a, a.dynamic, Object.keys(a)) dynamicMiddlewares[a.dynamic.key] = a } - return Object.assign(make({ genericMiddlewares: genericMiddlewares as any, dynamicMiddlewares }), it) + return Object.assign(make({ genericMiddlewares, dynamicMiddlewares }), it) } } return it as any diff --git a/packages/infra/test/controller.test.ts b/packages/infra/test/controller.test.ts index 558e90745f..2e3b63bbb2 100644 --- a/packages/infra/test/controller.test.ts +++ b/packages/infra/test/controller.test.ts @@ -211,8 +211,9 @@ export const middleware3 = makeNewMiddleware()( ) .addDynamicMiddleware(AllowAnonymous, RequireRoles) .addDynamicMiddleware(Test) +// .middleware(BogusMiddleware) -expectTypeOf(middleware3).toEqualTypeOf() +// expectTypeOf(middleware3).toExtend() export type RequestConfig = { /** Disable authentication requirement */ @@ -362,7 +363,7 @@ it("sorts based on requirements", () => { // eslint-disable-next-line unused-imports/no-unused-vars const matched = matchAll({ router }) -expectTypeOf({} as Layer.Context).toEqualTypeOf() +expectTypeOf({} as Layer.Context).toEqualTypeOf() type makeContext = MakeContext expectTypeOf({} as MakeErrors).toEqualTypeOf() @@ -434,7 +435,7 @@ const router2 = r2.Router(Something)({ // eslint-disable-next-line unused-imports/no-unused-vars const matched2 = matchAll({ router: router2 }) -expectTypeOf({} as Layer.Context).toEqualTypeOf() +expectTypeOf({} as Layer.Context).toEqualTypeOf() type makeContext2 = MakeContext expectTypeOf({} as MakeErrors).toEqualTypeOf() From 755696dd4bf61299cd12ae1600239facf43d3dce Mon Sep 17 00:00:00 2001 From: Patrick Roza Date: Sun, 3 Aug 2025 18:55:32 +0200 Subject: [PATCH 03/17] yay --- .../routing/middleware/DynamicMiddleware.ts | 5 +- .../api/routing/middleware/middleware-api.ts | 4 +- packages/infra/test/requires.test.ts | 39 ++++++++ packages/infra/test/requires.ts | 93 +++++++++++++++++++ 4 files changed, 137 insertions(+), 4 deletions(-) create mode 100644 packages/infra/test/requires.test.ts create mode 100644 packages/infra/test/requires.ts diff --git a/packages/infra/src/api/routing/middleware/DynamicMiddleware.ts b/packages/infra/src/api/routing/middleware/DynamicMiddleware.ts index d69ac3bbac..c9af702dbf 100644 --- a/packages/infra/src/api/routing/middleware/DynamicMiddleware.ts +++ b/packages/infra/src/api/routing/middleware/DynamicMiddleware.ts @@ -528,11 +528,9 @@ export declare namespace TagClass { */ export type Requires = Options extends { readonly requires: Context.Tag - readonly optional?: false } ? Context.Tag.Identifier : Options extends { readonly requires: ContextRepr - readonly optional?: false } ? ContextRepr.Identifier : never @@ -606,6 +604,9 @@ export declare namespace TagClass { readonly provides: Options extends { readonly provides: Context.Tag } ? Options["provides"] : Options extends { readonly provides: ContextRepr } ? Options["provides"] : undefined + readonly requires: Options extends { readonly requires: Context.Tag } ? Options["requires"] + : Options extends { readonly requires: ContextRepr } ? Options["requires"] + : undefined readonly dynamic: Options extends RpcOptionsDynamic ? Options["dynamic"] : undefined readonly requiredForClient: RequiredForClient diff --git a/packages/infra/src/api/routing/middleware/middleware-api.ts b/packages/infra/src/api/routing/middleware/middleware-api.ts index c43ce513d4..4255b5c3fa 100644 --- a/packages/infra/src/api/routing/middleware/middleware-api.ts +++ b/packages/infra/src/api/routing/middleware/middleware-api.ts @@ -12,7 +12,7 @@ export const contextMap = () => , Provided extends keyof RequestContext, - Middlewares extends NonEmptyReadonlyArray, + Middlewares extends ReadonlyArray, DynamicMiddlewareProviders extends RequestContextMapProvider, // out MiddlewareR = never MiddlewareR = never @@ -55,7 +55,7 @@ type DynamicMiddlewareMakerrsss< export const makeNewMiddleware: < RequestContextMap extends Record ->() => >( +>() => >( ...genericMiddlewares: Middlewares ) => DynamicMiddlewareMakerrsss = () => (...genericMiddlewares) => { const dynamicMiddlewares: Record = {} as any diff --git a/packages/infra/test/requires.test.ts b/packages/infra/test/requires.test.ts new file mode 100644 index 0000000000..57f92b6dc4 --- /dev/null +++ b/packages/infra/test/requires.test.ts @@ -0,0 +1,39 @@ +import { Effect } from "effect-app" +import { Middleware } from "../src/api/routing.js" +import { SomeService } from "./query.test.js" +import { makeNewMiddleware } from "./requires.js" + +export class SomeMiddleware extends Middleware.Tag()("SomeMiddleware", { + provides: SomeService, + wrap: true +})({ + effect: Effect.gen(function*() { + // yield* Effect.context<"test-dep">() + return ({ next }) => + Effect.gen(function*() { + // yield* Effect.context<"test-dep2">() + return yield* next.pipe(Effect.provideService(SomeService, null as any)) + }) + }) +}) { +} + +export class RequiresSomeMiddleware extends Middleware.Tag()("RequiresSomeMiddleware", { + requires: SomeService, + wrap: true +})({ + effect: Effect.gen(function*() { + // yield* Effect.context<"test-dep">() + return ({ next }) => + Effect.gen(function*() { + yield* SomeService + // yield* Effect.context<"test-dep2">() + return yield* next + }) + }) +}) { +} + +export const middleware3 = makeNewMiddleware<{}>()() + .middleware(RequiresSomeMiddleware) + .middleware(SomeMiddleware) diff --git a/packages/infra/test/requires.ts b/packages/infra/test/requires.ts new file mode 100644 index 0000000000..ea8479ceb1 --- /dev/null +++ b/packages/infra/test/requires.ts @@ -0,0 +1,93 @@ +import { type NonEmptyReadonlyArray } from "effect-app" +import { type RPCContextMap } from "effect-app/client" +import { type DynamicMiddlewareMaker, type GenericMiddlewareMaker, makeMiddleware, type RequestContextMapProvider } from "../src/api/routing.js" + +export interface MiddlewareM< + RequestContext extends Record, + Provided extends keyof RequestContext, + Middlewares extends ReadonlyArray, + DynamicMiddlewareProviders extends RequestContextMapProvider, + // out MiddlewareR = never + MiddlewareR = never +> { + middleware: ( + mw: MW + ) => DynamicMiddlewareMakerrsss< + RequestContext, + Provided, + [...Middlewares, MW], + DynamicMiddlewareProviders, + GenericMiddlewareMaker.ApplyServices + > +} + +interface Dynamic< + RequestContext extends Record, + Provided extends keyof RequestContext, + Middlewares extends ReadonlyArray, + DynamicMiddlewareProviders extends RequestContextMapProvider, + // out MiddlewareR = never + MiddlewareR = never +> extends + MiddlewareM< + RequestContext, + Provided, + Middlewares, + DynamicMiddlewareProviders, + MiddlewareR + > +{ + addDynamicMiddleware: >>( + ...middlewares: MW + ) => DynamicMiddlewareMakerrsss< + RequestContext, + Provided | MW[number]["dynamic"]["key"], + Middlewares, + & DynamicMiddlewareProviders + & { + [K in keyof MW as MW[K] extends DynamicMiddlewareMaker ? MW[K]["dynamic"]["key"] : never]: MW[K] + }, + MiddlewareR + > // GenericMiddlewareMaker.ApplyServices +} + +type DynamicMiddlewareMakerrsss< + RequestContext extends Record, + Provided extends keyof RequestContext, + Middlewares extends ReadonlyArray, + DynamicMiddlewareProviders extends RequestContextMapProvider, + // out MiddlewareR = never + MiddlewareR = never +> = keyof Omit extends never ? + & "happy" + & MiddlewareM< + RequestContext, + Provided, + Middlewares, + DynamicMiddlewareProviders, + MiddlewareR + > + : Dynamic + +export const makeNewMiddleware: < + RequestContextMap extends Record +>() => >( + ...genericMiddlewares: Middlewares +) => DynamicMiddlewareMakerrsss = () => (...genericMiddlewares) => { + const dynamicMiddlewares: Record = {} as any + const make = makeMiddleware() + const it = { + middleware: (mw: MW) => { + genericMiddlewares = [...genericMiddlewares, mw] as any + return Object.assign(make({ genericMiddlewares, dynamicMiddlewares }), it) + }, + addDynamicMiddleware: (...middlewares: any[]) => { + for (const a of middlewares) { + console.log("Adding dynamic middleware", a, a.dynamic, Object.keys(a)) + dynamicMiddlewares[a.dynamic.key] = a + } + return Object.assign(make({ genericMiddlewares, dynamicMiddlewares }), it) + } + } + return it as any +} From ba5e8bd55bb96b88fc7668815e58fa5f4f538b55 Mon Sep 17 00:00:00 2001 From: Patrick Roza Date: Sun, 3 Aug 2025 18:55:35 +0200 Subject: [PATCH 04/17] wtf --- .vscode/settings.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index fdc73f3d26..14cf11524a 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -7,7 +7,7 @@ // would prefer on in frontend, so might want to use a specialised frontend config.. "volar.updateImportsOnFileMove.enabled": false, "typescript.preferences.includePackageJsonAutoImports": "on", - "typescript.preferences.autoImportFileExcludePatterns": [w + "typescript.preferences.autoImportFileExcludePatterns": [ "node_modules/@sentry/node", "node_modules/vitest/dist", "node_modules/@azure/cosmos", From 83bf336a2c42486de38f5ca72ad1f8566993ccea Mon Sep 17 00:00:00 2001 From: Patrick Roza Date: Sun, 3 Aug 2025 18:59:54 +0200 Subject: [PATCH 05/17] nice --- packages/infra/test/requires.ts | 33 +++++++++++++++++++-------------- 1 file changed, 19 insertions(+), 14 deletions(-) diff --git a/packages/infra/test/requires.ts b/packages/infra/test/requires.ts index ea8479ceb1..1f7beffa6b 100644 --- a/packages/infra/test/requires.ts +++ b/packages/infra/test/requires.ts @@ -1,6 +1,6 @@ import { type NonEmptyReadonlyArray } from "effect-app" import { type RPCContextMap } from "effect-app/client" -import { type DynamicMiddlewareMaker, type GenericMiddlewareMaker, makeMiddleware, type RequestContextMapProvider } from "../src/api/routing.js" +import { type DynamicMiddlewareMaker, type GenericMiddlewareMaker, makeMiddleware, type makeMiddlewareBasic, type RequestContextMapProvider } from "../src/api/routing.js" export interface MiddlewareM< RequestContext extends Record, @@ -21,13 +21,12 @@ export interface MiddlewareM< > } -interface Dynamic< +export interface Dynamic< RequestContext extends Record, Provided extends keyof RequestContext, Middlewares extends ReadonlyArray, DynamicMiddlewareProviders extends RequestContextMapProvider, - // out MiddlewareR = never - MiddlewareR = never + out MiddlewareR = never > extends MiddlewareM< RequestContext, @@ -56,17 +55,23 @@ type DynamicMiddlewareMakerrsss< Provided extends keyof RequestContext, Middlewares extends ReadonlyArray, DynamicMiddlewareProviders extends RequestContextMapProvider, - // out MiddlewareR = never MiddlewareR = never -> = keyof Omit extends never ? - & "happy" - & MiddlewareM< - RequestContext, - Provided, - Middlewares, - DynamicMiddlewareProviders, - MiddlewareR - > +> = keyof Omit extends never ? [MiddlewareR] extends [never] ? + & ReturnType> + & MiddlewareM< + RequestContext, + Provided, + Middlewares, + DynamicMiddlewareProviders, + MiddlewareR + > + : MiddlewareM< + RequestContext, + Provided, + Middlewares, + DynamicMiddlewareProviders, + MiddlewareR + > : Dynamic export const makeNewMiddleware: < From 940c4e62889dcca1f0c12d107d0a104d0b763304 Mon Sep 17 00:00:00 2001 From: Patrick Roza Date: Sun, 3 Aug 2025 19:18:46 +0200 Subject: [PATCH 06/17] cleanup fixtures --- packages/infra/src/api/routing.ts | 10 +- .../src/api/routing/middleware/middleware.ts | 12 ++- packages/infra/test/contextProvider.test.ts | 3 +- packages/infra/test/controller.test.ts | 94 +------------------ packages/infra/test/fixtures.ts | 83 ++++++++++++++++ packages/infra/test/query.test.ts | 6 +- packages/infra/test/requires.test.ts | 26 +++-- packages/infra/test/requires.ts | 9 +- 8 files changed, 125 insertions(+), 118 deletions(-) create mode 100644 packages/infra/test/fixtures.ts diff --git a/packages/infra/src/api/routing.ts b/packages/infra/src/api/routing.ts index d54bcb563d..587f08f952 100644 --- a/packages/infra/src/api/routing.ts +++ b/packages/infra/src/api/routing.ts @@ -4,7 +4,7 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ import { determineMethod, isCommand } from "@effect-app/infra/api/routing/utils" import { Rpc, RpcGroup, RpcServer } from "@effect/rpc" -import { type Array, Duration, Effect, Layer, type NonEmptyReadonlyArray, Predicate, Request, S, Schedule, Schema, type Scope } from "effect-app" +import { type Array, Effect, Layer, type NonEmptyReadonlyArray, Predicate, S, Schedule, Schema, type Scope } from "effect-app" import type { GetEffectContext, GetEffectError, RPCContextMap } from "effect-app/client/req" import { type HttpHeaders, HttpRouter } from "effect-app/http" import { typedKeysOf, typedValuesOf } from "effect-app/utils" @@ -819,11 +819,3 @@ export type MakeHandlers> = Make exte * @since 3.9.0 */ export type MakeDepsOut = Contravariant.Type[Layer.LayerTypeId]["_ROut"]> - -export const RequestCacheLayers = Layer.mergeAll( - Layer.setRequestCache( - Request.makeCache({ capacity: 500, timeToLive: Duration.hours(8) }) - ), - Layer.setRequestCaching(true), - Layer.setRequestBatching(true) -) diff --git a/packages/infra/src/api/routing/middleware/middleware.ts b/packages/infra/src/api/routing/middleware/middleware.ts index 4d2d0cd862..93716ba37b 100644 --- a/packages/infra/src/api/routing/middleware/middleware.ts +++ b/packages/infra/src/api/routing/middleware/middleware.ts @@ -1,13 +1,21 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ -import { Cause, Context, Effect, ParseResult } from "effect-app" +import { Cause, Context, Duration, Effect, Layer, ParseResult, Request } from "effect-app" import { pretty } from "effect-app/utils" import { logError, reportError } from "../../../errorReporter.js" import { InfraLogger } from "../../../logger.js" -import { RequestCacheLayers, Tag } from "../../routing.js" +import { Tag } from "../middleware.js" const logRequestError = logError("Request") const reportRequestError = reportError("Request") +export const RequestCacheLayers = Layer.mergeAll( + Layer.setRequestCache( + Request.makeCache({ capacity: 500, timeToLive: Duration.hours(8) }) + ), + Layer.setRequestCaching(true), + Layer.setRequestBatching(true) +) + export class DevMode extends Context.Reference()("DevMode", { defaultValue: () => false }) {} export class RequestCacheMiddleware extends Tag()("RequestCacheMiddleware", { wrap: true })({ diff --git a/packages/infra/test/contextProvider.test.ts b/packages/infra/test/contextProvider.test.ts index 7391d77a19..8ef34f012f 100644 --- a/packages/infra/test/contextProvider.test.ts +++ b/packages/infra/test/contextProvider.test.ts @@ -3,8 +3,7 @@ import { expectTypeOf } from "@effect/vitest" import { Context, Effect, Scope } from "effect-app" import { ContextProvider, mergeContextProviders, MergedContextProvider } from "../src/api/routing.js" -import { CustomError1, Some, SomeElse } from "./controller.test.js" -import { SomeService } from "./query.test.js" +import { CustomError1, Some, SomeElse, SomeService } from "./fixtures.js" // @effect-diagnostics-next-line missingEffectServiceDependency:off class MyContextProvider extends Effect.Service()("MyContextProvider", { diff --git a/packages/infra/test/controller.test.ts b/packages/infra/test/controller.test.ts index 2e3b63bbb2..167dd935bf 100644 --- a/packages/infra/test/controller.test.ts +++ b/packages/infra/test/controller.test.ts @@ -1,36 +1,12 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ /* eslint-disable @typescript-eslint/no-unsafe-member-access */ import { type MakeContext, type MakeErrors, makeRouter } from "@effect-app/infra/api/routing" -import type { RequestContext } from "@effect-app/infra/RequestContext" import { expect, expectTypeOf, it } from "@effect/vitest" -import { type Array, Context, Effect, Layer, Option, S, Scope } from "effect-app" -import { InvalidStateError, makeRpcClient, type RPCContextMap, UnauthorizedError } from "effect-app/client" -import { Class, TaggedError } from "effect-app/Schema" -import { contextMap, DefaultGenericMiddlewares, implementMiddleware, makeMiddleware, makeNewMiddleware, Middleware, Tag } from "../src/api/routing/middleware.js" +import { Context, Effect, Layer, S, Scope } from "effect-app" +import { InvalidStateError, makeRpcClient, NotLoggedInError, UnauthorizedError } from "effect-app/client" +import { DefaultGenericMiddlewares, implementMiddleware, makeMiddleware, makeNewMiddleware, Middleware, Tag } from "../src/api/routing/middleware.js" import { sort } from "../src/api/routing/tsort.js" -import { SomeService } from "./query.test.js" - -export class UserProfile extends Context.assignTag("UserProfile")( - Class("UserProfile")({ - id: S.String, - roles: S.Array(S.String) - }) -) { -} - -export class NotLoggedInError extends TaggedError()("NotLoggedInError", { - message: S.String -}) {} - -export class CustomError1 extends TaggedError()("CustomError1", {}) {} -export class CustomError2 extends TaggedError()("CustomError1", {}) {} - -export interface CTX { - context: RequestContext -} - -export class Some extends Context.TagMakeId("Some", Effect.succeed({ a: 1 }))() {} -export class SomeElse extends Context.TagMakeId("SomeElse", Effect.succeed({ b: 2 }))() {} +import { AllowAnonymous, CustomError1, type RequestContextMap, RequireRoles, Some, SomeElse, SomeService, Test } from "./fixtures.js" // @effect-diagnostics-next-line missingEffectServiceDependency:off class MyContextProvider extends Middleware.Tag()("MyContextProvider", { @@ -76,71 +52,9 @@ class MyContextProvider2 extends Middleware.Tag()("MyContext // -export type RequestContextMap = { - allowAnonymous: RPCContextMap.Inverted - requireRoles: RPCContextMap.Custom> - test: RPCContextMap -} - const Str = Context.GenericTag<"str", "str">("str") const Str2 = Context.GenericTag<"str2", "str">("str2") -class AllowAnonymous extends Middleware.Tag()("AllowAnonymous", { - dynamic: contextMap()("allowAnonymous") -})({ - effect: Effect.gen(function*() { - return Effect.fnUntraced( - function*({ config, headers }) { - yield* Scope.Scope // provided by HttpRouter.HttpRouter.Provided - const isLoggedIn = !!headers["x-user"] - if (!isLoggedIn) { - if (!config.allowAnonymous) { - return yield* new NotLoggedInError({ message: "Not logged in" }) - } - return Option.none() - } - return Option.some(Context.make( - UserProfile, - { id: "whatever", roles: ["user", "manager"] } - )) - } - ) - }) -}) { -} - -// @effect-diagnostics-next-line missingEffectServiceDependency:off -class RequireRoles extends Middleware.Tag()("RequireRoles", { - dynamic: contextMap()("requireRoles"), - // had to move this in here, because once you put it manually as a readonly static property on the class, - // there's a weird issue where the fluent api stops behaving properly after adding this middleware via `addDynamicMiddleware` - dependsOn: [AllowAnonymous] -})({ - effect: Effect.gen(function*() { - yield* Some - return Effect.fnUntraced( - function*({ config }) { - // we don't know if the service will be provided or not, so we use option.. - const userProfile = yield* Effect.serviceOption(UserProfile) - const { requireRoles } = config - if (requireRoles && !userProfile.value?.roles?.some((role) => requireRoles.includes(role))) { - return yield* new UnauthorizedError({ message: "don't have the right roles" }) - } - return Option.none>() - } - ) - }) -}) { -} - -class Test extends Middleware.Tag()("Test", { dynamic: contextMap()("test") })({ - effect: Effect.gen(function*() { - return Effect.fn(function*() { - return Option.none>() - }) - }) -}) {} - export class BogusMiddleware extends Tag()("BogusMiddleware", { provides: SomeService, wrap: true diff --git a/packages/infra/test/fixtures.ts b/packages/infra/test/fixtures.ts new file mode 100644 index 0000000000..4c69a9e96e --- /dev/null +++ b/packages/infra/test/fixtures.ts @@ -0,0 +1,83 @@ +import { Context, Effect, Option, S, Scope } from "effect-app" +import { NotLoggedInError, type RPCContextMap, UnauthorizedError } from "effect-app/client" +import { TaggedError } from "effect-app/Schema" +import { contextMap, Middleware } from "../src/api/routing.js" + +export class UserProfile extends Context.assignTag("UserProfile")( + S.Class("UserProfile")({ + id: S.String, + roles: S.Array(S.String) + }) +) { +} + +export class Some extends Context.TagMakeId("Some", Effect.succeed({ a: 1 }))() {} +export class SomeElse extends Context.TagMakeId("SomeElse", Effect.succeed({ b: 2 }))() {} + +export type RequestContextMap = { + allowAnonymous: RPCContextMap.Inverted + requireRoles: RPCContextMap.Custom> + test: RPCContextMap +} + +export class AllowAnonymous extends Middleware.Tag()("AllowAnonymous", { + dynamic: contextMap()("allowAnonymous") +})({ + effect: Effect.gen(function*() { + return Effect.fnUntraced( + function*({ config, headers }) { + yield* Scope.Scope // provided by HttpRouter.HttpRouter.Provided + const isLoggedIn = !!headers["x-user"] + if (!isLoggedIn) { + if (!config.allowAnonymous) { + return yield* new NotLoggedInError({ message: "Not logged in" }) + } + return Option.none() + } + return Option.some(Context.make( + UserProfile, + { id: "whatever", roles: ["user", "manager"] } + )) + } + ) + }) +}) { +} + +// @effect-diagnostics-next-line missingEffectServiceDependency:off +export class RequireRoles extends Middleware.Tag()("RequireRoles", { + dynamic: contextMap()("requireRoles"), + // had to move this in here, because once you put it manually as a readonly static property on the class, + // there's a weird issue where the fluent api stops behaving properly after adding this middleware via `addDynamicMiddleware` + dependsOn: [AllowAnonymous] +})({ + effect: Effect.gen(function*() { + yield* Some + return Effect.fnUntraced( + function*({ config }) { + // we don't know if the service will be provided or not, so we use option.. + const userProfile = yield* Effect.serviceOption(UserProfile) + const { requireRoles } = config + if (requireRoles && !userProfile.value?.roles?.some((role) => requireRoles.includes(role))) { + return yield* new UnauthorizedError({ message: "don't have the right roles" }) + } + return Option.none>() + } + ) + }) +}) { +} + +export class Test extends Middleware.Tag()("Test", { dynamic: contextMap()("test") })({ + effect: Effect.gen(function*() { + return Effect.fn(function*() { + return Option.none>() + }) + }) +}) {} + +export class CustomError1 extends TaggedError()("CustomError1", {}) {} +export class CustomError2 extends TaggedError()("CustomError1", {}) {} + +const MakeSomeService = Effect.succeed({ a: 1 }) +export class SomeService extends Context.TagMakeId("SomeService", MakeSomeService)() {} diff --git a/packages/infra/test/query.test.ts b/packages/infra/test/query.test.ts index 245fbc094d..233839b5d9 100644 --- a/packages/infra/test/query.test.ts +++ b/packages/infra/test/query.test.ts @@ -1,13 +1,14 @@ /* eslint-disable unused-imports/no-unused-vars */ /* eslint-disable @typescript-eslint/no-empty-object-type */ /* eslint-disable @typescript-eslint/no-explicit-any */ -import { Context, Effect, flow, Layer, Option, pipe, S, Struct } from "effect-app" +import { Effect, flow, Layer, Option, pipe, S, Struct } from "effect-app" import { inspect } from "util" import { expect, expectTypeOf, it } from "vitest" import { setupRequestContextFromCurrent } from "../src/api/setupRequest.js" import { and, count, make, one, or, order, page, project, type QueryEnd, type QueryProjection, type QueryWhere, toFilter, where } from "../src/Model/query.js" import { makeRepo } from "../src/Model/Repository.js" import { memFilter, MemoryStoreLive } from "../src/Store/Memory.js" +import { SomeService } from "./fixtures.js" const str = S.Struct({ _tag: S.Literal("string"), value: S.String }) const num = S.Struct({ _tag: S.Literal("number"), value: S.Number }) @@ -25,9 +26,6 @@ export declare namespace Something { export interface Encoded extends S.Schema.Encoded {} } -const MakeSomeService = Effect.succeed({ a: 1 }) -export class SomeService extends Context.TagMakeId("SomeService", MakeSomeService)() {} - const q = make() .pipe( // provided automatically inside Repo.q2() where("displayName", "Verona"), diff --git a/packages/infra/test/requires.test.ts b/packages/infra/test/requires.test.ts index 57f92b6dc4..f8f3d1597a 100644 --- a/packages/infra/test/requires.test.ts +++ b/packages/infra/test/requires.test.ts @@ -1,6 +1,7 @@ -import { Effect } from "effect-app" +import { it } from "@effect/vitest" +import { Effect, S } from "effect-app" import { Middleware } from "../src/api/routing.js" -import { SomeService } from "./query.test.js" +import { AllowAnonymous, type RequestContextMap, RequireRoles, SomeService, Test } from "./fixtures.js" import { makeNewMiddleware } from "./requires.js" export class SomeMiddleware extends Middleware.Tag()("SomeMiddleware", { @@ -12,7 +13,7 @@ export class SomeMiddleware extends Middleware.Tag()("SomeMiddle return ({ next }) => Effect.gen(function*() { // yield* Effect.context<"test-dep2">() - return yield* next.pipe(Effect.provideService(SomeService, null as any)) + return yield* next.pipe(Effect.provideService(SomeService, new SomeService({ a: 1 }))) }) }) }) { @@ -34,6 +35,19 @@ export class RequiresSomeMiddleware extends Middleware.Tag()() - .middleware(RequiresSomeMiddleware) - .middleware(SomeMiddleware) +it("requires gets enforced", async () => { + const middleware3 = makeNewMiddleware() + .middleware(RequiresSomeMiddleware) + .middleware(SomeMiddleware) + .addDynamicMiddleware(AllowAnonymous) + .addDynamicMiddleware(RequireRoles) + .addDynamicMiddleware(Test) + + await Effect + .gen(function*() { + const mw = yield* middleware3 + const mwM = mw.effect(Object.assign({}, S.Any, { config: {} }), (req) => Effect.void, "some-module") + const v = yield* mwM({}, {}) + }) + .pipe(Effect.provide(middleware3.Default), Effect.runPromise) +}) diff --git a/packages/infra/test/requires.ts b/packages/infra/test/requires.ts index 1f7beffa6b..2693dfaf65 100644 --- a/packages/infra/test/requires.ts +++ b/packages/infra/test/requires.ts @@ -54,7 +54,7 @@ type DynamicMiddlewareMakerrsss< RequestContext extends Record, Provided extends keyof RequestContext, Middlewares extends ReadonlyArray, - DynamicMiddlewareProviders extends RequestContextMapProvider, + DynamicMiddlewareProviders extends RequestContextMapProvider = never, MiddlewareR = never > = keyof Omit extends never ? [MiddlewareR] extends [never] ? & ReturnType> @@ -76,14 +76,13 @@ type DynamicMiddlewareMakerrsss< export const makeNewMiddleware: < RequestContextMap extends Record ->() => >( - ...genericMiddlewares: Middlewares -) => DynamicMiddlewareMakerrsss = () => (...genericMiddlewares) => { +>() => DynamicMiddlewareMakerrsss = () => { const dynamicMiddlewares: Record = {} as any const make = makeMiddleware() + let genericMiddlewares: GenericMiddlewareMaker[] = [] const it = { middleware: (mw: MW) => { - genericMiddlewares = [...genericMiddlewares, mw] as any + genericMiddlewares = [mw, ...genericMiddlewares] as any return Object.assign(make({ genericMiddlewares, dynamicMiddlewares }), it) }, addDynamicMiddleware: (...middlewares: any[]) => { From 84cdbe6342dc870b39e2b3dede9f65ca777beb84 Mon Sep 17 00:00:00 2001 From: Patrick Roza Date: Sun, 3 Aug 2025 19:25:52 +0200 Subject: [PATCH 07/17] meh --- packages/infra/src/api/layerUtils.ts | 8 ++++---- packages/infra/test/requires.test.ts | 6 +++++- packages/infra/test/requires.ts | 8 ++++---- 3 files changed, 13 insertions(+), 9 deletions(-) diff --git a/packages/infra/src/api/layerUtils.ts b/packages/infra/src/api/layerUtils.ts index 9f41a4dfee..7973e38f6f 100644 --- a/packages/infra/src/api/layerUtils.ts +++ b/packages/infra/src/api/layerUtils.ts @@ -1,22 +1,22 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ -import { Context, Effect, type Layer, Option } from "effect-app" +import { Context, Effect, type Layer, type NonEmptyReadonlyArray, Option } from "effect-app" import { InfraLogger } from "../logger.js" export namespace LayerUtils { export type GetLayersSuccess> = Layers extends - ReadonlyArray ? { + NonEmptyReadonlyArray ? { [k in keyof Layers]: Layer.Layer.Success }[number] : never export type GetLayersContext> = Layers extends - ReadonlyArray ? { + NonEmptyReadonlyArray ? { [k in keyof Layers]: Layer.Layer.Context }[number] : never export type GetLayersError> = Layers extends - ReadonlyArray ? { + NonEmptyReadonlyArray ? { [k in keyof Layers]: Layer.Layer.Error }[number] : never diff --git a/packages/infra/test/requires.test.ts b/packages/infra/test/requires.test.ts index f8f3d1597a..c2c0170992 100644 --- a/packages/infra/test/requires.test.ts +++ b/packages/infra/test/requires.test.ts @@ -49,5 +49,9 @@ it("requires gets enforced", async () => { const mwM = mw.effect(Object.assign({}, S.Any, { config: {} }), (req) => Effect.void, "some-module") const v = yield* mwM({}, {}) }) - .pipe(Effect.provide(middleware3.Default), Effect.runPromise) + .pipe( + Effect.scoped, + Effect.provide(middleware3.Default), + Effect.runPromise + ) }) diff --git a/packages/infra/test/requires.ts b/packages/infra/test/requires.ts index 2693dfaf65..347df4aa8d 100644 --- a/packages/infra/test/requires.ts +++ b/packages/infra/test/requires.ts @@ -52,12 +52,12 @@ export interface Dynamic< type DynamicMiddlewareMakerrsss< RequestContext extends Record, - Provided extends keyof RequestContext, - Middlewares extends ReadonlyArray, + Provided extends keyof RequestContext = never, + Middlewares extends ReadonlyArray = [], DynamicMiddlewareProviders extends RequestContextMapProvider = never, MiddlewareR = never > = keyof Omit extends never ? [MiddlewareR] extends [never] ? - & ReturnType> + & ReturnType> & MiddlewareM< RequestContext, Provided, @@ -76,7 +76,7 @@ type DynamicMiddlewareMakerrsss< export const makeNewMiddleware: < RequestContextMap extends Record ->() => DynamicMiddlewareMakerrsss = () => { +>() => DynamicMiddlewareMakerrsss = () => { const dynamicMiddlewares: Record = {} as any const make = makeMiddleware() let genericMiddlewares: GenericMiddlewareMaker[] = [] From 78250797778bf4ad08a3af85209da1074fa4be97 Mon Sep 17 00:00:00 2001 From: Patrick Roza Date: Sun, 3 Aug 2025 20:21:28 +0200 Subject: [PATCH 08/17] nice --- packages/infra/src/api/layerUtils.ts | 19 +++++-------- .../api/routing/middleware/middleware-api.ts | 2 +- packages/infra/test/layerUtils.test.ts | 19 +++++++++++++ packages/infra/test/requires.test.ts | 13 +++++---- packages/infra/test/requires.ts | 27 ++++++++++++++----- 5 files changed, 55 insertions(+), 25 deletions(-) create mode 100644 packages/infra/test/layerUtils.test.ts diff --git a/packages/infra/src/api/layerUtils.ts b/packages/infra/src/api/layerUtils.ts index 7973e38f6f..2f6cb0e560 100644 --- a/packages/infra/src/api/layerUtils.ts +++ b/packages/infra/src/api/layerUtils.ts @@ -1,24 +1,17 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ -import { Context, Effect, type Layer, type NonEmptyReadonlyArray, Option } from "effect-app" +import { Context, Effect, type Layer, Option } from "effect-app" import { InfraLogger } from "../logger.js" export namespace LayerUtils { - export type GetLayersSuccess> = Layers extends - NonEmptyReadonlyArray ? { - [k in keyof Layers]: Layer.Layer.Success - }[number] + export type GetLayersSuccess = Layers extends ReadonlyArray + ? Layer.Layer.Success : never - export type GetLayersContext> = Layers extends - NonEmptyReadonlyArray ? { - [k in keyof Layers]: Layer.Layer.Context - }[number] + export type GetLayersContext = Layers extends ReadonlyArray + ? Layer.Layer.Context : never - export type GetLayersError> = Layers extends - NonEmptyReadonlyArray ? { - [k in keyof Layers]: Layer.Layer.Error - }[number] + export type GetLayersError = Layers extends ReadonlyArray ? Layer.Layer.Error : never } diff --git a/packages/infra/src/api/routing/middleware/middleware-api.ts b/packages/infra/src/api/routing/middleware/middleware-api.ts index 4255b5c3fa..8d73ad9a23 100644 --- a/packages/infra/src/api/routing/middleware/middleware-api.ts +++ b/packages/infra/src/api/routing/middleware/middleware-api.ts @@ -67,7 +67,7 @@ export const makeNewMiddleware: < }, addDynamicMiddleware: (...middlewares: any[]) => { for (const a of middlewares) { - console.log("Adding dynamic middleware", a, a.dynamic, Object.keys(a)) + console.log("Adding dynamic middleware", a, a.dynamic.key, Object.keys(a)) dynamicMiddlewares[a.dynamic.key] = a } return Object.assign(make({ genericMiddlewares, dynamicMiddlewares }), it) diff --git a/packages/infra/test/layerUtils.test.ts b/packages/infra/test/layerUtils.test.ts new file mode 100644 index 0000000000..32bb01d7fc --- /dev/null +++ b/packages/infra/test/layerUtils.test.ts @@ -0,0 +1,19 @@ +import { it } from "@effect/vitest" +import { type Layer } from "effect" +import { expectTypeOf } from "vitest/index.cjs" +import { type LayerUtils } from "../src/api/layerUtils.js" + +it("works", () => { + type B = (Layer.Layer | Layer.Layer)[] + type C = LayerUtils.GetLayersContext + type CE = LayerUtils.GetLayersError + + expectTypeOf({} as C).toEqualTypeOf<"a" | "b">() + expectTypeOf({} as CE).toEqualTypeOf<"error-a" | "error-b">() + + type B2 = [Layer.Layer, Layer.Layer | Layer.Layer] + type C2 = LayerUtils.GetLayersContext + type CE2 = LayerUtils.GetLayersError + expectTypeOf({} as C2).toEqualTypeOf<"a" | "b" | "c">() + expectTypeOf({} as CE2).toEqualTypeOf<"error-a" | "error-b" | "error-c">() +}) diff --git a/packages/infra/test/requires.test.ts b/packages/infra/test/requires.test.ts index c2c0170992..11354645fd 100644 --- a/packages/infra/test/requires.test.ts +++ b/packages/infra/test/requires.test.ts @@ -1,7 +1,7 @@ -import { it } from "@effect/vitest" -import { Effect, S } from "effect-app" +import { expectTypeOf, it } from "@effect/vitest" +import { Effect, Layer, S } from "effect-app" import { Middleware } from "../src/api/routing.js" -import { AllowAnonymous, type RequestContextMap, RequireRoles, SomeService, Test } from "./fixtures.js" +import { AllowAnonymous, type RequestContextMap, RequireRoles, Some, SomeService, Test } from "./fixtures.js" import { makeNewMiddleware } from "./requires.js" export class SomeMiddleware extends Middleware.Tag()("SomeMiddleware", { @@ -43,15 +43,18 @@ it("requires gets enforced", async () => { .addDynamicMiddleware(RequireRoles) .addDynamicMiddleware(Test) + type LayerContext = Layer.Layer.Context + expectTypeOf({} as LayerContext).toEqualTypeOf() + await Effect .gen(function*() { const mw = yield* middleware3 const mwM = mw.effect(Object.assign({}, S.Any, { config: {} }), (req) => Effect.void, "some-module") - const v = yield* mwM({}, {}) + const v = yield* mwM({}, { "x-user": "test-user" }) }) .pipe( Effect.scoped, - Effect.provide(middleware3.Default), + Effect.provide(middleware3.Default.pipe(Layer.provide(Layer.succeed(Some, new Some({ a: 1 }))))), Effect.runPromise ) }) diff --git a/packages/infra/test/requires.ts b/packages/infra/test/requires.ts index 347df4aa8d..adf3ef6adf 100644 --- a/packages/infra/test/requires.ts +++ b/packages/infra/test/requires.ts @@ -26,7 +26,7 @@ export interface Dynamic< Provided extends keyof RequestContext, Middlewares extends ReadonlyArray, DynamicMiddlewareProviders extends RequestContextMapProvider, - out MiddlewareR = never + out MiddlewareR > extends MiddlewareM< RequestContext, @@ -50,26 +50,41 @@ export interface Dynamic< > // GenericMiddlewareMaker.ApplyServices } +type GetDynamicMiddleware> = T extends + RequestContextMapProvider ? T : never + type DynamicMiddlewareMakerrsss< RequestContext extends Record, Provided extends keyof RequestContext = never, Middlewares extends ReadonlyArray = [], - DynamicMiddlewareProviders extends RequestContextMapProvider = never, + DynamicMiddlewareProviders = unknown, MiddlewareR = never > = keyof Omit extends never ? [MiddlewareR] extends [never] ? - & ReturnType> + & ReturnType< + typeof makeMiddlewareBasic< + RequestContext, + GetDynamicMiddleware, + Middlewares + > + > + // & { + // MiddlewareR: MiddlewareR + // Provided: Provided + // Middlewares: Middlewares + // DynamicMiddlewareProviders: Simplify + // } & MiddlewareM< RequestContext, Provided, Middlewares, - DynamicMiddlewareProviders, + GetDynamicMiddleware, MiddlewareR > : MiddlewareM< RequestContext, Provided, Middlewares, - DynamicMiddlewareProviders, + GetDynamicMiddleware, MiddlewareR > : Dynamic @@ -87,7 +102,7 @@ export const makeNewMiddleware: < }, addDynamicMiddleware: (...middlewares: any[]) => { for (const a of middlewares) { - console.log("Adding dynamic middleware", a, a.dynamic, Object.keys(a)) + console.log("Adding dynamic middleware", a.key, a.dynamic.key) dynamicMiddlewares[a.dynamic.key] = a } return Object.assign(make({ genericMiddlewares, dynamicMiddlewares }), it) From 03b139dfbc6d15b2c7642ac5295fa54ecab2d14b Mon Sep 17 00:00:00 2001 From: Patrick Roza Date: Sun, 3 Aug 2025 20:47:39 +0200 Subject: [PATCH 09/17] fix all --- packages/infra/src/api/layerUtils.ts | 25 ++++++++++++------- .../api/routing/middleware/ContextProvider.ts | 10 ++++---- .../routing/middleware/dynamic-middleware.ts | 2 +- packages/infra/test/requires.ts | 16 ++++++++---- 4 files changed, 33 insertions(+), 20 deletions(-) diff --git a/packages/infra/src/api/layerUtils.ts b/packages/infra/src/api/layerUtils.ts index 2f6cb0e560..12d1e0ca64 100644 --- a/packages/infra/src/api/layerUtils.ts +++ b/packages/infra/src/api/layerUtils.ts @@ -1,18 +1,25 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ -import { Context, Effect, type Layer, Option } from "effect-app" +import { Context, Effect, type Layer, type NonEmptyReadonlyArray, Option } from "effect-app" import { InfraLogger } from "../logger.js" export namespace LayerUtils { - export type GetLayersSuccess = Layers extends ReadonlyArray - ? Layer.Layer.Success - : never + export type GetLayersSuccess> = Layers extends + NonEmptyReadonlyArray ? { + [k in keyof Layers]: Layer.Layer.Success + }[number] + : Layer.Layer.Success - export type GetLayersContext = Layers extends ReadonlyArray - ? Layer.Layer.Context - : never + export type GetLayersContext> = Layers extends + NonEmptyReadonlyArray ? { + [k in keyof Layers]: Layer.Layer.Context + }[number] + : Layer.Layer.Context - export type GetLayersError = Layers extends ReadonlyArray ? Layer.Layer.Error - : never + export type GetLayersError> = Layers extends + NonEmptyReadonlyArray ? { + [k in keyof Layers]: Layer.Layer.Error + }[number] + : Layer.Layer.Error } export type ContextTagWithDefault = diff --git a/packages/infra/src/api/routing/middleware/ContextProvider.ts b/packages/infra/src/api/routing/middleware/ContextProvider.ts index 06d801a07a..b9078a21f4 100644 --- a/packages/infra/src/api/routing/middleware/ContextProvider.ts +++ b/packages/infra/src/api/routing/middleware/ContextProvider.ts @@ -87,9 +87,9 @@ export const mergeContextProviders = < effect: Effect.Effect< Effect.Effect< // we need to merge all contexts into one - Context.Context>>>, + Context.Context>>>, never, - EffectGenUtils.Context> + EffectGenUtils.Context> >, LayerUtils.GetLayersError<{ [K in keyof TDeps]: TDeps[K]["Default"] }>, LayerUtils.GetLayersSuccess<{ [K in keyof TDeps]: TDeps[K]["Default"] }> @@ -174,13 +174,13 @@ export const MergedContextProvider = < ContextProviderId, Effect.Effect< // we need to merge all contexts into one - Context.Context>>>, + Context.Context>>>, never, - EffectGenUtils.Context> + EffectGenUtils.Context> >, LayerUtils.GetLayersError<{ [K in keyof TDeps]: TDeps[K]["Default"] }>, | Exclude< - Tag.Service, + Tag.Identifier, LayerUtils.GetLayersSuccess<{ [K in keyof TDeps]: TDeps[K]["Default"] }> > | LayerUtils.GetLayersContext<{ [K in keyof TDeps]: TDeps[K]["Default"] }> diff --git a/packages/infra/src/api/routing/middleware/dynamic-middleware.ts b/packages/infra/src/api/routing/middleware/dynamic-middleware.ts index 612dcb6ad0..b791ac78ef 100644 --- a/packages/infra/src/api/routing/middleware/dynamic-middleware.ts +++ b/packages/infra/src/api/routing/middleware/dynamic-middleware.ts @@ -86,7 +86,7 @@ export const implementMiddleware = > ) => Effect.Effect< Context.Context>, Effect.Error>>, - Effect.Context>> + Effect.Context>> >, never, Tag.Identifier<{ [K in keyof TI]: TI[K] }[keyof TI]> diff --git a/packages/infra/test/requires.ts b/packages/infra/test/requires.ts index adf3ef6adf..2f3b4848b9 100644 --- a/packages/infra/test/requires.ts +++ b/packages/infra/test/requires.ts @@ -6,7 +6,7 @@ export interface MiddlewareM< RequestContext extends Record, Provided extends keyof RequestContext, Middlewares extends ReadonlyArray, - DynamicMiddlewareProviders extends RequestContextMapProvider, + DynamicMiddlewareProviders, // out MiddlewareR = never MiddlewareR = never > { @@ -25,7 +25,7 @@ export interface Dynamic< RequestContext extends Record, Provided extends keyof RequestContext, Middlewares extends ReadonlyArray, - DynamicMiddlewareProviders extends RequestContextMapProvider, + DynamicMiddlewareProviders, out MiddlewareR > extends MiddlewareM< @@ -77,17 +77,23 @@ type DynamicMiddlewareMakerrsss< RequestContext, Provided, Middlewares, - GetDynamicMiddleware, + DynamicMiddlewareProviders, MiddlewareR > : MiddlewareM< RequestContext, Provided, Middlewares, - GetDynamicMiddleware, + DynamicMiddlewareProviders, + MiddlewareR + > + : Dynamic< + RequestContext, + Provided, + Middlewares, + DynamicMiddlewareProviders, MiddlewareR > - : Dynamic export const makeNewMiddleware: < RequestContextMap extends Record From 339077cd09c595cf371a80a6ebc2ab8510990427 Mon Sep 17 00:00:00 2001 From: Patrick Roza Date: Sun, 3 Aug 2025 21:23:07 +0200 Subject: [PATCH 10/17] neat --- .../routing/middleware/DynamicMiddleware.ts | 5 ++-- packages/infra/test/fixtures.ts | 4 ++- packages/infra/test/requires.test.ts | 26 +++++++++++++++---- packages/infra/test/requires.ts | 25 +++++++++++++----- 4 files changed, 45 insertions(+), 15 deletions(-) diff --git a/packages/infra/src/api/routing/middleware/DynamicMiddleware.ts b/packages/infra/src/api/routing/middleware/DynamicMiddleware.ts index c9af702dbf..dee051625c 100644 --- a/packages/infra/src/api/routing/middleware/DynamicMiddleware.ts +++ b/packages/infra/src/api/routing/middleware/DynamicMiddleware.ts @@ -498,8 +498,9 @@ export interface TagClassDynamicAny { readonly [RpcMiddleware.TypeId]: RpcMiddleware.TypeId - readonly optional: boolean - readonly provides?: Context.Tag | undefined + readonly optional: boolean // TODO: support optional vs required.. with required, we can eliminate the context.. + // readonly provides?: Context.Tag | undefined + readonly requires?: Context.Tag | ContextRepr | undefined readonly failure: Schema.Schema.All readonly requiredForClient: boolean readonly dynamic: RpcDynamic diff --git a/packages/infra/test/fixtures.ts b/packages/infra/test/fixtures.ts index 4c69a9e96e..32ceb78133 100644 --- a/packages/infra/test/fixtures.ts +++ b/packages/infra/test/fixtures.ts @@ -21,11 +21,13 @@ export type RequestContextMap = { } export class AllowAnonymous extends Middleware.Tag()("AllowAnonymous", { - dynamic: contextMap()("allowAnonymous") + dynamic: contextMap()("allowAnonymous"), + requires: SomeElse })({ effect: Effect.gen(function*() { return Effect.fnUntraced( function*({ config, headers }) { + yield* SomeElse yield* Scope.Scope // provided by HttpRouter.HttpRouter.Provided const isLoggedIn = !!headers["x-user"] if (!isLoggedIn) { diff --git a/packages/infra/test/requires.test.ts b/packages/infra/test/requires.test.ts index 11354645fd..1ae828cb24 100644 --- a/packages/infra/test/requires.test.ts +++ b/packages/infra/test/requires.test.ts @@ -1,11 +1,11 @@ import { expectTypeOf, it } from "@effect/vitest" import { Effect, Layer, S } from "effect-app" import { Middleware } from "../src/api/routing.js" -import { AllowAnonymous, type RequestContextMap, RequireRoles, Some, SomeService, Test } from "./fixtures.js" +import { AllowAnonymous, type RequestContextMap, RequireRoles, Some, SomeElse, Test } from "./fixtures.js" import { makeNewMiddleware } from "./requires.js" export class SomeMiddleware extends Middleware.Tag()("SomeMiddleware", { - provides: SomeService, + provides: Some, wrap: true })({ effect: Effect.gen(function*() { @@ -13,21 +13,36 @@ export class SomeMiddleware extends Middleware.Tag()("SomeMiddle return ({ next }) => Effect.gen(function*() { // yield* Effect.context<"test-dep2">() - return yield* next.pipe(Effect.provideService(SomeService, new SomeService({ a: 1 }))) + return yield* next.pipe(Effect.provideService(Some, new Some({ a: 1 }))) + }) + }) +}) { +} + +export class SomeElseMiddleware extends Middleware.Tag()("SomeElseMiddleware", { + provides: SomeElse, + wrap: true +})({ + effect: Effect.gen(function*() { + // yield* Effect.context<"test-dep">() + return ({ next }) => + Effect.gen(function*() { + // yield* Effect.context<"test-dep2">() + return yield* next.pipe(Effect.provideService(SomeElse, new SomeElse({ b: 2 }))) }) }) }) { } export class RequiresSomeMiddleware extends Middleware.Tag()("RequiresSomeMiddleware", { - requires: SomeService, + requires: Some, wrap: true })({ effect: Effect.gen(function*() { // yield* Effect.context<"test-dep">() return ({ next }) => Effect.gen(function*() { - yield* SomeService + yield* Some // yield* Effect.context<"test-dep2">() return yield* next }) @@ -40,6 +55,7 @@ it("requires gets enforced", async () => { .middleware(RequiresSomeMiddleware) .middleware(SomeMiddleware) .addDynamicMiddleware(AllowAnonymous) + .middleware(SomeElseMiddleware) .addDynamicMiddleware(RequireRoles) .addDynamicMiddleware(Test) diff --git a/packages/infra/test/requires.ts b/packages/infra/test/requires.ts index 2f3b4848b9..c87f1e437e 100644 --- a/packages/infra/test/requires.ts +++ b/packages/infra/test/requires.ts @@ -1,4 +1,3 @@ -import { type NonEmptyReadonlyArray } from "effect-app" import { type RPCContextMap } from "effect-app/client" import { type DynamicMiddlewareMaker, type GenericMiddlewareMaker, makeMiddleware, type makeMiddlewareBasic, type RequestContextMapProvider } from "../src/api/routing.js" @@ -36,18 +35,30 @@ export interface Dynamic< MiddlewareR > { - addDynamicMiddleware: >>( - ...middlewares: MW + addDynamicMiddleware: >( + mw: MW ) => DynamicMiddlewareMakerrsss< RequestContext, - Provided | MW[number]["dynamic"]["key"], + Provided | MW["dynamic"]["key"], Middlewares, & DynamicMiddlewareProviders & { - [K in keyof MW as MW[K] extends DynamicMiddlewareMaker ? MW[K]["dynamic"]["key"] : never]: MW[K] + [K in MW["dynamic"]["key"]]: MW }, - MiddlewareR - > // GenericMiddlewareMaker.ApplyServices + GenericMiddlewareMaker.ApplyServices + > + // addDynamicMiddleware: >>( + // ...middlewares: MW + // ) => DynamicMiddlewareMakerrsss< + // RequestContext, + // Provided | MW[number]["dynamic"]["key"], + // Middlewares, + // & DynamicMiddlewareProviders + // & { + // [K in keyof MW as MW[K] extends DynamicMiddlewareMaker ? MW[K]["dynamic"]["key"] : never]: MW[K] + // }, + // MiddlewareR // TODO GenericMiddlewareMaker.ApplyServices + // > // GenericMiddlewareMaker.ApplyServices } type GetDynamicMiddleware> = T extends From 000956c613ba1ef53821c098eb3958b2628dd848 Mon Sep 17 00:00:00 2001 From: Patrick Roza Date: Sun, 3 Aug 2025 21:28:05 +0200 Subject: [PATCH 11/17] unify api --- packages/infra/test/requires.test.ts | 6 ++-- packages/infra/test/requires.ts | 46 ++++++++++++++++------------ 2 files changed, 30 insertions(+), 22 deletions(-) diff --git a/packages/infra/test/requires.test.ts b/packages/infra/test/requires.test.ts index 1ae828cb24..a761e9a060 100644 --- a/packages/infra/test/requires.test.ts +++ b/packages/infra/test/requires.test.ts @@ -54,10 +54,10 @@ it("requires gets enforced", async () => { const middleware3 = makeNewMiddleware() .middleware(RequiresSomeMiddleware) .middleware(SomeMiddleware) - .addDynamicMiddleware(AllowAnonymous) + .middleware(AllowAnonymous) .middleware(SomeElseMiddleware) - .addDynamicMiddleware(RequireRoles) - .addDynamicMiddleware(Test) + .middleware(RequireRoles) + .middleware(Test) type LayerContext = Layer.Layer.Context expectTypeOf({} as LayerContext).toEqualTypeOf() diff --git a/packages/infra/test/requires.ts b/packages/infra/test/requires.ts index c87f1e437e..bcc5aaf436 100644 --- a/packages/infra/test/requires.ts +++ b/packages/infra/test/requires.ts @@ -35,18 +35,25 @@ export interface Dynamic< MiddlewareR > { - addDynamicMiddleware: >( + middleware: | GenericMiddlewareMaker>( mw: MW - ) => DynamicMiddlewareMakerrsss< - RequestContext, - Provided | MW["dynamic"]["key"], - Middlewares, - & DynamicMiddlewareProviders - & { - [K in MW["dynamic"]["key"]]: MW - }, - GenericMiddlewareMaker.ApplyServices - > + ) => MW extends DynamicMiddlewareMaker ? DynamicMiddlewareMakerrsss< + RequestContext, + Provided | MW["dynamic"]["key"], + Middlewares, + & DynamicMiddlewareProviders + & { + [K in MW["dynamic"]["key"]]: MW + }, + GenericMiddlewareMaker.ApplyServices + > + : DynamicMiddlewareMakerrsss< + RequestContext, + Provided, + [...Middlewares, MW], + DynamicMiddlewareProviders, + GenericMiddlewareMaker.ApplyServices + > // addDynamicMiddleware: >>( // ...middlewares: MW // ) => DynamicMiddlewareMakerrsss< @@ -113,14 +120,15 @@ export const makeNewMiddleware: < const make = makeMiddleware() let genericMiddlewares: GenericMiddlewareMaker[] = [] const it = { - middleware: (mw: MW) => { - genericMiddlewares = [mw, ...genericMiddlewares] as any - return Object.assign(make({ genericMiddlewares, dynamicMiddlewares }), it) - }, - addDynamicMiddleware: (...middlewares: any[]) => { - for (const a of middlewares) { - console.log("Adding dynamic middleware", a.key, a.dynamic.key) - dynamicMiddlewares[a.dynamic.key] = a + middleware: (...middlewares: any[]) => { + for (const mw of middlewares) { + if (mw.dynamic) { + console.log("Adding dynamic middleware", mw.key, mw.dynamic.key) + dynamicMiddlewares[mw.dynamic.key] = mw + } else { + console.log("Adding generic middleware", mw.key) + genericMiddlewares = [mw, ...genericMiddlewares] as any + } } return Object.assign(make({ genericMiddlewares, dynamicMiddlewares }), it) } From 8d247f14e9cc14d9584a2252d84b1b79cf2a99c2 Mon Sep 17 00:00:00 2001 From: Patrick Roza Date: Sun, 3 Aug 2025 21:38:17 +0200 Subject: [PATCH 12/17] cleanup --- packages/infra/test/requires.ts | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/packages/infra/test/requires.ts b/packages/infra/test/requires.ts index bcc5aaf436..d7810a7daf 100644 --- a/packages/infra/test/requires.ts +++ b/packages/infra/test/requires.ts @@ -1,3 +1,4 @@ +import { Array, Either } from "effect-app" import { type RPCContextMap } from "effect-app/client" import { type DynamicMiddlewareMaker, type GenericMiddlewareMaker, makeMiddleware, type makeMiddlewareBasic, type RequestContextMapProvider } from "../src/api/routing.js" @@ -116,20 +117,30 @@ type DynamicMiddlewareMakerrsss< export const makeNewMiddleware: < RequestContextMap extends Record >() => DynamicMiddlewareMakerrsss = () => { - const dynamicMiddlewares: Record = {} as any const make = makeMiddleware() - let genericMiddlewares: GenericMiddlewareMaker[] = [] + let capturedMiddlewares: (DynamicMiddlewareMaker | GenericMiddlewareMaker)[] = [] const it = { middleware: (...middlewares: any[]) => { for (const mw of middlewares) { + capturedMiddlewares = [mw, ...capturedMiddlewares] if (mw.dynamic) { console.log("Adding dynamic middleware", mw.key, mw.dynamic.key) - dynamicMiddlewares[mw.dynamic.key] = mw } else { console.log("Adding generic middleware", mw.key) - genericMiddlewares = [mw, ...genericMiddlewares] as any } } + const [genericMiddlewares, dyn] = Array.partitionMap( + capturedMiddlewares, + (mw) => + "dynamic" in mw && mw.dynamic + ? Either.right(mw as DynamicMiddlewareMaker) + : Either.left(mw as GenericMiddlewareMaker) + ) + const dynamicMiddlewares = dyn.reduce( + (prev, cur) => ({ ...prev, [cur.dynamic.key]: cur }), + {} as Record + ) + // TODO: support dynamic and generic intertwined. treat them as one return Object.assign(make({ genericMiddlewares, dynamicMiddlewares }), it) } } From 2250a68d7d4071d4c80c569a8f051682512621d4 Mon Sep 17 00:00:00 2001 From: Patrick Roza Date: Sun, 3 Aug 2025 21:50:48 +0200 Subject: [PATCH 13/17] wicked --- .../routing/middleware/generic-middleware.ts | 3 + .../api/routing/middleware/middleware-api.ts | 166 +++++++++++++----- packages/infra/test/controller.test.ts | 9 +- packages/infra/test/requires.test.ts | 3 +- packages/infra/test/requires.ts | 148 ---------------- 5 files changed, 130 insertions(+), 199 deletions(-) delete mode 100644 packages/infra/test/requires.ts diff --git a/packages/infra/src/api/routing/middleware/generic-middleware.ts b/packages/infra/src/api/routing/middleware/generic-middleware.ts index 52801c6bdd..b9e3d7fb1f 100644 --- a/packages/infra/src/api/routing/middleware/generic-middleware.ts +++ b/packages/infra/src/api/routing/middleware/generic-middleware.ts @@ -40,6 +40,9 @@ export type DynamicMiddlewareMaker = Exclude> | Required + export type ApplyManyServices, R> = + | Exclude }[number]> + | { [K in keyof A]: Required }[number] export type Provided = T extends TagClassAny ? T extends { provides: Context.Tag } ? Context.Tag.Identifier : T extends { provides: ContextRepr } ? ContextRepr.Identifier diff --git a/packages/infra/src/api/routing/middleware/middleware-api.ts b/packages/infra/src/api/routing/middleware/middleware-api.ts index 8d73ad9a23..be621a2a57 100644 --- a/packages/infra/src/api/routing/middleware/middleware-api.ts +++ b/packages/infra/src/api/routing/middleware/middleware-api.ts @@ -1,4 +1,4 @@ -import { type NonEmptyReadonlyArray } from "effect-app" +import { Array, Either, type NonEmptyArray } from "effect-app" import { type RPCContextMap } from "effect-app/client" import { type DynamicMiddlewareMaker, type GenericMiddlewareMaker, makeMiddleware, type makeMiddlewareBasic, type RequestContextMapProvider } from "../../routing.js" @@ -9,39 +9,43 @@ export const contextMap = () => , Provided extends keyof RequestContext, Middlewares extends ReadonlyArray, - DynamicMiddlewareProviders extends RequestContextMapProvider, + DynamicMiddlewareProviders, // out MiddlewareR = never MiddlewareR = never -> = keyof Omit extends never ? - & ReturnType> - & { - middleware: ( - mw: MW - ) => DynamicMiddlewareMakerrsss< - RequestContext, - Provided, - [...Middlewares, MW], - DynamicMiddlewareProviders, - GenericMiddlewareMaker.ApplyServices - > - } - : { - middleware: ( - mw: MW - ) => DynamicMiddlewareMakerrsss< - RequestContext, - Provided, - Middlewares, - DynamicMiddlewareProviders, - GenericMiddlewareMaker.ApplyServices - > - addDynamicMiddleware: >>( - ...middlewares: MW - ) => DynamicMiddlewareMakerrsss< +> { + middleware>( + ...mw: MW + ): DynamicMiddlewareMakerrsss< + RequestContext, + Provided, + [...Middlewares, ...MW], + DynamicMiddlewareProviders, + GenericMiddlewareMaker.ApplyManyServices + > +} + +export interface MiddlewareDynamic< + RequestContext extends Record, + Provided extends keyof RequestContext, + Middlewares extends ReadonlyArray, + DynamicMiddlewareProviders, + out MiddlewareR +> extends + MiddlewareM< + RequestContext, + Provided, + Middlewares, + DynamicMiddlewareProviders, + MiddlewareR + > +{ + middleware> | NonEmptyArray>( + ...mw: MW + ): MW extends NonEmptyArray> ? DynamicMiddlewareMakerrsss< RequestContext, Provided | MW[number]["dynamic"]["key"], Middlewares, @@ -49,27 +53,101 @@ type DynamicMiddlewareMakerrsss< & { [K in keyof MW as MW[K] extends DynamicMiddlewareMaker ? MW[K]["dynamic"]["key"] : never]: MW[K] }, - MiddlewareR - > // GenericMiddlewareMaker.ApplyServices - } + GenericMiddlewareMaker.ApplyManyServices + > + : DynamicMiddlewareMakerrsss< + RequestContext, + Provided, + [...Middlewares, ...MW], + DynamicMiddlewareProviders, + GenericMiddlewareMaker.ApplyManyServices + > + // addDynamicMiddleware: >>( + // ...middlewares: MW + // ) => DynamicMiddlewareMakerrsss< + // RequestContext, + // Provided | MW[number]["dynamic"]["key"], + // Middlewares, + // & DynamicMiddlewareProviders + // & { + // [K in keyof MW as MW[K] extends DynamicMiddlewareMaker ? MW[K]["dynamic"]["key"] : never]: MW[K] + // }, + // MiddlewareR // TODO GenericMiddlewareMaker.ApplyServices + // > // GenericMiddlewareMaker.ApplyServices +} + +type GetDynamicMiddleware> = T extends + RequestContextMapProvider ? T : never + +type DynamicMiddlewareMakerrsss< + RequestContext extends Record, + Provided extends keyof RequestContext = never, + Middlewares extends ReadonlyArray = [], + DynamicMiddlewareProviders = unknown, + MiddlewareR = never +> = keyof Omit extends never ? [MiddlewareR] extends [never] ? + & ReturnType< + typeof makeMiddlewareBasic< + RequestContext, + GetDynamicMiddleware, + Middlewares + > + > + // & { + // MiddlewareR: MiddlewareR + // Provided: Provided + // Middlewares: Middlewares + // DynamicMiddlewareProviders: Simplify + // } + & MiddlewareM< + RequestContext, + Provided, + Middlewares, + DynamicMiddlewareProviders, + MiddlewareR + > + : MiddlewareM< + RequestContext, + Provided, + Middlewares, + DynamicMiddlewareProviders, + MiddlewareR + > + : MiddlewareDynamic< + RequestContext, + Provided, + Middlewares, + DynamicMiddlewareProviders, + MiddlewareR + > export const makeNewMiddleware: < RequestContextMap extends Record ->() => >( - ...genericMiddlewares: Middlewares -) => DynamicMiddlewareMakerrsss = () => (...genericMiddlewares) => { - const dynamicMiddlewares: Record = {} as any +>() => DynamicMiddlewareMakerrsss = () => { const make = makeMiddleware() + let capturedMiddlewares: (DynamicMiddlewareMaker | GenericMiddlewareMaker)[] = [] const it = { - middleware: (mw: MW) => { - genericMiddlewares = [...genericMiddlewares, mw] as any - return Object.assign(make({ genericMiddlewares, dynamicMiddlewares }), it) - }, - addDynamicMiddleware: (...middlewares: any[]) => { - for (const a of middlewares) { - console.log("Adding dynamic middleware", a, a.dynamic.key, Object.keys(a)) - dynamicMiddlewares[a.dynamic.key] = a + middleware: (...middlewares: any[]) => { + for (const mw of middlewares) { + capturedMiddlewares = [mw, ...capturedMiddlewares] + if (mw.dynamic) { + console.log("Adding dynamic middleware", mw.key, mw.dynamic.key) + } else { + console.log("Adding generic middleware", mw.key) + } } + const [genericMiddlewares, dyn] = Array.partitionMap( + capturedMiddlewares, + (mw) => + "dynamic" in mw && mw.dynamic + ? Either.right(mw as DynamicMiddlewareMaker) + : Either.left(mw as GenericMiddlewareMaker) + ) + const dynamicMiddlewares = dyn.reduce( + (prev, cur) => ({ ...prev, [cur.dynamic.key]: cur }), + {} as Record + ) + // TODO: support dynamic and generic intertwined. treat them as one return Object.assign(make({ genericMiddlewares, dynamicMiddlewares }), it) } } diff --git a/packages/infra/test/controller.test.ts b/packages/infra/test/controller.test.ts index 167dd935bf..5cd3b3f0f1 100644 --- a/packages/infra/test/controller.test.ts +++ b/packages/infra/test/controller.test.ts @@ -120,11 +120,10 @@ const middleware2 = makeMiddleware()({ } }) -export const middleware3 = makeNewMiddleware()( - ...genericMiddlewares -) - .addDynamicMiddleware(AllowAnonymous, RequireRoles) - .addDynamicMiddleware(Test) +export const middleware3 = makeNewMiddleware() + .middleware(...genericMiddlewares) + .middleware(AllowAnonymous, RequireRoles) + .middleware(Test) // .middleware(BogusMiddleware) // expectTypeOf(middleware3).toExtend() diff --git a/packages/infra/test/requires.test.ts b/packages/infra/test/requires.test.ts index a761e9a060..721ddda482 100644 --- a/packages/infra/test/requires.test.ts +++ b/packages/infra/test/requires.test.ts @@ -1,8 +1,7 @@ import { expectTypeOf, it } from "@effect/vitest" import { Effect, Layer, S } from "effect-app" -import { Middleware } from "../src/api/routing.js" +import { makeNewMiddleware, Middleware } from "../src/api/routing.js" import { AllowAnonymous, type RequestContextMap, RequireRoles, Some, SomeElse, Test } from "./fixtures.js" -import { makeNewMiddleware } from "./requires.js" export class SomeMiddleware extends Middleware.Tag()("SomeMiddleware", { provides: Some, diff --git a/packages/infra/test/requires.ts b/packages/infra/test/requires.ts deleted file mode 100644 index d7810a7daf..0000000000 --- a/packages/infra/test/requires.ts +++ /dev/null @@ -1,148 +0,0 @@ -import { Array, Either } from "effect-app" -import { type RPCContextMap } from "effect-app/client" -import { type DynamicMiddlewareMaker, type GenericMiddlewareMaker, makeMiddleware, type makeMiddlewareBasic, type RequestContextMapProvider } from "../src/api/routing.js" - -export interface MiddlewareM< - RequestContext extends Record, - Provided extends keyof RequestContext, - Middlewares extends ReadonlyArray, - DynamicMiddlewareProviders, - // out MiddlewareR = never - MiddlewareR = never -> { - middleware: ( - mw: MW - ) => DynamicMiddlewareMakerrsss< - RequestContext, - Provided, - [...Middlewares, MW], - DynamicMiddlewareProviders, - GenericMiddlewareMaker.ApplyServices - > -} - -export interface Dynamic< - RequestContext extends Record, - Provided extends keyof RequestContext, - Middlewares extends ReadonlyArray, - DynamicMiddlewareProviders, - out MiddlewareR -> extends - MiddlewareM< - RequestContext, - Provided, - Middlewares, - DynamicMiddlewareProviders, - MiddlewareR - > -{ - middleware: | GenericMiddlewareMaker>( - mw: MW - ) => MW extends DynamicMiddlewareMaker ? DynamicMiddlewareMakerrsss< - RequestContext, - Provided | MW["dynamic"]["key"], - Middlewares, - & DynamicMiddlewareProviders - & { - [K in MW["dynamic"]["key"]]: MW - }, - GenericMiddlewareMaker.ApplyServices - > - : DynamicMiddlewareMakerrsss< - RequestContext, - Provided, - [...Middlewares, MW], - DynamicMiddlewareProviders, - GenericMiddlewareMaker.ApplyServices - > - // addDynamicMiddleware: >>( - // ...middlewares: MW - // ) => DynamicMiddlewareMakerrsss< - // RequestContext, - // Provided | MW[number]["dynamic"]["key"], - // Middlewares, - // & DynamicMiddlewareProviders - // & { - // [K in keyof MW as MW[K] extends DynamicMiddlewareMaker ? MW[K]["dynamic"]["key"] : never]: MW[K] - // }, - // MiddlewareR // TODO GenericMiddlewareMaker.ApplyServices - // > // GenericMiddlewareMaker.ApplyServices -} - -type GetDynamicMiddleware> = T extends - RequestContextMapProvider ? T : never - -type DynamicMiddlewareMakerrsss< - RequestContext extends Record, - Provided extends keyof RequestContext = never, - Middlewares extends ReadonlyArray = [], - DynamicMiddlewareProviders = unknown, - MiddlewareR = never -> = keyof Omit extends never ? [MiddlewareR] extends [never] ? - & ReturnType< - typeof makeMiddlewareBasic< - RequestContext, - GetDynamicMiddleware, - Middlewares - > - > - // & { - // MiddlewareR: MiddlewareR - // Provided: Provided - // Middlewares: Middlewares - // DynamicMiddlewareProviders: Simplify - // } - & MiddlewareM< - RequestContext, - Provided, - Middlewares, - DynamicMiddlewareProviders, - MiddlewareR - > - : MiddlewareM< - RequestContext, - Provided, - Middlewares, - DynamicMiddlewareProviders, - MiddlewareR - > - : Dynamic< - RequestContext, - Provided, - Middlewares, - DynamicMiddlewareProviders, - MiddlewareR - > - -export const makeNewMiddleware: < - RequestContextMap extends Record ->() => DynamicMiddlewareMakerrsss = () => { - const make = makeMiddleware() - let capturedMiddlewares: (DynamicMiddlewareMaker | GenericMiddlewareMaker)[] = [] - const it = { - middleware: (...middlewares: any[]) => { - for (const mw of middlewares) { - capturedMiddlewares = [mw, ...capturedMiddlewares] - if (mw.dynamic) { - console.log("Adding dynamic middleware", mw.key, mw.dynamic.key) - } else { - console.log("Adding generic middleware", mw.key) - } - } - const [genericMiddlewares, dyn] = Array.partitionMap( - capturedMiddlewares, - (mw) => - "dynamic" in mw && mw.dynamic - ? Either.right(mw as DynamicMiddlewareMaker) - : Either.left(mw as GenericMiddlewareMaker) - ) - const dynamicMiddlewares = dyn.reduce( - (prev, cur) => ({ ...prev, [cur.dynamic.key]: cur }), - {} as Record - ) - // TODO: support dynamic and generic intertwined. treat them as one - return Object.assign(make({ genericMiddlewares, dynamicMiddlewares }), it) - } - } - return it as any -} From 0deea6a54a7286f3ee80e43ea2f3f429f06496d3 Mon Sep 17 00:00:00 2001 From: Patrick Roza Date: Sun, 3 Aug 2025 22:14:06 +0200 Subject: [PATCH 14/17] try also combined --- packages/infra/test/requires.test.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/infra/test/requires.test.ts b/packages/infra/test/requires.test.ts index 721ddda482..74b0fcea91 100644 --- a/packages/infra/test/requires.test.ts +++ b/packages/infra/test/requires.test.ts @@ -53,9 +53,8 @@ it("requires gets enforced", async () => { const middleware3 = makeNewMiddleware() .middleware(RequiresSomeMiddleware) .middleware(SomeMiddleware) - .middleware(AllowAnonymous) + .middleware(AllowAnonymous, RequireRoles) .middleware(SomeElseMiddleware) - .middleware(RequireRoles) .middleware(Test) type LayerContext = Layer.Layer.Context @@ -64,8 +63,9 @@ it("requires gets enforced", async () => { await Effect .gen(function*() { const mw = yield* middleware3 - const mwM = mw.effect(Object.assign({}, S.Any, { config: {} }), (req) => Effect.void, "some-module") - const v = yield* mwM({}, { "x-user": "test-user" }) + const mwM = mw.effect(Object.assign({}, S.Any, { config: {} }), (_req) => Effect.void, "some-module") + yield* mwM({}, { "x-user": "test-user" }) + // console.log({ v }) }) .pipe( Effect.scoped, From 9ba6b90ffdbef5f4fc234b41b06c53ea6542f83f Mon Sep 17 00:00:00 2001 From: Patrick Roza Date: Sun, 3 Aug 2025 22:14:22 +0200 Subject: [PATCH 15/17] play separate .. --- .../api/routing/middleware/middleware-api.ts | 54 +++++++------------ 1 file changed, 19 insertions(+), 35 deletions(-) diff --git a/packages/infra/src/api/routing/middleware/middleware-api.ts b/packages/infra/src/api/routing/middleware/middleware-api.ts index be621a2a57..5b5fbb61ef 100644 --- a/packages/infra/src/api/routing/middleware/middleware-api.ts +++ b/packages/infra/src/api/routing/middleware/middleware-api.ts @@ -34,46 +34,30 @@ export interface MiddlewareDynamic< Middlewares extends ReadonlyArray, DynamicMiddlewareProviders, out MiddlewareR -> extends - MiddlewareM< +> { + // TODO: this still allows to mix both types of middleware but with bad typing result + // either have to block it, or implement the support properly. + middleware>( + ...mw: MW + ): DynamicMiddlewareMakerrsss< RequestContext, Provided, - Middlewares, + [...Middlewares, ...MW], DynamicMiddlewareProviders, - MiddlewareR + GenericMiddlewareMaker.ApplyManyServices > -{ - middleware> | NonEmptyArray>( + middleware>>( ...mw: MW - ): MW extends NonEmptyArray> ? DynamicMiddlewareMakerrsss< - RequestContext, - Provided | MW[number]["dynamic"]["key"], - Middlewares, - & DynamicMiddlewareProviders - & { - [K in keyof MW as MW[K] extends DynamicMiddlewareMaker ? MW[K]["dynamic"]["key"] : never]: MW[K] - }, - GenericMiddlewareMaker.ApplyManyServices - > - : DynamicMiddlewareMakerrsss< - RequestContext, - Provided, - [...Middlewares, ...MW], - DynamicMiddlewareProviders, - GenericMiddlewareMaker.ApplyManyServices - > - // addDynamicMiddleware: >>( - // ...middlewares: MW - // ) => DynamicMiddlewareMakerrsss< - // RequestContext, - // Provided | MW[number]["dynamic"]["key"], - // Middlewares, - // & DynamicMiddlewareProviders - // & { - // [K in keyof MW as MW[K] extends DynamicMiddlewareMaker ? MW[K]["dynamic"]["key"] : never]: MW[K] - // }, - // MiddlewareR // TODO GenericMiddlewareMaker.ApplyServices - // > // GenericMiddlewareMaker.ApplyServices + ): DynamicMiddlewareMakerrsss< + RequestContext, + Provided | MW[number]["dynamic"]["key"], + Middlewares, + & DynamicMiddlewareProviders + & { + [U in MW[number] as U["dynamic"]["key"]]: U + }, + GenericMiddlewareMaker.ApplyManyServices + > } type GetDynamicMiddleware> = T extends From b713e5bd75cc305a876ae0e119a532fa371c87e6 Mon Sep 17 00:00:00 2001 From: Patrick Roza Date: Sun, 3 Aug 2025 22:16:02 +0200 Subject: [PATCH 16/17] revert separate --- .../api/routing/middleware/middleware-api.ts | 38 +++++++++---------- 1 file changed, 18 insertions(+), 20 deletions(-) diff --git a/packages/infra/src/api/routing/middleware/middleware-api.ts b/packages/infra/src/api/routing/middleware/middleware-api.ts index 5b5fbb61ef..7b66a7fdc7 100644 --- a/packages/infra/src/api/routing/middleware/middleware-api.ts +++ b/packages/infra/src/api/routing/middleware/middleware-api.ts @@ -37,27 +37,25 @@ export interface MiddlewareDynamic< > { // TODO: this still allows to mix both types of middleware but with bad typing result // either have to block it, or implement the support properly. - middleware>( - ...mw: MW - ): DynamicMiddlewareMakerrsss< - RequestContext, - Provided, - [...Middlewares, ...MW], - DynamicMiddlewareProviders, - GenericMiddlewareMaker.ApplyManyServices - > - middleware>>( + middleware> | NonEmptyArray>( ...mw: MW - ): DynamicMiddlewareMakerrsss< - RequestContext, - Provided | MW[number]["dynamic"]["key"], - Middlewares, - & DynamicMiddlewareProviders - & { - [U in MW[number] as U["dynamic"]["key"]]: U - }, - GenericMiddlewareMaker.ApplyManyServices - > + ): [MW] extends [NonEmptyArray>] ? DynamicMiddlewareMakerrsss< + RequestContext, + Provided | MW[number]["dynamic"]["key"], + Middlewares, + & DynamicMiddlewareProviders + & { + [U in MW[number] as U["dynamic"]["key"]]: U + }, + GenericMiddlewareMaker.ApplyManyServices + > + : DynamicMiddlewareMakerrsss< + RequestContext, + Provided, + [...Middlewares, ...MW], + DynamicMiddlewareProviders, + GenericMiddlewareMaker.ApplyManyServices + > } type GetDynamicMiddleware> = T extends From c1c50d9fefd49073d754790070e3d6d5c1d42a24 Mon Sep 17 00:00:00 2001 From: Patrick Roza Date: Sun, 3 Aug 2025 22:16:30 +0200 Subject: [PATCH 17/17] add changeset --- .changeset/ten-laws-join.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/ten-laws-join.md diff --git a/.changeset/ten-laws-join.md b/.changeset/ten-laws-join.md new file mode 100644 index 0000000000..8a95cb483a --- /dev/null +++ b/.changeset/ten-laws-join.md @@ -0,0 +1,5 @@ +--- +"@effect-app/infra": minor +--- + +support middleware `requires` and simplified api