diff --git a/api/src/HelloWorld.alt.controllers.ts b/api/src/HelloWorld.alt.controllers.ts new file mode 100644 index 000000000..350597815 --- /dev/null +++ b/api/src/HelloWorld.alt.controllers.ts @@ -0,0 +1,36 @@ +import { AppMiddlewareImpl } from "#lib/routing" +import { User } from "#models/User" +import { GetHelloWorld, HelloWorldRpc } from "#resources/HelloWorld.alt" +import { UserRepo } from "#services" +import { getRequestContext } from "@effect-app/infra/api/setupRequest" +import { generate } from "@effect-app/infra/test" +import { Effect, S } from "effect-app" + +export default AppMiddlewareImpl.Router(HelloWorldRpc)({ + dependencies: [UserRepo.Default], + effect: Effect.gen(function*() { + const userRepo = yield* UserRepo + return { + // TODO: generator support? *Get({ echo }) { - but then need to handle the span stacktrace, like in effect-app Router + Get: Effect.fn(function*({ echo }) { + const context = yield* getRequestContext + // yield* Effect.context<"not provided">() + const user = yield* userRepo + .tryGetCurrentUser + .pipe( + Effect.catchTags({ + "NotLoggedInError": () => Effect.succeed(null), + "NotFoundError": () => Effect.succeed(null) + }) + ) + + return new GetHelloWorld.success({ + context, + echo, + currentUser: user, + randomUser: generate(S.A.make(User)).value + }) + }) + } + }) +}) diff --git a/api/src/controllers.ts b/api/src/controllers.ts index 21be5610b..bf38e5650 100644 --- a/api/src/controllers.ts +++ b/api/src/controllers.ts @@ -1,7 +1,8 @@ // codegen:start {preset: barrel, include: ./*.controllers.ts, import: default} import accountsControllers from "./Accounts.controllers.js" +import helloWorldAltControllers from "./HelloWorld.alt.controllers.js" import helloWorldControllers from "./HelloWorld.controllers.js" import operationsControllers from "./Operations.controllers.js" -export { accountsControllers, helloWorldControllers, operationsControllers } +export { accountsControllers, helloWorldAltControllers, helloWorldControllers, operationsControllers } // codegen:end diff --git a/api/src/lib/routing.ts b/api/src/lib/routing.ts index 5f980b2fa..5131eb6b6 100644 --- a/api/src/lib/routing.ts +++ b/api/src/lib/routing.ts @@ -3,11 +3,16 @@ import { AllowAnonymous, AppMiddleware, getConf, RequireRoles } from "#resources/lib" import { makeUserProfileFromAuthorizationHeader, makeUserProfileFromUserHeader, UserProfile } from "#services" +import { type LayerUtils } from "@effect-app/infra/api/layerUtils" import { DefaultGenericMiddlewaresLive, makeRouter } from "@effect-app/infra/api/routing" -import { Effect, Exit, Layer } from "effect" -import { Option } from "effect-app" +import { type Rpc, type RpcGroup, RpcServer } from "@effect/rpc" +import { type HandlersFrom } from "@effect/rpc/RpcGroup" +import { type RpcSerialization } from "@effect/rpc/RpcSerialization" +import { Effect, Exit, Layer, type NonEmptyReadonlyArray, Option, type Scope } from "effect-app" import { NotLoggedInError, UnauthorizedError } from "effect-app/client" -import { type HttpHeaders } from "effect-app/http" +import { type HttpHeaders, type HttpLayerRouter } from "effect-app/http" +import { type HandlersContext } from "effect-app/rpc/RpcMiddleware" +import { type Service } from "effect/Effect" import { basicRuntime } from "./basicRuntime.js" import { AppLogger } from "./logger.js" @@ -80,12 +85,93 @@ const RequireRolesLive = Layer.effect( }) ) -class AppMiddlewareImpl extends AppMiddleware { +// todo +export const mergeOptionalDependencies = }>( + a: T +): T extends { dependencies: NonEmptyReadonlyArray } ? Layer< + LayerUtils.GetLayersSuccess, + LayerUtils.GetLayersError, + LayerUtils.GetLayersContext + > + : Layer.Layer => Layer.mergeAll(...(a.dependencies as any ?? [])) as any + +// TODO; nicer to make one global RpcServer and endpoint, but use prefixes on the RpcGroups instead... +// but it's not currently supported; https://discord.com/channels/795981131316985866/1270891146213589074/1405494953186033774 +export const makeServer = (group: RpcGroup.RpcGroup & { meta: { moduleName: string } }) => + RpcServer + .layerHttpRouter({ + spanPrefix: "RpcServer." + group.meta.moduleName, + group, + path: ("/rpc/" + group.meta.moduleName) as `/${typeof group.meta.moduleName}`, + protocol: "http" + }) + +const MiddlewareDefault = AppMiddleware.layer.pipe(Layer.provide([ + AllowAnonymousLive, + RequireRolesLive, + DefaultGenericMiddlewaresLive +])) + +export class AppMiddlewareImpl extends AppMiddleware { static Default = this.layer.pipe(Layer.provide([ AllowAnonymousLive, RequireRolesLive, DefaultGenericMiddlewaresLive ])) + static Router = ( + group: RpcGroup.RpcGroup & { + meta: { moduleName: string } + toLayerDynamic: < + Handlers extends HandlersFrom, + EX = never, + RX = never + >( + build: + | Handlers + | Effect.Effect + ) => Layer.Layer< + Rpc.ToHandler, + EX, + | Exclude + | HandlersContext + > + } + ) => + < + LayerOpts extends { + effect: Effect.Effect< + HandlersFrom, + any, + any + > + dependencies?: ReadonlyArray + } + >( + layerOpts: LayerOpts + ): Layer.Layer< + never, + Effect.Effect.Error, + | Exclude, Service.MakeDepsOut> + | Service.MakeDepsIn + | Exclude, AppMiddleware> + | HandlersContext> + | RpcSerialization + | HttpLayerRouter.HttpRouter + > => + makeServer(group).pipe( + Layer.provide(MiddlewareDefault), + Layer.provide( + group + .toLayerDynamic( + layerOpts.effect as Effect< + HandlersFrom, + Effect.Error, + Effect.Context + > + ) + .pipe(Layer.provide(mergeOptionalDependencies(layerOpts))) + ) + ) as any } export const { Router, matchAll } = makeRouter(AppMiddlewareImpl) diff --git a/api/src/resources/HelloWorld.alt.ts b/api/src/resources/HelloWorld.alt.ts new file mode 100644 index 000000000..4e4371671 --- /dev/null +++ b/api/src/resources/HelloWorld.alt.ts @@ -0,0 +1,33 @@ +import { RequestContext } from "@effect-app/infra/RequestContext" +import { RpcGroup } from "@effect/rpc" +import { AppMiddleware, S } from "./lib.js" +import { UserView } from "./views.js" + +class Response extends S.Class("Response")({ + now: S.Date.withDefault, + echo: S.String, + context: RequestContext, + currentUser: S.NullOr(UserView), + randomUser: UserView +}) {} + +export class GetHelloWorld extends S.Req()("GetHelloWorld", { + echo: S.String +}, { allowAnonymous: true, allowRoles: ["user"], success: Response }) {} + +// codegen:start {preset: meta, sourcePrefix: src/resources/} +export const meta = { moduleName: "HelloWorld.alt" } as const +// codegen:end + +export const HelloWorldRpc = Object.assign( + AppMiddleware.Group(RpcGroup + .make( + AppMiddleware.rpc("Get", { + payload: GetHelloWorld.fields, + // TODO: add fromTaggedRequeset with config support instead + success: GetHelloWorld.success, + config: GetHelloWorld.config + }) + )), + { meta } // todo; auto +) diff --git a/api/src/resources/lib/middleware.ts b/api/src/resources/lib/middleware.ts index 596e85138..81a268660 100644 --- a/api/src/resources/lib/middleware.ts +++ b/api/src/resources/lib/middleware.ts @@ -3,6 +3,7 @@ import { NotLoggedInError, UnauthorizedError } from "effect-app/client" // get rid of in resources and frontend import { DefaultGenericMiddlewares } from "effect-app/middleware" import { MiddlewareMaker, RpcMiddleware } from "effect-app/rpc" +import { middlewareGroup } from "effect-app/rpc/MiddlewareMaker" import { contextMap, getConfig, RpcContextMap } from "effect-app/rpc/RpcContextMap" import { UserProfile } from "./Userprofile.js" @@ -30,4 +31,6 @@ export class AppMiddleware extends MiddlewareMaker .middleware(RequireRoles) .middleware(AllowAnonymous) .middleware(...DefaultGenericMiddlewares) -{} +{ + static Group = middlewareGroup(this) +} diff --git a/frontend/pages/index.vue b/frontend/pages/index.vue index f37215755..1a0e9dad7 100644 --- a/frontend/pages/index.vue +++ b/frontend/pages/index.vue @@ -1,6 +1,8 @@