process-graph 1.0.5 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +85 -17
- package/dist/bin.d.ts +1 -0
- package/dist/bin.js +92 -0
- package/dist/cli.d.ts +512 -0
- package/dist/cli.js +3 -0
- package/dist/control-aDk9560d.js +294 -0
- package/dist/detail-panel-B1ZmMuJT.js +341 -0
- package/dist/dist-BpCBmnMl.js +582 -0
- package/dist/dist-HLlW8vgT.js +1388 -0
- package/dist/dist-IjrgWhHQ.js +3 -0
- package/dist/errors-BZe5WzCn.js +19 -0
- package/dist/exit-codes-0ultCqdS.js +23 -0
- package/dist/graphql-C1j5wQFz.d.ts +631 -0
- package/dist/index-CCfe0csl.d.ts +353 -0
- package/dist/index-CmZOswkx.d.ts +512 -0
- package/dist/index-CoA6vXqe.d.ts +404 -0
- package/dist/index-CptUHq6F.d.ts +58 -0
- package/dist/index-CrM5qWUA.d.ts +110 -0
- package/dist/index-DhbvDgK4.d.ts +1582 -0
- package/dist/index-DricKcyl.d.ts +421 -0
- package/dist/index.d.ts +111 -4
- package/dist/index.js +929 -5
- package/dist/layout-CCM-KBdp.js +307 -0
- package/dist/layout.d.ts +8 -0
- package/dist/layout.js +2 -0
- package/dist/node-AchI1T-K.js +40 -0
- package/dist/open-CyRXJ4bt.js +29 -0
- package/dist/parse-CuipC_2K.js +41 -0
- package/dist/queries-CCexJAE2.js +2202 -0
- package/dist/react-flow/style.css +658 -0
- package/dist/react-flow.d.ts +173 -0
- package/dist/react-flow.js +450 -0
- package/dist/react.d.ts +2 -0
- package/dist/react.js +107 -0
- package/dist/rolldown-runtime-D7D4PA-g.js +13 -0
- package/dist/router-D4gtTlXm.js +188 -0
- package/dist/schema-CVnS6HiX.js +234 -0
- package/dist/selection-BW_xXWOy.js +314 -0
- package/dist/source/github/node.d.ts +16 -0
- package/dist/source/github/node.js +2 -0
- package/dist/source/github.d.ts +49 -0
- package/dist/source/github.js +201 -0
- package/dist/source-DRDpEA8Q.js +1316 -0
- package/dist/status-Bm1CGu9C.js +11 -0
- package/dist/store/node.d.ts +24 -0
- package/dist/store/node.js +2 -0
- package/dist/store/wasm/worker.d.ts +16 -0
- package/dist/store/wasm/worker.js +98 -0
- package/dist/store/wasm.d.ts +9 -0
- package/dist/store/wasm.js +11 -0
- package/dist/store-BcR70CaO.js +1287 -0
- package/dist/store.d.ts +1328 -0
- package/dist/store.js +238 -0
- package/dist/ui/style.css +351 -0
- package/dist/ui.d.ts +235 -0
- package/dist/ui.js +2 -0
- package/dist/validate-DF3mQZww.js +125 -0
- package/package.json +148 -13
|
@@ -0,0 +1,1316 @@
|
|
|
1
|
+
import { r as overlaySchema, t as modelSchema } from "./schema-CVnS6HiX.js";
|
|
2
|
+
import { t as GraphSourceError } from "./errors-BZe5WzCn.js";
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
import { eventIterator, oc } from "@orpc/contract";
|
|
5
|
+
import { withEventMeta } from "@orpc/client";
|
|
6
|
+
import { implement } from "@orpc/server";
|
|
7
|
+
//#region ../../core/graph/source-contract/dist/schema.js
|
|
8
|
+
/**
|
|
9
|
+
* The model's own record schemas, taken from `modelSchema` rather than restated, so a page of a sync is exactly a slice of a model and can never drift from it.
|
|
10
|
+
*/
|
|
11
|
+
const modelShape = modelSchema.unwrap().shape;
|
|
12
|
+
/** A participant as the model declares it: an organisation, a repository, a person. */
|
|
13
|
+
const participantSchema = modelShape.participants.unwrap().element;
|
|
14
|
+
/** A transition as the model declares it. A source emits one per dependency, from the prerequisite to the work that waits on it. */
|
|
15
|
+
const transitionSchema = modelShape.transitions.unwrap().element;
|
|
16
|
+
/** The model's `activity` element: the first variant of its element union, pinned by a type test so a reordering of the union is a compile error. */
|
|
17
|
+
const [activityElementSchema] = modelShape.elements.unwrap().element.options;
|
|
18
|
+
/**
|
|
19
|
+
* An activity bound to the external record it stands for. `source` is required here, where the model leaves it optional, and it equals `id`: the source-qualified identifier (`github:org/repo#12`) is the element's identity, so the same record synced twice, or reached from two places, is the same element.
|
|
20
|
+
*/
|
|
21
|
+
const boundActivitySchema = activityElementSchema.unwrap().extend({ source: z.string().min(1) }).refine((activity) => activity.id === activity.source, {
|
|
22
|
+
message: "a bound activity's id must equal its source",
|
|
23
|
+
path: ["id"]
|
|
24
|
+
}).readonly();
|
|
25
|
+
/** The relation kinds a source reports beside the model's transitions. */
|
|
26
|
+
const relationKinds = [
|
|
27
|
+
"child-of",
|
|
28
|
+
"assigned-to",
|
|
29
|
+
"part-of"
|
|
30
|
+
];
|
|
31
|
+
/**
|
|
32
|
+
* A structural link between two identifiers that the model has no field for: `child-of` from a sub-task activity to its parent activity, `assigned-to` from an activity to a person participant, `part-of` from one participant to the participant that contains it (a repository to its organisation). Relations carry no ordering, so a consumer decides whether a parent waits on its children.
|
|
33
|
+
*/
|
|
34
|
+
const relationSchema = z.strictObject({
|
|
35
|
+
kind: z.enum(relationKinds),
|
|
36
|
+
from: z.string().min(1),
|
|
37
|
+
to: z.string().min(1)
|
|
38
|
+
}).readonly();
|
|
39
|
+
/** Why part of a requested scope is missing from a sync. */
|
|
40
|
+
const gapReasons = ["not-found", "forbidden"];
|
|
41
|
+
/**
|
|
42
|
+
* A part of the requested scope that the source could not read, reported as data so the rest of the sync still arrives. `target` is the source-qualified identifier of what is missing: a scope, a record, or one of a record's connections.
|
|
43
|
+
*/
|
|
44
|
+
const gapSchema = z.strictObject({
|
|
45
|
+
target: z.string().min(1),
|
|
46
|
+
reason: z.enum(gapReasons),
|
|
47
|
+
message: z.string()
|
|
48
|
+
}).readonly();
|
|
49
|
+
/**
|
|
50
|
+
* One page of a sync: bound activities, the participants they name, the transitions between them, their relations, their run state, and any gaps. A page is a partial view; `assembleSync` merges a whole sync into one model.
|
|
51
|
+
*
|
|
52
|
+
* `next` is the cursor to resume after this page. It is absent on the last page.
|
|
53
|
+
*/
|
|
54
|
+
const syncPageSchema = z.strictObject({
|
|
55
|
+
participants: z.array(participantSchema).readonly(),
|
|
56
|
+
activities: z.array(boundActivitySchema).readonly(),
|
|
57
|
+
transitions: z.array(transitionSchema).readonly(),
|
|
58
|
+
relations: z.array(relationSchema).readonly(),
|
|
59
|
+
overlay: overlaySchema,
|
|
60
|
+
gaps: z.array(gapSchema).readonly(),
|
|
61
|
+
next: z.string().min(1).exactOptional()
|
|
62
|
+
}).readonly();
|
|
63
|
+
/** The run states a sync filter can select by: whether the work is still open or done. */
|
|
64
|
+
const filterStates = ["open", "done"];
|
|
65
|
+
/** What a sync filter can order the listed records by: when they were created, when they last changed, or how much discussion they have. */
|
|
66
|
+
const orderFields = [
|
|
67
|
+
"created",
|
|
68
|
+
"updated",
|
|
69
|
+
"comments"
|
|
70
|
+
];
|
|
71
|
+
/** Which way a sync filter orders the listed records. */
|
|
72
|
+
const orderDirections$1 = ["ascending", "descending"];
|
|
73
|
+
/**
|
|
74
|
+
* Narrows and orders the records each scope lists, applied on the source's side where the source supports it. Every field is optional, and an absent field does not narrow.
|
|
75
|
+
*
|
|
76
|
+
* - `states` keeps records whose run state is one of those given.
|
|
77
|
+
* - `labels` keeps records carrying at least one of the labels given.
|
|
78
|
+
* - `order` is the order the records are listed in; absent, the source's own stable order.
|
|
79
|
+
*
|
|
80
|
+
* A filter narrows what a scope lists, not what the listed records reach: a blocker of a listed record is read whatever its own state or labels, because it is a dependency of work the filter kept.
|
|
81
|
+
*/
|
|
82
|
+
const syncFilterSchema = z.strictObject({
|
|
83
|
+
states: z.array(z.enum(filterStates)).min(1).readonly().exactOptional(),
|
|
84
|
+
labels: z.array(z.string().min(1)).min(1).readonly().exactOptional(),
|
|
85
|
+
order: z.strictObject({
|
|
86
|
+
field: z.enum(orderFields),
|
|
87
|
+
direction: z.enum(orderDirections$1)
|
|
88
|
+
}).readonly().exactOptional()
|
|
89
|
+
}).readonly();
|
|
90
|
+
/**
|
|
91
|
+
* What to sync and where to resume. `scope` names what to read as source-qualified scope identifiers (for GitHub, `github:org` for every repository an owner has, or `github:org/repo`), read in the order given. `after` is a cursor from an earlier page's `next`: the sync continues with the page that followed it. A cursor is valid only with the scope list and the filter it was issued for.
|
|
92
|
+
*
|
|
93
|
+
* `filter` narrows and orders what each scope lists (see `syncFilterSchema`).
|
|
94
|
+
*/
|
|
95
|
+
const syncInputSchema = z.strictObject({
|
|
96
|
+
scope: z.array(z.string().min(1)).min(1).readonly(),
|
|
97
|
+
after: z.string().min(1).exactOptional(),
|
|
98
|
+
filter: syncFilterSchema.exactOptional()
|
|
99
|
+
}).readonly();
|
|
100
|
+
/**
|
|
101
|
+
* A scope a person can choose to sync, as a picker lists it. `scope` is the scope identifier to pass to `sync` (for GitHub, `github:org` for an organisation, `github:org/repo` for a repository), `label` its display name, `description` the source's own description when it has one, `url` where it lives in the source's own interface, and `archived` whether the source has archived it.
|
|
102
|
+
*/
|
|
103
|
+
const scopeEntrySchema = z.strictObject({
|
|
104
|
+
scope: z.string().min(1),
|
|
105
|
+
label: z.string(),
|
|
106
|
+
description: z.string().exactOptional(),
|
|
107
|
+
url: z.url().exactOptional(),
|
|
108
|
+
archived: z.boolean()
|
|
109
|
+
}).readonly();
|
|
110
|
+
/**
|
|
111
|
+
* One page of a scope listing. `entries` are in the source's stable listing order, `gaps` name a parent the source could not read (it does not exist, or the credentials cannot see it), and `next` is the cursor to resume after this page, absent on the last.
|
|
112
|
+
*/
|
|
113
|
+
const scopeListingSchema = z.strictObject({
|
|
114
|
+
entries: z.array(scopeEntrySchema).readonly(),
|
|
115
|
+
gaps: z.array(gapSchema).readonly(),
|
|
116
|
+
next: z.string().min(1).exactOptional()
|
|
117
|
+
}).readonly();
|
|
118
|
+
/** Where a listing of the top-level scopes resumes: a cursor from an earlier page's `next`. */
|
|
119
|
+
const scopeRootsInputSchema = z.strictObject({ after: z.string().min(1).exactOptional() }).readonly();
|
|
120
|
+
/** The scope whose child scopes to list (for GitHub, the repositories of `github:owner`), and where to resume. A cursor is valid only for the parent it was issued for. */
|
|
121
|
+
const scopeChildrenInputSchema = z.strictObject({
|
|
122
|
+
scope: z.string().min(1),
|
|
123
|
+
after: z.string().min(1).exactOptional()
|
|
124
|
+
}).readonly();
|
|
125
|
+
/** What a source says about itself. `source` is the prefix of every identifier it emits, before the colon. */
|
|
126
|
+
const sourceDescriptionSchema = z.strictObject({ source: z.string().regex(/^[a-z][a-z0-9-]*$/u) }).readonly();
|
|
127
|
+
/** Why a request was rejected as malformed. */
|
|
128
|
+
const badRequestReasons = ["invalid-cursor", "invalid-scope"];
|
|
129
|
+
//#endregion
|
|
130
|
+
//#region ../../core/graph/source-contract/dist/contract.js
|
|
131
|
+
/**
|
|
132
|
+
* The typed errors every procedure can end with, each carrying its detail as data. The codes are oRPC's common codes, so an HTTP transport answers with the matching status (401, 429, 400) without any per-transport mapping.
|
|
133
|
+
*/
|
|
134
|
+
const graphSourceErrors = {
|
|
135
|
+
UNAUTHORIZED: {
|
|
136
|
+
message: "The source rejected the credentials.",
|
|
137
|
+
data: z.strictObject({})
|
|
138
|
+
},
|
|
139
|
+
TOO_MANY_REQUESTS: {
|
|
140
|
+
message: "The source is rate limiting requests.",
|
|
141
|
+
data: z.strictObject({ resetAt: z.iso.datetime().optional() })
|
|
142
|
+
},
|
|
143
|
+
BAD_REQUEST: {
|
|
144
|
+
message: "The cursor or a scope identifier is not valid for this source.",
|
|
145
|
+
data: z.strictObject({
|
|
146
|
+
reason: z.enum(badRequestReasons),
|
|
147
|
+
value: z.string()
|
|
148
|
+
})
|
|
149
|
+
}
|
|
150
|
+
};
|
|
151
|
+
/**
|
|
152
|
+
* The scope listings' errors: every procedure's, and `NOT_IMPLEMENTED` for a source that offers no listing, which oRPC's common code answers with HTTP 501 over HTTP.
|
|
153
|
+
*/
|
|
154
|
+
const scopeListingErrors = {
|
|
155
|
+
...graphSourceErrors,
|
|
156
|
+
NOT_IMPLEMENTED: {
|
|
157
|
+
message: "The source does not list its scopes.",
|
|
158
|
+
data: z.strictObject({})
|
|
159
|
+
}
|
|
160
|
+
};
|
|
161
|
+
/** The event iterator of pages `sync.stream` returns, typed by name so declarations stay small. */
|
|
162
|
+
const syncPageEvents = eventIterator(syncPageSchema);
|
|
163
|
+
/** The `GraphSource` port: an oRPC router contract, keyed by version. */
|
|
164
|
+
const graphSourceContract = { v1: oc.errors(graphSourceErrors).router({
|
|
165
|
+
describe: oc.output(sourceDescriptionSchema),
|
|
166
|
+
sync: {
|
|
167
|
+
page: oc.input(syncInputSchema).output(syncPageSchema),
|
|
168
|
+
stream: oc.input(syncInputSchema).output(syncPageEvents)
|
|
169
|
+
},
|
|
170
|
+
scopes: {
|
|
171
|
+
roots: oc.errors(scopeListingErrors).input(scopeRootsInputSchema).output(scopeListingSchema),
|
|
172
|
+
children: oc.errors(scopeListingErrors).input(scopeChildrenInputSchema).output(scopeListingSchema)
|
|
173
|
+
}
|
|
174
|
+
}) };
|
|
175
|
+
//#endregion
|
|
176
|
+
//#region ../../core/graph/source-contract/dist/paginate.js
|
|
177
|
+
/**
|
|
178
|
+
* Pages through a cursor-linked sequence on demand: each `next()` reads one page, starting after `first`, and the sequence ends after the page whose `next` is absent. Nothing is read ahead of the consumer, so a consumer that stops early stops the reads.
|
|
179
|
+
* @param first - The cursor to start after, or `undefined` for the first page.
|
|
180
|
+
* @param read - Reads the page after a cursor.
|
|
181
|
+
* @returns The pages, in order.
|
|
182
|
+
*/
|
|
183
|
+
const paginate = (first, read) => ({ [Symbol.asyncIterator]: () => {
|
|
184
|
+
let after = first;
|
|
185
|
+
let finished = false;
|
|
186
|
+
return { next: async () => {
|
|
187
|
+
if (finished) return {
|
|
188
|
+
done: true,
|
|
189
|
+
value: void 0
|
|
190
|
+
};
|
|
191
|
+
return read(after).then((page) => {
|
|
192
|
+
after = page.next;
|
|
193
|
+
finished = after === void 0;
|
|
194
|
+
return {
|
|
195
|
+
done: false,
|
|
196
|
+
value: page
|
|
197
|
+
};
|
|
198
|
+
});
|
|
199
|
+
} };
|
|
200
|
+
} });
|
|
201
|
+
//#endregion
|
|
202
|
+
//#region ../../core/graph/source-contract/dist/router.js
|
|
203
|
+
const os = implement(graphSourceContract);
|
|
204
|
+
/** Rethrows a pager's `GraphSourceError` as the contract's typed oRPC error; anything else propagates unchanged. */
|
|
205
|
+
const toContractError = (error, errors) => {
|
|
206
|
+
if (!(error instanceof GraphSourceError)) return error;
|
|
207
|
+
const { failure } = error;
|
|
208
|
+
if (failure.kind === "unauthorised") return errors.UNAUTHORIZED({
|
|
209
|
+
message: failure.message,
|
|
210
|
+
data: {},
|
|
211
|
+
cause: error
|
|
212
|
+
});
|
|
213
|
+
if (failure.kind === "rate-limited") return errors.TOO_MANY_REQUESTS({
|
|
214
|
+
message: failure.message,
|
|
215
|
+
data: failure.resetAt === void 0 ? {} : { resetAt: failure.resetAt },
|
|
216
|
+
cause: error
|
|
217
|
+
});
|
|
218
|
+
return errors.BAD_REQUEST({
|
|
219
|
+
message: failure.message,
|
|
220
|
+
data: {
|
|
221
|
+
reason: failure.reason,
|
|
222
|
+
value: failure.value
|
|
223
|
+
},
|
|
224
|
+
cause: error
|
|
225
|
+
});
|
|
226
|
+
};
|
|
227
|
+
/** Runs one call into the pager after checking the signal, as the contract's typed error when it fails. */
|
|
228
|
+
const guarded = async (signal, errors, read) => {
|
|
229
|
+
signal?.throwIfAborted();
|
|
230
|
+
try {
|
|
231
|
+
return await read();
|
|
232
|
+
} catch (error) {
|
|
233
|
+
throw toContractError(error, errors);
|
|
234
|
+
}
|
|
235
|
+
};
|
|
236
|
+
/** Reads one page from the pager, as the contract's typed error when it fails. */
|
|
237
|
+
const readPage = async (pager, request, errors) => guarded(request.signal, errors, async () => pager.page(request));
|
|
238
|
+
/** The pager's scope listing, or the contract's `NOT_IMPLEMENTED` when it has none. */
|
|
239
|
+
const listingOf = (pager, errors) => {
|
|
240
|
+
if (pager.scopes === void 0) throw errors.NOT_IMPLEMENTED({
|
|
241
|
+
message: `The ${pager.source} source does not list its scopes.`,
|
|
242
|
+
data: {}
|
|
243
|
+
});
|
|
244
|
+
return pager.scopes;
|
|
245
|
+
};
|
|
246
|
+
/**
|
|
247
|
+
* Builds the `GraphSource` router over a pager. Serve it over any oRPC transport, or call it in process with `createRouterClient`.
|
|
248
|
+
* @param pager - The adapter's page reader.
|
|
249
|
+
* @returns The implemented router for `graphSourceContract`.
|
|
250
|
+
*/
|
|
251
|
+
const createGraphSourceRouter = (pager) => os.router({ v1: {
|
|
252
|
+
describe: os.v1.describe.handler(() => ({ source: pager.source })),
|
|
253
|
+
sync: {
|
|
254
|
+
page: os.v1.sync.page.handler(async ({ input, signal, errors }) => readPage(pager, {
|
|
255
|
+
scope: input.scope,
|
|
256
|
+
cursor: input.after,
|
|
257
|
+
signal,
|
|
258
|
+
filter: input.filter
|
|
259
|
+
}, errors)),
|
|
260
|
+
stream: os.v1.sync.stream.handler(async function* ({ input, signal, lastEventId, errors }) {
|
|
261
|
+
const pages = paginate(input.after ?? lastEventId, async (cursor) => readPage(pager, {
|
|
262
|
+
scope: input.scope,
|
|
263
|
+
cursor,
|
|
264
|
+
signal,
|
|
265
|
+
filter: input.filter
|
|
266
|
+
}, errors));
|
|
267
|
+
for await (const page of pages) yield page.next === void 0 ? page : withEventMeta(page, { id: page.next });
|
|
268
|
+
})
|
|
269
|
+
},
|
|
270
|
+
scopes: {
|
|
271
|
+
roots: os.v1.scopes.roots.handler(async ({ input, signal, errors }) => {
|
|
272
|
+
const listing = listingOf(pager, errors);
|
|
273
|
+
return guarded(signal, errors, async () => listing.roots({
|
|
274
|
+
cursor: input.after,
|
|
275
|
+
signal
|
|
276
|
+
}));
|
|
277
|
+
}),
|
|
278
|
+
children: os.v1.scopes.children.handler(async ({ input, signal, errors }) => {
|
|
279
|
+
const listing = listingOf(pager, errors);
|
|
280
|
+
return guarded(signal, errors, async () => listing.children({
|
|
281
|
+
scope: input.scope,
|
|
282
|
+
cursor: input.after,
|
|
283
|
+
signal
|
|
284
|
+
}));
|
|
285
|
+
})
|
|
286
|
+
}
|
|
287
|
+
} });
|
|
288
|
+
//#endregion
|
|
289
|
+
//#region ../../core/graph/source-contract/dist/assemble.js
|
|
290
|
+
const relationKey = (relation) => JSON.stringify([
|
|
291
|
+
relation.kind,
|
|
292
|
+
relation.from,
|
|
293
|
+
relation.to
|
|
294
|
+
]);
|
|
295
|
+
/** Keeps one entry per key, in first-seen order, holding the latest value seen for it. */
|
|
296
|
+
const latestById = (items, keyOf) => {
|
|
297
|
+
const byKey = /* @__PURE__ */ new Map();
|
|
298
|
+
for (const item of items) byKey.set(keyOf(item), item);
|
|
299
|
+
return [...byKey.values()];
|
|
300
|
+
};
|
|
301
|
+
/**
|
|
302
|
+
* Merges the pages of a sync into one model. A record can appear on several pages, for example an issue in another repository reached through a dependency before its own repository is read; identity is the identifier, so it becomes one element, and where pages disagree the later page wins, being the more recent read.
|
|
303
|
+
* @param pages - The pages of one sync, in the order they arrived.
|
|
304
|
+
* @returns The model, overlay, relations and gaps of the whole sync.
|
|
305
|
+
*/
|
|
306
|
+
const assembleSync = (pages) => {
|
|
307
|
+
const overlay = Object.fromEntries(pages.flatMap((page) => Object.entries(page.overlay)));
|
|
308
|
+
return {
|
|
309
|
+
model: {
|
|
310
|
+
participants: latestById(pages.flatMap((page) => page.participants), (participant) => participant.id),
|
|
311
|
+
events: [],
|
|
312
|
+
actions: [],
|
|
313
|
+
artefacts: [],
|
|
314
|
+
elements: latestById(pages.flatMap((page) => page.activities), (activity) => activity.id),
|
|
315
|
+
transitions: latestById(pages.flatMap((page) => page.transitions), (transition) => transition.id),
|
|
316
|
+
fragments: [],
|
|
317
|
+
projections: []
|
|
318
|
+
},
|
|
319
|
+
overlay,
|
|
320
|
+
relations: latestById(pages.flatMap((page) => page.relations), relationKey),
|
|
321
|
+
gaps: pages.flatMap((page) => page.gaps)
|
|
322
|
+
};
|
|
323
|
+
};
|
|
324
|
+
//#endregion
|
|
325
|
+
//#region ../../adapters/graph/source-github-issues/dist/cursor.js
|
|
326
|
+
/**
|
|
327
|
+
* Where the next page of a sync starts. A position is plain data, so a cursor resumes in any process.
|
|
328
|
+
*
|
|
329
|
+
* - `scope` is the scope identifier being read; a cursor is valid only for a request whose scope list contains it.
|
|
330
|
+
* - For an owner scope, `repositories` is the `after` cursor GitHub gave for the page of the owner's repositories that holds the current one (`null` for the first), and `repository` is its index within that page. Repositories are listed by name, so the same listing page returns the same repositories while the owner is unchanged.
|
|
331
|
+
* - `issues` is the `after` cursor for the next page of the current repository's issues (`null` for the first). Issues are listed by creation time unless a filter orders them otherwise, so new issues append and never shift a position.
|
|
332
|
+
* - For a project scope, `items` is the `after` cursor for the next page of the board's items (`null` for the first).
|
|
333
|
+
*/
|
|
334
|
+
const repositoryPositionSchema = z.strictObject({
|
|
335
|
+
scope: z.string(),
|
|
336
|
+
issues: z.string().nullable()
|
|
337
|
+
});
|
|
338
|
+
const ownerPositionSchema = z.strictObject({
|
|
339
|
+
scope: z.string(),
|
|
340
|
+
repositories: z.string().nullable(),
|
|
341
|
+
repository: z.int().nonnegative(),
|
|
342
|
+
issues: z.string().nullable()
|
|
343
|
+
});
|
|
344
|
+
const projectPositionSchema = z.strictObject({
|
|
345
|
+
scope: z.string(),
|
|
346
|
+
items: z.string().nullable()
|
|
347
|
+
});
|
|
348
|
+
const positionSchema = z.union([
|
|
349
|
+
repositoryPositionSchema,
|
|
350
|
+
ownerPositionSchema,
|
|
351
|
+
projectPositionSchema
|
|
352
|
+
]);
|
|
353
|
+
/**
|
|
354
|
+
* The start of a scope.
|
|
355
|
+
* @param scope - The scope.
|
|
356
|
+
* @returns The scope with the position of its first page.
|
|
357
|
+
*/
|
|
358
|
+
const startOf = (scope) => {
|
|
359
|
+
if (scope.kind === "owner") return {
|
|
360
|
+
kind: "owner",
|
|
361
|
+
scope,
|
|
362
|
+
position: {
|
|
363
|
+
scope: scope.id,
|
|
364
|
+
repositories: null,
|
|
365
|
+
repository: 0,
|
|
366
|
+
issues: null
|
|
367
|
+
}
|
|
368
|
+
};
|
|
369
|
+
if (scope.kind === "project") return {
|
|
370
|
+
kind: "project",
|
|
371
|
+
scope,
|
|
372
|
+
position: {
|
|
373
|
+
scope: scope.id,
|
|
374
|
+
items: null
|
|
375
|
+
}
|
|
376
|
+
};
|
|
377
|
+
return {
|
|
378
|
+
kind: "repository",
|
|
379
|
+
scope,
|
|
380
|
+
position: {
|
|
381
|
+
scope: scope.id,
|
|
382
|
+
issues: null
|
|
383
|
+
}
|
|
384
|
+
};
|
|
385
|
+
};
|
|
386
|
+
/** The version tag inside every cursor, so a cursor from a later format is rejected rather than misread. */
|
|
387
|
+
const cursorFormat = 1;
|
|
388
|
+
/** A sync cursor: where the next page starts, and the filter it was issued under, absent for none. */
|
|
389
|
+
const envelopeSchema = z.strictObject({
|
|
390
|
+
format: z.literal(cursorFormat),
|
|
391
|
+
position: positionSchema,
|
|
392
|
+
filter: syncFilterSchema.optional()
|
|
393
|
+
});
|
|
394
|
+
/** A listing cursor: the parent scope whose children it pages (`null` for the top-level listing), and GitHub's cursor to read on after. */
|
|
395
|
+
const listingEnvelopeSchema = z.strictObject({
|
|
396
|
+
format: z.literal(cursorFormat),
|
|
397
|
+
parent: z.string().nullable(),
|
|
398
|
+
after: z.string()
|
|
399
|
+
});
|
|
400
|
+
/** A filter's fields in a fixed order, so two filters compare by value whatever order their fields were written in. */
|
|
401
|
+
const filterKey = (filter) => JSON.stringify(filter === void 0 ? null : [
|
|
402
|
+
filter.states ?? null,
|
|
403
|
+
filter.labels ?? null,
|
|
404
|
+
filter.order === void 0 ? null : [filter.order.field, filter.order.direction]
|
|
405
|
+
]);
|
|
406
|
+
/** Base64 of a small JSON envelope, which is printable and free of the line breaks an event-stream id must not contain. */
|
|
407
|
+
const encodeEnvelope = (envelope) => btoa(JSON.stringify(envelope));
|
|
408
|
+
/**
|
|
409
|
+
* Encodes a position as an opaque cursor, together with the filter the sync runs under.
|
|
410
|
+
* @param position - Where the next page starts.
|
|
411
|
+
* @param filter - The sync's filter, or `undefined` for none.
|
|
412
|
+
* @returns The cursor.
|
|
413
|
+
*/
|
|
414
|
+
const encodeCursor = (position, filter) => encodeEnvelope({
|
|
415
|
+
format: cursorFormat,
|
|
416
|
+
position,
|
|
417
|
+
...filter === void 0 ? {} : { filter }
|
|
418
|
+
});
|
|
419
|
+
const invalidCursor = (cursor, message) => new GraphSourceError({
|
|
420
|
+
kind: "bad-request",
|
|
421
|
+
message: `"${cursor}" is not a cursor this source issued: ${message}`,
|
|
422
|
+
reason: "invalid-cursor",
|
|
423
|
+
value: cursor
|
|
424
|
+
});
|
|
425
|
+
/** Parses JSON from a decoded cursor, reporting malformed text as an invalid cursor. */
|
|
426
|
+
const parseJson = (cursor, text) => {
|
|
427
|
+
try {
|
|
428
|
+
return JSON.parse(text);
|
|
429
|
+
} catch (error) {
|
|
430
|
+
throw invalidCursor(cursor, error instanceof Error ? error.message : "not JSON");
|
|
431
|
+
}
|
|
432
|
+
};
|
|
433
|
+
/** Decodes base64, reporting text outside the base64 alphabet as an invalid cursor. */
|
|
434
|
+
const decodeBase64 = (cursor) => {
|
|
435
|
+
try {
|
|
436
|
+
return atob(cursor);
|
|
437
|
+
} catch {
|
|
438
|
+
throw invalidCursor(cursor, "not base64");
|
|
439
|
+
}
|
|
440
|
+
};
|
|
441
|
+
/**
|
|
442
|
+
* Decodes a cursor this adapter issued.
|
|
443
|
+
* @param cursor - The cursor from an earlier page's `next`.
|
|
444
|
+
* @param scopes - The scopes of the request it is used with.
|
|
445
|
+
* @param filter - The filter of the request it is used with.
|
|
446
|
+
* @returns The position it encodes, with its scope.
|
|
447
|
+
* @throws GraphSourceError with `bad-request` and `invalid-cursor` when it is not a cursor this adapter issued, was issued under another filter, names a scope the request does not contain, or has the shape of another kind of scope.
|
|
448
|
+
*/
|
|
449
|
+
const decodeCursor = (cursor, scopes, filter) => {
|
|
450
|
+
const envelope = envelopeSchema.safeParse(parseJson(cursor, decodeBase64(cursor)));
|
|
451
|
+
if (!envelope.success) throw invalidCursor(cursor, envelope.error.message);
|
|
452
|
+
if (filterKey(envelope.data.filter) !== filterKey(filter)) throw invalidCursor(cursor, "it was issued under a different filter from the request's");
|
|
453
|
+
const { position } = envelope.data;
|
|
454
|
+
const scope = scopes.find((candidate) => candidate.id === position.scope);
|
|
455
|
+
if (scope === void 0) throw invalidCursor(cursor, `it is for scope ${position.scope}, which the request does not contain`);
|
|
456
|
+
if (scope.kind === "owner" && "repositories" in position) return {
|
|
457
|
+
kind: "owner",
|
|
458
|
+
scope,
|
|
459
|
+
position
|
|
460
|
+
};
|
|
461
|
+
if (scope.kind === "repository" && "issues" in position && !("repositories" in position)) return {
|
|
462
|
+
kind: "repository",
|
|
463
|
+
scope,
|
|
464
|
+
position
|
|
465
|
+
};
|
|
466
|
+
if (scope.kind === "project" && "items" in position) return {
|
|
467
|
+
kind: "project",
|
|
468
|
+
scope,
|
|
469
|
+
position
|
|
470
|
+
};
|
|
471
|
+
throw invalidCursor(cursor, `it does not fit the ${scope.kind} scope ${scope.id}`);
|
|
472
|
+
};
|
|
473
|
+
/**
|
|
474
|
+
* Encodes where a scope listing resumes.
|
|
475
|
+
* @param parent - The parent scope whose children are listed, or `null` for the top-level listing.
|
|
476
|
+
* @param after - GitHub's cursor to read on after.
|
|
477
|
+
* @returns The cursor.
|
|
478
|
+
*/
|
|
479
|
+
const encodeListingCursor = (parent, after) => encodeEnvelope({
|
|
480
|
+
format: cursorFormat,
|
|
481
|
+
parent,
|
|
482
|
+
after
|
|
483
|
+
});
|
|
484
|
+
/**
|
|
485
|
+
* Decodes a listing cursor this adapter issued.
|
|
486
|
+
* @param cursor - The cursor from an earlier listing page's `next`.
|
|
487
|
+
* @param parent - The parent of the request it is used with, or `null` for the top-level listing.
|
|
488
|
+
* @returns GitHub's cursor to read on after.
|
|
489
|
+
* @throws GraphSourceError with `bad-request` and `invalid-cursor` when it is not a listing cursor this adapter issued, or was issued for another listing.
|
|
490
|
+
*/
|
|
491
|
+
const decodeListingCursor = (cursor, parent) => {
|
|
492
|
+
const envelope = listingEnvelopeSchema.safeParse(parseJson(cursor, decodeBase64(cursor)));
|
|
493
|
+
if (!envelope.success) throw invalidCursor(cursor, envelope.error.message);
|
|
494
|
+
if (envelope.data.parent !== parent) throw invalidCursor(cursor, `it is for the listing of ${envelope.data.parent ?? "the top-level scopes"}`);
|
|
495
|
+
return envelope.data.after;
|
|
496
|
+
};
|
|
497
|
+
//#endregion
|
|
498
|
+
//#region ../../adapters/graph/source-github-issues/dist/graphql.js
|
|
499
|
+
/** GitHub's GraphQL endpoint. */
|
|
500
|
+
const githubGraphqlEndpoint = "https://api.github.com/graphql";
|
|
501
|
+
const millisecondsPerSecond = 1e3;
|
|
502
|
+
/** GraphQL error types that mean part of the data is absent rather than that the request failed. */
|
|
503
|
+
const problemReasons = {
|
|
504
|
+
NOT_FOUND: "not-found",
|
|
505
|
+
FORBIDDEN: "forbidden"
|
|
506
|
+
};
|
|
507
|
+
const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
508
|
+
/** The `errors` entries of a GraphQL response body, each with its `type` when GitHub gave one. */
|
|
509
|
+
const errorsOf = (body) => {
|
|
510
|
+
if (!isRecord(body) || !Array.isArray(body.errors)) return [];
|
|
511
|
+
return body.errors.filter(isRecord).map((entry) => ({
|
|
512
|
+
type: typeof entry.type === "string" ? entry.type : void 0,
|
|
513
|
+
message: typeof entry.message === "string" ? entry.message : "no message given"
|
|
514
|
+
}));
|
|
515
|
+
};
|
|
516
|
+
/** The `message` of a REST-style error body, which GitHub sends for HTTP-level refusals. */
|
|
517
|
+
const messageOf = (body) => isRecord(body) && typeof body.message === "string" ? body.message : void 0;
|
|
518
|
+
/**
|
|
519
|
+
* When GitHub says its limit resets: `x-ratelimit-reset` (epoch seconds) for the primary limit, `retry-after` (seconds from now) for a secondary limit. `undefined` when it gives neither.
|
|
520
|
+
*/
|
|
521
|
+
const resetAtOf = (headers, now) => {
|
|
522
|
+
const reset = headers.get("x-ratelimit-reset");
|
|
523
|
+
if (reset !== null && /^\d+$/u.test(reset)) return (/* @__PURE__ */ new Date(Number(reset) * millisecondsPerSecond)).toISOString();
|
|
524
|
+
const retryAfter = headers.get("retry-after");
|
|
525
|
+
if (retryAfter !== null && /^\d+$/u.test(retryAfter)) return new Date(now + Number(retryAfter) * millisecondsPerSecond).toISOString();
|
|
526
|
+
};
|
|
527
|
+
/**
|
|
528
|
+
* Whether a response is GitHub refusing for rate limiting: HTTP 429; HTTP 403 with no requests remaining or naming a secondary rate limit; or a `RATE_LIMITED` GraphQL error, which GitHub sends with HTTP 200.
|
|
529
|
+
*/
|
|
530
|
+
const isRateLimited = (response, body, errors) => {
|
|
531
|
+
const tooManyRequests = 429;
|
|
532
|
+
const forbidden = 403;
|
|
533
|
+
if (response.status === tooManyRequests) return true;
|
|
534
|
+
if (errors.some((error) => error.type === "RATE_LIMITED")) return true;
|
|
535
|
+
const message = messageOf(body);
|
|
536
|
+
return response.status === forbidden && (response.headers.get("x-ratelimit-remaining") === "0" || message !== void 0 && /secondary rate limit/iu.test(message));
|
|
537
|
+
};
|
|
538
|
+
/** Reads a response body as JSON, failing loudly when it is not. */
|
|
539
|
+
const readJson = async (response) => {
|
|
540
|
+
const text = await response.text();
|
|
541
|
+
try {
|
|
542
|
+
return JSON.parse(text);
|
|
543
|
+
} catch {
|
|
544
|
+
throw new Error(`GitHub answered HTTP ${String(response.status)} with a body that is not JSON`);
|
|
545
|
+
}
|
|
546
|
+
};
|
|
547
|
+
/**
|
|
548
|
+
* Sends one GraphQL request and classifies the answer.
|
|
549
|
+
* @param transport - The token, fetch and clock ports.
|
|
550
|
+
* @param request - The document, its variables, the owner whose token to use and the schema for `data`.
|
|
551
|
+
* @param signal - The caller's abort signal, passed to `fetch`.
|
|
552
|
+
* @returns The parsed data and any `NOT_FOUND` or `FORBIDDEN` problems beside it.
|
|
553
|
+
* @throws GraphSourceError with `unauthorised` for HTTP 401, and `rate-limited` (with the reset instant when GitHub gives one) for any form of rate limiting.
|
|
554
|
+
* @throws Error for any other failure: an HTTP error, another GraphQL error type, or data that does not match the schema.
|
|
555
|
+
*/
|
|
556
|
+
const graphql = async (transport, request, signal) => {
|
|
557
|
+
const token = await transport.token(request.owner, signal);
|
|
558
|
+
const response = await transport.fetch(githubGraphqlEndpoint, {
|
|
559
|
+
method: "POST",
|
|
560
|
+
headers: {
|
|
561
|
+
authorization: `Bearer ${token}`,
|
|
562
|
+
"content-type": "application/json"
|
|
563
|
+
},
|
|
564
|
+
body: JSON.stringify({
|
|
565
|
+
query: request.query,
|
|
566
|
+
variables: request.variables
|
|
567
|
+
}),
|
|
568
|
+
signal: signal ?? null
|
|
569
|
+
});
|
|
570
|
+
const body = await readJson(response);
|
|
571
|
+
const errors = errorsOf(body);
|
|
572
|
+
if (response.status === 401) throw new GraphSourceError({
|
|
573
|
+
kind: "unauthorised",
|
|
574
|
+
message: `GitHub rejected the token${request.owner === void 0 ? "" : ` for ${request.owner}`}: ${messageOf(body) ?? "HTTP 401"}`
|
|
575
|
+
});
|
|
576
|
+
if (isRateLimited(response, body, errors)) {
|
|
577
|
+
const resetAt = resetAtOf(response.headers, transport.now());
|
|
578
|
+
throw new GraphSourceError({
|
|
579
|
+
kind: "rate-limited",
|
|
580
|
+
message: resetAt === void 0 ? "GitHub is rate limiting requests" : `GitHub is rate limiting requests until ${resetAt}`,
|
|
581
|
+
resetAt
|
|
582
|
+
});
|
|
583
|
+
}
|
|
584
|
+
if (!response.ok) throw new Error(`GitHub answered HTTP ${String(response.status)}: ${messageOf(body) ?? "no message given"}`);
|
|
585
|
+
const problems = errors.map((error) => {
|
|
586
|
+
const reason = error.type === void 0 ? void 0 : problemReasons[error.type];
|
|
587
|
+
if (reason === void 0) throw new Error(`GitHub GraphQL error${error.type === void 0 ? "" : ` ${error.type}`}: ${error.message}`);
|
|
588
|
+
return {
|
|
589
|
+
reason,
|
|
590
|
+
message: error.message
|
|
591
|
+
};
|
|
592
|
+
});
|
|
593
|
+
const data = request.schema.safeParse(isRecord(body) ? body.data : void 0);
|
|
594
|
+
if (!data.success) throw new Error(`GitHub's response does not have the expected shape: ${data.error.message}`);
|
|
595
|
+
return {
|
|
596
|
+
data: data.data,
|
|
597
|
+
problems
|
|
598
|
+
};
|
|
599
|
+
};
|
|
600
|
+
//#endregion
|
|
601
|
+
//#region ../../adapters/graph/source-github-issues/dist/identifiers.js
|
|
602
|
+
/** The prefix of every identifier this adapter emits and every scope it accepts. */
|
|
603
|
+
const githubSource = "github";
|
|
604
|
+
const prefix = `${githubSource}:`;
|
|
605
|
+
/**
|
|
606
|
+
* A GitHub login or repository name as GitHub allows them: letters, digits, hyphens, underscores and dots. Anything else cannot name an account or repository, so a scope containing it is malformed rather than missing.
|
|
607
|
+
*/
|
|
608
|
+
const namePattern = /^[\w.-]+$/u;
|
|
609
|
+
/** A Projects v2 board's number as its URL shows it: a positive integer with no leading zero. */
|
|
610
|
+
const numberPattern = /^[1-9]\d*$/u;
|
|
611
|
+
/** The path segment between an owner and a board's number in a project scope, as in the board's own URL. */
|
|
612
|
+
const projectsSegment = "projects";
|
|
613
|
+
const invalidScope = (id) => new GraphSourceError({
|
|
614
|
+
kind: "bad-request",
|
|
615
|
+
message: `"${id}" is not a GitHub scope; expected github:owner, github:owner/repo or github:owner/projects/number`,
|
|
616
|
+
reason: "invalid-scope",
|
|
617
|
+
value: id
|
|
618
|
+
});
|
|
619
|
+
/**
|
|
620
|
+
* Parses a scope identifier: `github:owner` selects every repository the account owns, `github:owner/repo` one repository, and `github:owner/projects/number` the issues on an organisation's or user's Projects v2 board. A repository name cannot hold a slash, so the three forms never overlap.
|
|
621
|
+
* @param id - The scope identifier.
|
|
622
|
+
* @returns The parsed scope.
|
|
623
|
+
* @throws GraphSourceError with `bad-request` and `invalid-scope` when it is none of the forms.
|
|
624
|
+
*/
|
|
625
|
+
const parseScope = (id) => {
|
|
626
|
+
const [owner, name, number, ...rest] = id.startsWith(prefix) ? id.slice(prefix.length).split("/") : [];
|
|
627
|
+
if (owner === void 0 || !namePattern.test(owner) || rest.length > 0) throw invalidScope(id);
|
|
628
|
+
if (name === void 0) return {
|
|
629
|
+
kind: "owner",
|
|
630
|
+
id,
|
|
631
|
+
owner
|
|
632
|
+
};
|
|
633
|
+
if (number === void 0 && namePattern.test(name)) return {
|
|
634
|
+
kind: "repository",
|
|
635
|
+
id,
|
|
636
|
+
owner,
|
|
637
|
+
name
|
|
638
|
+
};
|
|
639
|
+
if (name === projectsSegment && number !== void 0 && numberPattern.test(number)) return {
|
|
640
|
+
kind: "project",
|
|
641
|
+
id,
|
|
642
|
+
owner,
|
|
643
|
+
number: Number(number)
|
|
644
|
+
};
|
|
645
|
+
throw invalidScope(id);
|
|
646
|
+
};
|
|
647
|
+
/** The scope identifier of an owner: `github:owner`. */
|
|
648
|
+
const ownerScopeId = (login) => `${prefix}${login}`;
|
|
649
|
+
/** The identifier of an issue: `github:owner/repo#number`, which is also its element identifier. */
|
|
650
|
+
const issueId = (owner, name, number) => `${prefix}${owner}/${name}#${String(number)}`;
|
|
651
|
+
/** The identifier of a repository participant: `github:owner/repo`. */
|
|
652
|
+
const repositoryId = (owner, name) => `${prefix}${owner}/${name}`;
|
|
653
|
+
/** The identifier of an account participant, organisation or user: `github:@login`. The `@` keeps it apart from the `github:owner` scope syntax. */
|
|
654
|
+
const accountId = (login) => `${prefix}@${login}`;
|
|
655
|
+
//#endregion
|
|
656
|
+
//#region ../../adapters/graph/source-github-issues/dist/page-builder.js
|
|
657
|
+
/**
|
|
658
|
+
* Starts an empty page.
|
|
659
|
+
* @returns A builder whose `build` returns the page.
|
|
660
|
+
*/
|
|
661
|
+
const createPageBuilder = () => {
|
|
662
|
+
const participants = /* @__PURE__ */ new Map();
|
|
663
|
+
const activities = /* @__PURE__ */ new Map();
|
|
664
|
+
const transitions = /* @__PURE__ */ new Map();
|
|
665
|
+
const relations = /* @__PURE__ */ new Map();
|
|
666
|
+
const overlay = /* @__PURE__ */ new Map();
|
|
667
|
+
const gaps = [];
|
|
668
|
+
const relate = (relation) => {
|
|
669
|
+
relations.set(JSON.stringify([
|
|
670
|
+
relation.kind,
|
|
671
|
+
relation.from,
|
|
672
|
+
relation.to
|
|
673
|
+
]), relation);
|
|
674
|
+
};
|
|
675
|
+
const account = (owner) => {
|
|
676
|
+
const id = accountId(owner.login);
|
|
677
|
+
participants.set(id, {
|
|
678
|
+
id,
|
|
679
|
+
label: owner.login,
|
|
680
|
+
kind: owner.__typename === "Organization" ? "system" : "role"
|
|
681
|
+
});
|
|
682
|
+
return id;
|
|
683
|
+
};
|
|
684
|
+
const repository = (owner, name) => {
|
|
685
|
+
const ownerParticipant = account(owner);
|
|
686
|
+
const id = repositoryId(owner.login, name);
|
|
687
|
+
participants.set(id, {
|
|
688
|
+
id,
|
|
689
|
+
label: `${owner.login}/${name}`,
|
|
690
|
+
kind: "system"
|
|
691
|
+
});
|
|
692
|
+
relate({
|
|
693
|
+
kind: "part-of",
|
|
694
|
+
from: id,
|
|
695
|
+
to: ownerParticipant
|
|
696
|
+
});
|
|
697
|
+
return id;
|
|
698
|
+
};
|
|
699
|
+
/** The transition from a prerequisite to the work that waits on it, identified by both ends so either side's read gives the same one. */
|
|
700
|
+
const depend = (source, target) => {
|
|
701
|
+
const id = `${source}->${target}`;
|
|
702
|
+
transitions.set(id, {
|
|
703
|
+
id,
|
|
704
|
+
source,
|
|
705
|
+
target
|
|
706
|
+
});
|
|
707
|
+
};
|
|
708
|
+
const issue = (ref) => {
|
|
709
|
+
const { owner, name } = ref.repository;
|
|
710
|
+
const actor = repository(owner, name);
|
|
711
|
+
const id = issueId(owner.login, name, ref.number);
|
|
712
|
+
activities.set(id, {
|
|
713
|
+
kind: "activity",
|
|
714
|
+
id,
|
|
715
|
+
source: id,
|
|
716
|
+
label: ref.title,
|
|
717
|
+
actor
|
|
718
|
+
});
|
|
719
|
+
overlay.set(id, ref.state === "OPEN" ? "open" : "done");
|
|
720
|
+
return id;
|
|
721
|
+
};
|
|
722
|
+
return {
|
|
723
|
+
account,
|
|
724
|
+
repository,
|
|
725
|
+
issue,
|
|
726
|
+
assignee: (issueIdentifier, user) => {
|
|
727
|
+
const id = accountId(user.login);
|
|
728
|
+
participants.set(id, {
|
|
729
|
+
id,
|
|
730
|
+
label: user.login,
|
|
731
|
+
kind: "role"
|
|
732
|
+
});
|
|
733
|
+
relate({
|
|
734
|
+
kind: "assigned-to",
|
|
735
|
+
from: issueIdentifier,
|
|
736
|
+
to: id
|
|
737
|
+
});
|
|
738
|
+
},
|
|
739
|
+
blockedBy: (issueIdentifier, blocker) => {
|
|
740
|
+
depend(issue(blocker), issueIdentifier);
|
|
741
|
+
},
|
|
742
|
+
blocking: (issueIdentifier, blocked) => {
|
|
743
|
+
depend(issueIdentifier, issue(blocked));
|
|
744
|
+
},
|
|
745
|
+
subIssue: (parent, child) => {
|
|
746
|
+
relate({
|
|
747
|
+
kind: "child-of",
|
|
748
|
+
from: issue(child),
|
|
749
|
+
to: parent
|
|
750
|
+
});
|
|
751
|
+
},
|
|
752
|
+
gap: (gap) => {
|
|
753
|
+
gaps.push(gap);
|
|
754
|
+
},
|
|
755
|
+
build: (next) => {
|
|
756
|
+
const page = {
|
|
757
|
+
participants: [...participants.values()],
|
|
758
|
+
activities: [...activities.values()],
|
|
759
|
+
transitions: [...transitions.values()],
|
|
760
|
+
relations: [...relations.values()],
|
|
761
|
+
overlay: Object.fromEntries(overlay),
|
|
762
|
+
gaps: [...gaps]
|
|
763
|
+
};
|
|
764
|
+
return next === void 0 ? page : {
|
|
765
|
+
...page,
|
|
766
|
+
next
|
|
767
|
+
};
|
|
768
|
+
}
|
|
769
|
+
};
|
|
770
|
+
};
|
|
771
|
+
//#endregion
|
|
772
|
+
//#region ../../adapters/graph/source-github-issues/dist/queries.js
|
|
773
|
+
const pageInfoSchema = z.object({
|
|
774
|
+
hasNextPage: z.boolean(),
|
|
775
|
+
endCursor: z.string().nullable()
|
|
776
|
+
});
|
|
777
|
+
const accountSchema = z.object({
|
|
778
|
+
__typename: z.enum(["Organization", "User"]),
|
|
779
|
+
login: z.string()
|
|
780
|
+
});
|
|
781
|
+
const repositoryRefSchema = z.object({
|
|
782
|
+
name: z.string(),
|
|
783
|
+
owner: accountSchema
|
|
784
|
+
});
|
|
785
|
+
/** An issue reached from another one: a blocker or a sub-issue, possibly in another repository or organisation. */
|
|
786
|
+
const issueRefSchema = z.object({
|
|
787
|
+
number: z.int(),
|
|
788
|
+
title: z.string(),
|
|
789
|
+
state: z.enum(["OPEN", "CLOSED"]),
|
|
790
|
+
repository: repositoryRefSchema
|
|
791
|
+
});
|
|
792
|
+
const connectionOf = (node) => z.object({
|
|
793
|
+
pageInfo: pageInfoSchema,
|
|
794
|
+
nodes: z.array(node.nullable())
|
|
795
|
+
});
|
|
796
|
+
const userSchema = z.object({ login: z.string() });
|
|
797
|
+
const issueSchema = issueRefSchema.extend({
|
|
798
|
+
assignees: connectionOf(userSchema),
|
|
799
|
+
blockedBy: connectionOf(issueRefSchema),
|
|
800
|
+
blocking: connectionOf(issueRefSchema),
|
|
801
|
+
subIssues: connectionOf(issueRefSchema)
|
|
802
|
+
});
|
|
803
|
+
const accountFields = "__typename login";
|
|
804
|
+
const issueRefFields = `number title state repository { name owner { ${accountFields} } }`;
|
|
805
|
+
const pageInfoFields = "pageInfo { hasNextPage endCursor }";
|
|
806
|
+
/** An issue with the first page of each of its connections: assignees, the issues blocking it, the issues it blocks, and its sub-issues. */
|
|
807
|
+
const issueFields = `${issueRefFields}
|
|
808
|
+
assignees(first: $first) { ${pageInfoFields} nodes { login } }
|
|
809
|
+
blockedBy(first: $first) { ${pageInfoFields} nodes { ${issueRefFields} } }
|
|
810
|
+
blocking(first: $first) { ${pageInfoFields} nodes { ${issueRefFields} } }
|
|
811
|
+
subIssues(first: $first) { ${pageInfoFields} nodes { ${issueRefFields} } }`;
|
|
812
|
+
/** A page of one repository's issues, in the states, labels and order the variables give, each with the first page of its connections. */
|
|
813
|
+
const repositoryIssuesQuery = `query RepositoryIssues($owner: String!, $name: String!, $first: Int!, $after: String, $states: [IssueState!]!, $labels: [String!], $orderBy: IssueOrder!) {
|
|
814
|
+
repository(owner: $owner, name: $name) {
|
|
815
|
+
name
|
|
816
|
+
owner { ${accountFields} }
|
|
817
|
+
issues(first: $first, after: $after, states: $states, labels: $labels, orderBy: $orderBy) {
|
|
818
|
+
${pageInfoFields}
|
|
819
|
+
nodes {
|
|
820
|
+
${issueFields}
|
|
821
|
+
}
|
|
822
|
+
}
|
|
823
|
+
}
|
|
824
|
+
}`;
|
|
825
|
+
const repositoryIssuesSchema = z.object({ repository: z.object({
|
|
826
|
+
name: z.string(),
|
|
827
|
+
owner: accountSchema,
|
|
828
|
+
issues: connectionOf(issueSchema)
|
|
829
|
+
}).nullable() });
|
|
830
|
+
/** A page of the repositories an account owns, by name, with what a scope picker shows of each. A sync reads the names; a scope listing reads the rest too, so both share one request. */
|
|
831
|
+
const ownerRepositoriesQuery = `query OwnerRepositories($login: String!, $first: Int!, $after: String) {
|
|
832
|
+
repositoryOwner(login: $login) {
|
|
833
|
+
${accountFields}
|
|
834
|
+
repositories(first: $first, after: $after, ownerAffiliations: [OWNER], orderBy: { field: NAME, direction: ASC }) {
|
|
835
|
+
${pageInfoFields}
|
|
836
|
+
nodes { name description url isArchived }
|
|
837
|
+
}
|
|
838
|
+
}
|
|
839
|
+
}`;
|
|
840
|
+
const repositoryEntrySchema = z.object({
|
|
841
|
+
name: z.string(),
|
|
842
|
+
description: z.string().nullable(),
|
|
843
|
+
url: z.url(),
|
|
844
|
+
isArchived: z.boolean()
|
|
845
|
+
});
|
|
846
|
+
const ownerRepositoriesSchema = z.object({ repositoryOwner: z.object({
|
|
847
|
+
__typename: z.enum(["Organization", "User"]),
|
|
848
|
+
login: z.string(),
|
|
849
|
+
repositories: connectionOf(repositoryEntrySchema)
|
|
850
|
+
}).nullable() });
|
|
851
|
+
/** The viewer's own account and a page of the organisations it belongs to, as a scope picker shows them. */
|
|
852
|
+
const viewerScopesQuery = `query ViewerScopes($first: Int!, $after: String) {
|
|
853
|
+
viewer {
|
|
854
|
+
login
|
|
855
|
+
name
|
|
856
|
+
url
|
|
857
|
+
organizations(first: $first, after: $after) {
|
|
858
|
+
${pageInfoFields}
|
|
859
|
+
nodes { login name description url archivedAt }
|
|
860
|
+
}
|
|
861
|
+
}
|
|
862
|
+
}`;
|
|
863
|
+
const organisationEntrySchema = z.object({
|
|
864
|
+
login: z.string(),
|
|
865
|
+
name: z.string().nullable(),
|
|
866
|
+
description: z.string().nullable(),
|
|
867
|
+
url: z.url(),
|
|
868
|
+
archivedAt: z.string().nullable()
|
|
869
|
+
});
|
|
870
|
+
const viewerScopesSchema = z.object({ viewer: z.object({
|
|
871
|
+
login: z.string(),
|
|
872
|
+
name: z.string().nullable(),
|
|
873
|
+
url: z.url(),
|
|
874
|
+
organizations: connectionOf(organisationEntrySchema)
|
|
875
|
+
}) });
|
|
876
|
+
/** A page of a Projects v2 board's items, each issue with the first page of its connections. Other kinds of item (pull requests, draft issues) are named only by type, since they are not issues. */
|
|
877
|
+
const projectItemsQuery = `query ProjectItems($login: String!, $number: Int!, $first: Int!, $after: String) {
|
|
878
|
+
repositoryOwner(login: $login) {
|
|
879
|
+
${accountFields}
|
|
880
|
+
... on ProjectV2Owner {
|
|
881
|
+
projectV2(number: $number) {
|
|
882
|
+
items(first: $first, after: $after) {
|
|
883
|
+
${pageInfoFields}
|
|
884
|
+
nodes {
|
|
885
|
+
content {
|
|
886
|
+
__typename
|
|
887
|
+
... on Issue {
|
|
888
|
+
${issueFields}
|
|
889
|
+
}
|
|
890
|
+
}
|
|
891
|
+
}
|
|
892
|
+
}
|
|
893
|
+
}
|
|
894
|
+
}
|
|
895
|
+
}
|
|
896
|
+
}`;
|
|
897
|
+
/** An item's content: an issue, or another kind of item named only by its type. */
|
|
898
|
+
const itemContentSchema = z.union([issueSchema.extend({ __typename: z.literal("Issue") }), z.object({ __typename: z.enum(["PullRequest", "DraftIssue"]) })]);
|
|
899
|
+
const projectItemsSchema = z.object({ repositoryOwner: z.object({
|
|
900
|
+
__typename: z.enum(["Organization", "User"]),
|
|
901
|
+
login: z.string(),
|
|
902
|
+
projectV2: z.object({ items: connectionOf(z.object({ content: itemContentSchema.nullable() })) }).nullable()
|
|
903
|
+
}).nullable() });
|
|
904
|
+
/** The rest of one issue connection after `after`. */
|
|
905
|
+
const issueConnectionQuery = (connection) => {
|
|
906
|
+
return `query IssueConnection($owner: String!, $name: String!, $number: Int!, $first: Int!, $after: String) {
|
|
907
|
+
repository(owner: $owner, name: $name) {
|
|
908
|
+
issue(number: $number) {
|
|
909
|
+
${connection}(first: $first, after: $after) { ${pageInfoFields} nodes { ${connection === "assignees" ? "login" : issueRefFields} } }
|
|
910
|
+
}
|
|
911
|
+
}
|
|
912
|
+
}`;
|
|
913
|
+
};
|
|
914
|
+
/** The data of an `issueConnectionQuery` response, for the connection `shape` names. */
|
|
915
|
+
const issueConnectionDataSchema = (shape) => z.object({ repository: z.object({ issue: z.object(shape).nullable() }).nullable() });
|
|
916
|
+
const assigneesConnectionSchema = issueConnectionDataSchema({ assignees: connectionOf(userSchema) });
|
|
917
|
+
const blockedByConnectionSchema = issueConnectionDataSchema({ blockedBy: connectionOf(issueRefSchema) });
|
|
918
|
+
const blockingConnectionSchema = issueConnectionDataSchema({ blocking: connectionOf(issueRefSchema) });
|
|
919
|
+
const subIssuesConnectionSchema = issueConnectionDataSchema({ subIssues: connectionOf(issueRefSchema) });
|
|
920
|
+
//#endregion
|
|
921
|
+
//#region ../../adapters/graph/source-github-issues/dist/source.js
|
|
922
|
+
/** GitHub's largest allowed `first` on a connection; the API rejects larger values. */
|
|
923
|
+
const githubMaxPageSize = 100;
|
|
924
|
+
/**
|
|
925
|
+
* Issues per page, and items per request of each issue's assignees, blockers and sub-issues. Half of GitHub's limit keeps a page's GraphQL cost and response moderate while needing few requests; Graphle used the same size.
|
|
926
|
+
*/
|
|
927
|
+
const defaultPageSize = githubMaxPageSize / 2;
|
|
928
|
+
/** GitHub's `IssueState` for each run state a filter selects by: an issue closed for any reason is done. */
|
|
929
|
+
const issueStates = {
|
|
930
|
+
open: "OPEN",
|
|
931
|
+
done: "CLOSED"
|
|
932
|
+
};
|
|
933
|
+
/** GitHub's `IssueOrderField` for each order field a filter names. */
|
|
934
|
+
const issueOrderFields = {
|
|
935
|
+
created: "CREATED_AT",
|
|
936
|
+
updated: "UPDATED_AT",
|
|
937
|
+
comments: "COMMENTS"
|
|
938
|
+
};
|
|
939
|
+
/** GitHub's `OrderDirection` for each direction a filter names. */
|
|
940
|
+
const orderDirections = {
|
|
941
|
+
ascending: "ASC",
|
|
942
|
+
descending: "DESC"
|
|
943
|
+
};
|
|
944
|
+
/**
|
|
945
|
+
* The issue listing's variables for a filter. With no filter, or where it says nothing, every issue is listed, oldest first: creation order keeps a cursor's position valid as new issues are opened.
|
|
946
|
+
*/
|
|
947
|
+
const issueListing = (filter) => ({
|
|
948
|
+
states: (filter?.states ?? ["open", "done"]).map((state) => issueStates[state]),
|
|
949
|
+
labels: filter?.labels ?? null,
|
|
950
|
+
orderBy: filter?.order === void 0 ? {
|
|
951
|
+
field: issueOrderFields.created,
|
|
952
|
+
direction: orderDirections.ascending
|
|
953
|
+
} : {
|
|
954
|
+
field: issueOrderFields[filter.order.field],
|
|
955
|
+
direction: orderDirections[filter.order.direction]
|
|
956
|
+
}
|
|
957
|
+
});
|
|
958
|
+
const present = (connection) => connection.nodes.filter((node) => node !== null);
|
|
959
|
+
const endCursorOf = (pageInfo) => pageInfo.hasNextPage && pageInfo.endCursor !== null ? pageInfo.endCursor : void 0;
|
|
960
|
+
/** A list a listing's gaps are recorded into. */
|
|
961
|
+
const gapList = () => {
|
|
962
|
+
const gaps = [];
|
|
963
|
+
return {
|
|
964
|
+
gaps,
|
|
965
|
+
sink: { gap: (gap) => {
|
|
966
|
+
gaps.push(gap);
|
|
967
|
+
} }
|
|
968
|
+
};
|
|
969
|
+
};
|
|
970
|
+
/** Records the problems of a response as one gap on `target`, when there are any. */
|
|
971
|
+
const recordProblems = (builder, target, problems) => {
|
|
972
|
+
const [first] = problems;
|
|
973
|
+
if (first !== void 0) builder.gap({
|
|
974
|
+
target,
|
|
975
|
+
reason: first.reason,
|
|
976
|
+
message: problems.map((problem) => problem.message).join("; ")
|
|
977
|
+
});
|
|
978
|
+
};
|
|
979
|
+
/**
|
|
980
|
+
* Records a scope GitHub answered with null as a gap. GitHub nulls a lookup with a `NOT_FOUND` or `FORBIDDEN` error when it can say why, and with no error at all for an owner login that does not exist, which is equally not found.
|
|
981
|
+
*/
|
|
982
|
+
const recordAbsence = (builder, target, problems) => {
|
|
983
|
+
if (problems.length === 0) builder.gap({
|
|
984
|
+
target,
|
|
985
|
+
reason: "not-found",
|
|
986
|
+
message: `${target} does not exist or is not visible to the token`
|
|
987
|
+
});
|
|
988
|
+
else recordProblems(builder, target, problems);
|
|
989
|
+
};
|
|
990
|
+
/**
|
|
991
|
+
* The GitHub issues `GraphSourcePager`. A page is one page of one repository's issues, or of one board's items: each issue becomes a bound activity (`github:owner/repo#n`) whose actor is its repository, each `blockedBy` and `blocking` entry a transition from the blocker to the blocked issue (one transition, whichever side it is read from), each sub-issue a `child-of` relation, each assignee an `assigned-to` relation, and the repository `part-of` its owner. Blockers, blocked issues and sub-issues in other repositories or organisations are included with their own repository and owner. Open issues are `open` in the overlay and closed ones `done`.
|
|
992
|
+
*
|
|
993
|
+
* Scopes are read in request order, each once: `github:owner/repo` for one repository, `github:owner` for every repository the account owns, by name, and `github:owner/projects/number` for the issues on an organisation's or user's Projects v2 board, in the board's order (its pull requests and draft issues are not issues, so they are passed over). A repository, owner or board that does not exist or that the token cannot see is a gap, and the sync continues. A filter narrows and orders each repository's issue listing on GitHub's side; a board has no such listing, so a filter with a board scope is a `bad-request` with `invalid-scope`.
|
|
994
|
+
*
|
|
995
|
+
* The pager also lists scopes: the viewer's own account and organisations at the top, and an owner's repositories, by name, inside it.
|
|
996
|
+
* @param options - The token port, and optionally fetch, clock and page size.
|
|
997
|
+
* @returns A pager for `createGraphSourceRouter`.
|
|
998
|
+
* @throws RangeError when `pageSize` is not an integer from 1 to 100.
|
|
999
|
+
*/
|
|
1000
|
+
const createGitHubIssuesSource = (options) => {
|
|
1001
|
+
const pageSize = options.pageSize ?? defaultPageSize;
|
|
1002
|
+
if (!Number.isInteger(pageSize) || pageSize < 1 || pageSize > githubMaxPageSize) throw new RangeError(`pageSize must be an integer from 1 to ${String(githubMaxPageSize)}, not ${String(pageSize)}`);
|
|
1003
|
+
const transport = {
|
|
1004
|
+
token: options.token,
|
|
1005
|
+
fetch: options.fetch ?? (async (url, init) => fetch(url, init)),
|
|
1006
|
+
now: options.now ?? Date.now
|
|
1007
|
+
};
|
|
1008
|
+
/** Every node of one issue connection: the page the issue query returned, then the rest read on their own. */
|
|
1009
|
+
const allOf = async (read, signal) => {
|
|
1010
|
+
const { key, connection, first, schema, extract } = read;
|
|
1011
|
+
const after = endCursorOf(first.pageInfo);
|
|
1012
|
+
if (after === void 0) return present(first);
|
|
1013
|
+
const rest = paginate(after, async (cursor) => {
|
|
1014
|
+
const { data } = await graphql(transport, {
|
|
1015
|
+
query: issueConnectionQuery(connection),
|
|
1016
|
+
variables: {
|
|
1017
|
+
...key,
|
|
1018
|
+
first: pageSize,
|
|
1019
|
+
after: cursor
|
|
1020
|
+
},
|
|
1021
|
+
owner: key.owner,
|
|
1022
|
+
schema
|
|
1023
|
+
}, signal);
|
|
1024
|
+
const page = extract(data);
|
|
1025
|
+
if (page === null || page === void 0) throw new Error(`${issueId(key.owner, key.name, key.number)} was not returned while its ${connection} were read`);
|
|
1026
|
+
const next = endCursorOf(page.pageInfo);
|
|
1027
|
+
return next === void 0 ? { nodes: present(page) } : {
|
|
1028
|
+
nodes: present(page),
|
|
1029
|
+
next
|
|
1030
|
+
};
|
|
1031
|
+
});
|
|
1032
|
+
const nodes = [...present(first)];
|
|
1033
|
+
for await (const page of rest) nodes.push(...page.nodes);
|
|
1034
|
+
return nodes;
|
|
1035
|
+
};
|
|
1036
|
+
const addIssue = async (builder, issue, signal) => {
|
|
1037
|
+
const id = builder.issue(issue);
|
|
1038
|
+
const key = {
|
|
1039
|
+
owner: issue.repository.owner.login,
|
|
1040
|
+
name: issue.repository.name,
|
|
1041
|
+
number: issue.number
|
|
1042
|
+
};
|
|
1043
|
+
const [assignees, blockers, blocked, children] = await Promise.all([
|
|
1044
|
+
allOf({
|
|
1045
|
+
key,
|
|
1046
|
+
connection: "assignees",
|
|
1047
|
+
first: issue.assignees,
|
|
1048
|
+
schema: assigneesConnectionSchema,
|
|
1049
|
+
extract: (data) => data.repository?.issue?.assignees
|
|
1050
|
+
}, signal),
|
|
1051
|
+
allOf({
|
|
1052
|
+
key,
|
|
1053
|
+
connection: "blockedBy",
|
|
1054
|
+
first: issue.blockedBy,
|
|
1055
|
+
schema: blockedByConnectionSchema,
|
|
1056
|
+
extract: (data) => data.repository?.issue?.blockedBy
|
|
1057
|
+
}, signal),
|
|
1058
|
+
allOf({
|
|
1059
|
+
key,
|
|
1060
|
+
connection: "blocking",
|
|
1061
|
+
first: issue.blocking,
|
|
1062
|
+
schema: blockingConnectionSchema,
|
|
1063
|
+
extract: (data) => data.repository?.issue?.blocking
|
|
1064
|
+
}, signal),
|
|
1065
|
+
allOf({
|
|
1066
|
+
key,
|
|
1067
|
+
connection: "subIssues",
|
|
1068
|
+
first: issue.subIssues,
|
|
1069
|
+
schema: subIssuesConnectionSchema,
|
|
1070
|
+
extract: (data) => data.repository?.issue?.subIssues
|
|
1071
|
+
}, signal)
|
|
1072
|
+
]);
|
|
1073
|
+
for (const user of assignees) builder.assignee(id, user);
|
|
1074
|
+
for (const blocker of blockers) builder.blockedBy(id, blocker);
|
|
1075
|
+
for (const issueBlocked of blocked) builder.blocking(id, issueBlocked);
|
|
1076
|
+
for (const child of children) builder.subIssue(id, child);
|
|
1077
|
+
};
|
|
1078
|
+
/**
|
|
1079
|
+
* Adds one page of a repository's issues to the page.
|
|
1080
|
+
* @returns The issues' page info, or `undefined` when the repository was a gap.
|
|
1081
|
+
*/
|
|
1082
|
+
const readRepository = async (builder, read, signal) => {
|
|
1083
|
+
const { target, repository, after, filter } = read;
|
|
1084
|
+
const { data, problems } = await graphql(transport, {
|
|
1085
|
+
query: repositoryIssuesQuery,
|
|
1086
|
+
variables: {
|
|
1087
|
+
...repository,
|
|
1088
|
+
first: pageSize,
|
|
1089
|
+
after,
|
|
1090
|
+
...issueListing(filter)
|
|
1091
|
+
},
|
|
1092
|
+
owner: repository.owner,
|
|
1093
|
+
schema: repositoryIssuesSchema
|
|
1094
|
+
}, signal);
|
|
1095
|
+
if (data.repository === null) {
|
|
1096
|
+
recordAbsence(builder, target, problems);
|
|
1097
|
+
return;
|
|
1098
|
+
}
|
|
1099
|
+
recordProblems(builder, target, problems);
|
|
1100
|
+
builder.repository(data.repository.owner, data.repository.name);
|
|
1101
|
+
await Promise.all(present(data.repository.issues).map(async (issue) => addIssue(builder, issue, signal)));
|
|
1102
|
+
return data.repository.issues.pageInfo;
|
|
1103
|
+
};
|
|
1104
|
+
/**
|
|
1105
|
+
* Adds one page of a board's items to the page: each issue on it, with its connections.
|
|
1106
|
+
* @returns The items' page info, or `undefined` when the owner or board was a gap.
|
|
1107
|
+
*/
|
|
1108
|
+
const readProject = async (builder, project, after, signal) => {
|
|
1109
|
+
const { data, problems } = await graphql(transport, {
|
|
1110
|
+
query: projectItemsQuery,
|
|
1111
|
+
variables: {
|
|
1112
|
+
login: project.owner,
|
|
1113
|
+
number: project.number,
|
|
1114
|
+
first: pageSize,
|
|
1115
|
+
after
|
|
1116
|
+
},
|
|
1117
|
+
owner: project.owner,
|
|
1118
|
+
schema: projectItemsSchema
|
|
1119
|
+
}, signal);
|
|
1120
|
+
const board = data.repositoryOwner?.projectV2;
|
|
1121
|
+
if (board === null || board === void 0) {
|
|
1122
|
+
recordAbsence(builder, project.id, problems);
|
|
1123
|
+
return;
|
|
1124
|
+
}
|
|
1125
|
+
recordProblems(builder, project.id, problems);
|
|
1126
|
+
const issues = present(board.items).flatMap(({ content }) => content?.__typename === "Issue" ? [content] : []);
|
|
1127
|
+
await Promise.all(issues.map(async (issue) => addIssue(builder, issue, signal)));
|
|
1128
|
+
return board.items.pageInfo;
|
|
1129
|
+
};
|
|
1130
|
+
const read = async ({ scope, cursor, signal, filter }) => {
|
|
1131
|
+
const scopes = [...new Set(scope)].map(parseScope);
|
|
1132
|
+
const [firstScope] = scopes;
|
|
1133
|
+
if (firstScope === void 0) throw new Error("unreachable: the contract requires at least one scope");
|
|
1134
|
+
const board = scopes.find((candidate) => candidate.kind === "project");
|
|
1135
|
+
if (filter !== void 0 && board !== void 0) throw new GraphSourceError({
|
|
1136
|
+
kind: "bad-request",
|
|
1137
|
+
message: `"${board.id}" is a Projects v2 board, which lists its items in the board's own order and cannot be filtered; sync it without a filter`,
|
|
1138
|
+
reason: "invalid-scope",
|
|
1139
|
+
value: board.id
|
|
1140
|
+
});
|
|
1141
|
+
const located = cursor === void 0 ? startOf(firstScope) : decodeCursor(cursor, scopes, filter);
|
|
1142
|
+
const following = scopes[scopes.indexOf(located.scope) + 1];
|
|
1143
|
+
const nextScope = following === void 0 ? void 0 : startOf(following).position;
|
|
1144
|
+
const builder = createPageBuilder();
|
|
1145
|
+
const build = (next) => builder.build(next === void 0 ? void 0 : encodeCursor(next, filter));
|
|
1146
|
+
if (located.kind === "project") {
|
|
1147
|
+
const { scope: current, position } = located;
|
|
1148
|
+
const items = await readProject(builder, current, position.items, signal);
|
|
1149
|
+
const after = items === void 0 ? void 0 : endCursorOf(items);
|
|
1150
|
+
return build(after === void 0 ? nextScope : {
|
|
1151
|
+
scope: current.id,
|
|
1152
|
+
items: after
|
|
1153
|
+
});
|
|
1154
|
+
}
|
|
1155
|
+
if (located.kind === "repository") {
|
|
1156
|
+
const { scope: current, position } = located;
|
|
1157
|
+
const issues = await readRepository(builder, {
|
|
1158
|
+
target: current.id,
|
|
1159
|
+
repository: {
|
|
1160
|
+
owner: current.owner,
|
|
1161
|
+
name: current.name
|
|
1162
|
+
},
|
|
1163
|
+
after: position.issues,
|
|
1164
|
+
filter
|
|
1165
|
+
}, signal);
|
|
1166
|
+
const after = issues === void 0 ? void 0 : endCursorOf(issues);
|
|
1167
|
+
return build(after === void 0 ? nextScope : {
|
|
1168
|
+
scope: current.id,
|
|
1169
|
+
issues: after
|
|
1170
|
+
});
|
|
1171
|
+
}
|
|
1172
|
+
const { scope: current, position } = located;
|
|
1173
|
+
const { data, problems } = await graphql(transport, {
|
|
1174
|
+
query: ownerRepositoriesQuery,
|
|
1175
|
+
variables: {
|
|
1176
|
+
login: current.owner,
|
|
1177
|
+
first: pageSize,
|
|
1178
|
+
after: position.repositories
|
|
1179
|
+
},
|
|
1180
|
+
owner: current.owner,
|
|
1181
|
+
schema: ownerRepositoriesSchema
|
|
1182
|
+
}, signal);
|
|
1183
|
+
const owner = data.repositoryOwner;
|
|
1184
|
+
if (owner === null) {
|
|
1185
|
+
recordAbsence(builder, current.id, problems);
|
|
1186
|
+
return build(nextScope);
|
|
1187
|
+
}
|
|
1188
|
+
recordProblems(builder, current.id, problems);
|
|
1189
|
+
builder.account(owner);
|
|
1190
|
+
const listed = present(owner.repositories);
|
|
1191
|
+
const listingAfter = endCursorOf(owner.repositories.pageInfo);
|
|
1192
|
+
const afterListing = listingAfter === void 0 ? nextScope : {
|
|
1193
|
+
scope: current.id,
|
|
1194
|
+
repositories: listingAfter,
|
|
1195
|
+
repository: 0,
|
|
1196
|
+
issues: null
|
|
1197
|
+
};
|
|
1198
|
+
const repository = listed[position.repository];
|
|
1199
|
+
if (repository === void 0) return build(afterListing);
|
|
1200
|
+
const issues = await readRepository(builder, {
|
|
1201
|
+
target: `${current.id}/${repository.name}`,
|
|
1202
|
+
repository: {
|
|
1203
|
+
owner: owner.login,
|
|
1204
|
+
name: repository.name
|
|
1205
|
+
},
|
|
1206
|
+
after: position.issues,
|
|
1207
|
+
filter
|
|
1208
|
+
}, signal);
|
|
1209
|
+
const issuesAfter = issues === void 0 ? void 0 : endCursorOf(issues);
|
|
1210
|
+
const afterRepository = position.repository + 1 < listed.length ? {
|
|
1211
|
+
...position,
|
|
1212
|
+
repository: position.repository + 1,
|
|
1213
|
+
issues: null
|
|
1214
|
+
} : afterListing;
|
|
1215
|
+
return build(issuesAfter === void 0 ? afterRepository : {
|
|
1216
|
+
...position,
|
|
1217
|
+
issues: issuesAfter
|
|
1218
|
+
});
|
|
1219
|
+
};
|
|
1220
|
+
/** A listing page from its entries, gaps and GitHub's page info, resuming under `parent`. */
|
|
1221
|
+
const listingOf = (parent, entries, gaps, pageInfo) => {
|
|
1222
|
+
const after = pageInfo === void 0 ? void 0 : endCursorOf(pageInfo);
|
|
1223
|
+
return after === void 0 ? {
|
|
1224
|
+
entries,
|
|
1225
|
+
gaps
|
|
1226
|
+
} : {
|
|
1227
|
+
entries,
|
|
1228
|
+
gaps,
|
|
1229
|
+
next: encodeListingCursor(parent, after)
|
|
1230
|
+
};
|
|
1231
|
+
};
|
|
1232
|
+
/** A scope entry, with the description only when GitHub has one. */
|
|
1233
|
+
const entryOf = (scope, label, details) => ({
|
|
1234
|
+
scope,
|
|
1235
|
+
label,
|
|
1236
|
+
...details.description === null ? {} : { description: details.description },
|
|
1237
|
+
url: details.url,
|
|
1238
|
+
archived: details.archived
|
|
1239
|
+
});
|
|
1240
|
+
const organisationEntry = (organisation) => entryOf(ownerScopeId(organisation.login), organisation.name ?? organisation.login, {
|
|
1241
|
+
description: organisation.description,
|
|
1242
|
+
url: organisation.url,
|
|
1243
|
+
archived: organisation.archivedAt !== null
|
|
1244
|
+
});
|
|
1245
|
+
const repositoryEntry = (owner, repository) => entryOf(repositoryId(owner, repository.name), repository.name, {
|
|
1246
|
+
description: repository.description,
|
|
1247
|
+
url: repository.url,
|
|
1248
|
+
archived: repository.isArchived
|
|
1249
|
+
});
|
|
1250
|
+
return {
|
|
1251
|
+
source: githubSource,
|
|
1252
|
+
page: read,
|
|
1253
|
+
scopes: {
|
|
1254
|
+
roots: async ({ cursor, signal }) => {
|
|
1255
|
+
const after = cursor === void 0 ? null : decodeListingCursor(cursor, null);
|
|
1256
|
+
const { data, problems } = await graphql(transport, {
|
|
1257
|
+
query: viewerScopesQuery,
|
|
1258
|
+
variables: {
|
|
1259
|
+
first: pageSize,
|
|
1260
|
+
after
|
|
1261
|
+
},
|
|
1262
|
+
owner: void 0,
|
|
1263
|
+
schema: viewerScopesSchema
|
|
1264
|
+
}, signal);
|
|
1265
|
+
const { viewer } = data;
|
|
1266
|
+
const { gaps, sink } = gapList();
|
|
1267
|
+
recordProblems(sink, ownerScopeId(viewer.login), problems);
|
|
1268
|
+
const own = after === null ? [entryOf(ownerScopeId(viewer.login), viewer.name ?? viewer.login, {
|
|
1269
|
+
description: null,
|
|
1270
|
+
url: viewer.url,
|
|
1271
|
+
archived: false
|
|
1272
|
+
})] : [];
|
|
1273
|
+
return listingOf(null, [...own, ...present(viewer.organizations).map(organisationEntry)], gaps, viewer.organizations.pageInfo);
|
|
1274
|
+
},
|
|
1275
|
+
children: async ({ scope, cursor, signal }) => {
|
|
1276
|
+
const parent = parseScope(scope);
|
|
1277
|
+
if (parent.kind !== "owner") {
|
|
1278
|
+
if (cursor !== void 0) throw new GraphSourceError({
|
|
1279
|
+
kind: "bad-request",
|
|
1280
|
+
message: `"${cursor}" is not a cursor this source issued: ${scope} has no scopes inside it`,
|
|
1281
|
+
reason: "invalid-cursor",
|
|
1282
|
+
value: cursor
|
|
1283
|
+
});
|
|
1284
|
+
return {
|
|
1285
|
+
entries: [],
|
|
1286
|
+
gaps: []
|
|
1287
|
+
};
|
|
1288
|
+
}
|
|
1289
|
+
const after = cursor === void 0 ? null : decodeListingCursor(cursor, scope);
|
|
1290
|
+
const { data, problems } = await graphql(transport, {
|
|
1291
|
+
query: ownerRepositoriesQuery,
|
|
1292
|
+
variables: {
|
|
1293
|
+
login: parent.owner,
|
|
1294
|
+
first: pageSize,
|
|
1295
|
+
after
|
|
1296
|
+
},
|
|
1297
|
+
owner: parent.owner,
|
|
1298
|
+
schema: ownerRepositoriesSchema
|
|
1299
|
+
}, signal);
|
|
1300
|
+
const { gaps, sink } = gapList();
|
|
1301
|
+
const owner = data.repositoryOwner;
|
|
1302
|
+
if (owner === null) {
|
|
1303
|
+
recordAbsence(sink, scope, problems);
|
|
1304
|
+
return {
|
|
1305
|
+
entries: [],
|
|
1306
|
+
gaps
|
|
1307
|
+
};
|
|
1308
|
+
}
|
|
1309
|
+
recordProblems(sink, scope, problems);
|
|
1310
|
+
return listingOf(scope, present(owner.repositories).map((repository) => repositoryEntry(owner.login, repository)), gaps, owner.repositories.pageInfo);
|
|
1311
|
+
}
|
|
1312
|
+
}
|
|
1313
|
+
};
|
|
1314
|
+
};
|
|
1315
|
+
//#endregion
|
|
1316
|
+
export { syncPageSchema as A, scopeChildrenInputSchema as C, sourceDescriptionSchema as D, scopeRootsInputSchema as E, syncFilterSchema as O, relationSchema as S, scopeListingSchema as T, gapSchema as _, ownerScopeId as a, participantSchema as b, githubGraphqlEndpoint as c, paginate as d, graphSourceContract as f, gapReasons as g, filterStates as h, issueId as i, transitionSchema as j, syncInputSchema as k, assembleSync as l, boundActivitySchema as m, accountId as n, parseScope as o, badRequestReasons as p, githubSource as r, repositoryId as s, createGitHubIssuesSource as t, createGraphSourceRouter as u, orderDirections$1 as v, scopeEntrySchema as w, relationKinds as x, orderFields as y };
|