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
Check · Review · Land · Live done — Landed and live in production.
scout/forge:scout/4852-the-openapi-document-describes-boards-li → main 4 commits fork 1 thread · 1 unresolved
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 @@ | ||
| 3 | 3 | export { route as deleteApiV1AdminAccountsUsername } from "./deleteApiV1AdminAccountsUsername.ts"; |
| 4 | 4 | export { route as deleteApiV1AdminInvitesId } from "./deleteApiV1AdminInvitesId.ts"; |
| 5 | 5 | export { route as deleteApiV1AgentsUsername } from "./deleteApiV1AgentsUsername.ts"; |
| 6 | export { route as deleteApiV1BoardsSlug } from "./deleteApiV1BoardsSlug.ts"; | |
| 6 | 7 | export { route as deleteApiV1OrgsOrgTeamsTeam } from "./deleteApiV1OrgsOrgTeamsTeam.ts"; |
| 7 | 8 | export { route as deleteApiV1OrgsOrgTeamsTeamMembersUsername } from "./deleteApiV1OrgsOrgTeamsTeamMembersUsername.ts"; |
| 8 | 9 | export { route as deleteApiV1ReposOwnerName } from "./deleteApiV1ReposOwnerName.ts"; |
| @@ -32,6 +33,11 @@ export { route as getApiV1AdminTeams } from "./getApiV1AdminTeams.ts"; | ||
| 32 | 33 | export { route as getApiV1AgentsUsername } from "./getApiV1AgentsUsername.ts"; |
| 33 | 34 | export { route as getApiV1Apps } from "./getApiV1Apps.ts"; |
| 34 | 35 | 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"; | |
| 35 | 41 | export { route as getApiV1Contribute } from "./getApiV1Contribute.ts"; |
| 36 | 42 | export { route as getApiV1CrewScorecard } from "./getApiV1CrewScorecard.ts"; |
| 37 | 43 | export { route as getApiV1Explore } from "./getApiV1Explore.ts"; |
| @@ -129,6 +135,8 @@ export { route as postApiV1AdminAccountsUsernameVerify } from "./postApiV1AdminA | ||
| 129 | 135 | export { route as postApiV1AdminInvites } from "./postApiV1AdminInvites.ts"; |
| 130 | 136 | export { route as postApiV1Agents } from "./postApiV1Agents.ts"; |
| 131 | 137 | export { route as postApiV1Apps } from "./postApiV1Apps.ts"; |
| 138 | export { route as postApiV1BoardsPins } from "./postApiV1BoardsPins.ts"; | |
| 139 | export { route as postApiV1BoardsQuery } from "./postApiV1BoardsQuery.ts"; | |
| 132 | 140 | export { route as postApiV1Drills } from "./postApiV1Drills.ts"; |
| 133 | 141 | export { route as postApiV1FeedTells } from "./postApiV1FeedTells.ts"; |
| 134 | 142 | export { route as postApiV1FirstAdmin } from "./postApiV1FirstAdmin.ts"; |
| @@ -193,6 +201,7 @@ export { route as postMcp } from "./postMcp.ts"; | ||
| 193 | 201 | export { route as putApiV1AdminCapacityKey } from "./putApiV1AdminCapacityKey.ts"; |
| 194 | 202 | export { route as putApiV1AdminLimitsKey } from "./putApiV1AdminLimitsKey.ts"; |
| 195 | 203 | export { route as putApiV1AdminTeamsSlug } from "./putApiV1AdminTeamsSlug.ts"; |
| 204 | export { route as putApiV1BoardsSlug } from "./putApiV1BoardsSlug.ts"; | |
| 196 | 205 | export { route as putApiV1Front } from "./putApiV1Front.ts"; |
| 197 | 206 | export { route as putApiV1ReposOwnerNameAppEnvVar } from "./putApiV1ReposOwnerNameAppEnvVar.ts"; |
| 198 | 207 | 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"; | ||
| 8 | 8 | * a route, then delete its line. An entry that is now described is ignored. |
| 9 | 9 | */ |
| 10 | 10 | const NOT_YET_DESCRIBED: readonly string[] = [ |
| 11 | "DELETE /api/v1/boards/{}", | |
| 12 | 11 | "DELETE /api/v1/orgs/{}/teams/{}/members/{}", |
| 13 | 12 | "DELETE /api/v1/repos/{}/{}/labels/{}", |
| 14 | 13 | "DELETE /api/v1/repos/{}/{}/pulls/{}/hold", |
| @@ -17,11 +16,6 @@ const NOT_YET_DESCRIBED: readonly string[] = [ | ||
| 17 | 16 | "DELETE /api/v1/user/pins/{}/{}", |
| 18 | 17 | "GET /api/v1/agents", |
| 19 | 18 | "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/{}", | |
| 25 | 19 | "GET /api/v1/feed", |
| 26 | 20 | "GET /api/v1/feed/chips", |
| 27 | 21 | "GET /api/v1/migrate/{}/stream", |
| @@ -67,8 +61,6 @@ const NOT_YET_DESCRIBED: readonly string[] = [ | ||
| 67 | 61 | "POST /api/v1/asks/decide", |
| 68 | 62 | "POST /api/v1/asks/decide/{}/undo", |
| 69 | 63 | "POST /api/v1/asks/proposal/{}/{}", |
| 70 | "POST /api/v1/boards/pins", | |
| 71 | "POST /api/v1/boards/query", | |
| 72 | 64 | "POST /api/v1/feed/chips", |
| 73 | 65 | "POST /api/v1/feed/interactions", |
| 74 | 66 | "POST /api/v1/repos/{}/{}/issues/{}/dependencies/{}/confirm", |
| @@ -88,7 +80,6 @@ const NOT_YET_DESCRIBED: readonly string[] = [ | ||
| 88 | 80 | "POST /api/v1/user/origins", |
| 89 | 81 | "POST /api/v1/user/origins/oauth", |
| 90 | 82 | "POST /api/v1/user/origins/{}/sync", |
| 91 | "PUT /api/v1/boards/{}", | |
| 92 | 83 | "PUT /api/v1/front", |
| 93 | 84 | "PUT /api/v1/repos/{}/{}/crew/{}", |
| 94 | 85 | "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", () | ||
| 128 | 128 | expect(post.responses.default.description).toContain(`\`${code}\``); |
| 129 | 129 | }); |
| 130 | 130 | |
| 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 | ||
| 131 | 145 | test("a repository's reads are described: branches, commits, contents, labels, milestones", () => { |
| 132 | 146 | const doc = openapi("https://f.test", "1") as any; |
| 133 | 147 | const R = "/api/v1/repos/{owner}/{name}"; |
new above
offline