1import * as quicksight from "@distilled.cloud/aws/quicksight";
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 { createInternalTags, hasAlchemyTags } from "../../Tags.ts";
10import { AWSEnvironment } from "../Environment.ts";
11import type { Providers } from "../Providers.ts";
12import {
13 readQuickSightTags,
14 syncQuickSightTags,
15 toWireTags,
16 waitForSettled,
17} from "./internal.ts";
18
19/**
20 * Properties for an Amazon QuickSight analysis — an editable workspace built
21 * from a template or an inline definition, which can be published as a
22 * dashboard.
23 */
24export interface AnalysisProps {
25 /**
26 * Unique id of the analysis within the account. Stable — changing it
27 * replaces the analysis. If omitted, a unique id is generated.
28 */
29 analysisId?: string;
30
31 /**
32 * Display name of the analysis.
33 */
34 name: string;
35
36 /**
37 * Source of the analysis content — a template to clone. Provide either
38 * `sourceEntity` or `definition`.
39 */
40 sourceEntity?: quicksight.AnalysisSourceEntity;
41
42 /**
43 * Inline definition of the analysis content. Provide either `definition`
44 * or `sourceEntity`.
45 */
46 definition?: quicksight.AnalysisDefinition;
47
48 /**
49 * Parameters passed to the analysis's datasets.
50 */
51 parameters?: quicksight.Parameters;
52
53 /**
54 * Resource-level permissions on the analysis.
55 */
56 permissions?: quicksight.ResourcePermission[];
57
58 /**
59 * ARN of the theme applied to the analysis.
60 */
61 themeArn?: string;
62
63 /**
64 * When deleting, whether to skip the recovery window and delete
65 * immediately. Recommended for ephemeral/test analyses so they don't
66 * linger in a recoverable state.
67 * @default true
68 */
69 forceDeleteWithoutRecovery?: boolean;
70
71 /**
72 * Tags to apply to the analysis.
73 */
74 tags?: Record<string, string>;
75}
76
77export interface Analysis extends Resource<
78 "AWS.QuickSight.Analysis",
79 AnalysisProps,
80 {
81 /** Unique id of the analysis within the account. */
82 analysisId: string;
83 /** ARN of the analysis. */
84 arn: string;
85 /** Display name of the analysis. */
86 name: string;
87 /** Current lifecycle status (e.g. `CREATION_SUCCESSFUL`). */
88 status: string;
89 },
90 never,
91 Providers
92> {}
93
94/**
95 * An Amazon QuickSight analysis — an editable workspace built from a template
96 * or an inline definition, which can be published as a dashboard.
97 *
98 * QuickSight requires an active account subscription in the region. Without
99 * one, create operations fail with the typed `QuickSightSubscriptionRequired`
100 * error.
101 *
102 * ### Creating an Analysis
103 * **Example:** Analysis from a Template
104 * ```typescript
105 * const analysis = yield* Analysis("explore-sales", {
106 * name: "Explore Sales",
107 * sourceEntity: {
108 * SourceTemplate: {
109 * Arn: templateArn,
110 * DataSetReferences: [
111 * { DataSetPlaceholder: "sales", DataSetArn: dataset.arn },
112 * ],
113 * },
114 * },
115 * });
116 * ```
117 *
118 * @resource
119 */
120export const Analysis = Resource<Analysis>("AWS.QuickSight.Analysis");
121
122export const AnalysisProvider = () =>
123 Provider.effect(
124 Analysis,
125 Effect.gen(function* () {
126 const toId = (id: string, props: Partial<AnalysisProps>) =>
127 props.analysisId
128 ? Effect.succeed(props.analysisId)
129 : createPhysicalName({ id, maxLength: 64 });
130
131 const readAnalysis = Effect.fn(function* (
132 accountId: string,
133 analysisId: string,
134 ) {
135 const response = yield* quicksight
136 .describeAnalysis({ AwsAccountId: accountId, AnalysisId: analysisId })
137 .pipe(
138 Effect.catchTag("ResourceNotFoundException", () =>
139 Effect.succeed(undefined),
140 ),
141 );
142 const analysis = response?.Analysis;
143 if (analysis === undefined || analysis.Status === "DELETED") {
144 return undefined;
145 }
146 return analysis;
147 });
148
149 const settle = (accountId: string, analysisId: string) =>
150 waitForSettled(
151 analysisId,
152 readAnalysis(accountId, analysisId).pipe(
153 Effect.map((a) =>
154 a === undefined ? undefined : { ...a, status: a.Status },
155 ),
156 ),
157 );
158
159 const toAttrs = (analysis: quicksight.Analysis) => ({
160 analysisId: analysis.AnalysisId!,
161 arn: analysis.Arn!,
162 name: analysis.Name ?? "",
163 status: analysis.Status ?? "",
164 });
165
166 return Analysis.Provider.of({
167 stables: ["analysisId", "arn"],
168
169 diff: Effect.fn(function* ({ id, olds = {}, news }) {
170 if (!isResolved(news)) return undefined;
171 if ((yield* toId(id, olds)) !== (yield* toId(id, news))) {
172 return { action: "replace" } as const;
173 }
174 }),
175
176 read: Effect.fn(function* ({ id, olds = {}, output }) {
177 const { accountId } = yield* AWSEnvironment.current;
178 const analysisId = output?.analysisId ?? (yield* toId(id, olds));
179 const analysis = yield* readAnalysis(accountId, analysisId);
180 if (analysis === undefined) return undefined;
181 const attrs = toAttrs(analysis);
182 const tags = yield* readQuickSightTags(attrs.arn);
183 return (yield* hasAlchemyTags(id, tags)) ? attrs : Unowned(attrs);
184 }),
185
186 reconcile: Effect.fn(function* ({ id, news, output, session }) {
187 const { accountId } = yield* AWSEnvironment.current;
188 const analysisId = output?.analysisId ?? (yield* toId(id, news));
189 const internalTags = yield* createInternalTags(id);
190 const desiredTags = { ...internalTags, ...news.tags };
191
192 // 1. Observe.
193 let observed = yield* readAnalysis(accountId, analysisId);
194
195 // 2. Ensure — create if missing.
196 if (observed === undefined) {
197 yield* quicksight
198 .createAnalysis({
199 AwsAccountId: accountId,
200 AnalysisId: analysisId,
201 Name: news.name,
202 SourceEntity: news.sourceEntity,
203 Definition: news.definition,
204 Parameters: news.parameters,
205 Permissions: news.permissions,
206 ThemeArn: news.themeArn,
207 Tags: toWireTags(desiredTags),
208 })
209 .pipe(
210 Effect.catchTag("ResourceExistsException", () => Effect.void),
211 );
212 } else {
213 // 3. Sync.
214 yield* quicksight.updateAnalysis({
215 AwsAccountId: accountId,
216 AnalysisId: analysisId,
217 Name: news.name,
218 SourceEntity: news.sourceEntity,
219 Definition: news.definition,
220 Parameters: news.parameters,
221 ThemeArn: news.themeArn,
222 });
223 }
224
225 observed = yield* settle(accountId, analysisId);
226 if (observed === undefined) {
227 return yield* Effect.fail(
228 new Error(
229 `QuickSight analysis '${analysisId}' not found after reconcile`,
230 ),
231 );
232 }
233
234 // 3b. Sync tags.
235 yield* syncQuickSightTags(observed.Arn!, desiredTags);
236
237 yield* session.note(analysisId);
238 return toAttrs(observed);
239 }),
240
241 delete: Effect.fn(function* ({ olds, output }) {
242 const { accountId } = yield* AWSEnvironment.current;
243 yield* quicksight
244 .deleteAnalysis({
245 AwsAccountId: accountId,
246 AnalysisId: output.analysisId,
247 ForceDeleteWithoutRecovery:
248 olds?.forceDeleteWithoutRecovery ?? true,
249 })
250 .pipe(
251 Effect.catchTag("ResourceNotFoundException", () => Effect.void),
252 );
253 }),
254
255 list: () =>
256 Effect.gen(function* () {
257 const { accountId } = yield* AWSEnvironment.current;
258 return yield* quicksight.listAnalyses
259 .pages({ AwsAccountId: accountId })
260 .pipe(
261 Stream.runCollect,
262 Effect.map((chunk) =>
263 Array.from(chunk)
264 .flatMap((page) => page.AnalysisSummaryList ?? [])
265 .flatMap((s) =>
266 s.AnalysisId !== undefined &&
267 s.Arn !== undefined &&
268 s.Status !== "DELETED"
269 ? [
270 {
271 analysisId: s.AnalysisId,
272 arn: s.Arn,
273 name: s.Name ?? "",
274 status: s.Status ?? "",
275 },
276 ]
277 : [],
278 ),
279 ),
280 );
281 }),
282 });
283 }),
284 );