1import * as location from "@distilled.cloud/aws/location";
2import * as Effect from "effect/Effect";
3import * as EffectStream 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 {
10 createInternalTags,
11 diffTags,
12 hasAlchemyTags,
13 type Tags,
14} from "../../Tags.ts";
15import type { Providers } from "../Providers.ts";
16import { toTagRecord } from "./internal.ts";
17
18export interface RouteCalculatorProps {
19 /**
20 * Name of the route calculator. Immutable — changing it replaces the
21 * calculator.
22 * @default ${app}-${stage}-${id}
23 */
24 calculatorName?: string;
25 /**
26 * Data provider for routing. Immutable — changing it replaces the
27 * calculator. One of `Esri`, `Grab`, or `Here`.
28 */
29 dataSource: string;
30 /**
31 * Optional description of the route calculator resource.
32 */
33 description?: string;
34 /**
35 * Tags to associate with the route calculator.
36 */
37 tags?: Record<string, string>;
38}
39
40export interface RouteCalculator extends Resource<
41 "AWS.Location.RouteCalculator",
42 RouteCalculatorProps,
43 {
44 /** Physical name of the route calculator. */
45 calculatorName: string;
46 /** ARN of the route calculator. */
47 calculatorArn: string;
48 /** Data provider backing the calculator. */
49 dataSource: string;
50 /** Description of the route calculator. */
51 description: string | undefined;
52 /** Tags currently associated with the calculator. */
53 tags: Record<string, string>;
54 },
55 never,
56 Providers
57> {}
58
59/**
60 * An Amazon Location Service route calculator. A route calculator computes
61 * routes and route matrices against a chosen data provider. The data source
62 * is immutable; the description can be updated in place.
63 *
64 * ### Creating Route Calculators
65 * **Example:** Basic Route Calculator
66 * ```typescript
67 * import * as Location from "alchemy/AWS/Location";
68 *
69 * const calculator = yield* Location.RouteCalculator("Routes", {
70 * dataSource: "Esri",
71 * });
72 * ```
73 *
74 * @resource
75 */
76export const RouteCalculator = Resource<RouteCalculator>(
77 "AWS.Location.RouteCalculator",
78);
79
80const createCalculatorName = (
81 id: string,
82 props: { calculatorName?: string | undefined },
83) =>
84 Effect.gen(function* () {
85 if (props.calculatorName) return props.calculatorName;
86 return yield* createPhysicalName({ id, maxLength: 100 });
87 });
88
89const readCalculator = Effect.fn(function* (calculatorName: string) {
90 const found = yield* location
91 .describeRouteCalculator({ CalculatorName: calculatorName })
92 .pipe(
93 Effect.catchTag("ResourceNotFoundException", () =>
94 Effect.succeed(undefined),
95 ),
96 );
97 if (!found) return undefined;
98 return {
99 calculatorName: found.CalculatorName,
100 calculatorArn: found.CalculatorArn,
101 dataSource: found.DataSource,
102 description: found.Description ? found.Description : undefined,
103 tags: toTagRecord(found.Tags),
104 } satisfies RouteCalculator["Attributes"];
105});
106
107export const RouteCalculatorProvider = () =>
108 Provider.effect(
109 RouteCalculator,
110 Effect.gen(function* () {
111 return {
112 stables: ["calculatorName", "calculatorArn"],
113 list: () =>
114 Effect.gen(function* () {
115 const names = yield* location.listRouteCalculators.pages({}).pipe(
116 EffectStream.runCollect,
117 Effect.map((chunk) =>
118 Array.from(chunk).flatMap((page) =>
119 (page.Entries ?? []).map((entry) => entry.CalculatorName),
120 ),
121 ),
122 );
123 const hydrated = yield* Effect.forEach(
124 names,
125 (name) => readCalculator(name),
126 { concurrency: 10 },
127 );
128 return hydrated.filter(
129 (attrs): attrs is RouteCalculator["Attributes"] =>
130 attrs !== undefined,
131 );
132 }),
133 read: Effect.fn(function* ({ id, olds, output }) {
134 const calculatorName =
135 output?.calculatorName ??
136 (yield* createCalculatorName(id, olds ?? {}));
137 const state = yield* readCalculator(calculatorName);
138 if (!state) return undefined;
139 return (yield* hasAlchemyTags(id, state.tags as Tags))
140 ? state
141 : Unowned(state);
142 }),
143 diff: Effect.fn(function* ({ id, news, olds }) {
144 if (!isResolved(news)) return;
145 const oldName = yield* createCalculatorName(id, olds);
146 const newName = yield* createCalculatorName(id, news);
147 if (oldName !== newName || olds.dataSource !== news.dataSource) {
148 return { action: "replace" } as const;
149 }
150 }),
151 reconcile: Effect.fn(function* ({ id, news, output, session }) {
152 const calculatorName =
153 output?.calculatorName ?? (yield* createCalculatorName(id, news));
154 const internalTags = yield* createInternalTags(id);
155 const desiredTags = { ...internalTags, ...news.tags };
156
157 let state = yield* readCalculator(calculatorName);
158
159 if (state === undefined) {
160 yield* location
161 .createRouteCalculator({
162 CalculatorName: calculatorName,
163 DataSource: news.dataSource,
164 Description: news.description,
165 Tags: desiredTags,
166 })
167 .pipe(Effect.catchTag("ConflictException", () => Effect.void));
168 state = yield* readCalculator(calculatorName);
169 if (state === undefined) {
170 return yield* Effect.fail(
171 new Error(
172 `failed to read created route calculator ${calculatorName}`,
173 ),
174 );
175 }
176 }
177
178 if (state.description !== (news.description ?? undefined)) {
179 yield* location.updateRouteCalculator({
180 CalculatorName: calculatorName,
181 Description: news.description,
182 });
183 }
184
185 const { removed, upsert } = diffTags(state.tags, desiredTags);
186 if (removed.length > 0) {
187 yield* location.untagResource({
188 ResourceArn: state.calculatorArn,
189 TagKeys: removed,
190 });
191 }
192 if (upsert.length > 0) {
193 yield* location.tagResource({
194 ResourceArn: state.calculatorArn,
195 Tags: Object.fromEntries(upsert.map((t) => [t.Key, t.Value])),
196 });
197 }
198
199 yield* session.note(state.calculatorArn);
200
201 const final = yield* readCalculator(calculatorName);
202 if (!final) {
203 return yield* Effect.fail(
204 new Error(
205 `failed to read reconciled route calculator ${calculatorName}`,
206 ),
207 );
208 }
209 return final;
210 }),
211 delete: Effect.fn(function* ({ output }) {
212 yield* location
213 .deleteRouteCalculator({ CalculatorName: output.calculatorName })
214 .pipe(
215 Effect.catchTag("ResourceNotFoundException", () => Effect.void),
216 );
217 }),
218 };
219 }),
220 );