react-embed-docs 0.1.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.
- package/LICENSE +21 -0
- package/README.md +422 -0
- package/dist/client/components/Breadcrumbs.d.ts +21 -0
- package/dist/client/components/Breadcrumbs.d.ts.map +1 -0
- package/dist/client/components/Breadcrumbs.js +123 -0
- package/dist/client/components/DocsLayout.d.ts +20 -0
- package/dist/client/components/DocsLayout.d.ts.map +1 -0
- package/dist/client/components/DocsLayout.js +387 -0
- package/dist/client/components/DocumentContent.d.ts +5 -0
- package/dist/client/components/DocumentContent.d.ts.map +1 -0
- package/dist/client/components/DocumentContent.js +15 -0
- package/dist/client/components/DocumentEdit.d.ts +6 -0
- package/dist/client/components/DocumentEdit.d.ts.map +1 -0
- package/dist/client/components/DocumentEdit.js +153 -0
- package/dist/client/components/DocumentList.d.ts +5 -0
- package/dist/client/components/DocumentList.d.ts.map +1 -0
- package/dist/client/components/DocumentList.js +39 -0
- package/dist/client/components/DocumentProvider.d.ts +42 -0
- package/dist/client/components/DocumentProvider.d.ts.map +1 -0
- package/dist/client/components/DocumentProvider.js +47 -0
- package/dist/client/components/DocumentView.d.ts +6 -0
- package/dist/client/components/DocumentView.d.ts.map +1 -0
- package/dist/client/components/DocumentView.js +58 -0
- package/dist/client/components/DragOverlayItem.d.ts +5 -0
- package/dist/client/components/DragOverlayItem.d.ts.map +1 -0
- package/dist/client/components/DragOverlayItem.js +9 -0
- package/dist/client/components/EmojiPicker.d.ts +8 -0
- package/dist/client/components/EmojiPicker.d.ts.map +1 -0
- package/dist/client/components/EmojiPicker.js +48 -0
- package/dist/client/components/ExportButton.d.ts +22 -0
- package/dist/client/components/ExportButton.d.ts.map +1 -0
- package/dist/client/components/ExportButton.js +97 -0
- package/dist/client/components/Layout.d.ts +7 -0
- package/dist/client/components/Layout.d.ts.map +1 -0
- package/dist/client/components/Layout.js +172 -0
- package/dist/client/components/ReactEmbedDocs.d.ts +8 -0
- package/dist/client/components/ReactEmbedDocs.d.ts.map +1 -0
- package/dist/client/components/ReactEmbedDocs.js +8 -0
- package/dist/client/components/SearchInput.d.ts +2 -0
- package/dist/client/components/SearchInput.d.ts.map +1 -0
- package/dist/client/components/SearchInput.js +7 -0
- package/dist/client/components/Sidebar.d.ts +10 -0
- package/dist/client/components/Sidebar.d.ts.map +1 -0
- package/dist/client/components/Sidebar.js +176 -0
- package/dist/client/components/SortableTreeItem.d.ts +13 -0
- package/dist/client/components/SortableTreeItem.d.ts.map +1 -0
- package/dist/client/components/SortableTreeItem.js +24 -0
- package/dist/client/components/VersionHistory.d.ts +14 -0
- package/dist/client/components/VersionHistory.d.ts.map +1 -0
- package/dist/client/components/VersionHistory.js +102 -0
- package/dist/client/hooks/useCollaboration.d.ts +99 -0
- package/dist/client/hooks/useCollaboration.d.ts.map +1 -0
- package/dist/client/hooks/useCollaboration.js +180 -0
- package/dist/client/hooks/useDocsQuery.d.ts +84 -0
- package/dist/client/hooks/useDocsQuery.d.ts.map +1 -0
- package/dist/client/hooks/useDocsQuery.js +241 -0
- package/dist/client/hooks/useExport.d.ts +31 -0
- package/dist/client/hooks/useExport.d.ts.map +1 -0
- package/dist/client/hooks/useExport.js +66 -0
- package/dist/client/hooks/useFileUpload.d.ts +44 -0
- package/dist/client/hooks/useFileUpload.d.ts.map +1 -0
- package/dist/client/hooks/useFileUpload.js +193 -0
- package/dist/client/hooks/useSystemTheme.d.ts +2 -0
- package/dist/client/hooks/useSystemTheme.d.ts.map +1 -0
- package/dist/client/hooks/useSystemTheme.js +19 -0
- package/dist/client/hooks/useVersions.d.ts +105 -0
- package/dist/client/hooks/useVersions.d.ts.map +1 -0
- package/dist/client/hooks/useVersions.js +129 -0
- package/dist/client/index.d.ts +23 -0
- package/dist/client/index.d.ts.map +1 -0
- package/dist/client/index.js +18 -0
- package/dist/client/lib/blocknoteTheme.d.ts +13 -0
- package/dist/client/lib/blocknoteTheme.d.ts.map +1 -0
- package/dist/client/lib/blocknoteTheme.js +76 -0
- package/dist/client/lib/path.d.ts +8 -0
- package/dist/client/lib/path.d.ts.map +1 -0
- package/dist/client/lib/path.js +30 -0
- package/dist/client/providers/DocumentProvider.d.ts +1 -0
- package/dist/client/providers/DocumentProvider.d.ts.map +1 -0
- package/dist/client/providers/DocumentProvider.js +1 -0
- package/dist/server/CollaborationService.d.ts +134 -0
- package/dist/server/CollaborationService.d.ts.map +1 -0
- package/dist/server/CollaborationService.js +307 -0
- package/dist/server/DocsService.d.ts +115 -0
- package/dist/server/DocsService.d.ts.map +1 -0
- package/dist/server/DocsService.js +512 -0
- package/dist/server/ExportService.d.ts +106 -0
- package/dist/server/ExportService.d.ts.map +1 -0
- package/dist/server/ExportService.js +501 -0
- package/dist/server/FilesService.d.ts +44 -0
- package/dist/server/FilesService.d.ts.map +1 -0
- package/dist/server/FilesService.js +78 -0
- package/dist/server/VersioningService.d.ts +112 -0
- package/dist/server/VersioningService.d.ts.map +1 -0
- package/dist/server/VersioningService.js +264 -0
- package/dist/server/db.d.ts +7 -0
- package/dist/server/db.d.ts.map +1 -0
- package/dist/server/db.js +22 -0
- package/dist/server/index.d.ts +55 -0
- package/dist/server/index.d.ts.map +1 -0
- package/dist/server/index.js +36 -0
- package/dist/server/routes.d.ts +9 -0
- package/dist/server/routes.d.ts.map +1 -0
- package/dist/server/routes.js +483 -0
- package/dist/server/schema.d.ts +587 -0
- package/dist/server/schema.d.ts.map +1 -0
- package/dist/server/schema.js +126 -0
- package/dist/shared/types.d.ts +314 -0
- package/dist/shared/types.d.ts.map +1 -0
- package/dist/shared/types.js +48 -0
- package/drizzle/migrations/0000_gray_monster_badoon.sql +88 -0
- package/drizzle/migrations/meta/0000_snapshot.json +574 -0
- package/drizzle/migrations/meta/_journal.json +13 -0
- package/package.json +109 -0
- package/styles/docs.css +981 -0
|
@@ -0,0 +1,483 @@
|
|
|
1
|
+
import { Hono } from 'hono';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import { createDocumentSchema, updateDocumentSchema, uploadFileSchema } from '../shared/types.js';
|
|
4
|
+
import { CollaborationService } from './CollaborationService.js';
|
|
5
|
+
import { ExportService } from './ExportService.js';
|
|
6
|
+
import { VersioningService } from './VersioningService.js';
|
|
7
|
+
/**
|
|
8
|
+
* Document routes
|
|
9
|
+
* All routes are prefixed with /docs
|
|
10
|
+
*/
|
|
11
|
+
export function createRouter(docsService, filesService, db) {
|
|
12
|
+
const router = new Hono();
|
|
13
|
+
const exportService = new ExportService();
|
|
14
|
+
const collaborationService = new CollaborationService(db);
|
|
15
|
+
const versioningService = new VersioningService(db);
|
|
16
|
+
/**
|
|
17
|
+
* GET /docs
|
|
18
|
+
* List all documents with optional filtering
|
|
19
|
+
*/
|
|
20
|
+
router.get('/', async (c) => {
|
|
21
|
+
const query = c.req.query();
|
|
22
|
+
const filters = {
|
|
23
|
+
search: query.search,
|
|
24
|
+
parentId: query.parentId === 'null' ? null : query.parentId,
|
|
25
|
+
isPublished: query.isPublished === 'true' ? true : query.isPublished === 'false' ? false : undefined,
|
|
26
|
+
};
|
|
27
|
+
console.log('filters', filters);
|
|
28
|
+
const options = {
|
|
29
|
+
offset: query.offset ? parseInt(query.offset, 10) : 0,
|
|
30
|
+
limit: query.limit ? parseInt(query.limit, 10) : 10,
|
|
31
|
+
sortBy: query.sortBy,
|
|
32
|
+
sortDir: query.sortDir,
|
|
33
|
+
};
|
|
34
|
+
const { documents, total } = await docsService.list(filters, options);
|
|
35
|
+
console.log('documents', documents);
|
|
36
|
+
return c.json({ items: documents, total });
|
|
37
|
+
});
|
|
38
|
+
/**
|
|
39
|
+
* GET /docs/search
|
|
40
|
+
* Search documents using text search
|
|
41
|
+
*/
|
|
42
|
+
router.get('/search', async (c) => {
|
|
43
|
+
const query = c.req.query();
|
|
44
|
+
const searchQuery = query.q || query.query || '';
|
|
45
|
+
if (!searchQuery) {
|
|
46
|
+
return c.json({ results: [], total: 0 });
|
|
47
|
+
}
|
|
48
|
+
const options = {
|
|
49
|
+
limit: query.limit ? parseInt(query.limit, 10) : 20,
|
|
50
|
+
offset: query.offset ? parseInt(query.offset, 10) : 0,
|
|
51
|
+
};
|
|
52
|
+
const { results, total } = await docsService.searchText(searchQuery, options);
|
|
53
|
+
return c.json({ results, total });
|
|
54
|
+
});
|
|
55
|
+
/**
|
|
56
|
+
* GET /docs/tree
|
|
57
|
+
* Get all documents in tree structure
|
|
58
|
+
*/
|
|
59
|
+
router.get('/tree', async (c) => {
|
|
60
|
+
const documents = await docsService.getTree();
|
|
61
|
+
return c.json({ items: documents });
|
|
62
|
+
});
|
|
63
|
+
/**
|
|
64
|
+
* GET /docs/:id
|
|
65
|
+
* Get a single document by ID or slug
|
|
66
|
+
* Tries ID first, then falls back to slug lookup
|
|
67
|
+
*/
|
|
68
|
+
router.get('/:id', async (c) => {
|
|
69
|
+
const id = c.req.param('id');
|
|
70
|
+
// Try to find by ID first
|
|
71
|
+
let doc = await docsService.getById(id);
|
|
72
|
+
// If not found by ID, try by slug
|
|
73
|
+
if (!doc) {
|
|
74
|
+
doc = await docsService.getBySlug(id);
|
|
75
|
+
}
|
|
76
|
+
if (!doc) {
|
|
77
|
+
return c.json({ message: 'Document not found' }, 404);
|
|
78
|
+
}
|
|
79
|
+
return c.json(doc);
|
|
80
|
+
});
|
|
81
|
+
/**
|
|
82
|
+
* GET /docs/slug/:slug
|
|
83
|
+
* Get a single document by slug
|
|
84
|
+
*/
|
|
85
|
+
router.get('/slug/:slug', async (c) => {
|
|
86
|
+
const slug = c.req.param('slug');
|
|
87
|
+
const doc = await docsService.getBySlug(slug);
|
|
88
|
+
if (!doc) {
|
|
89
|
+
return c.json({ message: 'Document not found' }, 404);
|
|
90
|
+
}
|
|
91
|
+
return c.json(doc);
|
|
92
|
+
});
|
|
93
|
+
/**
|
|
94
|
+
* POST /docs
|
|
95
|
+
* Create a new document
|
|
96
|
+
*/
|
|
97
|
+
router.post('/', async (c) => {
|
|
98
|
+
const body = await c.req.json();
|
|
99
|
+
// Validate input
|
|
100
|
+
const result = createDocumentSchema.safeParse(body);
|
|
101
|
+
if (!result.success) {
|
|
102
|
+
return c.json({ message: 'Invalid input', errors: result.error.errors }, 400);
|
|
103
|
+
}
|
|
104
|
+
try {
|
|
105
|
+
const doc = await docsService.create(result.data);
|
|
106
|
+
return c.json(doc, 201);
|
|
107
|
+
}
|
|
108
|
+
catch (error) {
|
|
109
|
+
return c.json({ message: 'Failed to create document', error: error.message }, 500);
|
|
110
|
+
}
|
|
111
|
+
});
|
|
112
|
+
/**
|
|
113
|
+
* PUT /docs/:id
|
|
114
|
+
* Update an existing document
|
|
115
|
+
*/
|
|
116
|
+
router.put('/:id', async (c) => {
|
|
117
|
+
const id = c.req.param('id');
|
|
118
|
+
const body = await c.req.json();
|
|
119
|
+
// Validate input
|
|
120
|
+
const result = updateDocumentSchema.safeParse(body);
|
|
121
|
+
if (!result.success) {
|
|
122
|
+
return c.json({ message: 'Invalid input', errors: result.error.errors }, 400);
|
|
123
|
+
}
|
|
124
|
+
const doc = await docsService.update(id, result.data);
|
|
125
|
+
if (!doc) {
|
|
126
|
+
return c.json({ message: 'Document not found' }, 404);
|
|
127
|
+
}
|
|
128
|
+
return c.json(doc);
|
|
129
|
+
});
|
|
130
|
+
/**
|
|
131
|
+
* DELETE /docs/:id
|
|
132
|
+
* Soft delete a document
|
|
133
|
+
*/
|
|
134
|
+
router.delete('/:id', async (c) => {
|
|
135
|
+
const id = c.req.param('id');
|
|
136
|
+
const deleted = await docsService.delete(id);
|
|
137
|
+
if (!deleted) {
|
|
138
|
+
return c.json({ message: 'Document not found' }, 404);
|
|
139
|
+
}
|
|
140
|
+
return c.body(null, 204);
|
|
141
|
+
});
|
|
142
|
+
/**
|
|
143
|
+
* GET /docs/:id/children
|
|
144
|
+
* Get children of a document
|
|
145
|
+
*/
|
|
146
|
+
router.get('/:id/children', async (c) => {
|
|
147
|
+
const id = c.req.param('id');
|
|
148
|
+
const children = await docsService.getChildren(id);
|
|
149
|
+
return c.json({ items: children });
|
|
150
|
+
});
|
|
151
|
+
/**
|
|
152
|
+
* GET /docs/:id/breadcrumbs
|
|
153
|
+
* Get breadcrumb path for a document (from root to document)
|
|
154
|
+
*/
|
|
155
|
+
router.get('/:id/breadcrumbs', async (c) => {
|
|
156
|
+
const id = c.req.param('id');
|
|
157
|
+
const breadcrumbs = await docsService.getBreadcrumbs(id);
|
|
158
|
+
return c.json({ items: breadcrumbs });
|
|
159
|
+
});
|
|
160
|
+
/**
|
|
161
|
+
* POST /docs/:id/reorder
|
|
162
|
+
* Reorder a document within its parent or move to new parent
|
|
163
|
+
*/
|
|
164
|
+
router.post('/:id/reorder', async (c) => {
|
|
165
|
+
const id = c.req.param('id');
|
|
166
|
+
const body = await c.req.json();
|
|
167
|
+
const reorderSchema = z.object({
|
|
168
|
+
parentId: z.string().nullable().optional(),
|
|
169
|
+
order: z.number(),
|
|
170
|
+
});
|
|
171
|
+
const result = reorderSchema.safeParse(body);
|
|
172
|
+
if (!result.success) {
|
|
173
|
+
return c.json({ message: 'Invalid input', errors: result.error.errors }, 400);
|
|
174
|
+
}
|
|
175
|
+
const { parentId, order } = result.data;
|
|
176
|
+
const doc = await docsService.reorder(id, parentId ?? null, order);
|
|
177
|
+
if (!doc) {
|
|
178
|
+
return c.json({ message: 'Document not found' }, 404);
|
|
179
|
+
}
|
|
180
|
+
return c.json(doc);
|
|
181
|
+
});
|
|
182
|
+
/**
|
|
183
|
+
* PATCH /docs/:id/order
|
|
184
|
+
* Update just the order field
|
|
185
|
+
*/
|
|
186
|
+
router.patch('/:id/order', async (c) => {
|
|
187
|
+
const id = c.req.param('id');
|
|
188
|
+
const body = await c.req.json();
|
|
189
|
+
const orderSchema = z.object({
|
|
190
|
+
order: z.number(),
|
|
191
|
+
});
|
|
192
|
+
const result = orderSchema.safeParse(body);
|
|
193
|
+
if (!result.success) {
|
|
194
|
+
return c.json({ message: 'Invalid input', errors: result.error.errors }, 400);
|
|
195
|
+
}
|
|
196
|
+
const doc = await docsService.updateOrder(id, result.data.order);
|
|
197
|
+
if (!doc) {
|
|
198
|
+
return c.json({ message: 'Document not found' }, 404);
|
|
199
|
+
}
|
|
200
|
+
return c.json(doc);
|
|
201
|
+
});
|
|
202
|
+
/**
|
|
203
|
+
* POST /files
|
|
204
|
+
* Upload a file
|
|
205
|
+
*/
|
|
206
|
+
router.post('/files', async (c) => {
|
|
207
|
+
console.log('uploading files');
|
|
208
|
+
const body = await c.req.json();
|
|
209
|
+
console.log('body', body);
|
|
210
|
+
// Validate input
|
|
211
|
+
const result = uploadFileSchema.safeParse(body);
|
|
212
|
+
if (!result.success) {
|
|
213
|
+
return c.json({ message: 'Invalid input', errors: result.error.errors }, 400);
|
|
214
|
+
}
|
|
215
|
+
const { filename, mimeType, content } = result.data;
|
|
216
|
+
// Validate base64 content
|
|
217
|
+
try {
|
|
218
|
+
// Check if content is empty
|
|
219
|
+
if (!content || content.length === 0) {
|
|
220
|
+
return c.json({ message: 'Empty file content' }, 400);
|
|
221
|
+
}
|
|
222
|
+
const decodedSize = Math.ceil(content.length * 0.75); // Approximate decoded size
|
|
223
|
+
const maxSize = 5 * 1024 * 1024; // 5MB limit
|
|
224
|
+
if (decodedSize > maxSize) {
|
|
225
|
+
return c.json({ message: 'File too large (max 5MB)' }, 413);
|
|
226
|
+
}
|
|
227
|
+
// Clean up base64 string - remove any whitespace or newlines
|
|
228
|
+
const cleanedContent = content.replace(/\s/g, '');
|
|
229
|
+
// Verify it is valid base64
|
|
230
|
+
try {
|
|
231
|
+
Buffer.from(cleanedContent, 'base64');
|
|
232
|
+
}
|
|
233
|
+
catch (e) {
|
|
234
|
+
return c.json({ message: 'Invalid base64 encoding' }, 400);
|
|
235
|
+
}
|
|
236
|
+
const file = await filesService.upload({
|
|
237
|
+
filename,
|
|
238
|
+
mimeType,
|
|
239
|
+
size: decodedSize,
|
|
240
|
+
content: cleanedContent,
|
|
241
|
+
});
|
|
242
|
+
return c.json({
|
|
243
|
+
id: file.id,
|
|
244
|
+
filename: file.filename,
|
|
245
|
+
mimeType: file.mimeType,
|
|
246
|
+
size: file.size,
|
|
247
|
+
url: `/api/docs/files/${file.id}`,
|
|
248
|
+
}, 201);
|
|
249
|
+
}
|
|
250
|
+
catch (error) {
|
|
251
|
+
console.error('File upload error:', error);
|
|
252
|
+
return c.json({ message: 'Failed to upload file' }, 500);
|
|
253
|
+
}
|
|
254
|
+
});
|
|
255
|
+
/**
|
|
256
|
+
* GET /files/:id
|
|
257
|
+
* Get a file by ID (returns binary content)
|
|
258
|
+
*/
|
|
259
|
+
router.get('/files/:id', async (c) => {
|
|
260
|
+
const id = c.req.param('id');
|
|
261
|
+
const file = await filesService.getById(id);
|
|
262
|
+
if (!file) {
|
|
263
|
+
return c.json({ message: 'File not found' }, 404);
|
|
264
|
+
}
|
|
265
|
+
// Decode base64 and return binary
|
|
266
|
+
const buffer = Buffer.from(file.content, 'base64');
|
|
267
|
+
c.header('Content-Type', file.mimeType);
|
|
268
|
+
c.header('Content-Disposition', `inline; filename="${file.filename}"`);
|
|
269
|
+
return c.body(buffer);
|
|
270
|
+
});
|
|
271
|
+
/**
|
|
272
|
+
* GET /files/:id/meta
|
|
273
|
+
* Get file metadata without content
|
|
274
|
+
*/
|
|
275
|
+
router.get('/files/:id/meta', async (c) => {
|
|
276
|
+
const id = c.req.param('id');
|
|
277
|
+
const file = await filesService.getById(id);
|
|
278
|
+
if (!file) {
|
|
279
|
+
return c.json({ message: 'File not found' }, 404);
|
|
280
|
+
}
|
|
281
|
+
return c.json({
|
|
282
|
+
id: file.id,
|
|
283
|
+
filename: file.filename,
|
|
284
|
+
mimeType: file.mimeType,
|
|
285
|
+
size: file.size,
|
|
286
|
+
createdAt: file.createdAt,
|
|
287
|
+
});
|
|
288
|
+
});
|
|
289
|
+
/**
|
|
290
|
+
* DELETE /files/:id
|
|
291
|
+
* Delete a file
|
|
292
|
+
*/
|
|
293
|
+
router.delete('/files/:id', async (c) => {
|
|
294
|
+
const id = c.req.param('id');
|
|
295
|
+
try {
|
|
296
|
+
await filesService.delete(id);
|
|
297
|
+
return c.body(null, 204);
|
|
298
|
+
}
|
|
299
|
+
catch (error) {
|
|
300
|
+
return c.json({ message: 'File not found' }, 404);
|
|
301
|
+
}
|
|
302
|
+
});
|
|
303
|
+
/**
|
|
304
|
+
* GET /docs/:id/export
|
|
305
|
+
* Export a document to various formats (docx, pdf, html, md)
|
|
306
|
+
*/
|
|
307
|
+
router.get('/:id/export', async (c) => {
|
|
308
|
+
const id = c.req.param('id');
|
|
309
|
+
const format = c.req.query('format') || 'docx';
|
|
310
|
+
// Try ID first, then slug
|
|
311
|
+
let doc = await docsService.getById(id);
|
|
312
|
+
if (!doc) {
|
|
313
|
+
doc = await docsService.getBySlug(id);
|
|
314
|
+
}
|
|
315
|
+
if (!doc) {
|
|
316
|
+
return c.json({ message: 'Document not found' }, 404);
|
|
317
|
+
}
|
|
318
|
+
try {
|
|
319
|
+
let result;
|
|
320
|
+
switch (format.toLowerCase()) {
|
|
321
|
+
case 'docx':
|
|
322
|
+
result = await exportService.exportToDocx(doc);
|
|
323
|
+
break;
|
|
324
|
+
case 'pdf':
|
|
325
|
+
result = await exportService.exportToPdf(doc);
|
|
326
|
+
break;
|
|
327
|
+
case 'html':
|
|
328
|
+
result = await exportService.exportToHtml(doc);
|
|
329
|
+
break;
|
|
330
|
+
case 'md':
|
|
331
|
+
case 'markdown':
|
|
332
|
+
result = await exportService.exportToMarkdown(doc);
|
|
333
|
+
break;
|
|
334
|
+
default:
|
|
335
|
+
return c.json({ message: `Unsupported format: ${format}` }, 400);
|
|
336
|
+
}
|
|
337
|
+
c.header('Content-Type', result.mimeType);
|
|
338
|
+
c.header('Content-Disposition', `attachment; filename="${result.filename}"`);
|
|
339
|
+
// Convert Node Buffer to Uint8Array for proper binary response
|
|
340
|
+
const uint8Array = new Uint8Array(result.buffer);
|
|
341
|
+
return c.body(uint8Array);
|
|
342
|
+
}
|
|
343
|
+
catch (error) {
|
|
344
|
+
console.error('Export error:', error);
|
|
345
|
+
return c.json({
|
|
346
|
+
message: 'Export failed',
|
|
347
|
+
error: error.message
|
|
348
|
+
}, 500);
|
|
349
|
+
}
|
|
350
|
+
});
|
|
351
|
+
/**
|
|
352
|
+
* ============================================================================
|
|
353
|
+
* VERSIONING ROUTES
|
|
354
|
+
* ============================================================================
|
|
355
|
+
*/
|
|
356
|
+
/**
|
|
357
|
+
* GET /docs/:id/versions
|
|
358
|
+
* List all versions of a document
|
|
359
|
+
*/
|
|
360
|
+
router.get('/:id/versions', async (c) => {
|
|
361
|
+
const id = c.req.param('id');
|
|
362
|
+
const doc = await docsService.getById(id);
|
|
363
|
+
if (!doc) {
|
|
364
|
+
return c.json({ message: 'Document not found' }, 404);
|
|
365
|
+
}
|
|
366
|
+
const versions = await versioningService.listVersions(id);
|
|
367
|
+
return c.json({ items: versions, total: versions.length });
|
|
368
|
+
});
|
|
369
|
+
/**
|
|
370
|
+
* POST /docs/:id/versions
|
|
371
|
+
* Create a new version manually
|
|
372
|
+
*/
|
|
373
|
+
router.post('/:id/versions', async (c) => {
|
|
374
|
+
const id = c.req.param('id');
|
|
375
|
+
const body = await c.req.json().catch(() => ({}));
|
|
376
|
+
const doc = await docsService.getById(id);
|
|
377
|
+
if (!doc) {
|
|
378
|
+
return c.json({ message: 'Document not found' }, 404);
|
|
379
|
+
}
|
|
380
|
+
try {
|
|
381
|
+
const version = await versioningService.createVersion(id, {
|
|
382
|
+
description: body.description,
|
|
383
|
+
createdBy: body.createdBy,
|
|
384
|
+
createdByName: body.createdByName,
|
|
385
|
+
});
|
|
386
|
+
return c.json(version, 201);
|
|
387
|
+
}
|
|
388
|
+
catch (error) {
|
|
389
|
+
return c.json({ message: 'Failed to create version' }, 500);
|
|
390
|
+
}
|
|
391
|
+
});
|
|
392
|
+
/**
|
|
393
|
+
* GET /docs/versions/:versionId
|
|
394
|
+
* Get a specific version
|
|
395
|
+
*/
|
|
396
|
+
router.get('/versions/:versionId', async (c) => {
|
|
397
|
+
const versionId = c.req.param('versionId');
|
|
398
|
+
const version = await versioningService.getVersion(versionId);
|
|
399
|
+
if (!version) {
|
|
400
|
+
return c.json({ message: 'Version not found' }, 404);
|
|
401
|
+
}
|
|
402
|
+
return c.json(version);
|
|
403
|
+
});
|
|
404
|
+
/**
|
|
405
|
+
* POST /docs/:id/restore/:versionId
|
|
406
|
+
* Restore document to a specific version
|
|
407
|
+
*/
|
|
408
|
+
router.post('/:id/restore/:versionId', async (c) => {
|
|
409
|
+
const id = c.req.param('id');
|
|
410
|
+
const versionId = c.req.param('versionId');
|
|
411
|
+
const body = await c.req.json().catch(() => ({}));
|
|
412
|
+
try {
|
|
413
|
+
const result = await versioningService.restoreVersion(id, versionId, {
|
|
414
|
+
restoredBy: body.restoredBy,
|
|
415
|
+
restoredByName: body.restoredByName,
|
|
416
|
+
});
|
|
417
|
+
return c.json(result);
|
|
418
|
+
}
|
|
419
|
+
catch (error) {
|
|
420
|
+
return c.json({ message: error.message }, 400);
|
|
421
|
+
}
|
|
422
|
+
});
|
|
423
|
+
/**
|
|
424
|
+
* GET /docs/:id/compare
|
|
425
|
+
* Compare two versions
|
|
426
|
+
*/
|
|
427
|
+
router.get('/:id/compare', async (c) => {
|
|
428
|
+
const id = c.req.param('id');
|
|
429
|
+
const v1 = c.req.query('v1');
|
|
430
|
+
const v2 = c.req.query('v2');
|
|
431
|
+
if (!v1 || !v2) {
|
|
432
|
+
return c.json({ message: 'Missing version IDs (v1 and v2 required)' }, 400);
|
|
433
|
+
}
|
|
434
|
+
try {
|
|
435
|
+
const diff = await versioningService.compareVersions(v1, v2);
|
|
436
|
+
return c.json(diff);
|
|
437
|
+
}
|
|
438
|
+
catch (error) {
|
|
439
|
+
return c.json({ message: error.message }, 400);
|
|
440
|
+
}
|
|
441
|
+
});
|
|
442
|
+
/**
|
|
443
|
+
* DELETE /docs/versions/:versionId
|
|
444
|
+
* Delete a version
|
|
445
|
+
*/
|
|
446
|
+
router.delete('/versions/:versionId', async (c) => {
|
|
447
|
+
const versionId = c.req.param('versionId');
|
|
448
|
+
try {
|
|
449
|
+
await versioningService.deleteVersion(versionId);
|
|
450
|
+
return c.body(null, 204);
|
|
451
|
+
}
|
|
452
|
+
catch (error) {
|
|
453
|
+
return c.json({ message: 'Failed to delete version' }, 500);
|
|
454
|
+
}
|
|
455
|
+
});
|
|
456
|
+
/**
|
|
457
|
+
* ============================================================================
|
|
458
|
+
* COLLABORATION ROUTES
|
|
459
|
+
* ============================================================================
|
|
460
|
+
*/
|
|
461
|
+
/**
|
|
462
|
+
* GET /docs/:id/collaborators
|
|
463
|
+
* Get active collaborators for a document
|
|
464
|
+
*/
|
|
465
|
+
router.get('/:id/collaborators', async (c) => {
|
|
466
|
+
const id = c.req.param('id');
|
|
467
|
+
const doc = await docsService.getById(id);
|
|
468
|
+
if (!doc) {
|
|
469
|
+
return c.json({ message: 'Document not found' }, 404);
|
|
470
|
+
}
|
|
471
|
+
const users = await collaborationService.getActiveUsers(id);
|
|
472
|
+
return c.json({ items: users });
|
|
473
|
+
});
|
|
474
|
+
/**
|
|
475
|
+
* POST /docs/:id/cleanup-presence
|
|
476
|
+
* Clean up stale presence records (admin/maintenance)
|
|
477
|
+
*/
|
|
478
|
+
router.post('/cleanup-presence', async (c) => {
|
|
479
|
+
const count = await collaborationService.cleanupStalePresence();
|
|
480
|
+
return c.json({ removed: count });
|
|
481
|
+
});
|
|
482
|
+
return router;
|
|
483
|
+
}
|