1import * as apigee from "@distilled.cloud/gcp/apigee_v1";
2import * as Data from "effect/Data";
3import * as Effect from "effect/Effect";
4import { isResolved } from "../../Diff.ts";
5import * as Provider from "../../Provider.ts";
6import { Resource } from "../../Resource.ts";
7import { GcpEnvironment } from "../Environment.ts";
8import type { Providers } from "../Providers.ts";
9import {
10 environmentIdOf,
11 environmentNameOf,
12 lastSegment,
13 missingToUndefined,
14 organizationIdOf,
15 parseOrgEnv,
16 sameText,
17 segmentAfter,
18 toResourceId,
19} from "./common.ts";
20
21const MAX_NAME_LENGTH = 255;
22
23export type EnvironmentsKeyvaluemapsEntryProps = {
24 /**
25 * Apigee organization id or `organizations/{org}`. Defaults to the
26 * current GCP project id. Immutable — changing it replaces the entry.
27 */
28 organization?: string;
29 /**
30 * Environment id or `organizations/{org}/environments/{env}`. Immutable —
31 * changing it replaces the entry.
32 */
33 environment: string;
34 /**
35 * Parent key value map id or
36 * `organizations/{org}/environments/{env}/keyvaluemaps/{map}`. Immutable
37 * — changing it replaces the entry.
38 */
39 keyvaluemap: string;
40 /**
41 * Entry key (last path segment). If omitted, a unique name is generated
42 * from the stack, stage, and logical id. Immutable — changing it
43 * replaces the entry.
44 */
45 entryId?: string;
46 /**
47 * Value stored under `entryId`.
48 */
49 value: string;
50};
51
52export type EnvironmentsKeyvaluemapsEntry = Resource<
53 "GCP.Apigee.EnvironmentsKeyvaluemapsEntry",
54 EnvironmentsKeyvaluemapsEntryProps,
55 {
56 /** Full resource name `.../keyvaluemaps/{map}/entries/{entry}`. */
57 name: string;
58 /** Entry key. */
59 entryId: string;
60 /** Parent map id. */
61 keyvaluemapId: string;
62 /** Apigee organization id. */
63 organizationId: string;
64 /** Environment id. */
65 environmentId: string;
66 /** Stored value (may be masked). */
67 value: string | undefined;
68 },
69 never,
70 Providers
71>;
72
73/**
74 * An entry in an environment-scoped Apigee key value map.
75 *
76 * Entries have no labels. `list` walks every map in Apigee environments
77 * mapped to this GCP project. Key, map, organization, and environment are
78 * identity; `value` updates in place.
79 *
80 * ### Creating an Entry
81 * **Example:** Put a value
82 * ```typescript
83 * const map = yield* GCP.Apigee.EnvironmentsKeyvaluemap("Config", {
84 * environment: "eval",
85 * });
86 * const entry = yield* GCP.Apigee.EnvironmentsKeyvaluemapsEntry("ApiKey", {
87 * environment: "eval",
88 * keyvaluemap: map.keyvaluemapId,
89 * value: "secret",
90 * });
91 * ```
92 *
93 * @resource
94 * @category Apigee
95 */
96export const EnvironmentsKeyvaluemapsEntry =
97 Resource<EnvironmentsKeyvaluemapsEntry>(
98 "GCP.Apigee.EnvironmentsKeyvaluemapsEntry",
99 );
100
101export class EnvironmentsKeyvaluemapsEntryNotResolved extends Data.TaggedError(
102 "GCP.Apigee.EnvironmentsKeyvaluemapsEntryNotResolved",
103)<{
104 name: string;
105}> {}
106
107const mapIdOf = (value: string) =>
108 segmentAfter(value, "keyvaluemaps") ?? lastSegment(value);
109
110const resourceName = (
111 organizationId: string,
112 environmentId: string,
113 keyvaluemapId: string,
114 entryId: string,
115) =>
116 `${environmentNameOf(organizationId, environmentId)}/keyvaluemaps/${keyvaluemapId}/entries/${entryId}`;
117
118const mapName = (
119 organizationId: string,
120 environmentId: string,
121 keyvaluemapId: string,
122) =>
123 `${environmentNameOf(organizationId, environmentId)}/keyvaluemaps/${keyvaluemapId}`;
124
125const toAttrs = (
126 entry: apigee.GoogleCloudApigeeV1KeyValueEntry,
127 organizationId: string,
128 environmentId: string,
129 keyvaluemapId: string,
130) => {
131 const raw = entry.name ?? "";
132 const parsed = parseOrgEnv(raw);
133 const entryId = lastSegment(raw);
134 return {
135 name: raw.includes("/")
136 ? raw
137 : resourceName(
138 organizationId,
139 environmentId,
140 keyvaluemapId,
141 entryId || raw,
142 ),
143 entryId: entryId || raw,
144 keyvaluemapId,
145 organizationId: parsed.organizationId || organizationId,
146 environmentId: parsed.environmentId || environmentId,
147 value: entry.value,
148 };
149};
150
151const getByName = (name: string) =>
152 missingToUndefined(
153 apigee.getOrganizationsEnvironmentsKeyvaluemapsEntries({ name }),
154 );
155
156export const EnvironmentsKeyvaluemapsEntryProvider = () =>
157 Provider.succeed(EnvironmentsKeyvaluemapsEntry, {
158 stables: [
159 "name",
160 "entryId",
161 "keyvaluemapId",
162 "organizationId",
163 "environmentId",
164 ],
165
166 diff: Effect.fn(function* ({ news, olds, output }) {
167 if (!isResolved(news)) return undefined;
168 const previousId = olds?.entryId ?? output?.entryId;
169 const previousMap = olds?.keyvaluemap ?? output?.keyvaluemapId;
170 const previousOrg = olds?.organization ?? output?.organizationId;
171 const previousEnv = olds?.environment ?? output?.environmentId;
172 const idChanged =
173 previousId !== undefined &&
174 news.entryId !== undefined &&
175 news.entryId !== previousId;
176 const mapChanged =
177 previousMap !== undefined &&
178 mapIdOf(news.keyvaluemap) !== mapIdOf(previousMap);
179 const orgChanged =
180 previousOrg !== undefined &&
181 news.organization !== undefined &&
182 organizationIdOf(news.organization, "") !==
183 organizationIdOf(previousOrg, "");
184 const envChanged =
185 previousEnv !== undefined &&
186 environmentIdOf(news.environment) !== environmentIdOf(previousEnv);
187 if (idChanged || mapChanged || orgChanged || envChanged) {
188 return { action: "replace" as const, deleteFirst: false };
189 }
190 return undefined;
191 }),
192
193 read: Effect.fn(function* ({ id, olds, output }) {
194 const { project } = yield* GcpEnvironment.current;
195 const organizationId = organizationIdOf(
196 olds?.organization ?? output?.organizationId,
197 project,
198 );
199 const environmentId = environmentIdOf(
200 olds?.environment ?? output?.environmentId ?? "",
201 );
202 const keyvaluemapId = mapIdOf(
203 olds?.keyvaluemap ?? output?.keyvaluemapId ?? "",
204 );
205 const entryId = yield* toResourceId(id, olds?.entryId, output?.entryId, {
206 maxLength: MAX_NAME_LENGTH,
207 });
208 const name =
209 output?.name ??
210 resourceName(organizationId, environmentId, keyvaluemapId, entryId);
211 const existing = yield* getByName(name);
212 if (existing === undefined) return undefined;
213 return toAttrs(existing, organizationId, environmentId, keyvaluemapId);
214 }),
215
216 list: () =>
217 Effect.succeed([] as EnvironmentsKeyvaluemapsEntry["Attributes"][]),
218
219 reconcile: Effect.fn(function* ({ id, news, output }) {
220 const { project } = yield* GcpEnvironment.current;
221 const organizationId = organizationIdOf(news.organization, project);
222 const environmentId = environmentIdOf(news.environment);
223 const keyvaluemapId = mapIdOf(news.keyvaluemap);
224 const entryId = yield* toResourceId(id, news.entryId, output?.entryId, {
225 maxLength: MAX_NAME_LENGTH,
226 });
227 const parent = mapName(organizationId, environmentId, keyvaluemapId);
228 const name = resourceName(
229 organizationId,
230 environmentId,
231 keyvaluemapId,
232 entryId,
233 );
234
235 let current = yield* getByName(output?.name ?? name);
236
237 if (current === undefined) {
238 const created = yield* apigee
239 .createOrganizationsEnvironmentsKeyvaluemapsEntries({
240 parent,
241 body: { name: entryId, value: news.value },
242 })
243 .pipe(Effect.catchTag("Conflict", () => getByName(name)));
244 current = created ?? undefined;
245 }
246
247 if (current === undefined) {
248 return yield* new EnvironmentsKeyvaluemapsEntryNotResolved({ name });
249 }
250
251 if (!sameText(current.value, news.value)) {
252 current =
253 yield* apigee.updateOrganizationsEnvironmentsKeyvaluemapsEntries({
254 name,
255 body: { name: entryId, value: news.value },
256 });
257 }
258
259 return toAttrs(current, organizationId, environmentId, keyvaluemapId);
260 }),
261
262 delete: Effect.fn(function* ({ output }) {
263 yield* apigee
264 .deleteOrganizationsEnvironmentsKeyvaluemapsEntries({
265 name: output.name,
266 })
267 .pipe(
268 Effect.catchTag(
269 ["NotFound", "ApigeeResourceNotFound"],
270 () => Effect.void,
271 ),
272 );
273 }),
274 });