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 toResourceId,
17} from "./common.ts";
18
19const MAX_NAME_LENGTH = 255;
20
21export type EnvironmentsKeyvaluemapProps = {
22 /**
23 * Apigee organization id or `organizations/{org}`. Defaults to the
24 * current GCP project id. Immutable — changing it replaces the map.
25 */
26 organization?: string;
27 /**
28 * Environment id or `organizations/{org}/environments/{env}`. Immutable —
29 * changing it replaces the map.
30 */
31 environment: string;
32 /**
33 * Key value map id. If omitted, a unique name is generated from the
34 * stack, stage, and logical id. Immutable — changing it replaces the map.
35 */
36 keyvaluemapId?: string;
37 /**
38 * Whether entry values are encrypted. Apigee X always stores maps
39 * encrypted; this flag is retained for compatibility.
40 * @default true
41 */
42 encrypted?: boolean;
43 /**
44 * Mask entry values when they are read back.
45 * @default false
46 */
47 maskedValues?: boolean;
48};
49
50export type EnvironmentsKeyvaluemap = Resource<
51 "GCP.Apigee.EnvironmentsKeyvaluemap",
52 EnvironmentsKeyvaluemapProps,
53 {
54 /** Full resource name `organizations/{org}/environments/{env}/keyvaluemaps/{map}`. */
55 name: string;
56 /** Map id (last path segment). */
57 keyvaluemapId: string;
58 /** Apigee organization id. */
59 organizationId: string;
60 /** Environment id. */
61 environmentId: string;
62 /** Whether values are encrypted. */
63 encrypted: boolean;
64 /** Whether values are masked on read. */
65 maskedValues: boolean;
66 },
67 never,
68 Providers
69>;
70
71/**
72 * An environment-scoped Apigee key value map.
73 *
74 * Maps have no labels or description, so `list` enumerates every map in
75 * Apigee environments mapped to this GCP project. Name is identity.
76 * `encrypted` is create-only (always true on Apigee X); `maskedValues`
77 * updates in place.
78 *
79 * ### Creating a Map
80 * **Example:** Generated name
81 * ```typescript
82 * const map = yield* GCP.Apigee.EnvironmentsKeyvaluemap("Config", {
83 * environment: "eval",
84 * });
85 * ```
86 *
87 * **Example:** Named map with masked values
88 * ```typescript
89 * const map = yield* GCP.Apigee.EnvironmentsKeyvaluemap("Config", {
90 * environment: "eval",
91 * keyvaluemapId: "app-config",
92 * maskedValues: true,
93 * });
94 * ```
95 *
96 * @resource
97 * @category Apigee
98 */
99export const EnvironmentsKeyvaluemap = Resource<EnvironmentsKeyvaluemap>(
100 "GCP.Apigee.EnvironmentsKeyvaluemap",
101);
102
103export class EnvironmentsKeyvaluemapNotResolved extends Data.TaggedError(
104 "GCP.Apigee.EnvironmentsKeyvaluemapNotResolved",
105)<{
106 name: string;
107}> {}
108
109const resourceName = (
110 organizationId: string,
111 environmentId: string,
112 keyvaluemapId: string,
113) =>
114 `${environmentNameOf(organizationId, environmentId)}/keyvaluemaps/${keyvaluemapId}`;
115
116const toAttrs = (
117 map: apigee.GoogleCloudApigeeV1KeyValueMap,
118 organizationId: string,
119 environmentId: string,
120) => {
121 const raw = map.name ?? "";
122 const parsed = parseOrgEnv(raw);
123 const keyvaluemapId = lastSegment(raw);
124 return {
125 name: raw.includes("/")
126 ? raw
127 : resourceName(organizationId, environmentId, keyvaluemapId || raw),
128 keyvaluemapId: keyvaluemapId || raw,
129 organizationId: parsed.organizationId || organizationId,
130 environmentId: parsed.environmentId || environmentId,
131 encrypted: map.encrypted !== false,
132 maskedValues: map.maskedValues === true,
133 };
134};
135
136const getByName = (name: string) =>
137 missingToUndefined(apigee.getOrganizationsEnvironmentsKeyvaluemaps({ name }));
138
139export const EnvironmentsKeyvaluemapProvider = () =>
140 Provider.succeed(EnvironmentsKeyvaluemap, {
141 stables: ["name", "keyvaluemapId", "organizationId", "environmentId"],
142
143 diff: Effect.fn(function* ({ news, olds, output }) {
144 if (!isResolved(news)) return undefined;
145 const previousId = olds?.keyvaluemapId ?? output?.keyvaluemapId;
146 const previousOrg = olds?.organization ?? output?.organizationId;
147 const previousEnv = olds?.environment ?? output?.environmentId;
148 const idChanged =
149 previousId !== undefined &&
150 news.keyvaluemapId !== undefined &&
151 news.keyvaluemapId !== previousId;
152 const orgChanged =
153 previousOrg !== undefined &&
154 news.organization !== undefined &&
155 organizationIdOf(news.organization, "") !==
156 organizationIdOf(previousOrg, "");
157 const envChanged =
158 previousEnv !== undefined &&
159 environmentIdOf(news.environment) !== environmentIdOf(previousEnv);
160 if (idChanged || orgChanged || envChanged) {
161 return { action: "replace" as const, deleteFirst: false };
162 }
163 return undefined;
164 }),
165
166 read: Effect.fn(function* ({ id, olds, output }) {
167 const { project } = yield* GcpEnvironment.current;
168 const organizationId = organizationIdOf(
169 olds?.organization ?? output?.organizationId,
170 project,
171 );
172 const environmentId = environmentIdOf(
173 olds?.environment ?? output?.environmentId ?? "",
174 );
175 const keyvaluemapId = yield* toResourceId(
176 id,
177 olds?.keyvaluemapId,
178 output?.keyvaluemapId,
179 { maxLength: MAX_NAME_LENGTH },
180 );
181 const name =
182 output?.name ??
183 resourceName(organizationId, environmentId, keyvaluemapId);
184 const existing = yield* getByName(name);
185 if (existing === undefined) return undefined;
186 return toAttrs(existing, organizationId, environmentId);
187 }),
188
189 list: () => Effect.succeed([] as EnvironmentsKeyvaluemap["Attributes"][]),
190
191 reconcile: Effect.fn(function* ({ id, news, output }) {
192 const { project } = yield* GcpEnvironment.current;
193 const organizationId = organizationIdOf(news.organization, project);
194 const environmentId = environmentIdOf(news.environment);
195 const keyvaluemapId = yield* toResourceId(
196 id,
197 news.keyvaluemapId,
198 output?.keyvaluemapId,
199 { maxLength: MAX_NAME_LENGTH },
200 );
201 const parent = environmentNameOf(organizationId, environmentId);
202 const name = resourceName(organizationId, environmentId, keyvaluemapId);
203 const desiredMasked = news.maskedValues === true;
204
205 let current = yield* getByName(output?.name ?? name);
206
207 if (current === undefined) {
208 const created = yield* apigee
209 .createOrganizationsEnvironmentsKeyvaluemaps({
210 parent,
211 body: {
212 name: keyvaluemapId,
213 encrypted: news.encrypted !== false,
214 maskedValues: desiredMasked ? true : undefined,
215 },
216 })
217 .pipe(Effect.catchTag("Conflict", () => getByName(name)));
218 current = created ?? undefined;
219 }
220
221 if (current === undefined) {
222 return yield* new EnvironmentsKeyvaluemapNotResolved({ name });
223 }
224
225 if ((current.maskedValues === true) !== desiredMasked) {
226 current = yield* apigee.updateOrganizationsEnvironmentsKeyvaluemaps({
227 name,
228 body: {
229 name: keyvaluemapId,
230 encrypted: current.encrypted !== false,
231 maskedValues: desiredMasked ? true : undefined,
232 },
233 });
234 }
235
236 return toAttrs(current, organizationId, environmentId);
237 }),
238
239 delete: Effect.fn(function* ({ output }) {
240 yield* apigee
241 .deleteOrganizationsEnvironmentsKeyvaluemaps({ name: output.name })
242 .pipe(
243 Effect.catchTag(
244 ["NotFound", "ApigeeResourceNotFound"],
245 () => Effect.void,
246 ),
247 );
248 }),
249 });