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 GeofenceCollectionProps {
19 /**
20 * Name of the geofence collection. Immutable — changing it replaces the
21 * collection.
22 * @default ${app}-${stage}-${id}
23 */
24 collectionName?: string;
25 /**
26 * KMS key ID used to encrypt the collection's geofence data. Immutable —
27 * changing it replaces the collection.
28 */
29 kmsKeyId?: string;
30 /**
31 * Optional description of the geofence collection.
32 */
33 description?: string;
34 /**
35 * Tags to associate with the geofence collection.
36 */
37 tags?: Record<string, string>;
38}
39
40export interface GeofenceCollection extends Resource<
41 "AWS.Location.GeofenceCollection",
42 GeofenceCollectionProps,
43 {
44 /** Physical name of the geofence collection. */
45 collectionName: string;
46 /** ARN of the geofence collection. */
47 collectionArn: string;
48 /** KMS key ID backing the collection, if configured. */
49 kmsKeyId: string | undefined;
50 /** Description of the collection. */
51 description: string | undefined;
52 /** Tags currently associated with the collection. */
53 tags: Record<string, string>;
54 },
55 never,
56 Providers
57> {}
58
59/**
60 * An Amazon Location Service geofence collection. A geofence collection stores
61 * geofences and evaluates device positions against them. The KMS key is
62 * immutable; the description can be updated in place.
63 *
64 * ### Creating Geofence Collections
65 * **Example:** Basic Geofence Collection
66 * ```typescript
67 * import * as Location from "alchemy/AWS/Location";
68 *
69 * const collection = yield* Location.GeofenceCollection("Fences", {});
70 * ```
71 *
72 * **Example:** Encrypted Geofence Collection
73 * ```typescript
74 * const collection = yield* Location.GeofenceCollection("SecureFences", {
75 * kmsKeyId: "alias/my-key",
76 * description: "Encrypted geofence collection",
77 * });
78 * ```
79 *
80 * @resource
81 */
82export const GeofenceCollection = Resource<GeofenceCollection>(
83 "AWS.Location.GeofenceCollection",
84);
85
86const createCollectionName = (
87 id: string,
88 props: { collectionName?: string | undefined },
89) =>
90 Effect.gen(function* () {
91 if (props.collectionName) return props.collectionName;
92 return yield* createPhysicalName({ id, maxLength: 100 });
93 });
94
95const readCollection = Effect.fn(function* (collectionName: string) {
96 const found = yield* location
97 .describeGeofenceCollection({ CollectionName: collectionName })
98 .pipe(
99 Effect.catchTag("ResourceNotFoundException", () =>
100 Effect.succeed(undefined),
101 ),
102 );
103 if (!found) return undefined;
104 return {
105 collectionName: found.CollectionName,
106 collectionArn: found.CollectionArn,
107 kmsKeyId: found.KmsKeyId,
108 description: found.Description ? found.Description : undefined,
109 tags: toTagRecord(found.Tags),
110 } satisfies GeofenceCollection["Attributes"];
111});
112
113export const GeofenceCollectionProvider = () =>
114 Provider.effect(
115 GeofenceCollection,
116 Effect.gen(function* () {
117 return {
118 stables: ["collectionName", "collectionArn"],
119 list: () =>
120 Effect.gen(function* () {
121 const names = yield* location.listGeofenceCollections
122 .pages({})
123 .pipe(
124 EffectStream.runCollect,
125 Effect.map((chunk) =>
126 Array.from(chunk).flatMap((page) =>
127 (page.Entries ?? []).map((entry) => entry.CollectionName),
128 ),
129 ),
130 );
131 const hydrated = yield* Effect.forEach(
132 names,
133 (name) => readCollection(name),
134 { concurrency: 10 },
135 );
136 return hydrated.filter(
137 (attrs): attrs is GeofenceCollection["Attributes"] =>
138 attrs !== undefined,
139 );
140 }),
141 read: Effect.fn(function* ({ id, olds, output }) {
142 const collectionName =
143 output?.collectionName ??
144 (yield* createCollectionName(id, olds ?? {}));
145 const state = yield* readCollection(collectionName);
146 if (!state) return undefined;
147 return (yield* hasAlchemyTags(id, state.tags as Tags))
148 ? state
149 : Unowned(state);
150 }),
151 diff: Effect.fn(function* ({ id, news = {}, olds = {} }) {
152 if (!isResolved(news)) return;
153 const oldName = yield* createCollectionName(id, olds);
154 const newName = yield* createCollectionName(id, news);
155 if (oldName !== newName || olds.kmsKeyId !== news.kmsKeyId) {
156 return { action: "replace" } as const;
157 }
158 }),
159 reconcile: Effect.fn(function* ({ id, news = {}, output, session }) {
160 const collectionName =
161 output?.collectionName ?? (yield* createCollectionName(id, news));
162 const internalTags = yield* createInternalTags(id);
163 const desiredTags = { ...internalTags, ...news.tags };
164
165 let state = yield* readCollection(collectionName);
166
167 if (state === undefined) {
168 yield* location
169 .createGeofenceCollection({
170 CollectionName: collectionName,
171 KmsKeyId: news.kmsKeyId,
172 Description: news.description,
173 Tags: desiredTags,
174 })
175 .pipe(Effect.catchTag("ConflictException", () => Effect.void));
176 state = yield* readCollection(collectionName);
177 if (state === undefined) {
178 return yield* Effect.fail(
179 new Error(
180 `failed to read created geofence collection ${collectionName}`,
181 ),
182 );
183 }
184 }
185
186 if (state.description !== (news.description ?? undefined)) {
187 yield* location.updateGeofenceCollection({
188 CollectionName: collectionName,
189 Description: news.description,
190 });
191 }
192
193 const { removed, upsert } = diffTags(state.tags, desiredTags);
194 if (removed.length > 0) {
195 yield* location.untagResource({
196 ResourceArn: state.collectionArn,
197 TagKeys: removed,
198 });
199 }
200 if (upsert.length > 0) {
201 yield* location.tagResource({
202 ResourceArn: state.collectionArn,
203 Tags: Object.fromEntries(upsert.map((t) => [t.Key, t.Value])),
204 });
205 }
206
207 yield* session.note(state.collectionArn);
208
209 const final = yield* readCollection(collectionName);
210 if (!final) {
211 return yield* Effect.fail(
212 new Error(
213 `failed to read reconciled geofence collection ${collectionName}`,
214 ),
215 );
216 }
217 return final;
218 }),
219 delete: Effect.fn(function* ({ output }) {
220 yield* location
221 .deleteGeofenceCollection({
222 CollectionName: output.collectionName,
223 })
224 .pipe(
225 Effect.catchTag("ResourceNotFoundException", () => Effect.void),
226 );
227 }),
228 };
229 }),
230 );