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 emptyOnMissing,
11 environmentIdOf,
12 environmentNameOf,
13 lastSegment,
14 listProjectEnvironments,
15 missingToUndefined,
16 organizationIdOf,
17 stripRevision,
18 toResourceId,
19} from "./common.ts";
20
21const MAX_NAME_LENGTH = 255;
22const DEFAULT_TYPE = "js";
23
24export type EnvironmentsResourcefileProps = {
25 /**
26 * Apigee organization id or `organizations/{org}`. Defaults to the
27 * current GCP project id. Immutable — changing it replaces the file.
28 */
29 organization?: string;
30 /**
31 * Environment id or `organizations/{org}/environments/{env}`. Immutable —
32 * changing it replaces the file.
33 */
34 environment: string;
35 /**
36 * Resource file type (`js`, `java`, `properties`, `py`, `xsl`, `wsdl`,
37 * `xsd`, …). Immutable — changing it replaces the file.
38 * @default "js"
39 */
40 fileType?: string;
41 /**
42 * File id. If omitted, a unique name is generated from the stack, stage,
43 * and logical id. Immutable — changing it replaces the file.
44 */
45 fileId?: string;
46 /**
47 * File contents as UTF-8 text. Sent as `google.api.HttpBody`.
48 */
49 content?: string;
50};
51
52export type EnvironmentsResourcefile = Resource<
53 "GCP.Apigee.EnvironmentsResourcefile",
54 EnvironmentsResourcefileProps,
55 {
56 /** Parent environment `organizations/{org}/environments/{env}`. */
57 parent: string;
58 /** File id. */
59 fileId: string;
60 /** Resource file type. */
61 fileType: string;
62 /** Apigee organization id. */
63 organizationId: string;
64 /** Environment id. */
65 environmentId: string;
66 },
67 never,
68 Providers
69>;
70
71/**
72 * An environment-scoped Apigee resource file (JavaScript, Java, XSL, …).
73 *
74 * Resource files have no labels. `list` enumerates files in Apigee
75 * environments mapped to this GCP project. Type and name are identity;
76 * `content` updates in place.
77 *
78 * ### Creating a JavaScript File
79 * **Example:** Generated name
80 * ```typescript
81 * const file = yield* GCP.Apigee.EnvironmentsResourcefile("Helper", {
82 * environment: "eval",
83 * fileType: "js",
84 * content: "function helper() { return 1; }",
85 * });
86 * ```
87 *
88 * @resource
89 * @category Apigee
90 */
91export const EnvironmentsResourcefile = Resource<EnvironmentsResourcefile>(
92 "GCP.Apigee.EnvironmentsResourcefile",
93);
94
95export class EnvironmentsResourcefileNotResolved extends Data.TaggedError(
96 "GCP.Apigee.EnvironmentsResourcefileNotResolved",
97)<{
98 parent: string;
99 fileType: string;
100 fileId: string;
101}> {}
102
103const fileTypeOf = (value: string | undefined) => value ?? DEFAULT_TYPE;
104
105const toAttrs = (
106 fileId: string,
107 fileType: string,
108 organizationId: string,
109 environmentId: string,
110) => ({
111 parent: environmentNameOf(organizationId, environmentId),
112 fileId,
113 fileType,
114 organizationId,
115 environmentId,
116});
117
118const toHttpBody = (content: string) =>
119 Effect.sync((): apigee.GoogleApiHttpBody => ({
120 contentType: "application/octet-stream",
121 data: Buffer.from(content, "utf8").toString("base64"),
122 }));
123
124const getById = (parent: string, type: string, name: string) =>
125 missingToUndefined(
126 apigee.getOrganizationsEnvironmentsResourcefiles({
127 parent,
128 type,
129 name,
130 }),
131 );
132
133const listFiles = (parent: string, type?: string) =>
134 emptyOnMissing(
135 apigee.listOrganizationsEnvironmentsResourcefiles({
136 parent,
137 type,
138 }),
139 { resourceFile: [] as apigee.GoogleCloudApigeeV1ResourceFileList },
140 );
141
142const hasFile = (
143 files: readonly apigee.GoogleCloudApigeeV1ResourceFile[] | undefined,
144 fileId: string,
145 fileType: string,
146) =>
147 (files ?? []).some(
148 (file) =>
149 lastSegment(stripRevision(file.name ?? "")) === fileId &&
150 (file.type === undefined || file.type === fileType),
151 );
152
153export const EnvironmentsResourcefileProvider = () =>
154 Provider.succeed(EnvironmentsResourcefile, {
155 stables: [
156 "parent",
157 "fileId",
158 "fileType",
159 "organizationId",
160 "environmentId",
161 ],
162
163 diff: Effect.fn(function* ({ news, olds, output }) {
164 if (!isResolved(news)) return undefined;
165 const previousId = olds?.fileId ?? output?.fileId;
166 const previousType = olds?.fileType ?? output?.fileType;
167 const previousOrg = olds?.organization ?? output?.organizationId;
168 const previousEnv = olds?.environment ?? output?.environmentId;
169 const idChanged =
170 previousId !== undefined &&
171 news.fileId !== undefined &&
172 news.fileId !== previousId;
173 const typeChanged =
174 previousType !== undefined &&
175 news.fileType !== undefined &&
176 fileTypeOf(news.fileType) !== fileTypeOf(previousType);
177 const orgChanged =
178 previousOrg !== undefined &&
179 news.organization !== undefined &&
180 organizationIdOf(news.organization, "") !==
181 organizationIdOf(previousOrg, "");
182 const envChanged =
183 previousEnv !== undefined &&
184 environmentIdOf(news.environment) !== environmentIdOf(previousEnv);
185 if (idChanged || typeChanged || orgChanged || envChanged) {
186 return { action: "replace" as const, deleteFirst: false };
187 }
188 return undefined;
189 }),
190
191 read: Effect.fn(function* ({ id, olds, output }) {
192 const { project } = yield* GcpEnvironment.current;
193 const organizationId = organizationIdOf(
194 olds?.organization ?? output?.organizationId,
195 project,
196 );
197 const environmentId = environmentIdOf(
198 olds?.environment ?? output?.environmentId ?? "",
199 );
200 const fileType = fileTypeOf(olds?.fileType ?? output?.fileType);
201 const fileId = yield* toResourceId(id, olds?.fileId, output?.fileId, {
202 maxLength: MAX_NAME_LENGTH,
203 });
204 const parent =
205 output?.parent ?? environmentNameOf(organizationId, environmentId);
206 const existing = yield* getById(parent, fileType, fileId);
207 if (existing === undefined) return undefined;
208 return toAttrs(fileId, fileType, organizationId, environmentId);
209 }),
210
211 list: () =>
212 Effect.gen(function* () {
213 const environments = yield* listProjectEnvironments();
214 const found: EnvironmentsResourcefile["Attributes"][] = [];
215 for (const item of environments) {
216 const page = yield* listFiles(item.parent);
217 for (const file of page.resourceFile ?? []) {
218 if (!file.name) continue;
219 found.push(
220 toAttrs(
221 lastSegment(stripRevision(file.name)),
222 file.type ?? DEFAULT_TYPE,
223 item.organizationId,
224 item.environmentId,
225 ),
226 );
227 }
228 }
229 return found;
230 }),
231
232 reconcile: Effect.fn(function* ({ id, news, output }) {
233 const { project } = yield* GcpEnvironment.current;
234 const organizationId = organizationIdOf(news.organization, project);
235 const environmentId = environmentIdOf(news.environment);
236 const fileType = fileTypeOf(news.fileType);
237 const fileId = yield* toResourceId(id, news.fileId, output?.fileId, {
238 maxLength: MAX_NAME_LENGTH,
239 });
240 const parent = environmentNameOf(organizationId, environmentId);
241 const body = yield* toHttpBody(news.content ?? "");
242
243 let current = yield* getById(parent, fileType, fileId);
244
245 if (current === undefined) {
246 yield* apigee
247 .createOrganizationsEnvironmentsResourcefiles({
248 parent,
249 type: fileType,
250 name: fileId,
251 body,
252 })
253 .pipe(
254 Effect.catchTag("Conflict", () =>
255 getById(parent, fileType, fileId),
256 ),
257 );
258 current = yield* getById(parent, fileType, fileId);
259 } else if (news.content !== undefined) {
260 yield* apigee.updateOrganizationsEnvironmentsResourcefiles({
261 parent,
262 type: fileType,
263 name: fileId,
264 body,
265 });
266 }
267
268 const listed = yield* listFiles(parent, fileType);
269 if (
270 current === undefined &&
271 !hasFile(listed.resourceFile, fileId, fileType)
272 ) {
273 return yield* new EnvironmentsResourcefileNotResolved({
274 parent,
275 fileType,
276 fileId,
277 });
278 }
279
280 return toAttrs(fileId, fileType, organizationId, environmentId);
281 }),
282
283 delete: Effect.fn(function* ({ output }) {
284 yield* apigee
285 .deleteOrganizationsEnvironmentsResourcefiles({
286 parent: output.parent,
287 type: output.fileType,
288 name: output.fileId,
289 })
290 .pipe(
291 Effect.catchTag(
292 ["NotFound", "ApigeeResourceNotFound"],
293 () => Effect.void,
294 ),
295 );
296 }),
297 });