1import * as frauddetector from "@distilled.cloud/aws/frauddetector";
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 type { Providers } from "../Providers.ts";
11import { readFraudDetectorTags, syncFraudDetectorTags } from "./internal.ts";
12
13export interface LabelProps {
14 /**
15 * Name of the label. If omitted, a unique lowercase name is generated from
16 * the app, stage, and logical ID. Changing the name replaces the label.
17 */
18 name?: string;
19 /**
20 * Human-readable description. This is an in-place update.
21 */
22 description?: string;
23 /**
24 * User-defined tags for the label.
25 */
26 tags?: Record<string, string>;
27}
28
29export interface Label extends Resource<
30 "AWS.FraudDetector.Label",
31 LabelProps,
32 {
33 /** The name of the label. */
34 name: string;
35 /** The ARN of the label. */
36 arn: string;
37 },
38 never,
39 Providers
40> {}
41
42/**
43 * An Amazon Fraud Detector label — a classification (e.g. `fraud`, `legit`)
44 * used to tag stored events for supervised model training. Event types
45 * reference labels; they are cheap metadata objects.
46 *
47 * ### Creating a Label
48 * **Example:** Fraud and Legit Labels
49 * ```typescript
50 * const fraud = yield* FraudDetector.Label("fraud", {
51 * description: "confirmed fraudulent event",
52 * });
53 * const legit = yield* FraudDetector.Label("legit", {});
54 * ```
55 *
56 * @resource
57 */
58export const Label = Resource<Label>("AWS.FraudDetector.Label");
59
60export const LabelProvider = () =>
61 Provider.effect(
62 Label,
63 Effect.gen(function* () {
64 const createName = Effect.fn(function* (id: string, props: LabelProps) {
65 return (
66 props.name ??
67 (yield* createPhysicalName({ id, maxLength: 64, lowercase: true }))
68 );
69 });
70
71 /** Look a label up by name; typed not-found → undefined. */
72 const get = Effect.fn(function* (name: string) {
73 const response = yield* frauddetector
74 .getLabels({ name })
75 .pipe(
76 Effect.catchTag("ResourceNotFoundException", () =>
77 Effect.succeed(undefined),
78 ),
79 );
80 return response?.labels?.[0];
81 });
82
83 const toAttrs = (label: frauddetector.Label) => ({
84 name: label.name!,
85 arn: label.arn!,
86 });
87
88 return {
89 stables: ["name", "arn"],
90
91 diff: Effect.fn(function* ({ id, olds = {}, news }) {
92 if (!isResolved(news)) return undefined;
93 const oldName = yield* createName(id, olds);
94 const newName = yield* createName(id, news);
95 if (oldName !== newName) {
96 return { action: "replace" } as const;
97 }
98 }),
99
100 read: Effect.fn(function* ({ id, olds, output }) {
101 const name = output?.name ?? (yield* createName(id, olds ?? {}));
102 const label = yield* get(name);
103 if (label === undefined) return undefined;
104 const attrs = toAttrs(label);
105 const tags = yield* readFraudDetectorTags(label.arn!);
106 return (yield* hasAlchemyTags(id, tags)) ? attrs : Unowned(attrs);
107 }),
108
109 reconcile: Effect.fn(function* ({ id, news = {}, session }) {
110 const name = yield* createName(id, news);
111 const internalTags = yield* createInternalTags(id);
112 const desiredTags = { ...internalTags, ...news.tags };
113
114 // putLabel is an idempotent upsert — call it to converge whether
115 // creating or updating the description.
116 yield* frauddetector.putLabel({
117 name,
118 description: news.description,
119 });
120
121 const label = yield* get(name);
122 // Sync tags — diff against OBSERVED cloud tags.
123 yield* syncFraudDetectorTags(label!.arn!, desiredTags);
124
125 yield* session.note(name);
126 return toAttrs(label!);
127 }),
128
129 delete: Effect.fn(function* ({ output }) {
130 yield* frauddetector.deleteLabel({ name: output.name }).pipe(
131 // Deleting an already-removed label is a no-op for us; Fraud
132 // Detector surfaces a missing label as a validation error.
133 Effect.catchTag("ValidationException", () => Effect.void),
134 );
135 }),
136
137 list: () =>
138 frauddetector.getLabels.pages({}).pipe(
139 Stream.runCollect,
140 Effect.map((chunk) =>
141 Array.from(chunk).flatMap((page) =>
142 (page.labels ?? []).map(toAttrs),
143 ),
144 ),
145 ),
146 };
147 }),
148 );