Skip to content

Commit e5d3219

Browse files
committed
Add basic Graph Controllers
1 parent 45b40c7 commit e5d3219

File tree

4 files changed

+759
-1
lines changed

4 files changed

+759
-1
lines changed

_project/api/_src/Usecases.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
// codegen:start {preset: barrel, include: ./Usecases/*.ts, export}
22
export * from "./Usecases/Blog.Controllers.js"
3+
export * from "./Usecases/Graph.Controllers.js"
34
export * from "./Usecases/HelloWorld.Controllers.js"
45
export * from "./Usecases/Me.Controllers.js"
56
export * from "./Usecases/Operations.Controllers.js"
Lines changed: 175 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,175 @@
1+
/**
2+
* @experimental
3+
*/
4+
5+
import type { CTX } from "@/lib/routing.js"
6+
import { BasicRequestEnv } from "@effect-app-boilerplate/messages/RequestLayers"
7+
import { GraphRsc } from "@effect-app-boilerplate/resources"
8+
import type { GraphMutationResponse } from "@effect-app-boilerplate/resources/Graph/Mutation"
9+
import type { GraphQueryRequest, GraphQueryResponse } from "@effect-app-boilerplate/resources/Graph/Query"
10+
import { dropUndefined } from "@effect-app/core/utils"
11+
import { makeRequestId, RequestContext } from "@effect-app/infra/RequestContext"
12+
import { BlogControllers } from "./Blog.Controllers.js"
13+
14+
// TODO: Apply roles&rights to individual actions.
15+
16+
const NoResponse = Effect(undefined)
17+
18+
function request<Key extends string>(
19+
req: Partial<Record<Key, { input?: any } | undefined>>,
20+
context: CTX
21+
) {
22+
return <RKey extends Key, R, E, A>(
23+
name: RKey,
24+
handler: (inp: any, ctx: CTX) => Effect<R, E, A>
25+
) => {
26+
const q = req[name]
27+
return q
28+
? Effect.gen(function*($) {
29+
const ctx = yield* $(RequestContext.Tag.access)
30+
const childCtx = RequestContext.inherit(ctx, {
31+
id: makeRequestId(),
32+
locale: ctx.locale,
33+
name: ReasonableString(name) // TODO: Use name from handler.Request
34+
})
35+
36+
const r = yield* $(
37+
handler(q.input ?? {}, { ...context, context: childCtx }).provideSomeContextEffect(
38+
BasicRequestEnv(
39+
childCtx
40+
)
41+
)
42+
)
43+
return r
44+
})["|>"](Effect.$.either)
45+
: NoResponse
46+
}
47+
}
48+
49+
const { controllers, matchWithEffect } = matchFor(GraphRsc)
50+
51+
// TODO: Auto generate from the clients
52+
const Query = matchWithEffect("Query")(
53+
Effect.gen(function*($) {
54+
const blogPostControllers = yield* $(BlogControllers)
55+
return (req, ctx) =>
56+
Effect.gen(function*($) {
57+
const handle = request(req, ctx)
58+
const r: GraphQueryResponse = yield* $(
59+
Effect.structPar({
60+
// TODO: AllMe currently has a temporal requirement; it must be executed first. (therefore atm requested first..)
61+
// for two reasons: 1. create the user if it didnt exist yet.
62+
// 2. because if the user changes locale, its stored on the user object, and put in cache for the follow-up handlers.
63+
// AllMe: handle("AllMe", AllMe.h),
64+
// AllMeCommentActivity: handle("AllMeCommentActivity", AllMeCommentActivity.h),
65+
// AllMeChangeRequests: handle("AllMeChangeRequests", AllMeChangeRequests.h),
66+
// AllMeEventlog: handle("AllMeEventlog", AllMeEventlog.h),
67+
// AllMeTasks: handle("AllMeTasks", AllMeTasks.h),
68+
69+
// FindPurchaseOrder: handle("FindPurchaseOrder", FindPurchaseOrder.h)
70+
FindBlogPost: handle("FindBlogPost", blogPostControllers.FindPost.h)
71+
})
72+
)
73+
return dropUndefined(r as any) // TODO: Fix optional encoder should ignore undefined values!
74+
})
75+
})
76+
)
77+
78+
const emptyResponse = Effect(undefined)
79+
80+
function mutation<Key extends string>(
81+
req: Partial<Record<Key, { input?: any; query?: GraphQueryRequest } | undefined>>,
82+
ctx: CTX
83+
) {
84+
const f = request(req, ctx)
85+
return <RKey extends Key, R, E, A, R2, E2, A2>(
86+
name: RKey,
87+
handler: (inp: any, ctx: CTX) => Effect<R, E, A>,
88+
resultQuery?: (inp: A, ctx: CTX) => Effect<R2, E2, A2>
89+
) => {
90+
const q = req[name]
91+
return f(name, handler).flatMap(x =>
92+
!x
93+
? Effect(x)
94+
: x.isLeft()
95+
? Effect(x)
96+
: (q?.query
97+
? Effect.structPar({
98+
query: Query.flatMap(_ => _(q.query!, ctx)),
99+
result: resultQuery ? resultQuery(x.right, ctx) : emptyResponse
100+
}).map(({ query, result }) => ({ ...query, result })) // TODO: Replace $ variables in the query parameters baed on mutation output!
101+
: emptyResponse).map(query => Either(query ? { query, response: x.right } : { response: x.right }))
102+
)
103+
}
104+
}
105+
106+
const Mutation = matchWithEffect("Mutation")(
107+
Effect.gen(function*($) {
108+
const blogPostControllers = yield* $(BlogControllers)
109+
return (req, ctx) =>
110+
Effect.gen(function*($) {
111+
const handle = mutation(req, ctx)
112+
const r: GraphMutationResponse = yield* $(
113+
Effect.structPar({
114+
CreatePost: handle(
115+
"CreatePost",
116+
blogPostControllers.CreatePost.h,
117+
(id, ctx) =>
118+
blogPostControllers.FindPost.h({ id }, ctx)
119+
.flatMap(x => (!x ? Effect.die("Post went away?") : Effect(x)))
120+
)
121+
// UpdatePurchaseOrder: handle("UpdatePurchaseOrder", UpdatePurchaseOrder.h, () =>
122+
// FindPurchaseOrder.h(req.UpdatePurchaseOrder!.input).flatMap(x =>
123+
// !x ?
124+
// Effect.die("PO went away?") :
125+
// Effect(x)
126+
// ))
127+
})
128+
)
129+
return dropUndefined(r as any) // TODO: Fix optional encoder should ignore undefined values!
130+
})
131+
})
132+
)
133+
134+
// TODO: Implement for mutations, with optional query on return
135+
// function createAndRetrievePO() {
136+
// const mutation = {
137+
// "PurchaseOrders.Create": {
138+
// input: { title: "dude" },
139+
// // optional
140+
// query: {
141+
// "PurchaseOrders.Get": {
142+
// input: {
143+
// id: "$id" /* $ prefix means it should be taken from response output */,
144+
// },
145+
// },
146+
// },
147+
// },
148+
// }
149+
// type Response = {
150+
// "PurchaseOrders.Create": Either<
151+
// MutationError,
152+
// {
153+
// result: {
154+
// id: "some id"
155+
// }
156+
// query: {
157+
// "PurchaseOrders.Get": Either<
158+
// QueryError,
159+
// {
160+
// id: "some id"
161+
// title: "dude"
162+
// }
163+
// >
164+
// }
165+
// }
166+
// >
167+
// }
168+
// }
169+
170+
export const GraphControllers = controllers(Effect.struct({ Query, Mutation }))
171+
172+
// export const GraphControllers = Effect.struct({
173+
// GraphQuery: match(GraphQuery, defaultErrorHandler, handleRequestEnv),
174+
// GraphMutation: match(GraphMutation, defaultErrorHandler, handleRequestEnv)
175+
// })

_project/api/_src/lib/routing.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -136,7 +136,7 @@ export type RouteAllTest<T extends RequestHandlersTest> = {
136136
type ContextA<X> = X extends Context<infer A> ? A : never
137137
export type RequestEnv = ContextA<Effect.Success<ReturnType<ReturnType<typeof RequestEnv>>>>
138138

139-
function handleRequestEnv<
139+
export function handleRequestEnv<
140140
R,
141141
PathA,
142142
CookieA,

0 commit comments

Comments
 (0)