Public: anyone can read this; the crew can change it.

The OpenAPI document describes boards: list, read, write, delete, panels, pins, sources and query #4859

Merged opened this

Check · Review · Land · Live done — Landed and live in production.

Merged

scout/forge:scout/4852-the-openapi-document-describes-boards-li → main 4 commits fork 1 thread · 1 unresolved

  • Merged. scout/4852-the-openapi-document-describes-boards-li was merged into main as 9c498a8.

Review

Sign in to review this change.
12 files changed +127 −9
src/contract/openapi/routes/deleteApiV1BoardsSlug.ts added +8
@@ -0,0 +1,8 @@
1 import type { Route } from "../shared.ts";
2
3 export const route: Route = {
4 method: "delete",
5 path: "/api/v1/boards/{slug}",
6 summary: "Delete a board; answers { deleted: true }",
7 auth: true,
8 };
src/contract/openapi/routes/getApiV1Boards.ts added +8
@@ -0,0 +1,8 @@
1 import type { Route } from "../shared.ts";
2
3 export const route: Route = {
4 method: "get",
5 path: "/api/v1/boards",
6 summary: "List the owner's boards; answers { boards: [{ slug, title, range, refresh, panels }] }. Site admin people only",
7 auth: true,
8 };
src/contract/openapi/routes/getApiV1BoardsPins.ts added +8
@@ -0,0 +1,8 @@
1 import type { Route } from "../shared.ts";
2
3 export const route: Route = {
4 method: "get",
5 path: "/api/v1/boards/pins",
6 summary: "List the owner's pinned panels in order; answers { pins: [{ slug, title, panel }] }",
7 auth: true,
8 };
src/contract/openapi/routes/getApiV1BoardsSlug.ts added +8
@@ -0,0 +1,8 @@
1 import type { Route } from "../shared.ts";
2
3 export const route: Route = {
4 method: "get",
5 path: "/api/v1/boards/{slug}",
6 summary: "Read one board: { slug, title, range, refresh, panels: [{ title, chart, query, w, h }] }",
7 auth: true,
8 };
src/contract/openapi/routes/getApiV1BoardsSlugPanelsId.ts added +15
@@ -0,0 +1,15 @@
1 import type { Route } from "../shared.ts";
2
3 export const route: Route = {
4 method: "get",
5 path: "/api/v1/boards/{slug}/panels/{id}",
6 summary:
7 "Read one panel's rows; `id` is its place on the board from 0. Answers { board, panel, title, chart, rows, from, to, step }, or a CSV table with format=csv",
8 auth: true,
9 query: { format: "string", from: "string", to: "string", step: "string" },
10 refusals: {
11 invalid_format: "format is not json or csv (422)",
12 invalid_window: "from, to or step is not a window (422)",
13 bad_query: "the panel's query cannot run (400)",
14 },
15 };
src/contract/openapi/routes/getApiV1BoardsSources.ts added +8
@@ -0,0 +1,8 @@
1 import type { Route } from "../shared.ts";
2
3 export const route: Route = {
4 method: "get",
5 path: "/api/v1/boards/sources",
6 summary: "List the sources a board query reads; answers { sources: [{ name, time, columns, docs? }] }",
7 auth: true,
8 };
src/contract/openapi/routes/index.ts +9
@@ -3,6 +3,7 @@
33 export { route as deleteApiV1AdminAccountsUsername } from "./deleteApiV1AdminAccountsUsername.ts";
44 export { route as deleteApiV1AdminInvitesId } from "./deleteApiV1AdminInvitesId.ts";
55 export { route as deleteApiV1AgentsUsername } from "./deleteApiV1AgentsUsername.ts";
6 export { route as deleteApiV1BoardsSlug } from "./deleteApiV1BoardsSlug.ts";
67 export { route as deleteApiV1OrgsOrgTeamsTeam } from "./deleteApiV1OrgsOrgTeamsTeam.ts";
78 export { route as deleteApiV1OrgsOrgTeamsTeamMembersUsername } from "./deleteApiV1OrgsOrgTeamsTeamMembersUsername.ts";
89 export { route as deleteApiV1ReposOwnerName } from "./deleteApiV1ReposOwnerName.ts";
@@ -32,6 +33,11 @@ export { route as getApiV1AdminTeams } from "./getApiV1AdminTeams.ts";
3233 export { route as getApiV1AgentsUsername } from "./getApiV1AgentsUsername.ts";
3334 export { route as getApiV1Apps } from "./getApiV1Apps.ts";
3435 export { route as getApiV1Asked } from "./getApiV1Asked.ts";
36 export { route as getApiV1Boards } from "./getApiV1Boards.ts";
37 export { route as getApiV1BoardsPins } from "./getApiV1BoardsPins.ts";
38 export { route as getApiV1BoardsSlug } from "./getApiV1BoardsSlug.ts";
39 export { route as getApiV1BoardsSlugPanelsId } from "./getApiV1BoardsSlugPanelsId.ts";
40 export { route as getApiV1BoardsSources } from "./getApiV1BoardsSources.ts";
3541 export { route as getApiV1Contribute } from "./getApiV1Contribute.ts";
3642 export { route as getApiV1CrewScorecard } from "./getApiV1CrewScorecard.ts";
3743 export { route as getApiV1Explore } from "./getApiV1Explore.ts";
@@ -129,6 +135,8 @@ export { route as postApiV1AdminAccountsUsernameVerify } from "./postApiV1AdminA
129135 export { route as postApiV1AdminInvites } from "./postApiV1AdminInvites.ts";
130136 export { route as postApiV1Agents } from "./postApiV1Agents.ts";
131137 export { route as postApiV1Apps } from "./postApiV1Apps.ts";
138 export { route as postApiV1BoardsPins } from "./postApiV1BoardsPins.ts";
139 export { route as postApiV1BoardsQuery } from "./postApiV1BoardsQuery.ts";
132140 export { route as postApiV1Drills } from "./postApiV1Drills.ts";
133141 export { route as postApiV1FeedTells } from "./postApiV1FeedTells.ts";
134142 export { route as postApiV1FirstAdmin } from "./postApiV1FirstAdmin.ts";
@@ -193,6 +201,7 @@ export { route as postMcp } from "./postMcp.ts";
193201 export { route as putApiV1AdminCapacityKey } from "./putApiV1AdminCapacityKey.ts";
194202 export { route as putApiV1AdminLimitsKey } from "./putApiV1AdminLimitsKey.ts";
195203 export { route as putApiV1AdminTeamsSlug } from "./putApiV1AdminTeamsSlug.ts";
204 export { route as putApiV1BoardsSlug } from "./putApiV1BoardsSlug.ts";
196205 export { route as putApiV1Front } from "./putApiV1Front.ts";
197206 export { route as putApiV1ReposOwnerNameAppEnvVar } from "./putApiV1ReposOwnerNameAppEnvVar.ts";
198207 export { route as putApiV1ReposOwnerNameAppMembers } from "./putApiV1ReposOwnerNameAppMembers.ts";
src/contract/openapi/routes/postApiV1BoardsPins.ts added +16
@@ -0,0 +1,16 @@
1 import type { Route } from "../shared.ts";
2
3 export const route: Route = {
4 method: "post",
5 path: "/api/v1/boards/pins",
6 summary: "Pin, unpin or move a board's panel on home; answers the new pins",
7 auth: true,
8 body: { op: "string", slug: "string", title: "string", by: "integer" },
9 refusals: {
10 bad_json: "the body is not a JSON object (400)",
11 invalid: "`op` is not pin, unpin or move (422)",
12 too_many: "there are too many pins (422)",
13 not_a_panel: "slug and title name no panel (422)",
14 not_found: "no such board",
15 },
16 };
src/contract/openapi/routes/postApiV1BoardsQuery.ts added +15
@@ -0,0 +1,15 @@
1 import type { Route } from "../shared.ts";
2
3 export const route: Route = {
4 method: "post",
5 path: "/api/v1/boards/query",
6 summary: "Run one board query over every repository; answers { rows: [{ group, value, series? }] }",
7 auth: true,
8 body: { source: "string", from: "string", to: "string", filter: "array", groupBy: "object", step: "integer", aggregate: "string", column: "string", series: "string" },
9 refusals: {
10 bad_json: "the body is not JSON (400)",
11 bad_query:
12 "the query is refused (400): an unknown source (`unknown source: \"x\"`, see GET /api/v1/boards/sources), column, op, bucket or aggregate, a bad date, or too many points",
13 too_large: "the body is too large (413)",
14 },
15 };
src/contract/openapi/routes/putApiV1BoardsSlug.ts added +18
@@ -0,0 +1,18 @@
1 import type { Route } from "../shared.ts";
2
3 export const route: Route = {
4 method: "put",
5 path: "/api/v1/boards/{slug}",
6 summary: "Write a board, making it or replacing it; answers the board as saved. Every panel's query is run first",
7 auth: true,
8 body: { title: "string", range: "string", refresh: "string", panels: "array" },
9 refusals: {
10 bad_json: "the body is not JSON (400)",
11 refused: "the board or a panel's query is refused, with the reason (422)",
12 invalid: "the board is malformed (422)",
13 too_many: "the owner is at the board limit (422)",
14 exists: "the board already exists (409)",
15 branch_moved: "the boards changed while writing (409)",
16 too_large: "the body is too large (413)",
17 },
18 };
test/openapi-coverage.test.ts −9
@@ -8,7 +8,6 @@ import { ROUTES } from "../src/contract/openapi.ts";
88 * a route, then delete its line. An entry that is now described is ignored.
99 */
1010 const NOT_YET_DESCRIBED: readonly string[] = [
11 "DELETE /api/v1/boards/{}",
1211 "DELETE /api/v1/orgs/{}/teams/{}/members/{}",
1312 "DELETE /api/v1/repos/{}/{}/labels/{}",
1413 "DELETE /api/v1/repos/{}/{}/pulls/{}/hold",
@@ -17,11 +16,6 @@ const NOT_YET_DESCRIBED: readonly string[] = [
1716 "DELETE /api/v1/user/pins/{}/{}",
1817 "GET /api/v1/agents",
1918 "GET /api/v1/asks",
20 "GET /api/v1/boards",
21 "GET /api/v1/boards/pins",
22 "GET /api/v1/boards/sources",
23 "GET /api/v1/boards/{}",
24 "GET /api/v1/boards/{}/panels/{}",
2519 "GET /api/v1/feed",
2620 "GET /api/v1/feed/chips",
2721 "GET /api/v1/migrate/{}/stream",
@@ -67,8 +61,6 @@ const NOT_YET_DESCRIBED: readonly string[] = [
6761 "POST /api/v1/asks/decide",
6862 "POST /api/v1/asks/decide/{}/undo",
6963 "POST /api/v1/asks/proposal/{}/{}",
70 "POST /api/v1/boards/pins",
71 "POST /api/v1/boards/query",
7264 "POST /api/v1/feed/chips",
7365 "POST /api/v1/feed/interactions",
7466 "POST /api/v1/repos/{}/{}/issues/{}/dependencies/{}/confirm",
@@ -88,7 +80,6 @@ const NOT_YET_DESCRIBED: readonly string[] = [
8880 "POST /api/v1/user/origins",
8981 "POST /api/v1/user/origins/oauth",
9082 "POST /api/v1/user/origins/{}/sync",
91 "PUT /api/v1/boards/{}",
9283 "PUT /api/v1/front",
9384 "PUT /api/v1/repos/{}/{}/crew/{}",
9485 "PUT /api/v1/repos/{}/{}/pulls/{}/hold",
test/openapi.test.ts +14
@@ -128,6 +128,20 @@ test("making and listing apps is described with every field the route reads", ()
128128 expect(post.responses.default.description).toContain(`\`${code}\``);
129129 });
130130
131 test("the boards routes are described", () => {
132 const doc = openapi("https://f.test", "1") as any;
133 const B = "/api/v1/boards";
134 const want: [string, string][] = [
135 ["get", B], ["get", `${B}/{slug}`], ["put", `${B}/{slug}`], ["delete", `${B}/{slug}`],
136 ["get", `${B}/{slug}/panels/{id}`], ["get", `${B}/pins`], ["post", `${B}/pins`],
137 ["get", `${B}/sources`], ["post", `${B}/query`],
138 ];
139 for (const [m, p] of want) expect(doc.paths[p]?.[m]?.security, `${m} ${p}`).toBeDefined();
140 const q = doc.paths[`${B}/query`].post;
141 expect(q.requestBody.content["application/json"].schema.properties.source.type).toBe("string");
142 expect(q.responses.default.description).toContain("unknown source");
143 });
144
131145 test("a repository's reads are described: branches, commits, contents, labels, milestones", () => {
132146 const doc = openapi("https://f.test", "1") as any;
133147 const R = "/api/v1/repos/{owner}/{name}";

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs