/* 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 { expect, expectTypeOf, it } from "@effect/vitest" import { Context, Effect, Layer, RpcX, S, Scope } from "effect-app" import { InvalidStateError, makeRpcClient, UnauthorizedError } from "effect-app/client" import { DefaultGenericMiddlewares } from "effect-app/middleware" import { MiddlewareMaker } from "effect-app/rpc" import { TypeTestId } from "effect-app/TypeTest" import { type RpcSerialization } from "effect/unstable/rpc" import { DefaultGenericMiddlewaresLive, DevModeMiddlewareLive } from "../src/api/routing/middleware.js" import { sort } from "../src/api/routing/tsort.js" import { AllowAnonymous, AllowAnonymousLive, CustomError1, RequestContextMap, RequireRoles, RequireRolesLive, Some, SomeElse, SomeService, Test, TestLive } from "./fixtures.js" class MyContextProvider extends RpcX.RpcMiddleware.Tag()("MyContextProvider") { static Default = Layer.make(this, { *make() { yield* SomeService if (Math.random() > 0.5) return yield* new CustomError1() return Effect.fnUntraced(function*(effect) { yield* SomeElse // the only requirements you can have are the one provided by HttpLayerRouter.Provided yield* Scope.Scope yield* Effect.logInfo("MyContextProviderGen", "this is a generator") yield* Effect.succeed("this is a generator") // this is allowed here but mergeContextProviders/MergedContextProvider will trigger an error // yield* SomeElse // currently the effectful context provider cannot trigger an error when building the per request context // this is allowed here but mergeContextProviders/MergedContextProvider will trigger an error // if (Math.random() > 0.5) return yield* new CustomError2() return yield* Effect.provideService(effect, Some, Some.of({ a: 1 })) }) } }) static but_why = "???" // remove me and life rocks } class MyContextProvider3 extends RpcX.RpcMiddleware.Tag()("MyContextProvider3") { static Default = Layer.make(this, { dependencies: [SomeService.Default], *make() { yield* SomeService if (Math.random() > 0.5) return yield* new CustomError1() return Effect.fnUntraced(function*(effect) { yield* SomeElse // the only requirements you can have are the one provided by HttpLayerRouter.Provided yield* Scope.Scope yield* Effect.logInfo("MyContextProviderGen", "this is a generator") yield* Effect.succeed("this is a generator") // this is allowed here but mergeContextProviders/MergedContextProvider will trigger an error // yield* SomeElse // currently the effectful context provider cannot trigger an error when building the per request context // this is allowed here but mergeContextProviders/MergedContextProvider will trigger an error // if (Math.random() > 0.5) return yield* new CustomError2() return yield* Effect.provideService(effect, Some, Some.of({ a: 1 })) }) } }) } expectTypeOf(MyContextProvider3.Default).toEqualTypeOf< Layer.Layer & { withoutDependencies: Layer.Layer } >() class MyContextProvider2 extends RpcX.RpcMiddleware.Tag()("MyContextProvider2") { static Default = Layer.make(this, { *make() { if (Math.random() > 0.5) return yield* new CustomError1() return Effect.fnUntraced(function*(effect) { // we test without dependencies, so that we end up with an R of never. return yield* Effect.provideService(effect, SomeElse, SomeElse.of({ b: 2 })) }) } }) } // class Str extends Context.Service()("str") {} export class BogusMiddleware extends RpcX.RpcMiddleware.Tag()("BogusMiddleware") { static Default = Layer.make(this, { *make() { yield* Str // yield* Effect.context<"test-dep">() return (effect) => Effect.gen(function*() { // yield* Effect.context<"test-dep2">() return yield* effect }) } }) } const genericMiddlewares = [ ...DefaultGenericMiddlewares, BogusMiddleware, MyContextProvider2 ] as const const genericMiddlewaresLive = [ DefaultGenericMiddlewaresLive, BogusMiddleware.Default, MyContextProvider2.Default, DevModeMiddlewareLive ] as const const MiddlewaresLive = [ RequireRolesLive, TestLive, AllowAnonymousLive, MyContextProvider.Default, ...genericMiddlewaresLive ] as const class middleware extends MiddlewareMaker .Tag()("middleware", RequestContextMap) .middleware( RequireRoles, Test ) // AllowAnonymous provided after RequireRoles so that RequireRoles can access what AllowAnonymous provides .middleware(AllowAnonymous) .middleware(MyContextProvider) .middleware(...genericMiddlewares) { static Default = this.layer.pipe(Layer.provide([...MiddlewaresLive])) // static override [Unify.unifySymbol]?: TagUnify // why we need this? } const middlewareBis = MiddlewareMaker .Tag()("middleware", RequestContextMap) .middleware( RequireRoles, Test ) // testing sideways elimination .middleware(AllowAnonymous, MyContextProvider, ...genericMiddlewares) expectTypeOf(middleware["Service"]).toEqualTypeOf() const middlewareTrisWip = MiddlewareMaker .Tag()("middleware", RequestContextMap) .middleware( MyContextProvider, RequireRoles, Test )[TypeTestId] expectTypeOf(middlewareTrisWip).toEqualTypeOf<{ missingDynamicMiddlewares: "allowAnonymous" missingContext: SomeElse }>() // testing more sideways elimination] const middlewareQuater = MiddlewareMaker .Tag()("middleware", RequestContextMap) .middleware( RequireRoles, Test, AllowAnonymous, MyContextProvider, ...genericMiddlewares ) expectTypeOf(middleware["Service"]).toEqualTypeOf() const middleware2 = MiddlewareMaker .Tag()("middleware", RequestContextMap) .middleware(MyContextProvider) .middleware(RequireRoles, Test) .middleware(AllowAnonymous) .middleware(...DefaultGenericMiddlewares, BogusMiddleware, MyContextProvider2) export const middleware3 = MiddlewareMaker .Tag()("middleware", RequestContextMap) .middleware(...genericMiddlewares) .middleware(AllowAnonymous, RequireRoles) .middleware(Test) .middleware(BogusMiddleware) export const { TaggedRequestFor } = makeRpcClient(RequestContextMap) const Req = TaggedRequestFor("Something") const Command = Req.Command const Query = Req.Query export class Eff extends Command()("Eff", {}, { success: S.Void }) {} export class Gen extends Command()("Gen", {}) {} expectTypeOf(Eff.error).toEqualTypeOf() export class DoSomething extends Command()("DoSomething", { id: S.String }, { success: S.Void }) {} // const rpc = makeRpc(middleware).pipe( // Effect.map(({ effect }) => // effect( // DoSomething, // Effect.fn(function*(req, headers) { // const user = yield* UserProfile // dynamic context // const some = yield* Some // context provided by ContextProvider // const someservice = yield* SomeService // extraneous service // yield* Console.log("DoSomething", req.id, some) // }) // ) // ) // ) export class GetSomething extends Query()("GetSomething", { id: S.String }, { success: S.String }) {} export class GetSomething2 extends Query()("GetSomething2", { id: S.String }, { success: S.FiniteFromString }) {} const Something = { Eff, Gen, DoSomething, GetSomething, GetSomething2 } // const client = ApiClientFactory.makeFor(Layer.empty)(Something) // client.pipe(Effect.map(c => c.DoSomething.name)) export class SomethingService extends Context.Service()( "SomethingService", { make: Effect.gen(function*() { return { a: 1 } }) } ) { static Default = Layer.effect(this, this.make) } declare const a: { (opt: { a: 1 }): void (opt: { a: 2 }): void (opt: { b: 3 }): void (opt: { b: 3 }): void } export class SomethingRepo extends Context.Service()( "SomethingRepo", { make: Effect.gen(function*() { const smth = yield* SomethingService console.log({ smth }) return { b: 2 } }) } ) { static Default = Layer.effect(this, this.make).pipe(Layer.provide(SomethingService.Default)) } export class SomethingService2 extends Context.Service()( "SomethingService2", { make: Effect.gen(function*() { return { c: 3 } }) } ) { static Default = Layer.effect(this, this.make) } export const { Router, matchAll } = makeRouter( middleware ) export const r2 = makeRouter( Object.assign(middleware2, { Default: middleware2.layer.pipe(Layer.provide([...MiddlewaresLive])) }) ) const router = Router(Something)({ dependencies: [ SomethingRepo.Default, SomethingService.Default, SomethingService2.Default ], *effect(match) { const repo = yield* SomethingRepo const smth = yield* SomethingService const smth2 = yield* SomethingService2 // this gets catched in 'routes' type if (Math.random() > 0.5) { return yield* new InvalidStateError("ciao") } console.log({ repo, smth, smth2 }) return match({ Eff: () => Effect .gen(function*() { const some = yield* Some return yield* Effect.logInfo("Some", some) }), *Gen() { const some = yield* Some return yield* Effect.logInfo("Some", some) }, *GetSomething(req) { console.log(req["id"]) const _b = yield* Effect.succeed(false) if (_b) { // expected errors here because RequestError is not a valid error for controllers // yield* new RequestError(1 as any) // return yield* new RequestError(1 as any) } if (Math.random() > 0.5) { return yield* Effect.succeed("12") } if (!_b) { return yield* Effect.fail(new UnauthorizedError()) } else { // expected an error here because a boolean is not a string // return _b return "12" } }, DoSomething: { *raw() { return yield* Effect.succeed(undefined) } }, GetSomething2: { raw: Some.use(() => Effect.succeed("12")) } }) } }) it("sorts based on requirements", () => { const input = [RequireRoles, AllowAnonymous, Test] const sorted = sort(input) console.dir({ input, sorted }, { depth: 10 }) expect(sorted).toEqual([AllowAnonymous, RequireRoles, Test]) }) // eslint-disable-next-line unused-imports/no-unused-vars const matched = matchAll({ router }) expectTypeOf({} as Layer.Services).toEqualTypeOf< RpcSerialization.RpcSerialization | SomeService | Str >() type makeContext = MakeContext expectTypeOf({} as MakeErrors).toEqualTypeOf() expectTypeOf({} as makeContext).toEqualTypeOf< SomethingService | SomethingRepo | SomethingService2 >() const router2 = r2.Router(Something)({ *effect(match) { return match({ Eff: () => Effect .gen(function*() { const some = yield* Some return yield* Effect.logInfo("Some", some) }), *Gen() { const some = yield* Some return yield* Effect.logInfo("Some", some) }, *GetSomething(req) { console.log(req["id"]) const _b = yield* Effect.succeed(false) if (_b) { // expected errors here because RequestError is not a valid error for controllers // yield* new RequestError(1 as any) // return yield* new RequestError(1 as any) } if (Math.random() > 0.5) { return yield* Effect.succeed("12") } if (!_b) { return yield* Effect.fail(new UnauthorizedError()) } else { // expected an error here because a boolean is not a string // return _b return "12" } }, DoSomething: { *raw() { return yield* Effect.succeed(undefined) } }, GetSomething2: { raw: Some.use(() => Effect.succeed("12")) } }) } }) // eslint-disable-next-line unused-imports/no-unused-vars const matched2 = matchAll({ router: router2 }) expectTypeOf({} as Layer.Services).toEqualTypeOf< RpcSerialization.RpcSerialization | SomeService | Str >() type makeContext2 = MakeContext expectTypeOf({} as makeContext2).toEqualTypeOf()