1import * as connectors from "@distilled.cloud/gcp/connectors_v2";
2import * as Effect from "effect/Effect";
3import * as Schedule from "effect/Schedule";
4
5export type EntityFields = Record<string, unknown>;
6
7export const lastSegment = (value: string) => {
8 const trimmed = value.replace(/\/+$/, "");
9 const parts = trimmed.split("/");
10 return parts[parts.length - 1] || trimmed;
11};
12
13export const parentOf = (name: string) => {
14 const parts = name.split("/").filter((part) => part.length > 0);
15 return parts.slice(0, -2).join("/");
16};
17
18const segmentAfter = (parts: readonly string[], key: string) => {
19 const index = parts.lastIndexOf(key);
20 return index >= 0 && parts[index + 1] ? parts[index + 1]! : "";
21};
22
23export const parseEntityName = (name: string) => {
24 const parts = name.split("/").filter((part) => part.length > 0);
25 return {
26 project: segmentAfter(parts, "projects"),
27 location: segmentAfter(parts, "locations"),
28 connection: segmentAfter(parts, "connections"),
29 entityType: segmentAfter(parts, "entityTypes"),
30 entityId: segmentAfter(parts, "entities") || lastSegment(name),
31 parent: parentOf(name),
32 };
33};
34
35export const entityNameOf = (parent: string, entityId: string) =>
36 `${parent.replace(/\/+$/, "")}/entities/${entityId}`;
37
38const canonical = (value: unknown): unknown => {
39 if (value === undefined || value === null) return null;
40 if (Array.isArray(value)) return value.map(canonical);
41 if (typeof value === "object") {
42 return Object.fromEntries(
43 Object.entries(value as Record<string, unknown>)
44 .sort(([left], [right]) => left.localeCompare(right))
45 .map(([key, item]) => [key, canonical(item)]),
46 );
47 }
48 return value;
49};
50
51export const sameJson = (left: unknown, right: unknown) =>
52 JSON.stringify(canonical(left)) === JSON.stringify(canonical(right));
53
54export const retryTransient = <A, E extends { readonly _tag: string }, R>(
55 effect: Effect.Effect<A, E, R>,
56) =>
57 effect.pipe(
58 Effect.retry({
59 while: (error) =>
60 error._tag === "TooManyRequests" ||
61 error._tag === "InternalServerError" ||
62 error._tag === "BadGateway" ||
63 error._tag === "ServiceUnavailable" ||
64 error._tag === "GatewayTimeout",
65 times: 8,
66 schedule: Schedule.exponential("250 millis"),
67 }),
68 );
69
70export const getEntity = (name: string) =>
71 name.length === 0
72 ? Effect.succeed(undefined)
73 : connectors
74 .getProjectsLocationsConnectionsEntityTypesEntities({ name })
75 .pipe(
76 // A missing connection answers 501, so its entities are gone too.
77 Effect.catchTag(["NotFound", "EntitiesNotImplemented"], () =>
78 Effect.succeed(undefined),
79 ),
80 );