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 VariableProps {
14 /**
15 * Name of the variable. If omitted, a unique lowercase name is generated
16 * from the app, stage, and logical ID. Changing the name replaces the
17 * variable.
18 */
19 name?: string;
20 /**
21 * The data type of the variable: `STRING`, `INTEGER`, `FLOAT`, `BOOLEAN`, or
22 * `DATETIME`. Immutable — changing it replaces the variable.
23 */
24 dataType: string;
25 /**
26 * The source of the variable's value: `EVENT`, `MODEL_SCORE`, or
27 * `EXTERNAL_MODEL_SCORE`. Immutable — changing it replaces the variable.
28 */
29 dataSource: string;
30 /**
31 * The default value used when the variable is missing from an event. This is
32 * an in-place update.
33 */
34 defaultValue: string;
35 /**
36 * Human-readable description. This is an in-place update.
37 */
38 description?: string;
39 /**
40 * The semantic variable type (e.g. `IP_ADDRESS`, `PRICE`, `EMAIL_ADDRESS`).
41 * If omitted, Fraud Detector infers one. This is an in-place update.
42 */
43 variableType?: string;
44 /**
45 * User-defined tags for the variable.
46 */
47 tags?: Record<string, string>;
48}
49
50export interface Variable extends Resource<
51 "AWS.FraudDetector.Variable",
52 VariableProps,
53 {
54 /** The name of the variable. */
55 name: string;
56 /** The ARN of the variable. */
57 arn: string;
58 /** The data type of the variable, e.g. `STRING` or `FLOAT`. */
59 dataType: string;
60 /** The data source of the variable, e.g. `EVENT`. */
61 dataSource: string;
62 },
63 never,
64 Providers
65> {}
66
67/**
68 * An Amazon Fraud Detector variable — a named input to fraud-detection models
69 * and rules, typed and sourced from event data or model scores. Variables are
70 * cheap metadata objects.
71 *
72 * ### Creating a Variable
73 * **Example:** Event Variable
74 * ```typescript
75 * const email = yield* FraudDetector.Variable("email", {
76 * dataType: "STRING",
77 * dataSource: "EVENT",
78 * defaultValue: "unknown",
79 * variableType: "EMAIL_ADDRESS",
80 * });
81 * ```
82 *
83 * @resource
84 */
85export const Variable = Resource<Variable>("AWS.FraudDetector.Variable");
86
87export const VariableProvider = () =>
88 Provider.effect(
89 Variable,
90 Effect.gen(function* () {
91 const createName = Effect.fn(function* (
92 id: string,
93 props: Partial<VariableProps>,
94 ) {
95 return (
96 props.name ??
97 (yield* createPhysicalName({ id, maxLength: 64, lowercase: true }))
98 );
99 });
100
101 /** Look a variable up by name; typed not-found → undefined. */
102 const get = Effect.fn(function* (name: string) {
103 const response = yield* frauddetector
104 .getVariables({ name })
105 .pipe(
106 Effect.catchTag("ResourceNotFoundException", () =>
107 Effect.succeed(undefined),
108 ),
109 );
110 return response?.variables?.[0];
111 });
112
113 const toAttrs = (variable: frauddetector.Variable) => ({
114 name: variable.name!,
115 arn: variable.arn!,
116 dataType: variable.dataType!,
117 dataSource: variable.dataSource!,
118 });
119
120 return {
121 stables: ["name", "arn"],
122
123 diff: Effect.fn(function* ({ id, olds = {}, news }) {
124 if (!isResolved(news)) return undefined;
125 const oldName = yield* createName(id, olds);
126 const newName = yield* createName(id, news);
127 if (
128 oldName !== newName ||
129 (olds.dataType ?? undefined) !== (news.dataType ?? undefined) ||
130 (olds.dataSource ?? undefined) !== (news.dataSource ?? undefined)
131 ) {
132 return { action: "replace" } as const;
133 }
134 }),
135
136 read: Effect.fn(function* ({ id, olds, output }) {
137 const name = output?.name ?? (yield* createName(id, olds ?? {}));
138 const variable = yield* get(name);
139 if (variable === undefined) return undefined;
140 const attrs = toAttrs(variable);
141 const tags = yield* readFraudDetectorTags(variable.arn!);
142 return (yield* hasAlchemyTags(id, tags)) ? attrs : Unowned(attrs);
143 }),
144
145 reconcile: Effect.fn(function* ({ id, news, session }) {
146 const name = yield* createName(id, news);
147 const internalTags = yield* createInternalTags(id);
148 const desiredTags = { ...internalTags, ...news.tags };
149
150 // 1. Observe — cloud state is authoritative.
151 let variable = yield* get(name);
152
153 // 2. Ensure — create if missing (createVariable rejects duplicates
154 // with ValidationException; tolerate the race by re-reading).
155 if (variable === undefined) {
156 yield* frauddetector
157 .createVariable({
158 name,
159 dataType: news.dataType,
160 dataSource: news.dataSource,
161 defaultValue: news.defaultValue,
162 description: news.description,
163 variableType: news.variableType,
164 tags: Object.entries(desiredTags).map(([key, value]) => ({
165 key,
166 value,
167 })),
168 })
169 .pipe(Effect.catchTag("ValidationException", () => Effect.void));
170 variable = yield* get(name);
171 } else {
172 // 3. Sync mutable aspects — update on drift.
173 const defaultDrift =
174 (variable.defaultValue ?? undefined) !==
175 (news.defaultValue ?? undefined);
176 const descriptionDrift =
177 news.description !== undefined &&
178 (variable.description ?? undefined) !== news.description;
179 const typeDrift =
180 news.variableType !== undefined &&
181 (variable.variableType ?? undefined) !== news.variableType;
182 if (defaultDrift || descriptionDrift || typeDrift) {
183 yield* frauddetector.updateVariable({
184 name,
185 defaultValue: news.defaultValue,
186 description: news.description,
187 variableType: news.variableType,
188 });
189 }
190 // 3b. Sync tags — diff against OBSERVED cloud tags.
191 yield* syncFraudDetectorTags(variable.arn!, desiredTags);
192 }
193
194 yield* session.note(name);
195 return toAttrs(variable!);
196 }),
197
198 delete: Effect.fn(function* ({ output }) {
199 yield* frauddetector.deleteVariable({ name: output.name }).pipe(
200 // Deleting an already-removed variable is a no-op for us; Fraud
201 // Detector surfaces a missing variable as a validation error.
202 Effect.catchTag("ValidationException", () => Effect.void),
203 );
204 }),
205
206 list: () =>
207 frauddetector.getVariables.pages({}).pipe(
208 Stream.runCollect,
209 Effect.map((chunk) =>
210 Array.from(chunk).flatMap((page) =>
211 (page.variables ?? []).map(toAttrs),
212 ),
213 ),
214 ),
215 };
216 }),
217 );