1import * as databrew from "@distilled.cloud/aws/databrew";
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 cleanMap,
14 databrewArn,
15 fetchObservedTags,
16 retryWhileConflict,
17 syncTags,
18} from "./internal.ts";
19
20export interface ScheduleProps {
21 /**
22 * Name of the schedule. If omitted, a unique name is generated. Changing
23 * the name replaces the schedule.
24 * @default a generated physical name
25 */
26 scheduleName?: string;
27 /**
28 * The cron expression (in AWS cron format, evaluated in UTC) that
29 * determines when the associated jobs run, e.g.
30 * `cron(0 12 * * ? *)` for every day at noon UTC.
31 */
32 cronExpression: string;
33 /**
34 * Names of the DataBrew jobs the schedule starts.
35 */
36 jobNames?: string[];
37 /**
38 * Tags to apply to the schedule. Merged with internal Alchemy tags.
39 */
40 tags?: Record<string, string>;
41}
42
43export interface Schedule extends Resource<
44 "AWS.DataBrew.Schedule",
45 ScheduleProps,
46 {
47 /** Name of the schedule. */
48 scheduleName: string;
49 /** ARN of the schedule. */
50 scheduleArn: string;
51 },
52 {},
53 Providers
54> {}
55
56/**
57 * An AWS Glue DataBrew schedule — a cron expression that starts one or more
58 * DataBrew jobs at recurring times. The schedule definition is free; only
59 * the job runs it triggers are billed.
60 * ### Creating Schedules
61 * **Example:** Nightly Job Schedule
62 * ```typescript
63 * import * as AWS from "alchemy/AWS";
64 *
65 * const schedule = yield* AWS.DataBrew.Schedule("Nightly", {
66 * cronExpression: "cron(0 3 * * ? *)",
67 * jobNames: [job.jobName],
68 * });
69 * ```
70 *
71 * **Example:** Schedule Without Jobs (attach later)
72 * ```typescript
73 * const schedule = yield* AWS.DataBrew.Schedule("Standing", {
74 * cronExpression: "cron(0 12 ? * MON-FRI *)",
75 * });
76 * ```
77 *
78 * @resource
79 */
80export const Schedule = Resource<Schedule>("AWS.DataBrew.Schedule");
81
82export const ScheduleProvider = () =>
83 Provider.effect(
84 Schedule,
85 Effect.gen(function* () {
86 const createName = Effect.fn(function* (
87 id: string,
88 props: { scheduleName?: string | undefined },
89 ) {
90 return (
91 props.scheduleName ??
92 (yield* createPhysicalName({ id, maxLength: 255 }))
93 );
94 });
95
96 const observe = Effect.fn(function* (name: string) {
97 return yield* databrew
98 .describeSchedule({ Name: name })
99 .pipe(
100 Effect.catchTag("ResourceNotFoundException", () =>
101 Effect.succeed(undefined),
102 ),
103 );
104 });
105
106 return Schedule.Provider.of({
107 stables: ["scheduleName", "scheduleArn"],
108
109 list: () =>
110 Effect.gen(function* () {
111 const { accountId, region } = yield* AWSEnvironment.current;
112 const pages = yield* databrew.listSchedules
113 .pages({})
114 .pipe(Stream.runCollect);
115 return Array.from(pages)
116 .flatMap((page) => page.Schedules ?? [])
117 .map((s) => ({
118 scheduleName: s.Name,
119 scheduleArn:
120 s.ResourceArn ??
121 databrewArn(region, accountId, "schedule", s.Name),
122 }));
123 }),
124
125 read: Effect.fn(function* ({ id, olds, output }) {
126 const { accountId, region } = yield* AWSEnvironment.current;
127 const name =
128 output?.scheduleName ?? (yield* createName(id, olds ?? {}));
129 const schedule = yield* observe(name);
130 if (schedule === undefined) return undefined;
131 const arn =
132 schedule.ResourceArn ??
133 databrewArn(region, accountId, "schedule", name);
134 const attrs = { scheduleName: name, scheduleArn: arn };
135 const tags = cleanMap(schedule.Tags);
136 return (yield* hasAlchemyTags(id, tags)) ? attrs : Unowned(attrs);
137 }),
138
139 diff: Effect.fn(function* ({ id, news, olds }) {
140 if (!isResolved(news)) return undefined;
141 const oldName = yield* createName(id, olds);
142 const newName = yield* createName(id, news);
143 if (oldName !== newName) return { action: "replace" } as const;
144 // cronExpression and jobNames are UpdateSchedule-able
145 }),
146
147 reconcile: Effect.fn(function* ({ id, news, output, session }) {
148 const { accountId, region } = yield* AWSEnvironment.current;
149 const name = output?.scheduleName ?? (yield* createName(id, news));
150 const internalTags = yield* createInternalTags(id);
151 const desiredTags = { ...news.tags, ...internalTags };
152
153 // 1. OBSERVE
154 const schedule = yield* observe(name);
155
156 // 2. ENSURE / 3. SYNC — UpdateSchedule is a full PUT
157 if (schedule === undefined) {
158 yield* databrew
159 .createSchedule({
160 Name: name,
161 CronExpression: news.cronExpression,
162 JobNames: news.jobNames,
163 Tags: desiredTags,
164 })
165 .pipe(Effect.catchTag("ConflictException", () => Effect.void));
166 } else if (
167 schedule.CronExpression !== news.cronExpression ||
168 JSON.stringify(schedule.JobNames ?? []) !==
169 JSON.stringify(news.jobNames ?? [])
170 ) {
171 yield* databrew.updateSchedule({
172 Name: name,
173 CronExpression: news.cronExpression,
174 JobNames: news.jobNames,
175 });
176 }
177
178 const arn =
179 schedule?.ResourceArn ??
180 databrewArn(region, accountId, "schedule", name);
181
182 // 3b. SYNC TAGS against observed cloud tags
183 const observedTags = yield* fetchObservedTags(arn);
184 yield* syncTags(arn, observedTags, desiredTags);
185
186 yield* session.note(name);
187 return { scheduleName: name, scheduleArn: arn };
188 }),
189
190 delete: Effect.fn(function* ({ output }) {
191 yield* retryWhileConflict(
192 databrew.deleteSchedule({ Name: output.scheduleName }),
193 ).pipe(
194 Effect.catchTag("ResourceNotFoundException", () =>
195 Effect.succeed(undefined),
196 ),
197 );
198 }),
199 });
200 }),
201 );