@volter/twin-github 0.1.1 → 2.0.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.
Files changed (215) hide show
  1. package/README.md +88 -36
  2. package/client/github-mirror.css +277 -319
  3. package/client/github-mirror.d.ts +418 -0
  4. package/client/github-mirror.js +485 -0
  5. package/client/github-mirror.tsx +153 -357
  6. package/client/pulls-rest.ts +159 -0
  7. package/client/pulls-workspace.tsx +841 -0
  8. package/dist/client/github-mirror.bundle.js +239 -0
  9. package/dist/client/github-mirror.css +916 -0
  10. package/dist/client/github-mirror.d.ts +418 -0
  11. package/dist/client/github-mirror.js +485 -0
  12. package/dist/client/github-mirror.tsx +1315 -0
  13. package/dist/client/pulls-rest.d.ts +42 -0
  14. package/dist/client/pulls-rest.js +140 -0
  15. package/dist/client/pulls-rest.ts +159 -0
  16. package/dist/client/pulls-workspace.bundle.js +22 -0
  17. package/dist/client/pulls-workspace.d.ts +114 -0
  18. package/dist/client/pulls-workspace.js +418 -0
  19. package/dist/client/pulls-workspace.tsx +841 -0
  20. package/dist/src/cli.d.ts +2 -0
  21. package/dist/src/cli.js +40 -0
  22. package/dist/src/generated/graphql-sdl.gen.json +1 -0
  23. package/dist/src/generated/graphql.gen.json +1 -0
  24. package/dist/src/generated/surface.gen.json +1 -0
  25. package/dist/src/generated/ui.gen.json +1 -0
  26. package/dist/src/github-budget.d.ts +69 -0
  27. package/dist/src/github-budget.js +172 -0
  28. package/dist/src/github-capabilities.d.ts +5 -0
  29. package/dist/src/github-capabilities.js +4468 -0
  30. package/dist/src/github-conformance.d.ts +43 -0
  31. package/dist/src/github-conformance.js +76 -0
  32. package/dist/src/github-connector.d.ts +307 -0
  33. package/dist/src/github-connector.js +1398 -0
  34. package/dist/src/github-events.d.ts +41 -0
  35. package/dist/src/github-events.js +230 -0
  36. package/dist/src/github-git-http.d.ts +49 -0
  37. package/dist/src/github-git-http.js +185 -0
  38. package/dist/src/github-git-plane.d.ts +114 -0
  39. package/dist/src/github-git-plane.js +407 -0
  40. package/dist/src/github-mirror-state.d.ts +2 -0
  41. package/dist/src/github-mirror-state.js +335 -0
  42. package/dist/src/github-mirror-ui.d.ts +20 -0
  43. package/dist/src/github-mirror-ui.js +101 -0
  44. package/dist/src/github-server.d.ts +14 -0
  45. package/dist/src/github-server.js +240 -0
  46. package/dist/src/github-shared.d.ts +12 -0
  47. package/dist/src/github-shared.js +21 -0
  48. package/dist/src/github-twin.d.ts +1411 -0
  49. package/dist/src/github-twin.js +4084 -0
  50. package/dist/src/github-ui-conformance.d.ts +4 -0
  51. package/dist/src/github-ui-conformance.js +105 -0
  52. package/dist/src/github-ui-structure.d.ts +18 -0
  53. package/dist/src/github-ui-structure.js +251 -0
  54. package/dist/src/graphql-wire.d.ts +22 -0
  55. package/dist/src/graphql-wire.js +89 -0
  56. package/dist/src/index.d.ts +17 -0
  57. package/dist/src/index.js +101 -0
  58. package/dist/src/manifest.d.ts +8 -0
  59. package/dist/src/manifest.js +597 -0
  60. package/dist/src/npm-registry.d.ts +7 -0
  61. package/dist/src/npm-registry.js +47 -0
  62. package/dist/src/screens/app-installation.d.ts +3 -0
  63. package/dist/src/screens/app-installation.js +166 -0
  64. package/dist/src/screens/app-manifest.d.ts +3 -0
  65. package/dist/src/screens/app-manifest.js +81 -0
  66. package/dist/src/screens/oauth.d.ts +15 -0
  67. package/dist/src/screens/oauth.js +257 -0
  68. package/dist/src/screens/session.d.ts +8 -0
  69. package/dist/src/screens/session.js +159 -0
  70. package/dist/src/semantics/actions.d.ts +2 -0
  71. package/dist/src/semantics/actions.js +413 -0
  72. package/dist/src/semantics/activity.d.ts +4 -0
  73. package/dist/src/semantics/activity.js +161 -0
  74. package/dist/src/semantics/apps.d.ts +2 -0
  75. package/dist/src/semantics/apps.js +144 -0
  76. package/dist/src/semantics/branches.d.ts +2 -0
  77. package/dist/src/semantics/branches.js +136 -0
  78. package/dist/src/semantics/checks.d.ts +2 -0
  79. package/dist/src/semantics/checks.js +176 -0
  80. package/dist/src/semantics/code-scanning-upload.d.ts +2 -0
  81. package/dist/src/semantics/code-scanning-upload.js +97 -0
  82. package/dist/src/semantics/codespaces.d.ts +2 -0
  83. package/dist/src/semantics/codespaces.js +58 -0
  84. package/dist/src/semantics/commits.d.ts +2 -0
  85. package/dist/src/semantics/commits.js +109 -0
  86. package/dist/src/semantics/contents.d.ts +2 -0
  87. package/dist/src/semantics/contents.js +131 -0
  88. package/dist/src/semantics/deployments.d.ts +2 -0
  89. package/dist/src/semantics/deployments.js +127 -0
  90. package/dist/src/semantics/gists.d.ts +2 -0
  91. package/dist/src/semantics/gists.js +86 -0
  92. package/dist/src/semantics/git.d.ts +2 -0
  93. package/dist/src/semantics/git.js +235 -0
  94. package/dist/src/semantics/graphql.d.ts +7 -0
  95. package/dist/src/semantics/graphql.js +512 -0
  96. package/dist/src/semantics/index.d.ts +5 -0
  97. package/dist/src/semantics/index.js +62 -0
  98. package/dist/src/semantics/issues.d.ts +2 -0
  99. package/dist/src/semantics/issues.js +456 -0
  100. package/dist/src/semantics/keys.d.ts +2 -0
  101. package/dist/src/semantics/keys.js +66 -0
  102. package/dist/src/semantics/labels.d.ts +2 -0
  103. package/dist/src/semantics/labels.js +100 -0
  104. package/dist/src/semantics/meta.d.ts +12 -0
  105. package/dist/src/semantics/meta.js +144 -0
  106. package/dist/src/semantics/notifications.d.ts +4 -0
  107. package/dist/src/semantics/notifications.js +62 -0
  108. package/dist/src/semantics/orgs.d.ts +2 -0
  109. package/dist/src/semantics/orgs.js +420 -0
  110. package/dist/src/semantics/packages.d.ts +2 -0
  111. package/dist/src/semantics/packages.js +55 -0
  112. package/dist/src/semantics/pages.d.ts +2 -0
  113. package/dist/src/semantics/pages.js +176 -0
  114. package/dist/src/semantics/projects.d.ts +2 -0
  115. package/dist/src/semantics/projects.js +239 -0
  116. package/dist/src/semantics/pulls.d.ts +2 -0
  117. package/dist/src/semantics/pulls.js +462 -0
  118. package/dist/src/semantics/push-reactions.d.ts +101 -0
  119. package/dist/src/semantics/push-reactions.js +513 -0
  120. package/dist/src/semantics/releases.d.ts +19 -0
  121. package/dist/src/semantics/releases.js +231 -0
  122. package/dist/src/semantics/repo-invitations.d.ts +2 -0
  123. package/dist/src/semantics/repo-invitations.js +98 -0
  124. package/dist/src/semantics/repos.d.ts +17 -0
  125. package/dist/src/semantics/repos.js +390 -0
  126. package/dist/src/semantics/rulesets.d.ts +2 -0
  127. package/dist/src/semantics/rulesets.js +111 -0
  128. package/dist/src/semantics/search.d.ts +2 -0
  129. package/dist/src/semantics/search.js +84 -0
  130. package/dist/src/semantics/security.d.ts +2 -0
  131. package/dist/src/semantics/security.js +177 -0
  132. package/dist/src/semantics/shared.d.ts +61 -0
  133. package/dist/src/semantics/shared.js +151 -0
  134. package/dist/src/semantics/users.d.ts +2 -0
  135. package/dist/src/semantics/users.js +255 -0
  136. package/dist/src/semantics/webhooks.d.ts +2 -0
  137. package/dist/src/semantics/webhooks.js +107 -0
  138. package/dist/test-fixtures/github-a11y-reference.pr-list.SOURCE.md +56 -0
  139. package/dist/test-fixtures/github-a11y-reference.pr-list.json +1447 -0
  140. package/dist/test-fixtures/github-comment-schema.SOURCE.md +11 -0
  141. package/dist/test-fixtures/github-comment-schema.json +702 -0
  142. package/dist/test-fixtures/github-known-deviations.json +94 -0
  143. package/dist/test-fixtures/github-openapi-operations.SOURCE.md +76 -0
  144. package/dist/test-fixtures/github-openapi-operations.json +362 -0
  145. package/dist/test-fixtures/github-pull-schema.SOURCE.md +37 -0
  146. package/dist/test-fixtures/github-pull-schema.json +3601 -0
  147. package/dist/test-fixtures/github-review-schema.SOURCE.md +12 -0
  148. package/dist/test-fixtures/github-review-schema.json +236 -0
  149. package/package.json +20 -11
  150. package/src/cli.ts +7 -6
  151. package/src/generated/graphql-sdl.gen.json +1 -0
  152. package/src/generated/graphql.gen.json +1 -0
  153. package/src/generated/surface.gen.json +1 -0
  154. package/src/generated/ui.gen.json +1 -0
  155. package/src/github-a11y-snapshot.uitest.ts +5 -5
  156. package/src/github-budget.ts +4 -4
  157. package/src/github-capabilities.ts +1993 -556
  158. package/src/github-conformance.ts +12 -7
  159. package/src/github-connector.ts +108 -96
  160. package/src/github-events.ts +223 -95
  161. package/src/github-git-http.ts +47 -83
  162. package/src/github-git-plane.ts +247 -385
  163. package/src/github-journey.uitest.ts +28 -44
  164. package/src/github-mirror-state.ts +23 -61
  165. package/src/github-mirror-ui.ts +16 -10
  166. package/src/github-server.ts +158 -60
  167. package/src/github-shared.ts +1 -1
  168. package/src/github-twin.ts +1525 -4494
  169. package/src/github-ui-conformance.ts +7 -8
  170. package/src/github-ui-structure.ts +19 -18
  171. package/src/graphql-wire.ts +115 -0
  172. package/src/index.ts +47 -7
  173. package/src/manifest.ts +605 -0
  174. package/src/npm-registry.ts +43 -0
  175. package/src/screens/app-installation.tsx +217 -0
  176. package/src/screens/app-manifest.tsx +97 -0
  177. package/src/screens/oauth.tsx +258 -0
  178. package/src/screens/session.tsx +186 -0
  179. package/src/semantics/actions.ts +395 -0
  180. package/src/semantics/activity.ts +171 -0
  181. package/src/semantics/apps.ts +133 -0
  182. package/src/semantics/branches.ts +133 -0
  183. package/src/semantics/checks.ts +163 -0
  184. package/src/semantics/code-scanning-upload.ts +92 -0
  185. package/src/semantics/codespaces.ts +58 -0
  186. package/src/semantics/commits.ts +109 -0
  187. package/src/semantics/contents.ts +112 -0
  188. package/src/semantics/deployments.ts +116 -0
  189. package/src/semantics/gists.ts +85 -0
  190. package/src/semantics/git.ts +226 -0
  191. package/src/semantics/graphql.ts +505 -0
  192. package/src/semantics/index.ts +68 -0
  193. package/src/semantics/issues.ts +434 -0
  194. package/src/semantics/keys.ts +66 -0
  195. package/src/semantics/labels.ts +91 -0
  196. package/src/semantics/meta.ts +139 -0
  197. package/src/semantics/notifications.ts +58 -0
  198. package/src/semantics/orgs.ts +383 -0
  199. package/src/semantics/packages.ts +59 -0
  200. package/src/semantics/pages.ts +154 -0
  201. package/src/semantics/projects.ts +246 -0
  202. package/src/semantics/pulls.ts +421 -0
  203. package/src/semantics/push-reactions.ts +471 -0
  204. package/src/semantics/releases.ts +213 -0
  205. package/src/semantics/repo-invitations.ts +112 -0
  206. package/src/semantics/repos.ts +373 -0
  207. package/src/semantics/rulesets.ts +107 -0
  208. package/src/semantics/search.ts +83 -0
  209. package/src/semantics/security.ts +153 -0
  210. package/src/semantics/shared.ts +181 -0
  211. package/src/semantics/users.ts +252 -0
  212. package/src/semantics/webhooks.ts +101 -0
  213. package/test-fixtures/github-known-deviations.json +7 -8
  214. package/test-fixtures/github-openapi-operations.json +46 -227
  215. package/src/github-graphql.ts +0 -398
@@ -0,0 +1,59 @@
1
+ // GitHub's packages family, for the authenticated user: list, read and delete packages and their
2
+ // versions, newest version first. Packages are published to GitHub's npm registry (src/npm-registry.ts);
3
+ // the twin's user is `octocat`.
4
+ import type { Semantics, SemanticsContext } from '@volter/world-core';
5
+ import { githubNotFound, paginate, toGithubPackage, toGithubPackageVersion, type GithubPackage, type GithubState } from '../github-twin.ts';
6
+ import { gh, put, send } from './shared.ts';
7
+
8
+ const versionCount = (st: GithubState, p: GithubPackage): number => st.packageVersions.filter((v) => v.owner === p.owner && v.package_type === p.package_type && v.package_name === p.name).length;
9
+
10
+ /** A read or write under /user/packages/:type/:name: Not Found when the package does not exist. */
11
+ const underPackage = (serve: (ctx: SemanticsContext, st: GithubState, pkg: GithubPackage, type: string, name: string) => Promise<Response>): Semantics => async (ctx) => {
12
+ const { seg, state } = gh(ctx);
13
+ const type = decodeURIComponent(seg[2]!);
14
+ const name = decodeURIComponent(seg[3]!);
15
+ const st = state();
16
+ const pkg = st.packages.find((p) => p.owner === 'octocat' && p.package_type === type && p.name === name);
17
+ return pkg ? serve(ctx, st, pkg, type, name) : send(ctx, githubNotFound());
18
+ };
19
+
20
+ export function packagesSemantics(): Record<string, Semantics> {
21
+ const packagesListPackagesForAuthenticatedUser: Semantics = async (ctx) => {
22
+ const { query, state } = gh(ctx);
23
+ const st = state();
24
+ const pkgType = new URLSearchParams((query ?? '').replace(/^\?/, '')).get('package_type');
25
+ let items = st.packages.filter((p) => p.owner === 'octocat');
26
+ if (pkgType) items = items.filter((p) => p.package_type === pkgType);
27
+ return ctx.reply(paginate(items, query).map((p) => toGithubPackage(p, versionCount(st, p))));
28
+ };
29
+ const packagesDeletePackageForAuthenticatedUser: Semantics = (ctx) => (underPackage(async (ctx, _st, pkg, type, name) => {
30
+ await put(ctx, { operation: 'package.delete', subjectType: 'package', subjectId: `package:${pkg.id}`, fields: { deleted: true, name } });
31
+ for (const v of gh(ctx).state().packageVersions.filter((x) => x.owner === 'octocat' && x.package_type === type && x.package_name === name)) {
32
+ await put(ctx, { operation: 'package_version.delete', subjectType: 'package_version', subjectId: `packageversion:${v.id}`, fields: { deleted: true, package_name: name } });
33
+ }
34
+ return send(ctx, { status: 204, body: undefined });
35
+ }))(ctx);
36
+ const packagesDeletePackageVersionForAuthenticatedUser: Semantics = async (ctx) => {
37
+ const { seg, state } = gh(ctx);
38
+ const type = decodeURIComponent(seg[2]!); const name = decodeURIComponent(seg[3]!); const vid = Number(decodeURIComponent(seg[5]!));
39
+ if (!state().packageVersions.find((x) => x.owner === 'octocat' && x.package_type === type && x.package_name === name && x.id === vid)) return send(ctx, githubNotFound());
40
+ await put(ctx, { operation: 'package_version.delete', subjectType: 'package_version', subjectId: `packageversion:${vid}`, fields: { deleted: true, package_name: name } });
41
+ return send(ctx, { status: 204, body: undefined });
42
+ };
43
+ const packagesGetAllPackageVersionsForPackageOwnedByAuthenticatedUser: Semantics = (ctx) => (underPackage(async (ctx, st, _pkg, type, name) =>
44
+ ctx.reply(paginate(st.packageVersions.filter((v) => v.owner === 'octocat' && v.package_type === type && v.package_name === name).sort((a, b) => String(b.created_at ?? '').localeCompare(String(a.created_at ?? '')) || b.id - a.id), gh(ctx).query).map(toGithubPackageVersion))))(ctx);
45
+ const packagesGetPackageVersionForAuthenticatedUser: Semantics = (ctx) => (underPackage(async (ctx, st, _pkg, type, name) => {
46
+ const vid = Number(decodeURIComponent(gh(ctx).seg[5]!));
47
+ const v = st.packageVersions.find((x) => x.owner === 'octocat' && x.package_type === type && x.package_name === name && x.id === vid);
48
+ return send(ctx, v ? { status: 200, body: toGithubPackageVersion(v) } : githubNotFound());
49
+ }))(ctx);
50
+ return {
51
+ 'packages/list-packages-for-authenticated-user': packagesListPackagesForAuthenticatedUser,
52
+ 'packages/get-package-for-authenticated-user': underPackage(async (ctx, st, pkg) => ctx.reply(toGithubPackage(pkg, versionCount(st, pkg)))),
53
+ 'packages/get-all-package-versions-for-package-owned-by-authenticated-user': packagesGetAllPackageVersionsForPackageOwnedByAuthenticatedUser,
54
+ 'packages/get-package-version-for-authenticated-user': packagesGetPackageVersionForAuthenticatedUser,
55
+ // deleting a package deletes its versions
56
+ 'packages/delete-package-for-authenticated-user': packagesDeletePackageForAuthenticatedUser,
57
+ 'packages/delete-package-version-for-authenticated-user': packagesDeletePackageVersionForAuthenticatedUser,
58
+ };
59
+ }
@@ -0,0 +1,154 @@
1
+ // GitHub's Pages family: a repository's Pages site (create, read, update, delete) and its builds. A
2
+ // requested build is recorded built at once (the twin publishes nothing).
3
+ import { createHash } from 'node:crypto';
4
+ import type { Semantics, SemanticsContext } from '@volter/world-core';
5
+ import { GITHUB_WORLD_USER, githubNotFound, paginate, toGithubPagesBuild, toGithubPagesSite } from '../github-twin.ts';
6
+ import { gh, nextSeq, put, send } from './shared.ts';
7
+ import { canWrite } from './push-reactions.ts';
8
+
9
+ /** POST creates the site (409 when enabled), PUT edits it (404 when not): the source branch and path,
10
+ * the custom domain, the build type and HTTPS enforcement. */
11
+ async function writeSite(ctx: SemanticsContext, creating: boolean): Promise<Response> {
12
+ const { repo, body, occurredAt, state } = gh(ctx);
13
+ // Pages belongs to a repository: none, no site (docs.github.com/rest/pages/pages#create-a-apiname-pages-site)
14
+ if (!state().repos.some((r) => r.full_name === repo)) return send(ctx, githubNotFound());
15
+ const existing = state().pagesSites.find((s) => s.repository === repo);
16
+ if (creating && existing) return ctx.reply({ message: 'Pages is already enabled for this repository.' }, 409);
17
+ if (!creating && !existing) return send(ctx, githubNotFound());
18
+ const src = body.source ?? {};
19
+ const fields: Record<string, unknown> = { repository: repo, updated_at: occurredAt };
20
+ if (creating) fields.created_at = occurredAt;
21
+ if (src.branch !== undefined) fields.source_branch = String(src.branch);
22
+ if (src.path !== undefined) fields.source_path = String(src.path);
23
+ // a new custom domain starts its certificate and turns HTTPS enforcement off until it is issued; enforcing it
24
+ // before then is refused (docs.github.com/pages/getting-started-with-github-pages/securing-your-github-pages-site-with-https)
25
+ const domainChanged = body.cname !== undefined && body.cname !== (existing?.cname ?? null);
26
+ if (body.cname !== undefined) fields.cname = body.cname;
27
+ if (domainChanged) { fields.cert_requested_at = body.cname ? occurredAt : null; fields.https_enforced = false; }
28
+ if (body.build_type !== undefined) fields.build_type = String(body.build_type);
29
+ if (body.https_enforced !== undefined) { const refused = enforceHttps(ctx, body, existing, domainChanged, fields); if (refused) return refused; }
30
+ if (creating) { fields.status = 'building'; if (fields.source_branch === undefined) fields.source_branch = 'main'; if (fields.source_path === undefined) fields.source_path = '/'; }
31
+ await put(ctx, { operation: creating ? 'pages_site.create' : 'pages_site.update', subjectType: 'pages_site', subjectId: `pages:${repo}`, fields });
32
+ if (!creating) return send(ctx, { status: 204, body: undefined });
33
+ return ctx.reply(siteBody(ctx, state().pagesSites.find((s) => s.repository === repo)!), 201);
34
+ }
35
+
36
+ /** HTTPS enforcement turned on or off: refused while the custom domain's certificate is still being issued. */
37
+ function enforceHttps(ctx: SemanticsContext, body: Record<string, any>, existing: { cname?: string | null; cert_requested_at?: string | null } | undefined, domainChanged: boolean, fields: Record<string, unknown>): Response | undefined {
38
+ const { occurredAt } = gh(ctx);
39
+ const cname = domainChanged ? body.cname : existing?.cname;
40
+ const requested = domainChanged ? occurredAt : existing?.cert_requested_at;
41
+ if (body.https_enforced === true && cname && certificateState(requested, occurredAt) !== 'approved') {
42
+ return ctx.reply({ message: 'The certificate does not exist yet', documentation_url: 'https://docs.github.com/rest/pages/pages#update-information-about-a-apiname-pages-site' }, 422);
43
+ }
44
+ fields.https_enforced = Boolean(body.https_enforced);
45
+ return undefined;
46
+ }
47
+
48
+ // How long a custom domain's certificate takes to issue: GitHub says it can take a while and gives no figure, so
49
+ // fifteen minutes is the twin's; the refusal's wording above is the twin's too.
50
+ const CERT_MS = 15 * 60 * 1000;
51
+ function certificateState(requestedAt: string | undefined | null, now: string): string {
52
+ return requestedAt && Date.parse(now) - Date.parse(requestedAt) >= CERT_MS ? 'approved' : 'authorization_pending';
53
+ }
54
+
55
+ /** The site as GitHub answers it: with its custom domain's certificate as the clock finds it. */
56
+ function siteBody(ctx: SemanticsContext, site: ReturnType<Gh['state']>['pagesSites'][number]): Record<string, unknown> {
57
+ const body = toGithubPagesSite(site);
58
+ if (!site.cname) return body;
59
+ const requested = site.cert_requested_at;
60
+ const state = certificateState(requested, gh(ctx).occurredAt);
61
+ return {
62
+ ...body,
63
+ https_certificate: {
64
+ state, description: state === 'approved' ? 'The certificate has been approved.' : 'The certificate is being issued.',
65
+ domains: [site.cname, `www.${site.cname}`], ...(state === 'approved' && requested ? { expires_at: new Date(Date.parse(requested) + 90 * 86_400_000).toISOString().slice(0, 10) } : {}),
66
+ },
67
+ };
68
+ }
69
+
70
+ /** A read under the site's builds: Not Found when Pages is not enabled. */
71
+ const builds = (serve: (ctx: SemanticsContext, list: ReturnType<Gh['state']>['pagesBuilds']) => Response): Semantics => async (ctx) => {
72
+ const { repo, state } = gh(ctx);
73
+ const st = state();
74
+ if (!st.pagesSites.find((s) => s.repository === repo)) return send(ctx, githubNotFound());
75
+ return serve(ctx, st.pagesBuilds.filter((b) => b.repository === repo));
76
+ };
77
+ type Gh = ReturnType<typeof gh>;
78
+
79
+ const DEPLOY_DOCS = 'https://docs.github.com/rest/pages/pages#create-a-github-pages-deployment';
80
+ /** A Pages deployment by its id or the build version (a commit sha) it deployed. */
81
+ const deploymentOf = (ctx: SemanticsContext) => {
82
+ const { repo, seg } = gh(ctx);
83
+ const key = decodeURIComponent(seg[5]!);
84
+ return ctx.tree().find((r) => r.type === 'pages_deployment' && r.repository === repo && r.deleted !== true && (String(r.number) === key || r.pages_build_version === key));
85
+ };
86
+
87
+ export function pagesSemantics(): Record<string, Semantics> {
88
+ const reposGetPagesDeployment: Semantics = async (ctx) => {
89
+ const d = deploymentOf(ctx);
90
+ return send(ctx, d ? { status: 200, body: { status: d.status } } : githubNotFound());
91
+ };
92
+ const reposCancelPagesDeployment: Semantics = async (ctx) => {
93
+ const d = deploymentOf(ctx);
94
+ return send(ctx, d ? { status: 204, body: undefined } : githubNotFound());
95
+ };
96
+ const reposDeletePagesSite: Semantics = async (ctx) => {
97
+ const { repo, state } = gh(ctx);
98
+ if (!state().pagesSites.find((s) => s.repository === repo)) return send(ctx, githubNotFound());
99
+ await put(ctx, { operation: 'pages_site.delete', subjectType: 'pages_site', subjectId: `pages:${repo}`, fields: { deleted: true, repository: repo } });
100
+ return send(ctx, { status: 204, body: undefined });
101
+ };
102
+ const reposGetPagesBuild: Semantics = (ctx) => (builds((ctx, list) => {
103
+ const id = Number(decodeURIComponent(gh(ctx).seg[5]!));
104
+ const b = list.find((x) => x.id === id);
105
+ return send(ctx, b ? { status: 200, body: toGithubPagesBuild(b) } : githubNotFound());
106
+ }))(ctx);
107
+ const reposRequestPagesBuild: Semantics = async (ctx) => {
108
+ const { repo, occurredAt, state } = gh(ctx);
109
+ if (!state().pagesSites.find((s) => s.repository === repo)) return send(ctx, githubNotFound());
110
+ const id = nextSeq(ctx, 'pagesbuild');
111
+ await put(ctx, { operation: 'pages_build.create', subjectType: 'pages_build', subjectId: `pagesbuild:${id}`, fields: { repository: repo, status: 'built', commit: createHash('sha256').update(`${repo}:build:${id}`).digest('hex').slice(0, 40), duration: 1000, created_at: occurredAt } });
112
+ return ctx.reply({ status: 'queued', url: `https://api.github.com/repos/${repo}/pages/builds/latest` }, 201);
113
+ };
114
+ return {
115
+ // A workflow's deploy step publishes the artifact its upload step made (actions/deploy-pages): with the run's
116
+ // GITHUB_TOKEN, which needs pages: write, or a person's with write access; pages_build_version and the run's
117
+ // oidc_token are required, and an artifact_id must name an artifact of the repository
118
+ // (docs.github.com/rest/pages/pages#create-a-github-pages-deployment). Where the documentation stops and the twin
119
+ // decides: the deployment succeeds at once and the site is built; the OIDC token is not verified.
120
+ 'repos/create-pages-deployment': async (ctx) => {
121
+ const { repo, body, token, caller, occurredAt, state } = gh(ctx);
122
+ const st = state();
123
+ const site = st.pagesSites.find((x) => x.repository === repo);
124
+ if (!site) return send(ctx, githubNotFound());
125
+ const grant = token ? ctx.tree().find((r) => r.type === 'app_token' && r.token === token && r.deleted !== true) : undefined;
126
+ if (grant ? (grant.permissions as Record<string, string> | undefined)?.pages !== 'write' : !canWrite(st, repo, caller ?? GITHUB_WORLD_USER)) return ctx.reply({ message: 'Resource not accessible by integration', documentation_url: DEPLOY_DOCS }, 403);
127
+ const absent = (['pages_build_version', 'oidc_token'] as const).filter((k) => !body[k]);
128
+ if (body.artifact_id === undefined && body.artifact_url === undefined) absent.push('artifact_id' as never);
129
+ if (absent.length) return ctx.reply({ message: 'Validation Failed', errors: absent.map((field) => ({ resource: 'PagesDeployment', field, code: 'missing_field' })), documentation_url: DEPLOY_DOCS }, 422);
130
+ if (body.artifact_id !== undefined && !st.actionsArtifacts.some((a) => a.repository === repo && a.id === Number(body.artifact_id))) return send(ctx, githubNotFound());
131
+ const number = ctx.tree().filter((r) => r.type === 'pages_deployment' && r.repository === repo).length + 1;
132
+ await put(ctx, { operation: 'pages_deployment.create', subjectType: 'pages_deployment', subjectId: `pagesdeploy:${repo}#${number}`, fields: { repository: repo, number, pages_build_version: String(body.pages_build_version), artifact_id: body.artifact_id ?? null, environment: body.environment ?? 'github-pages', status: 'succeed', created_at: occurredAt } });
133
+ await put(ctx, { operation: 'pages_site.update', subjectType: 'pages_site', subjectId: `pages:${repo}`, fields: { repository: repo, status: 'built', updated_at: occurredAt } });
134
+ const page = site.cname ? `https://${site.cname}/` : `https://${repo.split('/')[0]}.github.io/${repo.split('/')[1]}/`;
135
+ return ctx.reply({ id: number, status_url: `https://api.github.com/repos/${repo}/pages/deployments/${number}`, page_url: page, preview_url: page });
136
+ },
137
+ 'repos/get-pages-deployment': reposGetPagesDeployment,
138
+ // a deployment that has finished can no longer be cancelled; the twin's finish at once
139
+ 'repos/cancel-pages-deployment': reposCancelPagesDeployment,
140
+ 'repos/get-pages': async (ctx) => {
141
+ const { repo, state } = gh(ctx);
142
+ const site = state().pagesSites.find((s) => s.repository === repo);
143
+ return send(ctx, site ? { status: 200, body: siteBody(ctx, site) } : githubNotFound());
144
+ },
145
+ 'repos/create-pages-site': async (ctx) => writeSite(ctx, true),
146
+ 'repos/update-information-about-pages-site': async (ctx) => writeSite(ctx, false),
147
+ 'repos/delete-pages-site': reposDeletePagesSite,
148
+ 'repos/list-pages-builds': builds((ctx, list) => ctx.reply(paginate(list, gh(ctx).query).map(toGithubPagesBuild))),
149
+ 'repos/get-latest-pages-build': builds((ctx, list) => send(ctx, list.length ? { status: 200, body: toGithubPagesBuild(list[0]!) } : githubNotFound())),
150
+ 'repos/get-pages-build': reposGetPagesBuild,
151
+ // GitHub queues a build (201); the twin records it built
152
+ 'repos/request-pages-build': reposRequestPagesBuild,
153
+ };
154
+ }
@@ -0,0 +1,246 @@
1
+ // GitHub's Projects (v2) family, as GitHub's REST surface addresses a project: by its owner (an organization, or a
2
+ // user by login or by id) and its number. A project is created and closed over GraphQL (semantics/graphql.ts); its
3
+ // fields, items and views are REST's (docs.github.com/rest/projects).
4
+ //
5
+ // Where the documentation stops and the twin decides: a view's `filter` is read as space-separated `field:value`
6
+ // terms (a field's name, a single-select option's name) and `is:issue`, `is:pr`, `is:draft`; any other term is
7
+ // ignored. Adding an item already on the project answers that item, as GraphQL's addProjectV2ItemById does.
8
+ import type { Semantics, SemanticsContext } from '@volter/world-core';
9
+ import {
10
+ GITHUB_WORLD_USER, githubIssueId, githubNotFound, observedId, paginate, toGithubIssue, toGithubProject, toGithubRest, toSimpleUser, twinUserId,
11
+ type GithubProject, type GithubProjectField, type GithubProjectItem, type GithubState,
12
+ } from '../github-twin.ts';
13
+ import { asView, gh, nextSeq, put, send } from './shared.ts';
14
+
15
+ const DATA_TYPES = ['text', 'number', 'date', 'single_select'];
16
+ const COLORS = ['BLUE', 'GRAY', 'GREEN', 'ORANGE', 'PINK', 'PURPLE', 'RED', 'YELLOW'];
17
+ const LAYOUTS = ['table', 'board', 'roadmap'];
18
+
19
+ const invalid = (ctx: SemanticsContext, resource: string, field: string, code = 'invalid'): Response =>
20
+ ctx.reply({ message: 'Validation Failed', errors: [{ resource, field, code }], documentation_url: 'https://docs.github.com/rest/projects' }, 422);
21
+
22
+ /** The project a request names: `/orgs/{org}/…`, `/users/{username}/…`, or a user by id (`/user/{user_id}/…`,
23
+ * `/users/{user_id}/…` on the operations GitHub addresses that way). */
24
+ function projectOf(ctx: SemanticsContext): GithubProject | undefined {
25
+ const { seg, state } = gh(ctx);
26
+ const byOrg = seg[0] === 'orgs';
27
+ const byId = ctx.call.operation.id === 'projects/create-draft-item-for-authenticated-user' || ctx.call.operation.id === 'projects/create-view-for-user';
28
+ const who = decodeURIComponent(seg[1]!);
29
+ const number = Number(decodeURIComponent(seg[3]!));
30
+ return state().projects.find((p) => p.number === number && p.owner_type === (byOrg ? 'organization' : 'user')
31
+ && (byId ? String(twinUserId(p.owner)) === who : p.owner === who));
32
+ }
33
+
34
+ function projectUrl(p: GithubProject): string {
35
+ return `https://api.github.com/${p.owner_type === 'organization' ? 'orgs' : 'users'}/${p.owner}/projectsV2/${p.number}`;
36
+ }
37
+
38
+ function toField(p: GithubProject, f: GithubProjectField): Record<string, unknown> {
39
+ return {
40
+ id: f.id, node_id: `PVTF_${f.id}`, project_url: projectUrl(p), name: f.name, data_type: f.data_type,
41
+ ...(f.options ? { options: f.options.map((o) => ({ id: o.id, name: { raw: o.name, html: o.name }, description: { raw: o.description, html: o.description }, color: o.color })) } : {}),
42
+ created_at: f.created_at ?? null, updated_at: f.updated_at ?? f.created_at ?? null,
43
+ };
44
+ }
45
+
46
+ /** What an item tracks: the issue, the pull request, or the draft's own title and body. */
47
+ function contentOf(st: GithubState, item: GithubProjectItem): Record<string, unknown> | null {
48
+ if (item.content_type === 'DraftIssue') return draftContent(item);
49
+ if (item.content_type === 'PullRequest') return pullContent(st, item);
50
+ const iss = st.issues.find((x) => x.repository === item.content_repository && x.number === item.content_number);
51
+ return iss ? asView('issue', toGithubIssue(iss, st.milestones, undefined, st.comments)) : null;
52
+ }
53
+ /** A draft item's content: its own title and body. */
54
+ function draftContent(item: GithubProjectItem): Record<string, unknown> {
55
+ return { id: item.id, node_id: `DI_${item.id}`, title: item.title ?? '', body: item.body ?? null, user: toSimpleUser(item.creator_login ?? GITHUB_WORLD_USER), created_at: item.created_at ?? null, updated_at: item.updated_at ?? null };
56
+ }
57
+ /** A pull request item's content: the pull request. */
58
+ function pullContent(st: GithubState, item: GithubProjectItem): Record<string, unknown> | null {
59
+ const pr = st.prs.find((x) => x.is_pull_request && x.repository === item.content_repository && x.number === item.content_number);
60
+ return pr ? asView('pull-request-simple', toGithubRest(pr, st.milestones)) : null;
61
+ }
62
+
63
+ function toItem(st: GithubState, p: GithubProject, item: GithubProjectItem, withFields: boolean): Record<string, unknown> {
64
+ const fields = st.projectFields.filter((f) => f.project_id === p.id);
65
+ return {
66
+ id: item.id, node_id: `PVTI_${item.id}`, project_url: projectUrl(p), content_type: item.content_type, content: contentOf(st, item),
67
+ creator: toSimpleUser(item.creator_login ?? GITHUB_WORLD_USER), created_at: item.created_at ?? null, updated_at: item.updated_at ?? item.created_at ?? null,
68
+ archived_at: null, item_url: `${projectUrl(p)}/items/${item.id}`,
69
+ ...(withFields ? { fields: fields.filter((f) => item.field_values?.[f.id] !== undefined).map((f) => ({ id: f.id, name: f.name, data_type: f.data_type, value: item.field_values[f.id] })) } : {}),
70
+ };
71
+ }
72
+
73
+ /** Whether an item passes a view's filter (see the header for the terms read). */
74
+ function passes(st: GithubState, p: GithubProject, item: GithubProjectItem, filter: string | null | undefined): boolean {
75
+ const fields = st.projectFields.filter((f) => f.project_id === p.id);
76
+ for (const term of (filter ?? '').split(/\s+/).filter(Boolean)) {
77
+ const [key, want] = [term.slice(0, term.indexOf(':')), term.slice(term.indexOf(':') + 1)];
78
+ if (key === 'is') {
79
+ const kind = { issue: 'Issue', pr: 'PullRequest', draft: 'DraftIssue' }[want];
80
+ if (kind && item.content_type !== kind) return false;
81
+ continue;
82
+ }
83
+ const field = fields.find((f) => f.name.toLowerCase() === key.toLowerCase());
84
+ if (!field) continue;
85
+ const value = item.field_values?.[field.id];
86
+ const shown = field.data_type === 'single_select' ? field.options?.find((o) => o.id === value)?.name : value;
87
+ if (String(shown ?? '').toLowerCase() !== want.toLowerCase()) return false;
88
+ }
89
+ return true;
90
+ }
91
+
92
+ /** Operations on a project that exists, or GitHub's Not Found. */
93
+ const onProject = (run: (ctx: SemanticsContext, p: GithubProject) => Promise<Response>): Semantics => async (ctx) => {
94
+ const p = projectOf(ctx);
95
+ return p ? run(ctx, p) : send(ctx, githubNotFound());
96
+ };
97
+
98
+ const itemOf = (ctx: SemanticsContext, p: GithubProject): GithubProjectItem | undefined =>
99
+ gh(ctx).state().projectItems.find((i) => i.project_id === p.id && i.id === Number(decodeURIComponent(gh(ctx).seg[5]!)));
100
+
101
+ const listForOwner = (type: 'organization' | 'user'): Semantics => async (ctx) => {
102
+ const { seg, query, state } = gh(ctx);
103
+ const owner = decodeURIComponent(seg[1]!);
104
+ return ctx.reply(paginate(state().projects.filter((p) => p.owner === owner && p.owner_type === type), query).map(toGithubProject));
105
+ };
106
+
107
+ const createDraft: Semantics = (ctx) => onProject(async (ctx, p) => {
108
+ const { body, occurredAt, caller } = gh(ctx);
109
+ if (typeof body.title !== 'string' || !body.title) return invalid(ctx, 'ProjectV2Item', 'title', 'missing_field');
110
+ const id = nextSeq(ctx, 'projectitem');
111
+ await put(ctx, { operation: 'project_item.create', subjectType: 'project_item', subjectId: `projectitem:${id}`, fields: { project_id: p.id, content_type: 'DraftIssue', title: body.title, body: body.body ?? null, field_values: {}, creator_login: caller ?? GITHUB_WORLD_USER, created_at: occurredAt, updated_at: occurredAt } });
112
+ const st = gh(ctx).state();
113
+ return ctx.reply(toItem(st, p, st.projectItems.find((i) => i.id === id)!, false), 201);
114
+ })(ctx);
115
+
116
+ /** A single-select field's options: at least one, each named, with a colour of GitHub's when given. */
117
+ const validOptions = (raw: unknown): boolean => Array.isArray(raw) && raw.length > 0 && !raw.some((o: Record<string, unknown>) => typeof o?.name !== 'string' || (o.color !== undefined && !COLORS.includes(String(o.color))));
118
+
119
+ const addField = onProject(async (ctx, p) => {
120
+ const { body, occurredAt, state } = gh(ctx);
121
+ if (typeof body.name !== 'string' || !body.name) return invalid(ctx, 'ProjectV2Field', 'name', 'missing_field');
122
+ if (!DATA_TYPES.includes(String(body.data_type))) return invalid(ctx, 'ProjectV2Field', 'data_type');
123
+ if (state().projectFields.some((f) => f.project_id === p.id && f.name.toLowerCase() === String(body.name).toLowerCase())) return invalid(ctx, 'ProjectV2Field', 'name', 'already_exists');
124
+ const raw = body.single_select_options;
125
+ if (body.data_type === 'single_select' && !validOptions(raw)) return invalid(ctx, 'ProjectV2Field', 'single_select_options');
126
+ const id = nextSeq(ctx, 'projectfield');
127
+ const options = body.data_type === 'single_select' ? (raw as Array<Record<string, unknown>>).map((o, i) => ({ id: (observedId(`projectfield:${id}:option:${i}`) >>> 0).toString(16).slice(0, 8), name: String(o.name), description: String(o.description ?? ''), color: String(o.color ?? 'GRAY') })) : undefined;
128
+ await put(ctx, { operation: 'project_field.create', subjectType: 'project_field', subjectId: `projectfield:${id}`, fields: { project_id: p.id, name: body.name, data_type: body.data_type, ...(options ? { options } : {}), created_at: occurredAt, updated_at: occurredAt } });
129
+ return ctx.reply(toField(p, gh(ctx).state().projectFields.find((f) => f.id === id)!), 201);
130
+ });
131
+
132
+ const addItem = onProject(async (ctx, p) => {
133
+ const { body, occurredAt, caller, state } = gh(ctx);
134
+ if (body.type !== 'Issue' && body.type !== 'PullRequest') return invalid(ctx, 'ProjectV2Item', 'type');
135
+ const st = state();
136
+ // by its id, or by its repository and number
137
+ const target = body.type === 'Issue'
138
+ ? st.issues.find((i) => (body.id !== undefined ? githubIssueId(i.born_in ?? i.repository, i.number) === Number(body.id) : i.repository === `${String(body.owner)}/${String(body.repo)}` && i.number === Number(body.number)))
139
+ : st.prs.find((x) => x.is_pull_request && (body.id !== undefined ? observedId(`pull_request:${x.born_in ?? x.repository}#${x.number}`) === Number(body.id) : x.repository === `${String(body.owner)}/${String(body.repo)}` && x.number === Number(body.number)));
140
+ if (!target) return send(ctx, githubNotFound());
141
+ const already = st.projectItems.find((i) => i.project_id === p.id && i.content_type === body.type && i.content_repository === target.repository && i.content_number === target.number);
142
+ if (already) return ctx.reply(toItem(st, p, already, false), 201);
143
+ const id = nextSeq(ctx, 'projectitem');
144
+ await put(ctx, { operation: 'project_item.create', subjectType: 'project_item', subjectId: `projectitem:${id}`, fields: { project_id: p.id, content_type: body.type, content_repository: target.repository, content_number: target.number, field_values: {}, creator_login: caller ?? GITHUB_WORLD_USER, created_at: occurredAt, updated_at: occurredAt } });
145
+ const now = state();
146
+ return ctx.reply(toItem(now, p, now.projectItems.find((i) => i.id === id)!, false), 201);
147
+ });
148
+
149
+ // A field's value: text as given, a number, a date (YYYY-MM-DD), a single-select option's id; null clears it.
150
+ const updateItem = onProject(async (ctx, p) => {
151
+ const { body, occurredAt, state } = gh(ctx);
152
+ const item = itemOf(ctx, p);
153
+ if (!item) return send(ctx, githubNotFound());
154
+ if (!Array.isArray(body.fields) || !body.fields.length) return invalid(ctx, 'ProjectV2Item', 'fields', 'missing_field');
155
+ const fields = state().projectFields.filter((f) => f.project_id === p.id);
156
+ const values: Record<string, unknown> = { ...item.field_values };
157
+ for (const change of body.fields as Array<Record<string, unknown>>) {
158
+ const field = fields.find((f) => f.id === Number(change.id));
159
+ if (!field) return invalid(ctx, 'ProjectV2ItemFieldValue', 'id');
160
+ const v = change.value;
161
+ const ok = v === null
162
+ || (field.data_type === 'text' && typeof v === 'string')
163
+ || (field.data_type === 'number' && typeof v === 'number')
164
+ || (field.data_type === 'date' && typeof v === 'string' && /^\d{4}-\d{2}-\d{2}$/.test(v))
165
+ || (field.data_type === 'single_select' && field.options?.some((o) => o.id === v));
166
+ if (!ok) return invalid(ctx, 'ProjectV2ItemFieldValue', 'value');
167
+ if (v === null) delete values[field.id]; else values[field.id] = v;
168
+ }
169
+ await put(ctx, { operation: 'project_item.update', subjectType: 'project_item', subjectId: `projectitem:${item.id}`, fields: { field_values: values, updated_at: occurredAt } });
170
+ const st = state();
171
+ return ctx.reply(toItem(st, p, st.projectItems.find((i) => i.id === item.id)!, true));
172
+ });
173
+
174
+ const createView: Semantics = (ctx) => onProject(async (ctx, p) => {
175
+ const { body, occurredAt, caller, state } = gh(ctx);
176
+ if (typeof body.name !== 'string' || !body.name) return invalid(ctx, 'ProjectV2View', 'name', 'missing_field');
177
+ if (!LAYOUTS.includes(String(body.layout))) return invalid(ctx, 'ProjectV2View', 'layout');
178
+ const number = Math.max(0, ...state().projectViews.filter((v) => v.project_id === p.id).map((v) => v.number)) + 1;
179
+ const id = nextSeq(ctx, 'projectview');
180
+ const list = (k: string): unknown[] => (Array.isArray(body[k]) ? (body[k] as unknown[]) : []);
181
+ await put(ctx, { operation: 'project_view.create', subjectType: 'project_view', subjectId: `projectview:${id}`, fields: { project_id: p.id, number, name: body.name, layout: body.layout, filter: typeof body.filter === 'string' ? body.filter : null, visible_fields: list('visible_fields'), sort_by: list('sort_by'), group_by: list('group_by'), vertical_group_by: list('vertical_group_by'), creator_login: caller ?? GITHUB_WORLD_USER, created_at: occurredAt, updated_at: occurredAt } });
182
+ const v = state().projectViews.find((x) => x.id === id)!;
183
+ return ctx.reply({
184
+ id: v.id, number: v.number, name: v.name, layout: v.layout, node_id: `PVTV_${v.id}`, project_url: projectUrl(p),
185
+ html_url: `https://github.com/${p.owner_type === 'organization' ? 'orgs' : 'users'}/${p.owner}/projects/${p.number}/views/${v.number}`,
186
+ creator: toSimpleUser(v.creator_login ?? GITHUB_WORLD_USER), created_at: v.created_at ?? null, updated_at: v.updated_at ?? null,
187
+ filter: v.filter ?? null, visible_fields: v.visible_fields ?? [], sort_by: v.sort_by ?? [], group_by: v.group_by ?? [], vertical_group_by: v.vertical_group_by ?? [],
188
+ }, 201);
189
+ })(ctx);
190
+
191
+ export function projectsSemantics(): Record<string, Semantics> {
192
+ const listFields = onProject(async (ctx, p) => ctx.reply(paginate(gh(ctx).state().projectFields.filter((f) => f.project_id === p.id), gh(ctx).query).map((f) => toField(p, f))));
193
+ const getField: Semantics = (ctx) => onProject(async (ctx, p) => {
194
+ const f = gh(ctx).state().projectFields.find((x) => x.project_id === p.id && x.id === Number(decodeURIComponent(gh(ctx).seg[5]!)));
195
+ return f ? ctx.reply(toField(p, f)) : send(ctx, githubNotFound());
196
+ })(ctx);
197
+ const listItems: Semantics = (ctx) => onProject(async (ctx, p) => {
198
+ const st = gh(ctx).state();
199
+ return ctx.reply(paginate(st.projectItems.filter((i) => i.project_id === p.id), gh(ctx).query).map((i) => toItem(st, p, i, true)));
200
+ })(ctx);
201
+ const getItem: Semantics = (ctx) => onProject(async (ctx, p) => {
202
+ const item = itemOf(ctx, p);
203
+ return item ? ctx.reply(toItem(gh(ctx).state(), p, item, true)) : send(ctx, githubNotFound());
204
+ })(ctx);
205
+ const deleteItem: Semantics = (ctx) => onProject(async (ctx, p) => {
206
+ const item = itemOf(ctx, p);
207
+ if (!item) return send(ctx, githubNotFound());
208
+ await put(ctx, { operation: 'project_item.delete', subjectType: 'project_item', subjectId: `projectitem:${item.id}`, fields: { deleted: true, project_id: p.id } });
209
+ return send(ctx, { status: 204, body: undefined });
210
+ })(ctx);
211
+ const listViewItems: Semantics = (ctx) => onProject(async (ctx, p) => {
212
+ const st = gh(ctx).state();
213
+ const view = st.projectViews.find((v) => v.project_id === p.id && v.number === Number(decodeURIComponent(gh(ctx).seg[5]!)));
214
+ if (!view) return send(ctx, githubNotFound());
215
+ return ctx.reply(paginate(st.projectItems.filter((i) => i.project_id === p.id && passes(st, p, i, view.filter)), gh(ctx).query).map((i) => toItem(st, p, i, true)));
216
+ })(ctx);
217
+ const get = onProject(async (ctx, p) => ctx.reply(toGithubProject(p)));
218
+ return {
219
+ 'projects/list-for-org': listForOwner('organization'),
220
+ 'projects/list-for-user': listForOwner('user'),
221
+ 'projects/get-for-org': get,
222
+ 'projects/get-for-user': get,
223
+ 'projects/create-draft-item-for-org': createDraft,
224
+ 'projects/create-draft-item-for-authenticated-user': createDraft,
225
+ 'projects/list-fields-for-org': listFields,
226
+ 'projects/list-fields-for-user': listFields,
227
+ 'projects/add-field-for-org': addField,
228
+ 'projects/add-field-for-user': addField,
229
+ 'projects/get-field-for-org': getField,
230
+ 'projects/get-field-for-user': getField,
231
+ 'projects/list-items-for-org': listItems,
232
+ 'projects/list-items-for-user': listItems,
233
+ 'projects/add-item-for-org': addItem,
234
+ 'projects/add-item-for-user': addItem,
235
+ 'projects/get-org-item': getItem,
236
+ 'projects/get-user-item': getItem,
237
+ 'projects/update-item-for-org': updateItem,
238
+ 'projects/update-item-for-user': updateItem,
239
+ 'projects/delete-item-for-org': deleteItem,
240
+ 'projects/delete-item-for-user': deleteItem,
241
+ 'projects/create-view-for-org': createView,
242
+ 'projects/create-view-for-user': createView,
243
+ 'projects/list-view-items-for-org': listViewItems,
244
+ 'projects/list-view-items-for-user': listViewItems,
245
+ };
246
+ }