@effected/github 0.25.1 → 0.26.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/DeploymentEnvironment.js +7 -2
- package/GitHubApp.js +10 -1
- package/GitHubClient.js +68 -19
- package/GitHubError.js +13 -2
- package/GitHubRelease.js +23 -6
- package/GitHubRepository.js +74 -22
- package/GraphQL.js +7 -1
- package/index.d.ts +249 -27
- package/internal/confinement.js +62 -0
- package/internal/octokit.js +95 -5
- package/internal/requestOptions.js +84 -0
- package/internal/scrub.js +113 -0
- package/internal/uploads.js +70 -0
- package/package.json +6 -5
package/DeploymentEnvironment.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { ENVIRONMENT_PUT_ROUTE_PARAMS, reservedBodyKey } from "./internal/requestOptions.js";
|
|
1
2
|
import { unstubbed } from "./internal/unstubbed.js";
|
|
2
3
|
import { GitHubClient } from "./GitHubClient.js";
|
|
3
4
|
import { Repo } from "./Repo.js";
|
|
@@ -54,11 +55,15 @@ const make = (client) => {
|
|
|
54
55
|
repo,
|
|
55
56
|
environment: name
|
|
56
57
|
});
|
|
58
|
+
for (const key of Object.keys(config)) {
|
|
59
|
+
const reason = reservedBodyKey(key, ENVIRONMENT_PUT_ROUTE_PARAMS, "an environment setting");
|
|
60
|
+
if (reason !== void 0) return yield* Effect.die(/* @__PURE__ */ new RangeError(`DeploymentEnvironment.upsert: "${key}" ${reason}`));
|
|
61
|
+
}
|
|
57
62
|
yield* client.request("PUT /repos/{owner}/{repo}/environments/{environment_name}", {
|
|
58
|
-
...config,
|
|
59
63
|
owner,
|
|
60
64
|
repo,
|
|
61
|
-
environment_name: name
|
|
65
|
+
environment_name: name,
|
|
66
|
+
data: { ...config }
|
|
62
67
|
});
|
|
63
68
|
}),
|
|
64
69
|
list: Effect.fn("DeploymentEnvironment.list")(function* () {
|
package/GitHubApp.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { GitHubError } from "./GitHubError.js";
|
|
2
2
|
import { GitHubGraphQLError } from "./GraphQL.js";
|
|
3
|
+
import { uploadChannelOf, withUploadChannel } from "./internal/uploads.js";
|
|
3
4
|
import { unstubbed } from "./internal/unstubbed.js";
|
|
4
5
|
import { GitHubClient, makeClientShape, resolveApiVersion } from "./GitHubClient.js";
|
|
5
6
|
import { numericId } from "./internal/ids.js";
|
|
@@ -673,7 +674,11 @@ const makeRotatingClient = (mint, release, options, operation) => Effect.gen(fun
|
|
|
673
674
|
errors: [],
|
|
674
675
|
cause: error
|
|
675
676
|
}))));
|
|
676
|
-
|
|
677
|
+
const upload = (route, params, schema) => Effect.flatMap(current, (client) => {
|
|
678
|
+
const channel = uploadChannelOf(client);
|
|
679
|
+
return channel === void 0 ? Effect.die(/* @__PURE__ */ new Error(`${operation}: the inner client carries no upload channel`)) : channel.upload(route, params, schema);
|
|
680
|
+
});
|
|
681
|
+
const shape = {
|
|
677
682
|
request: (route, params) => Effect.flatMap(current, (client) => client.request(route, params)),
|
|
678
683
|
requestDecoded: (route, params, schema) => Effect.flatMap(current, (client) => client.requestDecoded(route, params, schema)),
|
|
679
684
|
paginate: (route, params, pageOptions) => Effect.flatMap(current, (client) => client.paginate(route, params, pageOptions)),
|
|
@@ -682,6 +687,10 @@ const makeRotatingClient = (mint, release, options, operation) => Effect.gen(fun
|
|
|
682
687
|
rateLimit: Effect.flatMap(Ref.get(held), (state) => Option.isSome(state) ? state.value.client.rateLimit : Effect.succeed(Option.none())),
|
|
683
688
|
apiVersion
|
|
684
689
|
};
|
|
690
|
+
return withUploadChannel(shape, {
|
|
691
|
+
baseUrl: options.baseUrl,
|
|
692
|
+
upload
|
|
693
|
+
});
|
|
685
694
|
});
|
|
686
695
|
|
|
687
696
|
export { AppIdentity, BotIdentity, BotUser, GitHubApp, GitHubAppError, Installation, InstallationToken };
|
package/GitHubClient.js
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { GitHubError } from "./GitHubError.js";
|
|
2
2
|
import { GitHubGraphQLError } from "./GraphQL.js";
|
|
3
|
+
import { DOTCOM_API_BASE_URL, originOf, uploadsBaseUrlFor, withUploadChannel } from "./internal/uploads.js";
|
|
4
|
+
import { rawRequestRefusal, refusedRequest } from "./internal/confinement.js";
|
|
3
5
|
import { RetryPolicy } from "./Resilience.js";
|
|
4
|
-
import { DEFAULT_API_VERSION, makeTransport } from "./internal/octokit.js";
|
|
6
|
+
import { DEFAULT_API_VERSION, expandOnDotcom, makeTransport } from "./internal/octokit.js";
|
|
5
7
|
import { fromArray, paginate } from "./internal/paginate.js";
|
|
6
8
|
import { unstubbed } from "./internal/unstubbed.js";
|
|
7
9
|
import { Clock, Config, Context, Effect, Layer, Option, Schema, Stream } from "effect";
|
|
@@ -82,6 +84,34 @@ var GitHubFixtures = class {
|
|
|
82
84
|
const resolvePolicy = (retry) => retry === void 0 ? RetryPolicy.default : retry === "off" ? RetryPolicy.none : retry;
|
|
83
85
|
const perPageOf = (options) => options?.perPage ?? DEFAULT_PER_PAGE;
|
|
84
86
|
const dies = unstubbed("GitHubClient.makeTest", "pass an override, or use GitHubClient.layerFixture for recorded responses.");
|
|
87
|
+
const DOTCOM_API_ORIGIN = originOf(DOTCOM_API_BASE_URL);
|
|
88
|
+
const doubleRefusal = (route, params) => rawRequestRefusal(expandOnDotcom, route, typeof params === "object" && params !== null ? params : {}, DOTCOM_API_ORIGIN);
|
|
89
|
+
/**
|
|
90
|
+
* `shape` with the live client's token-confinement refusals applied in front of
|
|
91
|
+
* its four raw members, so a suite on a double sees the `rejected` failure
|
|
92
|
+
* production would raise. A refused call never reaches the double's handler,
|
|
93
|
+
* stub, fixture or `requested` log, as a refused live call never reaches the
|
|
94
|
+
* wire.
|
|
95
|
+
*/
|
|
96
|
+
const confineDouble = (shape) => ({
|
|
97
|
+
...shape,
|
|
98
|
+
request: (route, params) => Effect.suspend(() => {
|
|
99
|
+
const reason = doubleRefusal(route, params);
|
|
100
|
+
return reason !== void 0 ? Effect.fail(refusedRequest(route, reason)) : shape.request(route, params);
|
|
101
|
+
}),
|
|
102
|
+
requestDecoded: (route, params, schema) => Effect.suspend(() => {
|
|
103
|
+
const reason = doubleRefusal(route, params);
|
|
104
|
+
return reason !== void 0 ? Effect.fail(refusedRequest(route, reason)) : shape.requestDecoded(route, params, schema);
|
|
105
|
+
}),
|
|
106
|
+
paginate: (route, params, options) => Effect.suspend(() => {
|
|
107
|
+
const reason = doubleRefusal(route, params);
|
|
108
|
+
return reason !== void 0 ? Effect.fail(refusedRequest(route, reason)) : shape.paginate(route, params, options);
|
|
109
|
+
}),
|
|
110
|
+
paginateStream: (route, params, options) => Stream.suspend(() => {
|
|
111
|
+
const reason = doubleRefusal(route, params);
|
|
112
|
+
return reason !== void 0 ? Stream.fail(refusedRequest(route, reason)) : shape.paginateStream(route, params, options);
|
|
113
|
+
})
|
|
114
|
+
});
|
|
85
115
|
/**
|
|
86
116
|
* The typed GitHub API client.
|
|
87
117
|
*
|
|
@@ -157,10 +187,16 @@ var GitHubClient = class GitHubClient extends Context.Service()("@effected/githu
|
|
|
157
187
|
* `rateLimit` answers `Option.none()` and `apiVersion` the live default,
|
|
158
188
|
* because a real client legitimately answers both without a request.
|
|
159
189
|
*
|
|
190
|
+
* It refuses what the live client refuses, before any override runs: a
|
|
191
|
+
* `baseUrl`, `url`, `request` or `method` parameter (a body field called
|
|
192
|
+
* `url` goes inside `data`), or a route that resolves off github.com's REST
|
|
193
|
+
* root, fails `kind: "rejected"` with no status. A double has no
|
|
194
|
+
* configured base, so an absolute-URL route must name `api.github.com`.
|
|
195
|
+
*
|
|
160
196
|
* For recorded responses that page for real, use
|
|
161
197
|
* {@link GitHubClient.layerFixture}.
|
|
162
198
|
*/
|
|
163
|
-
static makeTest = (overrides = {}) => ({
|
|
199
|
+
static makeTest = (overrides = {}) => confineDouble({
|
|
164
200
|
request: overrides.request ?? (() => dies("request")),
|
|
165
201
|
requestDecoded: overrides.requestDecoded ?? (() => dies("requestDecoded")),
|
|
166
202
|
paginate: overrides.paginate ?? (() => dies("paginate")),
|
|
@@ -189,8 +225,13 @@ var GitHubClient = class GitHubClient extends Context.Service()("@effected/githu
|
|
|
189
225
|
* one fixture both records the calls and classifies a raw failure the way
|
|
190
226
|
* the live client would; there is no need to classify by hand inside a
|
|
191
227
|
* {@link GitHubClient.layerTest} handler.
|
|
228
|
+
*
|
|
229
|
+
* It refuses what the live client refuses, exactly as
|
|
230
|
+
* {@link GitHubClient.makeTest} does. A refused call fails
|
|
231
|
+
* `kind: "rejected"` and is **not** recorded in `requested`: it is never
|
|
232
|
+
* served, as a refused live call never reaches the wire.
|
|
192
233
|
*/
|
|
193
|
-
static layerFixture = (fixtures) => Layer.succeed(GitHubClient, makeFixture(fixtures));
|
|
234
|
+
static layerFixture = (fixtures) => Layer.succeed(GitHubClient, confineDouble(makeFixture(fixtures)));
|
|
194
235
|
};
|
|
195
236
|
/** A calendar api-version: `YYYY-MM-DD`. */
|
|
196
237
|
const API_VERSION_PATTERN = /^\d{4}-\d{2}-\d{2}$/;
|
|
@@ -231,35 +272,43 @@ const makeClientShape = (options) => Effect.gen(function* () {
|
|
|
231
272
|
yield* Effect.annotateCurrentSpan({ route });
|
|
232
273
|
return (yield* transport.request(route, route, params)).data;
|
|
233
274
|
});
|
|
275
|
+
const decodeWith = (route, schema) => (data) => Schema.decodeUnknownEffect(schema)(data).pipe(Effect.catchTag("SchemaError", (error) => Effect.fail(GitHubError.decode(route, "response did not match its schema", error))));
|
|
234
276
|
const requestDecoded = Effect.fn("GitHubClient.requestDecoded")(function* (route, params, schema) {
|
|
235
277
|
yield* Effect.annotateCurrentSpan({ route });
|
|
236
278
|
const response = yield* transport.request(route, route, params);
|
|
237
|
-
return yield*
|
|
279
|
+
return yield* decodeWith(route, schema)(response.data);
|
|
238
280
|
});
|
|
281
|
+
const uploadsBaseUrl = uploadsBaseUrlFor(options.baseUrl);
|
|
282
|
+
const upload = (route, params, schema) => Effect.flatMap(transport.requestAt(route, route, params, uploadsBaseUrl), (response) => decodeWith(route, schema)(response.data));
|
|
239
283
|
const paginateStream = (route, params, options) => paginate(() => transport.pageSource(route, route, {
|
|
240
284
|
per_page: perPageOf(options),
|
|
241
285
|
...params
|
|
242
286
|
}), options?.maxPages);
|
|
243
|
-
|
|
287
|
+
const paginateAll = Effect.fn("GitHubClient.paginate")(function* (route, params, options) {
|
|
288
|
+
yield* Effect.annotateCurrentSpan({
|
|
289
|
+
route,
|
|
290
|
+
perPage: perPageOf(options),
|
|
291
|
+
maxPages: options?.maxPages ?? -1
|
|
292
|
+
});
|
|
293
|
+
return yield* Stream.runCollect(paginateStream(route, params, options));
|
|
294
|
+
});
|
|
295
|
+
const graphql = Effect.fn("GitHubClient.graphql")(function* (document, variables) {
|
|
296
|
+
yield* Effect.annotateCurrentSpan({ document: document.name });
|
|
297
|
+
const raw = yield* transport.graphql(document.name, document.document, document.encodeVariables(variables));
|
|
298
|
+
return yield* document.decode(raw).pipe(Effect.catchTag("SchemaError", (error) => Effect.fail(GitHubGraphQLError.decode(document.name, "response did not match its schema", error))));
|
|
299
|
+
});
|
|
300
|
+
return withUploadChannel({
|
|
244
301
|
request,
|
|
245
302
|
requestDecoded,
|
|
246
|
-
paginate:
|
|
247
|
-
yield* Effect.annotateCurrentSpan({
|
|
248
|
-
route,
|
|
249
|
-
perPage: perPageOf(options),
|
|
250
|
-
maxPages: options?.maxPages ?? -1
|
|
251
|
-
});
|
|
252
|
-
return yield* Stream.runCollect(paginateStream(route, params, options));
|
|
253
|
-
}),
|
|
303
|
+
paginate: paginateAll,
|
|
254
304
|
paginateStream,
|
|
255
|
-
graphql
|
|
256
|
-
yield* Effect.annotateCurrentSpan({ document: document.name });
|
|
257
|
-
const raw = yield* transport.graphql(document.name, document.document, document.encodeVariables(variables));
|
|
258
|
-
return yield* document.decode(raw).pipe(Effect.catchTag("SchemaError", (error) => Effect.fail(GitHubGraphQLError.decode(document.name, "response did not match its schema", error))));
|
|
259
|
-
}),
|
|
305
|
+
graphql,
|
|
260
306
|
rateLimit: transport.rateLimit,
|
|
261
307
|
apiVersion
|
|
262
|
-
}
|
|
308
|
+
}, {
|
|
309
|
+
baseUrl: options.baseUrl,
|
|
310
|
+
upload
|
|
311
|
+
});
|
|
263
312
|
});
|
|
264
313
|
const makeFixture = (fixtures) => {
|
|
265
314
|
const requested = fixtures.requested;
|
package/GitHubError.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { headerNumber, retryAfterMillisFrom } from "./internal/headers.js";
|
|
2
|
+
import { scrubThrowable } from "./internal/scrub.js";
|
|
2
3
|
import { Schema } from "effect";
|
|
3
4
|
|
|
4
5
|
// src/GitHubError.ts
|
|
@@ -106,7 +107,7 @@ var GitHubError = class GitHubError extends Schema.TaggedError()("GitHubError",
|
|
|
106
107
|
* 422s ("Update is not a fast forward") are prose only.
|
|
107
108
|
*/
|
|
108
109
|
validation: Schema.optionalKey(Schema.Array(GitHubValidationEntry)),
|
|
109
|
-
/** The underlying throwable, when one exists. */
|
|
110
|
+
/** The underlying throwable, when one exists; an octokit throwable is kept as a credential-free copy. */
|
|
110
111
|
cause: Schema.optionalKey(Schema.Defect())
|
|
111
112
|
}) {
|
|
112
113
|
/** `"GitBranch.upsert failed (422): Reference already exists"`. */
|
|
@@ -173,9 +174,19 @@ var GitHubError = class GitHubError extends Schema.TaggedError()("GitHubError",
|
|
|
173
174
|
* epoch second, and turning it into a delay needs a "now" the caller controls.
|
|
174
175
|
* The client supplies `Clock.currentTimeMillis`, which is the `TestClock`
|
|
175
176
|
* under test.
|
|
177
|
+
*
|
|
178
|
+
* The kept `cause` is a credential-free copy of the throwable, not the
|
|
179
|
+
* throwable itself. octokit redacts only the `authorization` credential and
|
|
180
|
+
* two query values; the copy also drops the request body (a webhook secret,
|
|
181
|
+
* GraphQL variables), the per-request options and the response body beyond
|
|
182
|
+
* its top-level `message` and `documentation_url`, drops `set-cookie`, and
|
|
183
|
+
* reads `REDACTED` for every other credential-bearing header. It keeps the
|
|
184
|
+
* prototype (so `instanceof` still answers), `name`, `message`, `status`,
|
|
185
|
+
* the method, the redacted url and the remaining headers. A throwable with
|
|
186
|
+
* no `request`, `response` or `headers` field is kept as it is.
|
|
176
187
|
*/
|
|
177
188
|
static fromOctokit(operation, error, nowMillis) {
|
|
178
|
-
return fromFacts(operation, readThrowable(error), nowMillis, error);
|
|
189
|
+
return fromFacts(operation, readThrowable(error), nowMillis, scrubThrowable(error));
|
|
179
190
|
}
|
|
180
191
|
/**
|
|
181
192
|
* Classify a raw HTTP response: a status, its headers and its JSON body.
|
package/GitHubRelease.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { GitHubError } from "./GitHubError.js";
|
|
2
|
+
import { originOf, uploadChannelOf, uploadsBaseUrlFor } from "./internal/uploads.js";
|
|
2
3
|
import { unstubbed } from "./internal/unstubbed.js";
|
|
3
4
|
import { GitHubClient } from "./GitHubClient.js";
|
|
4
5
|
import { Repo } from "./Repo.js";
|
|
@@ -19,7 +20,15 @@ var ReleaseInfo = class extends Schema.Class("ReleaseInfo")({
|
|
|
19
20
|
prerelease: Schema.Boolean,
|
|
20
21
|
/** The web URL. */
|
|
21
22
|
url: Schema.String,
|
|
22
|
-
/**
|
|
23
|
+
/**
|
|
24
|
+
* The templated upload endpoint GitHub hands back for assets.
|
|
25
|
+
*
|
|
26
|
+
* @remarks
|
|
27
|
+
* {@link GitHubReleaseShape.uploadAsset} never routes by this value: it is
|
|
28
|
+
* response data. It only cross-checks that its origin is the upload host
|
|
29
|
+
* derived from the client's configured base URL, and refuses the upload
|
|
30
|
+
* when it is not.
|
|
31
|
+
*/
|
|
23
32
|
uploadUrl: Schema.String
|
|
24
33
|
}) {};
|
|
25
34
|
/**
|
|
@@ -172,16 +181,26 @@ const make = (client) => {
|
|
|
172
181
|
release: release.id,
|
|
173
182
|
asset: asset.name
|
|
174
183
|
});
|
|
175
|
-
const
|
|
184
|
+
const channel = uploadChannelOf(client);
|
|
185
|
+
const uploadsBaseUrl = uploadsBaseUrlFor(channel?.baseUrl);
|
|
186
|
+
const expected = originOf(uploadsBaseUrl);
|
|
187
|
+
const offered = originOf(release.uploadUrl);
|
|
188
|
+
if (expected === void 0 || offered !== expected) return yield* Effect.fail(new GitHubError({
|
|
189
|
+
kind: "rejected",
|
|
190
|
+
operation: "GitHubRelease.uploadAsset",
|
|
191
|
+
reason: offered === void 0 ? `release ${release.id} has no absolute upload URL; refusing to upload` : `release ${release.id}'s upload URL is on ${offered}, not this client's upload host ${expected ?? uploadsBaseUrl}; refusing to send the token there`
|
|
192
|
+
}));
|
|
193
|
+
const route = asset.label === void 0 ? "POST /repos/{owner}/{repo}/releases/{release_id}/assets{?name}" : "POST /repos/{owner}/{repo}/releases/{release_id}/assets{?name,label}";
|
|
194
|
+
const params = {
|
|
176
195
|
owner,
|
|
177
196
|
repo,
|
|
178
197
|
release_id: release.id,
|
|
179
198
|
name: asset.name,
|
|
180
199
|
...asset.label === void 0 ? {} : { label: asset.label },
|
|
181
200
|
data: asset.data,
|
|
182
|
-
baseUrl: UPLOADS_BASE_URL,
|
|
183
201
|
headers: { "content-type": asset.contentType }
|
|
184
|
-
}
|
|
202
|
+
};
|
|
203
|
+
const raw = channel === void 0 ? yield* client.requestDecoded(route, params, AssetResponse) : yield* channel.upload(route, params, AssetResponse);
|
|
185
204
|
return assetOf(raw);
|
|
186
205
|
}),
|
|
187
206
|
listAssets: Effect.fn("GitHubRelease.listAssets")(function* (id, options) {
|
|
@@ -199,7 +218,5 @@ const make = (client) => {
|
|
|
199
218
|
})
|
|
200
219
|
};
|
|
201
220
|
};
|
|
202
|
-
/** Release assets do not go to the API host. */
|
|
203
|
-
const UPLOADS_BASE_URL = "https://uploads.github.com";
|
|
204
221
|
|
|
205
222
|
export { GitHubRelease, ReleaseAsset, ReleaseInfo };
|
package/GitHubRepository.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { GraphQLDocument } from "./GraphQL.js";
|
|
2
|
+
import { reservedSettingKey } from "./internal/requestOptions.js";
|
|
2
3
|
import { unstubbed, unstubbedRead } from "./internal/unstubbed.js";
|
|
3
4
|
import { GitHubClient } from "./GitHubClient.js";
|
|
4
5
|
import { Repo } from "./Repo.js";
|
|
@@ -8,7 +9,7 @@ import { Context, Effect, Layer, Schema } from "effect";
|
|
|
8
9
|
// src/GitHubRepository.ts
|
|
9
10
|
/**
|
|
10
11
|
* A {@link RepositoryPatch} from a draft, dropping every explicitly-`undefined`
|
|
11
|
-
* field.
|
|
12
|
+
* field, plus any settings octokit's types do not know yet.
|
|
12
13
|
*
|
|
13
14
|
* @remarks
|
|
14
15
|
* The supported spelling for "apply only what was configured" — the natural
|
|
@@ -18,6 +19,13 @@ import { Context, Effect, Layer, Schema } from "effect";
|
|
|
18
19
|
* `PATCH` treats an absent field as "leave it alone", while an explicit `null`
|
|
19
20
|
* or `undefined` is a value.
|
|
20
21
|
*
|
|
22
|
+
* `options.extra` is the typed escape hatch for a setting GitHub accepts
|
|
23
|
+
* before octokit's generated types declare it: the draft stays checked, and
|
|
24
|
+
* only the extras ride unchecked. The result is still typed `RepositoryPatch`,
|
|
25
|
+
* and `GitHubRepository.updateSettings` sends every key it carries. A known key
|
|
26
|
+
* passed through `extra` throws a `RangeError` rather than skipping the draft's
|
|
27
|
+
* type check (see {@link RepositoryPatchOptions.extra}).
|
|
28
|
+
*
|
|
21
29
|
* A key-by-key loop still defeats TypeScript's correlation between two indexed
|
|
22
30
|
* accesses (`draft[key] = source[key]` over a union `key`), which no helper can
|
|
23
31
|
* fix — build the draft as a literal where you can.
|
|
@@ -27,23 +35,60 @@ import { Context, Effect, Layer, Schema } from "effect";
|
|
|
27
35
|
* import { repositoryPatch } from "@effected/github";
|
|
28
36
|
*
|
|
29
37
|
* // `config.has_issues` is `boolean | undefined`; absent fields drop out.
|
|
30
|
-
* const patch = repositoryPatch(
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
38
|
+
* const patch = repositoryPatch(
|
|
39
|
+
* {
|
|
40
|
+
* has_issues: config.has_issues,
|
|
41
|
+
* has_wiki: config.has_wiki,
|
|
42
|
+
* description: config.description,
|
|
43
|
+
* },
|
|
44
|
+
* // A setting GitHub accepts before octokit's types declare it.
|
|
45
|
+
* { extra: { has_new_feature: true } },
|
|
46
|
+
* );
|
|
35
47
|
* ```
|
|
36
48
|
*
|
|
37
49
|
* @param draft - The fields to apply, any of which may be `undefined`.
|
|
50
|
+
* @param options - `extra`: settings the generated type does not declare.
|
|
38
51
|
* @returns A patch carrying only the fields that were actually set.
|
|
39
52
|
*
|
|
40
53
|
* @public
|
|
41
54
|
*/
|
|
42
|
-
const repositoryPatch = (draft) => {
|
|
55
|
+
const repositoryPatch = (draft, options) => {
|
|
43
56
|
const out = {};
|
|
44
|
-
for (const [key, value] of Object.entries(draft))
|
|
57
|
+
for (const [key, value] of Object.entries(draft)) {
|
|
58
|
+
refuseReserved(key);
|
|
59
|
+
if (value !== void 0) out[key] = value;
|
|
60
|
+
}
|
|
61
|
+
for (const [key, value] of Object.entries(options?.extra ?? {})) {
|
|
62
|
+
refuseReserved(key);
|
|
63
|
+
if (Object.hasOwn(GRAPHQL_ONLY_SETTINGS, key)) throw new RangeError(`repositoryPatch: "${key}" can only be set through GraphQL; use GitHubRepository.applySettings, not a REST patch`);
|
|
64
|
+
if (REPOSITORY_REST_SETTING_KEYS.has(key)) throw new RangeError(`repositoryPatch: "${key}" is a declared repository setting; pass it in the draft, where its type is checked, not in extra`);
|
|
65
|
+
if (value !== void 0) out[key] = value;
|
|
66
|
+
}
|
|
45
67
|
return out;
|
|
46
68
|
};
|
|
69
|
+
/** A `RangeError` for a key octokit would read as something other than a setting. */
|
|
70
|
+
const refuseReserved = (key) => {
|
|
71
|
+
const reason = reservedSettingKey(key);
|
|
72
|
+
if (reason !== void 0) throw new RangeError(`repositoryPatch: "${key}" ${reason}; it is never sent`);
|
|
73
|
+
};
|
|
74
|
+
/**
|
|
75
|
+
* The parameters of a repository PATCH, with the settings as octokit's `data`.
|
|
76
|
+
*
|
|
77
|
+
* @remarks
|
|
78
|
+
* `data` is octokit's explicit body: `@octokit/endpoint` sends it verbatim and
|
|
79
|
+
* reads no key inside it as a request option or a path parameter. Spreading
|
|
80
|
+
* the settings beside `owner` and `repo` instead lets an open map choose the
|
|
81
|
+
* request's host, headers, fetch implementation or path, which is how an
|
|
82
|
+
* authenticated PATCH reaches another host with the token attached. So the
|
|
83
|
+
* confinement holds by construction; the reserved-name refusals are defence in
|
|
84
|
+
* depth. The generated parameter type has no `data`, so the cast is here, on
|
|
85
|
+
* the one shape both write paths send.
|
|
86
|
+
*/
|
|
87
|
+
const patchParams = (owner, repo, body) => ({
|
|
88
|
+
owner,
|
|
89
|
+
repo,
|
|
90
|
+
data: body
|
|
91
|
+
});
|
|
47
92
|
/**
|
|
48
93
|
* Fields in the user-facing `security_and_analysis` block that GitHub accepts
|
|
49
94
|
* as `{ status: "enabled" | "disabled" }`.
|
|
@@ -268,26 +313,29 @@ const make = (client) => {
|
|
|
268
313
|
repo,
|
|
269
314
|
fields: Object.keys(patch).length
|
|
270
315
|
});
|
|
271
|
-
return yield* client.request("PATCH /repos/{owner}/{repo}",
|
|
272
|
-
...preparePatch(patch),
|
|
273
|
-
owner,
|
|
274
|
-
repo
|
|
275
|
-
});
|
|
316
|
+
return yield* client.request("PATCH /repos/{owner}/{repo}", patchParams(owner, repo, preparePatch(patch)));
|
|
276
317
|
}),
|
|
277
318
|
defaultBranch: Effect.map(settings, (repository) => repository.default_branch),
|
|
278
319
|
nodeId: Effect.map(settings, (repository) => repository.node_id),
|
|
279
320
|
ownerType,
|
|
280
|
-
applySettings: Effect.fn("GitHubRepository.applySettings")(function* (input) {
|
|
321
|
+
applySettings: Effect.fn("GitHubRepository.applySettings")(function* (input, options) {
|
|
281
322
|
const { owner, repo } = yield* Repo;
|
|
323
|
+
const sendUnknown = options?.unknownKeys === "send";
|
|
282
324
|
yield* Effect.annotateCurrentSpan({
|
|
283
325
|
owner,
|
|
284
|
-
repo
|
|
326
|
+
repo,
|
|
327
|
+
unknownKeys: sendUnknown ? "send" : "ignore"
|
|
285
328
|
});
|
|
286
329
|
const rest = {};
|
|
287
330
|
const graphql = {};
|
|
288
331
|
const graphqlKeys = [];
|
|
289
332
|
const ignored = [];
|
|
333
|
+
const unverified = [];
|
|
290
334
|
for (const [key, value] of Object.entries(input)) {
|
|
335
|
+
if (reservedSettingKey(key) !== void 0) {
|
|
336
|
+
ignored.push(key);
|
|
337
|
+
continue;
|
|
338
|
+
}
|
|
291
339
|
const graphqlField = Object.hasOwn(GRAPHQL_ONLY_SETTINGS, key) ? GRAPHQL_ONLY_SETTINGS[key] : void 0;
|
|
292
340
|
if (graphqlField !== void 0) {
|
|
293
341
|
graphql[graphqlField] = value;
|
|
@@ -295,19 +343,22 @@ const make = (client) => {
|
|
|
295
343
|
continue;
|
|
296
344
|
}
|
|
297
345
|
const removedIn = Object.hasOwn(REMOVED_BY_PINNED_VERSION, key) ? REMOVED_BY_PINNED_VERSION[key] : void 0;
|
|
298
|
-
if (
|
|
346
|
+
if (removedIn !== void 0 && client.apiVersion >= removedIn) {
|
|
299
347
|
ignored.push(key);
|
|
300
348
|
continue;
|
|
301
349
|
}
|
|
350
|
+
if (!REPOSITORY_REST_SETTING_KEYS.has(key)) {
|
|
351
|
+
if (!sendUnknown) {
|
|
352
|
+
ignored.push(key);
|
|
353
|
+
continue;
|
|
354
|
+
}
|
|
355
|
+
unverified.push(key);
|
|
356
|
+
}
|
|
302
357
|
rest[key] = value;
|
|
303
358
|
}
|
|
304
359
|
const prepared = preparePatch(rest);
|
|
305
360
|
const restKeys = Object.keys(prepared);
|
|
306
|
-
if (restKeys.length > 0) yield* client.request("PATCH /repos/{owner}/{repo}",
|
|
307
|
-
...prepared,
|
|
308
|
-
owner,
|
|
309
|
-
repo
|
|
310
|
-
});
|
|
361
|
+
if (restKeys.length > 0) yield* client.request("PATCH /repos/{owner}/{repo}", patchParams(owner, repo, prepared));
|
|
311
362
|
if (Object.keys(graphql).length > 0) {
|
|
312
363
|
const repository = yield* client.request("GET /repos/{owner}/{repo}", {
|
|
313
364
|
owner,
|
|
@@ -321,7 +372,8 @@ const make = (client) => {
|
|
|
321
372
|
return {
|
|
322
373
|
rest: restKeys,
|
|
323
374
|
graphql: graphqlKeys,
|
|
324
|
-
ignored
|
|
375
|
+
ignored,
|
|
376
|
+
unverified: unverified.filter((key) => Object.hasOwn(prepared, key))
|
|
325
377
|
};
|
|
326
378
|
})
|
|
327
379
|
};
|
package/GraphQL.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { retryAfterMillisFrom } from "./internal/headers.js";
|
|
2
|
+
import { scrubThrowable } from "./internal/scrub.js";
|
|
2
3
|
import { Schema } from "effect";
|
|
3
4
|
|
|
4
5
|
// src/GraphQL.ts
|
|
@@ -78,6 +79,11 @@ var GitHubGraphQLError = class GitHubGraphQLError extends Schema.TaggedError()("
|
|
|
78
79
|
* failure with a `status`. Both arrive as throwables and both are read
|
|
79
80
|
* structurally, for the same reason the REST classifier does it — the error
|
|
80
81
|
* classes live in packages this one does not declare.
|
|
82
|
+
*
|
|
83
|
+
* The kept `cause` is the same credential-free copy
|
|
84
|
+
* {@link GitHubError.fromOctokit} keeps: the GraphQL document, its variables
|
|
85
|
+
* and any partial `data` are dropped with the rest of the request and response
|
|
86
|
+
* bodies.
|
|
81
87
|
*/
|
|
82
88
|
static fromThrowable(operation, error, nowMillis) {
|
|
83
89
|
const record = typeof error === "object" && error !== null ? error : void 0;
|
|
@@ -92,7 +98,7 @@ var GitHubGraphQLError = class GitHubGraphQLError extends Schema.TaggedError()("
|
|
|
92
98
|
reason,
|
|
93
99
|
errors: entries,
|
|
94
100
|
...retryAfterMillis !== void 0 ? { retryAfterMillis } : {},
|
|
95
|
-
cause: error
|
|
101
|
+
cause: scrubThrowable(error)
|
|
96
102
|
});
|
|
97
103
|
}
|
|
98
104
|
};
|