1import * as appconfig from "@distilled.cloud/aws/appconfig";
2import * as Effect from "effect/Effect";
3import * as Schedule from "effect/Schedule";
4import * as Stream from "effect/Stream";
5import { Unowned } from "../../AdoptPolicy.ts";
6import { isResolved } from "../../Diff.ts";
7import { createPhysicalName } from "../../PhysicalName.ts";
8import * as Provider from "../../Provider.ts";
9import { Resource } from "../../Resource.ts";
10import { createInternalTags, hasAlchemyTags } from "../../Tags.ts";
11import { AWSEnvironment } from "../Environment.ts";
12import type { Providers } from "../Providers.ts";
13import {
14 applicationArn,
15 readAppConfigTags,
16 syncAppConfigTags,
17} from "./internal.ts";
18
19export interface ApplicationProps {
20 /**
21 * Name of the application. Must be 1-64 characters. If omitted, a
22 * deterministic physical name is generated. Changing the name replaces the
23 * application.
24 */
25 applicationName?: string;
26 /**
27 * Description of the application.
28 */
29 description?: string;
30 /**
31 * User-defined tags for the application.
32 */
33 tags?: Record<string, string>;
34}
35
36export interface Application extends Resource<
37 "AWS.AppConfig.Application",
38 ApplicationProps,
39 {
40 applicationId: string;
41 applicationName: string;
42 applicationArn: string;
43 },
44 never,
45 Providers
46> {}
47
48/**
49 * An AWS AppConfig application — the top-level container that groups the
50 * environments and configuration profiles for one application's configuration.
51 *
52 * ### Creating an Application
53 * **Example:** Basic Application
54 * ```typescript
55 * const app = yield* AppConfig.Application("MyApp", {
56 * description: "Configuration for my service",
57 * });
58 * ```
59 *
60 * **Example:** Named Application with Tags
61 * ```typescript
62 * const app = yield* AppConfig.Application("MyApp", {
63 * applicationName: "my-service",
64 * tags: { team: "platform" },
65 * });
66 * ```
67 *
68 * @resource
69 */
70export const Application = Resource<Application>("AWS.AppConfig.Application");
71
72export const ApplicationProvider = () =>
73 Provider.effect(
74 Application,
75 Effect.gen(function* () {
76 const toName = (id: string, props: Partial<ApplicationProps>) =>
77 props.applicationName
78 ? Effect.succeed(props.applicationName)
79 : createPhysicalName({ id, maxLength: 64 });
80
81 /** Find an application by name (get requires the generated id). */
82 const findByName = Effect.fn(function* (name: string) {
83 const apps = yield* appconfig.listApplications.pages({}).pipe(
84 Stream.runCollect,
85 Effect.map((chunk) =>
86 Array.from(chunk).flatMap((page) => page.Items ?? []),
87 ),
88 );
89 return apps.find((a) => a.Name === name);
90 });
91
92 const readApplication = Effect.fn(function* (applicationId: string) {
93 return yield* appconfig
94 .getApplication({ ApplicationId: applicationId })
95 .pipe(
96 Effect.catchTag("ResourceNotFoundException", () =>
97 Effect.succeed(undefined),
98 ),
99 );
100 });
101
102 return {
103 stables: ["applicationId", "applicationName", "applicationArn"],
104
105 diff: Effect.fn(function* ({ id, olds, news }) {
106 if (!isResolved(news)) return undefined;
107 if (
108 (yield* toName(id, olds ?? {})) !== (yield* toName(id, news ?? {}))
109 ) {
110 return { action: "replace" } as const;
111 }
112 }),
113
114 read: Effect.fn(function* ({ id, olds, output }) {
115 const { accountId, region } = yield* AWSEnvironment.current;
116 const app = output?.applicationId
117 ? yield* readApplication(output.applicationId)
118 : yield* findByName(yield* toName(id, olds ?? {}));
119 if (app?.Id === undefined) return undefined;
120 const arn = applicationArn(region, accountId, app.Id);
121 const attrs = {
122 applicationId: app.Id,
123 applicationName: app.Name!,
124 applicationArn: arn,
125 };
126 const tags = yield* readAppConfigTags(arn);
127 return (yield* hasAlchemyTags(id, tags)) ? attrs : Unowned(attrs);
128 }),
129
130 reconcile: Effect.fn(function* ({ id, news, output, session }) {
131 const { accountId, region } = yield* AWSEnvironment.current;
132 const name = output?.applicationName ?? (yield* toName(id, news));
133 const internalTags = yield* createInternalTags(id);
134 const desiredTags = { ...internalTags, ...news.tags };
135
136 // 1. Observe — cloud state is authoritative; output is only a cache.
137 let observed = output?.applicationId
138 ? yield* readApplication(output.applicationId)
139 : undefined;
140 if (observed === undefined) {
141 observed = yield* findByName(name);
142 }
143
144 // 2. Ensure — create if missing.
145 if (observed?.Id === undefined) {
146 observed = yield* appconfig.createApplication({
147 Name: name,
148 Description: news.description,
149 Tags: desiredTags,
150 });
151 } else if (
152 news.description !== undefined &&
153 observed.Description !== news.description
154 ) {
155 // 3. Sync — description is mutable in place.
156 observed = yield* appconfig.updateApplication({
157 ApplicationId: observed.Id,
158 Description: news.description,
159 });
160 }
161
162 const arn = applicationArn(region, accountId, observed.Id!);
163
164 // 3b. Sync tags — diff against OBSERVED cloud tags.
165 yield* syncAppConfigTags(arn, desiredTags);
166
167 yield* session.note(name);
168 return {
169 applicationId: observed.Id!,
170 applicationName: observed.Name ?? name,
171 applicationArn: arn,
172 };
173 }),
174
175 delete: Effect.fn(function* ({ output }) {
176 // An application cannot be deleted while environments or
177 // configuration profiles still exist under it ("Cannot delete
178 // application ..., because there are still environments existing
179 // under it" — surfaced as BadRequestException). Children are
180 // deleted first by the engine/nuke, but their deletion is
181 // eventually consistent, so absorb the window with a bounded retry.
182 yield* appconfig
183 .deleteApplication({ ApplicationId: output.applicationId })
184 .pipe(
185 Effect.retry({
186 while: (e): boolean => e._tag === "BadRequestException",
187 schedule: Schedule.fixed("3 seconds"),
188 times: 5,
189 }),
190 Effect.catchTag("ResourceNotFoundException", () => Effect.void),
191 );
192 }),
193
194 list: () =>
195 Effect.gen(function* () {
196 const { accountId, region } = yield* AWSEnvironment.current;
197 const apps = yield* appconfig.listApplications.pages({}).pipe(
198 Stream.runCollect,
199 Effect.map((chunk) =>
200 Array.from(chunk).flatMap((page) => page.Items ?? []),
201 ),
202 );
203 return apps.flatMap((a) =>
204 a.Id !== undefined && a.Name !== undefined
205 ? [
206 {
207 applicationId: a.Id,
208 applicationName: a.Name,
209 applicationArn: applicationArn(region, accountId, a.Id),
210 },
211 ]
212 : [],
213 );
214 }),
215 };
216 }),
217 );