1import * as licensing from "@distilled.cloud/gcp/licensing_v1";
2import * as Config from "effect/Config";
3import * as Effect from "effect/Effect";
4import * as Option from "effect/Option";
5import * as Stream from "effect/Stream";
6
7export const DEFAULT_PRODUCT_ID = "Google-Apps";
8export const DEFAULT_CUSTOMER_ID = "my_customer";
9
10export const sameText = (left: string | undefined, right: string | undefined) =>
11 (left ?? "") === (right ?? "");
12
13export const sameUser = (left: string | undefined, right: string | undefined) =>
14 (left ?? "").trim().toLowerCase() === (right ?? "").trim().toLowerCase();
15
16export const replaceOnIdentity = (input: {
17 previousProductId?: string;
18 nextProductId?: string;
19 previousUserId?: string;
20 nextUserId?: string;
21}) => {
22 if (
23 input.previousProductId !== undefined &&
24 input.nextProductId !== undefined &&
25 !sameText(input.previousProductId, input.nextProductId)
26 ) {
27 return { action: "replace" as const, deleteFirst: true };
28 }
29 if (
30 input.previousUserId !== undefined &&
31 input.nextUserId !== undefined &&
32 !sameUser(input.previousUserId, input.nextUserId)
33 ) {
34 return { action: "replace" as const, deleteFirst: true };
35 }
36 return undefined;
37};
38
39export const normalizeCustomerId = (value: string | undefined) => {
40 const trimmed = value?.trim();
41 if (trimmed === undefined || trimmed.length === 0) {
42 return undefined;
43 }
44 return trimmed.startsWith("customers/")
45 ? trimmed.slice("customers/".length)
46 : trimmed;
47};
48
49/** Optional setting from the ConfigProvider (env by default). */
50const optionalSetting = (key: string) =>
51 Effect.gen(function* () {
52 const value = Option.getOrUndefined(
53 yield* Config.option(Config.String(key)),
54 )?.trim();
55 return value !== undefined && value.length > 0 ? value : undefined;
56 });
57
58/** Customer for `list` and the `customerId` default: `GOOGLE_LICENSE_CUSTOMER_ID`. */
59export const listCustomerId = () =>
60 Effect.gen(function* () {
61 return normalizeCustomerId(
62 (yield* optionalSetting("GOOGLE_LICENSE_CUSTOMER_ID")) ??
63 (yield* optionalSetting("GOOGLE_WORKSPACE_CUSTOMER_ID")),
64 );
65 });
66
67/** Product swept by `list`: `GOOGLE_LICENSE_PRODUCT_ID`. */
68export const listProductId = () => optionalSetting("GOOGLE_LICENSE_PRODUCT_ID");
69
70/** SKU swept by `list`: `GOOGLE_LICENSE_SKU_ID`. */
71export const listSkuId = () => optionalSetting("GOOGLE_LICENSE_SKU_ID");
72
73/** User whose assignments `list` returns: `GOOGLE_LICENSE_USER_ID`. */
74export const listUserId = () => optionalSetting("GOOGLE_LICENSE_USER_ID");
75
76const emptyList = <A>() => Effect.succeed([] as A[]);
77
78export const getAssignment = (
79 productId: string,
80 skuId: string,
81 userId: string,
82) =>
83 productId.length === 0 || skuId.length === 0 || userId.length === 0
84 ? Effect.succeed(undefined)
85 : licensing
86 .getLicenseAssignments({
87 productId,
88 skuId,
89 userId,
90 })
91 .pipe(Effect.catchTag("NotFound", () => Effect.succeed(undefined)));
92
93export const listAssignments = (
94 productId: string,
95 customerId: string,
96 skuId?: string,
97) => {
98 if (productId.length === 0 || customerId.length === 0) {
99 return emptyList<licensing.LicenseAssignment>();
100 }
101 const pages =
102 skuId !== undefined && skuId.length > 0
103 ? licensing.listForProductAndSkuLicenseAssignments.pages({
104 productId,
105 skuId,
106 customerId,
107 maxResults: 100,
108 })
109 : licensing.listForProductLicenseAssignments.pages({
110 productId,
111 customerId,
112 maxResults: 100,
113 });
114 return pages.pipe(
115 Stream.flatMap((page) => Stream.fromIterable(page.items ?? [])),
116 Stream.runCollect,
117 Effect.map((chunk) => Array.from(chunk)),
118 Effect.catchTag("NotFound", () => emptyList<licensing.LicenseAssignment>()),
119 );
120};
121
122export const findAssignment = (
123 productId: string,
124 userId: string,
125 customerId: string | undefined,
126 skuId?: string,
127) =>
128 Effect.gen(function* () {
129 if (customerId === undefined || customerId.length === 0) {
130 return undefined;
131 }
132 const items = yield* listAssignments(productId, customerId, skuId);
133 return items.find((item) => sameUser(item.userId, userId));
134 });
135
136export const deleteAssignment = (
137 productId: string,
138 skuId: string,
139 userId: string,
140) =>
141 productId.length === 0 || skuId.length === 0 || userId.length === 0
142 ? Effect.void
143 : licensing
144 .deleteLicenseAssignments({
145 productId,
146 skuId,
147 userId,
148 })
149 .pipe(Effect.catchTag("NotFound", () => Effect.void));