1import * as appregistry from "@distilled.cloud/aws/service-catalog-appregistry";
2import * as Effect from "effect/Effect";
3import * as Stream from "effect/Stream";
4import { Unowned } from "../../AdoptPolicy.ts";
5import { isResolved } from "../../Diff.ts";
6import { createPhysicalName } from "../../PhysicalName.ts";
7import * as Provider from "../../Provider.ts";
8import { Resource } from "../../Resource.ts";
9import {
10 createInternalTags,
11 diffTags,
12 hasAlchemyTags,
13 tagRecord,
14} from "../../Tags.ts";
15import type { Providers } from "../Providers.ts";
16import { clientToken, stripAwsSystemTags } from "./internal.ts";
17
18export interface ApplicationProps {
19 /**
20 * Name of the application. Must be unique in the account and region and
21 * may only contain letters, numbers, dots, dashes, and underscores.
22 * If omitted, a unique name is generated. Changing it replaces the
23 * application.
24 */
25 applicationName?: string;
26 /**
27 * Description of the application. Updatable in place.
28 */
29 description?: string;
30 /**
31 * Tags to apply to the application. Merged with internal Alchemy tags.
32 */
33 tags?: Record<string, string>;
34}
35
36export interface Application extends Resource<
37 "AWS.AppRegistry.Application",
38 ApplicationProps,
39 {
40 /** The auto-generated application ID. */
41 applicationId: string;
42 /** The ARN of the application. */
43 applicationArn: string;
44 /** The name of the application. */
45 applicationName: string;
46 },
47 never,
48 Providers
49> {}
50
51/**
52 * An AWS Service Catalog AppRegistry application — the top-level node that
53 * groups related cloud resources and metadata under a single logical
54 * application (surfaced in myApplications and the `awsApplication` tag).
55 *
56 * ### Creating an Application
57 * **Example:** Basic Application
58 * ```typescript
59 * import * as AppRegistry from "alchemy/AWS/AppRegistry";
60 *
61 * const app = yield* AppRegistry.Application("Storefront", {});
62 * ```
63 *
64 * **Example:** Application with Description and Tags
65 * ```typescript
66 * const app = yield* AppRegistry.Application("Storefront", {
67 * applicationName: "storefront",
68 * description: "Customer-facing storefront services",
69 * tags: { team: "commerce" },
70 * });
71 * ```
72 *
73 * @resource
74 */
75export const Application = Resource<Application>("AWS.AppRegistry.Application");
76
77export const ApplicationProvider = () =>
78 Provider.effect(
79 Application,
80 Effect.gen(function* () {
81 const createName = Effect.fn(function* (
82 id: string,
83 props: Pick<ApplicationProps, "applicationName">,
84 ) {
85 return (
86 props.applicationName ??
87 (yield* createPhysicalName({ id, maxLength: 256 }))
88 );
89 });
90
91 // getApplication accepts a name, ID, or ARN specifier.
92 const observe = Effect.fn(function* (specifier: string) {
93 return yield* appregistry
94 .getApplication({ application: specifier })
95 .pipe(
96 Effect.catchTag("ResourceNotFoundException", () =>
97 Effect.succeed(undefined),
98 ),
99 );
100 });
101
102 return Application.Provider.of({
103 stables: ["applicationId", "applicationArn", "applicationName"],
104 list: () =>
105 appregistry.listApplications.pages({}).pipe(
106 Stream.runCollect,
107 Effect.map((chunk) =>
108 Array.from(chunk)
109 .flatMap((page) => page.applications ?? [])
110 .filter((a) => a.id != null && a.arn != null && a.name != null)
111 .map((a) => ({
112 applicationId: a.id!,
113 applicationArn: a.arn!,
114 applicationName: a.name!,
115 })),
116 ),
117 ),
118 read: Effect.fn(function* ({ id, olds, output }) {
119 const specifier =
120 output?.applicationId ?? (yield* createName(id, olds ?? {}));
121 const found = yield* observe(specifier);
122 if (!found?.id) return undefined;
123 const attrs = {
124 applicationId: found.id,
125 applicationArn: found.arn!,
126 applicationName: found.name!,
127 };
128 return (yield* hasAlchemyTags(id, tagRecord(found.tags)))
129 ? attrs
130 : Unowned(attrs);
131 }),
132 // The application name is its user-facing identity — changing it
133 // replaces the application.
134 diff: Effect.fn(function* ({ id, olds, news }) {
135 if (!isResolved(news)) return undefined;
136 const oldName = yield* createName(id, olds);
137 const newName = yield* createName(id, news);
138 if (oldName !== newName) return { action: "replace" } as const;
139 }),
140 reconcile: Effect.fn(function* ({
141 id,
142 news,
143 output,
144 session,
145 instanceId,
146 }) {
147 const applicationName = yield* createName(id, news);
148 const internalTags = yield* createInternalTags(id);
149 const desiredTags: Record<string, string> = {
150 ...news.tags,
151 ...internalTags,
152 };
153
154 // 1. OBSERVE — cloud is authoritative; output caches the ID only.
155 let found = output?.applicationId
156 ? yield* observe(output.applicationId)
157 : undefined;
158 if (!found?.id) {
159 found = yield* observe(applicationName);
160 }
161
162 // 2. ENSURE — create when missing; tolerate a concurrent-create
163 // race (ConflictException) by re-reading.
164 if (!found?.id) {
165 yield* appregistry
166 .createApplication({
167 name: applicationName,
168 description: news.description,
169 tags: desiredTags,
170 clientToken: clientToken(instanceId),
171 })
172 .pipe(Effect.catchTag("ConflictException", () => Effect.void));
173 found = yield* observe(applicationName);
174 }
175
176 const applicationId = found!.id!;
177 const applicationArn = found!.arn!;
178
179 // 3a. SYNC description — apply only when it actually changed.
180 if (
181 news.description !== undefined &&
182 found!.description !== news.description
183 ) {
184 yield* appregistry.updateApplication({
185 application: applicationId,
186 description: news.description,
187 });
188 }
189
190 // 3b. SYNC tags — diff against OBSERVED cloud tags so adoption
191 // converges.
192 const observedTags = stripAwsSystemTags(tagRecord(found!.tags));
193 const { upsert, removed } = diffTags(observedTags, desiredTags);
194 if (upsert.length > 0) {
195 yield* appregistry.tagResource({
196 resourceArn: applicationArn,
197 tags: Object.fromEntries(upsert.map((t) => [t.Key, t.Value])),
198 });
199 }
200 if (removed.length > 0) {
201 yield* appregistry.untagResource({
202 resourceArn: applicationArn,
203 tagKeys: removed,
204 });
205 }
206
207 yield* session.note(applicationId);
208 return {
209 applicationId,
210 applicationArn,
211 applicationName,
212 };
213 }),
214 delete: Effect.fn(function* ({ output }) {
215 yield* appregistry
216 .deleteApplication({ application: output.applicationId })
217 .pipe(
218 Effect.catchTag("ResourceNotFoundException", () => Effect.void),
219 );
220 }),
221 });
222 }),
223 );