1import * as appregistry from "@distilled.cloud/aws/service-catalog-appregistry";
2import * as Effect from "effect/Effect";
3import * as Stream from "effect/Stream";
4import { isResolved } from "../../Diff.ts";
5import * as Provider from "../../Provider.ts";
6import { Resource } from "../../Resource.ts";
7import { AWSEnvironment } from "../Environment.ts";
8import type { Providers } from "../Providers.ts";
9
10export interface AttributeGroupAssociationProps {
11 /**
12 * The application to associate the attribute group with: its ID, name, or
13 * ARN. Changing it replaces the association.
14 */
15 application: string;
16 /**
17 * The attribute group to associate: its ID, name, or ARN. Changing it
18 * replaces the association.
19 */
20 attributeGroup: string;
21}
22
23export interface AttributeGroupAssociation extends Resource<
24 "AWS.AppRegistry.AttributeGroupAssociation",
25 AttributeGroupAssociationProps,
26 {
27 /** The ID of the associated application. */
28 applicationId: string;
29 /** The ARN of the associated application. */
30 applicationArn: string;
31 /** The ID of the associated attribute group. */
32 attributeGroupId: string;
33 /** The ARN of the associated attribute group. */
34 attributeGroupArn: string;
35 },
36 never,
37 Providers
38> {}
39
40/**
41 * Associates an AppRegistry {@link AttributeGroup} with an
42 * {@link Application} so the group's user-defined JSON metadata augments the
43 * application's machine-readable description.
44 *
45 * ### Associating an Attribute Group
46 * **Example:** Attach an Attribute Group to an Application
47 * ```typescript
48 * import * as AppRegistry from "alchemy/AWS/AppRegistry";
49 *
50 * const app = yield* AppRegistry.Application("Storefront", {});
51 * const group = yield* AppRegistry.AttributeGroup("Ownership", {
52 * attributes: { owner: "commerce-team" },
53 * });
54 *
55 * const association = yield* AppRegistry.AttributeGroupAssociation("Assoc", {
56 * application: app.applicationId,
57 * attributeGroup: group.attributeGroupId,
58 * });
59 * ```
60 *
61 * @resource
62 */
63export const AttributeGroupAssociation = Resource<AttributeGroupAssociation>(
64 "AWS.AppRegistry.AttributeGroupAssociation",
65);
66
67export const AttributeGroupAssociationProvider = () =>
68 Provider.effect(
69 AttributeGroupAssociation,
70 Effect.gen(function* () {
71 // getApplication / getAttributeGroup both accept a name, ID, or ARN.
72 const observeApplication = Effect.fn(function* (specifier: string) {
73 return yield* appregistry
74 .getApplication({ application: specifier })
75 .pipe(
76 Effect.catchTag("ResourceNotFoundException", () =>
77 Effect.succeed(undefined),
78 ),
79 );
80 });
81 const observeAttributeGroup = Effect.fn(function* (specifier: string) {
82 return yield* appregistry
83 .getAttributeGroup({ attributeGroup: specifier })
84 .pipe(
85 Effect.catchTag("ResourceNotFoundException", () =>
86 Effect.succeed(undefined),
87 ),
88 );
89 });
90
91 const isAssociated = Effect.fn(function* (
92 applicationId: string,
93 attributeGroupId: string,
94 ) {
95 const groupIds = yield* appregistry.listAssociatedAttributeGroups
96 .pages({ application: applicationId })
97 .pipe(
98 Stream.runCollect,
99 Effect.map((chunk) =>
100 Array.from(chunk).flatMap((page) => page.attributeGroups ?? []),
101 ),
102 );
103 return groupIds.includes(attributeGroupId);
104 });
105
106 return AttributeGroupAssociation.Provider.of({
107 stables: [
108 "applicationId",
109 "applicationArn",
110 "attributeGroupId",
111 "attributeGroupArn",
112 ],
113 // The application/attribute-group pair IS the association's identity —
114 // changing either side replaces the association.
115 diff: Effect.fn(function* ({ olds, news }) {
116 if (!isResolved(news)) return undefined;
117 if (
118 olds !== undefined &&
119 (olds.application !== news.application ||
120 olds.attributeGroup !== news.attributeGroup)
121 ) {
122 return { action: "replace" } as const;
123 }
124 }),
125 read: Effect.fn(function* ({ olds, output }) {
126 const applicationSpecifier =
127 output?.applicationId ?? olds?.application;
128 const attributeGroupSpecifier =
129 output?.attributeGroupId ?? olds?.attributeGroup;
130 if (
131 applicationSpecifier === undefined ||
132 attributeGroupSpecifier === undefined
133 ) {
134 return undefined;
135 }
136 const application = yield* observeApplication(applicationSpecifier);
137 if (application?.id === undefined) return undefined;
138 const attributeGroup = yield* observeAttributeGroup(
139 attributeGroupSpecifier,
140 );
141 if (attributeGroup?.id === undefined) return undefined;
142 if (!(yield* isAssociated(application.id, attributeGroup.id))) {
143 return undefined;
144 }
145 // Associations cannot carry tags; existence between our two parents
146 // is the ownership signal.
147 return {
148 applicationId: application.id,
149 applicationArn: application.arn!,
150 attributeGroupId: attributeGroup.id,
151 attributeGroupArn: attributeGroup.arn!,
152 };
153 }),
154 reconcile: Effect.fn(function* ({ news, session }) {
155 // 1. OBSERVE — resolve both sides to canonical IDs; a missing parent
156 // surfaces its typed ResourceNotFoundException.
157 const application = yield* appregistry.getApplication({
158 application: news.application,
159 });
160 const attributeGroup = yield* appregistry.getAttributeGroup({
161 attributeGroup: news.attributeGroup,
162 });
163 const applicationId = application.id!;
164 const attributeGroupId = attributeGroup.id!;
165
166 // 2. ENSURE — associate when missing; a ConflictException is a
167 // concurrent-associate race, not a failure.
168 if (!(yield* isAssociated(applicationId, attributeGroupId))) {
169 yield* appregistry
170 .associateAttributeGroup({
171 application: applicationId,
172 attributeGroup: attributeGroupId,
173 })
174 .pipe(Effect.catchTag("ConflictException", () => Effect.void));
175 }
176
177 yield* session.note(`${applicationId}/${attributeGroupId}`);
178 return {
179 applicationId,
180 applicationArn: application.arn!,
181 attributeGroupId,
182 attributeGroupArn: attributeGroup.arn!,
183 };
184 }),
185 delete: Effect.fn(function* ({ output }) {
186 yield* appregistry
187 .disassociateAttributeGroup({
188 application: output.applicationId,
189 attributeGroup: output.attributeGroupId,
190 })
191 .pipe(
192 Effect.catchTag("ResourceNotFoundException", () => Effect.void),
193 );
194 }),
195 list: () =>
196 Effect.gen(function* () {
197 const { accountId, region } = yield* AWSEnvironment.current;
198 const applications = yield* appregistry.listApplications
199 .pages({})
200 .pipe(
201 Stream.runCollect,
202 Effect.map((chunk) =>
203 Array.from(chunk).flatMap((page) => page.applications ?? []),
204 ),
205 );
206 const results: {
207 applicationId: string;
208 applicationArn: string;
209 attributeGroupId: string;
210 attributeGroupArn: string;
211 }[] = [];
212 for (const application of applications) {
213 if (application.id === undefined) continue;
214 const groupIds = yield* appregistry.listAssociatedAttributeGroups
215 .pages({ application: application.id })
216 .pipe(
217 Stream.runCollect,
218 Effect.map((chunk) =>
219 Array.from(chunk).flatMap(
220 (page) => page.attributeGroups ?? [],
221 ),
222 ),
223 );
224 for (const attributeGroupId of groupIds) {
225 results.push({
226 applicationId: application.id,
227 applicationArn: application.arn!,
228 attributeGroupId,
229 attributeGroupArn: `arn:aws:servicecatalog:${region}:${accountId}:/attribute-groups/${attributeGroupId}`,
230 });
231 }
232 }
233 return results;
234 }),
235 });
236 }),
237 );