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} from "./internal.ts";
17
18/**
19 * Properties for an Amazon QuickSight dataset — a prepared, queryable model
20 * built on top of one or more data sources.
21 */
22export interface DataSetProps {
23 /**
24 * Unique id of the dataset within the account. Stable — changing it
25 * replaces the dataset. If omitted, a unique id is generated.
26 */
27 dataSetId?: string;
28
29 /**
30 * Display name of the dataset.
31 */
32 name: string;
33
34 /**
35 * Declares the physical tables in the dataset, keyed by a physical table
36 * id. Each maps to a relational table, custom SQL, or an S3 source.
37 */
38 physicalTableMap: { [key: string]: quicksight.PhysicalTable | undefined };
39
40 /**
41 * Whether the data is imported into SPICE or queried directly.
42 */
43 importMode: quicksight.DataSetImportMode;
44
45 /**
46 * Declares logical tables (joins, transforms) keyed by logical table id.
47 */
48 logicalTableMap?: { [key: string]: quicksight.LogicalTable | undefined };
49
50 /**
51 * Column groupings (e.g. geospatial hierarchies).
52 */
53 columnGroups?: quicksight.ColumnGroup[];
54
55 /**
56 * Field folders that organize the dataset's fields.
57 */
58 fieldFolders?: { [key: string]: quicksight.FieldFolder | undefined };
59
60 /**
61 * Resource-level permissions on the dataset.
62 */
63 permissions?: quicksight.ResourcePermission[];
64
65 /**
66 * Row-level permission configuration backed by a permissions dataset.
67 */
68 rowLevelPermissionDataSet?: quicksight.RowLevelPermissionDataSet;
69
70 /**
71 * Usage configuration controlling how the dataset can be used
72 * (e.g. as a source for other datasets).
73 */
74 dataSetUsageConfiguration?: quicksight.DataSetUsageConfiguration;
75
76 /**
77 * Parameters exposed by the dataset.
78 */
79 datasetParameters?: quicksight.DatasetParameter[];
80
81 /**
82 * Tags to apply to the dataset.
83 */
84 tags?: Record<string, string>;
85}
86
87export interface DataSet extends Resource<
88 "AWS.QuickSight.DataSet",
89 DataSetProps,
90 {
91 /** Unique id of the dataset within the account. */
92 dataSetId: string;
93 /** ARN of the dataset. */
94 arn: string;
95 /** Display name of the dataset. */
96 name: string;
97 },
98 never,
99 Providers
100> {}
101
102/**
103 * An Amazon QuickSight dataset — a prepared, queryable model built on top of
104 * one or more data sources.
105 *
106 * QuickSight requires an active account subscription in the region. Without
107 * one, create operations fail with the typed `QuickSightSubscriptionRequired`
108 * error.
109 *
110 * ### Creating a Dataset
111 * **Example:** SPICE Dataset from a Relational Table
112 * ```typescript
113 * const dataset = yield* DataSet("sales", {
114 * name: "Sales",
115 * importMode: "SPICE",
116 * physicalTableMap: {
117 * sales: {
118 * RelationalTable: {
119 * DataSourceArn: source.arn,
120 * Name: "sales",
121 * InputColumns: [{ Name: "amount", Type: "DECIMAL" }],
122 * },
123 * },
124 * },
125 * });
126 * ```
127 *
128 * @resource
129 */
130export const DataSet = Resource<DataSet>("AWS.QuickSight.DataSet");
131
132export const DataSetProvider = () =>
133 Provider.effect(
134 DataSet,
135 Effect.gen(function* () {
136 const toId = (id: string, props: Partial<DataSetProps>) =>
137 props.dataSetId
138 ? Effect.succeed(props.dataSetId)
139 : createPhysicalName({ id, maxLength: 64 });
140
141 const readSet = Effect.fn(function* (
142 accountId: string,
143 dataSetId: string,
144 ) {
145 const response = yield* quicksight
146 .describeDataSet({ AwsAccountId: accountId, DataSetId: dataSetId })
147 .pipe(
148 Effect.catchTag("ResourceNotFoundException", () =>
149 Effect.succeed(undefined),
150 ),
151 );
152 return response?.DataSet;
153 });
154
155 const toAttrs = (set: quicksight.DataSet) => ({
156 dataSetId: set.DataSetId!,
157 arn: set.Arn!,
158 name: set.Name ?? "",
159 });
160
161 return DataSet.Provider.of({
162 stables: ["dataSetId", "arn"],
163
164 diff: Effect.fn(function* ({ id, olds = {}, news }) {
165 if (!isResolved(news)) return undefined;
166 if ((yield* toId(id, olds)) !== (yield* toId(id, news))) {
167 return { action: "replace" } as const;
168 }
169 }),
170
171 read: Effect.fn(function* ({ id, olds = {}, output }) {
172 const { accountId } = yield* AWSEnvironment.current;
173 const dataSetId = output?.dataSetId ?? (yield* toId(id, olds));
174 const set = yield* readSet(accountId, dataSetId);
175 if (set === undefined) return undefined;
176 const attrs = toAttrs(set);
177 const tags = yield* readQuickSightTags(attrs.arn);
178 return (yield* hasAlchemyTags(id, tags)) ? attrs : Unowned(attrs);
179 }),
180
181 reconcile: Effect.fn(function* ({ id, news, output, session }) {
182 const { accountId } = yield* AWSEnvironment.current;
183 const dataSetId = output?.dataSetId ?? (yield* toId(id, news));
184 const internalTags = yield* createInternalTags(id);
185 const desiredTags = { ...internalTags, ...news.tags };
186
187 // 1. Observe.
188 let observed = yield* readSet(accountId, dataSetId);
189
190 // 2. Ensure — create if missing (tolerate AlreadyExists race).
191 if (observed === undefined) {
192 yield* quicksight
193 .createDataSet({
194 AwsAccountId: accountId,
195 DataSetId: dataSetId,
196 Name: news.name,
197 PhysicalTableMap: news.physicalTableMap,
198 ImportMode: news.importMode,
199 LogicalTableMap: news.logicalTableMap,
200 ColumnGroups: news.columnGroups,
201 FieldFolders: news.fieldFolders,
202 Permissions: news.permissions,
203 RowLevelPermissionDataSet: news.rowLevelPermissionDataSet,
204 DataSetUsageConfiguration: news.dataSetUsageConfiguration,
205 DatasetParameters: news.datasetParameters,
206 Tags: toWireTags(desiredTags),
207 })
208 .pipe(
209 Effect.catchTag("ResourceExistsException", () => Effect.void),
210 );
211 } else {
212 // 3. Sync — idempotent update of the dataset definition.
213 yield* quicksight.updateDataSet({
214 AwsAccountId: accountId,
215 DataSetId: dataSetId,
216 Name: news.name,
217 PhysicalTableMap: news.physicalTableMap,
218 ImportMode: news.importMode,
219 LogicalTableMap: news.logicalTableMap,
220 ColumnGroups: news.columnGroups,
221 FieldFolders: news.fieldFolders,
222 RowLevelPermissionDataSet: news.rowLevelPermissionDataSet,
223 DataSetUsageConfiguration: news.dataSetUsageConfiguration,
224 DatasetParameters: news.datasetParameters,
225 });
226 }
227
228 observed = yield* readSet(accountId, dataSetId);
229 if (observed === undefined) {
230 return yield* Effect.fail(
231 new Error(
232 `QuickSight dataset '${dataSetId}' not found after reconcile`,
233 ),
234 );
235 }
236
237 // 3b. Sync tags.
238 yield* syncQuickSightTags(observed.Arn!, desiredTags);
239
240 yield* session.note(dataSetId);
241 return toAttrs(observed);
242 }),
243
244 delete: Effect.fn(function* ({ output }) {
245 const { accountId } = yield* AWSEnvironment.current;
246 yield* quicksight
247 .deleteDataSet({
248 AwsAccountId: accountId,
249 DataSetId: output.dataSetId,
250 })
251 .pipe(
252 Effect.catchTag("ResourceNotFoundException", () => Effect.void),
253 );
254 }),
255
256 list: () =>
257 Effect.gen(function* () {
258 const { accountId } = yield* AWSEnvironment.current;
259 return yield* quicksight.listDataSets
260 .pages({ AwsAccountId: accountId })
261 .pipe(
262 Stream.runCollect,
263 Effect.map((chunk) =>
264 Array.from(chunk)
265 .flatMap((page) => page.DataSetSummaries ?? [])
266 .flatMap((s) =>
267 s.DataSetId !== undefined && s.Arn !== undefined
268 ? [
269 {
270 dataSetId: s.DataSetId,
271 arn: s.Arn,
272 name: s.Name ?? "",
273 },
274 ]
275 : [],
276 ),
277 ),
278 );
279 }),
280 });
281 }),
282 );