1import * as AWS from "@/AWS";
2import { Dataset, Project, Recipe } from "@/AWS/DataBrew";
3import { Role } from "@/AWS/IAM";
4import { Bucket } from "@/AWS/S3";
5import * as Test from "@/Test/Alchemy";
6import * as databrew from "@distilled.cloud/aws/databrew";
7import * as s3 from "@distilled.cloud/aws/s3";
8import { expect } from "alchemy-test";
9import * as Effect from "effect/Effect";
10
11const { test } = Test.make({ providers: AWS.providers() });
12
13const getProject = (name: string) =>
14 databrew
15 .describeProject({ Name: name })
16 .pipe(
17 Effect.catchTag("ResourceNotFoundException", () =>
18 Effect.succeed(undefined),
19 ),
20 );
21
22const brewRole = () =>
23 Role("DataBrewProjectRole", {
24 assumeRolePolicyDocument: {
25 Version: "2012-10-17",
26 Statement: [
27 {
28 Effect: "Allow",
29 Principal: { Service: "databrew.amazonaws.com" },
30 Action: ["sts:AssumeRole"],
31 },
32 ],
33 },
34 managedPolicyArns: [
35 "arn:aws:iam::aws:policy/service-role/AwsGlueDataBrewServiceRole",
36 "arn:aws:iam::aws:policy/AmazonS3FullAccess",
37 ],
38 });
39
40// CreateProject validates that the dataset's source object exists (it reads
41// the interactive sample), so the CSV is seeded between the two deploys.
42test.provider(
43 "create, update, delete DataBrew project",
44 (stack) =>
45 Effect.gen(function* () {
46 yield* stack.destroy();
47
48 const foundation = Effect.gen(function* () {
49 const bucket = yield* Bucket("ProjectBucket", { forceDestroy: true });
50 const role = yield* brewRole();
51 const dataset = yield* Dataset("Source", {
52 format: "CSV",
53 formatOptions: { csv: { delimiter: ",", headerRow: true } },
54 input: {
55 s3InputDefinition: {
56 bucket: bucket.bucketName,
57 key: "raw/data.csv",
58 },
59 },
60 });
61 const recipe = yield* Recipe("Transform", {
62 steps: [
63 {
64 action: {
65 operation: "UPPER_CASE",
66 parameters: { sourceColumn: "name" },
67 },
68 },
69 ],
70 });
71 return { bucket, role, dataset, recipe };
72 });
73
74 const withProject = (sample?: {
75 type: "FIRST_N" | "LAST_N" | "RANDOM";
76 size?: number;
77 }) =>
78 Effect.gen(function* () {
79 const base = yield* foundation;
80 const project = yield* Project("Explore", {
81 datasetName: base.dataset.datasetName,
82 recipeName: base.recipe.recipeName,
83 sample,
84 role: base.role.roleArn,
85 tags: { Environment: "test" },
86 });
87 return { ...base, project };
88 });
89
90 // stage 1: bucket + dataset + recipe (no project yet)
91 const base = yield* stack.deploy(foundation);
92
93 // seed the source object CreateProject samples from
94 yield* s3.putObject({
95 Bucket: base.bucket.bucketName,
96 Key: "raw/data.csv",
97 Body: new TextEncoder().encode("id,name\n1,alice\n2,bob\n"),
98 ContentType: "text/csv",
99 });
100
101 // stage 2: add the project
102 const created = yield* stack.deploy(withProject());
103
104 expect(created.project.projectName).toBeDefined();
105 expect(created.project.projectArn).toContain(
106 `:project/${created.project.projectName}`,
107 );
108
109 // out-of-band verification
110 const observed = yield* getProject(created.project.projectName);
111 expect(observed?.Name).toEqual(created.project.projectName);
112 expect(observed?.DatasetName).toEqual(created.dataset.datasetName);
113 expect(observed?.RecipeName).toEqual(created.recipe.recipeName);
114 expect(observed?.Tags?.["alchemy::id"]).toBeDefined();
115
116 // update: change the sample
117 yield* stack.deploy(withProject({ type: "RANDOM", size: 250 }));
118
119 const reobserved = yield* getProject(created.project.projectName);
120 expect(reobserved?.Sample?.Type).toEqual("RANDOM");
121 expect(reobserved?.Sample?.Size).toEqual(250);
122
123 yield* stack.destroy();
124 const gone = yield* getProject(created.project.projectName);
125 expect(gone).toBeUndefined();
126 }),
127 {
128 tags: [
129 "provider:aws",
130 "provider:aws:databrew",
131 "provider:aws:iam",
132 "provider:aws:s3",
133 "live",
134 ],
135 timeout: 180_000,
136 },
137);