Skip to content

Commit

Permalink
feat: add routing2 based on @effect/rpc
Browse files Browse the repository at this point in the history
  • Loading branch information
patroza committed Oct 7, 2024
1 parent aa5e0b2 commit 0fa59b0
Show file tree
Hide file tree
Showing 5 changed files with 612 additions and 3 deletions.
5 changes: 5 additions & 0 deletions .changeset/quiet-items-mate.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@effect-app/infra": minor
---

feat: add routing2 based on @effect/rpc
22 changes: 22 additions & 0 deletions packages/infra/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@
"@azure/service-bus": "^7.9.5",
"@effect-app/core": "workspace:*",
"@effect-app/infra-adapters": "workspace:*",
"@effect/rpc": "^0.40.2",
"@effect/rpc-http": "^0.38.3",
"effect-app": "workspace:*",
"@effect-app/schema": "workspace:*",
"express-oauth2-jwt-bearer": "^1.6.0",
Expand Down Expand Up @@ -186,6 +188,26 @@
"default": "./_cjs/api/routing/schema/routing.cjs"
}
},
"./api/routing2": {
"import": {
"types": "./dist/api/routing2.d.ts",
"default": "./dist/api/routing2.js"
},
"require": {
"types": "./dist/api/routing2.d.ts",
"default": "./_cjs/api/routing2.cjs"
}
},
"./api/routing2/DynamicMiddleware": {
"import": {
"types": "./dist/api/routing2/DynamicMiddleware.d.ts",
"default": "./dist/api/routing2/DynamicMiddleware.js"
},
"require": {
"types": "./dist/api/routing2/DynamicMiddleware.d.ts",
"default": "./_cjs/api/routing2/DynamicMiddleware.cjs"
}
},
"./api/setupRequest": {
"import": {
"types": "./dist/api/setupRequest.d.ts",
Expand Down
338 changes: 338 additions & 0 deletions packages/infra/src/api/routing2.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,338 @@
/* eslint-disable @typescript-eslint/no-empty-object-type */
/* eslint-disable @typescript-eslint/no-explicit-any */

import type { EffectUnunified } from "@effect-app/core/Effect"
import { typedKeysOf } from "@effect-app/core/utils"
import type { Compute } from "@effect-app/core/utils"
import type { _E, _R } from "@effect-app/infra/api/routing"
import type { Rpc } from "@effect/rpc"
import { RpcRouter } from "@effect/rpc"
import { HttpRpcRouter } from "@effect/rpc-http"
import type { S } from "effect-app"
import { Effect, Predicate } from "effect-app"
import { HttpRouter } from "effect-app/http"
import type { GetEffectContext, Middleware } from "./routing2/DynamicMiddleware.js"
import { makeRpc } from "./routing2/DynamicMiddleware.js"

export interface Hint<Err extends string> {
Err: Err
}

type HandleVoid<Expected, Actual, Result> = [Expected] extends [void]
? [Actual] extends [void] ? Result : Hint<"You're returning non void for a void Response, please fix">
: Result

type AnyRequestModule = S.Schema.Any & { success?: S.Schema.Any; failure?: S.Schema.Any }

type GetSuccess<T> = T extends { success: S.Schema.Any } ? T["success"] : typeof S.Void

type GetSuccessShape<Action extends { success?: S.Schema.Any }, RT extends "d" | "raw"> = RT extends "raw"
? S.Schema.Encoded<GetSuccess<Action>>
: S.Schema.Type<GetSuccess<Action>>
type GetFailure<T extends { failure?: S.Schema.Any }> = T["failure"] extends never ? typeof S.Never : T["failure"]

export interface Handler<Action extends AnyRequestModule, RT extends "raw" | "d", A, E, R, Context> {
new(): {}
_tag: RT
handler: (
req: S.Schema.Type<Action>,
ctx: Context
) => Effect<
A,
E,
R
>
}

// Separate "raw" vs "d" to verify A (Encoded for "raw" vs Type for "d")
type AHandler<Action extends AnyRequestModule> =
| Handler<
Action,
"raw",
S.Schema.Encoded<GetSuccess<Action>>,
S.Schema.Type<GetFailure<Action>>,
any,
{ Response: any }
>
| Handler<
Action,
"d",
S.Schema.Type<GetSuccess<Action>>,
S.Schema.Type<GetFailure<Action>>,
any,
{ Response: any }
>

type Filter<T> = {
[K in keyof T as T[K] extends S.Schema.All & { success: S.Schema.Any; failure: S.Schema.Any } ? K : never]: T[K]
}

export const makeRouter2 = <Context, CTXMap extends Record<string, [string, any, S.Schema.All, any]>>(
middleware: Middleware<Context, CTXMap>
) => {
const rpc = makeRpc(middleware)
function matchFor<Rsc extends Record<string, any> & { meta: { moduleName: string } }>(
rsc: Rsc
) {
const meta = (rsc as any).meta as { moduleName: string }
if (!meta) throw new Error("Resource has no meta specified") // TODO: do something with moduleName+cur etc.

type Filtered = Filter<Rsc>
const filtered = typedKeysOf(rsc).reduce((acc, cur) => {
if (Predicate.isObject(rsc[cur]) && rsc[cur]["success"]) {
acc[cur as keyof Filtered] = rsc[cur]
}
return acc
}, {} as Filtered)

const matchWithServices = <Key extends keyof Filtered>(action: Key) => {
return <
SVC extends Record<
string,
Effect<any, any, any>
>,
R2,
E,
A
>(
_services: SVC,
f: (
req: S.Schema.Type<Rsc[Key]>,
ctx: any
// ctx: Compute<
// LowerServices<EffectDeps<SVC>> & never // ,
// "flat"
// >
) => Effect<A, E, R2>
) =>
(req: any) =>
// Effect.andThen(allLower(services), (svc2) =>
// ...ctx, ...svc2,
f(req, { Response: rsc[action].success })
}

type MatchWithServicesNew<RT extends "raw" | "d", Key extends keyof Rsc> = {
<R2, E, A>(
f: Effect<A, E, R2>
): HandleVoid<
GetSuccessShape<Rsc[Key], RT>,
A,
Handler<
Rsc[Key],
RT,
A,
E,
Exclude<R2, GetEffectContext<CTXMap, Rsc[Key]["config"]>>,
{ Response: Rsc[Key]["success"] } //
>
>

<R2, E, A>(
f: (
req: S.Schema.Type<Rsc[Key]>,
ctx: { Response: Rsc[Key]["success"] }
) => Effect<A, E, R2>
): HandleVoid<
GetSuccessShape<Rsc[Key], RT>,
A,
Handler<
Rsc[Key],
RT,
A,
E,
Exclude<R2, GetEffectContext<CTXMap, Rsc[Key]["config"]>>,
{ Response: Rsc[Key]["success"] } //
>
>

<
SVC extends Record<
string,
EffectUnunified<any, any, any>
>,
R2,
E,
A
>(
services: SVC,
f: (
req: S.Schema.Type<Rsc[Key]>,
ctx: Compute<
// LowerServices<EffectDeps<SVC>> & Pick<Rsc[Key], "success">,
{ Response: Rsc[Key] },
"flat"
>
) => Effect<A, E, R2>
): HandleVoid<
GetSuccessShape<Rsc[Key], RT>,
A,
Handler<
Rsc[Key],
RT,
A,
E,
Exclude<R2, GetEffectContext<CTXMap, Rsc[Key]["config"]>>,
{ Response: Rsc[Key]["success"] } //
>
>
}

type Keys = keyof Filtered

const controllers = <
THandlers extends {
// import to keep them separate via | for type checking!!
[K in Keys]: AHandler<Rsc[K]>
}
>(
controllers: THandlers
) => {
const handlers = typedKeysOf(filtered).reduce(
(acc, cur) => {
if (cur === "meta") return acc
;(acc as any)[cur] = {
h: controllers[cur as keyof typeof controllers].handler,
Request: rsc[cur]
}

return acc
},
{} as {
[K in Keys]: {
h: (
r: S.Schema.Type<Rsc[K]>
) => Effect<
S.Schema.Type<GetSuccess<Rsc[K]>>,
_E<ReturnType<THandlers[K]["handler"]>>,
_R<ReturnType<THandlers[K]["handler"]>>
>
Request: Rsc[K]
}
}
)

const mapped = typedKeysOf(handlers).reduce((acc, cur) => {
const handler = handlers[cur]
const req = handler.Request

acc[cur] = rpc.effect(req, handler.h as any, meta.moduleName) // TODO
return acc
}, {} as any) as {
[K in Keys]: Rpc.Rpc<
Rsc[K],
_R<ReturnType<THandlers[K]["handler"]>>
>
}

type RPCRouteR<T extends Rpc.Rpc<any, any>> = [T] extends [
Rpc.Rpc<any, infer R>
] ? R
: never

type RPCRouteReq<T extends Rpc.Rpc<any, any>> = [T] extends [
Rpc.Rpc<infer Req, any>
] ? Req
: never

const router = RpcRouter.make(...Object.values(mapped) as any) as RpcRouter.RpcRouter<
RPCRouteReq<typeof mapped[keyof typeof mapped]>,
RPCRouteR<typeof mapped[keyof typeof mapped]>
>

return HttpRouter.empty.pipe(
HttpRouter.all(("/rpc/" + rsc.meta.moduleName) as any, HttpRpcRouter.toHttpApp(router))
)
}

const r = {
controllers,
...typedKeysOf(filtered).reduce(
(prev, cur) => {
;(prev as any)[cur] = (svcOrFnOrEffect: any, fnOrNone: any) => {
const stack = new Error().stack?.split("\n").slice(2).join("\n")
return Effect.isEffect(svcOrFnOrEffect)
? class {
static stack = stack
static _tag = "d"
static handler = () => svcOrFnOrEffect
}
: typeof svcOrFnOrEffect === "function"
? class {
static stack = stack
static _tag = "d"
static handler = (req: any, ctx: any) => svcOrFnOrEffect(req, { ...ctx, Response: rsc[cur].success })
}
: class {
static stack = stack
static _tag = "d"
static handler = matchWithServices(cur)(svcOrFnOrEffect, fnOrNone)
}
}
;(prev as any)[(cur as any) + "Raw"] = (svcOrFnOrEffect: any, fnOrNone: any) => {
const stack = new Error().stack?.split("\n").slice(2).join("\n")
return Effect.isEffect(svcOrFnOrEffect)
? class {
static stack = stack
static _tag = "raw"
static handler = () => svcOrFnOrEffect
}
: typeof svcOrFnOrEffect === "function"
? class {
static stack = stack
static _tag = "raw"
static handler = (req: any, ctx: any) => svcOrFnOrEffect(req, { ...ctx, Response: rsc[cur].success })
}
: class {
static stack = stack
static _tag = "raw"
static handler = matchWithServices(cur)(svcOrFnOrEffect, fnOrNone)
}
}
return prev
},
{} as
& {
// use Rsc as Key over using Keys, so that the Go To on X.Action remain in tact in Controllers files
/**
* Requires the Type shape
*/
[Key in keyof Filtered]: MatchWithServicesNew<"d", Key>
}
& {
// use Rsc as Key over using Keys, so that the Go To on X.Action remain in tact in Controllers files
/**
* Requires the Encoded shape (e.g directly undecoded from DB, so that we don't do multiple Decode/Encode)
*/
[Key in keyof Filtered as Key extends string ? `${Key}Raw` : never]: MatchWithServicesNew<"raw", Key>
}
)
}
return r
}

type RequestHandlersTest = {
[key: string]: HttpRouter.HttpRouter<any, any>
}
function matchAll<T extends RequestHandlersTest>(handlers: T) {
const r = typedKeysOf(handlers).reduce((acc, cur) => {
return HttpRouter.concat(acc, handlers[cur] as any)
}, HttpRouter.empty)

type _RRouter<T extends HttpRouter.HttpRouter<any, any>> = [T] extends [
HttpRouter.HttpRouter<any, infer R>
] ? R
: never

type _ERouter<T extends HttpRouter.HttpRouter<any, any>> = [T] extends [
HttpRouter.HttpRouter<infer E, any>
] ? E
: never

return r as HttpRouter.HttpRouter<
_ERouter<typeof handlers[keyof typeof handlers]>,
_RRouter<typeof handlers[keyof typeof handlers]>
>
}

return { matchAll, matchFor }
}
Loading

0 comments on commit 0fa59b0

Please sign in to comment.