@shivaedev/github 0.8.6 → 0.10.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/CHANGELOG.md +47 -0
- package/README.md +142 -8
- package/dist/GitHub.d.ts +360 -18
- package/dist/GitHub.d.ts.map +1 -1
- package/dist/GitHub.js +19 -6
- package/dist/GitHub.js.map +1 -1
- package/dist/GitHubToken.d.ts +2 -2
- package/dist/GitHubToken.d.ts.map +1 -1
- package/dist/GitHubToken.js +1 -1
- package/dist/GitHubToken.js.map +1 -1
- package/dist/adapters/resolveApiUrl.d.ts +2 -0
- package/dist/adapters/resolveApiUrl.d.ts.map +1 -0
- package/dist/adapters/resolveApiUrl.js +6 -0
- package/dist/adapters/resolveApiUrl.js.map +1 -0
- package/dist/adapters/zipEntries.d.ts +17 -0
- package/dist/adapters/zipEntries.d.ts.map +1 -0
- package/dist/adapters/zipEntries.js +57 -0
- package/dist/adapters/zipEntries.js.map +1 -0
- package/dist/appTokens.d.ts +6 -0
- package/dist/appTokens.d.ts.map +1 -0
- package/dist/appTokens.js +65 -0
- package/dist/appTokens.js.map +1 -0
- package/dist/artifactErrors.d.ts +34 -0
- package/dist/artifactErrors.d.ts.map +1 -0
- package/dist/artifactErrors.js +27 -0
- package/dist/artifactErrors.js.map +1 -0
- package/dist/artifactExtraction.d.ts +12 -0
- package/dist/artifactExtraction.d.ts.map +1 -0
- package/dist/artifactExtraction.js +40 -0
- package/dist/artifactExtraction.js.map +1 -0
- package/dist/artifactPaths.d.ts +4 -0
- package/dist/artifactPaths.d.ts.map +1 -0
- package/dist/artifactPaths.js +35 -0
- package/dist/artifactPaths.js.map +1 -0
- package/dist/artifacts.d.ts +13 -8
- package/dist/artifacts.d.ts.map +1 -1
- package/dist/artifacts.js +15 -8
- package/dist/artifacts.js.map +1 -1
- package/dist/authenticatedRequest.d.ts +1 -1
- package/dist/authenticatedRequest.d.ts.map +1 -1
- package/dist/authenticatedRequest.js +3 -2
- package/dist/authenticatedRequest.js.map +1 -1
- package/dist/cache.d.ts +2 -0
- package/dist/cache.d.ts.map +1 -1
- package/dist/cache.js +3 -0
- package/dist/cache.js.map +1 -1
- package/dist/checkRuns.d.ts +116 -11
- package/dist/checkRuns.d.ts.map +1 -1
- package/dist/checkRuns.js +41 -18
- package/dist/checkRuns.js.map +1 -1
- package/dist/gitSchemas.d.ts +195 -0
- package/dist/gitSchemas.d.ts.map +1 -0
- package/dist/gitSchemas.js +45 -0
- package/dist/gitSchemas.js.map +1 -0
- package/dist/githubApp.d.ts +2 -4
- package/dist/githubApp.d.ts.map +1 -1
- package/dist/githubApp.js +5 -55
- package/dist/githubApp.js.map +1 -1
- package/dist/installationClients.d.ts +461 -0
- package/dist/installationClients.d.ts.map +1 -0
- package/dist/installationClients.js +37 -0
- package/dist/installationClients.js.map +1 -0
- package/dist/json.d.ts.map +1 -1
- package/dist/json.js +4 -0
- package/dist/json.js.map +1 -1
- package/dist/pagedLists.d.ts +15 -0
- package/dist/pagedLists.d.ts.map +1 -0
- package/dist/pagedLists.js +16 -0
- package/dist/pagedLists.js.map +1 -0
- package/dist/repositoryReads.d.ts +261 -0
- package/dist/repositoryReads.d.ts.map +1 -0
- package/dist/repositoryReads.js +30 -0
- package/dist/repositoryReads.js.map +1 -0
- package/dist/repositorySchemas.d.ts +73 -0
- package/dist/repositorySchemas.d.ts.map +1 -0
- package/dist/repositorySchemas.js +38 -0
- package/dist/repositorySchemas.js.map +1 -0
- package/dist/representations.d.ts +4 -4
- package/dist/representations.d.ts.map +1 -1
- package/dist/representations.js +3 -2
- package/dist/representations.js.map +1 -1
- package/dist/requests.d.ts +2 -2
- package/dist/requests.d.ts.map +1 -1
- package/dist/requests.js +11 -9
- package/dist/requests.js.map +1 -1
- package/dist/status.d.ts +1 -1
- package/dist/status.d.ts.map +1 -1
- package/dist/stickyComments.d.ts +1 -1
- package/dist/viewerRepositoryAccess.d.ts +46 -0
- package/dist/viewerRepositoryAccess.d.ts.map +1 -0
- package/dist/viewerRepositoryAccess.js +54 -0
- package/dist/viewerRepositoryAccess.js.map +1 -0
- package/package.json +14 -7
- package/src/GitHub.ts +32 -11
- package/src/GitHubToken.ts +4 -2
- package/src/adapters/resolveApiUrl.ts +5 -0
- package/src/adapters/zipEntries.ts +69 -0
- package/src/appTokens.ts +93 -0
- package/src/artifactErrors.ts +26 -0
- package/src/artifactExtraction.ts +62 -0
- package/src/artifactPaths.ts +36 -0
- package/src/artifacts.ts +36 -39
- package/src/authenticatedRequest.ts +3 -1
- package/src/cache.ts +5 -0
- package/src/checkRuns.ts +58 -18
- package/src/gitSchemas.ts +52 -0
- package/src/githubApp.ts +6 -80
- package/src/installationClients.ts +50 -0
- package/src/json.ts +4 -0
- package/src/pagedLists.ts +40 -0
- package/src/repositoryReads.ts +61 -0
- package/src/repositorySchemas.ts +43 -0
- package/src/representations.ts +5 -4
- package/src/requests.ts +11 -9
- package/src/viewerRepositoryAccess.ts +66 -0
package/src/artifacts.ts
CHANGED
|
@@ -4,13 +4,15 @@ import * as HttpStatus from "effect/http/HttpStatus";
|
|
|
4
4
|
import { readBoundedBody } from "@shivaedev/http/body.ts";
|
|
5
5
|
import type { RemoteStatusError } from "@shivaedev/http/errors.ts";
|
|
6
6
|
import { filterStatus } from "@shivaedev/http/filterStatus.ts";
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import
|
|
10
|
-
import
|
|
11
|
-
import
|
|
7
|
+
import { resolveApiUrl } from "#adapters/resolveApiUrl.ts";
|
|
8
|
+
import { type ArtifactExtractionOptions, extractArtifactArchive } from "#artifactExtraction.ts";
|
|
9
|
+
import { authenticatedRequest } from "#authenticatedRequest.ts";
|
|
10
|
+
import { GitHubArtifactError, GitHubArtifactTooLarge, GitHubTransportError } from "#errors.ts";
|
|
11
|
+
import { GitHubToken } from "#GitHubToken.ts";
|
|
12
|
+
import type { RateLimits } from "#rateLimits.ts";
|
|
13
|
+
import { TokenRefresh } from "#TokenRefresh.ts";
|
|
12
14
|
|
|
13
|
-
export interface ArtifactArchiveOptions {
|
|
15
|
+
export interface ArtifactArchiveOptions extends ArtifactExtractionOptions {
|
|
14
16
|
readonly artifactId: number;
|
|
15
17
|
readonly byteLimit: number;
|
|
16
18
|
readonly owner: string;
|
|
@@ -23,41 +25,36 @@ function downloadFailure(path: string, cause: HttpClientError.HttpClientError |
|
|
|
23
25
|
: new GitHubArtifactError({ stage: "download", status: cause.status });
|
|
24
26
|
}
|
|
25
27
|
|
|
26
|
-
export
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
rates: RateLimits,
|
|
31
|
-
refresh: typeof TokenRefresh.Service = undefined,
|
|
32
|
-
) {
|
|
28
|
+
export const artifacts = Effect.fn("GitHub.artifacts")(function* (apiUrl: string, rates: RateLimits) {
|
|
29
|
+
const client = yield* HttpClient.HttpClient;
|
|
30
|
+
const token = yield* GitHubToken;
|
|
31
|
+
const refresh = yield* TokenRefresh;
|
|
33
32
|
const send = authenticatedRequest(HttpClient.withScope(client), token, rates, refresh);
|
|
34
33
|
const download = filterStatus(HttpClient.withScope(client));
|
|
35
34
|
return {
|
|
36
|
-
downloadArtifactArchive: (options: ArtifactArchiveOptions)
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
RemoteGone: (error) => Effect.fail(new GitHubArtifactError({ stage: "redirect", status: error.status }, { cause: error })),
|
|
43
|
-
RemoteNotFound: (error) => Effect.fail(new GitHubArtifactError({ stage: "redirect", status: error.status }, { cause: error })),
|
|
44
|
-
}),
|
|
45
|
-
);
|
|
46
|
-
const location = redirect.headers.location;
|
|
47
|
-
if (redirect.status !== HttpStatus.fromLiteral("Found") || location === undefined) {
|
|
48
|
-
return yield* new GitHubArtifactError({ stage: "redirect", status: redirect.status });
|
|
49
|
-
}
|
|
50
|
-
const response = yield* download.get(location).pipe(
|
|
51
|
-
Effect.provideService(FetchHttpClient.RequestInit, { credentials: "omit", redirect: "manual" }),
|
|
52
|
-
Effect.mapError((cause) => downloadFailure(path, cause)),
|
|
53
|
-
);
|
|
54
|
-
return yield* readBoundedBody(response, options.byteLimit).pipe(
|
|
55
|
-
Effect.catchTags({
|
|
56
|
-
HttpClientError: (cause) => Effect.fail(new GitHubTransportError({ path }, { cause })),
|
|
57
|
-
ResponseBodyTooLarge: (cause) => Effect.fail(new GitHubArtifactTooLarge({ limit: cause.limit }, { cause })),
|
|
58
|
-
}),
|
|
59
|
-
);
|
|
35
|
+
downloadArtifactArchive: Effect.fn("GitHub.downloadArtifactArchive")(function* (options: ArtifactArchiveOptions) {
|
|
36
|
+
const path = `/repos/${encodeURIComponent(options.owner)}/${encodeURIComponent(options.repo)}/actions/artifacts/${options.artifactId}/zip`;
|
|
37
|
+
const redirect = yield* send(HttpClientRequest.get(resolveApiUrl(apiUrl, path), { accept: "application/vnd.github+json" }), "manual").pipe(
|
|
38
|
+
Effect.catchTags({
|
|
39
|
+
RemoteGone: (error) => Effect.fail(new GitHubArtifactError({ stage: "redirect", status: error.status }, { cause: error })),
|
|
40
|
+
RemoteNotFound: (error) => Effect.fail(new GitHubArtifactError({ stage: "redirect", status: error.status }, { cause: error })),
|
|
60
41
|
}),
|
|
61
|
-
)
|
|
42
|
+
);
|
|
43
|
+
const location = redirect.headers.location;
|
|
44
|
+
if (redirect.status !== HttpStatus.fromLiteral("Found") || location === undefined) {
|
|
45
|
+
return yield* new GitHubArtifactError({ stage: "redirect", status: redirect.status });
|
|
46
|
+
}
|
|
47
|
+
const response = yield* download.get(location).pipe(
|
|
48
|
+
Effect.provideService(FetchHttpClient.RequestInit, { credentials: "omit", redirect: "manual" }),
|
|
49
|
+
Effect.mapError((cause) => downloadFailure(path, cause)),
|
|
50
|
+
);
|
|
51
|
+
return yield* readBoundedBody(response, options.byteLimit).pipe(
|
|
52
|
+
Effect.catchTags({
|
|
53
|
+
HttpClientError: (cause) => Effect.fail(new GitHubTransportError({ path }, { cause })),
|
|
54
|
+
ResponseBodyTooLarge: (cause) => Effect.fail(new GitHubArtifactTooLarge({ limit: cause.limit }, { cause })),
|
|
55
|
+
}),
|
|
56
|
+
Effect.flatMap((bytes) => extractArtifactArchive(bytes, options)),
|
|
57
|
+
);
|
|
58
|
+
}, Effect.scoped),
|
|
62
59
|
};
|
|
63
|
-
}
|
|
60
|
+
});
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { DateTime, Effect, Option, Redacted } from "effect";
|
|
1
|
+
import { DateTime, Effect, Option, Redacted, Stream } from "effect";
|
|
2
2
|
import { FetchHttpClient, type HttpClient, type HttpClientError, HttpClientRequest } from "effect/http";
|
|
3
3
|
import * as HttpStatus from "effect/http/HttpStatus";
|
|
4
4
|
import { GitHubTransportError } from "./errors.ts";
|
|
@@ -30,12 +30,14 @@ export function authenticatedRequest<R>(
|
|
|
30
30
|
const credential = yield* accessToken;
|
|
31
31
|
let response = yield* send(credential);
|
|
32
32
|
if (isUnauthorized(response) && refresh !== undefined) {
|
|
33
|
+
yield* Effect.scoped(Stream.toPull(response.stream));
|
|
33
34
|
yield* refresh(credential);
|
|
34
35
|
response = yield* send(yield* accessToken);
|
|
35
36
|
}
|
|
36
37
|
const observedResource = rates.observe(response);
|
|
37
38
|
if (!((mode === "revalidate" && isUnchanged(response)) || (mode === "manual" && response.status === HttpStatus.fromLiteral("Found")))) {
|
|
38
39
|
yield* checkStatus(response, observedResource, rates.failures.get(observedResource) ?? 0).pipe(
|
|
40
|
+
Effect.onError(() => Effect.scoped(Stream.toPull(response.stream))),
|
|
39
41
|
Effect.catchTag("HttpClientError", (cause) => Effect.fail(new GitHubTransportError({ path }, { cause }))),
|
|
40
42
|
Effect.catchTag("GitHubRateLimited", (error) => {
|
|
41
43
|
rates.blocked.set(error.resource, error);
|
package/src/cache.ts
CHANGED
|
@@ -4,9 +4,14 @@ import type { HttpClientResponse } from "effect/http";
|
|
|
4
4
|
export interface CachedResponse {
|
|
5
5
|
readonly bytes: Uint8Array;
|
|
6
6
|
readonly decoded: Map<Schema.Constraint, unknown>;
|
|
7
|
+
readonly etag?: string | undefined;
|
|
7
8
|
readonly response: HttpClientResponse.HttpClientResponse;
|
|
8
9
|
}
|
|
9
10
|
|
|
11
|
+
export function revalidatedResponse(cached: CachedResponse, response: HttpClientResponse.HttpClientResponse): CachedResponse {
|
|
12
|
+
return { ...cached, etag: response.headers.etag ?? cached.etag, response };
|
|
13
|
+
}
|
|
14
|
+
|
|
10
15
|
export function responseCache() {
|
|
11
16
|
const entries = new Map<string, CachedResponse>();
|
|
12
17
|
return {
|
package/src/checkRuns.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { Effect, Schema } from "effect";
|
|
1
|
+
import { Effect, Option, Schema, Stream } from "effect";
|
|
2
|
+
import { decodedPages } from "#pagedLists.ts";
|
|
2
3
|
import type { Representations } from "#representations.ts";
|
|
3
4
|
|
|
4
5
|
export const CheckRunOutput = Schema.Struct({
|
|
@@ -8,16 +9,22 @@ export const CheckRunOutput = Schema.Struct({
|
|
|
8
9
|
});
|
|
9
10
|
|
|
10
11
|
export const CheckRunConclusion = Schema.Literals(["action_required", "cancelled", "failure", "neutral", "success", "skipped", "stale", "timed_out"]);
|
|
11
|
-
export const CheckRunStatus = Schema.Literals(["queued", "in_progress", "completed"]);
|
|
12
|
+
export const CheckRunStatus = Schema.Literals(["queued", "in_progress", "completed", "waiting", "requested", "pending"]);
|
|
12
13
|
|
|
13
|
-
|
|
14
|
-
conclusion: Schema.optional(CheckRunConclusion),
|
|
14
|
+
const createFields = {
|
|
15
15
|
detailsUrl: Schema.optional(Schema.String),
|
|
16
16
|
headSha: Schema.String,
|
|
17
17
|
name: Schema.String,
|
|
18
18
|
output: Schema.optional(CheckRunOutput),
|
|
19
|
-
|
|
20
|
-
|
|
19
|
+
};
|
|
20
|
+
export const CreateCheckRun = Schema.Union([
|
|
21
|
+
Schema.Struct({ ...createFields, conclusion: Schema.optional(CheckRunConclusion), status: Schema.Literals(["queued", "in_progress"]) }).pipe(
|
|
22
|
+
Schema.encodeKeys({ detailsUrl: "details_url", headSha: "head_sha" }),
|
|
23
|
+
),
|
|
24
|
+
Schema.Struct({ ...createFields, conclusion: CheckRunConclusion, status: Schema.Literal("completed") }).pipe(
|
|
25
|
+
Schema.encodeKeys({ detailsUrl: "details_url", headSha: "head_sha" }),
|
|
26
|
+
),
|
|
27
|
+
]);
|
|
21
28
|
|
|
22
29
|
export const UpdateCheckRun = Schema.Union([
|
|
23
30
|
Schema.Struct({
|
|
@@ -34,6 +41,7 @@ export const UpdateCheckRun = Schema.Union([
|
|
|
34
41
|
]);
|
|
35
42
|
|
|
36
43
|
export const CheckRun = Schema.Struct({
|
|
44
|
+
app: Schema.optional(Schema.NullOr(Schema.Struct({ id: Schema.Number }))),
|
|
37
45
|
conclusion: Schema.NullOr(CheckRunConclusion),
|
|
38
46
|
detailsUrl: Schema.NullOr(Schema.String),
|
|
39
47
|
headSha: Schema.String,
|
|
@@ -43,17 +51,49 @@ export const CheckRun = Schema.Struct({
|
|
|
43
51
|
status: CheckRunStatus,
|
|
44
52
|
}).pipe(Schema.encodeKeys({ detailsUrl: "details_url", headSha: "head_sha" }));
|
|
45
53
|
|
|
54
|
+
export const FindCheckRun = Schema.Union([
|
|
55
|
+
Schema.Struct({ appId: Schema.optional(Schema.Number), headSha: Schema.String, name: Schema.String }),
|
|
56
|
+
Schema.Struct({ appId: Schema.Number, headSha: Schema.String, name: Schema.optional(Schema.String) }),
|
|
57
|
+
]);
|
|
58
|
+
|
|
46
59
|
export function checkRuns(client: Representations) {
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
60
|
+
const createCheckRun = Effect.fn("GitHub.createCheckRun")(function* (owner: string, repo: string, input: typeof CreateCheckRun.Type) {
|
|
61
|
+
const body = yield* Schema.encodeEffect(CreateCheckRun)(input);
|
|
62
|
+
const response = yield* client.write("POST", `/repos/${owner}/${repo}/check-runs`, body);
|
|
63
|
+
return yield* response.decode(CheckRun);
|
|
64
|
+
});
|
|
65
|
+
const updateCheckRun = Effect.fn("GitHub.updateCheckRun")(function* (owner: string, repo: string, id: number, input: typeof UpdateCheckRun.Type) {
|
|
66
|
+
const body = yield* Schema.encodeEffect(UpdateCheckRun)(input);
|
|
67
|
+
const response = yield* client.write("PATCH", `/repos/${owner}/${repo}/check-runs/${id}`, body);
|
|
68
|
+
return yield* response.decode(CheckRun);
|
|
69
|
+
});
|
|
70
|
+
const findLatestCheckRun = Effect.fn("GitHub.findLatestCheckRun")(function* (owner: string, repo: string, input: typeof FindCheckRun.Type) {
|
|
71
|
+
const { headSha, name, appId } = yield* Schema.decodeUnknownEffect(FindCheckRun)(input);
|
|
72
|
+
const query = new URLSearchParams({ filter: "all", "per_page": "100" });
|
|
73
|
+
if (name !== undefined) {
|
|
74
|
+
query.set("check_name", name);
|
|
75
|
+
}
|
|
76
|
+
const pages = decodedPages(client, `/repos/${owner}/${repo}/commits/${encodeURIComponent(headSha)}/check-runs?${query}`, CheckRun, {
|
|
77
|
+
arrayKey: "check_runs",
|
|
78
|
+
});
|
|
79
|
+
return yield* pages.pipe(
|
|
80
|
+
Stream.flatMap((page) => Stream.fromIterable(page.items)),
|
|
81
|
+
Stream.filter((run) => (name === undefined || run.name === name) && (appId === undefined || run.app?.id === appId)),
|
|
82
|
+
Stream.runFold(
|
|
83
|
+
() => Option.none<typeof CheckRun.Type>(),
|
|
84
|
+
(latest, run) => (Option.isNone(latest) || run.id > latest.value.id ? Option.some(run) : latest),
|
|
85
|
+
),
|
|
86
|
+
);
|
|
87
|
+
});
|
|
88
|
+
const upsertCheckRun = Effect.fn("GitHub.upsertCheckRun")(function* (
|
|
89
|
+
owner: string,
|
|
90
|
+
repo: string,
|
|
91
|
+
input: typeof CreateCheckRun.Type,
|
|
92
|
+
appId?: number,
|
|
93
|
+
) {
|
|
94
|
+
yield* Schema.encodeEffect(CreateCheckRun)(input);
|
|
95
|
+
const existing = yield* findLatestCheckRun(owner, repo, { appId, headSha: input.headSha, name: input.name });
|
|
96
|
+
return yield* Option.isSome(existing) ? updateCheckRun(owner, repo, existing.value.id, input) : createCheckRun(owner, repo, input);
|
|
97
|
+
});
|
|
98
|
+
return { createCheckRun, findLatestCheckRun, updateCheckRun, upsertCheckRun };
|
|
59
99
|
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { Schema } from "effect";
|
|
2
|
+
import { PullRequestFile, User } from "#repositorySchemas.ts";
|
|
3
|
+
|
|
4
|
+
const CommitIdentity = Schema.Struct({ date: Schema.String, email: Schema.String, name: Schema.String });
|
|
5
|
+
const CommitData = Schema.Struct({
|
|
6
|
+
author: Schema.NullOr(CommitIdentity),
|
|
7
|
+
committer: Schema.NullOr(CommitIdentity),
|
|
8
|
+
message: Schema.String,
|
|
9
|
+
tree: Schema.Struct({ sha: Schema.String, url: Schema.String }),
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
const commitFields = {
|
|
13
|
+
author: Schema.NullOr(User),
|
|
14
|
+
commit: CommitData,
|
|
15
|
+
committer: Schema.NullOr(User),
|
|
16
|
+
htmlUrl: Schema.String,
|
|
17
|
+
parents: Schema.Array(Schema.Struct({ sha: Schema.String, url: Schema.String })),
|
|
18
|
+
sha: Schema.String,
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export const Commit = Schema.Struct(commitFields).pipe(Schema.encodeKeys({ htmlUrl: "html_url" }));
|
|
22
|
+
|
|
23
|
+
export const CommitDetail = Schema.Struct({
|
|
24
|
+
...commitFields,
|
|
25
|
+
files: Schema.optional(Schema.Array(PullRequestFile)),
|
|
26
|
+
stats: Schema.Struct({ additions: Schema.Number, deletions: Schema.Number, total: Schema.Number }),
|
|
27
|
+
}).pipe(Schema.encodeKeys({ htmlUrl: "html_url" }));
|
|
28
|
+
|
|
29
|
+
export const Comparison = Schema.Struct({
|
|
30
|
+
aheadBy: Schema.Number,
|
|
31
|
+
behindBy: Schema.Number,
|
|
32
|
+
commits: Schema.Array(Commit),
|
|
33
|
+
files: Schema.optional(Schema.Array(PullRequestFile)),
|
|
34
|
+
status: Schema.Literals(["diverged", "ahead", "behind", "identical"]),
|
|
35
|
+
totalCommits: Schema.Number,
|
|
36
|
+
}).pipe(Schema.encodeKeys({ aheadBy: "ahead_by", behindBy: "behind_by", totalCommits: "total_commits" }));
|
|
37
|
+
|
|
38
|
+
export const GitTree = Schema.Struct({
|
|
39
|
+
sha: Schema.String,
|
|
40
|
+
tree: Schema.Array(
|
|
41
|
+
Schema.Struct({
|
|
42
|
+
mode: Schema.String,
|
|
43
|
+
path: Schema.String,
|
|
44
|
+
sha: Schema.String,
|
|
45
|
+
size: Schema.optional(Schema.Number),
|
|
46
|
+
type: Schema.Literals(["blob", "tree", "commit"]),
|
|
47
|
+
url: Schema.optional(Schema.String),
|
|
48
|
+
}),
|
|
49
|
+
),
|
|
50
|
+
truncated: Schema.Boolean,
|
|
51
|
+
url: Schema.String,
|
|
52
|
+
});
|
package/src/githubApp.ts
CHANGED
|
@@ -1,12 +1,7 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import { type AppCredentials, appJwt } from "#appJwt.ts";
|
|
6
|
-
import { GitHubAppError } from "#errors.ts";
|
|
7
|
-
import { GitHubConfig } from "#GitHubConfig.ts";
|
|
8
|
-
import { GitHubToken } from "#GitHubToken.ts";
|
|
9
|
-
import { TokenRefresh } from "#TokenRefresh.ts";
|
|
1
|
+
import { Layer } from "effect";
|
|
2
|
+
import type { AppCredentials } from "#appJwt.ts";
|
|
3
|
+
import { appTokens } from "#appTokens.ts";
|
|
4
|
+
import { GitHubApp, GitHubAppCredentials } from "#installationClients.ts";
|
|
10
5
|
|
|
11
6
|
export interface InstallationScope {
|
|
12
7
|
readonly permissions?: Readonly<Record<string, "read" | "write" | "admin">>;
|
|
@@ -21,76 +16,7 @@ export type GitHubAppOptions = AppCredentials &
|
|
|
21
16
|
| { readonly installationId?: never; readonly owner?: never; readonly repository?: never; readonly scope?: never }
|
|
22
17
|
);
|
|
23
18
|
|
|
24
|
-
const Installation = Schema.Struct({ id: Schema.Number });
|
|
25
|
-
const AccessToken = Schema.Struct({ expiresAt: Schema.DateFromString, token: Schema.RedactedFromValue(Schema.String) }).pipe(
|
|
26
|
-
Schema.encodeKeys({ expiresAt: "expires_at" }),
|
|
27
|
-
);
|
|
28
|
-
|
|
29
19
|
export function githubApp(options: GitHubAppOptions) {
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
if (!("installationId" in options || "owner" in options)) {
|
|
33
|
-
return Context.make(GitHubToken, Effect.map(appJwt(options), Option.some)).pipe(Context.add(TokenRefresh, undefined));
|
|
34
|
-
}
|
|
35
|
-
const client = yield* HttpClient.HttpClient;
|
|
36
|
-
const { apiUrl } = yield* GitHubConfig;
|
|
37
|
-
const clock = yield* Clock.Clock;
|
|
38
|
-
let installationId = "installationId" in options ? options.installationId : undefined;
|
|
39
|
-
const request = appRequests(client, apiUrl);
|
|
40
|
-
const exchange = Effect.gen(function* () {
|
|
41
|
-
const jwt = yield* appJwt(options);
|
|
42
|
-
if (installationId === undefined && "owner" in options && options.owner !== undefined) {
|
|
43
|
-
const path = `/repos/${encodeURIComponent(options.owner)}/${encodeURIComponent(options.repository)}/installation`;
|
|
44
|
-
installationId = (yield* request("GET", path, jwt, Installation)).id;
|
|
45
|
-
}
|
|
46
|
-
return yield* request("POST", `/app/installations/${installationId}/access_tokens`, jwt, AccessToken, {
|
|
47
|
-
permissions: options.scope?.permissions,
|
|
48
|
-
repositories: options.scope?.repositories,
|
|
49
|
-
"repository_ids": options.scope?.repositoryIds,
|
|
50
|
-
});
|
|
51
|
-
});
|
|
52
|
-
const cache = yield* Cache.makeWith((_: undefined) => exchange, {
|
|
53
|
-
capacity: 1,
|
|
54
|
-
timeToLive: (exit) => (Exit.isSuccess(exit) ? Math.max(0, exit.value.expiresAt.getTime() - clock.currentTimeMillisUnsafe() - 300_000) : 0),
|
|
55
|
-
});
|
|
56
|
-
return Context.make(
|
|
57
|
-
GitHubToken,
|
|
58
|
-
Effect.map(Cache.get(cache, undefined), ({ token }) => Option.some(token)),
|
|
59
|
-
).pipe(
|
|
60
|
-
Context.add(TokenRefresh, (rejected) =>
|
|
61
|
-
Cache.invalidateWhen(cache, undefined, ({ token }) => Option.isSome(rejected) && token === rejected.value),
|
|
62
|
-
),
|
|
63
|
-
);
|
|
64
|
-
}),
|
|
65
|
-
);
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
function appRequests(client: HttpClient.HttpClient, apiUrl: string) {
|
|
69
|
-
const checked = filterStatus(client);
|
|
70
|
-
return Effect.fnUntraced(function* <TSchema extends Schema.Constraint>(
|
|
71
|
-
method: "GET" | "POST",
|
|
72
|
-
path: string,
|
|
73
|
-
jwt: Redacted.Redacted<string>,
|
|
74
|
-
schema: TSchema,
|
|
75
|
-
body?: unknown,
|
|
76
|
-
) {
|
|
77
|
-
let input = HttpClientRequest.make(method)(new URL(path, apiUrl).href, {
|
|
78
|
-
headers: { Accept: "application/vnd.github+json", Authorization: `Bearer ${Redacted.value(jwt)}`, "X-GitHub-Api-Version": "2022-11-28" },
|
|
79
|
-
});
|
|
80
|
-
if (body !== undefined) {
|
|
81
|
-
input = yield* HttpClientRequest.bodyJson(input, body).pipe(Effect.mapError((cause) => new GitHubAppError({ reason: "encode" }, { cause })));
|
|
82
|
-
}
|
|
83
|
-
const response = yield* checked
|
|
84
|
-
.execute(input)
|
|
85
|
-
.pipe(
|
|
86
|
-
Effect.mapError((cause) =>
|
|
87
|
-
cause._tag === "HttpClientError"
|
|
88
|
-
? new GitHubAppError({ reason: "transport" }, { cause })
|
|
89
|
-
: new GitHubAppError({ reason: "status", status: cause.status }),
|
|
90
|
-
),
|
|
91
|
-
);
|
|
92
|
-
return yield* readBoundedJson(response, 64 * 1024, schema).pipe(
|
|
93
|
-
Effect.mapError((cause) => new GitHubAppError({ reason: cause._tag === "HttpClientError" ? "transport" : "response" }, { cause })),
|
|
94
|
-
);
|
|
95
|
-
});
|
|
20
|
+
const authentication = appTokens(options);
|
|
21
|
+
return Layer.merge(authentication, GitHubApp.layer.pipe(Layer.provide(Layer.succeed(GitHubAppCredentials)({ credentials: options }))));
|
|
96
22
|
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { Cache, Context, Effect, Layer } from "effect";
|
|
2
|
+
import { HttpClient } from "effect/http";
|
|
3
|
+
import { defineService } from "@shivaedev/service/defineService.ts";
|
|
4
|
+
import type { AppCredentials } from "#appJwt.ts";
|
|
5
|
+
import { appTokens } from "#appTokens.ts";
|
|
6
|
+
import { GitHub } from "#GitHub.ts";
|
|
7
|
+
import { GitHubConfig } from "#GitHubConfig.ts";
|
|
8
|
+
import { viewerRepositoryAccess } from "#viewerRepositoryAccess.ts";
|
|
9
|
+
|
|
10
|
+
export const GitHubAppCredentials = Context.Service<{ readonly credentials: AppCredentials }>("@shivaedev/github/AppCredentials");
|
|
11
|
+
|
|
12
|
+
export const GitHubApp = defineService({
|
|
13
|
+
id: "@shivaedev/github/GitHubApp",
|
|
14
|
+
initialize: Effect.gen(function* () {
|
|
15
|
+
const { credentials: supplied } = yield* GitHubAppCredentials;
|
|
16
|
+
const credentials = { appId: supplied.appId, privateKey: supplied.privateKey };
|
|
17
|
+
const http = yield* HttpClient.HttpClient;
|
|
18
|
+
const config = yield* GitHubConfig;
|
|
19
|
+
const environment = Layer.mergeAll(Layer.succeed(HttpClient.HttpClient)(http), Layer.succeed(GitHubConfig)(config));
|
|
20
|
+
const app = yield* Layer.build(GitHub.layer.pipe(Layer.provide(appTokens(credentials)), Layer.provide(environment))).pipe(
|
|
21
|
+
Effect.scoped,
|
|
22
|
+
Effect.map((context) => Context.get(context, GitHub)),
|
|
23
|
+
);
|
|
24
|
+
const clients = yield* Cache.make({
|
|
25
|
+
capacity: 100,
|
|
26
|
+
lookup: (installationId: number) =>
|
|
27
|
+
Layer.build(GitHub.layer.pipe(Layer.provide(appTokens({ ...credentials, installationId })), Layer.provide(environment))).pipe(
|
|
28
|
+
Effect.scoped,
|
|
29
|
+
Effect.map((context) => Context.get(context, GitHub)),
|
|
30
|
+
),
|
|
31
|
+
timeToLive: "30 minutes",
|
|
32
|
+
});
|
|
33
|
+
function installation(id: number) {
|
|
34
|
+
return Cache.get(clients, id).pipe(Effect.tap((client) => Cache.set(clients, id, client)));
|
|
35
|
+
}
|
|
36
|
+
return { installation, viewers: yield* viewerRepositoryAccess(app, installation) };
|
|
37
|
+
}),
|
|
38
|
+
methods: ({ installation, viewers }) => ({
|
|
39
|
+
installation,
|
|
40
|
+
viewerRepositories: (account: number | string) =>
|
|
41
|
+
Cache.get(viewers, typeof account === "number" ? `id:${account}` : `login:${account.toLowerCase()}`),
|
|
42
|
+
}),
|
|
43
|
+
requires: [GitHubAppCredentials, HttpClient.HttpClient, GitHubConfig],
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
export function withInstallation<A, E, R>(installationId: number, effect: Effect.Effect<A, E, R>) {
|
|
47
|
+
return Effect.flatMap(GitHubApp, (app) =>
|
|
48
|
+
Effect.flatMap(app.installation(installationId), (client) => Effect.provideService(effect, GitHub, client)),
|
|
49
|
+
);
|
|
50
|
+
}
|
package/src/json.ts
CHANGED
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
import { Effect, Schema } from "effect";
|
|
2
|
+
import * as HttpStatus from "effect/http/HttpStatus";
|
|
2
3
|
import type { CachedResponse } from "./cache.ts";
|
|
3
4
|
|
|
4
5
|
export const decodeJson = Effect.fnUntraced(function* <TSchema extends Schema.Constraint>(entry: CachedResponse, schema: TSchema) {
|
|
6
|
+
if (entry.response.status === HttpStatus.fromLiteral("NoContent")) {
|
|
7
|
+
return yield* Schema.decodeUnknownEffect(schema)(undefined);
|
|
8
|
+
}
|
|
5
9
|
const cached = entry.decoded.get(schema);
|
|
6
10
|
if (entry.decoded.has(schema) && Schema.is(schema)(cached)) {
|
|
7
11
|
return cached;
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { Effect, Option, Schema, Stream } from "effect";
|
|
2
|
+
import type { ServiceUnavailable } from "@shivaedev/errors/taxonomy.ts";
|
|
3
|
+
import type { Representations } from "#representations.ts";
|
|
4
|
+
|
|
5
|
+
export interface ListOptions {
|
|
6
|
+
readonly arrayKey?: string | undefined;
|
|
7
|
+
readonly beforeRequest?: (resource: string) => Effect.Effect<void, ServiceUnavailable>;
|
|
8
|
+
readonly onDecodeError?: "fail" | "skip" | undefined;
|
|
9
|
+
readonly responseByteLimit?: number | undefined;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function decodedPages<TSchema extends Schema.Constraint>(
|
|
13
|
+
client: Representations,
|
|
14
|
+
path: string,
|
|
15
|
+
schema: TSchema,
|
|
16
|
+
options: ListOptions | number = {},
|
|
17
|
+
) {
|
|
18
|
+
const settings = typeof options === "number" ? { responseByteLimit: options } : options;
|
|
19
|
+
const decode = Schema.decodeUnknownEffect(schema);
|
|
20
|
+
return Stream.unwrap(client.list(path, settings.beforeRequest, settings.responseByteLimit)).pipe(
|
|
21
|
+
Stream.mapEffect((page) =>
|
|
22
|
+
Effect.gen(function* () {
|
|
23
|
+
const body = yield* page.decode(Schema.Unknown);
|
|
24
|
+
const source =
|
|
25
|
+
settings.arrayKey === undefined
|
|
26
|
+
? body
|
|
27
|
+
: (yield* Schema.decodeUnknownEffect(Schema.Record(Schema.String, Schema.Unknown))(body))[settings.arrayKey];
|
|
28
|
+
const values = yield* Schema.decodeUnknownEffect(Schema.Array(Schema.Unknown))(source);
|
|
29
|
+
const decoded = yield* Effect.forEach(values, (value) =>
|
|
30
|
+
decode(value).pipe(
|
|
31
|
+
Effect.map(Option.some),
|
|
32
|
+
Effect.catchTag("SchemaError", (error) => (settings.onDecodeError === "skip" ? Effect.succeed(Option.none()) : Effect.fail(error))),
|
|
33
|
+
),
|
|
34
|
+
);
|
|
35
|
+
const items = decoded.flatMap(Option.toArray);
|
|
36
|
+
return { hasNext: page.hasNext, items, skipped: values.length - items.length };
|
|
37
|
+
}),
|
|
38
|
+
),
|
|
39
|
+
);
|
|
40
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { Effect } from "effect";
|
|
2
|
+
import { Commit, CommitDetail, Comparison, GitTree } from "#gitSchemas.ts";
|
|
3
|
+
import { decodedPages, type ListOptions } from "#pagedLists.ts";
|
|
4
|
+
import { CollaboratorPermission, Issue, PullRequestFile, User } from "#repositorySchemas.ts";
|
|
5
|
+
import type { Representations } from "#representations.ts";
|
|
6
|
+
|
|
7
|
+
export interface RepositoryListOptions extends ListOptions {
|
|
8
|
+
readonly query?: Readonly<Record<string, string | number | boolean | undefined>>;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function repositoryReads(client: Representations) {
|
|
12
|
+
function repository(owner: string, repo: string) {
|
|
13
|
+
return `/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}`;
|
|
14
|
+
}
|
|
15
|
+
function pathWithQuery(path: string, options?: RepositoryListOptions) {
|
|
16
|
+
const query = new URLSearchParams({ "per_page": "100" });
|
|
17
|
+
for (const [key, value] of Object.entries(options?.query ?? {})) {
|
|
18
|
+
if (value !== undefined) {
|
|
19
|
+
query.set(key, String(value));
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
return `${path}?${query}`;
|
|
23
|
+
}
|
|
24
|
+
return {
|
|
25
|
+
compareCommits: (owner: string, repo: string, base: string, head: string, byteLimit?: number) =>
|
|
26
|
+
Effect.flatMap(client.read(`${repository(owner, repo)}/compare/${encodeURIComponent(base)}...${encodeURIComponent(head)}`, byteLimit), (page) =>
|
|
27
|
+
page.decode(Comparison),
|
|
28
|
+
),
|
|
29
|
+
getCollaboratorPermission: (owner: string, repo: string, username: string, byteLimit?: number) =>
|
|
30
|
+
Effect.flatMap(client.read(`${repository(owner, repo)}/collaborators/${encodeURIComponent(username)}/permission`, byteLimit), (page) =>
|
|
31
|
+
page.decode(CollaboratorPermission),
|
|
32
|
+
),
|
|
33
|
+
getCommit: (owner: string, repo: string, ref: string, byteLimit?: number) =>
|
|
34
|
+
Effect.flatMap(client.read(`${repository(owner, repo)}/commits/${encodeURIComponent(ref)}`, byteLimit), (page) => page.decode(CommitDetail)),
|
|
35
|
+
getIssue: (owner: string, repo: string, number: number, byteLimit?: number) =>
|
|
36
|
+
Effect.flatMap(client.read(`${repository(owner, repo)}/issues/${number}`, byteLimit), (page) => page.decode(Issue)),
|
|
37
|
+
getTree: (
|
|
38
|
+
owner: string,
|
|
39
|
+
repo: string,
|
|
40
|
+
sha: string,
|
|
41
|
+
options?: { readonly recursive?: boolean; readonly responseByteLimit?: number | undefined },
|
|
42
|
+
) =>
|
|
43
|
+
Effect.flatMap(
|
|
44
|
+
client.read(
|
|
45
|
+
`${repository(owner, repo)}/git/trees/${encodeURIComponent(sha)}${options?.recursive ? "?recursive=1" : ""}`,
|
|
46
|
+
options?.responseByteLimit,
|
|
47
|
+
),
|
|
48
|
+
(page) => page.decode(GitTree),
|
|
49
|
+
),
|
|
50
|
+
getUser: (username?: string, byteLimit?: number) =>
|
|
51
|
+
Effect.flatMap(client.read(username === undefined ? "/user" : `/users/${encodeURIComponent(username)}`, byteLimit), (page) =>
|
|
52
|
+
page.decode(User),
|
|
53
|
+
),
|
|
54
|
+
listCommits: (owner: string, repo: string, options?: RepositoryListOptions) =>
|
|
55
|
+
Effect.succeed(decodedPages(client, pathWithQuery(`${repository(owner, repo)}/commits`, options), Commit, options)),
|
|
56
|
+
listIssues: (owner: string, repo: string, options?: RepositoryListOptions) =>
|
|
57
|
+
Effect.succeed(decodedPages(client, pathWithQuery(`${repository(owner, repo)}/issues`, options), Issue, options)),
|
|
58
|
+
listPullRequestFiles: (owner: string, repo: string, number: number, options?: RepositoryListOptions) =>
|
|
59
|
+
Effect.succeed(decodedPages(client, pathWithQuery(`${repository(owner, repo)}/pulls/${number}/files`, options), PullRequestFile, options)),
|
|
60
|
+
};
|
|
61
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { Schema } from "effect";
|
|
2
|
+
|
|
3
|
+
export const User = Schema.Struct({
|
|
4
|
+
avatarUrl: Schema.String,
|
|
5
|
+
htmlUrl: Schema.String,
|
|
6
|
+
id: Schema.Number,
|
|
7
|
+
login: Schema.String,
|
|
8
|
+
type: Schema.String,
|
|
9
|
+
}).pipe(Schema.encodeKeys({ avatarUrl: "avatar_url", htmlUrl: "html_url" }));
|
|
10
|
+
|
|
11
|
+
export const Issue = Schema.Struct({
|
|
12
|
+
body: Schema.NullOr(Schema.String),
|
|
13
|
+
closedAt: Schema.NullOr(Schema.String),
|
|
14
|
+
createdAt: Schema.String,
|
|
15
|
+
htmlUrl: Schema.String,
|
|
16
|
+
id: Schema.Number,
|
|
17
|
+
labels: Schema.Array(Schema.Union([Schema.String, Schema.Struct({ name: Schema.optional(Schema.String) })])),
|
|
18
|
+
number: Schema.Number,
|
|
19
|
+
pullRequest: Schema.optional(Schema.Struct({ url: Schema.String })),
|
|
20
|
+
state: Schema.Literals(["open", "closed"]),
|
|
21
|
+
title: Schema.String,
|
|
22
|
+
updatedAt: Schema.String,
|
|
23
|
+
user: Schema.NullOr(User),
|
|
24
|
+
}).pipe(
|
|
25
|
+
Schema.encodeKeys({ closedAt: "closed_at", createdAt: "created_at", htmlUrl: "html_url", pullRequest: "pull_request", updatedAt: "updated_at" }),
|
|
26
|
+
);
|
|
27
|
+
|
|
28
|
+
export const PullRequestFile = Schema.Struct({
|
|
29
|
+
additions: Schema.Number,
|
|
30
|
+
changes: Schema.Number,
|
|
31
|
+
deletions: Schema.Number,
|
|
32
|
+
filename: Schema.String,
|
|
33
|
+
patch: Schema.optional(Schema.String),
|
|
34
|
+
previousFilename: Schema.optional(Schema.String),
|
|
35
|
+
sha: Schema.String,
|
|
36
|
+
status: Schema.String,
|
|
37
|
+
}).pipe(Schema.encodeKeys({ previousFilename: "previous_filename" }));
|
|
38
|
+
|
|
39
|
+
export const CollaboratorPermission = Schema.Struct({
|
|
40
|
+
permission: Schema.String,
|
|
41
|
+
roleName: Schema.String,
|
|
42
|
+
user: User,
|
|
43
|
+
}).pipe(Schema.encodeKeys({ roleName: "role_name" }));
|
package/src/representations.ts
CHANGED
|
@@ -3,6 +3,7 @@ import { HttpClient, HttpClientRequest, type HttpClientResponse } from "effect/h
|
|
|
3
3
|
import type { ServiceUnavailable } from "@shivaedev/errors/taxonomy.ts";
|
|
4
4
|
import { nextLink } from "@shivaedev/http/nextLink.ts";
|
|
5
5
|
import { paginate } from "@shivaedev/http/paginate.ts";
|
|
6
|
+
import { resolveApiUrl } from "#adapters/resolveApiUrl.ts";
|
|
6
7
|
import type { CachedResponse } from "#cache.ts";
|
|
7
8
|
import { jsonResponse } from "#json.ts";
|
|
8
9
|
import type { requests } from "#requests.ts";
|
|
@@ -14,7 +15,7 @@ export function representations(
|
|
|
14
15
|
responseByteLimit = 1024 * 1024,
|
|
15
16
|
) {
|
|
16
17
|
function get(path: string, accept = "application/vnd.github+json") {
|
|
17
|
-
return HttpClientRequest.get(
|
|
18
|
+
return HttpClientRequest.get(resolveApiUrl(apiUrl, path), { headers: { Accept: accept } });
|
|
18
19
|
}
|
|
19
20
|
function readRepresentation(path: string, byteLimit = responseByteLimit) {
|
|
20
21
|
return Effect.map(request(get(path), byteLimit), jsonResponse);
|
|
@@ -45,9 +46,9 @@ export function representations(
|
|
|
45
46
|
),
|
|
46
47
|
);
|
|
47
48
|
}
|
|
48
|
-
function writeRepresentation(method: "POST" | "PATCH", path: string, body
|
|
49
|
-
return HttpClientRequest.make(method)(
|
|
50
|
-
HttpClientRequest.bodyJson(body),
|
|
49
|
+
function writeRepresentation(method: "POST" | "PATCH" | "PUT" | "DELETE", path: string, body?: unknown, byteLimit = responseByteLimit) {
|
|
50
|
+
return HttpClientRequest.make(method)(resolveApiUrl(apiUrl, path), { headers: { Accept: "application/vnd.github+json" } }).pipe(
|
|
51
|
+
(input) => (body === undefined ? Effect.succeed(input) : HttpClientRequest.bodyJson(input, body)),
|
|
51
52
|
Effect.flatMap((input) => request(input, byteLimit)),
|
|
52
53
|
Effect.map(jsonResponse),
|
|
53
54
|
);
|