1import * as cip from "@distilled.cloud/aws/cognito-identity-provider";
2import * as Effect from "effect/Effect";
3import { isResolved } from "../../Diff.ts";
4import { createPhysicalName } from "../../PhysicalName.ts";
5import * as Provider from "../../Provider.ts";
6import { Resource } from "../../Resource.ts";
7import type { Providers } from "../Providers.ts";
8
9export interface GroupProps {
10 /**
11 * The ID of the user pool the group belongs to. Changing this triggers a
12 * replacement.
13 */
14 userPoolId: string;
15 /**
16 * Name of the group. If omitted, a deterministic physical name is
17 * generated from the app, stage, and logical ID. Changing this triggers a
18 * replacement.
19 */
20 groupName?: string;
21 /**
22 * Description of the group (up to 2048 characters).
23 */
24 description?: string;
25 /**
26 * ARN of an IAM role associated with the group. Users in the group can
27 * assume this role via an identity pool (`cognito:roles` /
28 * `cognito:preferred_role` claims).
29 */
30 roleArn?: string;
31 /**
32 * Non-negative precedence; lower values take priority when a user belongs
33 * to multiple groups with role ARNs.
34 */
35 precedence?: number;
36}
37
38export interface Group extends Resource<
39 "AWS.Cognito.Group",
40 GroupProps,
41 {
42 /** The name of the group. */
43 groupName: string;
44 /** The ID of the user pool the group belongs to. */
45 userPoolId: string;
46 },
47 never,
48 Providers
49> {}
50
51/**
52 * A group within an Amazon Cognito user pool. Groups organize users, appear
53 * in the `cognito:groups` token claim, and can carry an IAM role for
54 * identity-pool federation.
55 * ### Creating Groups
56 * **Example:** Basic Group
57 * ```typescript
58 * import * as Cognito from "alchemy/AWS/Cognito";
59 *
60 * const pool = yield* Cognito.UserPool("Users", {});
61 * const admins = yield* Cognito.Group("Admins", {
62 * userPoolId: pool.userPoolId,
63 * description: "Administrators",
64 * });
65 * ```
66 *
67 * **Example:** Group with Role and Precedence
68 * ```typescript
69 * const admins = yield* Cognito.Group("Admins", {
70 * userPoolId: pool.userPoolId,
71 * roleArn: role.roleArn,
72 * precedence: 1,
73 * });
74 * ```
75 *
76 * @resource
77 */
78export const Group = Resource<Group>("AWS.Cognito.Group");
79
80export const GroupProvider = () =>
81 Provider.effect(
82 Group,
83 Effect.gen(function* () {
84 const createName = Effect.fn(function* (
85 id: string,
86 props: Pick<GroupProps, "groupName">,
87 ) {
88 return (
89 props.groupName ?? (yield* createPhysicalName({ id, maxLength: 128 }))
90 );
91 });
92
93 const getGroup = Effect.fn(function* (
94 userPoolId: string,
95 groupName: string,
96 ) {
97 return yield* cip
98 .getGroup({ UserPoolId: userPoolId, GroupName: groupName })
99 .pipe(
100 Effect.map((r) => r.Group),
101 Effect.catchTag("ResourceNotFoundException", () =>
102 Effect.succeed(undefined),
103 ),
104 );
105 });
106
107 return Group.Provider.of({
108 stables: ["groupName", "userPoolId"],
109
110 // Sub-resource keyed entirely by its user pool (userPoolId) with no global
111 // enumeration API of its own — nuke reaches it through the parent's
112 // deletion, so enumeration returns empty per the ProviderService
113 // doctrine.
114 list: () => Effect.succeed([]),
115
116 read: Effect.fn(function* ({ id, olds, output }) {
117 const userPoolId = output?.userPoolId ?? olds?.userPoolId;
118 if (userPoolId === undefined) return undefined;
119 const name = output?.groupName ?? (yield* createName(id, olds ?? {}));
120 const observed = yield* getGroup(userPoolId, name);
121 return observed === undefined
122 ? undefined
123 : { groupName: name, userPoolId };
124 }),
125
126 diff: Effect.fn(function* ({ id, news, olds }) {
127 if (!isResolved(news)) return undefined;
128 const oldName = yield* createName(id, olds ?? {});
129 const newName = yield* createName(id, news ?? {});
130 if (oldName !== newName || olds?.userPoolId !== news?.userPoolId) {
131 return { action: "replace" } as const;
132 }
133 }),
134
135 reconcile: Effect.fn(function* ({ id, news, output, session }) {
136 const groupName = output?.groupName ?? (yield* createName(id, news));
137 const userPoolId = news.userPoolId;
138
139 // 1. OBSERVE
140 let observed = yield* getGroup(userPoolId, groupName);
141
142 // 2. ENSURE — tolerate the create race.
143 if (observed === undefined) {
144 observed = yield* cip
145 .createGroup({
146 UserPoolId: userPoolId,
147 GroupName: groupName,
148 Description: news.description,
149 RoleArn: news.roleArn,
150 Precedence: news.precedence,
151 })
152 .pipe(
153 Effect.map((r) => r.Group),
154 Effect.catchTag("GroupExistsException", () =>
155 getGroup(userPoolId, groupName),
156 ),
157 );
158 } else {
159 // 3. SYNC — description/role/precedence are mutable in place.
160 const drift =
161 (news.description !== undefined &&
162 observed.Description !== news.description) ||
163 (news.roleArn !== undefined &&
164 observed.RoleArn !== news.roleArn) ||
165 (news.precedence !== undefined &&
166 observed.Precedence !== news.precedence) ||
167 (news.description === undefined &&
168 observed.Description !== undefined) ||
169 (news.roleArn === undefined && observed.RoleArn !== undefined) ||
170 (news.precedence === undefined &&
171 observed.Precedence !== undefined);
172 if (drift) {
173 yield* cip.updateGroup({
174 UserPoolId: userPoolId,
175 GroupName: groupName,
176 Description: news.description,
177 RoleArn: news.roleArn,
178 Precedence: news.precedence,
179 });
180 }
181 }
182
183 yield* session.note(groupName);
184 return { groupName, userPoolId };
185 }),
186
187 delete: Effect.fn(function* ({ output }) {
188 yield* cip
189 .deleteGroup({
190 UserPoolId: output.userPoolId,
191 GroupName: output.groupName,
192 })
193 .pipe(
194 Effect.catchTag("ResourceNotFoundException", () => Effect.void),
195 );
196 }),
197 });
198 }),
199 );