@osdlc/typed-gh 0.16.0 → 0.17.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 +13 -0
- package/README.md +73 -6
- package/dist/async.d.ts +3 -1
- package/dist/async.js +0 -5
- package/dist/doors.d.ts +53 -6
- package/dist/doors.js +5 -4
- package/dist/index.d.ts +1 -1
- package/dist/sync.d.ts +6 -1
- package/dist/sync.js +0 -6
- package/dist/verbs.d.ts +94 -6
- package/dist/verbs.js +119 -30
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,18 @@
|
|
|
1
1
|
# @osdlc/typed-gh
|
|
2
2
|
|
|
3
|
+
## 0.17.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 61b12ce: `graphql` (experimental) reads a partial answer with `{ partial: true }`. Each page comes back as a `GraphqlPage`, its `data` with its `errors` (`GraphqlError`: `message`, `type`, `path`), so a batch of aliased lookups keeps the ones that worked when one is gone. gh exits 1 for such a response; the partial form reads it as the answer on the plain method, `try` and `soft`. It still fails when there is no `data` to read, when gh failed for another reason, or when `paginate` stopped at a page with errors that has a next page. `nodesAt(pages, path, { partial: true })` joins one connection across partial pages and returns `PartialNodes`: the nodes and every page's errors. Without `partial`, nothing changes. There is no page cap: `paginate` follows every page, and a caller that needs to stop early pages with `$endCursor` itself, as the README shows. The `try` and `soft` types now keep both overloads of an overloaded method.
|
|
8
|
+
|
|
9
|
+
## 0.16.1
|
|
10
|
+
|
|
11
|
+
### Patch Changes
|
|
12
|
+
|
|
13
|
+
- Updated dependencies [25ccd9e]
|
|
14
|
+
- @osdlc/typed-git@0.13.0
|
|
15
|
+
|
|
3
16
|
## 0.16.0
|
|
4
17
|
|
|
5
18
|
### Minor Changes
|
package/README.md
CHANGED
|
@@ -492,7 +492,7 @@ const branches = gh.api('repos/{owner}/{repo}/branches', { paginate: true }).bod
|
|
|
492
492
|
```
|
|
493
493
|
|
|
494
494
|
`graphql` (experimental) runs a query and returns each page's `data`. It throws when the
|
|
495
|
-
call failed or any page has `errors` or no `data` (`soft.graphql` is `null`). A partial answer counts as a failure. `nodesAt` joins
|
|
495
|
+
call failed or any page has `errors` or no `data` (`soft.graphql` is `null`). A partial answer counts as a failure, unless you pass `partial: true` (see [Partial answers](#partial-answers)). `nodesAt` joins
|
|
496
496
|
one connection's `nodes` across the pages, and is `null` when any page lacks them.
|
|
497
497
|
`fromTarget('owner')` and `fromTarget('repo')` fill a variable from the client's repo, or let gh
|
|
498
498
|
fill it from the clone. A plain string is always sent as written, so a real value of `{owner}` stays
|
|
@@ -518,6 +518,70 @@ const prs = nodesAt(pages, ['repository', 'pullRequests'])
|
|
|
518
518
|
With `paginate`, the query must take `$endCursor` and page one connection. gh follows only that
|
|
519
519
|
one. `graphql` refuses a document holding a mutation or subscription with `RangeError`. Send one through `raw`.
|
|
520
520
|
|
|
521
|
+
### Partial answers
|
|
522
|
+
|
|
523
|
+
When part of a query fails, GitHub still answers the rest: an aliased pull request that was
|
|
524
|
+
deleted is `null` in `data`, and an entry in `errors` names its path. gh prints that response and
|
|
525
|
+
exits 1. Pass `partial: true` to read it. Each page comes back as a `GraphqlPage`, its `data` with
|
|
526
|
+
its `errors` (each a `GraphqlError` of `message`, `type` and `path`). `errors` is empty for a page
|
|
527
|
+
that came back whole.
|
|
528
|
+
|
|
529
|
+
```ts
|
|
530
|
+
const [page] = gh.graphql(
|
|
531
|
+
`query($owner: String!, $name: String!) {
|
|
532
|
+
repository(owner: $owner, name: $name) {
|
|
533
|
+
a: pullRequest(number: 1) { title }
|
|
534
|
+
b: pullRequest(number: 2) { title }
|
|
535
|
+
}
|
|
536
|
+
}`,
|
|
537
|
+
{ owner: fromTarget('owner'), name: fromTarget('repo') },
|
|
538
|
+
{ partial: true },
|
|
539
|
+
)
|
|
540
|
+
const missing = page?.errors.map((e) => e.path.join('.')) // ['repository.b'] when #2 is gone
|
|
541
|
+
```
|
|
542
|
+
|
|
543
|
+
It still throws (`soft.graphql` is `null`) when there is no `data` to read: a failed call, a
|
|
544
|
+
response with no `data` or `data: null` (a rate limit, a query that does not validate), or output
|
|
545
|
+
that does not parse. With `paginate`, gh stops at the first page with errors. When that page still has a next
|
|
546
|
+
page, the pages after it were never fetched, so that throws too, rather than return fewer rows.
|
|
547
|
+
`nodesAt(pages, path, { partial: true })` joins one connection across partial pages and returns
|
|
548
|
+
`PartialNodes`: the `nodes`, where a node GitHub could not resolve is `null`, and every page's
|
|
549
|
+
`errors`.
|
|
550
|
+
|
|
551
|
+
### Stopping early
|
|
552
|
+
|
|
553
|
+
`paginate` has no page cap: gh follows the connection to its end. To read at most a few pages, or
|
|
554
|
+
to carry on past a page with errors (add `partial: true` and read each page's `data`), leave
|
|
555
|
+
`paginate` off and pass the cursor yourself:
|
|
556
|
+
|
|
557
|
+
```ts
|
|
558
|
+
const query = `query($endCursor: String) {
|
|
559
|
+
repository(owner: "acme", name: "widgets") {
|
|
560
|
+
pullRequests(first: 100, after: $endCursor) {
|
|
561
|
+
pageInfo { hasNextPage endCursor }
|
|
562
|
+
nodes { number }
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
}`
|
|
566
|
+
interface PageInfo {
|
|
567
|
+
hasNextPage: boolean
|
|
568
|
+
endCursor: string | null
|
|
569
|
+
}
|
|
570
|
+
type Page = { repository: { pullRequests: { pageInfo: PageInfo } } }
|
|
571
|
+
const prs: unknown[] = []
|
|
572
|
+
let endCursor: string | null = null
|
|
573
|
+
for (let n = 0; n < 3; n += 1) {
|
|
574
|
+
const [data]: unknown[] = gh.graphql(query, { endCursor })
|
|
575
|
+
prs.push(...(nodesAt([data], ['repository', 'pullRequests']) ?? []))
|
|
576
|
+
const info: PageInfo = (data as Page).repository.pullRequests.pageInfo
|
|
577
|
+
if (!info.hasNextPage) break
|
|
578
|
+
endCursor = info.endCursor
|
|
579
|
+
}
|
|
580
|
+
```
|
|
581
|
+
|
|
582
|
+
The decision is in
|
|
583
|
+
[docs/planning/decisions/graphql-partial-and-page-cap.md](docs/planning/decisions/graphql-partial-and-page-cap.md).
|
|
584
|
+
|
|
521
585
|
## Repositories and search
|
|
522
586
|
|
|
523
587
|
```ts
|
|
@@ -617,7 +681,7 @@ Methods on both clients:
|
|
|
617
681
|
- Issue and label queries: `issueList`, `issueView` and `labelList`.
|
|
618
682
|
- Repository queries: `repoList` and `repoView`.
|
|
619
683
|
- Issue and label changes: `setIssueLabel`, `createLabel`, `editLabel` and `issueCommentEdit`.
|
|
620
|
-
- REST and GraphQL: `api` is a GET through `gh api` and never throws (failure is `ok: false`); `graphql` is a query and throws on a failed or partial answer
|
|
684
|
+
- REST and GraphQL: `api` is a GET through `gh api` and never throws (failure is `ok: false`); `graphql` is a query and throws on a failed or partial answer, or returns a partial answer as `{ data, errors }` pages with `partial: true`.
|
|
621
685
|
- Setup checks: `installed`, `authenticated`, `authStatus` and `ready`.
|
|
622
686
|
- `ownerRepo` reads the owner and repository from a remote URL. It starts no process.
|
|
623
687
|
- `try` and `soft`: the whole surface again as a `Result<T, GhFailure>`, and the queries again with `null` for a failure. `TriedSync`, `TriedAsync` and `Soft` are their types.
|
|
@@ -649,13 +713,15 @@ a verb before running it. Each verb takes an optional `GhTarget` last, except `r
|
|
|
649
713
|
- `pickPrForBranch`: the first row whose head branch is exactly the one you name. `prList({ head })`
|
|
650
714
|
already filters exactly. This is for rows from elsewhere, such as a `head:` search. It takes plain
|
|
651
715
|
rows or a `ListResult`, so `openPrsForBranch`'s or `prList`'s parsed answer passes straight in.
|
|
652
|
-
- `nodesAt`: one connection's nodes, joined across `graphql` pages.
|
|
716
|
+
- `nodesAt`: one connection's nodes, joined across `graphql` pages. With `{ partial: true }`, across
|
|
717
|
+
partial pages, with their errors.
|
|
653
718
|
- Field lists: `PR_REF_FIELDS` (enough to identify a PR), `PR_LIST_FIELDS` (for triage),
|
|
654
719
|
`PR_VIEW_FIELDS` (the full set), `REPO_FIELDS` and `SEARCH_PR_FIELDS`.
|
|
655
720
|
- Option and result types: `ListResult`, `PrListOptions`, `IssueListOptions`, `RepoListOptions`,
|
|
656
721
|
`SearchPrsOptions`, `CreatePrOptions`, `CreateReviewOptions`, `ReviewComment`,
|
|
657
|
-
`CreateLabelOptions`, `ApiOptions`, `ApiResponse`, `GraphqlOptions`, `
|
|
658
|
-
`
|
|
722
|
+
`CreateLabelOptions`, `ApiOptions`, `ApiResponse`, `GraphqlOptions`, `GraphqlPartialOptions`,
|
|
723
|
+
`GraphqlPage`, `GraphqlError`, `PartialNodes`, `GraphqlVariable`, `AuthStatus`, `PrMergeMethod`
|
|
724
|
+
and `ReviewThread`.
|
|
659
725
|
|
|
660
726
|
### Parsers
|
|
661
727
|
|
|
@@ -702,7 +768,8 @@ The experimental items:
|
|
|
702
768
|
- `ReviewComment`
|
|
703
769
|
- `graphql`, the verb and the method on both clients
|
|
704
770
|
- `nodesAt`
|
|
705
|
-
- `GraphqlOptions`
|
|
771
|
+
- `GraphqlOptions` and `GraphqlPartialOptions`
|
|
772
|
+
- `GraphqlPage`, `GraphqlError` and `PartialNodes`
|
|
706
773
|
- `GraphqlVariable`
|
|
707
774
|
- `fromTarget` and `FromTarget`
|
|
708
775
|
|
package/dist/async.d.ts
CHANGED
|
@@ -116,10 +116,12 @@ export declare class AsyncGh {
|
|
|
116
116
|
/** A REST GET through `gh api`. Failure is `ok: false`, never a rejection. */
|
|
117
117
|
api(endpoint: string, opts?: ApiOptions & AsyncCallOptions): Promise<ApiResponse>;
|
|
118
118
|
/**
|
|
119
|
-
* A GraphQL query. Resolves to each page's `data`. Rejects on a failed or partial result.
|
|
119
|
+
* A GraphQL query. Resolves to each page's `data`. Rejects on a failed or partial result.
|
|
120
|
+
* With `partial: true`, resolves to each page's `data` with its `errors`. See `Gh.graphql`.
|
|
120
121
|
*
|
|
121
122
|
* @experimental
|
|
122
123
|
*/
|
|
124
|
+
graphql(query: string, variables: Record<string, v.GraphqlVariable> | undefined, opts: v.GraphqlPartialOptions & AsyncCallOptions): Promise<v.GraphqlPage[]>;
|
|
123
125
|
graphql(query: string, variables?: Record<string, v.GraphqlVariable>, opts?: v.GraphqlOptions & AsyncCallOptions): Promise<unknown[]>;
|
|
124
126
|
/** Every label on the repository (up to `opts.limit`, default 1000). Rejects when gh fails. */
|
|
125
127
|
labelList(opts?: {
|
package/dist/async.js
CHANGED
|
@@ -157,11 +157,6 @@ export class AsyncGh {
|
|
|
157
157
|
async api(endpoint, opts = {}) {
|
|
158
158
|
return this.exec(v.api(endpoint, opts, this.target), opts);
|
|
159
159
|
}
|
|
160
|
-
/**
|
|
161
|
-
* A GraphQL query. Resolves to each page's `data`. Rejects on a failed or partial result. See `Gh.graphql`.
|
|
162
|
-
*
|
|
163
|
-
* @experimental
|
|
164
|
-
*/
|
|
165
160
|
async graphql(query, variables = {}, opts = {}) {
|
|
166
161
|
return this.read(v.graphql(query, variables, opts, this.target), opts);
|
|
167
162
|
}
|
package/dist/doors.d.ts
CHANGED
|
@@ -29,9 +29,10 @@ import type { GhVerb } from './verbs.ts';
|
|
|
29
29
|
* A checked verb has already thrown on a non-zero exit (the runner is
|
|
30
30
|
* {@link runCheckedGh}). A probe (`installed`, `authStatus`, `api`, `raw`)
|
|
31
31
|
* carries its failure in its value. A read that exited non-zero throws the
|
|
32
|
-
* classified failure, unless the verb's `absent` says
|
|
33
|
-
* is no such thing", which `parse` turns into `null
|
|
34
|
-
*
|
|
32
|
+
* classified failure, unless the verb's `absent` says the result is still an
|
|
33
|
+
* answer: gh's "there is no such thing", which `parse` turns into `null`, or a
|
|
34
|
+
* partial `graphql` response. A read that exited 0 but parsed to `null` could
|
|
35
|
+
* not be read completely, and throws.
|
|
35
36
|
*
|
|
36
37
|
* A result flagged `outputIncomplete` throws for every verb but a probe, as
|
|
37
38
|
* typed-git's `gate` does: gh exited, but the runner stopped reading at the
|
|
@@ -59,13 +60,47 @@ export declare function runCheckedGh(runner: CommandRunner, argv: string[], opts
|
|
|
59
60
|
export declare function runCheckedGhAsync(runner: AsyncCommandRunner, argv: string[], opts: AsyncCommandRunOptions): Promise<CommandResult>;
|
|
60
61
|
/** The members `try` and `soft` leave out: `raw`, `rawChecked`, helpers that run nothing, and properties. */
|
|
61
62
|
type Triable<C> = Omit<C, 'raw' | 'rawChecked' | 'at' | 'withRepo' | 'ownerRepo' | 'try' | 'soft' | 'cwd' | 'repo' | 'hostname'>;
|
|
63
|
+
/**
|
|
64
|
+
* A method's call signatures as `[args, return]` pairs: its last two when it
|
|
65
|
+
* is overloaded (`graphql` is, on its `partial` option), and its one
|
|
66
|
+
* signature twice when it is not. The types below map each pair, so `try` and
|
|
67
|
+
* `soft` keep both of a method's overloads.
|
|
68
|
+
*/
|
|
69
|
+
type Signatures<F> = F extends {
|
|
70
|
+
(...a: infer A1): infer R1;
|
|
71
|
+
(...a: infer A2): infer R2;
|
|
72
|
+
} ? [[A1, R1], [A2, R2]] : never;
|
|
62
73
|
/** Every synchronous client method, returning a `Result<T, GhFailure>`. */
|
|
63
74
|
export type TriedSync<C> = {
|
|
64
|
-
[K in keyof Triable<C>]: Triable<C>[K] extends
|
|
75
|
+
[K in keyof Triable<C>]: Signatures<Triable<C>[K]> extends [
|
|
76
|
+
[
|
|
77
|
+
infer A1 extends unknown[],
|
|
78
|
+
infer R1
|
|
79
|
+
],
|
|
80
|
+
[
|
|
81
|
+
infer A2 extends unknown[],
|
|
82
|
+
infer R2
|
|
83
|
+
]
|
|
84
|
+
] ? {
|
|
85
|
+
(...a: A1): Result<R1, GhFailure>;
|
|
86
|
+
(...a: A2): Result<R2, GhFailure>;
|
|
87
|
+
} : never;
|
|
65
88
|
};
|
|
66
89
|
/** Every awaitable client method, promising a `Result<T, GhFailure>`. */
|
|
67
90
|
export type TriedAsync<C> = {
|
|
68
|
-
[K in keyof Triable<C>]: Triable<C>[K] extends
|
|
91
|
+
[K in keyof Triable<C>]: Signatures<Triable<C>[K]> extends [
|
|
92
|
+
[
|
|
93
|
+
infer A1 extends unknown[],
|
|
94
|
+
Promise<infer R1>
|
|
95
|
+
],
|
|
96
|
+
[
|
|
97
|
+
infer A2 extends unknown[],
|
|
98
|
+
Promise<infer R2>
|
|
99
|
+
]
|
|
100
|
+
] ? {
|
|
101
|
+
(...a: A1): Promise<Result<R1, GhFailure>>;
|
|
102
|
+
(...a: A2): Promise<Result<R2, GhFailure>>;
|
|
103
|
+
} : never;
|
|
69
104
|
};
|
|
70
105
|
/**
|
|
71
106
|
* The queries that have a soft form. A mutation has none. If a write's failure
|
|
@@ -77,7 +112,19 @@ export type SoftMethod = (typeof SOFT_METHODS)[number];
|
|
|
77
112
|
type OrNull<R> = R extends Promise<infer U> ? Promise<U | null> : R | null;
|
|
78
113
|
/** Each queried method again, returning `null` instead of throwing. One type for both clients. */
|
|
79
114
|
export type Soft<C> = {
|
|
80
|
-
[K in SoftMethod & keyof C]: C[K] extends
|
|
115
|
+
[K in SoftMethod & keyof C]: Signatures<C[K]> extends [
|
|
116
|
+
[
|
|
117
|
+
infer A1 extends unknown[],
|
|
118
|
+
infer R1
|
|
119
|
+
],
|
|
120
|
+
[
|
|
121
|
+
infer A2 extends unknown[],
|
|
122
|
+
infer R2
|
|
123
|
+
]
|
|
124
|
+
] ? {
|
|
125
|
+
(...a: A1): OrNull<R1>;
|
|
126
|
+
(...a: A2): OrNull<R2>;
|
|
127
|
+
} : never;
|
|
81
128
|
};
|
|
82
129
|
/** The failure a caught `CommandFailed` or `FailureError` stands for. */
|
|
83
130
|
export declare function failureOf(error: {
|
package/dist/doors.js
CHANGED
|
@@ -28,9 +28,10 @@ import { classifyGhFailure, unreadableFailure } from "./failure.js";
|
|
|
28
28
|
* A checked verb has already thrown on a non-zero exit (the runner is
|
|
29
29
|
* {@link runCheckedGh}). A probe (`installed`, `authStatus`, `api`, `raw`)
|
|
30
30
|
* carries its failure in its value. A read that exited non-zero throws the
|
|
31
|
-
* classified failure, unless the verb's `absent` says
|
|
32
|
-
* is no such thing", which `parse` turns into `null
|
|
33
|
-
*
|
|
31
|
+
* classified failure, unless the verb's `absent` says the result is still an
|
|
32
|
+
* answer: gh's "there is no such thing", which `parse` turns into `null`, or a
|
|
33
|
+
* partial `graphql` response. A read that exited 0 but parsed to `null` could
|
|
34
|
+
* not be read completely, and throws.
|
|
34
35
|
*
|
|
35
36
|
* A result flagged `outputIncomplete` throws for every verb but a probe, as
|
|
36
37
|
* typed-git's `gate` does: gh exited, but the runner stopped reading at the
|
|
@@ -49,7 +50,7 @@ export function settle(verb, result, binary) {
|
|
|
49
50
|
: runFailed(result, (r) => verb.absent?.(r) === true);
|
|
50
51
|
if (failed)
|
|
51
52
|
throw ghFailureError(new CommandFailed(verb.argv, result, binary));
|
|
52
|
-
// A non-zero exit that got this far is
|
|
53
|
+
// A non-zero exit that got this far is an answer the verb's `absent` accepted.
|
|
53
54
|
if (verb.checked || result.exitCode !== 0)
|
|
54
55
|
return verb.parse(result);
|
|
55
56
|
const value = verb.parse(result);
|
package/dist/index.d.ts
CHANGED
|
@@ -23,7 +23,7 @@ export { classifyGhFailure, type GhFailure, type GhFailureKind } from './failure
|
|
|
23
23
|
export { type Soft, type TriedAsync, type TriedSync } from './doors.ts';
|
|
24
24
|
export { runVerb, runVerbAsync, type RunVerbAsyncOptions, type RunVerbOptions } from './run.ts';
|
|
25
25
|
export { Gh, defaultRunnerFactory, type GhOptions, type GhRepoOptions, type RunnerFactory, } from './sync.ts';
|
|
26
|
-
export { api, authStatus, createPr, editPr, editPrBody, fromTarget, graphql, createLabel, createReview, editLabel, installed, issueList, issueView, labelList, nodesAt, openPrsForBranch, pickPrForBranch, prBody, prComment, prSetDraft, issueCommentEdit, prDiff, prList, prMerge, prReviewThreads, prUpdateBranch, prView, raw, rawChecked, repoList, repoView, searchPrs, setIssueLabel, PR_LIST_FIELDS, PR_REF_FIELDS, PR_VIEW_FIELDS, REPO_FIELDS, SEARCH_PR_FIELDS, type ApiOptions, type ApiResponse, type AuthStatus, type CreateLabelOptions, type CreatePrOptions, type CreateReviewOptions, type FromTarget, type GhTarget, type GraphqlOptions, type GraphqlVariable, type GhVerb, type IssueListOptions, type ListResult, type PrListOptions, type PrMergeMethod, type RepoListOptions, type ReviewComment, type ReviewThread, type SearchPrsOptions, } from './verbs.ts';
|
|
26
|
+
export { api, authStatus, createPr, editPr, editPrBody, fromTarget, graphql, createLabel, createReview, editLabel, installed, issueList, issueView, labelList, nodesAt, openPrsForBranch, pickPrForBranch, prBody, prComment, prSetDraft, issueCommentEdit, prDiff, prList, prMerge, prReviewThreads, prUpdateBranch, prView, raw, rawChecked, repoList, repoView, searchPrs, setIssueLabel, PR_LIST_FIELDS, PR_REF_FIELDS, PR_VIEW_FIELDS, REPO_FIELDS, SEARCH_PR_FIELDS, type ApiOptions, type ApiResponse, type AuthStatus, type CreateLabelOptions, type CreatePrOptions, type CreateReviewOptions, type FromTarget, type GhTarget, type GraphqlError, type GraphqlOptions, type GraphqlPage, type GraphqlPartialOptions, type GraphqlVariable, type GhVerb, type IssueListOptions, type ListResult, type PartialNodes, type PrListOptions, type PrMergeMethod, type RepoListOptions, type ReviewComment, type ReviewThread, type SearchPrsOptions, } from './verbs.ts';
|
|
27
27
|
export { anyCheckFailed, normalizeAuthorLogin, parsePullRequest } from './parse.ts';
|
|
28
28
|
export { ownerRepoSlug, parseOwnerRepo, parsePrRef, parsePrUrl, type PrRef, type PullRequestRef, } from './repo.ts';
|
|
29
29
|
export type { Actor, CheckConclusion, CheckRun, Comment, IssueRow, Label, MergeableState, MergeStateStatus, OwnerRepo, PullRequest, PrSearchHit, PullRequestState, Repository, RepositoryVisibility, Review, ReviewDecision, ReviewState, } from './types.ts';
|
package/dist/sync.d.ts
CHANGED
|
@@ -202,10 +202,15 @@ export declare class Gh {
|
|
|
202
202
|
}): ApiResponse;
|
|
203
203
|
/**
|
|
204
204
|
* A GraphQL query. Returns each page's `data`. Throws on a failed or partial
|
|
205
|
-
* result. `soft.graphql` gives null.
|
|
205
|
+
* result. `soft.graphql` gives null. With `partial: true`, returns each
|
|
206
|
+
* page's `data` with its `errors`, and throws only when there is no data to
|
|
207
|
+
* read. See the `graphql` verb.
|
|
206
208
|
*
|
|
207
209
|
* @experimental
|
|
208
210
|
*/
|
|
211
|
+
graphql(query: string, variables: Record<string, v.GraphqlVariable> | undefined, opts: v.GraphqlPartialOptions & {
|
|
212
|
+
timeoutMs?: number | undefined;
|
|
213
|
+
}): v.GraphqlPage[];
|
|
209
214
|
graphql(query: string, variables?: Record<string, v.GraphqlVariable>, opts?: v.GraphqlOptions & {
|
|
210
215
|
timeoutMs?: number | undefined;
|
|
211
216
|
}): unknown[];
|
package/dist/sync.js
CHANGED
|
@@ -225,12 +225,6 @@ export class Gh {
|
|
|
225
225
|
api(endpoint, opts = {}) {
|
|
226
226
|
return this.exec(v.api(endpoint, opts, this.target), opts.timeoutMs);
|
|
227
227
|
}
|
|
228
|
-
/**
|
|
229
|
-
* A GraphQL query. Returns each page's `data`. Throws on a failed or partial
|
|
230
|
-
* result. `soft.graphql` gives null. See the `graphql` verb.
|
|
231
|
-
*
|
|
232
|
-
* @experimental
|
|
233
|
-
*/
|
|
234
228
|
graphql(query, variables = {}, opts = {}) {
|
|
235
229
|
return this.read(v.graphql(query, variables, opts, this.target), opts.timeoutMs);
|
|
236
230
|
}
|
package/dist/verbs.d.ts
CHANGED
|
@@ -42,10 +42,13 @@ export interface GhVerb<T> {
|
|
|
42
42
|
*/
|
|
43
43
|
probe?: boolean | undefined;
|
|
44
44
|
/**
|
|
45
|
-
*
|
|
46
|
-
* thing (`prView`, `issueView`, `repoView`)
|
|
47
|
-
*
|
|
48
|
-
*
|
|
45
|
+
* Whether a non-zero result is still an answer. For a read of one thing, it
|
|
46
|
+
* says GitHub has no such thing (`prView`, `issueView`, `repoView`), and
|
|
47
|
+
* `parse` makes `null` of it, because a missing item is an answer. For
|
|
48
|
+
* `graphql` with `partial`, it says gh exited 1 over a response that
|
|
49
|
+
* carries `errors` alongside its `data`, and `parse` reads the pages. The
|
|
50
|
+
* plain method returns what `parse` makes of it. Other non-zero results
|
|
51
|
+
* fail the call.
|
|
49
52
|
*/
|
|
50
53
|
absent?: ((result: CommandResult) => boolean) | undefined;
|
|
51
54
|
parse(result: CommandResult): T;
|
|
@@ -261,9 +264,59 @@ export interface GraphqlOptions {
|
|
|
261
264
|
/**
|
|
262
265
|
* Follow every page (`--paginate --slurp`). The query must take
|
|
263
266
|
* `$endCursor: String` and select `pageInfo { hasNextPage endCursor }` on
|
|
264
|
-
* the one connection it pages through. gh follows only that connection
|
|
267
|
+
* the one connection it pages through. gh follows only that connection, to
|
|
268
|
+
* its end. There is no page cap: to stop early, page yourself (see the
|
|
269
|
+
* README's "Reading every page").
|
|
265
270
|
*/
|
|
266
271
|
paginate?: boolean | undefined;
|
|
272
|
+
/**
|
|
273
|
+
* Unset or `false`: a page with `errors` fails the call. Pass
|
|
274
|
+
* {@link GraphqlPartialOptions} to read partial pages instead.
|
|
275
|
+
*/
|
|
276
|
+
partial?: false | undefined;
|
|
277
|
+
}
|
|
278
|
+
/**
|
|
279
|
+
* Options for {@link graphql} that read a partial answer: each page's `data`
|
|
280
|
+
* with the `errors` that came with it.
|
|
281
|
+
*
|
|
282
|
+
* @experimental
|
|
283
|
+
*/
|
|
284
|
+
export interface GraphqlPartialOptions extends Omit<GraphqlOptions, 'partial'> {
|
|
285
|
+
/**
|
|
286
|
+
* Return each page as a {@link GraphqlPage}, so a page whose `data` came
|
|
287
|
+
* back with `errors` is still read. GitHub answers this way when part of a
|
|
288
|
+
* query fails: an aliased pull request that was deleted is `null` in
|
|
289
|
+
* `data`, and an error names its path.
|
|
290
|
+
*/
|
|
291
|
+
partial: true;
|
|
292
|
+
}
|
|
293
|
+
/**
|
|
294
|
+
* One entry of a GraphQL response's `errors`.
|
|
295
|
+
*
|
|
296
|
+
* @experimental
|
|
297
|
+
*/
|
|
298
|
+
export interface GraphqlError {
|
|
299
|
+
/** What GitHub said went wrong. `''` when the error had no message. */
|
|
300
|
+
readonly message: string;
|
|
301
|
+
/** GitHub's error type, such as `NOT_FOUND` or `FORBIDDEN`. `null` when it gave none. */
|
|
302
|
+
readonly type: string | null;
|
|
303
|
+
/**
|
|
304
|
+
* Where in `data` the error happened: field names (an alias, when the
|
|
305
|
+
* query used one) and list indexes, from the root. Empty when the error
|
|
306
|
+
* names no path, as one about the whole query does.
|
|
307
|
+
*/
|
|
308
|
+
readonly path: readonly (string | number)[];
|
|
309
|
+
}
|
|
310
|
+
/**
|
|
311
|
+
* One page of a partial {@link graphql} answer: the page's `data`, and the
|
|
312
|
+
* `errors` that came with it. `errors` is empty for a page that came back
|
|
313
|
+
* whole. A field the errors name is `null` in `data`.
|
|
314
|
+
*
|
|
315
|
+
* @experimental
|
|
316
|
+
*/
|
|
317
|
+
export interface GraphqlPage {
|
|
318
|
+
readonly data: unknown;
|
|
319
|
+
readonly errors: readonly GraphqlError[];
|
|
267
320
|
}
|
|
268
321
|
/**
|
|
269
322
|
* `gh api graphql -f query=… [--paginate --slurp]`: a GraphQL query.
|
|
@@ -273,6 +326,16 @@ export interface GraphqlOptions {
|
|
|
273
326
|
* carries `errors` or lacks `data`. A partial answer counts as a failure. Read one
|
|
274
327
|
* connection across the pages with {@link nodesAt}.
|
|
275
328
|
*
|
|
329
|
+
* With `partial: true` ({@link GraphqlPartialOptions}), returns each page as
|
|
330
|
+
* a {@link GraphqlPage}, its `data` with its `errors`, so a batch of aliased
|
|
331
|
+
* lookups keeps the ones that worked. gh exits 1 for a response with
|
|
332
|
+
* `errors`; the verb declares that result an answer (`absent`), so the
|
|
333
|
+
* methods that throw return it. It is still null when the output did not
|
|
334
|
+
* parse, a page has no `data` object, gh failed for another reason, or
|
|
335
|
+
* `paginate` stopped at a page with errors that says it has a next page. gh
|
|
336
|
+
* stops at the first page with errors, so the pages after it were never
|
|
337
|
+
* fetched.
|
|
338
|
+
*
|
|
276
339
|
* A {@link fromTarget} variable is filled from the target's repo, or by gh
|
|
277
340
|
* from the clone. A plain string is sent as written. A document holding any mutation or
|
|
278
341
|
* subscription throws `RangeError`, as does a variable named `query` or
|
|
@@ -281,16 +344,41 @@ export interface GraphqlOptions {
|
|
|
281
344
|
*
|
|
282
345
|
* @experimental
|
|
283
346
|
*/
|
|
347
|
+
export declare function graphql(query: string, variables: Record<string, GraphqlVariable> | undefined, opts: GraphqlPartialOptions, target?: GhTarget): GhVerb<GraphqlPage[] | null>;
|
|
284
348
|
export declare function graphql(query: string, variables?: Record<string, GraphqlVariable>, opts?: GraphqlOptions, target?: GhTarget): GhVerb<unknown[] | null>;
|
|
349
|
+
export declare function graphql(query: string, variables: Record<string, GraphqlVariable> | undefined, opts: GraphqlOptions | GraphqlPartialOptions, target?: GhTarget): GhVerb<unknown[] | GraphqlPage[] | null>;
|
|
285
350
|
/**
|
|
286
351
|
* The `nodes` of one connection, joined across {@link graphql} pages. It
|
|
287
352
|
* follows `path` into each page and takes its `nodes` array. Null when `pages` is
|
|
288
353
|
* null, or any page lacks the path or a `nodes` array, so a page that came
|
|
289
354
|
* back short is never read as fewer rows.
|
|
290
355
|
*
|
|
356
|
+
* With `{ partial: true }`, it reads the {@link GraphqlPage}s of a partial
|
|
357
|
+
* `graphql` call. It follows `path` into each page's `data`, and returns the
|
|
358
|
+
* joined `nodes` with every error of every page, as {@link PartialNodes}. A
|
|
359
|
+
* node that failed is `null` among the nodes, and its error's `path` names
|
|
360
|
+
* it. It is null on the same terms as the plain form: a connection that is
|
|
361
|
+
* itself `null` because of an error has no nodes to give, and the errors are
|
|
362
|
+
* on the pages.
|
|
363
|
+
*
|
|
291
364
|
* @experimental
|
|
292
365
|
*/
|
|
293
|
-
export declare function nodesAt(pages:
|
|
366
|
+
export declare function nodesAt(pages: readonly GraphqlPage[] | null, path: readonly string[], opts: {
|
|
367
|
+
partial: true;
|
|
368
|
+
}): PartialNodes | null;
|
|
369
|
+
export declare function nodesAt(pages: readonly unknown[] | null, path: readonly string[]): unknown[] | null;
|
|
370
|
+
/**
|
|
371
|
+
* One connection's nodes across partial {@link graphql} pages, and the errors
|
|
372
|
+
* that came with them. What {@link nodesAt} returns with `{ partial: true }`.
|
|
373
|
+
*
|
|
374
|
+
* @experimental
|
|
375
|
+
*/
|
|
376
|
+
export interface PartialNodes {
|
|
377
|
+
/** The joined `nodes`. A node GitHub could not resolve is `null`. */
|
|
378
|
+
readonly nodes: unknown[];
|
|
379
|
+
/** Every error of every page, in page order, including any outside `path`. */
|
|
380
|
+
readonly errors: readonly GraphqlError[];
|
|
381
|
+
}
|
|
294
382
|
/** `gh --version`: checks that `gh` is installed. True when `gh` runs. */
|
|
295
383
|
export declare function installed(): GhVerb<boolean>;
|
|
296
384
|
/**
|
package/dist/verbs.js
CHANGED
|
@@ -428,6 +428,87 @@ export function fromTarget(part) {
|
|
|
428
428
|
}
|
|
429
429
|
return Object.freeze({ fromTarget: part });
|
|
430
430
|
}
|
|
431
|
+
/** One `errors` entry, read leniently: a string is its message, anything else counts with none. */
|
|
432
|
+
function readGraphqlError(entry) {
|
|
433
|
+
if (typeof entry === 'string')
|
|
434
|
+
return { message: entry, type: null, path: [] };
|
|
435
|
+
if (entry === null || typeof entry !== 'object')
|
|
436
|
+
return { message: '', type: null, path: [] };
|
|
437
|
+
const rec = entry;
|
|
438
|
+
const path = rec['path'];
|
|
439
|
+
const wellFormed = Array.isArray(path) && path.every((p) => typeof p === 'string' || typeof p === 'number');
|
|
440
|
+
return {
|
|
441
|
+
message: typeof rec['message'] === 'string' ? rec['message'] : '',
|
|
442
|
+
type: typeof rec['type'] === 'string' ? rec['type'] : null,
|
|
443
|
+
path: wellFormed ? path : [],
|
|
444
|
+
};
|
|
445
|
+
}
|
|
446
|
+
/**
|
|
447
|
+
* A page's `errors`. None when the field is missing or `null`. gh counts a
|
|
448
|
+
* string as an error, as an array of entries is, so both are read here.
|
|
449
|
+
*/
|
|
450
|
+
function readGraphqlErrors(errors) {
|
|
451
|
+
if (errors === undefined || errors === null)
|
|
452
|
+
return [];
|
|
453
|
+
return Array.isArray(errors) ? errors.map(readGraphqlError) : [readGraphqlError(errors)];
|
|
454
|
+
}
|
|
455
|
+
/**
|
|
456
|
+
* True when anything in `value` says `hasNextPage: true`. gh's own test for
|
|
457
|
+
* another page reads the first `pageInfo` it meets. This one reads every
|
|
458
|
+
* `hasNextPage`, so it says yes whenever gh would have, and sometimes when it
|
|
459
|
+
* would not have.
|
|
460
|
+
*/
|
|
461
|
+
function mentionsNextPage(value) {
|
|
462
|
+
if (Array.isArray(value))
|
|
463
|
+
return value.some(mentionsNextPage);
|
|
464
|
+
if (value === null || typeof value !== 'object')
|
|
465
|
+
return false;
|
|
466
|
+
const rec = value;
|
|
467
|
+
if (rec['hasNextPage'] === true)
|
|
468
|
+
return true;
|
|
469
|
+
return Object.values(rec).some(mentionsNextPage);
|
|
470
|
+
}
|
|
471
|
+
/**
|
|
472
|
+
* Read `gh api graphql` output as partial pages, or `null` when it is no
|
|
473
|
+
* answer. Every page must be an object whose `data` is an object.
|
|
474
|
+
*
|
|
475
|
+
* gh exits 1 when a response carries `errors`, after printing it. So a
|
|
476
|
+
* non-zero exit is an answer only when the last page has errors, which is
|
|
477
|
+
* why gh stopped. Any other non-zero exit is a failure that left some output
|
|
478
|
+
* behind, such as earlier pages before a network error. With `paginate`, gh
|
|
479
|
+
* also stops at the first page with errors. When that page says there was a
|
|
480
|
+
* next one, the pages after it were never fetched, so the answer is short and
|
|
481
|
+
* is not read.
|
|
482
|
+
*/
|
|
483
|
+
function readPartialPages(r, paginate) {
|
|
484
|
+
let body;
|
|
485
|
+
try {
|
|
486
|
+
body = JSON.parse(r.stdout);
|
|
487
|
+
}
|
|
488
|
+
catch {
|
|
489
|
+
return null;
|
|
490
|
+
}
|
|
491
|
+
const pages = paginate ? body : [body];
|
|
492
|
+
if (!Array.isArray(pages))
|
|
493
|
+
return null;
|
|
494
|
+
const out = [];
|
|
495
|
+
for (const page of pages) {
|
|
496
|
+
if (page === null || typeof page !== 'object')
|
|
497
|
+
return null;
|
|
498
|
+
const rec = page;
|
|
499
|
+
if (rec['data'] === null || typeof rec['data'] !== 'object')
|
|
500
|
+
return null;
|
|
501
|
+
out.push({ data: rec['data'], errors: readGraphqlErrors(rec['errors']) });
|
|
502
|
+
}
|
|
503
|
+
if (r.exitCode !== 0) {
|
|
504
|
+
const last = out.at(-1);
|
|
505
|
+
if (last === undefined || last.errors.length === 0)
|
|
506
|
+
return null;
|
|
507
|
+
if (paginate && mentionsNextPage(last.data))
|
|
508
|
+
return null;
|
|
509
|
+
}
|
|
510
|
+
return out;
|
|
511
|
+
}
|
|
431
512
|
/**
|
|
432
513
|
* True when any operation in a GraphQL document is a `mutation` or a
|
|
433
514
|
* `subscription`. Strings and comments are dropped first, and only the words
|
|
@@ -449,22 +530,6 @@ function writesOrStreams(document) {
|
|
|
449
530
|
}
|
|
450
531
|
return false;
|
|
451
532
|
}
|
|
452
|
-
/**
|
|
453
|
-
* `gh api graphql -f query=… [--paginate --slurp]`: a GraphQL query.
|
|
454
|
-
*
|
|
455
|
-
* Returns each page's `data`, one entry per page (one without `paginate`).
|
|
456
|
-
* Returns null when the call failed, the output did not parse, or any page
|
|
457
|
-
* carries `errors` or lacks `data`. A partial answer counts as a failure. Read one
|
|
458
|
-
* connection across the pages with {@link nodesAt}.
|
|
459
|
-
*
|
|
460
|
-
* A {@link fromTarget} variable is filled from the target's repo, or by gh
|
|
461
|
-
* from the clone. A plain string is sent as written. A document holding any mutation or
|
|
462
|
-
* subscription throws `RangeError`, as does a variable named `query` or
|
|
463
|
-
* `operationName`, which gh would send as the request's own fields. This is a
|
|
464
|
-
* read, and changes go through `raw`.
|
|
465
|
-
*
|
|
466
|
-
* @experimental
|
|
467
|
-
*/
|
|
468
533
|
export function graphql(query, variables = {}, opts = {}, target) {
|
|
469
534
|
checkTarget(target);
|
|
470
535
|
if (writesOrStreams(query)) {
|
|
@@ -505,6 +570,25 @@ export function graphql(query, variables = {}, opts = {}, target) {
|
|
|
505
570
|
}
|
|
506
571
|
}
|
|
507
572
|
argv.push('-f', `query=${query}`);
|
|
573
|
+
if (opts.partial === true) {
|
|
574
|
+
// `settle` calls `absent` and then `parse` on the same result, so each
|
|
575
|
+
// result's output is parsed once and kept here.
|
|
576
|
+
const read = new WeakMap();
|
|
577
|
+
const pagesOf = (r) => {
|
|
578
|
+
if (!read.has(r))
|
|
579
|
+
read.set(r, readPartialPages(r, paginate));
|
|
580
|
+
return read.get(r) ?? null;
|
|
581
|
+
};
|
|
582
|
+
return {
|
|
583
|
+
argv,
|
|
584
|
+
checked: false,
|
|
585
|
+
readOnly: true,
|
|
586
|
+
// A response with errors exits 1. When it still holds the pages, it is
|
|
587
|
+
// the answer, not a failure.
|
|
588
|
+
absent: (r) => pagesOf(r) !== null,
|
|
589
|
+
parse: pagesOf,
|
|
590
|
+
};
|
|
591
|
+
}
|
|
508
592
|
return {
|
|
509
593
|
argv,
|
|
510
594
|
checked: false,
|
|
@@ -537,20 +621,25 @@ export function graphql(query, variables = {}, opts = {}, target) {
|
|
|
537
621
|
},
|
|
538
622
|
};
|
|
539
623
|
}
|
|
540
|
-
|
|
541
|
-
* The `nodes` of one connection, joined across {@link graphql} pages. It
|
|
542
|
-
* follows `path` into each page and takes its `nodes` array. Null when `pages` is
|
|
543
|
-
* null, or any page lacks the path or a `nodes` array, so a page that came
|
|
544
|
-
* back short is never read as fewer rows.
|
|
545
|
-
*
|
|
546
|
-
* @experimental
|
|
547
|
-
*/
|
|
548
|
-
export function nodesAt(pages, path) {
|
|
624
|
+
export function nodesAt(pages, path, opts = {}) {
|
|
549
625
|
if (pages === null)
|
|
550
626
|
return null;
|
|
551
|
-
const
|
|
627
|
+
const partial = opts.partial === true;
|
|
628
|
+
const nodes = [];
|
|
629
|
+
const errors = [];
|
|
552
630
|
for (const page of pages) {
|
|
553
631
|
let at = page;
|
|
632
|
+
if (partial) {
|
|
633
|
+
if (page === null || typeof page !== 'object')
|
|
634
|
+
return null;
|
|
635
|
+
const { data, errors: pageErrors } = page;
|
|
636
|
+
if (!Array.isArray(pageErrors))
|
|
637
|
+
return null;
|
|
638
|
+
// Read each entry as `graphql` does, so a hand-built page cannot pass
|
|
639
|
+
// a malformed error through as a GraphqlError.
|
|
640
|
+
errors.push(...pageErrors.map(readGraphqlError));
|
|
641
|
+
at = data;
|
|
642
|
+
}
|
|
554
643
|
for (const key of path) {
|
|
555
644
|
if (at === null || typeof at !== 'object')
|
|
556
645
|
return null;
|
|
@@ -558,12 +647,12 @@ export function nodesAt(pages, path) {
|
|
|
558
647
|
}
|
|
559
648
|
if (at === null || typeof at !== 'object')
|
|
560
649
|
return null;
|
|
561
|
-
const
|
|
562
|
-
if (!Array.isArray(
|
|
650
|
+
const found = at['nodes'];
|
|
651
|
+
if (!Array.isArray(found))
|
|
563
652
|
return null;
|
|
564
|
-
|
|
653
|
+
nodes.push(...found);
|
|
565
654
|
}
|
|
566
|
-
return
|
|
655
|
+
return partial ? { nodes, errors } : nodes;
|
|
567
656
|
}
|
|
568
657
|
/** Split one header block off the front of `text`: the block, and what follows its blank line. */
|
|
569
658
|
function takeHeaderBlock(text) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@osdlc/typed-gh",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.17.0",
|
|
4
4
|
"description": "A typed GitHub client for TypeScript over the gh CLI. A total PullRequest/Review/CheckRun model, pure argv builders and parsers, and a tri-modal client — queries return parsed values, mutations throw, raw() is the escape hatch. Uses gh's own credentials; reads no token.",
|
|
5
5
|
"license": "MIT OR Apache-2.0",
|
|
6
6
|
"author": "sksizer",
|
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
],
|
|
28
28
|
"dependencies": {
|
|
29
29
|
"@osdlc/command-seam": "^0.14.0",
|
|
30
|
-
"@osdlc/typed-git": "^0.
|
|
30
|
+
"@osdlc/typed-git": "^0.13.0",
|
|
31
31
|
"@osdlc/result": "^0.1.0"
|
|
32
32
|
},
|
|
33
33
|
"engines": {
|