Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 36 additions & 0 deletions api/src/HelloWorld.alt.controllers.ts
Original file line number Diff line number Diff line change
@@ -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
})
})
}
})
})
3 changes: 2 additions & 1 deletion api/src/controllers.ts
Original file line number Diff line number Diff line change
@@ -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
94 changes: 90 additions & 4 deletions api/src/lib/routing.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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"

Expand Down Expand Up @@ -80,12 +85,93 @@ const RequireRolesLive = Layer.effect(
})
)

class AppMiddlewareImpl extends AppMiddleware {
// todo
export const mergeOptionalDependencies = <T extends { readonly dependencies?: ReadonlyArray<Layer.Layer.Any> }>(
a: T
): T extends { dependencies: NonEmptyReadonlyArray<Layer.Layer.Any> } ? Layer<
LayerUtils.GetLayersSuccess<T["dependencies"]>,
LayerUtils.GetLayersError<T["dependencies"]>,
LayerUtils.GetLayersContext<T["dependencies"]>
>
: Layer.Layer<never> => 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 = <R extends Rpc.Any>(group: RpcGroup.RpcGroup<R> & { 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 = <R extends Rpc.Any>(
group: RpcGroup.RpcGroup<R> & {
meta: { moduleName: string }
toLayerDynamic: <
Handlers extends HandlersFrom<R>,
EX = never,
RX = never
>(
build:
| Handlers
| Effect.Effect<Handlers, EX, RX>
) => Layer.Layer<
Rpc.ToHandler<R>,
EX,
| Exclude<RX, Scope>
| HandlersContext<R, Handlers>
>
}
) =>
<
LayerOpts extends {
effect: Effect.Effect<
HandlersFrom<R>,
any,
any
>
dependencies?: ReadonlyArray<Layer.Layer.Any>
}
>(
layerOpts: LayerOpts
): Layer.Layer<
never,
Effect.Effect.Error<LayerOpts["effect"]>,
| Exclude<Effect.Effect.Context<LayerOpts["effect"]>, Service.MakeDepsOut<LayerOpts>>
| Service.MakeDepsIn<LayerOpts>
| Exclude<Rpc.Middleware<R>, AppMiddleware>
| HandlersContext<R, Effect.Effect.Success<LayerOpts["effect"]>>
| RpcSerialization
| HttpLayerRouter.HttpRouter
> =>
makeServer(group).pipe(
Layer.provide(MiddlewareDefault),
Layer.provide(
group
.toLayerDynamic(
layerOpts.effect as Effect<
HandlersFrom<R>,
Effect.Error<LayerOpts["effect"]>,
Effect.Context<LayerOpts["effect"]>
>
)
.pipe(Layer.provide(mergeOptionalDependencies(layerOpts)))
)
) as any
}

export const { Router, matchAll } = makeRouter(AppMiddlewareImpl)
33 changes: 33 additions & 0 deletions api/src/resources/HelloWorld.alt.ts
Original file line number Diff line number Diff line change
@@ -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>("Response")({
now: S.Date.withDefault,
echo: S.String,
context: RequestContext,
currentUser: S.NullOr(UserView),
randomUser: UserView
}) {}

export class GetHelloWorld extends S.Req<GetHelloWorld>()("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
)
5 changes: 4 additions & 1 deletion api/src/resources/lib/middleware.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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"

Expand Down Expand Up @@ -30,4 +31,6 @@ export class AppMiddleware extends MiddlewareMaker
.middleware(RequireRoles)
.middleware(AllowAnonymous)
.middleware(...DefaultGenericMiddlewares)
{}
{
static Group = middlewareGroup(this)
}
18 changes: 15 additions & 3 deletions frontend/pages/index.vue
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
<script setup lang="ts">
import { HelloWorldRsc } from "#resources"
import { HelloWorldRpc } from "#resources/HelloWorld.alt"
import { buildFormFromSchema } from "@effect-app/vue/form"
import { Atom, AtomRpc } from "@effect-atom/atom"
import { useAtomValue } from "@effect-atom/atom-vue"
import { S } from "effect-app"

class Input extends S.Class<Input>("Input")({
Expand All @@ -27,8 +29,18 @@ const makeReq = () => ({

const req = ref(makeReq())

const helloWorldClient = clientFor(HelloWorldRsc)
const [result] = useSafeQuery(helloWorldClient.GetHelloWorld, req)
class HelloWorldClient extends AtomRpc.Tag<HelloWorldClient>()(
"HelloWorldClient",
{
protocol: RpcClientProtocolLayers("/HelloWorld.alt"),
group: HelloWorldRpc,
},
) {}

const result = useAtomValue(() => {
console.log("Computing Atom:", req.value)
return Atom.refreshOnWindowFocus(HelloWorldClient.query("Get", req.value))
})

// onMounted(() => {
// setInterval(() => {
Expand Down