1import * as ag from "@distilled.cloud/aws/api-gateway";
2import * as Effect from "effect/Effect";
3import * as Stream from "effect/Stream";
4import { isResolved } from "../../Diff.ts";
5import type { Input } from "../../Input.ts";
6import * as Provider from "../../Provider.ts";
7import { Resource } from "../../Resource.ts";
8import type { Providers } from "../Providers.ts";
9import { retryOnApiStatusUpdating } from "./common.ts";
10
11export interface GatewayResponseProps {
12 /** ID of the REST API the response mapping belongs to. */
13 restApiId: Input<string>;
14 /** The gateway response type to customize (e.g. `DEFAULT_4XX`, `ACCESS_DENIED`). */
15 responseType: ag.GatewayResponseType;
16 /** HTTP status code to return; defaults to the response type's standard code. */
17 statusCode?: string;
18 /** Response header parameters (e.g. `gatewayresponse.header.X-Foo`) as mapping expressions. */
19 responseParameters?: { [key: string]: string | undefined };
20 /** Response body mapping templates keyed by content type. */
21 responseTemplates?: { [key: string]: string | undefined };
22}
23
24/** @resource */
25export interface GatewayResponse extends Resource<
26 "AWS.ApiGateway.GatewayResponse",
27 GatewayResponseProps,
28 {
29 restApiId: string;
30 responseType: ag.GatewayResponseType;
31 statusCode: string | undefined;
32 },
33 never,
34 Providers
35> {}
36
37/**
38 * Gateway response mapping for a REST API (e.g. DEFAULT_4XX, DEFAULT_5XX).
39 *
40 * ### Gateway responses
41 * **Example:** Default 4xx JSON body
42 * ```typescript
43 * yield* ApiGateway.GatewayResponse("Default4xx", {
44 * restApiId: api.restApiId,
45 * responseType: "DEFAULT_4XX",
46 * responseTemplates: { "application/json": '{"message":$context.error.messageString}' },
47 * });
48 * ```
49 */
50const GatewayResponseResource = Resource<GatewayResponse>(
51 "AWS.ApiGateway.GatewayResponse",
52);
53
54export { GatewayResponseResource as GatewayResponse };
55
56export const GatewayResponseProvider = () =>
57 Provider.effect(
58 GatewayResponseResource,
59 Effect.gen(function* () {
60 return {
61 stables: ["restApiId", "responseType"] as const,
62 diff: Effect.fn(function* ({ news: newsIn, olds }) {
63 if (!isResolved(newsIn)) return;
64 const news = newsIn as Input.ResolveProps<GatewayResponseProps>;
65 if (
66 news.restApiId !== olds.restApiId ||
67 news.responseType !== olds.responseType
68 ) {
69 return { action: "replace" } as const;
70 }
71 }),
72 read: Effect.fn(function* ({ output }) {
73 if (!output) return undefined;
74 const g = yield* ag
75 .getGatewayResponse({
76 restApiId: output.restApiId,
77 responseType: output.responseType,
78 })
79 .pipe(
80 Effect.catchTag("NotFoundException", () =>
81 Effect.succeed(undefined),
82 ),
83 );
84 if (!g?.responseType) return undefined;
85 return {
86 restApiId: output.restApiId,
87 responseType: g.responseType!,
88 statusCode: g.statusCode,
89 };
90 }),
91 // GatewayResponses are sub-resources of a RestApi, so enumerate every
92 // RestApi first (paginated), then list the gateway responses per api.
93 list: () =>
94 Effect.gen(function* () {
95 const restApiIds = yield* ag.getRestApis.pages({}).pipe(
96 Stream.runCollect,
97 Effect.map((chunk) =>
98 Array.from(chunk).flatMap((page) =>
99 (page.items ?? [])
100 .map((a) => a.id)
101 .filter((id): id is string => id != null),
102 ),
103 ),
104 );
105 const rows = yield* Effect.forEach(
106 restApiIds,
107 (restApiId) =>
108 ag.getGatewayResponses({ restApiId }).pipe(
109 Effect.map((res) =>
110 (res.items ?? [])
111 .filter(
112 (
113 g,
114 ): g is ag.GatewayResponse & {
115 responseType: ag.GatewayResponseType;
116 } => g.responseType != null,
117 )
118 .map((g) => ({
119 restApiId,
120 responseType: g.responseType,
121 statusCode: g.statusCode,
122 })),
123 ),
124 Effect.catchTag("NotFoundException", () =>
125 Effect.succeed([]),
126 ),
127 ),
128 { concurrency: 10 },
129 );
130 return rows.flat();
131 }),
132 reconcile: Effect.fn(function* ({ news: newsIn, output, session }) {
133 if (!isResolved(newsIn)) {
134 return yield* Effect.die("GatewayResponse props were not resolved");
135 }
136 const news = newsIn as Input.ResolveProps<GatewayResponseProps>;
137 const restApiId = (output?.restApiId ?? news.restApiId) as string;
138 const responseType = output?.responseType ?? news.responseType;
139
140 // Observe + Ensure + Sync as one atomic op.
141 // `putGatewayResponse` is an upsert: it creates the response if
142 // missing, replaces it if present. The reconciler is therefore a
143 // single put against the desired state regardless of whether
144 // `output` exists.
145 yield* retryOnApiStatusUpdating(
146 ag.putGatewayResponse({
147 restApiId,
148 responseType,
149 statusCode: news.statusCode,
150 responseParameters: news.responseParameters,
151 responseTemplates: news.responseTemplates,
152 }),
153 );
154 yield* session.note(
155 `Reconciled gateway response ${responseType} on ${restApiId}`,
156 );
157 return {
158 restApiId,
159 responseType,
160 statusCode: news.statusCode,
161 };
162 }),
163 delete: Effect.fn(function* ({ output, session }) {
164 yield* retryOnApiStatusUpdating(
165 ag
166 .deleteGatewayResponse({
167 restApiId: output.restApiId,
168 responseType: output.responseType,
169 })
170 .pipe(Effect.catchTag("NotFoundException", () => Effect.void)),
171 );
172 yield* session.note(
173 `Deleted gateway response ${output.responseType}`,
174 );
175 }),
176 };
177 }),
178 );