1import * as marketplace from "@distilled.cloud/gcp/authorizedbuyersmarketplace_v1";
2import * as Effect from "effect/Effect";
3import * as Stream from "effect/Stream";
4import { createPhysicalName } from "../../PhysicalName.ts";
5
6export const PROBE_PARENT = "buyers/1/clients/1";
7export const PROBE_NAME = `${PROBE_PARENT}/users/0`;
8
9export type ClientUserState = marketplace.ClientUserStateEnum;
10
11export const lastSegment = (value: string) => {
12 const trimmed = value.replace(/\/+$/, "");
13 const parts = trimmed.split("/");
14 return parts[parts.length - 1] || trimmed;
15};
16
17export const normalizeResourceName = (value: string) =>
18 value.replace(/\/+$/, "").trim();
19
20export const expandParent = (value: string) => {
21 const trimmed = normalizeResourceName(value);
22 if (trimmed.length === 0) return trimmed;
23 if (trimmed.includes("/clients/")) {
24 return trimmed.startsWith("buyers/") ? trimmed : `buyers/${trimmed}`;
25 }
26 return trimmed;
27};
28
29export const parentOfName = (name: string) => {
30 const parts = name.split("/").filter((part) => part.length > 0);
31 const usersIndex = parts.lastIndexOf("users");
32 if (usersIndex > 0) return parts.slice(0, usersIndex).join("/");
33 const clientUsersIndex = parts.lastIndexOf("clientUsers");
34 if (clientUsersIndex > 0) return parts.slice(0, clientUsersIndex).join("/");
35 return "";
36};
37
38export const resourceName = (parent: string, userId: string) =>
39 `${expandParent(parent)}/users/${userId}`;
40
41export const userIdOf = (name: string) => lastSegment(name);
42
43export const sameText = (left: string | undefined, right: string | undefined) =>
44 (left ?? "") === (right ?? "");
45
46export const normalizeEmail = (email: string | undefined) =>
47 (email ?? "").trim().toLowerCase();
48
49export const replaceOnIdentity = (input: {
50 previousParent?: string;
51 nextParent: string;
52 previousEmail?: string;
53 nextEmail?: string;
54 previousUserId?: string;
55 nextUserId?: string;
56}) => {
57 if (
58 input.previousParent !== undefined &&
59 input.previousParent.length > 0 &&
60 input.previousParent !== input.nextParent
61 ) {
62 return { action: "replace" as const, deleteFirst: false };
63 }
64 if (
65 input.previousEmail !== undefined &&
66 input.nextEmail !== undefined &&
67 normalizeEmail(input.previousEmail) !== normalizeEmail(input.nextEmail)
68 ) {
69 return { action: "replace" as const, deleteFirst: false };
70 }
71 if (
72 input.previousUserId !== undefined &&
73 input.nextUserId !== undefined &&
74 input.previousUserId !== input.nextUserId
75 ) {
76 return { action: "replace" as const, deleteFirst: true };
77 }
78 return undefined;
79};
80
81export const toEmail = (
82 id: string,
83 requested: string | undefined,
84 existing: string | undefined,
85) =>
86 Effect.gen(function* () {
87 if (requested !== undefined && requested.length > 0) return requested;
88 if (existing !== undefined && existing.length > 0) return existing;
89 const generated = yield* createPhysicalName({
90 id,
91 maxLength: 32,
92 lowercase: true,
93 });
94 const local = generated.replace(/[^a-z0-9]/g, "").slice(0, 32) || "alchemy";
95 return `${local}@example.com`;
96 });
97
98export type ClientUserAttrs = {
99 name: string;
100 userId: string;
101 parent: string;
102 project: string;
103 email: string;
104 state: string | undefined;
105};
106
107export const toAttrs = (
108 user: marketplace.ClientUser,
109 parent: string,
110 project: string,
111): ClientUserAttrs => {
112 const name = user.name ?? "";
113 return {
114 name,
115 userId: userIdOf(name),
116 parent: parentOfName(name) || parent,
117 project,
118 email: user.email ?? "",
119 state: user.state,
120 };
121};
122
123/** Email is unique per client, so it identifies the user. */
124export const findUserByEmail = (
125 users: readonly marketplace.ClientUser[],
126 email: string,
127) => users.find((user) => normalizeEmail(user.email) === normalizeEmail(email));
128
129export const listUsers = (parent: string) =>
130 marketplace.listBuyersClientsUsers.pages({ parent, pageSize: 200 }).pipe(
131 Stream.flatMap((page) => Stream.fromIterable(page.clientUsers ?? [])),
132 Stream.runCollect,
133 Effect.map((chunk) => Array.from(chunk)),
134 );