1import * as sdb from "@distilled.cloud/aws/simpledb";
2import * as Effect from "effect/Effect";
3import * as Schedule from "effect/Schedule";
4import * as Stream from "effect/Stream";
5import { isResolved } from "../../Diff.ts";
6import { createPhysicalName } from "../../PhysicalName.ts";
7import * as Provider from "../../Provider.ts";
8import { Resource } from "../../Resource.ts";
9import { AWSEnvironment } from "../Environment.ts";
10import type { Providers } from "../Providers.ts";
11
12export interface DomainProps {
13 /**
14 * Name of the SimpleDB domain. 3-255 characters; letters, digits, `_`,
15 * `-`, and `.` are allowed. Changing the name replaces the domain.
16 * @default a generated physical name
17 */
18 domainName?: string;
19}
20
21export interface Domain extends Resource<
22 "AWS.SimpleDB.Domain",
23 DomainProps,
24 {
25 domainName: string;
26 domainArn: string;
27 },
28 {},
29 Providers
30> {}
31
32/**
33 * An Amazon SimpleDB domain — the container for SimpleDB items and
34 * attributes, analogous to a table.
35 *
36 * SimpleDB is a legacy service (closed to accounts that never used it and
37 * slated for migration via the SimpleDBv2 export API), but domains remain
38 * fully manageable on grandfathered accounts. A domain has no mutable
39 * configuration: the name is its identity, so any name change replaces the
40 * domain. SimpleDB has no tagging API, so Alchemy cannot brand domains for
41 * ownership detection.
42 * ### Creating Domains
43 * **Example:** Basic Domain
44 * ```typescript
45 * import * as AWS from "alchemy/AWS";
46 *
47 * const domain = yield* AWS.SimpleDB.Domain("MyDomain", {});
48 * ```
49 *
50 * **Example:** Named Domain
51 * ```typescript
52 * const domain = yield* AWS.SimpleDB.Domain("MyDomain", {
53 * domainName: "my-application-data",
54 * });
55 * ```
56 *
57 * @resource
58 */
59export const Domain = Resource<Domain>("AWS.SimpleDB.Domain");
60
61/**
62 * CreateDomain is eventually consistent — DomainMetadata directly after a
63 * create can briefly report NoSuchDomain. Explicitly typed so the
64 * conditional `Retry.Return` type does not leak into declaration emit.
65 */
66const retryWhileNoSuchDomain = <A, E extends { readonly _tag: string }, R>(
67 self: Effect.Effect<A, E, R>,
68): Effect.Effect<A, E, R> =>
69 Effect.retry(self, {
70 while: (e) => e._tag === "NoSuchDomain",
71 schedule: Schedule.max([Schedule.fixed("2 seconds"), Schedule.recurs(10)]),
72 });
73
74export const DomainProvider = () =>
75 Provider.effect(
76 Domain,
77 Effect.gen(function* () {
78 const createName = Effect.fn(function* (id: string, props: DomainProps) {
79 return (
80 props.domainName ??
81 (yield* createPhysicalName({ id, maxLength: 255 }))
82 );
83 });
84
85 const domainArn = (region: string, accountId: string, name: string) =>
86 `arn:aws:sdb:${region}:${accountId}:domain/${name}`;
87
88 const observeDomain = Effect.fn(function* (name: string) {
89 return yield* sdb
90 .domainMetadata({ DomainName: name })
91 .pipe(
92 Effect.catchTag("NoSuchDomain", () => Effect.succeed(undefined)),
93 );
94 });
95
96 return Domain.Provider.of({
97 stables: ["domainName", "domainArn"],
98
99 list: () =>
100 Effect.gen(function* () {
101 const { accountId, region } = yield* AWSEnvironment.current;
102 const names = yield* sdb.listDomains
103 .items({})
104 .pipe(Stream.runCollect);
105 return Array.from(names).map((name) => ({
106 domainName: name,
107 domainArn: domainArn(region, accountId, name),
108 }));
109 }),
110
111 read: Effect.fn(function* ({ id, olds, output }) {
112 const { accountId, region } = yield* AWSEnvironment.current;
113 const name =
114 output?.domainName ?? (yield* createName(id, olds ?? {}));
115 const metadata = yield* observeDomain(name);
116 if (metadata === undefined) {
117 return undefined;
118 }
119 // SimpleDB has no tagging API, so ownership cannot be verified —
120 // an existing domain with the derived name is treated as ours.
121 return {
122 domainName: name,
123 domainArn: domainArn(region, accountId, name),
124 };
125 }),
126
127 diff: Effect.fn(function* ({ id, news, olds }) {
128 if (!isResolved(news)) return undefined;
129 const oldName = yield* createName(id, olds ?? {});
130 const newName = yield* createName(id, news);
131 if (oldName !== newName) {
132 return { action: "replace" } as const;
133 }
134 }),
135
136 reconcile: Effect.fn(function* ({ id, news, output, session }) {
137 const { accountId, region } = yield* AWSEnvironment.current;
138 const name = output?.domainName ?? (yield* createName(id, news));
139
140 // 1. OBSERVE — cloud state is authoritative
141 const metadata = yield* observeDomain(name);
142
143 // 2. ENSURE — CreateDomain is idempotent (creating an existing
144 // domain succeeds); wait until the domain is visible.
145 if (metadata === undefined) {
146 yield* sdb.createDomain({ DomainName: name });
147 yield* retryWhileNoSuchDomain(
148 sdb.domainMetadata({ DomainName: name }),
149 );
150 }
151
152 // 3. SYNC — a domain has no mutable configuration and no tags.
153
154 yield* session.note(name);
155 return {
156 domainName: name,
157 domainArn: domainArn(region, accountId, name),
158 };
159 }),
160
161 delete: Effect.fn(function* ({ output }) {
162 // DeleteDomain is idempotent — deleting a missing domain succeeds.
163 yield* sdb.deleteDomain({ DomainName: output.domainName });
164 }),
165 });
166 }),
167 );