@tequila99/githuman 0.4.0 → 0.4.1
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 +8 -1
- package/dist/cli/config.js +4 -2
- package/dist/cli/server-runtime.js +1 -0
- package/dist/server/app.js +10 -0
- package/dist/server/config/openapi.js +29 -0
- package/dist/server/repositories/review.repo.js +31 -5
- package/dist/server/routes/agent.js +145 -18
- package/dist/server/routes/app-info.js +8 -1
- package/dist/server/routes/comments.js +73 -27
- package/dist/server/routes/diff.js +58 -31
- package/dist/server/routes/events.js +14 -1
- package/dist/server/routes/export.js +22 -5
- package/dist/server/routes/git.js +52 -21
- package/dist/server/routes/health.js +8 -1
- package/dist/server/routes/openapi.js +4 -0
- package/dist/server/routes/reviews.js +59 -33
- package/dist/server/services/agent/agent-session.js +5 -3
- package/dist/shared/agents/schemas.js +120 -31
- package/dist/shared/app/schemas.js +7 -0
- package/dist/shared/comments/schemas.js +48 -0
- package/dist/shared/diff/schemas.js +77 -0
- package/dist/shared/diff/types.js +0 -3
- package/dist/shared/events/schemas.js +4 -0
- package/dist/shared/git/schemas.js +59 -0
- package/dist/shared/http/schemas.js +23 -0
- package/dist/shared/reviews/schemas.js +96 -0
- package/dist/shared/reviews/summary.js +5 -0
- package/dist/shared/utils/schemas.js +9 -0
- package/dist/web/assets/{AppPage-BNPBe_fi.js → AppPage-BSvF7ik1.js} +1 -1
- package/dist/web/assets/CreateReviewFabButton-M0od1QEZ.js +1 -0
- package/dist/web/assets/{IndexPage-xqMEFqAN.js → IndexPage-Do-EZOBf.js} +1 -1
- package/dist/web/assets/{MainLayout-CMbAu8_d.js → MainLayout-daEN-VtX.js} +1 -1
- package/dist/web/assets/{QMenu-QYPSkHBE.js → QMenu-DXgFwoTJ.js} +1 -1
- package/dist/web/assets/{QSelect-D4U0Of4k.js → QSelect-DzYPZHrW.js} +1 -1
- package/dist/web/assets/ReviewDetailPage-DEtNg2UN.js +1 -0
- package/dist/web/assets/{ReviewsPage-CmHdSBsU.js → ReviewsPage-Dg-iCxsQ.js} +1 -1
- package/dist/web/assets/{StatusSwitcher-CenEW-Lu.js → StatusSwitcher-BpebV47C.js} +3 -3
- package/dist/web/assets/{abnfDiagram-O67JEVCF-C4PFTrGZ.js → abnfDiagram-O67JEVCF-BUX560Qb.js} +1 -1
- package/dist/web/assets/{agent-icon-C1oimBWa.js → agent-icon-BJbyp2DP.js} +3 -3
- package/dist/web/assets/{agent-store-D1WRRYIE.js → agent-store-B9E-aiz3.js} +1 -1
- package/dist/web/assets/{arc-CiNErV2i.js → arc-dElW-85p.js} +1 -1
- package/dist/web/assets/architecture-7GRP2DOG-bgxww3gY.js +1 -0
- package/dist/web/assets/{architectureDiagram-NJMV4G6O-DnAqHYSW.js → architectureDiagram-NJMV4G6O-CUUKpxro.js} +1 -1
- package/dist/web/assets/{blockDiagram-BEXU5L5S-CCN9Ja5L.js → blockDiagram-BEXU5L5S-D4r5wIie.js} +1 -1
- package/dist/web/assets/{c4Diagram-YGBWAQC7-CPzIw79A.js → c4Diagram-YGBWAQC7-ww04LJhe.js} +1 -1
- package/dist/web/assets/channel-CdThk9yc.js +1 -0
- package/dist/web/assets/{chunk-3FUC2YCW-BbeiQKv_.js → chunk-3FUC2YCW-9i3K5FXP.js} +1 -1
- package/dist/web/assets/{chunk-5DYCD2WN-CuzXaWoe.js → chunk-5DYCD2WN-BmtGl0Xn.js} +1 -1
- package/dist/web/assets/{chunk-742MDFTN-DJUCmQfr.js → chunk-742MDFTN-5p0wX5Yc.js} +1 -1
- package/dist/web/assets/{chunk-7INBJB4K-BBd4ghzW.js → chunk-7INBJB4K-CWFVY0jp.js} +1 -1
- package/dist/web/assets/{chunk-7M6MHVWA-B0L68u_O.js → chunk-7M6MHVWA-DUgZIYj4.js} +1 -1
- package/dist/web/assets/{chunk-7PRAP22T-BvEmVPr6.js → chunk-7PRAP22T-BrhA6Tm7.js} +1 -1
- package/dist/web/assets/{chunk-GTNCS2PH-B_vHEKRO.js → chunk-GTNCS2PH-B83nsnuR.js} +1 -1
- package/dist/web/assets/{chunk-GWA4HPMP-DWaW5-VL.js → chunk-GWA4HPMP-cewRmL7f.js} +1 -1
- package/dist/web/assets/{chunk-MBY4JIJT-Dfo1FM_M.js → chunk-MBY4JIJT-D18SeqyB.js} +1 -1
- package/dist/web/assets/{chunk-NETBCI7D-DzjBtXVV.js → chunk-NETBCI7D-5SjiRygT.js} +1 -1
- package/dist/web/assets/{chunk-O7XYJQB3-DY5ZFCJH.js → chunk-O7XYJQB3-H6mLYErS.js} +1 -1
- package/dist/web/assets/{chunk-UA2S7LBM-DX_QjVLS.js → chunk-UA2S7LBM-C5VQVDlC.js} +1 -1
- package/dist/web/assets/{chunk-WEXAMYUT-Cckc2fqg.js → chunk-WEXAMYUT-CrOcvEcH.js} +1 -1
- package/dist/web/assets/{chunk-XXDRQBXY-DjF0kpJr.js → chunk-XXDRQBXY-BuVPxQhc.js} +1 -1
- package/dist/web/assets/{chunk-Z7XXMR3K-Do7oG6KK.js → chunk-Z7XXMR3K-BVP3f1-g.js} +1 -1
- package/dist/web/assets/{chunk-ZIGJFQKS-DTcqsqHq.js → chunk-ZIGJFQKS-CF-OzGXH.js} +1 -1
- package/dist/web/assets/{classDiagram-v2-NBCMYWYE-Z6ZhPv4N.js → classDiagram-v2-NBCMYWYE-Bd3VDNaV.js} +1 -1
- package/dist/web/assets/{core-tP7fpdit.js → core-DiiZIGCg.js} +1 -1
- package/dist/web/assets/{cose-bilkent-JH36ORCC-B5DV641c.js → cose-bilkent-JH36ORCC-ifMLrEkd.js} +1 -1
- package/dist/web/assets/{cynefin-OW5HDTMX-BYrEwheK.js → cynefin-OW5HDTMX-6l9c0yRf.js} +1 -1
- package/dist/web/assets/{cynefinDiagram-VND7K2PF-BXbLnUDP.js → cynefinDiagram-VND7K2PF-u9Gy0Odz.js} +1 -1
- package/dist/web/assets/{dagre-6A5THRUB-DJHHAqZU.js → dagre-6A5THRUB-D1jsEBl4.js} +1 -1
- package/dist/web/assets/{diagram-22UHCM2B-DA9deaTX.js → diagram-22UHCM2B-DpyH5n1y.js} +1 -1
- package/dist/web/assets/{diagram-3UASUU5V-C6bo1gN6.js → diagram-3UASUU5V-3X1RJV9-.js} +1 -1
- package/dist/web/assets/{diagram-ATOU4E4O-TTQR35E_.js → diagram-ATOU4E4O-B_bvvAua.js} +1 -1
- package/dist/web/assets/{diagram-CDSNMT55-ChFXwow0.js → diagram-CDSNMT55-DheqAuOA.js} +1 -1
- package/dist/web/assets/{diagram-MLGK6HIB-D3F8BzhV.js → diagram-MLGK6HIB-CYwe9zYy.js} +1 -1
- package/dist/web/assets/{diagram-MPIPVDR6-NJ6F45a1.js → diagram-MPIPVDR6-CVd9i23e.js} +1 -1
- package/dist/web/assets/{dist-Besl4xjp.js → dist-BzuUT3GV.js} +1 -1
- package/dist/web/assets/{ebnfDiagram-ZINNZB2B-u2e1UzYe.js → ebnfDiagram-ZINNZB2B-B5vtXULf.js} +1 -1
- package/dist/web/assets/{elk-276RUBZZ-tCLm9cCU.js → elk-276RUBZZ-BmMM0tV5.js} +1 -1
- package/dist/web/assets/{engine-oniguruma-DaaPGLR3.js → engine-oniguruma-D6DCTEub.js} +1 -1
- package/dist/web/assets/{erDiagram-OPXOYQCR-ChNVjKDe.js → erDiagram-OPXOYQCR-1ORPVLQR.js} +1 -1
- package/dist/web/assets/eventmodeling-NTZA5JFV-BH97sTDs.js +1 -0
- package/dist/web/assets/flowDiagram-KWPJA3E3-CRwhE8ka.js +1 -0
- package/dist/web/assets/{ganttDiagram-FUAMR5RP-8mcAm2ao.js → ganttDiagram-FUAMR5RP-CAWNdy1S.js} +1 -1
- package/dist/web/assets/{gitGraph-4MIJSDKK-DtM1RZrd.js → gitGraph-4MIJSDKK-CXCCnZ1j.js} +1 -1
- package/dist/web/assets/{gitGraphDiagram-X574FWY7-C7KXkLLU.js → gitGraphDiagram-X574FWY7-DPDrqSt6.js} +1 -1
- package/dist/web/assets/{i18n-BmgFD4Gx.js → i18n-D5wmXwHr.js} +1 -1
- package/dist/web/assets/{index-CuqF4_ks.js → index-DWi4DKrl.js} +2 -2
- package/dist/web/assets/{info-A6RAGUB7-C5tVl8cQ.js → info-A6RAGUB7-CGg3Xa4X.js} +1 -1
- package/dist/web/assets/{infoDiagram-VRGFBTTK-DO4D3mI7.js → infoDiagram-VRGFBTTK-vx9EUrdV.js} +1 -1
- package/dist/web/assets/{init-app-C0kkg1Ie.js → init-app-CgDkvlRb.js} +1 -1
- package/dist/web/assets/{ishikawaDiagram-OU5B5YK6-DA2H3Eih.js → ishikawaDiagram-OU5B5YK6-DL7T0J63.js} +1 -1
- package/dist/web/assets/{journeyDiagram-ZHPQQLJL-CdPvR3Sr.js → journeyDiagram-ZHPQQLJL-D45T1Pj5.js} +1 -1
- package/dist/web/assets/{kanban-definition-PNTS6WVX-BRIiazVf.js → kanban-definition-PNTS6WVX-C9ZnTE1l.js} +1 -1
- package/dist/web/assets/{line-CmXhYhmh.js → line-CLBEcmiH.js} +1 -1
- package/dist/web/assets/{linear-Cyx-ptw-.js → linear-B5bBEu0i.js} +1 -1
- package/dist/web/assets/{mermaid-parser.core-CZ05RARR.js → mermaid-parser.core-Dj-n65Ou.js} +3 -3
- package/dist/web/assets/{mermaid.core-ZosqiKX-.js → mermaid.core-CmPsESsw.js} +4 -4
- package/dist/web/assets/{mindmap-definition-NLK3R4M7-a7nG9rPn.js → mindmap-definition-NLK3R4M7-ZX638ce_.js} +1 -1
- package/dist/web/assets/{packet-AYTQ26CC-yIJsAali.js → packet-AYTQ26CC-D-XZHUq_.js} +1 -1
- package/dist/web/assets/{pegDiagram-GJSIUBJH-CBdB4i41.js → pegDiagram-GJSIUBJH-sE-EOKNL.js} +1 -1
- package/dist/web/assets/{pie-WAS4IAKB-ybMEHYPX.js → pie-WAS4IAKB-DMkVggJA.js} +1 -1
- package/dist/web/assets/{pieDiagram-5QR66LMP-BfiG4Lho.js → pieDiagram-5QR66LMP-DSdIEQyS.js} +1 -1
- package/dist/web/assets/pinia-BMN9Drn3.js +1 -0
- package/dist/web/assets/{quadrantDiagram-O4NWA36T-7A6x_hoF.js → quadrantDiagram-O4NWA36T-BReXSVss.js} +1 -1
- package/dist/web/assets/{radar-RG4KPBEZ-BbeFIczu.js → radar-RG4KPBEZ-CJzPwlj6.js} +1 -1
- package/dist/web/assets/{railroad-74A4TZTK-CAyDA7Zz.js → railroad-74A4TZTK-CkhRuR3N.js} +1 -1
- package/dist/web/assets/railroad-abnf-HS5TGJTU-CcS8fVgm.js +1 -0
- package/dist/web/assets/railroad-ebnf-LZEXJU2U-BlpYA103.js +1 -0
- package/dist/web/assets/railroad-peg-WCYAUIDC-C2EznudM.js +1 -0
- package/dist/web/assets/{railroadDiagram-XR7U4H2S-BXiOJ_AA.js → railroadDiagram-XR7U4H2S-DCoF4AEN.js} +1 -1
- package/dist/web/assets/{requirementDiagram-PLB6GJNP-bQNGcpuV.js → requirementDiagram-PLB6GJNP-B15aXZXI.js} +1 -1
- package/dist/web/assets/{safe-storage-CqQMHcn7.js → safe-storage-ak36tVRO.js} +1 -1
- package/dist/web/assets/{sankeyDiagram-IPEJSGJF-CgfGV4TY.js → sankeyDiagram-IPEJSGJF-DcWsnYYP.js} +1 -1
- package/dist/web/assets/{sequenceDiagram-PO4LG4MO-BdCuKkDZ.js → sequenceDiagram-PO4LG4MO-CXWUNmSl.js} +1 -1
- package/dist/web/assets/{src-AvcVSqt3.js → src-CB-shhJB.js} +1 -1
- package/dist/web/assets/{stateDiagram-v2-GCMORJYK-aj1Op7xi.js → stateDiagram-v2-GCMORJYK-M9PkKdix.js} +1 -1
- package/dist/web/assets/{swimlanes-2SLR337P-DQ_YYhZu.js → swimlanes-2SLR337P-B13LpAqh.js} +1 -1
- package/dist/web/assets/swimlanesDiagram-TC7HE7FX-BmCQPnJC.js +8 -0
- package/dist/web/assets/theme-BMaTNJdz.js +1 -0
- package/dist/web/assets/{timeline-definition-EJHVYXUP-Bk3Mvy3w.js → timeline-definition-EJHVYXUP-Dli0jMaX.js} +1 -1
- package/dist/web/assets/{treeView-Q6P3EWNA-BzM7oZcT.js → treeView-Q6P3EWNA-BwBFpWIC.js} +1 -1
- package/dist/web/assets/{treemap-WGGIJYW6-DzgzhZei.js → treemap-WGGIJYW6-DYGjPuTv.js} +1 -1
- package/dist/web/assets/{use-app-theme-DEiGbVuj.js → use-app-theme-C9HTPdVu.js} +1 -1
- package/dist/web/assets/{use-panel-width-ujFPWf-l.js → use-panel-width-DGbjs_T8.js} +1 -1
- package/dist/web/assets/{usecaseDiagram-POWQR4AR-kriFnh7L.js → usecaseDiagram-POWQR4AR-Chs4O-o-.js} +1 -1
- package/dist/web/assets/{vennDiagram-UO4OBE2U-CDdCMQs7.js → vennDiagram-UO4OBE2U-D3GVZcIr.js} +1 -1
- package/dist/web/assets/{wardley-WFR3VGLG-BUWTYQyf.js → wardley-WFR3VGLG-B5YUEVo5.js} +1 -1
- package/dist/web/assets/{wardleyDiagram-VNRHLVJA-CW7lMcTj.js → wardleyDiagram-VNRHLVJA-liqH-4lm.js} +1 -1
- package/dist/web/assets/{xychartDiagram-PMCCYNJV-DoIu8J_3.js → xychartDiagram-PMCCYNJV-C63N6ZvA.js} +1 -1
- package/dist/web/index.html +1 -1
- package/package.json +2 -2
- package/dist/web/assets/CreateReviewFabButton-3V9ZYSHd.js +0 -1
- package/dist/web/assets/ReviewDetailPage-DSCeLI9D.js +0 -1
- package/dist/web/assets/architecture-7GRP2DOG-DORh0iBM.js +0 -1
- package/dist/web/assets/channel-WV0H_O-w.js +0 -1
- package/dist/web/assets/eventmodeling-NTZA5JFV-KBTHxbEB.js +0 -1
- package/dist/web/assets/flowDiagram-KWPJA3E3-DM-yXypD.js +0 -1
- package/dist/web/assets/pinia-r4XAEwbB.js +0 -1
- package/dist/web/assets/railroad-abnf-HS5TGJTU-BzS6thC5.js +0 -1
- package/dist/web/assets/railroad-ebnf-LZEXJU2U-MS09sAPW.js +0 -1
- package/dist/web/assets/railroad-peg-WCYAUIDC-DEQwqbcE.js +0 -1
- package/dist/web/assets/swimlanesDiagram-TC7HE7FX-9frEtrG_.js +0 -8
- package/dist/web/assets/theme-BSjPUW3f.js +0 -1
package/README.md
CHANGED
|
@@ -114,6 +114,13 @@ node dist/cli/index.js serve --db-prefix myteam- # .githuman/myteam-reviews.db
|
|
|
114
114
|
node dist/cli/index.js list --db-prefix "" # .githuman/reviews.db (оригинальный githuman)
|
|
115
115
|
```
|
|
116
116
|
|
|
117
|
+
### HTTP API
|
|
118
|
+
|
|
119
|
+
Сервер отдаёт OpenAPI-спецификацию своего API по адресу `/api/openapi.json`
|
|
120
|
+
(OpenAPI 3.1, всегда). Swagger UI — на `/api/docs`, только с флагом
|
|
121
|
+
`--api-docs` (`pnpm run dev:server` включает его сам). Агентские маршруты
|
|
122
|
+
попадают в спецификацию, только когда сервер их отдаёт (loopback-хост).
|
|
123
|
+
|
|
117
124
|
## Разработка
|
|
118
125
|
|
|
119
126
|
Два процесса нужны одновременно — backend (Fastify, автоперезапуск при
|
|
@@ -134,7 +141,7 @@ githuman — запустите `dev:server` из cwd того репозито
|
|
|
134
141
|
```bash
|
|
135
142
|
cd /path/to/other/repo
|
|
136
143
|
node --watch-path=/path/to/githuman/src \
|
|
137
|
-
/path/to/githuman/src/cli/index.ts serve --no-open
|
|
144
|
+
/path/to/githuman/src/cli/index.ts serve --no-open --api-docs
|
|
138
145
|
```
|
|
139
146
|
|
|
140
147
|
Прочие команды:
|
package/dist/cli/config.js
CHANGED
|
@@ -53,7 +53,8 @@ export function parseServeArgs(argv) {
|
|
|
53
53
|
host: { type: 'string' },
|
|
54
54
|
open: { type: 'boolean', default: true },
|
|
55
55
|
'no-open': { type: 'boolean', default: false },
|
|
56
|
-
'db-prefix': { type: 'string' }
|
|
56
|
+
'db-prefix': { type: 'string' },
|
|
57
|
+
'api-docs': { type: 'boolean', default: false }
|
|
57
58
|
},
|
|
58
59
|
strict: false
|
|
59
60
|
});
|
|
@@ -61,5 +62,6 @@ export function parseServeArgs(argv) {
|
|
|
61
62
|
const host = resolveHost(typeof values.host === 'string' ? values.host : undefined);
|
|
62
63
|
const open = values['no-open'] === true ? false : values.open !== false;
|
|
63
64
|
const dbPrefix = resolveDbPrefix(typeof values['db-prefix'] === 'string' ? values['db-prefix'] : undefined);
|
|
64
|
-
|
|
65
|
+
const apiDocs = values['api-docs'] === true;
|
|
66
|
+
return { port, host, open, dbPrefix, apiDocs };
|
|
65
67
|
}
|
package/dist/server/app.js
CHANGED
|
@@ -4,6 +4,8 @@ import { readFileSync } from 'node:fs';
|
|
|
4
4
|
import { join } from 'node:path';
|
|
5
5
|
import Fastify from 'fastify';
|
|
6
6
|
import fastifyStatic from '@fastify/static';
|
|
7
|
+
import fastifySwagger from '@fastify/swagger';
|
|
8
|
+
import fastifySwaggerUi from '@fastify/swagger-ui';
|
|
7
9
|
// @fastify/sse's published types declare an ESM default export that doesn't match
|
|
8
10
|
// its actual CJS runtime shape, so the default import resolves to the whole module
|
|
9
11
|
// namespace under NodeNext — cast it back to the plugin function type.
|
|
@@ -17,6 +19,8 @@ import { commentRoutes } from "./routes/comments.js";
|
|
|
17
19
|
import { exportRoutes } from "./routes/export.js";
|
|
18
20
|
import { eventRoutes } from "./routes/events.js";
|
|
19
21
|
import { agentRoutes } from "./routes/agent.js";
|
|
22
|
+
import { openApiRoutes } from "./routes/openapi.js";
|
|
23
|
+
import { openApiOptions } from "./config/openapi.js";
|
|
20
24
|
import { createSessionRegistry } from "./services/agent/session-registry.js";
|
|
21
25
|
import { createTestDatabase } from "./db/index.js";
|
|
22
26
|
import { createEventBus } from "./event-bus.js";
|
|
@@ -29,6 +33,12 @@ export function buildApp(options = {}) {
|
|
|
29
33
|
const db = options.db ?? createTestDatabase();
|
|
30
34
|
const eventBus = options.eventBus ?? createEventBus();
|
|
31
35
|
app.register(fastifySse);
|
|
36
|
+
// The spec collects routes as they are added, so it comes before them.
|
|
37
|
+
app.register(fastifySwagger, openApiOptions());
|
|
38
|
+
if (options.apiDocs) {
|
|
39
|
+
app.register(fastifySwaggerUi, { routePrefix: '/api/docs' });
|
|
40
|
+
}
|
|
41
|
+
app.register(openApiRoutes);
|
|
32
42
|
app.register(healthRoutes);
|
|
33
43
|
app.register(appInfoRoutes);
|
|
34
44
|
app.register(diffRoutes, { repositoryPath });
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { getAppVersion } from "../app-version.js";
|
|
2
|
+
/**
|
|
3
|
+
* Options of `@fastify/swagger`. OpenAPI 3.1 because TypeBox writes null as
|
|
4
|
+
* `{ type: 'null' }`, and OpenAPI 3.0 does not allow it.
|
|
5
|
+
*/
|
|
6
|
+
export function openApiOptions() {
|
|
7
|
+
return {
|
|
8
|
+
openapi: {
|
|
9
|
+
openapi: '3.1.0',
|
|
10
|
+
info: {
|
|
11
|
+
title: 'githuman',
|
|
12
|
+
version: getAppVersion(),
|
|
13
|
+
description: 'HTTP API of the local githuman server. The API has no authentication: the tool is local.'
|
|
14
|
+
},
|
|
15
|
+
tags: [
|
|
16
|
+
{ name: 'diff', description: 'Changes of the working tree and refs.' },
|
|
17
|
+
{ name: 'git', description: 'Repository facts, files and index.' },
|
|
18
|
+
{ name: 'reviews', description: 'Reviews and their export.' },
|
|
19
|
+
{ name: 'comments', description: 'Comments of reviews.' },
|
|
20
|
+
{ name: 'events', description: 'Server-sent change events.' },
|
|
21
|
+
{ name: 'app', description: 'Server version and health.' },
|
|
22
|
+
{
|
|
23
|
+
name: 'agent',
|
|
24
|
+
description: 'Agent chats. The server has these routes only on a loopback host, and each request needs a local Host and Origin.'
|
|
25
|
+
}
|
|
26
|
+
]
|
|
27
|
+
}
|
|
28
|
+
};
|
|
29
|
+
}
|
|
@@ -1,12 +1,28 @@
|
|
|
1
1
|
import { UniqueNameError } from "../errors/reviews.js";
|
|
2
|
-
|
|
2
|
+
// Every column but snapshot_data: the list doesn't need the diff, and it is
|
|
3
|
+
// requested on each review/comment event (#56). Update this list when
|
|
4
|
+
// ReviewSummaryRow changes: a missing column reads as undefined, silently.
|
|
5
|
+
const SUMMARY_COLUMNS = [
|
|
6
|
+
'id',
|
|
7
|
+
'repository_path',
|
|
8
|
+
'base_ref',
|
|
9
|
+
'source_type',
|
|
10
|
+
'source_ref',
|
|
11
|
+
'status',
|
|
12
|
+
'name',
|
|
13
|
+
'branch',
|
|
14
|
+
'created_at',
|
|
15
|
+
'updated_at'
|
|
16
|
+
]
|
|
17
|
+
.map(column => `reviews.${column}`)
|
|
18
|
+
.join(', ');
|
|
19
|
+
function rowToSummary(row) {
|
|
3
20
|
return {
|
|
4
21
|
id: row.id,
|
|
5
22
|
repositoryPath: row.repository_path,
|
|
6
23
|
baseRef: row.base_ref,
|
|
7
24
|
sourceType: row.source_type,
|
|
8
25
|
sourceRef: row.source_ref,
|
|
9
|
-
snapshotData: row.snapshot_data,
|
|
10
26
|
status: row.status,
|
|
11
27
|
name: row.name,
|
|
12
28
|
branch: row.branch,
|
|
@@ -14,6 +30,9 @@ function rowToReview(row) {
|
|
|
14
30
|
updatedAt: row.updated_at
|
|
15
31
|
};
|
|
16
32
|
}
|
|
33
|
+
function rowToReview(row) {
|
|
34
|
+
return { ...rowToSummary(row), snapshotData: row.snapshot_data };
|
|
35
|
+
}
|
|
17
36
|
export function createReview(db, review) {
|
|
18
37
|
try {
|
|
19
38
|
db.prepare(`INSERT INTO reviews (id, repository_path, base_ref, source_type, source_ref, snapshot_data, status, name, branch, created_at, updated_at)
|
|
@@ -32,6 +51,13 @@ export function findReviewById(db, id) {
|
|
|
32
51
|
const row = db.prepare('SELECT * FROM reviews WHERE id = ?').get(id);
|
|
33
52
|
return row ? rowToReview(row) : null;
|
|
34
53
|
}
|
|
54
|
+
/** Reads a review without its snapshot, for callers that only need its fields (#57). */
|
|
55
|
+
export function findReviewSummaryById(db, id) {
|
|
56
|
+
const row = db
|
|
57
|
+
.prepare(`SELECT ${SUMMARY_COLUMNS} FROM reviews WHERE id = ?`)
|
|
58
|
+
.get(id);
|
|
59
|
+
return row ? rowToSummary(row) : null;
|
|
60
|
+
}
|
|
35
61
|
export function listReviews(db, filters = {}) {
|
|
36
62
|
const { branch, search, createdFrom, createdTo, filePaths } = filters;
|
|
37
63
|
const conditions = [];
|
|
@@ -56,12 +82,12 @@ export function listReviews(db, filters = {}) {
|
|
|
56
82
|
}
|
|
57
83
|
const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(' AND ')}` : '';
|
|
58
84
|
const rows = db
|
|
59
|
-
.prepare(`SELECT DISTINCT
|
|
85
|
+
.prepare(`SELECT DISTINCT ${SUMMARY_COLUMNS} FROM reviews
|
|
60
86
|
${joins.join(' ')}
|
|
61
87
|
${whereClause}
|
|
62
88
|
ORDER BY reviews.created_at DESC`)
|
|
63
89
|
.all(...params);
|
|
64
|
-
const reviews = rows.map(
|
|
90
|
+
const reviews = rows.map(rowToSummary);
|
|
65
91
|
// Done in JS, not SQL `LOWER()`/`NOCASE` (ASCII-only in SQLite without the
|
|
66
92
|
// ICU extension) — `String.prototype.toLowerCase()` handles Cyrillic and
|
|
67
93
|
// other non-ASCII scripts correctly (see ADR 0017).
|
|
@@ -82,5 +108,5 @@ export function updateReviewStatus(db, id, status) {
|
|
|
82
108
|
if (result.changes === 0) {
|
|
83
109
|
return null;
|
|
84
110
|
}
|
|
85
|
-
return
|
|
111
|
+
return findReviewSummaryById(db, id);
|
|
86
112
|
}
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { NotFoundError } from "../errors/http.js";
|
|
2
|
-
import {
|
|
2
|
+
import { Type } from '@sinclair/typebox';
|
|
3
|
+
import { SessionParams, PermissionParams, CreateSessionBody, AutoApproveBody, FileSearchQuery, PromptBody, PermissionBody, ConfigBody, AgentPresetInfoSchema, AgentSessionInfoSchema, AgentFileSearchResponseSchema, AgentEventsHeaders, AgentStreamSchema } from "../../shared/agents/schemas.js";
|
|
4
|
+
import { ApiErrorSchema, ERROR_RESPONSES, NoContentSchema } from "../../shared/http/schemas.js";
|
|
3
5
|
import { buildPromptBlocks } from "../services/agent/context-builder.js";
|
|
4
6
|
import { MAX_ATTACHMENT_BYTES, DEFAULT_FILE_SEARCH_LIMIT } from "../../shared/agents/constants.js";
|
|
5
7
|
import { createFileIndex } from "../services/agent/file-index.js";
|
|
@@ -8,6 +10,16 @@ import { requireLocalOrigin } from "../hooks/agents/local-origin.js";
|
|
|
8
10
|
import { findReviewById } from "../repositories/review.repo.js";
|
|
9
11
|
/** Room for a handful of max-size base64 attachments; the schema caps each one. */
|
|
10
12
|
const PROMPT_BODY_LIMIT = 5 * Math.ceil((MAX_ATTACHMENT_BYTES * 4) / 3) + 1024 * 1024;
|
|
13
|
+
// OpenAPI group of these routes.
|
|
14
|
+
const TAGS = ['agent'];
|
|
15
|
+
// The spec shows the local-origin guard on each operation, not only on the tag.
|
|
16
|
+
const AGENT_ERROR_RESPONSES = {
|
|
17
|
+
...ERROR_RESPONSES,
|
|
18
|
+
403: {
|
|
19
|
+
...ApiErrorSchema,
|
|
20
|
+
description: 'Host or Origin of the request is not local.'
|
|
21
|
+
}
|
|
22
|
+
};
|
|
11
23
|
export async function agentRoutes(app, opts) {
|
|
12
24
|
const { registry, db, repositoryPath, eventBus } = opts;
|
|
13
25
|
const typedApp = app.withTypeProvider();
|
|
@@ -17,9 +29,44 @@ export async function agentRoutes(app, opts) {
|
|
|
17
29
|
registry.closeAll();
|
|
18
30
|
fileIndex.close();
|
|
19
31
|
});
|
|
20
|
-
typedApp.get('/api/agent/presets',
|
|
21
|
-
|
|
22
|
-
|
|
32
|
+
typedApp.get('/api/agent/presets', {
|
|
33
|
+
schema: {
|
|
34
|
+
tags: TAGS,
|
|
35
|
+
summary: 'List agent presets',
|
|
36
|
+
response: {
|
|
37
|
+
200: Type.Array(AgentPresetInfoSchema, {
|
|
38
|
+
description: 'Presets from the config.'
|
|
39
|
+
}),
|
|
40
|
+
...AGENT_ERROR_RESPONSES
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}, async () => registry.listPresets());
|
|
44
|
+
typedApp.get('/api/agent/sessions', {
|
|
45
|
+
schema: {
|
|
46
|
+
tags: TAGS,
|
|
47
|
+
summary: 'List chat sessions',
|
|
48
|
+
response: {
|
|
49
|
+
200: Type.Array(AgentSessionInfoSchema, {
|
|
50
|
+
description: 'Open and closed sessions.'
|
|
51
|
+
}),
|
|
52
|
+
...AGENT_ERROR_RESPONSES
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}, async () => registry.list());
|
|
56
|
+
typedApp.post('/api/agent/sessions', {
|
|
57
|
+
schema: {
|
|
58
|
+
tags: TAGS,
|
|
59
|
+
summary: 'Start a chat session',
|
|
60
|
+
body: CreateSessionBody,
|
|
61
|
+
response: {
|
|
62
|
+
201: {
|
|
63
|
+
...AgentSessionInfoSchema,
|
|
64
|
+
description: 'The session. The agent starts in the background.'
|
|
65
|
+
},
|
|
66
|
+
...AGENT_ERROR_RESPONSES
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}, async (request, reply) => {
|
|
23
70
|
const { presetId, reviewId, name } = request.body;
|
|
24
71
|
if (reviewId !== undefined && !findReviewById(db, reviewId)) {
|
|
25
72
|
throw new NotFoundError(`Review ${reviewId} not found`);
|
|
@@ -29,21 +76,46 @@ export async function agentRoutes(app, opts) {
|
|
|
29
76
|
reply.code(201);
|
|
30
77
|
return session.info();
|
|
31
78
|
});
|
|
32
|
-
typedApp.get('/api/agent/sessions/:id', {
|
|
79
|
+
typedApp.get('/api/agent/sessions/:id', {
|
|
80
|
+
schema: {
|
|
81
|
+
tags: TAGS,
|
|
82
|
+
summary: 'Get a chat session',
|
|
83
|
+
params: SessionParams,
|
|
84
|
+
response: { 200: AgentSessionInfoSchema, ...AGENT_ERROR_RESPONSES }
|
|
85
|
+
}
|
|
86
|
+
}, async (request) => {
|
|
33
87
|
const session = registry.get(request.params.id);
|
|
34
88
|
if (!session) {
|
|
35
89
|
throw new NotFoundError('Session not found');
|
|
36
90
|
}
|
|
37
91
|
return session.info();
|
|
38
92
|
});
|
|
39
|
-
typedApp.delete('/api/agent/sessions/:id', {
|
|
93
|
+
typedApp.delete('/api/agent/sessions/:id', {
|
|
94
|
+
schema: {
|
|
95
|
+
tags: TAGS,
|
|
96
|
+
summary: 'Close a chat session and stop its agent',
|
|
97
|
+
params: SessionParams,
|
|
98
|
+
response: { 204: NoContentSchema, ...AGENT_ERROR_RESPONSES }
|
|
99
|
+
}
|
|
100
|
+
}, async (request, reply) => {
|
|
40
101
|
if (!registry.close(request.params.id)) {
|
|
41
102
|
throw new NotFoundError('Session not found');
|
|
42
103
|
}
|
|
43
|
-
return reply.code(204).send();
|
|
104
|
+
return reply.code(204).send(null);
|
|
44
105
|
});
|
|
45
106
|
typedApp.post('/api/agent/sessions/:id/prompt', {
|
|
46
|
-
schema: {
|
|
107
|
+
schema: {
|
|
108
|
+
tags: TAGS,
|
|
109
|
+
summary: 'Send a message to the agent',
|
|
110
|
+
description: 'The answer comes over GET /api/agent/events. 202 means the session took the message.',
|
|
111
|
+
params: SessionParams,
|
|
112
|
+
body: PromptBody,
|
|
113
|
+
response: {
|
|
114
|
+
// No content type: the answer has no body, as before the schema.
|
|
115
|
+
202: { description: 'The session took the message. No body.' },
|
|
116
|
+
...AGENT_ERROR_RESPONSES
|
|
117
|
+
}
|
|
118
|
+
},
|
|
47
119
|
bodyLimit: PROMPT_BODY_LIMIT
|
|
48
120
|
}, async (request, reply) => {
|
|
49
121
|
const session = registry.get(request.params.id);
|
|
@@ -61,44 +133,99 @@ export async function agentRoutes(app, opts) {
|
|
|
61
133
|
session.prompt(blocks, request.body);
|
|
62
134
|
return reply.code(202).send();
|
|
63
135
|
});
|
|
64
|
-
typedApp.post('/api/agent/sessions/:id/cancel', {
|
|
136
|
+
typedApp.post('/api/agent/sessions/:id/cancel', {
|
|
137
|
+
schema: {
|
|
138
|
+
tags: TAGS,
|
|
139
|
+
summary: 'Cancel the current turn',
|
|
140
|
+
params: SessionParams,
|
|
141
|
+
response: { 204: NoContentSchema, ...AGENT_ERROR_RESPONSES }
|
|
142
|
+
}
|
|
143
|
+
}, async (request, reply) => {
|
|
65
144
|
const session = registry.get(request.params.id);
|
|
66
145
|
if (!session) {
|
|
67
146
|
throw new NotFoundError('Session not found');
|
|
68
147
|
}
|
|
69
148
|
await session.cancel();
|
|
70
|
-
return reply.code(204).send();
|
|
149
|
+
return reply.code(204).send(null);
|
|
71
150
|
});
|
|
72
|
-
typedApp.post('/api/agent/sessions/:id/permissions/:requestId', {
|
|
151
|
+
typedApp.post('/api/agent/sessions/:id/permissions/:requestId', {
|
|
152
|
+
schema: {
|
|
153
|
+
tags: TAGS,
|
|
154
|
+
summary: 'Answer a permission request',
|
|
155
|
+
params: PermissionParams,
|
|
156
|
+
body: PermissionBody,
|
|
157
|
+
response: { 204: NoContentSchema, ...AGENT_ERROR_RESPONSES }
|
|
158
|
+
}
|
|
159
|
+
}, async (request, reply) => {
|
|
73
160
|
const session = registry.get(request.params.id);
|
|
74
161
|
if (!session?.answerPermission(request.params.requestId, request.body.optionId)) {
|
|
75
162
|
throw new NotFoundError('No such pending permission request');
|
|
76
163
|
}
|
|
77
|
-
return reply.code(204).send();
|
|
164
|
+
return reply.code(204).send(null);
|
|
78
165
|
});
|
|
79
|
-
typedApp.post('/api/agent/sessions/:id/config', {
|
|
166
|
+
typedApp.post('/api/agent/sessions/:id/config', {
|
|
167
|
+
schema: {
|
|
168
|
+
tags: TAGS,
|
|
169
|
+
summary: 'Change an agent setting',
|
|
170
|
+
params: SessionParams,
|
|
171
|
+
body: ConfigBody,
|
|
172
|
+
response: { 204: NoContentSchema, ...AGENT_ERROR_RESPONSES }
|
|
173
|
+
}
|
|
174
|
+
}, async (request, reply) => {
|
|
80
175
|
const session = registry.get(request.params.id);
|
|
81
176
|
if (!session) {
|
|
82
177
|
throw new NotFoundError('Session not found');
|
|
83
178
|
}
|
|
84
179
|
await session.setConfig(request.body.configId, request.body.value);
|
|
85
|
-
return reply.code(204).send();
|
|
180
|
+
return reply.code(204).send(null);
|
|
86
181
|
});
|
|
87
|
-
typedApp.post('/api/agent/sessions/:id/auto-approve', {
|
|
182
|
+
typedApp.post('/api/agent/sessions/:id/auto-approve', {
|
|
183
|
+
schema: {
|
|
184
|
+
tags: TAGS,
|
|
185
|
+
summary: 'Switch auto-approve of a session',
|
|
186
|
+
params: SessionParams,
|
|
187
|
+
body: AutoApproveBody,
|
|
188
|
+
response: { 204: NoContentSchema, ...AGENT_ERROR_RESPONSES }
|
|
189
|
+
}
|
|
190
|
+
}, async (request, reply) => {
|
|
88
191
|
const session = registry.get(request.params.id);
|
|
89
192
|
if (!session) {
|
|
90
193
|
throw new NotFoundError('Session not found');
|
|
91
194
|
}
|
|
92
195
|
session.setAutoApprove(request.body.enabled);
|
|
93
|
-
return reply.code(204).send();
|
|
196
|
+
return reply.code(204).send(null);
|
|
94
197
|
});
|
|
95
|
-
typedApp.get('/api/agent/files', {
|
|
198
|
+
typedApp.get('/api/agent/files', {
|
|
199
|
+
schema: {
|
|
200
|
+
tags: TAGS,
|
|
201
|
+
summary: 'Find repository files for the context picker',
|
|
202
|
+
querystring: FileSearchQuery,
|
|
203
|
+
response: {
|
|
204
|
+
200: AgentFileSearchResponseSchema,
|
|
205
|
+
...AGENT_ERROR_RESPONSES
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
}, async (request) => ({
|
|
96
209
|
files: await fileIndex.search(request.query.q ?? '', request.query.limit ?? DEFAULT_FILE_SEARCH_LIMIT)
|
|
97
210
|
}));
|
|
98
211
|
// One stream for every chat (ADR 0024): browsers allow ~6 connections per
|
|
99
212
|
// origin over HTTP/1.1, so a stream per session would starve the page's own
|
|
100
213
|
// requests. Event ids are shared across sessions, so `Last-Event-ID` works.
|
|
101
|
-
typedApp.get('/api/agent/events', {
|
|
214
|
+
typedApp.get('/api/agent/events', {
|
|
215
|
+
sse: true,
|
|
216
|
+
schema: {
|
|
217
|
+
tags: TAGS,
|
|
218
|
+
summary: 'Stream of all chat sessions',
|
|
219
|
+
headers: AgentEventsHeaders,
|
|
220
|
+
response: {
|
|
221
|
+
200: {
|
|
222
|
+
description: 'An SSE stream that stays open.',
|
|
223
|
+
content: { 'text/event-stream': { schema: AgentStreamSchema } }
|
|
224
|
+
},
|
|
225
|
+
...AGENT_ERROR_RESPONSES
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
}, async (request, reply) => {
|
|
102
229
|
const lastEventId = Number(request.headers['last-event-id']);
|
|
103
230
|
reply.sse.keepAlive();
|
|
104
231
|
// Flushes the headers: with no session yet nothing else would be sent, and
|
|
@@ -1,6 +1,13 @@
|
|
|
1
1
|
import { getAppVersion } from "../app-version.js";
|
|
2
|
+
import { AppInfoSchema } from "../../shared/app/schemas.js";
|
|
2
3
|
export async function appInfoRoutes(app) {
|
|
3
|
-
app.get('/api/app-info',
|
|
4
|
+
app.withTypeProvider().get('/api/app-info', {
|
|
5
|
+
schema: {
|
|
6
|
+
tags: ['app'],
|
|
7
|
+
summary: 'Server version',
|
|
8
|
+
response: { 200: AppInfoSchema }
|
|
9
|
+
}
|
|
10
|
+
}, async () => {
|
|
4
11
|
return { version: getAppVersion() };
|
|
5
12
|
});
|
|
6
13
|
}
|
|
@@ -2,31 +2,26 @@ import { NotFoundError } from "../errors/http.js";
|
|
|
2
2
|
import { Type } from '@sinclair/typebox';
|
|
3
3
|
import { getReview } from "../services/review.service.js";
|
|
4
4
|
import { createComment, getComments, editComment, removeComment, resolveComment, unresolveComment } from "../services/comment.service.js";
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
lineNumber: Type.Optional(Type.Union([Type.Null(), Type.Integer()])),
|
|
11
|
-
lineNumberEnd: Type.Optional(Type.Union([Type.Null(), Type.Integer()])),
|
|
12
|
-
lineType: Type.Optional(Type.Union([
|
|
13
|
-
Type.Null(),
|
|
14
|
-
Type.Literal('added'),
|
|
15
|
-
Type.Literal('removed'),
|
|
16
|
-
Type.Literal('context')
|
|
17
|
-
])),
|
|
18
|
-
content: Type.String(),
|
|
19
|
-
suggestion: Type.Optional(Type.Union([Type.Null(), Type.String()]))
|
|
20
|
-
});
|
|
21
|
-
const UpdateCommentBody = Type.Object({
|
|
22
|
-
content: Type.String()
|
|
23
|
-
});
|
|
24
|
-
const ReviewIdParams = Type.Object({ id: Type.String() });
|
|
25
|
-
const CommentIdParams = Type.Object({ id: Type.String() });
|
|
5
|
+
import { CommentIdParams, CommentSchema, CreateCommentBody, UpdateCommentBody } from "../../shared/comments/schemas.js";
|
|
6
|
+
import { ReviewIdParams } from "../../shared/reviews/schemas.js";
|
|
7
|
+
import { ERROR_RESPONSES, NoContentSchema } from "../../shared/http/schemas.js";
|
|
8
|
+
// OpenAPI group of these routes.
|
|
9
|
+
const TAGS = ['comments'];
|
|
26
10
|
export async function commentRoutes(app, opts) {
|
|
27
11
|
const { db, eventBus } = opts;
|
|
28
12
|
const typedApp = app.withTypeProvider();
|
|
29
|
-
typedApp.post('/api/reviews/:id/comments', {
|
|
13
|
+
typedApp.post('/api/reviews/:id/comments', {
|
|
14
|
+
schema: {
|
|
15
|
+
tags: TAGS,
|
|
16
|
+
summary: 'Add a comment to a review',
|
|
17
|
+
params: ReviewIdParams,
|
|
18
|
+
body: CreateCommentBody,
|
|
19
|
+
response: {
|
|
20
|
+
201: { ...CommentSchema, description: 'The new comment.' },
|
|
21
|
+
...ERROR_RESPONSES
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
}, async (request, reply) => {
|
|
30
25
|
if (!getReview(db, request.params.id)) {
|
|
31
26
|
throw new NotFoundError(`Review ${request.params.id} not found`);
|
|
32
27
|
}
|
|
@@ -34,34 +29,85 @@ export async function commentRoutes(app, opts) {
|
|
|
34
29
|
reply.code(201);
|
|
35
30
|
return comment;
|
|
36
31
|
});
|
|
37
|
-
typedApp.get('/api/reviews/:id/comments', {
|
|
32
|
+
typedApp.get('/api/reviews/:id/comments', {
|
|
33
|
+
schema: {
|
|
34
|
+
tags: TAGS,
|
|
35
|
+
summary: 'List comments of a review',
|
|
36
|
+
params: ReviewIdParams,
|
|
37
|
+
response: {
|
|
38
|
+
200: Type.Array(CommentSchema, {
|
|
39
|
+
description: 'Comments of the review.'
|
|
40
|
+
}),
|
|
41
|
+
...ERROR_RESPONSES
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}, async (request) => {
|
|
38
45
|
if (!getReview(db, request.params.id)) {
|
|
39
46
|
throw new NotFoundError(`Review ${request.params.id} not found`);
|
|
40
47
|
}
|
|
41
48
|
return getComments(db, request.params.id);
|
|
42
49
|
});
|
|
43
|
-
typedApp.patch('/api/comments/:id', {
|
|
50
|
+
typedApp.patch('/api/comments/:id', {
|
|
51
|
+
schema: {
|
|
52
|
+
tags: TAGS,
|
|
53
|
+
summary: 'Change the comment text',
|
|
54
|
+
params: CommentIdParams,
|
|
55
|
+
body: UpdateCommentBody,
|
|
56
|
+
response: {
|
|
57
|
+
200: { ...CommentSchema, description: 'The changed comment.' },
|
|
58
|
+
...ERROR_RESPONSES
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}, async (request) => {
|
|
44
62
|
const updated = editComment(db, request.params.id, request.body.content, eventBus);
|
|
45
63
|
if (!updated) {
|
|
46
64
|
throw new NotFoundError(`Comment ${request.params.id} not found`);
|
|
47
65
|
}
|
|
48
66
|
return updated;
|
|
49
67
|
});
|
|
50
|
-
typedApp.patch('/api/comments/:id/resolve', {
|
|
68
|
+
typedApp.patch('/api/comments/:id/resolve', {
|
|
69
|
+
schema: {
|
|
70
|
+
tags: TAGS,
|
|
71
|
+
summary: 'Mark a comment as resolved',
|
|
72
|
+
params: CommentIdParams,
|
|
73
|
+
response: {
|
|
74
|
+
200: { ...CommentSchema, description: 'The changed comment.' },
|
|
75
|
+
...ERROR_RESPONSES
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}, async (request) => {
|
|
51
79
|
const updated = resolveComment(db, request.params.id, eventBus);
|
|
52
80
|
if (!updated) {
|
|
53
81
|
throw new NotFoundError(`Comment ${request.params.id} not found`);
|
|
54
82
|
}
|
|
55
83
|
return updated;
|
|
56
84
|
});
|
|
57
|
-
typedApp.patch('/api/comments/:id/unresolve', {
|
|
85
|
+
typedApp.patch('/api/comments/:id/unresolve', {
|
|
86
|
+
schema: {
|
|
87
|
+
tags: TAGS,
|
|
88
|
+
summary: 'Mark a comment as not resolved',
|
|
89
|
+
params: CommentIdParams,
|
|
90
|
+
response: {
|
|
91
|
+
200: { ...CommentSchema, description: 'The changed comment.' },
|
|
92
|
+
...ERROR_RESPONSES
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}, async (request) => {
|
|
58
96
|
const updated = unresolveComment(db, request.params.id, eventBus);
|
|
59
97
|
if (!updated) {
|
|
60
98
|
throw new NotFoundError(`Comment ${request.params.id} not found`);
|
|
61
99
|
}
|
|
62
100
|
return updated;
|
|
63
101
|
});
|
|
64
|
-
typedApp.delete('/api/comments/:id', {
|
|
102
|
+
typedApp.delete('/api/comments/:id', {
|
|
103
|
+
schema: {
|
|
104
|
+
tags: TAGS,
|
|
105
|
+
summary: 'Delete a comment',
|
|
106
|
+
description: 'A missing comment also gives 204.',
|
|
107
|
+
params: CommentIdParams,
|
|
108
|
+
response: { 204: NoContentSchema, ...ERROR_RESPONSES }
|
|
109
|
+
}
|
|
110
|
+
}, async (request, reply) => {
|
|
65
111
|
removeComment(db, request.params.id, eventBus);
|
|
66
112
|
reply.code(204);
|
|
67
113
|
});
|