1import * as AppSync from "@/AWS/AppSync";
2import * as Lambda from "@/AWS/Lambda";
3import * as Duration from "effect/Duration";
4import * as Effect from "effect/Effect";
5import * as Layer from "effect/Layer";
6import * as Result from "effect/Result";
7import * as Stream from "effect/Stream";
8import { HttpServerRequest } from "effect/http/HttpServerRequest";
9import * as HttpServerResponse from "effect/http/HttpServerResponse";
10import path from "pathe";
11
12const main = path.resolve(import.meta.dirname, "bindings-handler.ts");
13
14const SCHEMA = `
15type Query {
16 add(a: Int!, b: Int!): Int!
17 greeting: String
18}
19schema { query: Query }
20`;
21
22/** NONE data source: compute the sum locally in the resolver. */
23const ADD_RESOLVER = `
24export function request(ctx) {
25 return { payload: ctx.args.a + ctx.args.b };
26}
27export function response(ctx) {
28 return ctx.result;
29}
30`;
31
32/** Reads the API's environment variables (ctx.env) at resolve time. */
33const GREETING_RESOLVER = `
34export function request(ctx) {
35 return { payload: null };
36}
37export function response(ctx) {
38 return ctx.env.GREETING;
39}
40`;
41
42export class AppSyncBindingsFunction extends Lambda.Function<Lambda.Function>()(
43 "AppSyncBindingsFunction",
44) {}
45
46export default AppSyncBindingsFunction.make(
47 {
48 main,
49 functionUrl: true,
50 // The GraphQL round-trip fans out a signed HTTP call; AWS's 3s default
51 // intermittently times out on a cold start.
52 timeout: Duration.seconds(30),
53 },
54 Effect.gen(function* () {
55 const api = yield* AppSync.GraphqlApi("BindingsApi", {
56 authenticationType: "AWS_IAM",
57 schema: SCHEMA,
58 environmentVariables: { GREETING: "hello from ctx.env" },
59 });
60 const local = yield* AppSync.DataSource("LocalDS", {
61 api,
62 type: "NONE",
63 });
64 yield* AppSync.Resolver("AddResolver", {
65 api,
66 typeName: "Query",
67 fieldName: "add",
68 dataSource: local,
69 code: ADD_RESOLVER,
70 });
71 yield* AppSync.Resolver("GreetingResolver", {
72 api,
73 typeName: "Query",
74 fieldName: "greeting",
75 dataSource: local,
76 code: GREETING_RESOLVER,
77 });
78
79 const graphql = yield* AppSync.GraphQL(api);
80 const flushCache = yield* AppSync.FlushApiCache(api);
81 const getSchema = yield* AppSync.GetIntrospectionSchema(api);
82 const evaluateCode = yield* AppSync.EvaluateCode();
83 const evaluateTemplate = yield* AppSync.EvaluateMappingTemplate();
84
85 return {
86 fetch: Effect.gen(function* () {
87 const request = yield* HttpServerRequest;
88 const pathname = new URL(request.originalUrl).pathname;
89
90 if (request.method === "POST" && pathname === "/graphql") {
91 const body = (yield* request.json) as unknown as {
92 query: string;
93 variables?: Record<string, unknown>;
94 };
95 const result = yield* graphql.execute({
96 query: body.query,
97 variables: body.variables,
98 });
99 return yield* HttpServerResponse.json(result);
100 }
101
102 if (request.method === "POST" && pathname === "/flush") {
103 // Surface the typed outcome tag to the test — the API has no
104 // cache provisioned, so the expected result is the typed
105 // NotFoundException (which proves the call executed with the
106 // granted appsync:FlushApiCache).
107 const outcome = yield* Effect.result(flushCache());
108 return yield* HttpServerResponse.json(
109 Result.isSuccess(outcome)
110 ? { flushed: true }
111 : {
112 flushed: false,
113 reason: outcome.failure._tag,
114 message: String(
115 (outcome.failure as { message?: string }).message ?? "",
116 ),
117 },
118 );
119 }
120
121 if (request.method === "GET" && pathname === "/schema") {
122 // The introspected schema arrives as a streaming body.
123 const response = yield* getSchema({ format: "SDL" });
124 const sdl = yield* Stream.mkString(
125 Stream.decodeText(response.schema!),
126 );
127 return yield* HttpServerResponse.json({ sdl });
128 }
129
130 if (request.method === "POST" && pathname === "/evaluate") {
131 // Evaluate the add resolver's request function against a mock
132 // context — evaluation errors surface on `result.error`.
133 const result = yield* evaluateCode({
134 runtime: AppSync.APPSYNC_JS,
135 code: ADD_RESOLVER,
136 context: JSON.stringify({ arguments: { a: 2, b: 3 } }),
137 function: "request",
138 });
139 return yield* HttpServerResponse.json({
140 evaluationResult: result.evaluationResult,
141 error: result.error,
142 });
143 }
144
145 if (request.method === "POST" && pathname === "/evaluate-template") {
146 // Render a VTL template against a mock context — template
147 // evaluation errors surface on `result.error`.
148 const result = yield* evaluateTemplate({
149 // VTL only allows arithmetic inside #set directives.
150 template: [
151 `#set($sum = $ctx.args.a + $ctx.args.b)`,
152 `{ "sum": $util.toJson($sum) }`,
153 ].join("\n"),
154 context: JSON.stringify({ arguments: { a: 2, b: 3 } }),
155 });
156 return yield* HttpServerResponse.json({
157 evaluationResult: result.evaluationResult,
158 error: result.error,
159 });
160 }
161
162 if (request.method === "GET" && pathname === "/ping") {
163 return yield* HttpServerResponse.json({ ok: true });
164 }
165
166 return yield* HttpServerResponse.json(
167 { error: "Not found", method: request.method, pathname },
168 { status: 404 },
169 );
170 }).pipe(Effect.orDie),
171 };
172 }).pipe(
173 Effect.provide(
174 Layer.mergeAll(
175 AppSync.GraphQLHttp,
176 AppSync.FlushApiCacheHttp,
177 AppSync.GetIntrospectionSchemaHttp,
178 AppSync.EvaluateCodeHttp,
179 AppSync.EvaluateMappingTemplateHttp,
180 ),
181 ),
182 ),
183);