1import * as contacts from "@distilled.cloud/aws/ssm-contacts";
2import * as Effect from "effect/Effect";
3import { isResolved } from "../../Diff.ts";
4import * as Provider from "../../Provider.ts";
5import { Resource } from "../../Resource.ts";
6import type { Providers } from "../Providers.ts";
7
8export interface PlanProps {
9 /**
10 * The ARN of the contact or escalation plan whose engagement plan this
11 * resource manages. Changing it replaces the plan.
12 */
13 contactId: string;
14
15 /**
16 * The stages that Incident Manager runs through when engaging the contact:
17 * each stage has a duration and a set of channel/contact targets.
18 */
19 stages?: contacts.Stage[];
20
21 /**
22 * ARNs of the on-call rotations associated with the plan (only valid for
23 * `ONCALL_SCHEDULE` contacts).
24 */
25 rotationIds?: string[];
26}
27
28/** @resource */
29export interface Plan extends Resource<
30 "AWS.SSMContacts.Plan",
31 PlanProps,
32 {
33 /** ARN of the contact whose engagement plan is managed. */
34 contactArn: string;
35 /** Number of stages in the plan. */
36 stageCount: number;
37 },
38 never,
39 Providers
40> {}
41
42/**
43 * The engagement plan of an Incident Manager contact — the staged sequence
44 * of contact channels (for `PERSONAL` contacts), contacts (for `ESCALATION`
45 * plans), or rotations (for `ONCALL_SCHEDULE` contacts) that Incident
46 * Manager engages during an incident.
47 *
48 * Manage a contact's plan either inline via the `plan` prop on
49 * `SSMContacts.Contact` or standalone with this resource — not both.
50 * Deleting this resource resets the contact's plan to empty.
51 *
52 * ### Managing Engagement Plans
53 * **Example:** Engage an email channel for 5 minutes
54 * ```typescript
55 * const plan = yield* SSMContacts.Plan("OncallPlan", {
56 * contactId: oncall.contactArn,
57 * stages: [
58 * {
59 * DurationInMinutes: 5,
60 * Targets: [
61 * {
62 * ChannelTargetInfo: {
63 * ContactChannelId: email.contactChannelArn,
64 * RetryIntervalInMinutes: 1,
65 * },
66 * },
67 * ],
68 * },
69 * ],
70 * });
71 * ```
72 */
73const PlanResource = Resource<Plan>("AWS.SSMContacts.Plan");
74
75export { PlanResource as Plan };
76
77export const PlanProvider = () =>
78 Provider.effect(
79 PlanResource,
80 Effect.gen(function* () {
81 const getContact = (arn: string) =>
82 contacts
83 .getContact({ ContactId: arn })
84 .pipe(
85 Effect.catchTag("ResourceNotFoundException", () =>
86 Effect.succeed(undefined),
87 ),
88 );
89
90 const buildAttrs = (contact: contacts.GetContactResult) => ({
91 contactArn: contact.ContactArn,
92 stageCount: contact.Plan.Stages?.length ?? 0,
93 });
94
95 return PlanResource.Provider.of({
96 stables: ["contactArn"],
97
98 // Settings sub-resource of a contact — nothing to enumerate.
99 list: () => Effect.succeed([]),
100
101 read: Effect.fn(function* ({ olds, output }) {
102 const arn = output?.contactArn ?? olds?.contactId;
103 if (arn === undefined) return undefined;
104 const contact = yield* getContact(arn);
105 if (contact === undefined) return undefined;
106 // The plan is a setting on the contact, not a distinct cloud
107 // object — it "exists" when the contact has stages or rotations.
108 const hasPlan =
109 (contact.Plan.Stages?.length ?? 0) > 0 ||
110 (contact.Plan.RotationIds?.length ?? 0) > 0;
111 return hasPlan ? buildAttrs(contact) : undefined;
112 }),
113
114 diff: Effect.fn(function* ({ news, olds }) {
115 if (!isResolved(news)) return undefined;
116 if (olds.contactId !== news.contactId) {
117 return { action: "replace" } as const;
118 }
119 }),
120
121 reconcile: Effect.fn(function* ({ news, session }) {
122 // 1. OBSERVE — the contact is authoritative for its current plan.
123 const contact = yield* contacts.getContact({
124 ContactId: news.contactId,
125 });
126
127 // 2/3. SYNC — apply the desired plan when it differs from the
128 // observed one (updateContact fully replaces the plan).
129 const desired: contacts.Plan = {
130 Stages: news.stages ?? [],
131 ...(news.rotationIds !== undefined
132 ? { RotationIds: news.rotationIds }
133 : {}),
134 };
135 const observed: contacts.Plan = {
136 Stages: contact.Plan.Stages ?? [],
137 ...(contact.Plan.RotationIds !== undefined
138 ? { RotationIds: contact.Plan.RotationIds }
139 : {}),
140 };
141 if (JSON.stringify(desired) !== JSON.stringify(observed)) {
142 yield* contacts.updateContact({
143 ContactId: contact.ContactArn,
144 Plan: desired,
145 });
146 }
147
148 // 4. RETURN fresh attributes.
149 const final = yield* contacts.getContact({
150 ContactId: news.contactId,
151 });
152 yield* session.note(final.ContactArn);
153 return buildAttrs(final);
154 }),
155
156 delete: Effect.fn(function* ({ output }) {
157 // Deleting the plan resets the contact's engagement plan. A
158 // vanished contact means there is nothing left to reset.
159 yield* contacts
160 .updateContact({
161 ContactId: output.contactArn,
162 Plan: { Stages: [] },
163 })
164 .pipe(
165 Effect.asVoid,
166 Effect.catchTag("ResourceNotFoundException", () => Effect.void),
167 );
168 }),
169 });
170 }),
171 );