1import * as Effect from "effect/Effect";
2import { createPhysicalName } from "../../PhysicalName.ts";
3import {
4 alchemyLabelKeys,
5 createInternalLabels,
6 hasAlchemyLabels,
7 sanitizeLabelValue,
8} from "../Labels.ts";
9
10export const MAX_NAME_LENGTH = 64;
11
12/** Locations swept by `list`: the stack region and the multi-regions (the API has no `locations/-` wildcard). */
13export const listLocations = (region: string) =>
14 Array.from(new Set([region, "US", "EU"]));
15const OWNER_PREFIX = "alch---";
16
17export const lastSegment = (value: string) => {
18 const trimmed = value.replace(/\/+$/, "");
19 const parts = trimmed.split("/");
20 return parts[parts.length - 1] || trimmed;
21};
22
23export const normalizeLocation = (location: string) => {
24 const value = lastSegment(location);
25 const upper = value.toUpperCase();
26 if (upper === "US" || upper === "EU") return upper;
27 return value.toLowerCase();
28};
29
30export const parentOf = (project: string, location: string) =>
31 `projects/${project}/locations/${location}`;
32
33export const parseResourceName = (
34 name: string,
35 collection: "capacityCommitments" | "reservationGroups",
36) => {
37 const parts = name.split("/").filter((part) => part.length > 0);
38 const collectionAt = parts.lastIndexOf(collection);
39 const locationsAt = parts.lastIndexOf("locations");
40 const projectsAt = parts.lastIndexOf("projects");
41 return {
42 project:
43 projectsAt >= 0 && parts[projectsAt + 1] ? parts[projectsAt + 1]! : "",
44 location:
45 locationsAt >= 0 && parts[locationsAt + 1] ? parts[locationsAt + 1]! : "",
46 resourceId:
47 collectionAt >= 0 && parts[collectionAt + 1]
48 ? parts[collectionAt + 1]!
49 : lastSegment(name),
50 };
51};
52
53export const compact = <T extends Record<string, unknown>>(value: T): T =>
54 Object.fromEntries(
55 Object.entries(value).filter(([, item]) => item !== undefined),
56 ) as T;
57
58const sanitizeIdPart = (value: string) => {
59 const cleaned = sanitizeLabelValue(value)
60 .replace(/_/g, "-")
61 .replace(/-+/g, "-")
62 .replace(/^-|-$/g, "");
63 return cleaned.length > 0 ? cleaned : "x";
64};
65
66const ensureId = (value: string) => {
67 const cleaned = value
68 .toLowerCase()
69 .replace(/[^a-z0-9-]/g, "-")
70 .replace(/^-|-$/g, "")
71 .slice(0, MAX_NAME_LENGTH)
72 .replace(/-+$/g, "");
73 if (cleaned.length === 0) return "alchx";
74 return /^[a-z]/.test(cleaned)
75 ? cleaned
76 : `a${cleaned}`.slice(0, MAX_NAME_LENGTH);
77};
78
79export const encodeOwnershipId = (
80 labels: Record<string, string>,
81 extra?: string,
82): string => {
83 let stack = sanitizeIdPart(labels[alchemyLabelKeys.stack] ?? "x");
84 let stage = sanitizeIdPart(labels[alchemyLabelKeys.stage] ?? "x");
85 let id = sanitizeIdPart(labels[alchemyLabelKeys.id] ?? "x");
86 let rest = extra ? sanitizeIdPart(extra) : "";
87 const build = () =>
88 rest.length > 0
89 ? `${OWNER_PREFIX}${stack}---${stage}---${id}---${rest}`
90 : `${OWNER_PREFIX}${stack}---${stage}---${id}`;
91 let name = build();
92 while (name.length > MAX_NAME_LENGTH) {
93 if (rest.length > 1) rest = rest.slice(0, -1);
94 else if (id.length > 1) id = id.slice(0, -1);
95 else if (stack.length > 1) stack = stack.slice(0, -1);
96 else if (stage.length > 1) stage = stage.slice(0, -1);
97 else break;
98 name = build();
99 }
100 return name.slice(0, MAX_NAME_LENGTH).replace(/-+$/g, "");
101};
102
103export const parseOwnershipId = (
104 resourceId: string | undefined,
105): { labels: Record<string, string> } => {
106 const value = resourceId ?? "";
107 if (!value.startsWith(OWNER_PREFIX)) return { labels: {} };
108 const parts = value.slice(OWNER_PREFIX.length).split("---");
109 if (parts.length < 3) return { labels: {} };
110 return {
111 labels: {
112 [alchemyLabelKeys.stack]: parts[0] ?? "",
113 [alchemyLabelKeys.stage]: parts[1] ?? "",
114 [alchemyLabelKeys.id]: parts[2] ?? "",
115 },
116 };
117};
118
119export const hasOwnershipMarker = (resourceId: string | undefined) =>
120 (resourceId ?? "").startsWith("alch-") &&
121 Object.keys(parseOwnershipId(resourceId).labels).some((key) =>
122 key.startsWith("alchemy-"),
123 );
124
125const prefixMatch = (expected: string, observed: string) =>
126 expected === observed ||
127 expected.startsWith(observed) ||
128 observed.startsWith(expected);
129
130export const ownedByAlchemy = (id: string, resourceId: string | undefined) =>
131 Effect.gen(function* () {
132 if (!hasOwnershipMarker(resourceId)) return false;
133 const expected = yield* createInternalLabels(id);
134 const { labels } = parseOwnershipId(resourceId);
135 const exact = yield* hasAlchemyLabels(id, labels);
136 if (exact) return true;
137 return (
138 prefixMatch(
139 sanitizeIdPart(expected[alchemyLabelKeys.stack] ?? ""),
140 labels[alchemyLabelKeys.stack] ?? "",
141 ) &&
142 prefixMatch(
143 sanitizeIdPart(expected[alchemyLabelKeys.stage] ?? ""),
144 labels[alchemyLabelKeys.stage] ?? "",
145 ) &&
146 prefixMatch(
147 sanitizeIdPart(expected[alchemyLabelKeys.id] ?? ""),
148 labels[alchemyLabelKeys.id] ?? "",
149 )
150 );
151 });
152
153export const toResourceId = (
154 id: string,
155 requested: string | undefined,
156 existing?: string,
157) =>
158 Effect.gen(function* () {
159 const labels = yield* createInternalLabels(id);
160 if (requested !== undefined && requested.length > 0) {
161 if (requested.startsWith("alch-")) return ensureId(requested);
162 return encodeOwnershipId(labels, requested);
163 }
164 if (existing !== undefined && existing.length > 0) return existing;
165 const physical = yield* createPhysicalName({
166 id,
167 maxLength: 40,
168 lowercase: true,
169 });
170 return encodeOwnershipId(labels, physical);
171 });