1import { Inject, Body, Controller, Delete, Get, Param, Patch, Post, Put, Query, Req, Res } from "@nestjs/common";
2import type { Request, Response } from "express";
3import { PageService } from "./page.service.js";
4import { ReviewService } from "./review.service.js";
5import { ZodValidate } from "../../common/pipes/zod-validation.pipe.js";
6import { requireActor } from "../../common/types.js";
7import { ReviewActionDto, type ReviewActionInput } from "./review.dto.js";
8import { ApiError } from "../../common/errors.js";
9import {
10 ApplyTemplateDto,
11 type ApplyTemplateInput,
12 BulkPagesDto,
13 type BulkPagesInput,
14 CreatePageDto,
15 type CreatePageInput,
16 ConvertSelectionDto,
17 type ConvertSelectionInput,
18 CompareVersionsQuery,
19 type CompareVersionsInput,
20 CreateTemplateDto,
21 type CreateTemplateInput,
22 DuplicatePageDto,
23 type DuplicatePageInput,
24 ListPagesQuery,
25 type ListPagesInput,
26 MovePageDto,
27 type MovePageInput,
28 UpdatePageDto,
29 type UpdatePageInput
30} from "./dto.js";
31
32@Controller()
33export class PagesController {
34 constructor(
35 @Inject(PageService) private readonly pages: PageService,
36 @Inject(ReviewService) private readonly reviews: ReviewService
37 ) {}
38
39
40
41 @Get("workspaces/:workspaceId/pages")
42 list(
43 @Param("workspaceId") workspaceId: string,
44 @Query(ZodValidate(ListPagesQuery)) query: ListPagesInput,
45 @Req() req: Request
46 ) {
47 return this.pages.list(workspaceId, requireActor(req), query);
48 }
49
50 @Post("workspaces/:workspaceId/pages")
51 create(
52 @Param("workspaceId") workspaceId: string,
53 @Body(ZodValidate(CreatePageDto)) body: CreatePageInput,
54 @Req() req: Request
55 ) {
56
57
58 return this.pages.create(workspaceId, requireActor(req), { ...body, parentId: body.parentId ?? undefined }, req);
59 }
60
61
62
63 @Get("pages/:id")
64 get(@Param("id") id: string, @Req() req: Request) {
65 return this.pages.get(id, requireActor(req));
66 }
67
68
69
70
77 @Get("pages/:id/export/markdown")
78 async exportMarkdown(
79 @Param("id") id: string,
80 @Query("includeChildren") includeChildren: string | undefined,
81 @Req() req: Request,
82 @Res() res: Response
83 ) {
84 const { markdown, filename } = await this.pages.exportMarkdown(
85 id,
86 requireActor(req),
87 { includeChildren: includeChildren === "1" || includeChildren === "true" },
88 req
89 );
90 res.setHeader("Content-Type", "text/markdown; charset=utf-8");
91 res.setHeader("Content-Disposition", `attachment; filename="${filename}"`);
92 res.send(markdown);
93 }
94
95
96
97 @Get("pages/:id/review")
98 reviewState(@Param("id") id: string, @Req() req: Request) {
99 return this.reviews.reviewState(id, requireActor(req));
100 }
101
102 @Post("pages/:id/review")
103 reviewAct(
104 @Param("id") id: string,
105 @Body(ZodValidate(ReviewActionDto)) body: ReviewActionInput,
106 @Req() req: Request
107 ) {
108 return this.reviews.act(id, requireActor(req), body, req);
109 }
110
111 @Patch("pages/:id")
112 update(
113 @Param("id") id: string,
114 @Body(ZodValidate(UpdatePageDto)) body: UpdatePageInput,
115 @Req() req: Request
116 ) {
117 return this.pages.update(id, requireActor(req), body, req);
118 }
119
120
121
122
123
124
125
126
127
128 @Get("pages/:id/whiteboard")
129 getCanvas(@Param("id") id: string, @Req() req: Request) {
130 return this.pages.getCanvas(id, requireActor(req));
131 }
132
133 @Put("pages/:id/whiteboard")
134 saveCanvas(
135 @Param("id") id: string,
136 @Body() canvas: unknown,
137 @Req() req: Request
138 ) {
139 return this.pages.saveCanvas(id, requireActor(req), canvas, req);
140 }
141
142
143 @Post("pages/:id/whiteboard/convert")
144 convertSelection(
145 @Param("id") id: string,
146 @Body(ZodValidate(ConvertSelectionDto)) body: ConvertSelectionInput,
147 @Req() req: Request
148 ) {
149 return this.pages.convertSelection(id, requireActor(req), body, req);
150 }
151
152 @Delete("pages/:id")
153 trash(@Param("id") id: string, @Req() req: Request) {
154 return this.pages.trash(id, requireActor(req), req);
155 }
156
157 @Delete("pages/:id/permanent")
158 deletePermanent(@Param("id") id: string, @Req() req: Request) {
159 return this.pages.deletePermanent(id, requireActor(req), req).then(() => ({ deleted: true }));
160 }
161
162 @Post("pages/:id/restore")
163 restore(@Param("id") id: string, @Req() req: Request) {
164 return this.pages.restore(id, requireActor(req), req);
165 }
166
167
168
169 @Post("pages/:id/duplicate")
170 duplicate(
171 @Param("id") id: string,
172 @Body(ZodValidate(DuplicatePageDto)) body: DuplicatePageInput,
173 @Req() req: Request
174 ) {
175 return this.pages.duplicate(id, requireActor(req), { parentId: body.parentId ?? undefined, title: body.title }, req);
176 }
177
178
179
180 @Post("pages/bulk")
181 bulk(
182 @Body(ZodValidate(BulkPagesDto)) body: BulkPagesInput,
183 @Req() req: Request
184 ) {
185 const workspaceId = req.headers["x-factory-workspace"];
186 if (typeof workspaceId !== "string" || !workspaceId) {
187 throw ApiError.validation("workspaceId is required (X-Factory-Workspace header).", [
188 { field: "workspaceId", issue: "required" }
189 ]);
190 }
191 return this.pages.bulk(workspaceId, requireActor(req), body, req);
192 }
193
194
195
196 @Get("pages/:id/backlinks")
197 backlinks(@Param("id") id: string, @Req() req: Request) {
198 return this.pages.backlinks(id, requireActor(req));
199 }
200
201
202
203 @Post("pages/:id/move")
204 move(
205 @Param("id") id: string,
206 @Body(ZodValidate(MovePageDto)) body: MovePageInput,
207 @Req() req: Request
208 ) {
209 return this.pages.moveCrossWorkspace(id, requireActor(req), { toWorkspaceId: body.toWorkspaceId, toParentId: body.toParentId ?? undefined }, req);
210 }
211
212
213
214 @Get("pages/:id/versions")
215 versions(@Param("id") id: string, @Req() req: Request) {
216 return this.pages.listVersions(id, requireActor(req));
217 }
218
219
220
221 @Get("pages/:id/versions/compare")
222 compareVersions(
223 @Param("id") id: string,
224 @Query(ZodValidate(CompareVersionsQuery)) query: CompareVersionsInput,
225 @Req() req: Request
226 ) {
227 return this.pages.compareVersions(id, query.from, query.to, requireActor(req));
228 }
229
230 @Post("pages/:id/versions")
231 createVersion(@Param("id") id: string, @Body() body: { note?: string } | undefined, @Req() req: Request) {
232 return this.pages.createVersion(id, requireActor(req), body?.note);
233 }
234
235
236 @Get("pages/:id/versions/:versionId")
237 getVersionPreview(@Param("id") id: string, @Param("versionId") versionId: string, @Req() req: Request) {
238 return this.pages.getVersionPreview(id, versionId, requireActor(req));
239 }
240
241 @Post("pages/:id/versions/:versionId/restore")
242 restoreVersion(
243 @Param("id") id: string,
244 @Param("versionId") versionId: string,
245 @Req() req: Request
246 ) {
247 return this.pages.restoreVersion(id, versionId, requireActor(req), req);
248 }
249
250
251
252 @Get("pages/:id/watch")
253 isWatching(@Param("id") id: string, @Req() req: Request) {
254 return this.pages.isWatching(id, requireActor(req));
255 }
256
257 @Post("pages/:id/watch")
258 watch(@Param("id") id: string, @Req() req: Request) {
259 return this.pages.watch(id, requireActor(req));
260 }
261
262 @Delete("pages/:id/watch")
263 unwatch(@Param("id") id: string, @Req() req: Request) {
264 return this.pages.unwatch(id, requireActor(req));
265 }
266
267
268
269 @Get("pages/:id/favorite")
270 favorite(@Param("id") id: string, @Req() req: Request) {
271 return this.pages.isFavorite(id, requireActor(req));
272 }
273
274 @Post("pages/:id/favorite")
275 addFavorite(@Param("id") id: string, @Req() req: Request) {
276 return this.pages.favorite(id, requireActor(req));
277 }
278
279 @Delete("pages/:id/favorite")
280 removeFavorite(@Param("id") id: string, @Req() req: Request) {
281 return this.pages.unfavorite(id, requireActor(req));
282 }
283
284
285
286 @Post("pages/:id/templates")
287 createTemplate(
288 @Param("id") id: string,
289 @Body(ZodValidate(CreateTemplateDto)) body: CreateTemplateInput,
290 @Req() req: Request
291 ) {
292 return this.pages.createTemplate(id, requireActor(req), body, req);
293 }
294
295 @Get("workspaces/:workspaceId/templates")
296 templates(
297 @Param("workspaceId") workspaceId: string,
298 @Query("type") type: string | undefined,
299 @Query("category") category: string | undefined,
300 @Query("search") search: string | undefined,
301 @Req() req: Request
302 ) {
303
304 return this.pages.listTemplates(workspaceId, requireActor(req), type, { category, search });
305 }
306
307 @Post("templates/:id/apply")
308 applyTemplate(
309 @Param("id") id: string,
310 @Body(ZodValidate(ApplyTemplateDto)) body: ApplyTemplateInput,
311 @Req() req: Request
312 ) {
313 const workspaceId = body.workspaceId ?? req.headers["x-factory-workspace"];
314 if (typeof workspaceId !== "string" || !workspaceId) {
315 throw ApiError.validation("workspaceId is required (body or X-Factory-Workspace header).", [
316 { field: "workspaceId", issue: "required" }
317 ]);
318 }
319
320
321 return this.pages.applyTemplate(workspaceId, requireActor(req), id, { parentId: body.parentId ?? undefined, title: body.title, values: body.values }, req);
322 }
323
324
328 @Get("templates/:id/preview")
329 previewTemplate(@Param("id") id: string, @Req() req: Request) {
330 return this.pages.previewTemplate(id, requireActor(req));
331 }
332
333
334
335 @Post("templates/:id/versions")
336 createTemplateVersion(
337 @Param("id") id: string,
338 @Body() body: { note?: string } | undefined,
339 @Req() req: Request
340 ) {
341 const workspaceId = req.headers["x-factory-workspace"];
342 if (typeof workspaceId !== "string" || !workspaceId) {
343 throw ApiError.validation("workspaceId is required (X-Factory-Workspace header).", [{ field: "workspaceId", issue: "required" }]);
344 }
345 return this.pages.createTemplateVersion(id, workspaceId, requireActor(req), body?.note, req);
346 }
347
348 @Get("templates/:id/versions")
349 listTemplateVersions(@Param("id") id: string, @Req() req: Request) {
350 const workspaceId = req.headers["x-factory-workspace"];
351 if (typeof workspaceId !== "string" || !workspaceId) {
352 throw ApiError.validation("workspaceId is required (X-Factory-Workspace header).", [{ field: "workspaceId", issue: "required" }]);
353 }
354 return this.pages.listTemplateVersions(id, workspaceId, requireActor(req));
355 }
356
357 @Post("templates/:id/versions/:versionId/restore")
358 restoreTemplateVersion(
359 @Param("id") id: string,
360 @Param("versionId") versionId: string,
361 @Req() req: Request
362 ) {
363 const workspaceId = req.headers["x-factory-workspace"];
364 if (typeof workspaceId !== "string" || !workspaceId) {
365 throw ApiError.validation("workspaceId is required (X-Factory-Workspace header).", [{ field: "workspaceId", issue: "required" }]);
366 }
367 return this.pages.restoreTemplateVersion(id, versionId, workspaceId, requireActor(req), req);
368 }
369}
370