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";
9
10export interface UsagePlanKeyProps {
11 /** ID of the usage plan to add the key to. */
12 usagePlanId: Input<string>;
13 /** ID of the API key to associate. */
14 keyId: Input<string>;
15 /**
16 * @default "API_KEY"
17 */
18 keyType?: string;
19}
20
21/** @resource */
22export interface UsagePlanKey extends Resource<
23 "AWS.ApiGateway.UsagePlanKey",
24 UsagePlanKeyProps,
25 {
26 usagePlanId: string;
27 keyId: string;
28 keyType: string;
29 name: string | undefined;
30 },
31 never,
32 Providers
33> {}
34
35/**
36 * Associates an API key with a usage plan.
37 *
38 * ### Usage plan keys
39 * **Example:** Associate key with plan
40 * ```typescript
41 * yield* ApiGateway.UsagePlanKey("PlanKey", {
42 * usagePlanId: plan.id,
43 * keyId: key.id,
44 * });
45 * ```
46 */
47const UsagePlanKeyResource = Resource<UsagePlanKey>(
48 "AWS.ApiGateway.UsagePlanKey",
49);
50
51export { UsagePlanKeyResource as UsagePlanKey };
52
53export const UsagePlanKeyProvider = () =>
54 Provider.effect(
55 UsagePlanKeyResource,
56 Effect.gen(function* () {
57 return {
58 stables: ["usagePlanId", "keyId"] as const,
59 diff: Effect.fn(function* ({ news: newsIn, olds }) {
60 if (!isResolved(newsIn)) return;
61 const news = newsIn as UsagePlanKeyProps;
62 if (
63 news.usagePlanId !== olds.usagePlanId ||
64 news.keyId !== olds.keyId
65 ) {
66 return { action: "replace" } as const;
67 }
68 }),
69 read: Effect.fn(function* ({ output }) {
70 if (!output) return undefined;
71 const k = yield* ag
72 .getUsagePlanKey({
73 usagePlanId: output.usagePlanId,
74 keyId: output.keyId,
75 })
76 .pipe(
77 Effect.catchTag("NotFoundException", () =>
78 Effect.succeed(undefined),
79 ),
80 );
81 if (!k?.id) return undefined;
82 return {
83 usagePlanId: output.usagePlanId,
84 keyId: k.id,
85 keyType: k.type ?? "API_KEY",
86 name: k.name,
87 };
88 }),
89 reconcile: Effect.fn(function* ({ news: newsIn, output, session }) {
90 if (!isResolved(newsIn)) {
91 return yield* Effect.die("UsagePlanKey props were not resolved");
92 }
93 const news = newsIn as Input.ResolveProps<UsagePlanKeyProps>;
94 const usagePlanId = (output?.usagePlanId ??
95 news.usagePlanId) as string;
96 const keyId = (output?.keyId ?? news.keyId) as string;
97
98 // Observe — fetch the live link. UsagePlanKey is a pure
99 // association (no patchable fields apart from the natural-key
100 // tuple, which is modeled as `replace` in `diff`), so once it
101 // exists there's nothing to sync.
102 let observed = yield* ag
103 .getUsagePlanKey({ usagePlanId, keyId })
104 .pipe(
105 Effect.catchTag("NotFoundException", () =>
106 Effect.succeed(undefined),
107 ),
108 );
109
110 // Ensure — create the link if missing.
111 if (!observed?.id) {
112 const created = yield* ag.createUsagePlanKey({
113 usagePlanId: news.usagePlanId as string,
114 keyId: news.keyId as string,
115 keyType: news.keyType ?? "API_KEY",
116 });
117 yield* session.note(
118 `Linked key ${news.keyId} to usage plan ${news.usagePlanId}`,
119 );
120 observed = yield* ag.getUsagePlanKey({
121 usagePlanId,
122 keyId: created.id ?? keyId,
123 });
124 }
125
126 return {
127 usagePlanId,
128 keyId: observed.id!,
129 keyType: observed.type ?? "API_KEY",
130 name: observed.name,
131 };
132 }),
133 // UsagePlanKey is a sub-resource keyed by {usagePlanId, keyId}. There
134 // is no account-wide enumeration, so enumerate every usage plan first
135 // (getUsagePlans) then list keys per plan (getUsagePlanKeys).
136 list: () =>
137 Effect.gen(function* () {
138 const usagePlanIds = yield* ag.getUsagePlans.pages({}).pipe(
139 Stream.runCollect,
140 Effect.map((chunk) =>
141 Array.from(chunk).flatMap((page) =>
142 (page.items ?? [])
143 .filter(
144 (p): p is ag.UsagePlan & { id: string } => p.id != null,
145 )
146 .map((p) => p.id),
147 ),
148 ),
149 );
150 const rows = yield* Effect.forEach(
151 usagePlanIds,
152 (usagePlanId) =>
153 ag.getUsagePlanKeys.pages({ usagePlanId }).pipe(
154 Stream.runCollect,
155 Effect.map((chunk) =>
156 Array.from(chunk).flatMap((page) =>
157 (page.items ?? [])
158 .filter(
159 (k): k is ag.UsagePlanKey & { id: string } =>
160 k.id != null,
161 )
162 .map((k) => ({
163 usagePlanId,
164 keyId: k.id,
165 keyType: k.type ?? "API_KEY",
166 name: k.name,
167 })),
168 ),
169 ),
170 ),
171 { concurrency: 10 },
172 );
173 return rows.flat();
174 }),
175 delete: Effect.fn(function* ({ output, session }) {
176 yield* ag
177 .deleteUsagePlanKey({
178 usagePlanId: output.usagePlanId,
179 keyId: output.keyId,
180 })
181 .pipe(Effect.catchTag("NotFoundException", () => Effect.void));
182 yield* session.note(`Removed key from usage plan`);
183 }),
184 };
185 }),
186 );