@effected/github 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/GraphQL.js ADDED
@@ -0,0 +1,183 @@
1
+ import { retryAfterMillisFrom } from "./internal/headers.js";
2
+ import { Schema } from "effect";
3
+
4
+ //#region src/GraphQL.ts
5
+ /**
6
+ * One entry from a GraphQL response's `errors` array.
7
+ *
8
+ * @public
9
+ */
10
+ var GraphQLErrorEntry = class extends Schema.Class("GraphQLErrorEntry")({
11
+ /** GitHub's prose. */
12
+ message: Schema.String,
13
+ /** GitHub's own classification, e.g. `"NOT_FOUND"` or `"FORBIDDEN"`. */
14
+ type: Schema.optionalKey(Schema.String)
15
+ }) {};
16
+ /**
17
+ * A GraphQL call failed.
18
+ *
19
+ * @remarks
20
+ * Separate from `GitHubError` because GraphQL genuinely answers differently:
21
+ * a 200 response can still carry failures, and it carries a **list** of them.
22
+ * `errors` is the one structured field a surveyed consumer actually read.
23
+ *
24
+ * @public
25
+ */
26
+ var GitHubGraphQLError = class GitHubGraphQLError extends Schema.TaggedErrorClass()("GitHubGraphQLError", {
27
+ /**
28
+ * Structural routing, mirroring `GitHubError`'s.
29
+ *
30
+ * @remarks
31
+ * `"alreadyExists"` exists here for the same reason it exists on the REST
32
+ * error: without it a consumer lowercases the message and greps it for
33
+ * `"already"` and `"exists"`, which is exactly what one surveyed repo did to
34
+ * make project creation idempotent.
35
+ */
36
+ kind: Schema.Literals([
37
+ "alreadyExists",
38
+ "notFound",
39
+ "rejected",
40
+ "unauthorized",
41
+ "rateLimited",
42
+ "transport",
43
+ "decode"
44
+ ]),
45
+ /** The document's name, e.g. `"linkedIssues"` — never the literal `"graphql"`. */
46
+ operation: Schema.String,
47
+ /** Human-readable cause, for logs. */
48
+ reason: Schema.String,
49
+ /** Everything GitHub reported, in order. */
50
+ errors: Schema.Array(GraphQLErrorEntry),
51
+ /** A server-advised delay in milliseconds, read only by the retry schedule. */
52
+ retryAfterMillis: Schema.optionalKey(Schema.Int),
53
+ /** The underlying throwable, when one exists. */
54
+ cause: Schema.optionalKey(Schema.Defect())
55
+ }) {
56
+ get message() {
57
+ return `${this.operation} failed: ${this.reason}`;
58
+ }
59
+ /** Whether retrying could plausibly succeed. Derived, like the REST error's. */
60
+ get retryable() {
61
+ return this.kind === "transport" || this.kind === "rateLimited";
62
+ }
63
+ /** A response arrived but did not match the document's declared schema. */
64
+ static decode(operation, reason, cause) {
65
+ return new GitHubGraphQLError({
66
+ kind: "decode",
67
+ operation,
68
+ reason,
69
+ errors: [],
70
+ ...cause !== void 0 ? { cause } : {}
71
+ });
72
+ }
73
+ /**
74
+ * Classify anything the GraphQL transport threw.
75
+ *
76
+ * @remarks
77
+ * octokit surfaces two different failures here: a `GraphqlResponseError`,
78
+ * which is an HTTP 200 whose body carries `errors`, and an ordinary HTTP
79
+ * failure with a `status`. Both arrive as throwables and both are read
80
+ * structurally, for the same reason the REST classifier does it — the error
81
+ * classes live in packages this one does not declare.
82
+ */
83
+ static fromThrowable(operation, error, nowMillis) {
84
+ const record = typeof error === "object" && error !== null ? error : void 0;
85
+ const status = typeof record?.status === "number" ? record.status : void 0;
86
+ const headers = asRecord(record?.headers) ?? asRecord(asRecord(record?.response)?.headers);
87
+ const entries = readEntries(record?.errors);
88
+ const reason = entries.length > 0 ? entries.map((entry) => entry.message).join("; ") : typeof record?.message === "string" ? record.message : String(error);
89
+ const retryAfterMillis = retryAfterMillisFrom(headers, nowMillis);
90
+ return new GitHubGraphQLError({
91
+ kind: classify(status, entries, reason, retryAfterMillis),
92
+ operation,
93
+ reason,
94
+ errors: entries,
95
+ ...retryAfterMillis !== void 0 ? { retryAfterMillis } : {},
96
+ cause: error
97
+ });
98
+ }
99
+ };
100
+ const asRecord = (value) => typeof value === "object" && value !== null ? value : void 0;
101
+ const readEntries = (value) => {
102
+ if (!Array.isArray(value)) return [];
103
+ const entries = [];
104
+ for (const raw of value) {
105
+ const record = asRecord(raw);
106
+ if (record === void 0) continue;
107
+ const message = typeof record.message === "string" ? record.message : String(raw);
108
+ const type = typeof record.type === "string" ? record.type : void 0;
109
+ entries.push(GraphQLErrorEntry.make({
110
+ message,
111
+ ...type !== void 0 ? { type } : {}
112
+ }));
113
+ }
114
+ return entries;
115
+ };
116
+ const classify = (status, entries, reason, retryAfterMillis) => {
117
+ if (reason.toLowerCase().includes("already exists") || entries.some((entry) => entry.message.toLowerCase().includes("already exists"))) return "alreadyExists";
118
+ if (entries.some((entry) => entry.type === "NOT_FOUND")) return "notFound";
119
+ if (entries.some((entry) => entry.type === "FORBIDDEN" || entry.type === "UNAUTHORIZED")) return "unauthorized";
120
+ if (entries.some((entry) => entry.type === "RATE_LIMITED")) return "rateLimited";
121
+ if (status === void 0) return entries.length > 0 ? "rejected" : "transport";
122
+ if (status === 404) return "notFound";
123
+ if (status === 401) return "unauthorized";
124
+ if (status === 429) return "rateLimited";
125
+ if (status === 403) return retryAfterMillis !== void 0 ? "rateLimited" : "unauthorized";
126
+ if (status >= 500) return "transport";
127
+ return "rejected";
128
+ };
129
+ /**
130
+ * A named GraphQL document, its variables, and how to read its answer.
131
+ *
132
+ * @remarks
133
+ * This is the mechanism that makes `client.graphql` return a **domain value**
134
+ * rather than an `unknown` the caller casts. The package this replaces took a
135
+ * query string and a caller-chosen type parameter with nothing connecting them,
136
+ * so every consumer wrote its own response interface and hoped.
137
+ *
138
+ * The kit owns the documents its resources need; a consumer with a domain of
139
+ * its own — silk-sync-action's ProjectV2 work, for instance — builds its own
140
+ * `GraphQLDocument` and gets the same typing and the same error taxonomy
141
+ * without this package having to know about its schema.
142
+ *
143
+ * @example
144
+ * ```ts
145
+ * import { GraphQLDocument } from "@effected/github";
146
+ * import { Schema } from "effect";
147
+ *
148
+ * const ViewerLogin = GraphQLDocument.make({
149
+ * name: "viewerLogin",
150
+ * document: `query { viewer { login } }`,
151
+ * response: Schema.Struct({ viewer: Schema.Struct({ login: Schema.String }) }),
152
+ * })<{ readonly login: string }>();
153
+ * ```
154
+ *
155
+ * @public
156
+ */
157
+ var GraphQLDocument = class GraphQLDocument {
158
+ name;
159
+ document;
160
+ decode;
161
+ encodeVariables;
162
+ constructor(name, document, decode, encodeVariables) {
163
+ this.name = name;
164
+ this.document = document;
165
+ this.decode = decode;
166
+ this.encodeVariables = encodeVariables;
167
+ }
168
+ /**
169
+ * Build a document from a response schema.
170
+ *
171
+ * @remarks
172
+ * Curried, because `A` is inferred from `response` while `V` is stated:
173
+ * TypeScript takes explicit type arguments all-or-nothing, so a single call
174
+ * would force the caller to spell out the decoded type as well.
175
+ */
176
+ static make(options) {
177
+ const decode = Schema.decodeUnknownEffect(options.response);
178
+ return (encodeVariables) => new GraphQLDocument(options.name, options.document, (raw) => decode(raw), encodeVariables ?? ((variables) => variables));
179
+ }
180
+ };
181
+
182
+ //#endregion
183
+ export { GitHubGraphQLError, GraphQLDocument, GraphQLErrorEntry };
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 C. Spencer Beggs
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/PullRequest.js ADDED
@@ -0,0 +1,300 @@
1
+ import { GitHubError } from "./GitHubError.js";
2
+ import { GraphQLDocument } from "./GraphQL.js";
3
+ import { GitHubClient } from "./GitHubClient.js";
4
+ import { Repo } from "./Repo.js";
5
+ import { PageOptions } from "./Rest.js";
6
+ import { Context, DateTime, Effect, Layer, Option, Schema } from "effect";
7
+
8
+ //#region src/PullRequest.ts
9
+ /** How a pull request is merged. @public */
10
+ const MergeMethod = Schema.Literals([
11
+ "merge",
12
+ "squash",
13
+ "rebase"
14
+ ]);
15
+ /**
16
+ * A pull request, projected to what callers read.
17
+ *
18
+ * @public
19
+ */
20
+ var PullRequestInfo = class extends Schema.Class("PullRequestInfo")({
21
+ /** The number in `#123`. */
22
+ number: Schema.Int,
23
+ /** The GraphQL node id, which the auto-merge mutations need. */
24
+ nodeId: Schema.String,
25
+ /** The web URL. */
26
+ url: Schema.String,
27
+ title: Schema.String,
28
+ state: Schema.Literals(["open", "closed"]),
29
+ /** The source branch name. */
30
+ head: Schema.String,
31
+ /** The target branch name. */
32
+ base: Schema.String,
33
+ draft: Schema.Boolean,
34
+ merged: Schema.Boolean,
35
+ /**
36
+ * When it merged, if it did.
37
+ *
38
+ * @remarks
39
+ * An `Option`, not an optional field. Whether a pull request has merged is a
40
+ * fact GitHub always reports, so modelling it as "maybe absent" would be
41
+ * modelling a gap in our fixtures rather than a gap in the domain.
42
+ */
43
+ mergedAt: Schema.Option(Schema.DateTimeUtcFromString),
44
+ /** The description, when GitHub sent one. */
45
+ body: Schema.optionalKey(Schema.String),
46
+ /** The merge commit, once there is one. */
47
+ mergeCommitSha: Schema.optionalKey(Schema.String)
48
+ }) {};
49
+ const AutoMergeResponse = Schema.Struct({});
50
+ const EnableAutoMerge = GraphQLDocument.make({
51
+ name: "enablePullRequestAutoMerge",
52
+ document: `mutation ($pullRequestId: ID!, $mergeMethod: PullRequestMergeMethod!) {
53
+ enablePullRequestAutoMerge(input: { pullRequestId: $pullRequestId, mergeMethod: $mergeMethod }) {
54
+ clientMutationId
55
+ }
56
+ }`,
57
+ response: AutoMergeResponse
58
+ })();
59
+ const DisableAutoMerge = GraphQLDocument.make({
60
+ name: "disablePullRequestAutoMerge",
61
+ document: `mutation ($pullRequestId: ID!) {
62
+ disablePullRequestAutoMerge(input: { pullRequestId: $pullRequestId }) { clientMutationId }
63
+ }`,
64
+ response: AutoMergeResponse
65
+ })();
66
+ /** GraphQL spells the merge methods in capitals. */
67
+ const GRAPHQL_MERGE_METHOD = {
68
+ merge: "MERGE",
69
+ squash: "SQUASH",
70
+ rebase: "REBASE"
71
+ };
72
+ /**
73
+ * Pull requests.
74
+ *
75
+ * @public
76
+ */
77
+ var PullRequest = class PullRequest extends Context.Service()("@effected/github/PullRequest") {
78
+ static layer = Layer.effect(this, Effect.map(GitHubClient, (client) => make(client)));
79
+ /** An in-memory double; unstubbed members die naming themselves. */
80
+ static makeTest = (overrides = {}) => ({
81
+ get: overrides.get ?? (() => unstubbed("get")),
82
+ list: overrides.list ?? (() => unstubbed("list")),
83
+ listFiles: overrides.listFiles ?? (() => unstubbed("listFiles")),
84
+ listAssociatedWithCommit: overrides.listAssociatedWithCommit ?? (() => unstubbed("listAssociatedWithCommit")),
85
+ create: overrides.create ?? (() => unstubbed("create")),
86
+ update: overrides.update ?? (() => unstubbed("update")),
87
+ upsert: overrides.upsert ?? (() => unstubbed("upsert")),
88
+ merge: overrides.merge ?? (() => unstubbed("merge")),
89
+ addLabels: overrides.addLabels ?? (() => unstubbed("addLabels")),
90
+ requestReviewers: overrides.requestReviewers ?? (() => unstubbed("requestReviewers")),
91
+ setAutoMerge: overrides.setAutoMerge ?? (() => unstubbed("setAutoMerge"))
92
+ });
93
+ /** {@link PullRequest.makeTest} behind a `Layer`. */
94
+ static layerTest = (overrides = {}) => Layer.succeed(PullRequest, PullRequest.makeTest(overrides));
95
+ };
96
+ const unstubbed = (member) => {
97
+ throw new Error(`PullRequest.makeTest: ${member}() was called but not stubbed — pass an override.`);
98
+ };
99
+ const project = (raw) => Effect.try({
100
+ try: () => PullRequestInfo.make({
101
+ number: raw.number,
102
+ nodeId: raw.node_id,
103
+ url: raw.html_url,
104
+ title: raw.title,
105
+ state: raw.state === "closed" ? "closed" : "open",
106
+ head: raw.head.ref,
107
+ base: raw.base.ref,
108
+ draft: raw.draft ?? false,
109
+ merged: raw.merged ?? raw.merged_at != null,
110
+ mergedAt: raw.merged_at == null ? Option.none() : Option.some(DateTime.makeUnsafe(raw.merged_at)),
111
+ ...raw.body != null ? { body: raw.body } : {},
112
+ ...raw.merge_commit_sha != null ? { mergeCommitSha: raw.merge_commit_sha } : {}
113
+ }),
114
+ catch: (error) => GitHubError.decode("PullRequest", "GitHub returned an unexpected pull request", error)
115
+ });
116
+ /** `owner:branch` is what GitHub's `head` filter wants for a cross-fork search. */
117
+ const qualifyHead = (owner, head) => head.includes(":") ? head : `${owner}:${head}`;
118
+ const make = (client) => {
119
+ const list = Effect.fn("PullRequest.list")(function* (options) {
120
+ const { owner, repo } = yield* Repo;
121
+ const raw = yield* client.paginate("GET /repos/{owner}/{repo}/pulls", {
122
+ owner,
123
+ repo,
124
+ ...options?.state !== void 0 ? { state: options.state } : {},
125
+ ...options?.head !== void 0 ? { head: qualifyHead(owner, options.head) } : {},
126
+ ...options?.base !== void 0 ? { base: options.base } : {}
127
+ }, options?.page);
128
+ return yield* Effect.forEach(raw, project);
129
+ });
130
+ const create = Effect.fn("PullRequest.create")(function* (input) {
131
+ const { owner, repo } = yield* Repo;
132
+ yield* Effect.annotateCurrentSpan({
133
+ owner,
134
+ repo,
135
+ head: input.head,
136
+ base: input.base
137
+ });
138
+ const created = yield* client.request("POST /repos/{owner}/{repo}/pulls", {
139
+ owner,
140
+ repo,
141
+ title: input.title,
142
+ head: input.head,
143
+ base: input.base,
144
+ ...input.body !== void 0 ? { body: input.body } : {},
145
+ ...input.draft !== void 0 ? { draft: input.draft } : {}
146
+ });
147
+ return yield* project(created);
148
+ });
149
+ const update = Effect.fn("PullRequest.update")(function* (number, patch) {
150
+ const { owner, repo } = yield* Repo;
151
+ yield* Effect.annotateCurrentSpan({
152
+ owner,
153
+ repo,
154
+ number
155
+ });
156
+ const updated = yield* client.request("PATCH /repos/{owner}/{repo}/pulls/{pull_number}", {
157
+ owner,
158
+ repo,
159
+ pull_number: number,
160
+ ...patch.title !== void 0 ? { title: patch.title } : {},
161
+ ...patch.body !== void 0 ? { body: patch.body } : {},
162
+ ...patch.state !== void 0 ? { state: patch.state } : {},
163
+ ...patch.base !== void 0 ? { base: patch.base } : {}
164
+ });
165
+ return yield* project(updated);
166
+ });
167
+ return {
168
+ list,
169
+ create,
170
+ update,
171
+ get: Effect.fn("PullRequest.get")(function* (number) {
172
+ const { owner, repo } = yield* Repo;
173
+ yield* Effect.annotateCurrentSpan({
174
+ owner,
175
+ repo,
176
+ number
177
+ });
178
+ const raw = yield* client.request("GET /repos/{owner}/{repo}/pulls/{pull_number}", {
179
+ owner,
180
+ repo,
181
+ pull_number: number
182
+ });
183
+ return yield* project(raw);
184
+ }),
185
+ listFiles: Effect.fn("PullRequest.listFiles")(function* (number, options) {
186
+ const { owner, repo } = yield* Repo;
187
+ yield* Effect.annotateCurrentSpan({
188
+ owner,
189
+ repo,
190
+ number
191
+ });
192
+ return (yield* client.paginate("GET /repos/{owner}/{repo}/pulls/{pull_number}/files", {
193
+ owner,
194
+ repo,
195
+ pull_number: number
196
+ }, options?.page)).map((file) => file.filename);
197
+ }),
198
+ listAssociatedWithCommit: Effect.fn("PullRequest.listAssociatedWithCommit")(function* (sha, options) {
199
+ const { owner, repo } = yield* Repo;
200
+ yield* Effect.annotateCurrentSpan({
201
+ owner,
202
+ repo,
203
+ sha
204
+ });
205
+ const raw = yield* client.paginate("GET /repos/{owner}/{repo}/commits/{commit_sha}/pulls", {
206
+ owner,
207
+ repo,
208
+ commit_sha: sha
209
+ }, options?.page);
210
+ return yield* Effect.forEach(raw, project);
211
+ }),
212
+ upsert: Effect.fn("PullRequest.upsert")(function* (input) {
213
+ const found = (yield* list({
214
+ head: input.head,
215
+ base: input.base,
216
+ state: "open",
217
+ page: PageOptions.make({
218
+ perPage: 1,
219
+ maxPages: 1
220
+ })
221
+ }))[0];
222
+ if (found === void 0) return {
223
+ pullRequest: yield* create(input),
224
+ created: true
225
+ };
226
+ const patch = {
227
+ title: input.title,
228
+ ...input.body !== void 0 ? { body: input.body } : {}
229
+ };
230
+ return {
231
+ pullRequest: yield* update(found.number, patch),
232
+ created: false
233
+ };
234
+ }),
235
+ merge: Effect.fn("PullRequest.merge")(function* (number, options) {
236
+ const { owner, repo } = yield* Repo;
237
+ yield* Effect.annotateCurrentSpan({
238
+ owner,
239
+ repo,
240
+ number,
241
+ method: options?.method ?? "merge"
242
+ });
243
+ return (yield* client.request("PUT /repos/{owner}/{repo}/pulls/{pull_number}/merge", {
244
+ owner,
245
+ repo,
246
+ pull_number: number,
247
+ ...options?.method !== void 0 ? { merge_method: options.method } : {},
248
+ ...options?.commitTitle !== void 0 ? { commit_title: options.commitTitle } : {},
249
+ ...options?.commitMessage !== void 0 ? { commit_message: options.commitMessage } : {}
250
+ })).sha;
251
+ }),
252
+ addLabels: Effect.fn("PullRequest.addLabels")(function* (number, labels) {
253
+ const { owner, repo } = yield* Repo;
254
+ yield* Effect.annotateCurrentSpan({
255
+ owner,
256
+ repo,
257
+ number,
258
+ labels: labels.length
259
+ });
260
+ yield* client.request("POST /repos/{owner}/{repo}/issues/{issue_number}/labels", {
261
+ owner,
262
+ repo,
263
+ issue_number: number,
264
+ labels: [...labels]
265
+ });
266
+ }),
267
+ requestReviewers: Effect.fn("PullRequest.requestReviewers")(function* (number, reviewers) {
268
+ const { owner, repo } = yield* Repo;
269
+ yield* Effect.annotateCurrentSpan({
270
+ owner,
271
+ repo,
272
+ number
273
+ });
274
+ yield* client.request("POST /repos/{owner}/{repo}/pulls/{pull_number}/requested_reviewers", {
275
+ owner,
276
+ repo,
277
+ pull_number: number,
278
+ ...reviewers.users !== void 0 ? { reviewers: [...reviewers.users] } : {},
279
+ ...reviewers.teams !== void 0 ? { team_reviewers: [...reviewers.teams] } : {}
280
+ });
281
+ }),
282
+ setAutoMerge: Effect.fn("PullRequest.setAutoMerge")(function* (pullRequest, method) {
283
+ yield* Effect.annotateCurrentSpan({
284
+ number: pullRequest.number,
285
+ method
286
+ });
287
+ if (method === "off") {
288
+ yield* client.graphql(DisableAutoMerge, { pullRequestId: pullRequest.nodeId });
289
+ return;
290
+ }
291
+ yield* client.graphql(EnableAutoMerge, {
292
+ pullRequestId: pullRequest.nodeId,
293
+ mergeMethod: GRAPHQL_MERGE_METHOD[method]
294
+ });
295
+ })
296
+ };
297
+ };
298
+
299
+ //#endregion
300
+ export { MergeMethod, PullRequest, PullRequestInfo };
@@ -0,0 +1,132 @@
1
+ import { GitHubClient } from "./GitHubClient.js";
2
+ import { Repo } from "./Repo.js";
3
+ import { Context, Effect, Layer, Option, Schema } from "effect";
4
+
5
+ //#region src/PullRequestComment.ts
6
+ /**
7
+ * The hidden marker that makes a comment findable again.
8
+ *
9
+ * @remarks
10
+ * A pure class, not a hardcoded string. The surface this replaces baked
11
+ * `<!-- savvy-web:${key} -->` into the library — one vendor's name, inside a
12
+ * package meant to be general. Here the namespace is the caller's, the marker is
13
+ * testable without a client, and the library has no opinion about whose comments
14
+ * these are.
15
+ *
16
+ * @public
17
+ */
18
+ var CommentMarker = class extends Schema.Class("CommentMarker")({
19
+ /** Whose comments these are, e.g. your action's name. */
20
+ namespace: Schema.NonEmptyString,
21
+ /** Which comment, within that namespace. */
22
+ key: Schema.NonEmptyString
23
+ }) {
24
+ /** The HTML comment appended to a body so the comment can be found again. */
25
+ get html() {
26
+ return `<!-- ${this.namespace}:${this.key} -->`;
27
+ }
28
+ /** Does this body carry the marker? */
29
+ matches(body) {
30
+ return body.includes(this.html);
31
+ }
32
+ };
33
+ /**
34
+ * A comment this package wrote or found.
35
+ *
36
+ * @public
37
+ */
38
+ var CommentRecord = class extends Schema.Class("CommentRecord")({
39
+ id: Schema.Int,
40
+ body: Schema.String,
41
+ url: Schema.String
42
+ }) {};
43
+ /**
44
+ * Sticky comments.
45
+ *
46
+ * @public
47
+ */
48
+ var PullRequestComment = class PullRequestComment extends Context.Service()("@effected/github/PullRequestComment") {
49
+ static layer = Layer.effect(this, Effect.map(GitHubClient, (client) => make(client)));
50
+ /** An in-memory double; unstubbed members die naming themselves. */
51
+ static makeTest = (overrides = {}) => ({
52
+ create: overrides.create ?? (() => unstubbed("create")),
53
+ upsert: overrides.upsert ?? (() => unstubbed("upsert")),
54
+ find: overrides.find ?? (() => unstubbed("find")),
55
+ delete: overrides.delete ?? (() => unstubbed("delete"))
56
+ });
57
+ /** {@link PullRequestComment.makeTest} behind a `Layer`. */
58
+ static layerTest = (overrides = {}) => Layer.succeed(PullRequestComment, PullRequestComment.makeTest(overrides));
59
+ };
60
+ const unstubbed = (member) => {
61
+ throw new Error(`PullRequestComment.makeTest: ${member}() was called but not stubbed — pass an override.`);
62
+ };
63
+ const recordOf = (raw) => CommentRecord.make({
64
+ id: raw.id,
65
+ body: raw.body ?? "",
66
+ url: raw.html_url
67
+ });
68
+ const make = (client) => {
69
+ const create = Effect.fn("PullRequestComment.create")(function* (issueNumber, body) {
70
+ const { owner, repo } = yield* Repo;
71
+ yield* Effect.annotateCurrentSpan({
72
+ owner,
73
+ repo,
74
+ issueNumber
75
+ });
76
+ const created = yield* client.request("POST /repos/{owner}/{repo}/issues/{issue_number}/comments", {
77
+ owner,
78
+ repo,
79
+ issue_number: issueNumber,
80
+ body
81
+ });
82
+ return recordOf(created);
83
+ });
84
+ const find = Effect.fn("PullRequestComment.find")(function* (issueNumber, marker, options) {
85
+ const { owner, repo } = yield* Repo;
86
+ yield* Effect.annotateCurrentSpan({
87
+ owner,
88
+ repo,
89
+ issueNumber,
90
+ marker: marker.key
91
+ });
92
+ const found = (yield* client.paginate("GET /repos/{owner}/{repo}/issues/{issue_number}/comments", {
93
+ owner,
94
+ repo,
95
+ issue_number: issueNumber
96
+ }, options?.page)).find((comment) => marker.matches(comment.body ?? ""));
97
+ return found === void 0 ? Option.none() : Option.some(recordOf(found));
98
+ });
99
+ return {
100
+ create,
101
+ find,
102
+ upsert: Effect.fn("PullRequestComment.upsert")(function* (issueNumber, marker, body) {
103
+ const { owner, repo } = yield* Repo;
104
+ const marked = `${body}\n\n${marker.html}`;
105
+ const existing = yield* find(issueNumber, marker);
106
+ if (Option.isNone(existing)) return yield* create(issueNumber, marked);
107
+ const updated = yield* client.request("PATCH /repos/{owner}/{repo}/issues/comments/{comment_id}", {
108
+ owner,
109
+ repo,
110
+ comment_id: existing.value.id,
111
+ body: marked
112
+ });
113
+ return recordOf(updated);
114
+ }),
115
+ delete: Effect.fn("PullRequestComment.delete")(function* (commentId) {
116
+ const { owner, repo } = yield* Repo;
117
+ yield* Effect.annotateCurrentSpan({
118
+ owner,
119
+ repo,
120
+ commentId
121
+ });
122
+ yield* client.request("DELETE /repos/{owner}/{repo}/issues/comments/{comment_id}", {
123
+ owner,
124
+ repo,
125
+ comment_id: commentId
126
+ });
127
+ })
128
+ };
129
+ };
130
+
131
+ //#endregion
132
+ export { CommentMarker, CommentRecord, PullRequestComment };