1import * as androidpublisher from "@distilled.cloud/gcp/androidpublisher_v3";
2import * as Data from "effect/Data";
3import * as Effect from "effect/Effect";
4import { Unowned } from "../../AdoptPolicy.ts";
5import { isResolved } from "../../Diff.ts";
6import * as Provider from "../../Provider.ts";
7import { Resource } from "../../Resource.ts";
8import { GcpEnvironment } from "../Environment.ts";
9import type { Providers } from "../Providers.ts";
10import { getEdit, ignoreMissing } from "./internal.ts";
11
12export type EditProps = {
13 /**
14 * Play package name of the app (for example `com.example.app`).
15 * Immutable — changing it replaces the edit.
16 */
17 packageName: string;
18 /**
19 * Server-assigned edit id. Immutable — changing it replaces the edit.
20 * An expired edit is recreated on the next reconcile.
21 */
22 editId?: string;
23};
24
25export type Edit = Resource<
26 "GCP.AndroidPublisher.Edit",
27 EditProps,
28 {
29 /** Server-assigned edit id. */
30 editId: string;
31 /** Play package name. */
32 packageName: string;
33 /** Project id used when the edit was reconciled. */
34 project: string;
35 /** Unix epoch seconds when the edit expires. */
36 expiryTimeSeconds: string | undefined;
37 },
38 never,
39 Providers
40>;
41
42/**
43 * A Google Play app edit (`edits`).
44 *
45 * An edit is a short-lived staging session for APK, listing, and track
46 * changes. The API has no list method and no labels field, so `list`
47 * returns an empty set — nuke cannot discover orphaned edits, and they
48 * expire on their own. `packageName` and `editId` are identity. There is
49 * nothing mutable to sync: reconcile observes the edit and inserts one
50 * when it is missing or expired.
51 *
52 * ### Creating an Edit
53 * **Example:** Open an edit
54 * ```typescript
55 * const edit = yield* GCP.AndroidPublisher.Edit("Release", {
56 * packageName: "com.example.app",
57 * });
58 * ```
59 *
60 * ### Reusing an Edit
61 * **Example:** Continue an existing edit
62 * ```typescript
63 * const edit = yield* GCP.AndroidPublisher.Edit("Release", {
64 * packageName: "com.example.app",
65 * editId: "03487915625743125468", // an edit opened elsewhere
66 * });
67 * ```
68 *
69 * @resource
70 * @category AndroidPublisher
71 */
72export const Edit = Resource<Edit>("GCP.AndroidPublisher.Edit");
73
74export class EditNotResolved extends Data.TaggedError(
75 "GCP.AndroidPublisher.EditNotResolved",
76)<{
77 packageName: string;
78 editId: string;
79}> {}
80
81const toAttrs = (
82 edit: androidpublisher.AppEdit,
83 packageName: string,
84 project: string,
85) => ({
86 editId: edit.id ?? "",
87 packageName,
88 project,
89 expiryTimeSeconds: edit.expiryTimeSeconds,
90});
91
92export const EditProvider = () =>
93 Provider.succeed(Edit, {
94 stables: ["editId", "packageName", "project"],
95
96 diff: Effect.fn(function* ({ news, olds, output }) {
97 if (!isResolved(news)) return undefined;
98 const previousPackage = olds?.packageName ?? output?.packageName;
99 if (
100 previousPackage !== undefined &&
101 news.packageName !== previousPackage
102 ) {
103 return { action: "replace" as const, deleteFirst: false };
104 }
105 const previousId = olds?.editId ?? output?.editId;
106 if (
107 previousId !== undefined &&
108 news.editId !== undefined &&
109 news.editId !== previousId
110 ) {
111 return { action: "replace" as const, deleteFirst: true };
112 }
113 return undefined;
114 }),
115
116 read: Effect.fn(function* ({ olds, output }) {
117 const env = yield* GcpEnvironment.current;
118 const packageName = olds?.packageName ?? output?.packageName ?? "";
119 const editId = olds?.editId ?? output?.editId ?? "";
120 const existing = yield* getEdit(packageName, editId);
121 if (existing === undefined) return undefined;
122 const attrs = toAttrs(existing, packageName, env.project);
123 return output !== undefined ? attrs : Unowned(attrs);
124 }),
125
126 list: () =>
127 Effect.succeed(
128 [] as Array<{
129 editId: string;
130 packageName: string;
131 project: string;
132 expiryTimeSeconds: string | undefined;
133 }>,
134 ),
135
136 reconcile: Effect.fn(function* ({ news, output }) {
137 const env = yield* GcpEnvironment.current;
138 const packageName = news.packageName;
139 const editId = news.editId ?? output?.editId ?? "";
140
141 let current = yield* getEdit(packageName, editId);
142
143 if (current === undefined) {
144 const created = yield* androidpublisher
145 .insertEdits({
146 packageName,
147 body: {},
148 })
149 .pipe(
150 Effect.catchTag("Conflict", () => getEdit(packageName, editId)),
151 );
152 current = created ?? undefined;
153 }
154
155 if (current === undefined) {
156 return yield* new EditNotResolved({
157 packageName,
158 editId,
159 });
160 }
161
162 return toAttrs(current, packageName, env.project);
163 }),
164
165 delete: Effect.fn(function* ({ output }) {
166 if (!output.packageName || !output.editId) return;
167 yield* ignoreMissing(
168 androidpublisher.deleteEdits({
169 packageName: output.packageName,
170 editId: output.editId,
171 }),
172 );
173 }),
174 });